Skip to content

Mobile & Native App Redirect URIs

Mobile and native apps are public clients — like SPAs, they cannot store a client_secret safely, since the app binary can be extracted and inspected. AuthAction authenticates them the same way it authenticates SPAs: the Authorization Code Flow with PKCE. The part that’s different for mobile is the redirect_uri, since there’s no web origin to redirect back to — this guide covers what to use.

There are two supported patterns, in increasing order of security:

1. Custom URL scheme (works with no extra platform setup)

Section titled “1. Custom URL scheme (works with no extra platform setup)”

The most common approach, and the one that works immediately without any native entitlements. Use your app’s reverse-DNS bundle identifier as the scheme — not a plain word — and embed your domain in the path:

{your-bundle-id}://{your-domain}/mobile/callback

Example:

com.yourcompany.app://auth.yourcompany.com/mobile/callback

Why reverse-DNS and not just yourapp://callback: app store bundle identifiers are unique per app, so basing the scheme on it reduces the chance of an unrelated app accidentally registering the same scheme. This is a widely used convention among mobile SDKs. Note this is a convention that reduces accidental collisions — see the security note below for what it does and doesn’t protect against.

Section titled “2. Claimed HTTPS URI (Universal Links / App Links)”

The strongest option: an https:// redirect that only your app can receive, verified by the OS via a hosted association file (apple-app-site-association on iOS, assetlinks.json on Android) plus the associatedDomains/App Links entitlement. No other app — malicious or accidental — can register as a handler for a verified domain, so this closes the gap that custom schemes leave open.

https://{your-domain}/mobile/callback

This requires more setup (a paid Apple Developer account for the Associated Domains entitlement, hosting the association files on your domain), so it’s common to ship with a custom scheme first and move to this before a public app store release.

Security Note: What PKCE Does and Doesn’t Protect

Section titled “Security Note: What PKCE Does and Doesn’t Protect”

PKCE (mandatory for all AuthAction Authorization Code flows) fully protects against an attacker intercepting a code meant for your app and exchanging it for tokens — without your app’s code_verifier, AuthAction’s token endpoint rejects the exchange (see PKCE Authorization Flow).

It does not protect against a different attack: a malicious app that also registers your custom scheme can run its own, self-contained authorization flow — with its own valid code_verifier/code_challenge pair — and if the user already has an active session with your tenant’s login page, that flow can complete silently, with no visible login screen. If the OS hands the resulting redirect to the malicious app instead of yours (scheme registration is not exclusive — the OS’s dispatch when two apps claim the same scheme is undefined), the attacker already holds the matching verifier and can complete the token exchange. This isn’t a code-interception attack, so PKCE has nothing to say about it.

This is only closed by a claimed HTTPS redirect (Universal Links/App Links) — there’s no redirect-URI formatting trick that fixes it on a custom scheme. If your app handles sensitive data or you’re preparing for a public release, plan to move to option 2 above.

Add your app’s redirect URI to the application’s Allowed Callback URLs in the same field used for SPA applications (see Applications Configuration). Matching is:

  • Exact match, trailing-slash tolerant (.../callback and .../callback/ are treated the same).
  • Wildcard hostnames (*.example.com) are supported for non-production tenants only, to keep dev/staging registration convenient — production tenants must register exact URLs.

The redirect_uri sent in both the /oauth2/authorize request and the /oauth2/token exchange must match this registered value exactly.

Using expo-auth-session, PKCE is handled automatically:

import * as AuthSession from "expo-auth-session";
const redirectUri = "com.yourcompany.app://auth.yourcompany.com/mobile/callback";
const discovery = {
authorizationEndpoint: "https://{tenant-name}.{region}.authaction.com/oauth2/authorize",
tokenEndpoint: "https://{tenant-name}.{region}.authaction.com/oauth2/token",
};
const [request, , promptAsync] = AuthSession.useAuthRequest(
{
clientId: YOUR_CLIENT_ID,
redirectUri,
scopes: ["openid", "profile", "email"],
usePKCE: true, // generates code_verifier/code_challenge automatically
},
discovery
);
// After promptAsync() resolves with an authorization code:
const tokenResponse = await AuthSession.exchangeCodeAsync(
{
clientId: YOUR_CLIENT_ID,
code: result.params.code,
redirectUri,
extraParams: { code_verifier: request.codeVerifier },
},
discovery
);

On web builds of the same app, let expo-auth-session derive the redirect from the page’s own origin instead of the custom scheme — a custom scheme has no meaning in a browser:

const redirectUri =
Platform.OS === "web"
? AuthSession.makeRedirectUri({ path: "callback" })
: "com.yourcompany.app://auth.yourcompany.com/mobile/callback";