Integrate on mobile (iOS & Android)
This guide adapts the OAuth2 PKCE flow to mobile. One rule matters above all: use the system browser, never an embedded WebView. The flow is unchanged — generate a PKCE pair, authorize in the browser, receive a deep-link callback, and exchange the code on your backend — only the platform plumbing differs. PKCE generation is covered in the OAuth2 PKCE guide; the snippets below focus on the mobile-specific parts.
iOS (Swift)
Use ASWebAuthenticationSession with a custom callback scheme:
import AuthenticationServices
func startAuth() {
let state = UUID().uuidString
KeychainHelper.save(key: "oauth_state", value: state)
let challenge = generateCodeChallenge(verifier: codeVerifier) // see PKCE guide
var components = URLComponents(string: "https://accounts.ppoppo.com/oauth/authorize")!
components.queryItems = [
.init(name: "client_id", value: Config.ppoppoClientId),
.init(name: "redirect_uri", value: "yourapp://auth/callback"),
.init(name: "response_type", value: "code"),
.init(name: "scope", value: "openid profile"),
.init(name: "state", value: state),
.init(name: "code_challenge", value: challenge),
.init(name: "code_challenge_method", value: "S256"),
]
let session = ASWebAuthenticationSession(url: components.url!, callbackURLScheme: "yourapp") {
callbackURL, _ in
guard let url = callbackURL else { return }
self.handleCallback(url: url) // verify state, then exchange the code on your backend
}
session.presentationContextProvider = self
session.start()
}In handleCallback, confirm the returned state matches the Keychain value before exchanging the code.
Android (Kotlin)
Use Custom Tabs to launch the browser:
val authUri = Uri.Builder()
.scheme("https").authority("accounts.ppoppo.com")
.appendPath("oauth").appendPath("authorize")
.appendQueryParameter("client_id", BuildConfig.PPOPPO_CLIENT_ID)
.appendQueryParameter("redirect_uri", "yourapp://auth/callback")
.appendQueryParameter("response_type", "code")
.appendQueryParameter("scope", "openid profile")
.appendQueryParameter("state", state)
.appendQueryParameter("code_challenge", codeChallenge)
.appendQueryParameter("code_challenge_method", "S256")
.build()
CustomTabsIntent.Builder().build().launchUrl(activity, authUri)Register the callback scheme so the deep link returns to your app:
<activity android:name=".AuthCallbackActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="yourapp" android:host="auth" android:path="/callback" />
</intent-filter>
</activity>Refreshing tokens on mobile
Store the refresh token in secure platform storage — the iOS Keychain or Android EncryptedSharedPreferences — and refresh proactively, before the one-hour access-token expiry:
if tokenExpiresAt.timeIntervalSinceNow < 300 { refreshTokens() }If a refresh fails with invalid_grant, the user has revoked access — clear stored tokens and restart the flow. See the Sign-in reference for the refresh request.