# Mobile and desktop apps

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

Source: https://9in.io/docs/native-apps/

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](https://appauth.io) 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**.
