Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public final class io/ktor/server/auth/oidc/OidcOAuthConfig {
public final fun getFetchUserInfo ()Z
public final fun getIdTokenAudience ()Ljava/lang/String;
public final fun getLoginUri ()Lkotlin/jvm/functions/Function1;
public final fun getPostLogoutRedirectUri ()Lkotlin/jvm/functions/Function1;
public final fun getRedirectUri ()Lkotlin/jvm/functions/Function1;
public final fun getResourceIndicators ()Ljava/util/List;
public final fun getScopes ()Ljava/util/List;
Expand All @@ -83,6 +84,7 @@ public final class io/ktor/server/auth/oidc/OidcOAuthConfig {
public final fun setFetchUserInfo (Z)V
public final fun setIdTokenAudience (Ljava/lang/String;)V
public final fun setLoginUri (Lkotlin/jvm/functions/Function1;)V
public final fun setPostLogoutRedirectUri (Lkotlin/jvm/functions/Function1;)V
public final fun setRedirectUri (Lkotlin/jvm/functions/Function1;)V
public final fun setResourceIndicators (Ljava/util/List;)V
public final fun setScopes (Ljava/util/List;)V
Expand All @@ -104,11 +106,14 @@ public final class io/ktor/server/auth/oidc/OidcPluginConfig {
}

public final class io/ktor/server/auth/oidc/OidcProvider {
public final fun buildLogoutUrl (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
public final fun currentJwkProvider ()Lcom/auth0/jwk/JwkProvider;
public final fun currentMetadata ()Lio/ktor/server/auth/oidc/OpenIdProviderMetadata;
public final fun getBearer ()Lio/ktor/server/auth/typesafe/DefaultAuthScheme;
public final fun getIssuer ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
public final fun getSessions ()Lio/ktor/server/auth/typesafe/SessionAuthScheme;
public final fun refreshToken (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class io/ktor/server/auth/oidc/OidcProviderConfig {
Expand All @@ -123,6 +128,8 @@ public final class io/ktor/server/auth/oidc/OidcProviderConfig {
public final fun jwt (Lkotlin/jvm/functions/Function1;)V
public final fun oauth (Lkotlin/jvm/functions/Function1;)V
public static synthetic fun oauth$default (Lio/ktor/server/auth/oidc/OidcProviderConfig;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public final fun sessions (Lkotlin/jvm/functions/Function1;)V
public static synthetic fun sessions$default (Lio/ktor/server/auth/oidc/OidcProviderConfig;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public final fun setIssuer (Ljava/lang/String;)V
public final fun setMetadata (Lio/ktor/server/auth/oidc/OpenIdProviderMetadata;)V
}
Expand All @@ -131,6 +138,34 @@ public abstract interface class io/ktor/server/auth/oidc/OidcProviderContext {
public abstract fun provider ()Lio/ktor/server/auth/oidc/OidcProvider;
}

public final class io/ktor/server/auth/oidc/OidcSessionConfig {
public final fun cookie (Lkotlin/jvm/functions/Function1;)V
public final fun csrfProtection (Lkotlin/jvm/functions/Function1;)V
public final fun disableCsrfProtection ()V
public final fun getLogoutUri ()Lkotlin/jvm/functions/Function1;
public final fun getName ()Ljava/lang/String;
public final fun getRefreshUri ()Lkotlin/jvm/functions/Function1;
public final fun getStorage ()Lio/ktor/server/sessions/SessionStorage;
public final fun getTokenRefreshStrategy ()Lio/ktor/server/auth/oidc/OidcTokenRefreshStrategy;
public final fun setLogoutUri (Lkotlin/jvm/functions/Function1;)V
public final fun setName (Ljava/lang/String;)V
public final fun setRefreshUri (Lkotlin/jvm/functions/Function1;)V
public final fun setStorage (Lio/ktor/server/sessions/SessionStorage;)V
public final fun setTokenRefreshStrategy (Lio/ktor/server/auth/oidc/OidcTokenRefreshStrategy;)V
}

public final class io/ktor/server/auth/oidc/OidcSessionsContext : io/ktor/server/auth/oidc/OidcProviderContext, io/ktor/server/auth/typesafe/SessionAuthenticatedContext {
public fun clearSession (Lio/ktor/server/routing/RoutingContext;)V
public fun principal (Lio/ktor/server/routing/RoutingContext;)Ljava/lang/Object;
public fun provider ()Lio/ktor/server/auth/oidc/OidcProvider;
public fun session (Lio/ktor/server/routing/RoutingContext;)Lio/ktor/server/auth/oidc/OidcToken$Id;
public synthetic fun session (Lio/ktor/server/routing/RoutingContext;)Ljava/lang/Object;
public fun setSession (Lio/ktor/server/routing/RoutingContext;Lio/ktor/server/auth/oidc/OidcToken$Id;)V
public synthetic fun setSession (Lio/ktor/server/routing/RoutingContext;Ljava/lang/Object;)V
public fun updateSession (Lio/ktor/server/routing/RoutingContext;Lkotlin/jvm/functions/Function1;)Lio/ktor/server/auth/oidc/OidcToken$Id;
public synthetic fun updateSession (Lio/ktor/server/routing/RoutingContext;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
}

public final class io/ktor/server/auth/oidc/OidcStateEncryptionKey {
public static final field Companion Lio/ktor/server/auth/oidc/OidcStateEncryptionKey$Companion;
public static final field KEY_SIZE I
Expand Down Expand Up @@ -260,6 +295,34 @@ public final class io/ktor/server/auth/oidc/OidcToken$UserInfo$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class io/ktor/server/auth/oidc/OidcTokenRefreshResult {
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lkotlin/time/Duration;Ljava/lang/String;Ljava/lang/String;Lio/ktor/server/auth/oidc/OidcToken$Id;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lkotlin/time/Duration;Ljava/lang/String;Ljava/lang/String;Lio/ktor/server/auth/oidc/OidcToken$Id;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getAccessToken ()Ljava/lang/String;
public final fun getExpiresIn-FghU774 ()Lkotlin/time/Duration;
public final fun getIdToken ()Lio/ktor/server/auth/oidc/OidcToken$Id;
public final fun getRefreshToken ()Ljava/lang/String;
public final fun getScope ()Ljava/lang/String;
public final fun getTokenType ()Ljava/lang/String;
}

public abstract interface class io/ktor/server/auth/oidc/OidcTokenRefreshStrategy {
}

public final class io/ktor/server/auth/oidc/OidcTokenRefreshStrategy$Auto : io/ktor/server/auth/oidc/OidcTokenRefreshStrategy {
public synthetic fun <init> (JILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (JLkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getBeforeExpiry-UwyO8pc ()J
}

public abstract interface class io/ktor/server/auth/oidc/OidcTokenRefreshStrategy$Custom : io/ktor/server/auth/oidc/OidcTokenRefreshStrategy {
public abstract fun refresh (Lio/ktor/server/auth/oidc/OidcProvider;Lio/ktor/server/auth/oidc/OidcToken$Id;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class io/ktor/server/auth/oidc/OidcTokenRefreshStrategy$Disabled : io/ktor/server/auth/oidc/OidcTokenRefreshStrategy {
public static final field INSTANCE Lio/ktor/server/auth/oidc/OidcTokenRefreshStrategy$Disabled;
}

public final class io/ktor/server/auth/oidc/OpaqueTokenIntrospection {
public static final field Companion Lio/ktor/server/auth/oidc/OpaqueTokenIntrospection$Companion;
public fun <init> (ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.ktor.events.EventDefinition
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.config.*
import io.ktor.server.sessions.*
import io.ktor.util.*
import io.ktor.utils.io.*
import kotlinx.coroutines.*
Expand All @@ -30,8 +31,10 @@ private val ProviderNameRegex = Regex("[a-z0-9]+(?:-[a-z0-9]+)*")
* - Bearer token authentication (`bearer { }`) that validates JWT access tokens issued by the provider.
* Use [OidcProvider.bearer] with `authenticateWith`.
* - **OAuth 2.0 / OIDC login flow** (`oauth { }`) — handles the authorization code flow,
* including login and redirect routes.
* Registered internally as `"$name-oauth"` and used only for the auto-registered routes.
* including login and redirect routes. Session storage is opt-in via `sessions { }`, which also
* enables plugin-managed refresh and logout routes.
* Registered internally as `"$name-oauth"` and used only for the auto-registered routes;
* browser session authentication is exposed as [OidcProvider.sessions].
*
* This plugin implements the Authorization Code Flow with PKCE (RFC 6749 §4.1, OIDC Core §3.1) and resource-server
* Bearer / RFC 7662 introspection. Implicit and Hybrid flows are not supported.
Expand Down Expand Up @@ -94,6 +97,13 @@ private val ProviderNameRegex = Regex("[a-z0-9]+(?:-[a-z0-9]+)*")
* call.respondRedirect("/dashboard")
* }
* }
*
* // Browser sessions are opt-in. When enabled, callbacks store the verified
* // ID-token principal in the session and plugin-managed refresh/logout routes
* // are installed.
* sessions {
* name = "GOOGLE_SESSION"
* }
* }
*
* // Protect routes using typed provider capabilities.
Expand All @@ -111,6 +121,13 @@ private val ProviderNameRegex = Regex("[a-z0-9]+(?:-[a-z0-9]+)*")
* call.respond(user.userInfo)
* }
* }
*
* authenticateWith(google.sessions) {
* get("/me") {
* val user = principal as OidcToken.Id
* call.respond(user.userInfo)
* }
* }
* }
* ```
*
Expand Down Expand Up @@ -295,6 +312,15 @@ public class Oidc internal constructor(
private fun checkProductionEnvironment(provider: OidcProvider<*>) {
val devMode = application.developmentMode

// check production session storage is not in-memory
provider.config.sessionConfig?.let { sessionConfig ->
if (!devMode && sessionConfig.storage is SessionStorageMemory) {
provider.logger.warn(
"OpenID Connect sessions use SessionStorageMemory. Configure shared SessionStorage for clustered production deployments."
)
}
}

// ensure production stateEncryptionKey is configured
val oauthConfig = provider.config.oauthConfig
if (oauthConfig == null || oauthConfig.stateEncryptionKey != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,36 @@ public class OidcBearerContext<P : Any> internal constructor(
override fun provider(): OidcProvider<P> = provider
}

/**
* Route context used by OpenID Connect session authentication.
*
* It exposes the typed authenticated principal, the raw stored [OidcToken.Id] session, and provider-bound helpers
* inside `authenticateWith(provider.sessions)` route bodies.
*
* @param P provider principal type exposed to the route.
*/
@KtorDsl
@OptIn(ExperimentalKtorApi::class)
public class OidcSessionsContext<P : Any> internal constructor(
default: SessionAuthenticatedContext<OidcToken.Id, P>,
private val provider: OidcProvider<P>,
) : SessionAuthenticatedContext<OidcToken.Id, P> by default, OidcProviderContext<P> {

override fun provider(): OidcProvider<P> = provider
}

/**
* Typed Bearer authentication scheme for an OpenID Connect provider.
*
* @param P provider principal type exposed to the route.
*/
@OptIn(ExperimentalKtorApi::class)
public typealias OidcBearerScheme<P> = DefaultAuthScheme<P, OidcBearerContext<P>>

/**
* Typed session authentication scheme for an OpenID Connect provider.
*
* @param P provider principal type exposed to the route.
*/
@OptIn(ExperimentalKtorApi::class)
public typealias OidcSessionsScheme<P> = SessionAuthScheme<OidcToken.Id, P, OidcSessionsContext<P>>
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.typesafe.*
import io.ktor.server.response.*
import io.ktor.server.sessions.serialization.*
import io.ktor.util.reflect.*
import io.ktor.utils.io.*
import kotlinx.coroutines.CancellationException
import kotlinx.serialization.json.Json
import org.slf4j.Logger

private const val AuthorizationHeaderLogLimit: Int = 96
Expand Down Expand Up @@ -63,6 +66,69 @@ internal fun <P : Any> OidcProvider<P>.createOauthFlow(): OAuth2Flow =
}
}

internal fun <P : Any> OidcProvider<P>.createSessions(
secure: Boolean
): OAuth2SessionFlow<OidcToken.Id, P, OidcSessionsContext<P>> {
val sessionJson = Json {
ignoreUnknownKeys = true
serializersModule = OidcToken.serializersModule
}

@OptIn(InternalAPI::class)
val sessionFlowConfig = OAuth2SessionConfig<OidcToken.Id, P, OidcSessionsContext<P>>().apply {
sessionConfig.name?.let { sessionName = it }

storage { scheme ->
cookie(scheme, storage = sessionConfig.storage) {
serializer = KotlinxSessionSerializer(
OidcToken.Id.serializer(),
format = sessionJson,
)
cookie.httpOnly = true
cookie.secure = secure
cookie.extensions["SameSite"] = "lax"
sessionConfig.cookieConfigure?.invoke(this)
}
}

sessionCreator = sessionCreator@{ oauthResponse ->
call.validateAuthorizationResponseIssuer(currentMetadata())
val response = requireNotNull(oauthResponse as? OAuthAccessTokenResponse.OAuth2) {
"Expected OAuth2 token response, but got: ${oauthResponse::class.simpleName}"
}
val oauthState = response.state ?: call.request.queryParameters["state"]
val authorizationTransaction = oauthState?.let {
call.consumeAuthorizationTransaction(stateCodec, it)
}

val token = buildOAuthToken(response, expectedNonce = authorizationTransaction?.nonce)
if (token !is OidcToken.Id) {
logger.debug("Received non-ID token, skipping session creation")
return@sessionCreator null
}
token
}

transformSession { refreshSessionIfNeeded(token = it) }

validate { transformPrincipal(token = it) }

sessionConfig.csrfConfigurer?.let { configure ->
csrfProtection(configure)
}

contextFactory = { default -> OidcSessionsContext(default, provider = this@createSessions) }
}

@OptIn(InternalAPI::class)
return OAuth2SessionFlow.from(
oauth = oauthFlow,
config = sessionFlowConfig,
principalType = principalType,
sessionTypeInfo = typeInfo<OidcToken.Id>(),
)
}

private fun OidcProvider<*>.oauthServerSettings(): OAuthServerSettings.OAuth2ServerSettings {
val config = oauthConfig
val metadata = currentMetadata()
Expand Down
Loading