Build your app

Mobile and desktop apps

Sign users in from iOS, Android, React Native, Flutter and desktop apps with the system browser and PKCE.

View .md llms.txt

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:

PlatformRedirect URI
iOS / Android with a custom schemecom.acme.app:/callback (reverse-domain scheme)
Universal / App Linkshttps://app.acme.com/callback
Desktop app with a loopback serverhttp://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.

swift
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

ts
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.