Skip to main content
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

val oauth2Client = session.component.oauth2Client

Authorization Server Discovery

Fetch authorization server metadata:

// 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()

Authorization Code Flow

Starting Authorization

// 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)

Handling the Callback

// 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

Token Refresh

// Refresh tokens before expiry
val newTokens = oauth2Client.refreshTokens(
refreshToken = currentRefreshToken,
clientId = "my-client-id"
)

// Update stored tokens
saveTokens(newTokens)

ID Token Validation

// 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}")
}

Token Introspection

// 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()
}

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

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}")
}
}