Skip to Content
Framework GuidesOAuth on Mobile

OAuth on Mobile

Web apps receive OAuth tokens in a URL fragment on their callback page. A mobile app has no callback page: the system browser must hand control back to the app, and tokens must never travel through the operating system’s URL handler. Ghayma supports two ways to come back and one way to skip the browser entirely.

Custom schemeVerified linkNative sign-in
Setupnonehost association files on your domain + app entitlementsprovider SDK + client IDs
What comes backone-time codeone-time codenothing to redirect (provider ID token)
ProvidersGoogle, GitHubGoogle, GitHubGoogle

A project that only serves a mobile app needs no site: answer No site — only databases, storage and auth at ghayma init and it holds your auth app, databases and buckets without deploying anything.

1. Register where the browser comes back

In the console, open Project → Auth → your app → Allowed Origins and add your app’s deep-link origin exactly:

  • Custom scheme: com.example.app://callback (scheme + host, no path). Only an exact entry covers a custom scheme, and the link must have a host segment: myapp://callback, not myapp:///callback.
  • Verified link (iOS universal link / Android App Link): the https origin, for example https://app.example.com. Nothing to add when that domain belongs to a site of the project: those are always allowed. Your domain must host the Apple and Google association files.

file, javascript, data, blob, about, vbscript and ftp are refused.

2. Browser flow with a one-time code (PKCE)

  1. Generate a random code_verifier (43–128 characters from A-Z a-z 0-9 - . _ ~) and its challenge BASE64URL(SHA-256(code_verifier)) without padding.
  2. Open this URL in the system browser (ASWebAuthenticationSession on iOS, Custom Tabs on Android, flutter_web_auth_2 in Flutter):
https://auth.ghayma.tech/v1/{appSlug}/auth/google ?redirect_uri=com.example.app://callback &code_challenge=<challenge> &code_challenge_method=S256

Use /auth/github for GitHub. 3. After the provider, the browser is sent to com.example.app://callback?code=<code>. On failure it is sent to com.example.app://callback?error=<message>. 4. Exchange the code within 120 seconds; it works once:

curl -X POST https://auth.ghayma.tech/v1/{appSlug}/oauth/exchange \ -H "Content-Type: application/json" \ -d '{"code":"<code>","code_verifier":"<code_verifier>"}'

Response: the same bodies as login. Usually the session:

{ "access_token": "…", "refresh_token": "…", "expires_in": 900, "token_type": "Bearer", "user": { "…": "…" } }

When the app’s 2FA policy applies to the user, it is a pending second factor instead, with no tokens — see Second factor. The code is spent either way.

Errors: 400 {"error":"invalid or expired code","code":"invalid_grant"} (unknown, expired, reused, wrong verifier, wrong app, or minted before the user’s sessions last ended), 403 {"error":"Account is disabled"}, 429 with Retry-After.

Web apps keep the fragment flow: without code_challenge the callback still returns #access_token=…. Nothing changes for existing sites.

3. Native sign-in with Google (no browser)

  1. In Google Cloud, create iOS and Android OAuth client IDs in the same project as your web client ID.
  2. Paste them in the console under Google OAuth → Native client IDs (iOS / Android).
  3. Sign in with the Google Sign-In SDK on the device and post the ID token:
curl -X POST https://auth.ghayma.tech/v1/{appSlug}/oauth/id-token \ -H "Content-Type: application/json" \ -d '{"provider":"google","id_token":"<id_token>","nonce":"<nonce if you set one>"}'

Ghayma verifies the token against Google’s public keys, checks that its audience is one of your registered client IDs, creates or links the user by email (how), and answers like the exchange: the session, or a pending second factor. Errors: 401 {"error":"invalid id token","code":"invalid_token"}, 400 when the provider is disabled or the token carries no email, 403 when the app’s user limit is reached or the account is disabled, 403 {"error":"Your Google account's email address is not verified","code":"email_not_verified"} when Google has not verified the address, 429.

Android’s Credential Manager returns tokens whose audience is your web client ID; iOS returns the iOS client ID. Both are accepted once listed.

Second factor

When the app’s 2FA policy applies to the user, neither endpoint issues tokens. Both answer 200 with the pending step POST /login returns:

{ "two_fa_required": true, "challenge_token": "…", "methods": ["totp"], "phone_hint": "" }
{ "two_fa_enrollment_required": true, "enroll_token": "…", "methods": ["totp"] }

Finish with POST /v1/{appSlug}/2fa/verify and the user’s code for a challenge_token, or with TOTP enrolment for an enroll_token (the policy is Enforced and the user has no second factor yet) — see Auth Integration → Two-Factor Authentication. The PKCE browser flow keeps this step out of the redirect: the deep link only ever receives ?code=.

The SDKs throw the step and store no session: TwoFactorRequiredException in Dart and PHP, TwoFactorRequiredError in JavaScript.

Update the SDK before you turn 2FA on: ghayma_auth 0.2.0, @ghayma/sdk 1.3.0, PHP ghayma/sdk 0.2.0. ghayma_auth 0.1.0 and @ghayma/sdk 1.2.0 read the pending step as a session without tokens and report a sign-in; PHP 0.1.0 fails with a ValueError. See Minimum versions for 2FA.

Rate limits

EndpointPer clientPer app
POST /oauth/exchange20 / 15 min600 / 15 min
POST /oauth/id-token10 / 15 min600 / 15 min
POST /2fa/verify10 / 15 min—

The client is the device’s IP; an IPv6 address counts as its /64. See Rate Limits for every endpoint.

What is coming

  • Sign in with Apple (required by the App Store for iOS apps that offer Google sign-in) is planned.
  • SDKs: the Dart / Flutter SDK (ghayma_auth) is out — it runs the PKCE flow, the code exchange and native Google sign-in for you — @ghayma/sdk ships the PKCE helpers and native sign-in for JavaScript, and the PHP SDK exchanges codes and ID tokens on your server. Swift and Kotlin are planned.

See the full contract in the Auth Service API.