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_appauthThe 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:
- require an access token and ID token;
- require the returned token type to be
Bearer; - validate the ID token’s signature, exact issuer, client ID audience, expiry and nonce through the platform OIDC boundary;
- derive the product key from the exact
(iss, sub)pair; - write the access token, refresh token and expiry to platform secure storage as one session update.
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.
| Outcome | App behavior |
|---|---|
| user closes the browser | remain signed out and offer login again |
callback has access_denied | discard the attempt and show that authorization was cancelled |
| callback state or issuer is invalid | discard the attempt and report a failed security check |
token endpoint returns invalid_grant | discard the attempt and restart authorization |
| no refresh token is returned | keep a short session and authorize again after access-token expiry |
| API rejects audience or scope | clear 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_grantclears 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.