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 scheme | Verified link | Native sign-in | |
|---|---|---|---|
| Setup | none | host association files on your domain + app entitlements | provider SDK + client IDs |
| What comes back | one-time code | one-time code | nothing to redirect (provider ID token) |
| Providers | Google, GitHub | Google, GitHub |
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, notmyapp:///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)
- Generate a random
code_verifier(43–128 characters fromA-Z a-z 0-9 - . _ ~) and its challengeBASE64URL(SHA-256(code_verifier))without padding. - Open this URL in the system browser (
ASWebAuthenticationSessionon iOS, Custom Tabs on Android,flutter_web_auth_2in Flutter):
https://auth.ghayma.tech/v1/{appSlug}/auth/google
?redirect_uri=com.example.app://callback
&code_challenge=<challenge>
&code_challenge_method=S256Use /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)
- In Google Cloud, create iOS and Android OAuth client IDs in the same project as your web client ID.
- Paste them in the console under Google OAuth → Native client IDs (iOS / Android).
- 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
| Endpoint | Per client | Per app |
|---|---|---|
POST /oauth/exchange | 20 / 15 min | 600 / 15 min |
POST /oauth/id-token | 10 / 15 min | 600 / 15 min |
POST /2fa/verify | 10 / 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/sdkships 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.