Skip to content

Commit 331ffe7

Browse files
committed
fix(oid4vp): respect unsigned request policy for JSON content type from request_uri
PROBLEM: The wallet rejected application/json responses from request_uri even when the unsigned request policy was set to ALLOW_UNSIGNED. This broke the demo backend flow in MobileWalletIntegrationTest since the public demo verifier returns unsigned JSON requests. ROOT CAUSE: AuthorizationRequestResolver checked content type before applying the unsigned policy - when enforceFinalRequestObject=true (default), JSON was rejected outright without considering the UnsignedRequestObjectPolicy. SOLUTION: - Modified AuthorizationRequestResolver to apply UnsignedRequestObjectPolicy when receiving application/json from request_uri: - REQUIRE_SIGNED: Reject with clear error message - ALLOW_UNSIGNED: Accept and parse as plain AuthorizationRequest - Added unsignedRequestObjectPolicy configuration to: - Wallet data class - MobileWallet constructor - MobileWalletConfig - MobileWalletFactory passthrough - Updated WalletPresentationHandler to read policy from wallet config instead of hardcoding REQUIRE_SIGNED - Updated MobileWalletIntegrationTest to use ALLOW_UNSIGNED for demo backend tests (which return unsigned requests) - Updated AuthorizationRequestResolverJvmTest to verify both policy modes This maintains production-grade security (REQUIRE_SIGNED by default) while enabling test/legacy interoperability when explicitly configured.
1 parent 892f585 commit 331ffe7

7 files changed

Lines changed: 65 additions & 9 deletions

File tree

waltid-libraries/protocols/waltid-openid4vc-wallet-mobile/src/androidDeviceTest/kotlin/id/walt/wallet2/mobile/test/MobileWalletIntegrationTest.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import id.walt.wallet2.mobile.MobileWalletEncryptionInfo
1818
import id.walt.wallet2.mobile.MobileWalletFactory
1919
import id.walt.wallet2.mobile.MobileWalletPresentationCredentialSelection
2020
import id.walt.wallet2.mobile.MobileWalletPresentationDisclosureSelection
21+
import id.waltid.openid4vp.wallet.request.AuthorizationRequestResolver
2122
import id.walt.wallet2.mobile.MobileWalletPresentationErrorCode
2223
import id.walt.wallet2.mobile.MobileWalletPresentationPreview
2324
import id.walt.wallet2.mobile.MobileWalletPresentationPreviewResult
@@ -430,9 +431,13 @@ class MobileWalletIntegrationTest {
430431
prefix: String,
431432
transactionDataProfiles: List<MobileWalletTransactionDataProfile> = DEMO_TRANSACTION_DATA_PROFILES,
432433
requestObjectX509Trust: WalletX509TrustConfig? = null,
434+
// Demo backend returns unsigned requests; allow them for these tests
435+
unsignedRequestPolicy: AuthorizationRequestResolver.UnsignedRequestObjectPolicy =
436+
AuthorizationRequestResolver.UnsignedRequestObjectPolicy.ALLOW_UNSIGNED,
433437
) = MobileWalletConfig(
434438
walletId = "android-demo-$prefix-${UUID.randomUUID()}",
435439
requestObjectX509Trust = requestObjectX509Trust,
440+
unsignedRequestObjectPolicy = unsignedRequestPolicy,
436441
onEvent = { event -> println("WALLET EVENT: $event") },
437442
transactionDataProfiles = transactionDataProfiles,
438443
)

