@@ -43,11 +43,14 @@ import id.walt.openid4vci.requests.credential.CredentialRequestResult
4343import id.walt.openid4vci.metadata.issuer.CredentialConfiguration
4444import id.walt.openid4vci.metadata.issuer.CredentialDisplay
4545import id.walt.mdoc.dataelement.json.JsonObjectToCborMappingConfig as LegacyMdocJsonObjectToCborMappingConfig
46+ import id.walt.openid4vci.proofs.CredentialProofValidationContext
47+ import id.walt.openid4vci.proofs.CredentialProofValidationException
4648import id.walt.crypto.keys.Key
4749import id.walt.mdoc.objects.mso.Status
4850import id.walt.openid4vci.tokens.access.AccessTokenAuthorizationScheme
4951import id.walt.openid4vci.tokens.access.CredentialAccessTokenContext
5052import id.walt.openid4vci.tokens.access.dpopJwkThumbprint
53+ import id.walt.openid4vci.tokens.jwt.JwtPayloadClaims
5154import id.walt.sdjwt.SDMap
5255import id.walt.x509.CertificateDer
5356import kotlinx.coroutines.CancellationException
@@ -56,6 +59,8 @@ import kotlinx.serialization.json.JsonNull
5659import kotlinx.serialization.json.JsonObject
5760import kotlinx.serialization.json.JsonPrimitive
5861import kotlinx.serialization.json.buildJsonArray
62+ import kotlinx.serialization.json.contentOrNull
63+ import kotlinx.serialization.json.jsonPrimitive
5964import kotlin.time.Clock
6065import kotlin.time.Instant
6166
@@ -504,7 +509,10 @@ class DefaultOAuth2Provider(
504509 session : Session ? ,
505510 accessTokenContext : CredentialAccessTokenContext ?
506511 ): CredentialRequestResult {
507- verifyCredentialAccessToken(accessTokenContext)?.let { return it }
512+ val tokenClaims = when (val tokenResult = verifyCredentialAccessToken(accessTokenContext)) {
513+ is CredentialAccessTokenVerification .Success -> tokenResult.claims
514+ is CredentialAccessTokenVerification .Failure -> return tokenResult.result
515+ }
508516 return when (val result = config.credentialRequestValidator.validate(parameters, session ? : DefaultSession ())) {
509517 is CredentialRequestResult .Success ->
510518 if (result.request.credentialResponseEncryption != null ) {
@@ -515,7 +523,7 @@ class DefaultOAuth2Provider(
515523 )
516524 )
517525 } else {
518- result
526+ CredentialRequestResult . Success ( result.request.withAccessTokenClient(tokenClaims))
519527 }
520528
521529 is CredentialRequestResult .Failure -> result
@@ -528,7 +536,10 @@ class DefaultOAuth2Provider(
528536 session : Session ? ,
529537 accessTokenContext : CredentialAccessTokenContext ?
530538 ): CredentialRequestResult {
531- verifyCredentialAccessToken(accessTokenContext)?.let { return it }
539+ val tokenClaims = when (val tokenResult = verifyCredentialAccessToken(accessTokenContext)) {
540+ is CredentialAccessTokenVerification .Success -> tokenResult.claims
541+ is CredentialAccessTokenVerification .Failure -> return tokenResult.result
542+ }
532543 val decryptor = config.credentialRequestDecryptor
533544 ? : return CredentialRequestResult .Failure (
534545 CredentialError (
@@ -545,7 +556,12 @@ class DefaultOAuth2Provider(
545556 } catch (e: Exception ) {
546557 return CredentialRequestResult .Failure (CredentialError (CredentialErrorCodes .INVALID_CREDENTIAL_REQUEST , e.message))
547558 }
548- return config.credentialRequestValidator.validate(parameters, session ? : DefaultSession ())
559+ return when (val result = config.credentialRequestValidator.validate(parameters, session ? : DefaultSession ())) {
560+ is CredentialRequestResult .Success ->
561+ CredentialRequestResult .Success (result.request.withAccessTokenClient(tokenClaims))
562+ is CredentialRequestResult .Failure -> result
563+ is CredentialRequestResult .OAuthFailure -> result
564+ }
549565 }
550566
551567 override suspend fun createCredentialResponse (
@@ -563,7 +579,19 @@ class DefaultOAuth2Provider(
563579 credentialStatus : Status ? ,
564580 validFrom : Instant ? ,
565581 validUntil : Instant ? ,
582+ proofValidationContext : CredentialProofValidationContext ? ,
566583 ): CredentialResponseResult {
584+ val verifiedProofs = when (
585+ val proofResult = verifyCredentialProofs(
586+ request = request,
587+ configuration = configuration,
588+ proofValidationContext = proofValidationContext,
589+ )
590+ ) {
591+ is CredentialProofVerification .Success -> proofResult.proofs
592+ is CredentialProofVerification .Failure -> return CredentialResponseResult .Failure (proofResult.error)
593+ }
594+
567595 val handler = config.credentialEndpointHandlers.get(configuration.format)
568596 ? : return CredentialResponseResult .Failure (
569597 CredentialError (
@@ -586,6 +614,7 @@ class DefaultOAuth2Provider(
586614 credentialStatus = credentialStatus,
587615 validFrom = validFrom,
588616 validUntil = validUntil,
617+ verifiedProofs = verifiedProofs,
589618 )
590619 }
591620
@@ -638,11 +667,13 @@ class DefaultOAuth2Provider(
638667
639668 private suspend fun verifyCredentialAccessToken (
640669 accessTokenContext : CredentialAccessTokenContext ? ,
641- ): CredentialRequestResult . OAuthFailure ? {
642- if (accessTokenContext == null ) return null
670+ ): CredentialAccessTokenVerification {
671+ if (accessTokenContext == null ) return CredentialAccessTokenVerification . Success ( null )
643672 val verifier = config.accessTokenVerifier
644- ? : return CredentialRequestResult .OAuthFailure (
645- OAuthError (OAuthErrorCodes .SERVER_ERROR , " access token verifier not configured" )
673+ ? : return CredentialAccessTokenVerification .Failure (
674+ CredentialRequestResult .OAuthFailure (
675+ OAuthError (OAuthErrorCodes .SERVER_ERROR , " access token verifier not configured" )
676+ )
646677 )
647678 return try {
648679 val claims = verifier.verify(
@@ -651,10 +682,61 @@ class DefaultOAuth2Provider(
651682 expectedAudience = accessTokenContext.expectedAudience,
652683 )
653684 verifyCredentialAccessTokenBinding(accessTokenContext, claims)
685+ ?.let { CredentialAccessTokenVerification .Failure (it) }
686+ ? : CredentialAccessTokenVerification .Success (claims)
687+ } catch (e: CancellationException ) {
688+ throw e
689+ } catch (e: Exception ) {
690+ CredentialAccessTokenVerification .Failure (
691+ invalidCredentialAccessToken(e.message ? : " Access token is invalid" ),
692+ )
693+ }
694+ }
695+
696+ private fun CredentialRequest.withAccessTokenClient (tokenClaims : JsonObject ? ): CredentialRequest {
697+ val clientId = tokenClaims?.get(JwtPayloadClaims .CLIENT_ID )
698+ ?.jsonPrimitive
699+ ?.contentOrNull
700+ ?.takeIf { it.isNotBlank() }
701+ val anonymousPreAuthorizedAccess = clientId == null &&
702+ tokenClaims?.get(JwtPayloadClaims .PRE_AUTHORIZED_CODE )?.jsonPrimitive?.contentOrNull != null
703+ return withAccessTokenClient(clientId, anonymousPreAuthorizedAccess)
704+ }
705+
706+ private suspend fun verifyCredentialProofs (
707+ request : CredentialRequest ,
708+ configuration : CredentialConfiguration ,
709+ proofValidationContext : CredentialProofValidationContext ? ,
710+ ): CredentialProofVerification {
711+ val shouldVerifyProofs = configuration.proofTypesSupported != null ||
712+ (proofValidationContext != null && request.proofs != null )
713+ if (! shouldVerifyProofs) return CredentialProofVerification .Success (emptyList())
714+
715+ val context = proofValidationContext
716+ ? : return CredentialProofVerification .Failure (
717+ CredentialError (CredentialErrorCodes .INVALID_PROOF , " Credential proof validation context is required" ),
718+ )
719+ val verifier = config.credentialProofVerifier
720+ ? : return CredentialProofVerification .Failure (
721+ CredentialError (CredentialErrorCodes .INVALID_PROOF , " Credential proof verification is not configured" ),
722+ )
723+
724+ return try {
725+ CredentialProofVerification .Success (
726+ verifier.verify(
727+ credentialRequest = request,
728+ credentialConfiguration = configuration,
729+ context = context,
730+ )
731+ )
654732 } catch (e: CancellationException ) {
655733 throw e
734+ } catch (e: CredentialProofValidationException ) {
735+ CredentialProofVerification .Failure (CredentialError (e.errorCode, e.message))
656736 } catch (e: Exception ) {
657- invalidCredentialAccessToken(e.message ? : " Access token is invalid" )
737+ CredentialProofVerification .Failure (
738+ CredentialError (CredentialErrorCodes .INVALID_PROOF , e.message ? : " Invalid credential proof" ),
739+ )
658740 }
659741 }
660742
@@ -663,14 +745,11 @@ class DefaultOAuth2Provider(
663745 claims : JsonObject ,
664746 ): CredentialRequestResult .OAuthFailure ? {
665747 val boundJwkThumbprint = claims.dpopJwkThumbprint()
666-
667- if (boundJwkThumbprint == null ) {
668- return if (context.authorization.scheme == AccessTokenAuthorizationScheme .BEARER ) {
748+ ? : return if (context.authorization.scheme == AccessTokenAuthorizationScheme .BEARER ) {
669749 null
670750 } else {
671751 invalidCredentialAccessToken(" Access token is not DPoP-bound" )
672752 }
673- }
674753
675754 if (context.authorization.scheme != AccessTokenAuthorizationScheme .DPOP ) {
676755 return invalidCredentialAccessToken(" DPoP-bound access token must use the DPoP authorization scheme" )
@@ -715,6 +794,18 @@ class DefaultOAuth2Provider(
715794 OAuthError (OAuthErrorCodes .INVALID_DPOP_PROOF , description),
716795 )
717796
797+ private sealed class CredentialAccessTokenVerification {
798+ data class Success (val claims : JsonObject ? ) : CredentialAccessTokenVerification()
799+ data class Failure (val result : CredentialRequestResult ) : CredentialAccessTokenVerification()
800+ }
801+
802+ private sealed class CredentialProofVerification {
803+ data class Success (val proofs : List <id.walt.openid4vci.proofs.VerifiedCredentialProof >) :
804+ CredentialProofVerification ()
805+
806+ data class Failure (val error : CredentialError ) : CredentialProofVerification()
807+ }
808+
718809 private fun dpopAuthenticationChallenge (error : OAuthError ): String = buildString {
719810 append(TOKEN_TYPE_DPOP )
720811 append(" error=\" " ).append(error.error.escapeAuthenticationParameter()).append(' "' )
0 commit comments