Native app login flow for Internet Identity

Hi Internet Identity team,
and hi everyone in the community interested in Internet Identity and native client integration.

I’m exploring native app login support for Internet Identity and opened two draft PRs in my fork before preparing an upstream proposal.

The current PRs are in my own fork, but if the design direction looks acceptable, my intent is to reduce this into an upstream-ready change set and send it to the DFINITY Internet Identity repository. Before doing that, I would really appreciate early design feedback, especially from the II team / maintainers.

The goal is to let desktop and mobile apps start authentication in the system browser and receive the result through a registered redirect URI, without requiring a browser bridge.

This is not intended to turn Internet Identity into a generic OIDC provider or to introduce a generic JWT/userinfo-based identity layer. The goal is narrower: use an OIDC/OAuth-compatible authorization request shape where it fits, while keeping IC delegation retrieval as an Internet Computer-specific extension.

Draft PRs:

PR #5 is the initial prototype. PR #6 is stacked on top of it and is the direction I currently think is cleaner, because native apps can open the discovered authorization endpoint directly instead of depending on a custom prepare-style API.

Background

Today, Internet Identity is mainly optimized for browser-based dapp login.

For native desktop/mobile clients, I would like to support a flow where:

  1. the native app discovers Internet Identity endpoints from /.well-known/openid-configuration

  2. the app opens the system browser

  3. the user authenticates with Internet Identity

  4. Internet Identity redirects back to the native app

  5. the native app exchanges the authorization result for an IC delegation

The design uses standard OIDC/OAuth concepts where they fit, but keeps IC-specific delegation retrieval separate from generic OIDC token issuance.

PR #5: native app redirect login flow

PR #5 adds the first version of the native app redirect flow.

Main points:

  • adds native authorization preparation, completion, token exchange, and delegation exchange support

  • exposes authorization_endpoint, token_endpoint, and ic_delegation_endpoint in discovery metadata

  • supports RFC 8252-style loopback redirects, allowing desktop apps to use an ephemeral local port

  • adds /oauth2/token and /oauth2/delegation

  • keeps /oauth2/delegation separate because the signed delegation response needs query-call certificate context

  • adds frontend helper APIs, generated bindings, integration tests, frontend tests, and docs

In this version, native apps first call a prepare-style API before opening the browser.

PR #6: OIDC-style authorization request variant

PR #6 is stacked on top of PR #5 and changes the browser entrypoint to be closer to a standard OIDC authorization-code request.

Instead of exposing a public prepare_native_authorization flow to native apps, native apps open the discovered authorization_endpoint directly with parameters such as:

  • response_type=code

  • client_id

  • redirect_uri

  • scope

  • state

  • nonce

  • code_challenge

  • code_challenge_method=S256

It also adds IC-specific extension parameters:

  • ic_origin

  • ic_session_public_key

  • optional ic_max_time_to_live

Native apps still use the same two-step delegation retrieval flow:

  1. /oauth2/token

  2. /oauth2/delegation

My current feeling is that PR #6 may be the cleaner direction because it reduces the custom native-app preparation contract and makes the authorization request easier to understand for clients familiar with OIDC/OAuth.

Security notes / intended invariants

The intended security model is:

  • native clients are treated as public clients; no shared client secret is assumed

  • authorization code flow uses PKCE, with S256 required

  • authorization codes are short-lived and single-use

  • token exchange is bound to the original request, including client_id, redirect_uri, code_verifier, and the IC-specific authorization parameters

  • redirect URI matching is exact, except for the RFC 8252 loopback-port exception

  • loopback redirects should use loopback IP literals such as 127.0.0.1 or [::1]

  • state and nonce remain client-managed anti-CSRF / replay protections

  • the final IC delegation remains bounded by II’s existing delegation lifetime rules

I would especially appreciate review of whether these are the right invariants for this flow, and whether there are any II-specific security concerns I am missing.

Feedback requested

I’m mainly looking for early design feedback before reducing this into an upstream-ready change set.

In particular:

  1. Should PR #6’s OIDC-style authorization request be the preferred direction over the prepare-style flow in PR #5?

  2. Is exposing ic_delegation_endpoint as an IC-specific discovery metadata extension acceptable?

  3. Is the two-step /oauth2/token/oauth2/delegation flow the right shape, given the certificate/query-call context needed for signed delegation retrieval?

  4. Are the IC-specific authorization parameters named and scoped appropriately?
    Examples: ic_origin, ic_session_public_key, ic_max_time_to_live

  5. Does RFC 8252-style loopback redirect handling make sense for II native clients?

  6. Are there security concerns around redirect URI validation, PKCE/code binding, replay prevention, token reuse, or delegation retrieval?

  7. Would maintainers prefer this as one PR, or split into smaller PRs?

Thanks. I would appreciate early design feedback, especially from the DFINITY II team / maintainers, on whether the PR #6 direction is an appropriate shape for upstream before I reduce this into an upstream-ready change set.

