Skip to content

Commit 3d1a2d1

Browse files
committed
Release 0.5.0: Refactor session storage and client validation DSL
Major changes: - Extract session storage to ktor-bearer-sessions module - Refactor client validation into clients {} DSL block - registration {} for public clients via RFC 7591 dynamic registration - credentials {} for confidential clients at /token (RFC 6749) - Move credentials validation from /authorize to /token per RFC 6749 - Simplify provision flow with native Ktor routing (call.provision) - Add direct session access for SSE/WebSocket contexts - Refactor claims: use AesGcmEncryption, simplify ProvisionClaims - Separate flow identity vs client identity architecture Breaking changes: - Session storage APIs moved to ktor-bearer-sessions - Client validation now uses clients { registration, credentials } DSL - Provision route setup uses call.provision instead of custom DSL
1 parent 97717af commit 3d1a2d1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+937
-1490
lines changed

README.md

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Many applications need user-provided configuration that OAuth alone can't handle
6060

6161
```kotlin
6262
dependencies {
63-
implementation("com.vcontrol:ktor-server-oauth:0.4.10")
63+
implementation("com.vcontrol:ktor-server-oauth:0.5.0")
6464
}
6565
```
6666

@@ -76,8 +76,10 @@ import io.ktor.server.routing.*
7676
fun Application.module() {
7777
// 1. Install OAuth plugin with local authorization server
7878
install(OAuth) {
79-
authorizationServer(LocalAuthServer) {
80-
openRegistration = true
79+
server {
80+
clients {
81+
registration = true // Accept all registrations
82+
}
8183
}
8284
}
8385

@@ -131,25 +133,72 @@ The plugin automatically configures these endpoints:
131133
| `GET /.well-known/oauth-authorization-server` | Authorization server metadata |
132134
| `GET /.well-known/oauth-protected-resource` | Protected resource metadata |
133135

136+
## Error Responses
137+
138+
All error responses follow [RFC 6749 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2):
139+
140+
```json
141+
{
142+
"error": "invalid_grant",
143+
"error_description": "Invalid or expired authorization code"
144+
}
145+
```
146+
147+
| Error Code | HTTP Status | When |
148+
|------------|-------------|------|
149+
| `invalid_request` | 400 | Missing or malformed parameters |
150+
| `invalid_client` | 401 | Bad client_id or client_secret |
151+
| `invalid_grant` | 400 | Expired/invalid auth code, PKCE failure, redirect_uri mismatch |
152+
| `unsupported_grant_type` | 400 | Grant type not enabled |
153+
154+
For `invalid_client`, the response includes a `WWW-Authenticate: Bearer` header per RFC 6750.
155+
156+
Protected routes (behind `authenticate { }`) return `401 Unauthorized` with `WWW-Authenticate: Bearer` for missing/invalid/expired tokens - this is standard Ktor JWT authentication behavior.
157+
158+
## Defaults
159+
160+
| Setting | Default | Notes |
161+
|---------|---------|----------------------------------------|
162+
| Token expiration | 90 days | JWT `exp` claim |
163+
| Session TTL | 90 days | Matches token lifetime |
164+
| Session storage | File-based | `~/.ktor-oauth/sessions/` |
165+
| Session encryption | Enabled | AES-256-GCM with per-client key in JWT |
166+
| Session cleanup | Enabled, 1 hour | Removes expired session files |
167+
| Route prefix | `/.oauth` | All internal endpoints under this path |
168+
| Auth code storage | In-memory | Lost on restart (stateless by design) |
169+
170+
**Security notes:**
171+
- JWT signing key is auto-generated and stored at `~/.ktor-oauth/jwt.secret`
172+
- Session encryption keys are embedded in each JWT token, so sessions can only be decrypted by the token holder
173+
- Without cleanup, session files accumulate on disk (one file per session per type)
174+
134175
## Configuration
135176

136177
### Plugin Configuration
137178

138179
```kotlin
139180
install(OAuth) {
140-
authorizationServer(LocalAuthServer) {
141-
// Enable/disable dynamic client registration
142-
openRegistration = true
181+
server {
182+
// Client validation
183+
clients {
184+
// Dynamic registration (RFC 7591)
185+
// Has access to: origin, headers, resource, request
186+
registration = true // or:
187+
// registration { clientId, clientName ->
188+
// origin.remoteHost in allowedIps
189+
// }
190+
191+
// Client credentials grant
192+
// Has access to: origin, headers, resource, request
193+
credentials { clientId, secret ->
194+
origin.remoteHost !in blockedIps && db.check(clientId, secret)
195+
}
196+
// Or static: credentials("app" to "secret", "app2" to "secret2")
197+
}
143198

144199
// Token lifetime
145200
tokenExpiration = 90.days
146201

147-
// Global client blocklist
148-
client { clientId -> clientId !in blockedClients }
149-
150-
// Client credentials grant validation
151-
clientCredentials { id, secret -> validateCredentials(id, secret) }
152-
153202
// Custom JWT claims
154203
claims(SessionKeyClaimsProvider)
155204
claims { builder, clientId ->
@@ -223,8 +272,46 @@ Or via application.conf:
223272
oauth.sessions.cleanup.interval = "PT1H"
224273
```
225274

226-
Cleanup is disabled by default. The job runs on the configured interval and properly
227-
shuts down when the application stops.
275+
Cleanup is enabled by default (1 hour interval). The job runs on the configured interval
276+
and properly shuts down when the application stops.
277+
278+
### Relationship with Ktor Sessions
279+
280+
`OAuthSessions` internally installs Ktor's `Sessions` plugin - do not install both.
281+
282+
```kotlin
283+
// ✅ Correct - OAuthSessions manages Sessions internally
284+
install(OAuthSessions) {
285+
session<MySession>()
286+
}
287+
288+
// ❌ Wrong - will conflict
289+
install(Sessions) { ... } // Don't do this
290+
install(OAuthSessions) { ... }
291+
```
292+
293+
**What OAuthSessions provides:**
294+
295+
- Bearer-bound sessions (`session<T>()`) - stored server-side, keyed by JWT claims
296+
- Standard Ktor sessions (`cookie<T>()`, `header<T>()`) - available through the same DSL
297+
- Internal OAuth cookies (auth_request, provision_session)
298+
299+
**Mixing session types:**
300+
301+
```kotlin
302+
install(OAuthSessions) {
303+
// Bearer-bound session (server-side, keyed by JWT)
304+
session<UserPreferences>()
305+
306+
// Standard Ktor cookie session (for non-OAuth flows)
307+
cookie<AdminSession>("admin_session") {
308+
cookie.httpOnly = true
309+
cookie.secure = true
310+
}
311+
}
312+
```
313+
314+
Both session types use `call.sessions.get<T>()` / `call.sessions.set<T>()` - the transport is determined by how they're registered.
228315

229316
### application.conf
230317

build.gradle.kts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
plugins {
22
id("buildsrc.convention.kotlin-jvm")
33
`java-library`
4-
alias(libs.plugins.kotlinPluginSerialization)
4+
alias(libs.plugins.kotlin.plugin.serialization)
55
id("com.vanniktech.maven.publish") version "0.30.0"
66
}
77

@@ -11,38 +11,41 @@ version = file("version.properties")
1111

1212
dependencies {
1313
// Ktor BOM for version alignment
14-
api(platform(libs.ktorBom))
14+
api(platform(libs.ktor.bom))
1515

1616
// Coroutines
17-
implementation(libs.kotlinxCoroutines)
17+
implementation(libs.kotlinx.coroutines)
1818

1919
// Kotlin serialization
20-
implementation(libs.kotlinxSerialization)
20+
implementation(libs.kotlinx.serialization)
2121

2222
// Ktor server
23-
implementation(libs.ktorServerCore)
24-
api(libs.ktorServerAuth)
25-
api(libs.ktorServerAuthJwt)
26-
implementation(libs.ktorServerContentNegotiation)
27-
implementation(libs.ktorSerializationJson)
28-
implementation(libs.ktorServerStatusPages)
29-
implementation(libs.ktorServerForwardedHeader)
30-
api(libs.ktorServerSessions)
23+
implementation(libs.ktor.server.core)
24+
api(libs.ktor.server.auth)
25+
api(libs.ktor.server.auth.jwt)
26+
implementation(libs.ktor.server.content.negotiation)
27+
implementation(libs.ktor.serialization.json)
28+
implementation(libs.ktor.server.status.pages)
29+
implementation(libs.ktor.server.forwarded.header)
30+
api(libs.ktor.server.sessions)
31+
32+
// Bearer sessions (shared session storage infrastructure)
33+
api("com.vcontrol:ktor-bearer-sessions:0.2.0")
3134

3235
// JWT
33-
implementation(libs.javaJwt)
36+
implementation(libs.java.jwt)
3437

3538
// Logging
36-
api(libs.kotlinLogging)
39+
api(libs.kotlin.logging)
3740

3841
// Test dependencies
3942
testImplementation(kotlin("test"))
40-
testImplementation(libs.kotlinxCoroutines)
41-
testImplementation(libs.ktorServerTests)
42-
testImplementation(libs.ktorServerCio)
43-
testImplementation(libs.ktorClientContentNegotiation)
44-
testImplementation(libs.ktorSerializationJson)
45-
testImplementation(libs.slf4jSimple)
43+
testImplementation(libs.kotlinx.coroutines)
44+
testImplementation(libs.ktor.server.tests)
45+
testImplementation(libs.ktor.server.cio)
46+
testImplementation(libs.ktor.client.content.negotiation)
47+
testImplementation(libs.ktor.serialization.json)
48+
testImplementation(libs.slf4j.simple)
4649
}
4750

4851
// Generate version file for runtime access

buildSrc/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ kotlin {
1111
}
1212

1313
dependencies {
14-
implementation(libs.kotlinGradlePlugin)
14+
implementation(libs.kotlin.gradle.plugin)
1515
}

gradle/libs.versions.toml

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,41 @@
11
[versions]
22
kotlin = "2.2.20"
3-
kotlinxSerializationJSON = "1.8.1"
4-
kotlinxCoroutines = "1.10.2"
3+
kotlinx-serialization-json = "1.8.1"
4+
kotlinx-coroutines = "1.10.2"
55
ktor = "3.3.0"
6-
kotlinLogging = "7.0.3"
7-
javaJwt = "4.4.0"
6+
kotlin-logging = "7.0.3"
7+
java-jwt = "4.4.0"
88
slf4j = "2.0.16"
99

1010
[libraries]
11-
kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
12-
kotlinxSerialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJSON" }
13-
kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
11+
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
12+
kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
13+
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
1414

1515
# Ktor BOM for version alignment
16-
ktorBom = { module = "io.ktor:ktor-bom", version.ref = "ktor" }
16+
ktor-bom = { module = "io.ktor:ktor-bom", version.ref = "ktor" }
1717

1818
# Ktor Server
19-
ktorServerCore = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
20-
ktorServerCio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
21-
ktorServerAuth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" }
22-
ktorServerAuthJwt = { module = "io.ktor:ktor-server-auth-jwt", version.ref = "ktor" }
23-
ktorServerContentNegotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }
24-
ktorSerializationJson = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
25-
ktorServerStatusPages = { module = "io.ktor:ktor-server-status-pages", version.ref = "ktor" }
26-
ktorServerForwardedHeader = { module = "io.ktor:ktor-server-forwarded-header", version.ref = "ktor" }
27-
ktorServerSessions = { module = "io.ktor:ktor-server-sessions", version.ref = "ktor" }
28-
ktorServerTests = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
19+
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
20+
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
21+
ktor-server-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" }
22+
ktor-server-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt", version.ref = "ktor" }
23+
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }
24+
ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
25+
ktor-server-status-pages = { module = "io.ktor:ktor-server-status-pages", version.ref = "ktor" }
26+
ktor-server-forwarded-header = { module = "io.ktor:ktor-server-forwarded-header", version.ref = "ktor" }
27+
ktor-server-sessions = { module = "io.ktor:ktor-server-sessions", version.ref = "ktor" }
28+
ktor-server-tests = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
2929

3030
# Ktor Client (for tests)
31-
ktorClientContentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
31+
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
3232

3333
# JWT
34-
javaJwt = { module = "com.auth0:java-jwt", version.ref = "javaJwt" }
34+
java-jwt = { module = "com.auth0:java-jwt", version.ref = "java-jwt" }
3535

3636
# Logging
37-
slf4jSimple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
38-
kotlinLogging = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = "kotlinLogging" }
37+
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
38+
kotlin-logging = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = "kotlin-logging" }
3939

4040
[plugins]
41-
kotlinPluginSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
41+
kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
dependencyResolutionManagement {
22
@Suppress("UnstableApiUsage")
33
repositories {
4+
mavenLocal()
45
mavenCentral()
56
}
67
}

0 commit comments

Comments
 (0)