Version: v0.13
OAuth 2.0 Client
The IDK provides a comprehensive OAuth 2.0 client implementation supporting authorization code flow, token management, and modern security extensions like PKCE and DPoP.
Overview
The OAuth 2.0 client handles:
- Authorization server metadata discovery
- Authorization code flow with PKCE
- Token exchange and refresh
- DPoP token binding
- Token introspection
Obtaining the Client
- Android/kotlin
- iOS/Swift
val oauth2Client = session.component.oauth2Client
let oauth2Client = session.component.oauth2Client
Authorization Server Discovery
Fetch authorization server metadata:
- Android/kotlin
- iOS/Swift
// Discover server metadata from well-known endpoint
val metadata = oauth2Client.discoverMetadata(
issuer = "https://auth.example.com"
)
// Access discovered endpoints
val authorizationEndpoint = metadata.authorizationEndpoint
val tokenEndpoint = metadata.tokenEndpoint
val jwksUri = metadata.jwksUri
// Check supported features
val supportsPkce = metadata.codeChallengeMethodsSupported.contains("S256")
val supportsDpop = metadata.dpopSigningAlgValuesSupported.isNotEmpty()
// Discover server metadata from well-known endpoint
let metadata = try await oauth2Client.discoverMetadata(
issuer: "https://auth.example.com"
)
// Access discovered endpoints
let authorizationEndpoint = metadata.authorizationEndpoint
let tokenEndpoint = metadata.tokenEndpoint
let jwksUri = metadata.jwksUri
// Check supported features
let supportsPkce = metadata.codeChallengeMethodsSupported.contains("S256")
let supportsDpop = !metadata.dpopSigningAlgValuesSupported.isEmpty
Authorization Code Flow
Starting Authorization
- Android/kotlin
- iOS/Swift
// Build authorization request
val authRequest = oauth2Client.buildAuthorizationRequest {
clientId = "my-client-id"
redirectUri = "myapp://callback"
scopes = setOf("openid", "profile", "email")
// PKCE is enabled by default
usePkce = true
// Optional: request specific claims
claims {
idToken {
claim("email") { essential = true }
claim("email_verified")
}
}
}
// Get the authorization URL
val authUrl = authRequest.authorizationUrl
// Open in browser or WebView
openBrowser(authUrl)
// Store state and code verifier for later
saveState(authRequest.state, authRequest.codeVerifier)
// Build authorization request
let authRequest = try await oauth2Client.buildAuthorizationRequest { builder in
builder.clientId = "my-client-id"
builder.redirectUri = "myapp://callback"
builder.scopes = Set(["openid", "profile", "email"])
// PKCE is enabled by default
builder.usePkce = true
// Optional: request specific claims
builder.claims { claims in
claims.idToken { idToken in
idToken.claim(name: "email") { c in c.essential = true }
idToken.claim(name: "email_verified")
}
}
}
// Get the authorization URL
let authUrl = authRequest.authorizationUrl
// Open in browser
openBrowser(url: authUrl)
// Store state and code verifier for later
saveState(state: authRequest.state, codeVerifier: authRequest.codeVerifier)
Handling the Callback
- Android/kotlin
- iOS/Swift
// Parse the callback URL
val callbackUrl = "myapp://callback?code=abc123&state=xyz789"
val authResponse = oauth2Client.parseAuthorizationResponse(callbackUrl)
// Validate state matches
val savedState = loadState()
if (authResponse.state != savedState.state) {
throw SecurityException("State mismatch - possible CSRF attack")
}
// Exchange code for tokens
val tokenResponse = oauth2Client.exchangeCode(
code = authResponse.code,
redirectUri = "myapp://callback",
codeVerifier = savedState.codeVerifier,
clientId = "my-client-id"
)
// Access tokens
val accessToken = tokenResponse.accessToken
val idToken = tokenResponse.idToken
val refreshToken = tokenResponse.refreshToken
// Parse the callback URL
let callbackUrl = "myapp://callback?code=abc123&state=xyz789"
let authResponse = try oauth2Client.parseAuthorizationResponse(url: callbackUrl)
// Validate state matches
let savedState = loadState()
guard authResponse.state == savedState.state else {
throw SecurityError.stateMismatch
}
// Exchange code for tokens
let tokenResponse = try await oauth2Client.exchangeCode(
code: authResponse.code,
redirectUri: "myapp://callback",
codeVerifier: savedState.codeVerifier,
clientId: "my-client-id"
)
// Access tokens
let accessToken = tokenResponse.accessToken
let idToken = tokenResponse.idToken
let refreshToken = tokenResponse.refreshToken
Token Refresh
- Android/kotlin
- iOS/Swift
// Refresh tokens before expiry
val newTokens = oauth2Client.refreshTokens(
refreshToken = currentRefreshToken,
clientId = "my-client-id"
)
// Update stored tokens
saveTokens(newTokens)
// Refresh tokens before expiry
let newTokens = try await oauth2Client.refreshTokens(
refreshToken: currentRefreshToken,
clientId: "my-client-id"
)
// Update stored tokens
saveTokens(tokens: newTokens)
ID Token Validation
- Android/kotlin
- iOS/Swift
// Validate ID token
val validationResult = oauth2Client.validateIdToken(
idToken = tokenResponse.idToken,
expectedIssuer = "https://auth.example.com",
expectedAudience = "my-client-id",
expectedNonce = savedNonce // If nonce was used
)
if (validationResult.isValid) {
val claims = validationResult.claims
val subject = claims["sub"] as String
val email = claims["email"] as String?
} else {
throw SecurityException("ID token validation failed: ${validationResult.error}")
}
// Validate ID token
let validationResult = try await oauth2Client.validateIdToken(
idToken: tokenResponse.idToken,
expectedIssuer: "https://auth.example.com",
expectedAudience: "my-client-id",
expectedNonce: savedNonce // If nonce was used
)
if validationResult.isValid {
let claims = validationResult.claims
let subject = claims["sub"] as! String
let email = claims["email"] as? String
} else {
throw SecurityError.idTokenValidationFailed(error: validationResult.error)
}
Token Introspection
- Android/kotlin
- iOS/Swift
// Check if token is still valid
val introspection = oauth2Client.introspectToken(
token = accessToken,
tokenTypeHint = "access_token",
clientId = "my-client-id",
clientSecret = "my-client-secret" // If required
)
if (introspection.active) {
val expiresAt = introspection.exp
val scopes = introspection.scope
val subject = introspection.sub
} else {
// Token is no longer valid
handleInvalidToken()
}
// Check if token is still valid
let introspection = try await oauth2Client.introspectToken(
token: accessToken,
tokenTypeHint: "access_token",
clientId: "my-client-id",
clientSecret: "my-client-secret" // If required
)
if introspection.active {
let expiresAt = introspection.exp
let scopes = introspection.scope
let subject = introspection.sub
} else {
// Token is no longer valid
handleInvalidToken()
}
Configuration
Configure the OAuth 2.0 client via properties:
# Authorization server
oauth2.issuer=https://auth.example.com
oauth2.client-id=my-client-id
oauth2.client-secret=my-client-secret
# Redirect URI
oauth2.redirect-uri=myapp://callback
# Token settings
oauth2.token.refresh.margin.seconds=60
# Security features
oauth2.pkce.enabled=true
oauth2.dpop.enabled=false
Error Handling
- Android/kotlin
- iOS/Swift
try {
val tokens = oauth2Client.exchangeCode(code, redirectUri, codeVerifier, clientId)
} catch (e: OAuth2Exception) {
when (e.error) {
"invalid_grant" -> showError("Authorization expired. Please try again.")
"invalid_client" -> showError("Client authentication failed.")
"invalid_request" -> showError("Invalid request: ${e.errorDescription}")
else -> showError("OAuth error: ${e.error}")
}
}
do {
let tokens = try await oauth2Client.exchangeCode(
code: code,
redirectUri: redirectUri,
codeVerifier: codeVerifier,
clientId: clientId
)
} catch let error as OAuth2Exception {
switch error.error {
case "invalid_grant":
showError(message: "Authorization expired. Please try again.")
case "invalid_client":
showError(message: "Client authentication failed.")
case "invalid_request":
showError(message: "Invalid request: \(error.errorDescription ?? "")")
default:
showError(message: "OAuth error: \(error.error)")
}
}