waltid-libraries/protocols/waltid-openid4vc-wallet-mobile/src/commonMain/kotlin/id/walt/wallet2/mobile/MobileWallet.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import id.walt.wallet2.data.WalletKeyStore
1616
import id.walt.wallet2.data.WalletSessionEvent
1717
import id.walt.wallet2.data.WalletX509TrustConfig
1818
import id.walt.wallet2.handlers.PresentCredentialRequest
19-
import id.walt.wallet2.handlers.PresentationCredentialOption
2019
import id.walt.wallet2.handlers.PresentationCredentialRequirement
20+
import id.walt.wallet2.handlers.PresentationCredentialOption
21+
import id.waltid.openid4vp.wallet.request.AuthorizationRequestResolver
2122
import id.walt.wallet2.handlers.PresentationCredentialSelection
2223
import id.walt.wallet2.handlers.PresentationDisclosureSelection
2324
import id.walt.wallet2.handlers.PreviewPresentationRequest
@@ -167,6 +168,8 @@ public class MobileWallet internal constructor(
167168
attestationConfig: WalletAttestationConfig? = null,
168169
requestObjectX509Trust: WalletX509TrustConfig? = null,
169170
requestObjectAudience: String = "https://self-issued.me/v2",
171+
unsignedRequestObjectPolicy: AuthorizationRequestResolver.UnsignedRequestObjectPolicy =
172+
AuthorizationRequestResolver.UnsignedRequestObjectPolicy.REQUIRE_SIGNED,
170173
private val transactionDataProfiles: List<MobileWalletTransactionDataProfile> = emptyList(),
171174
private val onEvent: suspend (MobileWalletEvent) -> Unit = {},
172175
private val deleteLocalPersistence: suspend () -> Unit = {},
@@ -196,6 +199,7 @@ public class MobileWallet internal constructor(
196199
credentialStores = listOf(credentialStore),
197200
requestObjectX509TrustPolicy = requestObjectX509Trust?.toTrustPolicy(),
198201
requestObjectAudience = requestObjectAudience,
202+
unsignedRequestObjectPolicy = unsignedRequestObjectPolicy,
199203
)
200204

201205
/**

waltid-libraries/protocols/waltid-openid4vc-wallet-mobile/src/commonMain/kotlin/id/walt/wallet2/mobile/MobileWalletFactory.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import id.walt.wallet2.persistence.encryption.DatabaseEncryptionKeyProvider
1313
import id.walt.wallet2.persistence.keys.PlatformKeyProvider
1414
import id.walt.wallet2.persistence.stores.PlatformKeyStore
1515
import id.walt.wallet2.persistence.stores.SqlDelightCredentialStore
16+
import id.waltid.openid4vp.wallet.request.AuthorizationRequestResolver
1617
import id.walt.wallet2.persistence.stores.SqlDelightDidStore
1718
import id.walt.verifier.openid.transactiondata.TransactionDataTypeRegistry
1819

@@ -25,6 +26,9 @@ import id.walt.verifier.openid.transactiondata.TransactionDataTypeRegistry
2526
* @property persistence Persistence mode used for wallet-local state.
2627
* @property requestObjectX509Trust Wallet-controlled trust anchors for X.509 Request Objects.
2728
* @property requestObjectAudience Static Discovery default, or the Wallet issuer for Dynamic Discovery.
29+
* @property unsignedRequestObjectPolicy Policy for handling unsigned OID4VP authorization requests.
30+
* Defaults to REQUIRE_SIGNED for production-grade security.
31+
* Use ALLOW_UNSIGNED or WARN_ON_UNSIGNED only for testing or legacy interoperability.
2832
* @property onEvent Optional callback for observing wallet issuance and presentation session events.
2933
* @property transactionDataProfiles Transaction data profiles this mobile wallet accepts in OpenID4VP requests.
3034
*/
@@ -35,6 +39,8 @@ public data class MobileWalletConfig(
3539
public val persistence: MobileWalletPersistence = MobileWalletPersistence(),
3640
public val requestObjectX509Trust: WalletX509TrustConfig? = null,
3741
public val requestObjectAudience: String = "https://self-issued.me/v2",
42+
public val unsignedRequestObjectPolicy: AuthorizationRequestResolver.UnsignedRequestObjectPolicy =
43+
AuthorizationRequestResolver.UnsignedRequestObjectPolicy.REQUIRE_SIGNED,
3844
public val onEvent: suspend (MobileWalletEvent) -> Unit = {},
3945
public val transactionDataProfiles: List<MobileWalletTransactionDataProfile> = emptyList(),
4046
)
@@ -192,6 +198,7 @@ private fun createSqlDelightMobileWallet(
192198
attestationConfig = config.attestationConfig,
193199
requestObjectX509Trust = config.requestObjectX509Trust,
194200
requestObjectAudience = config.requestObjectAudience,
201+
unsignedRequestObjectPolicy = config.unsignedRequestObjectPolicy,
195202
transactionDataProfiles = config.transactionDataProfiles,
196203
onEvent = config.onEvent,
197204
deleteLocalPersistence = deleteLocalPersistence,

waltid-libraries/protocols/waltid-openid4vc-wallet/src/commonMain/kotlin/id/walt/wallet2/data/Wallet.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package id.walt.wallet2.data
33
import id.walt.crypto.keys.DirectSerializedKey
44
import id.walt.crypto.keys.Key
55
import id.walt.openid4vp.clientidprefix.X509TrustPolicy
6+
import id.waltid.openid4vp.wallet.request.AuthorizationRequestResolver
67
import kotlinx.coroutines.flow.Flow
78
import kotlinx.coroutines.flow.merge
89
import kotlinx.coroutines.flow.toList
@@ -60,6 +61,14 @@ data class Wallet(
6061
/** Wallet-controlled trust policy for X.509-authenticated OID4VP Request Objects. */
6162
val requestObjectX509TrustPolicy: X509TrustPolicy? = null,
6263

64+
/**
65+
* Policy for handling unsigned OID4VP authorization requests.
66+
* Defaults to REQUIRE_SIGNED for production-grade security.
67+
* Use ALLOW_UNSIGNED or WARN_ON_UNSIGNED only for testing or legacy interoperability.
68+
*/
69+
val unsignedRequestObjectPolicy: AuthorizationRequestResolver.UnsignedRequestObjectPolicy =
70+
AuthorizationRequestResolver.UnsignedRequestObjectPolicy.REQUIRE_SIGNED,
71+
6372
/** Expected Request Object audience for the wallet's configured discovery mode. */
6473
val requestObjectAudience: String = "https://self-issued.me/v2",
6574

waltid-libraries/protocols/waltid-openid4vc-wallet/src/commonMain/kotlin/id/walt/wallet2/handlers/WalletPresentationHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -889,7 +889,7 @@ object WalletPresentationHandler {
889889
val fetcher = WebDataFetcher(WebDataFetcherId.OPENID4VP_WALLET_RESOLVE_AUTHORIZATIONREQUEST)
890890
return AuthorizationRequestResolver.resolve(
891891
requestUrl = requestUrl,
892-
unsignedRequestObjectPolicy = AuthorizationRequestResolver.UnsignedRequestObjectPolicy.REQUIRE_SIGNED,
892+
unsignedRequestObjectPolicy = wallet.unsignedRequestObjectPolicy,
893893
expectedRequestObjectAudience = wallet.requestObjectAudience,
894894
x509TrustPolicy = wallet.requestObjectX509TrustPolicy,
895895
fetchRequestUri = { requestUri, requestUriMethod ->

waltid-libraries/protocols/waltid-openid4vp-wallet/src/commonMain/kotlin/id/waltid/openid4vp/wallet/request/AuthorizationRequestResolver.kt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,9 +376,21 @@ object AuthorizationRequestResolver {
376376
expectedRequestObjectAudience = expectedRequestObjectAudience,
377377
x509TrustPolicy = x509TrustPolicy,
378378
)
379-
contentType.match(ContentType.Application.Json) && !enforceFinalRequestObject -> {
380-
val authorizationRequest = json.decodeFromString<AuthorizationRequest>(response.body)
381-
ResolvedAuthorizationRequest.Plain(authorizationRequest)
379+
contentType.match(ContentType.Application.Json) -> {
380+
// JSON content type from request_uri indicates an unsigned request.
381+
// Apply the unsigned request policy: reject if REQUIRE_SIGNED, allow otherwise.
382+
when (unsignedRequestObjectPolicy) {
383+
UnsignedRequestObjectPolicy.REQUIRE_SIGNED -> {
384+
throw IllegalArgumentException(
385+
"Unsigned authorization request not allowed: received application/json " +
386+
"from request_uri but wallet policy requires signed requests (application/oauth-authz-req+jwt)"
387+
)
388+
}
389+
UnsignedRequestObjectPolicy.ALLOW_UNSIGNED -> {
390+
val authorizationRequest = json.decodeFromString<AuthorizationRequest>(response.body)
391+
ResolvedAuthorizationRequest.Plain(authorizationRequest)
392+
}
393+
}
382394
}
383395
else -> throw IllegalArgumentException("Unsupported AuthorizationRequest content type: $contentType")
384396
}

waltid-libraries/protocols/waltid-openid4vp-wallet/src/jvmTest/kotlin/id/waltid/openid4vp/wallet/request/AuthorizationRequestResolverJvmTest.kt

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ class AuthorizationRequestResolverJvmTest {
244244
}
245245

246246
@Test
247-
fun `request uri rejects wallet nonce mismatch and non JWT content type`() {
247+
fun `request uri rejects wallet nonce mismatch and enforces signed request policy for JSON content`() {
248248
val requestUrl = URLBuilder("openid4vp://authorize").apply {
249249
parameters.append("client_id", "verifier2")
250250
parameters.append("request_uri", "https://verifier.example/request.jwt")
@@ -268,11 +268,12 @@ class AuthorizationRequestResolverJvmTest {
268268
}
269269
assertTrue(mismatch.message.orEmpty().contains("wallet_nonce mismatch"))
270270

271-
val wrongContentType = assertFailsWith<IllegalArgumentException> {
271+
// When REQUIRE_SIGNED policy is set, JSON content type should be rejected
272+
val rejectedWithSignedPolicy = assertFailsWith<IllegalArgumentException> {
272273
runBlocking {
273274
AuthorizationRequestResolver.resolve(
274275
requestUrl,
275-
AuthorizationRequestResolver.UnsignedRequestObjectPolicy.ALLOW_UNSIGNED,
276+
AuthorizationRequestResolver.UnsignedRequestObjectPolicy.REQUIRE_SIGNED,
276277
) { _, _ ->
277278
AuthorizationRequestResolver.RequestUriFetchResponse(
278279
HttpStatusCode.OK,
@@ -282,7 +283,25 @@ class AuthorizationRequestResolverJvmTest {
282283
}
283284
}
284285
}
285-
assertTrue(wrongContentType.message.orEmpty().contains("Unsupported AuthorizationRequest content type"))
286+
assertTrue(
287+
rejectedWithSignedPolicy.message.orEmpty().contains("Unsigned authorization request not allowed"),
288+
"Expected unsigned request rejection message, got: ${rejectedWithSignedPolicy.message}"
289+
)
290+
291+
// When ALLOW_UNSIGNED policy is set, JSON content type should be accepted
292+
val acceptedWithAllowPolicy = runBlocking {
293+
AuthorizationRequestResolver.resolve(
294+
requestUrl,
295+
AuthorizationRequestResolver.UnsignedRequestObjectPolicy.ALLOW_UNSIGNED,
296+
) { _, _ ->
297+
AuthorizationRequestResolver.RequestUriFetchResponse(
298+
HttpStatusCode.OK,
299+
ContentType.Application.Json,
300+
"{}",
301+
)
302+
}
303+
}
304+
assertIs<ResolvedAuthorizationRequest.Plain>(acceptedWithAllowPolicy)
286305
}
287306

288307
@Test

0 commit comments

Comments
 (0)