Skip to content

Authorization code with PKCE — for native apps

For iOS, Android, and desktop apps, the OAuth flow is almost the same as the web variant — same /oauth2/authorize, same PKCE, same /oauth2/token. The difference is how the user reaches the sign-in page and how they come back. RFC 8252 ("OAuth 2.0 for Native Apps") sets the rules; the platforms each implement them slightly differently.

A native app could open the IntelliAuth sign-in page in a WebView (iOS), WebView (Android), or WebView2 (Windows). Don't.

Three reasons:

  1. The user cannot see the URL. Phishing-resistance depends on the user recognising the IntelliAuth domain. A webview hides it.
  2. The webview can read everything the user types. Including their password. Even if your app does not exploit this, app-store reviewers will refuse you, and so will the platform's WebAuthn implementation.
  3. No session sharing. A user who already signed into IntelliAuth in their system browser has to sign in again inside your app's webview. Friction with no upside.

Use the platform's system browser surface instead:

  • iOS / iPadOSASWebAuthenticationSession. Looks like an in-app sheet; runs in the system Safari sandbox. Shares cookies with Safari.
  • Android — Chrome Custom Tabs (or the equivalent on other Chromium browsers). Shares cookies with the user's installed Chrome.
  • macOSASWebAuthenticationSession (same as iOS).
  • Windows / Linux — open the URL in the default browser; receive the redirect via a local loopback HTTP server.

The IntelliAuth SDKs do this for you when they ship for these platforms. While @intelliauth/react-native-sdk is still shipping (see the SDK roadmap), the manual flow below is the contract you target.

Native app PKCE — sign-in lives in the trustworthy system browser, the token exchange lives in the app.

Two patterns, choose by platform:

Section titled “Custom URI scheme (recommended for mobile)”

Register a unique scheme — com.cymmetri.banking://callback — in your app's manifest. When the system browser hits that URL, the OS routes the redirect back to your app. The system browser closes automatically.

iOS: declare the scheme in Info.plist (CFBundleURLTypes).

Android: declare an <intent-filter> on the activity with android:scheme="com.cymmetri.banking".

Pitfall: the scheme must be unique. myapp:// is hijack-prone — any other app registering the same scheme could intercept the redirect. Reverse-DNS-style schemes (com.companyname.appname) are the convention.

Open a local listener on http://127.0.0.1:<random-port>/callback, then send the user to the auth URL with that as redirect_uri. The browser redirects back to your localhost, where your app reads the authorization code.

This is the right pattern for CLI tools and desktop apps that cannot register a system URI scheme.

What the redirect URI must look like in the application config

Section titled “What the redirect URI must look like in the application config”

Register every redirect URI you intend to use. Examples:

  • com.cymmetri.banking://callback — iOS / Android.
  • http://127.0.0.1/callback — desktop. (Port is wildcarded for loopback per RFC 8252 §7.3.)

The application's settings page in the tenant admin console accepts both.

For native apps PKCE is required, not optional. There is no client secret to substitute for it; the platform refuses the token exchange if you omit code_verifier.

Once the SDK has the refresh token, store it in the platform's secure storage:

  • iOS: Keychain (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly).
  • Android: EncryptedSharedPreferences or Keystore-backed storage.
  • Desktop: OS keyring (Windows Credential Manager, macOS Keychain, libsecret on Linux).

Never write a refresh token to plain shared preferences or NSUserDefaults. Refresh-token rotation (see the dedicated topic) reduces the blast radius if one leaks, but secure storage is still the baseline.

Calling /oauth2/revoke with the refresh token is the right server-side cleanup. To clear the browser session as well (so a future sign-in does not skip the password step), redirect the user through the platform's logout endpoint and then close the browser tab / sheet.