Skip to content

Add OIDC token validation and Bearer auth#5639

Open
Pantus Oleh (zibet27) wants to merge 1 commit into
zibet27/oidc-provider-lifecyclefrom
zibet27/oidc-token-validation-bearer
Open

Add OIDC token validation and Bearer auth#5639
Pantus Oleh (zibet27) wants to merge 1 commit into
zibet27/oidc-provider-lifecyclefrom
zibet27/oidc-token-validation-bearer

Conversation

@zibet27

Copy link
Copy Markdown
Collaborator

Subsystem
Server Auth

Motivation
KTOR-5001 Add OpenID Connect (OIDC) support on client and server

Solution
Adds JWT-based OIDC access-token validation and typed Bearer authentication on top of the provider lifecycle.

This introduces

  • JWT/JWKS configuration
  • OIDCPrincipal and principal transformation hooks
  • Bearer auth contexts/schemes, and OidcProvider.bearer

OAuth login/callback handling, UserInfo, PKCE, sessions/logout, introspection, resource indicators, and protected resource metadata stay in later PRs.

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • main
  • release/*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dcf40ef9-f4c7-4f38-bd91-590d1bd47971

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces strongly-typed OpenID Connect authentication to Ktor by adding generic principal support, bearer JWT verification with JWK management, and comprehensive configuration for token validation and caching.

Changes

Typed OIDC Principal and Bearer Auth

Layer / File(s) Summary
Public API contract and build setup
api/ktor-server-auth-openid.api, build.gradle.kts, Oidc.kt (KDoc)
Updated Oidc.provider() to accept KClass and configuration lambdas; added OidcPrincipal hierarchy, TokenClaims, and context types to the public surface; configured build with context-parameters compiler flag and JWT/auth/CIO dependencies.
Configuration and principal model types
OidcConfig.kt, OidcPrincipal.kt
Introduced generic OidcProviderConfig<P> with JWT/access-token/bearer sub-configs; defined OidcPrincipal abstract base with IdToken/AccessToken subclasses and serializable UserInfo; implemented TokenClaims wrapper exposing JWT claims and header/payload access.
Provider typing and context infrastructure
Oidc.kt, OidcAuthenticatedContext.kt, OidcProvider.kt
Generalized provider registration and discovery to OidcProvider<P> with principal transformation; added OidcProviderContext<P> interface and OidcBearerContext<P> for typed route access; implemented JWK provider lifecycle with caching and rate-limit configuration.
Access token verification pipeline
OidcTokens.kt
Implemented verifyAccessToken(token) with algorithm validation, JWK selection via kid or fallback, JWK usability checks, and signature verification; structured error handling converts failures to rejections.
Bearer authentication scheme
OidcBearer.kt
Created createBearerScheme() extracting bearer tokens via optional TokenExtractor or standard header parsing, verifying and transforming principals, with trace-level logging for malformed headers and Bearer challenge responses.
Test utilities for OIDC
utils/OidcTestKeys.kt, utils/OidcTestLogging.kt
Added OpenIdTestKeys for RSA key generation and JWT signing; helpers for multi-key and operation-restricted JWK providers; log capture utility for provider-scoped logger validation.
Test coverage for typed principals and bearer auth
OidcBearerJwtTest.kt, OidcConfigValidationTest.kt, OidcJwkProviderConfigTest.kt, OidcJwtKeyAndAlgorithmTest.kt, OidcPluginRegistrationTest.kt, OidcDiscoveryTest.kt, OidcEnvironmentConfigTest.kt
Comprehensive tests validating bearer JWT rejection/acceptance, principal transformation, configuration validation (bearer requirements, jwkProviderFactory conflicts), JWK caching/rate-limiting behavior, algorithm allow-listing, and concurrent provider registration.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

👍 ship!

Suggested reviewers

  • osipxd
  • e5l
  • nomisRev
  • bjhham
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add OIDC token validation and Bearer auth' directly summarizes the main changes: JWT-based token validation and Bearer authentication for OpenID Connect providers.
Description check ✅ Passed The description follows the template structure with Subsystem, Motivation (referencing KTOR-5001), and Solution sections that clearly explain the JWT/JWKS configuration, OIDCPrincipal, and Bearer auth additions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch zibet27/oidc-token-validation-bearer

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zibet27

Copy link
Copy Markdown
Collaborator Author

CodeRabbit (@coderabbitai) review

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/src/io/ktor/server/auth/openid/OidcProvider.kt (1)

60-92: 💤 Low value

Consider a more descriptive error message.

Line 88's error message "Invalid principal type." doesn't include the expected vs actual type, which would help debugging.

Suggested improvement
         check(config.principalType.isInstance(principal)) {
-            "Invalid principal type."
+            "Principal type ${principal::class.simpleName} is not assignable to ${config.principalType.simpleName}"
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/src/io/ktor/server/auth/openid/OidcProvider.kt`
around lines 60 - 92, Update the error message in transformPrincipal (in
OidcProvider.transformPrincipal) to include the expected principal type from
config.principalType and the actual runtime type of principal when throwing;
replace the current check(config.principalType.isInstance(principal)) { "Invalid
principal type." } with a message that interpolates config.principalType (or its
name) and principal::class (or principal.javaClass) so the thrown
IllegalStateException shows both expected and actual types to aid debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/src/io/ktor/server/auth/openid/OidcBearer.kt`:
- Around line 40-42: The trace log inside the onFailure block in OidcBearer.kt
currently only logs the provider name; update the logger.trace call in the
onFailure { ... } handler to include the caught exception (it) so the exception
message/stacktrace is recorded (use the logger.trace overload that accepts a
Throwable or include the throwable in the log arguments) for better debugging of
token validation failures.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/test/io/ktor/server/auth/openid/OidcJwkProviderConfigTest.kt`:
- Around line 169-171: Replace the TOCTOU port allocation that uses
ServerSocket(0) and then binds embeddedServer to that port; instead start
embeddedServer with port = 0 and after starting call server.start() (or use
server.start(false) as appropriate) then read the actual bound port via
server.engine.resolvedConnectors().first().port; update both places that create
the test server (the blocks around the ServerSocket usage in
OidcJwkProviderConfigTest where embeddedServer(...) is constructed) to use port
= 0 and to retrieve the bound port from
server.engine.resolvedConnectors().first().port after server start.

---

Nitpick comments:
In
`@ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/src/io/ktor/server/auth/openid/OidcProvider.kt`:
- Around line 60-92: Update the error message in transformPrincipal (in
OidcProvider.transformPrincipal) to include the expected principal type from
config.principalType and the actual runtime type of principal when throwing;
replace the current check(config.principalType.isInstance(principal)) { "Invalid
principal type." } with a message that interpolates config.principalType (or its
name) and principal::class (or principal.javaClass) so the thrown
IllegalStateException shows both expected and actual types to aid debugging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 578ef392-02a9-4b0f-bacb-90e4d70811f7

📥 Commits

Reviewing files that changed from the base of the PR and between 5b3c889 and cdfd0df.

📒 Files selected for processing (18)
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/api/ktor-server-auth-openid.api
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/build.gradle.kts
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/src/io/ktor/server/auth/openid/Oidc.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/src/io/ktor/server/auth/openid/OidcAuthenticatedContext.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/src/io/ktor/server/auth/openid/OidcBearer.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/src/io/ktor/server/auth/openid/OidcConfig.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/src/io/ktor/server/auth/openid/OidcPrincipal.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/src/io/ktor/server/auth/openid/OidcProvider.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/src/io/ktor/server/auth/openid/OidcTokens.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/test/io/ktor/server/auth/openid/OidcBearerJwtTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/test/io/ktor/server/auth/openid/OidcConfigValidationTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/test/io/ktor/server/auth/openid/OidcDiscoveryTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/test/io/ktor/server/auth/openid/OidcEnvironmentConfigTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/test/io/ktor/server/auth/openid/OidcJwkProviderConfigTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/test/io/ktor/server/auth/openid/OidcJwtKeyAndAlgorithmTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/test/io/ktor/server/auth/openid/OidcPluginRegistrationTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/test/io/ktor/server/auth/openid/utils/OidcTestKeys.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/test/io/ktor/server/auth/openid/utils/OidcTestLogging.kt

Comment on lines +40 to +42
}.onFailure {
logger.trace("OpenID access token authentication failed for provider {}", name)
}.getOrNull()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Include the exception in trace log for debugging.

The onFailure block has access to the exception (it) but it's not logged. Without the exception details, debugging token validation failures (expired token, wrong issuer, invalid signature, etc.) becomes difficult.

🔧 Proposed fix
             }.onFailure {
-                logger.trace("OpenID access token authentication failed for provider {}", name)
+                logger.trace("OpenID access token authentication failed for provider {}", name, it)
             }.getOrNull()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}.onFailure {
logger.trace("OpenID access token authentication failed for provider {}", name)
}.getOrNull()
}.onFailure {
logger.trace("OpenID access token authentication failed for provider {}", name, it)
}.getOrNull()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/src/io/ktor/server/auth/openid/OidcBearer.kt`
around lines 40 - 42, The trace log inside the onFailure block in OidcBearer.kt
currently only logs the provider name; update the logger.trace call in the
onFailure { ... } handler to include the caught exception (it) so the exception
message/stacktrace is recorded (use the logger.trace overload that accepts a
Throwable or include the throwable in the log arguments) for better debugging of
token validation failures.

Comment on lines +169 to +171
val port = ServerSocket(0).use { it.localPort }
val server = embeddedServer(CIO, port = port) {
routing {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify current TOCTOU pattern and discover established connector-access API usage in repo.
rg -n --type=kt -C2 'ServerSocket\(0\)\.use \{ it\.localPort \}'
rg -n --type=kt -C2 'resolvedConnectors'
rg -n --type=kt -C2 'embeddedServer\([^)]*port\s*=\s*0'

Repository: ktorio/ktor

Length of output: 82


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the test file to verify the current code at lines 169-171 and 184
echo "=== Current test file content (lines 160-200) ==="
sed -n '160,200p' ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/test/io/ktor/server/auth/openid/OidcJwkProviderConfigTest.kt

echo ""
echo "=== Searching for resolvedConnectors usage ==="
rg -n 'resolvedConnectors' --glob='*.kt' -A2 -B2

echo ""
echo "=== Searching for ServerSocket(0) pattern ==="
rg -n 'ServerSocket\(0\)' --glob='*.kt' -A2 -B2

echo ""
echo "=== Searching for embeddedServer with port = 0 pattern ==="
rg -n 'embeddedServer.*port\s*=\s*0' --glob='*.kt' -A5 -B2

Repository: ktorio/ktor

Length of output: 34956


Fix TOCTOU port allocation in test server setup.

Using ServerSocket(0) to allocate a port, then binding embeddedServer to that port, creates a race condition where another process can claim the port between release and bind, causing intermittent test failures. Instead, bind embeddedServer directly to port = 0, then retrieve the actual bound port from server.engine.resolvedConnectors().first().port after the server starts.

Suggested change
-        val port = ServerSocket(0).use { it.localPort }
-        val server = embeddedServer(CIO, port = port) {
+        val server = embeddedServer(CIO, port = 0) {
             routing {
                 get("/jwks") {
                     fetchCount.incrementAndGet()
                     call.respondText(
                         text = keys.jwksJson(),
                         status = HttpStatusCode.OK,
                         contentType = ContentType.Application.Json,
                     )
                 }
             }
         }.start(wait = false)
+        val port = server.engine.resolvedConnectors().first().port

Apply to both occurrences at lines 169–171 and 184.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val port = ServerSocket(0).use { it.localPort }
val server = embeddedServer(CIO, port = port) {
routing {
val server = embeddedServer(CIO, port = 0) {
routing {
get("/jwks") {
fetchCount.incrementAndGet()
call.respondText(
text = keys.jwksJson(),
status = HttpStatusCode.OK,
contentType = ContentType.Application.Json,
)
}
}
}.start(wait = false)
val port = server.engine.resolvedConnectors().first().port
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-openid/jvm/test/io/ktor/server/auth/openid/OidcJwkProviderConfigTest.kt`
around lines 169 - 171, Replace the TOCTOU port allocation that uses
ServerSocket(0) and then binds embeddedServer to that port; instead start
embeddedServer with port = 0 and after starting call server.start() (or use
server.start(false) as appropriate) then read the actual bound port via
server.engine.resolvedConnectors().first().port; update both places that create
the test server (the blocks around the ServerSocket usage in
OidcJwkProviderConfigTest where embeddedServer(...) is constructed) to use port
= 0 and to retrieve the bound port from
server.engine.resolvedConnectors().first().port after server start.

@e5l Leonid Stashevsky (e5l) removed their request for review June 1, 2026 08:59
@zibet27 Pantus Oleh (zibet27) force-pushed the zibet27/oidc-provider-lifecycle branch from 6767cec to 8faf94e Compare June 10, 2026 14:26
@zibet27 Pantus Oleh (zibet27) force-pushed the zibet27/oidc-token-validation-bearer branch from cdfd0df to 3763385 Compare June 10, 2026 15:51
@zibet27 Pantus Oleh (zibet27) force-pushed the zibet27/oidc-token-validation-bearer branch from 3763385 to 6e1c5f4 Compare June 11, 2026 08:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant