Skip to content

Commit aaaf1e9

Browse files
authored
Merge pull request #987 from shev-pro/dynamic_persession_ttl
Dynamic Session TTL Configuration for Issuance and Verification
2 parents 957faa3 + 570ccbe commit aaaf1e9

File tree

16 files changed

+183
-53
lines changed

16 files changed

+183
-53
lines changed

waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/interfaces/ISessionCache.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ interface ISessionCache<T> {
88

99
/**
1010
* Puts the specified [session] with the specified [id] in the session cache.
11+
* @param id The ID to store the session under
12+
* @param session The session to store
13+
* @param ttl Optional custom time-to-live duration for this session.
14+
* If null, the default expiration is used.
1115
* @return the previous value associated with the [id], or `null` if the key was not present in the cache.
1216
*/
13-
fun putSession(id: String, session: T)
17+
fun putSession(id: String, session: T, ttl: kotlin.time.Duration? = null)
1418

1519
fun getSessionByAuthServerState(authServerState: String): T?
1620
/**

waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/OpenIDCredentialVerifier.kt

+15-4
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ abstract class OpenIDCredentialVerifier(val config: CredentialVerifierConfig) :
4646
clientIdScheme: ClientIdScheme = config.defaultClientIdScheme,
4747
openId4VPProfile: OpenId4VPProfile = OpenId4VPProfile.DEFAULT,
4848
walletInitiatedAuthState: String? = null,
49-
trustedRootCAs: List<String>? = null
49+
trustedRootCAs: List<String>? = null,
50+
sessionTtl: Duration? = null // Custom TTL duration for the session
5051
): PresentationSession {
5152
val session = PresentationSession(
5253
id = sessionId ?: ShortIdUtils.randomSessionId(),
@@ -58,7 +59,7 @@ abstract class OpenIDCredentialVerifier(val config: CredentialVerifierConfig) :
5859
openId4VPProfile = openId4VPProfile,
5960
trustedRootCAs = trustedRootCAs
6061
).also {
61-
putSession(it.id, it)
62+
putSession(it.id, it, sessionTtl ?: expiresIn)
6263
}
6364
val presentationDefinitionUri = when(openId4VPProfile) {
6465
OpenId4VPProfile.ISO_18013_7_MDOC, OpenId4VPProfile.HAIP -> null
@@ -108,18 +109,28 @@ abstract class OpenIDCredentialVerifier(val config: CredentialVerifierConfig) :
108109
nonce = Uuid.random().toString()
109110
)
110111
return session.copy(authorizationRequest = authReq).also {
111-
putSession(session.id, it)
112+
putSession(session.id, it, sessionTtl ?: expiresIn)
112113
}
113114
}
114115

115116
open fun verify(tokenResponse: TokenResponse, session: PresentationSession): PresentationSession {
116117
// https://json-schema.org/specification
117118
// https://github.com/OptimumCode/json-schema-validator
119+
// Calculate the remaining time to live based on the session's expiration timestamp
120+
val remainingTtl = session.expirationTimestamp?.let {
121+
val now = Clock.System.now()
122+
if (it > now) {
123+
it - now // Calculate duration between now and expiration
124+
} else {
125+
null // Already expired
126+
}
127+
}
128+
118129
return session.copy(
119130
tokenResponse = tokenResponse,
120131
verificationResult = doVerify(tokenResponse, session)
121132
).also {
122-
putSession(it.id, it)
133+
putSession(it.id, it, remainingTtl)
123134
}
124135
}
125136

waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/EBSITestWallet.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import io.ktor.serialization.kotlinx.json.*
4242
import kotlinx.coroutines.runBlocking
4343
import kotlinx.datetime.Instant
4444
import kotlinx.serialization.json.*
45+
import kotlin.time.Duration
4546
import kotlin.uuid.ExperimentalUuidApi
4647
import kotlin.uuid.Uuid
4748

@@ -225,7 +226,7 @@ class EBSITestWallet(
225226
)
226227
}
227228

228-
override fun putSession(id: String, session: SIOPSession) {
229+
override fun putSession(id: String, session: SIOPSession, ttl: Duration?) {
229230
sessionCache[id] = session
230231
}
231232

waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/TestCredentialWallet.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import io.ktor.util.*
5252
import kotlinx.coroutines.runBlocking
5353
import kotlinx.datetime.Instant
5454
import kotlinx.serialization.json.*
55+
import kotlin.time.Duration
5556

5657
const val WALLET_PORT = 8001
5758
const val WALLET_BASE_URL = "http://localhost:${WALLET_PORT}"
@@ -241,7 +242,7 @@ class TestCredentialWallet(
241242
TODO("Not yet implemented")
242243
}
243244

244-
override fun putSession(id: String, session: SIOPSession) {
245+
override fun putSession(id: String, session: SIOPSession, ttl: Duration?) {
245246
sessionCache[id] = session
246247
}
247248

waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/VPTestVerifier.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import kotlinx.coroutines.runBlocking
2222
import kotlinx.serialization.json.Json
2323
import kotlinx.serialization.json.jsonObject
2424
import kotlinx.serialization.json.jsonPrimitive
25+
import kotlin.time.Duration
2526

2627
const val VP_VERIFIER_PORT = 8002
2728
const val VP_VERIFIER_BASE_URL = "http://localhost:$VP_VERIFIER_PORT"
@@ -34,7 +35,7 @@ class VPTestVerifier : OpenIDCredentialVerifier(
3435
private val presentationDefinitionCache = mutableMapOf<String, PresentationDefinition>()
3536
override fun getSession(id: String): PresentationSession? = sessionCache[id]
3637

37-
override fun putSession(id: String, session: PresentationSession) {
38+
override fun putSession(id: String, session: PresentationSession, ttl: Duration?) {
3839
sessionCache[id] = session
3940
}
4041

waltid-libraries/waltid-core-wallet/src/jvmMain/java/id/walt/wallet/core/service/oidc4vc/TestCredentialWallet.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ class TestCredentialWallet(
383383
TODO("Not yet implemented")
384384
}
385385

386-
override fun putSession(id: String, session: VPresentationSession) {
386+
override fun putSession(id: String, session: VPresentationSession, ttl: Duration?) {
387387
sessionCache[id] = session
388388
}
389389

waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt

+16-6
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,9 @@ open class CIProvider(
154154
}
155155

156156

157-
fun putSession(id: String, session: IssuanceSession) {
157+
fun putSession(id: String, session: IssuanceSession, ttl: Duration? = null) {
158158
log.debug { "SETTING CI AUTH SESSION: $id = $session" }
159-
authSessions[id] = session
159+
authSessions.set(id, session, ttl)
160160
}
161161

162162
fun removeSession(id: String) {
@@ -444,10 +444,20 @@ open class CIProvider(
444444
}
445445

446446
private fun generateProofOfPossessionNonceFor(session: IssuanceSession): IssuanceSession {
447+
// Calculate remaining TTL based on session expiration
448+
val remainingTtl = session.expirationTimestamp?.let {
449+
val now = Clock.System.now()
450+
if (it > now) {
451+
it - now // Calculate duration between now and expiration
452+
} else {
453+
null // Already expired
454+
}
455+
}
456+
447457
return session.copy(
448458
cNonce = randomUUID()
449459
).also {
450-
putSession(it.id, it)
460+
putSession(it.id, it, remainingTtl)
451461
}
452462
}
453463

@@ -496,7 +506,7 @@ open class CIProvider(
496506
val updatedSession = IssuanceSession(
497507
id = it.id,
498508
authorizationRequest = authorizationRequest,
499-
expirationTimestamp = Clock.System.now().plus(5.minutes),
509+
expirationTimestamp = Clock.System.now().plus(expiresIn),
500510
issuanceRequests = it.issuanceRequests,
501511
authServerState = authServerState,
502512
txCode = it.txCode,
@@ -506,7 +516,7 @@ open class CIProvider(
506516
callbackUrl = it.callbackUrl,
507517
customParameters = it.customParameters
508518
)
509-
putSession(it.id, updatedSession)
519+
putSession(it.id, updatedSession, expiresIn)
510520
}
511521
}
512522

@@ -544,7 +554,7 @@ open class CIProvider(
544554
credentialOffer = credentialOfferBuilder.build(),
545555
callbackUrl = callbackUrl
546556
).also {
547-
putSession(it.id, it)
557+
putSession(it.id, it, expiresIn)
548558
}
549559
}
550560

waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/IssuerApi.kt

+25-6
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ import kotlinx.serialization.json.jsonPrimitive
2626
import kotlin.reflect.KClass
2727
import kotlin.time.Duration
2828
import kotlin.time.Duration.Companion.minutes
29+
import kotlin.time.Duration.Companion.seconds
2930

3031
private val logger = KotlinLogging.logger {}
3132
suspend fun createCredentialOfferUri(
3233
issuanceRequests: List<IssuanceRequest>,
3334
credentialFormat: CredentialFormat,
3435
callbackUrl: String? = null,
3536
expiresIn: Duration = 5.minutes,
37+
sessionTtl: Duration? = null,
3638
): String {
3739
val overwrittenIssuanceRequests = issuanceRequests.map {
3840
it.copy(
@@ -44,7 +46,7 @@ suspend fun createCredentialOfferUri(
4446

4547
val issuanceSession = OidcApi.initializeCredentialOffer(
4648
issuanceRequests = overwrittenIssuanceRequests,
47-
expiresIn = expiresIn,
49+
expiresIn = sessionTtl ?: expiresIn,
4850
callbackUrl = callbackUrl,
4951
standardVersion = overwrittenIssuanceRequests.first().standardVersion!!
5052
)
@@ -211,8 +213,15 @@ fun Application.issuerApi() {
211213
description = "Callback to push state changes of the issuance process to"
212214
required = false
213215
}
216+
217+
fun OpenApiRequest.sessionTtlHeader() = headerParameter<Long>("sessionTtl") {
218+
description = "Custom session time-to-live in seconds"
219+
required = false
220+
}
214221

215222
fun RoutingContext.getCallbackUriHeader() = call.request.header("statusCallbackUri")
223+
224+
fun RoutingContext.getSessionTtl() = call.request.header("sessionTtl")?.toLongOrNull()?.let { it.seconds }
216225

217226
route("raw") {
218227
route("jwt") {
@@ -281,6 +290,7 @@ fun Application.issuerApi() {
281290

282291
request {
283292
statusCallbackUriHeader()
293+
sessionTtlHeader()
284294
body<IssuanceRequest> {
285295
description =
286296
"Pass the unsigned credential that you intend to issue as the body of the request."
@@ -343,7 +353,8 @@ fun Application.issuerApi() {
343353
listOf(jwtIssuanceRequest),
344354
getFormatByCredentialConfigurationId(jwtIssuanceRequest.credentialConfigurationId)
345355
?: throw IllegalArgumentException("Invalid Credential Configuration Id"),
346-
getCallbackUriHeader()
356+
getCallbackUriHeader(),
357+
sessionTtl = getSessionTtl()
347358
)
348359
call.respond(HttpStatusCode.OK, offerUri)
349360
}
@@ -354,6 +365,7 @@ fun Application.issuerApi() {
354365

355366
request {
356367
statusCallbackUriHeader()
368+
sessionTtlHeader()
357369
body<List<IssuanceRequest>> {
358370
description =
359371
"Pass the unsigned credential that you intend to issue as the body of the request."
@@ -379,7 +391,8 @@ fun Application.issuerApi() {
379391
issuanceRequests,
380392
getFormatByCredentialConfigurationId(issuanceRequests.first().credentialConfigurationId)
381393
?: throw IllegalArgumentException("Invalid Credential Configuration Id"),
382-
getCallbackUriHeader()
394+
getCallbackUriHeader(),
395+
sessionTtl = getSessionTtl()
383396
)
384397
logger.debug { "Offer URI: $offerUri" }
385398
call.respond(HttpStatusCode.OK, offerUri)
@@ -394,6 +407,7 @@ fun Application.issuerApi() {
394407

395408
request {
396409
statusCallbackUriHeader()
410+
sessionTtlHeader()
397411
body<IssuanceRequest> {
398412
description =
399413
"Pass the unsigned credential that you intend to issue in the body of the request."
@@ -426,7 +440,8 @@ fun Application.issuerApi() {
426440
listOf(sdJwtIssuanceRequest),
427441
getFormatByCredentialConfigurationId(sdJwtIssuanceRequest.credentialConfigurationId)
428442
?: throw IllegalArgumentException("Invalid Credential Configuration Id"),
429-
getCallbackUriHeader()
443+
getCallbackUriHeader(),
444+
sessionTtl = getSessionTtl()
430445
)
431446

432447
call.respond(
@@ -441,6 +456,7 @@ fun Application.issuerApi() {
441456

442457
request {
443458
statusCallbackUriHeader()
459+
sessionTtlHeader()
444460
body<List<IssuanceRequest>> {
445461
description =
446462
"Pass the unsigned credential that you intend to issue as the body of the request."
@@ -467,7 +483,8 @@ fun Application.issuerApi() {
467483
sdJwtIssuanceRequests,
468484
getFormatByCredentialConfigurationId(sdJwtIssuanceRequests.first().credentialConfigurationId)
469485
?: throw IllegalArgumentException("Invalid Credential Configuration Id"),
470-
getCallbackUriHeader()
486+
getCallbackUriHeader(),
487+
sessionTtl = getSessionTtl()
471488
)
472489

473490
logger.debug { "Offer URI: $offerUri" }
@@ -484,6 +501,7 @@ fun Application.issuerApi() {
484501
description = "This endpoint issues a mdoc and returns an issuance URL "
485502
request {
486503
statusCallbackUriHeader()
504+
sessionTtlHeader()
487505
body<IssuanceRequest> {
488506
description =
489507
"Pass the unsigned credential that you intend to issue as the body of the request."
@@ -498,7 +516,8 @@ fun Application.issuerApi() {
498516
listOf(mdocIssuanceRequest),
499517
getFormatByCredentialConfigurationId(mdocIssuanceRequest.credentialConfigurationId)
500518
?: throw IllegalArgumentException("Invalid Credential Configuration Id"),
501-
getCallbackUriHeader()
519+
getCallbackUriHeader(),
520+
sessionTtl = getSessionTtl()
502521
)
503522

504523
call.respond(

waltid-services/waltid-service-commons/src/main/kotlin/id/walt/commons/persistence/ConfiguredPersistence.kt

+21-2
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,27 @@ class ConfiguredPersistence<V : Any>(
4848
override fun getAll(): Sequence<V> = underlyingPersistence.getAll()
4949
override fun listSize(id: String): Int = underlyingPersistence.listSize(id)
5050

51-
override fun listAdd(id: String, value: V) = underlyingPersistence.listAdd(id, value)
51+
/**
52+
* Add a value to a list with a specified or default expiration.
53+
* @param id The key of the list
54+
* @param value The value to add
55+
* @param ttl Optional expiration duration. If null, defaultExpiration will be used
56+
*/
57+
override fun listAdd(id: String, value: V, ttl: Duration?) = underlyingPersistence.listAdd(id, value, ttl)
5258

53-
override fun set(id: String, value: V) = underlyingPersistence.set(id, value)
59+
/**
60+
* Store a value with the default expiration.
61+
* @param id The key to store the value under
62+
* @param value The value to store
63+
*/
64+
override operator fun set(id: String, value: V) { underlyingPersistence[id] = value }
65+
66+
/**
67+
* Store a value with a specified expiration.
68+
* @param id The key to store the value under
69+
* @param value The value to store
70+
* @param ttl Expiration duration. If null, defaultExpiration will be used
71+
*/
72+
override fun set(id: String, value: V, ttl: Duration?) = underlyingPersistence.set(id, value, ttl)
5473

5574
}

