OpenID Connect Authentication
Authentication follows the Authorization Code Flow with PKCE — the recommended flow for mobile apps, as defined in RFC 6749 and RFC 7636. For a detailed protocol reference, see the OpenID Connect Core 1.0 spec.
Authorization Request
The app opens a secure system browser and redirects the user to the Identity Provider (e.g. Keycloak). A PKCE code_challenge is included so the token exchange can later be verified.
Callback
After the user authenticates, the Identity Provider redirects back to the app via a custom URI scheme or App/Universal Link, carrying a short-lived authorization_code.
Token Request
The app exchanges the authorization_code — together with the PKCE code_verifier — for an access_token, id_token, and optionally a refresh_token.
Since mobile apps are public clients (no client_secret), PKCE is mandatory.
Implementation
The recommended library for both platforms is AppAuth, a certified OpenID Connect implementation maintained by the OpenID Foundation:
| Platform | Library | Minimum OS |
|---|---|---|
| iOS / macOS | openid/AppAuth-iOS | iOS 12+ |
| Android | openid/AppAuth-Android | API 16+ |
AppAuth handles PKCE, endpoint discovery, and the secure browser session automatically — iOS uses ASWebAuthenticationSession, Android uses Chrome Custom Tabs.
- iOS (AppAuth-iOS)
- Android (AppAuth-Android)
Installation
- Swift Package Manager
- CocoaPods
https://github.com/openid/AppAuth-iOS
pod 'AppAuth'
Redirect URI — Info.plist
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>com.example.app</string>
</array>
</dict>
</array>
Login
import AppAuth
class AuthManager {
// Retain the session to keep ASWebAuthenticationSession alive
var currentAuthFlow: OIDExternalUserAgentSession?
func login(presenting viewController: UIViewController) {
// Get the Authentication URL from the SDK
guard let openIDBaseUrl = URL(string:kapeSDK?.manager.getConfiguration().domains().getOpenidApiDomain() ?? "") else { return }
// 1. Discover AS endpoints
OIDAuthorizationService.discoverConfiguration(forIssuer: openIDBaseUrl) { configuration, error in
guard let configuration else {
print("Discovery failed: \(error!)")
return
}
// 2. Build authorization request — PKCE is handled automatically
let request = OIDAuthorizationRequest(
configuration: configuration,
clientId: "my-mobile-app",
// use offline_access to request long-living Refresh Tokens
scopes: [OIDScopeOpenID, OIDScopeProfile, OIDScopeEmail, "offline_access"],
redirectURL: URL(string: "com.example.app:/callback")!,
responseType: OIDResponseTypeCode,
additionalParameters: nil
)
// 3. Open ASWebAuthenticationSession and exchange code for tokens
self.currentAuthFlow = OIDAuthState.authState(
byPresenting: request,
presenting: viewController
) { authState, error in
guard let authState else {
print("Auth failed: \(error!)")
return
}
let credentials = AuthCredentials(
accessToken: authState.lastTokenResponse?.accessToken ?? "",
refreshToken: authState.lastTokenResponse?.refreshToken ?? "",
idToken: authState.lastTokenResponse?.idToken ?? ""
)
try kapeSDK!.manager.identity().openidAuthenticateWithAuthCredentials(authCredentials: credentials)
}
}
}
}
Hold a strong reference to currentAuthFlow for the duration of the session. Releasing it cancels the ASWebAuthenticationSession.
Installation
build.gradle:
dependencies {
implementation "net.openid:appauth:0.11.1"
}
Redirect URI — AndroidManifest.xml
<activity android:name="net.openid.appauth.RedirectUriReceiverActivity"
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="com.example.app" />
</intent-filter>
</activity>
Login
import net.openid.appauth.*
class LoginActivity : AppCompatActivity() {
private lateinit var authService: AuthorizationService
private val RC_AUTH = 100
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
authService = AuthorizationService(this)
val issuer = Uri.parse(kapeSDK?.manager?.getConfiguration()?.domains()?.getOpenidApiDomain() ?: return)
// 1. Discover AS endpoints
AuthorizationServiceConfiguration.fetchFromIssuer(issuer) { configuration, error ->
if (configuration == null) {
Log.e("OIDC", "Discovery failed: $error")
return@fetchFromIssuer
}
// 2. Build authorization request — PKCE is handled automatically
val request = AuthorizationRequest.Builder(
configuration,
"my-mobile-app",
ResponseTypeValues.CODE,
Uri.parse("com.example.app:/callback")
)
.setScopes("openid", "profile", "email", "offline_access")
.build()
// 3. Open Chrome Custom Tab
val intent = authService.getAuthorizationRequestIntent(request)
startActivityForResult(intent, RC_AUTH)
}
}
// 4. Receive the authorization code callback
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode != RC_AUTH || data == null) return
val response = AuthorizationResponse.fromIntent(data)
val error = AuthorizationException.fromIntent(data)
if (response == null) {
Log.e("OIDC", "Authorization failed: $error")
return
}
// 5. Exchange code for tokens
authService.performTokenRequest(response.createTokenExchangeRequest()) { tokenResponse, ex ->
if (tokenResponse != null) {
val credentials = AuthCredentials(
accessToken = tokenResponse.accessToken ?: "",
refreshToken = tokenResponse.refreshToken ?: "",
idToken = tokenResponse.idToken ?: ""
)
kapeSDK?.manager?.identity()?.openidAuthenticateWithAuthCredentials(authCredentials = credentials)
} else {
Log.e("OIDC", "Token exchange failed: $ex")
}
}
}
override fun onDestroy() {
super.onDestroy()
authService.dispose()
}
}
Terminating an OpenID SSO Session
The authentication request will usually initiate an SSO Session, which will ensure that other apps using the same authorization URL can use the session to automatically log in the user. If you want to terminate such a session — preventing other apps from getting automatically logged in — you need to create an EndSessionRequest instead of AuthorizationRequest, providing the ID Token which you can fetch from the SDK with fetchTokenTypeType.
Login Hint (Cross-App SSO)
The SDK supports cross-app Single Sign-On (SSO) via the standard OpenID Connect login_hint parameter. When a user is already authenticated in one app, another app can obtain a Login Token from the SDK and pass it as login_hint in the Authorization Code Flow with PKCE. The Identity Provider uses this token to recognise the existing session and authenticate the user without requiring them to re-enter their credentials.
Generating a Login Token
Call openidGenerateLoginToken on the SDK's identity module, passing the clientId of the target application. The method returns a short-lived token string. The calling app must already be authenticated; the call will fail otherwise.
- iOS (Swift)
- Android (Kotlin)
func loginWithHint(presenting viewController: UIViewController, targetClientId: String) {
guard let issuerURL = URL(string: kapeSDK?.manager.getConfiguration().domains().getOpenidApiDomain() ?? "") else { return }
execute_sdk_operation {
// 1. Generate a Login Token for the target app
let loginHint = try kapeSDK!.manager.identity().openidGenerateLoginToken(clientId: targetClientId)
OIDAuthorizationService.discoverConfiguration(forIssuer: issuerURL) { configuration, error in
guard let configuration else {
print("Discovery failed: \(error!)")
return
}
// 2. Build the Authorization Request with login_hint
let request = OIDAuthorizationRequest(
configuration: configuration,
clientId: "my-mobile-app",
scopes: [OIDScopeOpenID, OIDScopeProfile, OIDScopeEmail, "offline_access"],
redirectURL: URL(string: "com.example.app:/callback")!,
responseType: OIDResponseTypeCode,
additionalParameters: ["login_hint": loginHint]
)
// 3. Open ASWebAuthenticationSession and exchange code for tokens
self.currentAuthFlow = OIDAuthState.authState(
byPresenting: request,
presenting: viewController
) { authState, error in
guard let authState else {
print("Auth failed: \(error!)")
return
}
let credentials = AuthCredentials(
accessToken: authState.lastTokenResponse?.accessToken ?? "",
refreshToken: authState.lastTokenResponse?.refreshToken ?? "",
idToken: authState.lastTokenResponse?.idToken ?? ""
)
try kapeSDK!.manager.identity().openidAuthenticateWithAuthCredentials(authCredentials: credentials)
}
}
}
}
fun loginWithHint(targetClientId: String) {
val issuer = Uri.parse(kapeSDK?.manager?.getConfiguration()?.domains()?.getOpenidApiDomain() ?: return)
// 1. Generate a Login Token for the target app
val loginHint = kapeSDK?.manager?.identity()?.openidGenerateLoginToken(clientId = targetClientId) ?: return
AuthorizationServiceConfiguration.fetchFromIssuer(issuer) { configuration, error ->
if (configuration == null) {
Log.e("OIDC", "Discovery failed: $error")
return@fetchFromIssuer
}
// 2. Build the Authorization Request with login_hint
val request = AuthorizationRequest.Builder(
configuration,
"my-mobile-app",
ResponseTypeValues.CODE,
Uri.parse("com.example.app:/callback")
)
.setScopes("openid", "profile", "email", "offline_access")
.setAdditionalParameters(mapOf("login_hint" to loginHint))
.build()
// 3. Open Chrome Custom Tab
val intent = authService.getAuthorizationRequestIntent(request)
startActivityForResult(intent, RC_AUTH)
}
}
// 4. Receive the authorization code and authenticate the SDK
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode != RC_AUTH || data == null) return
val response = AuthorizationResponse.fromIntent(data) ?: return
authService.performTokenRequest(response.createTokenExchangeRequest()) { tokenResponse, ex ->
if (tokenResponse != null) {
val credentials = AuthCredentials(
accessToken = tokenResponse.accessToken ?: "",
refreshToken = tokenResponse.refreshToken ?: "",
idToken = tokenResponse.idToken ?: ""
)
kapeSDK?.manager?.identity()?.openidAuthenticateWithAuthCredentials(authCredentials = credentials)
} else {
Log.e("OIDC", "Token exchange failed: $ex")
}
}
}
How It Works
The Identity Provider uses the login_hint value to look up the existing user session. If the session is still valid, the user is authenticated silently and the authorization code is issued without any additional login prompt. The resulting flow is identical to the standard Authorization Code Flow — the same openidAuthenticateWithAuthCredentials call on the SDK is used to hand the tokens to the SDK.