Oidc
First-class OpenID Connect plugin for Ktor server authentication.
This API is experimental and may change in future releases.
Installs per-issuer support for:
OAuth 2.0 / OIDC login (
oauth { }) — authorization code flow with login and callback routes. Sessions are enabled by default; customize withoauth { sessions { } }or opt out withdisableSessions(). Browser session authentication is exposed as OidcProvider.session.Resource-server Bearer authentication (
bearer { }) — two independent schemes: OidcProvider.jwtBearer for locally verified JWTs, and OidcProvider.introspectionBearer for RFC 7662 token introspection (enabled by nestedintrospection { }).
This plugin implements the Authorization Code Flow with PKCE (RFC 6749 §4.1, OIDC Core §3.1), resource-server Bearer / RFC 7662 introspection, and optional OAuth 2.0 Protected Resource Metadata (RFC 9728) via OidcPluginConfig.protectedResource. Implicit and Hybrid flows are not supported.
Provider metadata is fetched automatically from the issuer's discovery document (<issuer>/.well-known/openid-configuration) and periodically refreshed unless a provider configures static OpenIdProviderMetadata.
Initial discovery is part of identityProvider registration. The function discovers metadata, installs provider routes, and starts periodic refresh before returning the registered OidcProvider. After the final failed discovery attempt, registration fails with a OidcDiscoveryException.
Full configuration example
The example below registers identity providers from a suspend application module because registration performs initial discovery.
val oidc = install(Oidc) {
discoveryRefreshInterval = 15.minutes
initialDiscoveryAttempts = 3
initialDiscoveryRetryDelay = 1.minutes
}
// One issuer. Schemes expose OidcToken types; map them on the routes that use them.
val auth0 = oidc.identityProvider("auth0") {
issuer = "https://issuer"
jwt {
clockSkew = 60.seconds
}
// jwtBearer for locally verified JWTs. Nested introspection { } also enables introspectionBearer.
bearer {
audience = setOf("my-api")
tokenExtractor = { call.request.cookies["MY_TOKEN"] }
introspection {
endpoint = "https://issuer/oauth/introspect"
clientId = "api-client"
clientSecret = "..."
}
}
// Authorization-code login. Sessions store OidcToken.Id unless disableSessions() is called.
oauth {
clientId = "web-client"
clientSecret = "..."
scopes = listOf("openid", "profile", "email")
onAuthenticated { token ->
call.respondRedirect("/dashboard")
}
sessions {
name = "AUTH0_SESSION"
}
}
}
// Mapping runs when a derived scheme authenticates a route, not during the OAuth callback.
val userSession = auth0.session.mapPrincipal { token -> findUser(token.userInfo.subject) }
val apiUser = auth0.jwtBearer.mapPrincipal { token -> findUser(token.claims.subject) }
routing {
authenticateWith(apiUser) {
get("/api/me") {
val user = call.principal
call.respond(user)
}
}
authenticateWith(auth0.introspectionBearer) {
get("/api/opaque") {
val token = call.principal
call.respond(token.introspection)
}
}
authenticateWith(userSession) {
get("/me") {
val user = call.principal
call.respond(user)
}
}
}Testing with static metadata and local keys
Tests can avoid real discovery and JWKS calls while keeping normal issuer, audience, algorithm, and signature validation:
val keys = OpenIdTestKeys.rsa(issuer = TEST_ISSUER, audience = TEST_AUDIENCE)
// Static metadata skips discovery; jwt(keys) verifies signatures against the in-memory public key.
val provider = oidc.identityProvider("test") {
issuer = TEST_ISSUER
metadata = OpenIdProviderMetadata(
issuer = TEST_ISSUER,
authorizationEndpoint = "$TEST_ISSUER/authorize",
tokenEndpoint = "$TEST_ISSUER/token",
jwksUri = "$TEST_ISSUER/jwks",
)
jwt(keys)
bearer {
audience = setOf(TEST_AUDIENCE)
}
}
val token = keys.accessToken {
subject = "user-1"
email = "user@example.com"
}Environment-based configuration
Provider values can be stored in application.conf (or equivalent) and applied explicitly with OidcEnvConfig:
ktor.oidc.google {
issuer = "https://accounts.google.com"
clientId = ${GOOGLE_CLIENT_ID}
clientSecret = ${GOOGLE_CLIENT_SECRET}
scopes = ["openid", "profile", "email"]
}val env = environment.config
.property("ktor.oidc.google")
.getAs<OidcEnvConfig>()
val oidc = install(Oidc) { }
// env.scopes must include openid; assigning scopes replaces the OAuth default list.
val google = oidc.identityProvider("google") {
issuer = env.issuer
bearer {
audience = setOf("api")
}
oauth {
clientId = env.clientId
clientSecret = env.clientSecret
scopes = env.scopes
}
}Types
Installs Oidc in this application and returns the identity-provider registry.