waltid-services/waltid-service-commons/src/main/kotlin/id/walt/commons/persistence/InMemoryPersistence.kt

+12-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ class InMemoryPersistence<V : Any>(discriminator: String, defaultExpiration: Dur
1313
.expireAfterWrite(defaultExpiration)
1414
.build() }
1515

16-
override fun listAdd(id: String, value: V) {
16+
// Note: For in-memory persistence, we can't easily change TTL for individual entries
17+
// without creating a new cache for each TTL, which would be inefficient.
18+
// The Cache4k library doesn't support per-entry TTL configuration.
19+
// This implementation will still use the default expiration for all entries.
20+
21+
override fun listAdd(id: String, value: V, ttl: Duration?) {
1722
if (!listStore.asMap().containsKey(id)) {
1823
listStore.put(id, arrayListOf(value))
1924
} else {
@@ -29,6 +34,12 @@ class InMemoryPersistence<V : Any>(discriminator: String, defaultExpiration: Dur
2934
override operator fun set(id: String, value: V) {
3035
store.put(id, value)
3136
}
37+
38+
override fun set(id: String, value: V, ttl: Duration?) {
39+
// For in-memory persistence, we always use the defaultExpiration
40+
// as Cache4k doesn't support per-entry TTL
41+
store.put(id, value)
42+
}
3243

3344
override fun remove(id: String) {
3445
store.invalidate(id)

0 commit comments

Comments
 (0)