Skip to Content
TutorialsMobile login with AppAuth

Sign in from a mobile app with AppAuth

Hosted login is the shortest native integration. AppAuth opens Identity in the system browser, restores the app from its registered callback and exchanges the authorization code with PKCE. Identity owns every interactive branch: registration, password, email verification, upstream providers, MFA and consent.

Use the native registration and native password with MFA tutorials only when the product must render credential screens itself. Both approaches end at the same OAuth token endpoint.

Before you start

Register a public native client with:

{ "clientId": "acme-mobile", "displayName": "Acme mobile", "applicationType": "NATIVE", "tokenEndpointAuthMethod": "NONE", "consentPolicy": "EXPLICIT", "redirectUris": ["com.acme.mobile:/oauth/callback"], "scopes": ["openid", "email", "offline_access", "orders.read"], "defaultResource": "https://api.example.com", "nativeAuthenticationEnabled": false }

Hosted login does not require nativeAuthenticationEnabled. That flag enables the separate native credential API.

Configure the exact callback in Android and iOS according to the AppAuth wrapper’s platform setup. Prefer an app-claimed HTTPS redirect when the platform and deployment can verify ownership; otherwise use a reverse-domain custom scheme that another installed app is unlikely to claim.

Never ship a client secret in a native application.

1. Add AppAuth

For Flutter:

flutter pub add flutter_appauth

The same protocol is available through AppAuth for Android and AppAuth for iOS when the application is not built with Flutter.

2. Authorize and exchange the code

import 'package:flutter_appauth/flutter_appauth.dart'; const issuer = 'https://auth.example.com'; const clientId = 'acme-mobile'; const redirectUri = 'com.acme.mobile:/oauth/callback'; const scopes = <String>[ 'openid', 'email', 'offline_access', 'orders.read', ]; const appAuth = FlutterAppAuth(); final result = await appAuth.authorizeAndExchangeCode( AuthorizationTokenRequest( clientId, redirectUri, issuer: issuer, scopes: scopes, additionalParameters: const { 'resource': 'https://api.example.com', }, ), ); if (result.accessToken == null || result.idToken == null) { throw StateError('Identity returned an incomplete token response'); }

AppAuth discovers the tenant endpoints, generates state, nonce and PKCE material, opens the external user agent, validates the callback and performs the code exchange. Do not replace the system browser with an embedded web view.

allowInsecureConnections is for a controlled local environment only. A production issuer and callback use HTTPS or the platform’s registered application scheme as applicable.

3. Commit the session only after validation

Before publishing an authenticated application state:

  1. require an access token and ID token;
  2. require the returned token type to be Bearer;
  3. validate the ID token’s signature, exact issuer, client ID audience, expiry and nonce through the platform OIDC boundary;
  4. derive the product key from the exact (iss, sub) pair;
  5. write the access token, refresh token and expiry to platform secure storage as one session update.
Security boundary

Keychain and Android Keystore-backed storage protect persisted credentials. Plain preferences, SQLite records, logs and analytics events are not token stores.

Create product state only after the product API accepts the access token. A verified email does not authorize linking the new subject to an existing product account.

4. Refresh with rotation

final refreshed = await appAuth.token( TokenRequest( clientId, redirectUri, issuer: issuer, refreshToken: currentRefreshToken, scopes: scopes, ), ); if (refreshed.accessToken == null) { throw StateError('Identity returned no refreshed access token'); }

Serialize refresh for one local session. When Identity returns a replacement refresh token, persist the new access token, refresh token and expiry atomically. Never retry with the previous refresh token after the replacement has been committed.

On invalid_grant, delete the local token set and start a fresh hosted authorization. A second login can reuse the Identity browser SSO session according to tenant policy.

5. Handle browser outcomes

Treat user cancellation as an ordinary anonymous outcome. Do not create a partial session or report successful login.

OutcomeApp behavior
user closes the browserremain signed out and offer login again
callback has access_denieddiscard the attempt and show that authorization was cancelled
callback state or issuer is invaliddiscard the attempt and report a failed security check
token endpoint returns invalid_grantdiscard the attempt and restart authorization
no refresh token is returnedkeep a short session and authorize again after access-token expiry
API rejects audience or scopeclear product authentication and fix client/resource registration

Registration, password recovery and MFA failures are rendered by the hosted Identity experience. They do not create an application callback until authorization succeeds or returns an OAuth error.

6. Sign out of the application

Revoke the refresh token at the discovered revocation_endpoint, then revoke the access token when one remains, and clear secure storage even if the network call fails. The public client sends its client_id and no secret.

Identity does not currently advertise an OIDC end_session_endpoint, so do not call FlutterAppAuth.endSession for this issuer. Application logout deliberately leaves the browser SSO session intact.

Verify the finished use case

  • login opens the platform system browser rather than an embedded web view;
  • the callback returns only to the exact registered application route;
  • cancelling the browser creates no local session;
  • registration, MFA and consent complete inside the hosted flow;
  • tokens are committed only after callback and ID-token validation;
  • one concurrent refresh updates the rotating token set atomically;
  • invalid_grant clears the local session and starts a new authorization;
  • application logout revokes its grant and leaves the shared browser SSO session separate.

Use OAuth and OpenID Connect responses for the endpoint contract and native applications to compare hosted and native credential UX.