This would be very cool. I love building native apps and being able to provide a login mechanism like this would supercharge them

This flow should also benefit desktop apps and developer tools such as CLIs. In particular, RFC 8252-style loopback redirects allow macOS, Windows, Linux, and command-line clients to start authentication in the system browser and receive the authorization result locally.

Quick update: main has moved quite a bit since I started this branch, so I’ll sync/rebase on the latest main before continuing.

I also made a small interaction diagram to make the proposed native app flow easier to discuss.

Would this allow a scenario where a dApp running at https://x.y.com where you normally login in the browser at that URL, now can also provide a CLI running local that can login via browser popup, and you get the same principal/account?

Would enable running CLI based AI workflows

I believe this implementation will make it possible. However, there might be a reason why redirects haven’t been supported for a long time. I just hope I haven’t overlooked anything.

this is sth. that will be possible with icp-cli going forward (cc @raymondk)

Thanks for digging into this, but I don’t think the framing or the chosen primitives fit II.

Native app login isn’t actually a gap. The pattern has been documented for a while:
Security best practices: Identity and access management | Internet Computer. It uses a web-hosted bridge with an intermediate session key, App Links / Universal Links, and URI-fragment delivery. As mentioned by @marc0olo icp-cli uses the same concepts on desktop.

The OAuth/OIDC pieces in the PR are redundant given ic_session_public_key:

  • id_token / JWKS: a generic OIDC RP that consumes the JWT still can’t make canister calls without IC-specific code, so the OIDC compatibility is mostly decorative.
  • PKCE: a one-shot proof at code exchange. Session-key binding is a continuous signature proof on every IC call after, which strictly subsumes it.
  • Bearer on /oauth2/delegation: puts a credential on the public HTTP gateway path, which is exactly what envelope-signed calls were designed to avoid. Safe here only because the delegation is already pubkey-locked, but the pattern contradicts the IC’s call model.

The IC-native primitives for this are App Links / Universal Links (cryptographic domain binding) and intermediate session keys, both already in the documented pattern. With ~5,800 LOC parallel to ICRC-25/29/34, plus static client_id registration (a meaningful permission-model shift since any web origin can currently call II without registration), this would be a second canister-level auth surface to support indefinitely, in service of a problem that doesn’t need solving in the canister.

The one thing the bridge approach doesn’t give you is a fully seamless flow, since opening the II tab from the bridge requires a user gesture. If we want that, the right venue is in wg-identity-authentication, where “Browser URL Transport” is already listed as IDEA. A draft proposal there would be welcome. Once we have a proper standard with the security details nailed down, implementing it in II isn’t a heavy lift.

That said, fair point that this isn’t obvious. The pattern is tucked away in the security section of the legacy docs and didn’t get carried over to the new docs site, so it’s reasonable to land on this looking like an unsolved problem. We should pull it forward into more visible native-integration docs so people don’t have to rediscover it from scratch.

Yes, that’s exactly the use case the documented bridge pattern covers. If the CLI’s login flow goes through a bridge page hosted under x.y.com’s origin, the CLI ends up with the same principal as the browser session at x.y.com.

One thing to be aware of: the bridge can’t actually distinguish your legitimate CLI from a malicious binary on the same machine. Both look like “some local listener on http://127.0.0.1:<port>”. If a malicious CLI launches the browser with x.y.com’s bridge URL and its own loopback redirect, the user could approve thinking it’s the legitimate flow, and the malicious binary walks away with a delegation for their x.y.com identity.

App Links / Universal Links close this gap for OS-registered apps on mobile and macOS (Windows has App URI Handlers as an equivalent): the OS verifies the binary’s identity against the domain via signed associations, so only the registered app receives the redirect. Plain CLI binaries on Linux or generic desktops don’t have that, so the practical defenses are user trust at install time and OS process isolation.

So yes, this works today. If you go beyond a CLI to a proper native app, lean on App Links / Universal Links for the binding rather than loopback alone.

Thanks, that helps clarify the intended bridge pattern. I understand that this is the currently documented approach, and that it can cover some cases, especially when App Links / Universal Links are available. My concern is that it does not feel like a good foundation for native app and CLI integrations in general.

I agree that IC delegation will always remain IC-specific, and that an id_token alone is not enough to make canister calls. That said, I think there is an important developer-adoption angle to OAuth/OIDC compatibility. The value is not that II becomes a generic OIDC provider, but that native apps can reuse existing, well-tested libraries and SDKs for discovery, system-browser login, redirect handling, state/nonce checks, PKCE, and code exchange. For example: AppAuth on iOS/macOS/Android, react-native-app-auth, openid-client for JavaScript/Node.js, openidconnect/openid for Rust, coreos/go-oidc for Go, and Authlib for Python. The IC-specific part can then stay much smaller: ic_session_public_key, ic_origin, and delegation retrieval.

