Mobile and desktop apps
Sign users in from iOS, Android, React Native, Flutter and desktop apps with the system browser and PKCE.
Mobile and desktop apps are public clients: they can't keep a secret, so they use the authorization code flow with PKCE, and they show the sign-in page in the system browser (ASWebAuthenticationSession on iOS, Custom Tabs on Android), never in an embedded web view. That keeps passwords out of your app, lets password managers fill them in, and gives single sign-on with the browser.
#Set up the application
Applications → Create application → Native / Mobile, then add the redirect URI your app listens on to Allowed callback URLs:
| Platform | Redirect URI |
|---|---|
| iOS / Android with a custom scheme | com.acme.app:/callback (reverse-domain scheme) |
| Universal / App Links | https://app.acme.com/callback |
| Desktop app with a loopback server | http://127.0.0.1:8765/callback (matched exactly, port included: listen on a fixed port) |
#iOS and Android: AppAuth
AppAuth handles discovery, PKCE and the system browser.
let issuer = URL(string: "https://acme.9in.io")!
OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { config, _ in
let request = OIDAuthorizationRequest(
configuration: config!,
clientId: "app_…",
scopes: ["openid", "profile", "email", "offline_access"],
redirectURL: URL(string: "com.acme.app:/callback")!,
responseType: OIDResponseTypeCode,
additionalParameters: nil)
appDelegate.currentFlow = OIDAuthState.authState(byPresenting: request, presenting: self) { state, error in
// state?.lastTokenResponse?.accessToken, state?.refreshToken
}
}On Android use net.openid:appauth the same way: AuthorizationServiceConfiguration.fetchFromIssuer, then an AuthorizationRequest with ResponseTypeValues.CODE.
#React Native and Expo
import * as AuthSession from "expo-auth-session";
const discovery = AuthSession.useAutoDiscovery("https://acme.9in.io");
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: "app_…",
redirectUri: AuthSession.makeRedirectUri({ scheme: "com.acme.app", path: "callback" }),
scopes: ["openid", "profile", "email", "offline_access"],
usePKCE: true,
},
discovery,
);
// then AuthSession.exchangeCodeAsync({ code, clientId, redirectUri, extraParams: { code_verifier: request.codeVerifier } }, discovery)react-native-app-auth works too, with issuer, clientId, redirectUrl and scopes.
#Flutter
flutter_appauth: authorizeAndExchangeCode(AuthorizationTokenRequest(clientId, redirectUrl, issuer: 'https://acme.9in.io', scopes: [...])).
#Keep tokens safe
- Store refresh tokens in the Keychain (iOS) or EncryptedSharedPreferences / Keystore (Android).
- Refresh tokens rotate: save the new one after every refresh.
- To sign out, revoke the refresh token (
POST /oauth/revoke) and open<issuer>/oauth/logout?id_token_hint=…&post_logout_redirect_uri=…in the system browser to end the 9in session. Add that URI to Allowed logout URLs.