This is the part where I think the standard shape matters. A rough analogy: on EVM, a fungible token that is not ERC-20 may be technically valid, but most wallets, indexers, and developer tools will not support it by default. Similarly, an IC-native auth flow can be technically valid without OAuth/OIDC compatibility, but it is much harder for developers to reuse the existing native auth tooling ecosystem.

I also want to emphasize that my concern with the documented bridge pattern is not just theoretical. I implemented that approach, and the DX/UX was quite painful. Every native integration needs to build and host a custom web bridge mainly to adapt II’s browser channel/postMessage flow back into the native app.

This is especially problematic on iOS/macOS. The browser surfaces native apps actually use — Safari, SFSafariViewController, ASWebAuthenticationSession, etc. — are not surfaces where the native app can directly and reliably participate in a webpage’s postMessage channel. ASWebAuthenticationSession is built around opening an authentication page and returning a callback URL to the app. That is much closer to the OAuth/OIDC native-app model than to II’s current browser-channel model.

WKWebView might allow a custom JS bridge, but that is exactly the kind of custom integration burden I’m trying to avoid, and it also goes against the usual OAuth/OIDC native-app best practice of using an external/system browser.

Maybe before deciding that OIDC compatibility is mostly decorative, we should validate this with native app / CLI developers. I’d be interested in a small survey asking whether they would prefer the documented bridge pattern, an IC-specific Browser URL Transport, or an OAuth/OIDC-compatible native authorization profile with IC delegation extensions.

Personally, I’m not necessarily attached to OAuth/OIDC as the only possible solution. If Browser URL Transport can solve this cleanly while preserving the IC-native model, I would be very happy with that direction too. My main concern is avoiding a world where every native integration has to build its own fragile bridge around the current postMessage-based browser flow.

Hi folks - we have a “hidden” feature in icp-cli starting 0.2.4 that we’re testing out.

Follow this to install icp-cli - Installation | ICP CLI

Essentially, you can do:

# Check you have a version of icp-cli that supports it
icp identity link ii --help

# Create a new local identity linked to II
# It gets a signed delegation for your II identity at x.y.com
icp identity link ii <local-id-name> [--domain <x.y.com>]

# when the delegation expires you can refresh it with
icp identity login <local-id-name>

The domain you’re trying to login to needs to have:

  • /.well-known/ic-cli-login - containing the path of the login page on x.y.com
  • It needs to receive the public key and port number of the local http server to post back the delegation

https://cli.id.ai/ already supports this, so you can try it out, this will take you through the login flow and default to that domain.

icp identity link ii <name>

The libraries are still under review but we’re expecting to release them soon.
The mechanism is pretty straight forward and can be extended to other native tools not only the cli.

I’ve already used it to extend an existing app so I could login and execute canister calls with the same identity from the cli.

Keep in mind that the authentication methods used in II (Passkeys and OpenID) are tied to the id.ai origin, rendering the II page inside something like a native WebView will break passkey discovery and is detected and blocked by e.g. Google sign-in.

If I remember correctly both iOS (ASWebAuthenticationSession) and Android (Chrome Custom Tabs) have APIs available for loading a sign-in page with the actual default browser while keeping the user experience within the app itself. Those should be compatible with a redirect flow, either directly or indirectly through a web-hosted bridge.

Most OIDC related checks are there due to the lack of signatures, e.g. authentication goes through bearer tokens which can be intercepted, replayed etc. Delegations have plenty of details that can be improved, but are not subject to these risks, making checks like state/nonce, PKCE etc irrelevant as mentioned earlier.

This should be the quickest step forward towards a world where apps no longer have to build bridges around PostMessage. Any help with writing out a transport standard in this regard would be highly appreciated.

Though keep in mind that one way or another, a native app needs to identify itself as a given domain. With OAuth this is indirect, you identify as a given client_id but there’s still the allowed callback URL list tied to that client.

With OAuth, the OpenID provider keeps track of clients and their configuration (including callback URL). In the case of II, we try to avoid such a permission model. First thing that comes to mind would be simply hosting something like a ./well-known/ii-callback-urls file on the domain that serves as an alternative to II itself keeping track of callback URLs.

Ideally with App Links / Universal links, custom URL schemes and loopback URL approaches can be avoided (more secure) but that mostly only applies to mobile development unfortunately.

This is an amazing feature!
I would definitely love to try it out.

It would be even more helpful if users didn’t have to manage their local identities themselves. Is the library planning to support something like that as well?

You don’t need to “manage” the local identity with this. The actual name that you create for it locally doesn’t matter and the private key that gets created doesn’t matter because if you lose it, you can just get another delegation.

The actual identity that you end up effectively using is the one that II derives for you for that domain. Does that make sense?

The library I’m talking about is a very simple library that anyone can add on a website they own the domain for and it makes it easy to construct the delegation after a user logs into ii.

You don’t need to “manage” the local identity with this. The actual name that you create for it locally doesn’t matter and the private key that gets created doesn’t matter because if you lose it, you can just get another delegation.

That sounds excellent. Thank you for the clarification.