diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/AuthorizeIssuance.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/AuthorizeIssuance.kt index 95b04abf..3717d464 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/AuthorizeIssuance.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/AuthorizeIssuance.kt @@ -37,6 +37,11 @@ data class AuthorizationRequestPrepared( val dpopNonce: Nonce?, ) : java.io.Serializable +enum class Grant : java.io.Serializable { + AuthorizationCode, + PreAuthorizedCodeGrant, +} + /** * Sealed hierarchy of states describing an authorized issuance request. These states hold an access token issued by the * authorization server that protects the credential issuer. @@ -50,9 +55,22 @@ sealed interface AuthorizedRequest : java.io.Serializable { val refreshToken: RefreshToken? val credentialIdentifiers: Map>? val timestamp: Instant + + /** + * Authorization server-provided DPoP Nonce, if any + */ val authorizationServerDpopNonce: Nonce? + + /** + * Protected resource-provided DPoP Nonce, if any + */ val resourceServerDpopNonce: Nonce? + /** + * The Grant through which the authorization was obtained + */ + val grant: Grant + fun isAccessTokenExpired(at: Instant): Boolean = accessToken.isExpired(timestamp, at) fun isRefreshTokenExpiredOrMissing(at: Instant): Boolean = refreshToken?.isExpired(timestamp, at) ?: true @@ -65,13 +83,14 @@ sealed interface AuthorizedRequest : java.io.Serializable { */ fun withCNonce(cNonce: CNonce): ProofRequired = ProofRequired( - accessToken, - refreshToken, + accessToken = accessToken, + refreshToken = refreshToken, cNonce = cNonce, - credentialIdentifiers, - timestamp, + credentialIdentifiers = credentialIdentifiers, + timestamp = timestamp, authorizationServerDpopNonce = authorizationServerDpopNonce, resourceServerDpopNonce = resourceServerDpopNonce, + grant = grant, ) fun withRefreshedAccessToken( @@ -111,6 +130,7 @@ sealed interface AuthorizedRequest : java.io.Serializable { * @param timestamp the point in time of the authorization (when tokens were issued) * @param authorizationServerDpopNonce Nonce value for DPoP provided by the Authorization Server * @param resourceServerDpopNonce Nonce value for DPoP provided by the Resource Server + * @param grant the Grant through which the authorization was obtained */ data class NoProofRequired( override val accessToken: AccessToken, @@ -119,6 +139,7 @@ sealed interface AuthorizedRequest : java.io.Serializable { override val timestamp: Instant, override val authorizationServerDpopNonce: Nonce?, override val resourceServerDpopNonce: Nonce?, + override val grant: Grant, ) : AuthorizedRequest /** @@ -132,6 +153,7 @@ sealed interface AuthorizedRequest : java.io.Serializable { * @param timestamp the point in time of the authorization (when tokens were issued) * @param authorizationServerDpopNonce Nonce value for DPoP provided by the Authorization Server * @param resourceServerDpopNonce Nonce value for DPoP provided by the Resource Server + * @param grant the Grant through which the authorization was obtained */ data class ProofRequired( override val accessToken: AccessToken, @@ -141,6 +163,7 @@ sealed interface AuthorizedRequest : java.io.Serializable { override val timestamp: Instant, override val authorizationServerDpopNonce: Nonce?, override val resourceServerDpopNonce: Nonce?, + override val grant: Grant, ) : AuthorizedRequest } diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/DeferredIssuer.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/DeferredIssuer.kt index a467aeac..e11b1fa0 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/DeferredIssuer.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/DeferredIssuer.kt @@ -66,6 +66,7 @@ data class AuthorizedTransaction( timestamp = authorizedRequest.timestamp, authorizationServerDpopNonce = authorizedRequest.authorizationServerDpopNonce, resourceServerDpopNonce = authorizedRequest.resourceServerDpopNonce, + grant = authorizedRequest.grant, ) }, transactionId = transactionId, diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/AuthorizeIssuanceImpl.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/AuthorizeIssuanceImpl.kt index dd449558..e5d8d8e0 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/AuthorizeIssuanceImpl.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/AuthorizeIssuanceImpl.kt @@ -95,7 +95,7 @@ internal class AuthorizeIssuanceImpl( credConfigIdsAsAuthDetails, dpopNonce, ).getOrThrow() - authorizedRequest(credentialOffer, tokenResponse, newDpopNonce) + authorizedRequest(credentialOffer, tokenResponse, newDpopNonce, Grant.AuthorizationCode) } override suspend fun authorizeWithPreAuthorizationCode( @@ -119,7 +119,7 @@ internal class AuthorizeIssuanceImpl( credConfigIdsAsAuthDetails, dpopNonce = null, ).getOrThrow() - authorizedRequest(credentialOffer, tokenResponse, newDpopNonce) + authorizedRequest(credentialOffer, tokenResponse, newDpopNonce, Grant.PreAuthorizedCodeGrant) } } @@ -150,6 +150,7 @@ private fun authorizedRequest( offer: CredentialOffer, tokenResponse: TokenResponse, newDpopNonce: Nonce?, + grant: Grant, ): AuthorizedRequest { val offerRequiresProofs = offer.credentialConfigurationIdentifiers.any { val credentialConfiguration = offer.credentialIssuerMetadata.credentialConfigurationsSupported[it] @@ -166,6 +167,7 @@ private fun authorizedRequest( timestamp, authorizationServerDpopNonce = newDpopNonce, resourceServerDpopNonce = null, + grant = grant, ) else -> @@ -176,6 +178,7 @@ private fun authorizedRequest( timestamp, authorizationServerDpopNonce = newDpopNonce, resourceServerDpopNonce = null, + grant = grant, ) } } diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/ProofBuilder.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/ProofBuilder.kt index bbdab481..2d1130c8 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/ProofBuilder.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/ProofBuilder.kt @@ -30,7 +30,7 @@ private interface CheckPopSigner { internal abstract class ProofBuilder( val clock: Clock, - val iss: ClientId, + val iss: ClientId?, val aud: CredentialIssuerId, val nonce: CNonce, val popSigner: POP_SIGNER, @@ -42,7 +42,7 @@ internal abstract class ProofBuilder( operator fun invoke( proofTypesSupported: ProofTypesSupported, clock: Clock, - iss: ClientId, + iss: ClientId?, aud: CredentialIssuerId, nonce: CNonce, popSigner: PopSigner, @@ -54,12 +54,33 @@ internal abstract class ProofBuilder( } } } + operator fun invoke( + proofTypesSupported: ProofTypesSupported, + clock: Clock, + client: Client, + grant: Grant, + aud: CredentialIssuerId, + nonce: CNonce, + popSigner: PopSigner, + ): ProofBuilder<*, *> = + invoke(proofTypesSupported, clock, iss(client, grant), aud, nonce, popSigner) + + private fun iss(client: Client, grant: Grant): ClientId? { + val useIss = when (grant) { + Grant.AuthorizationCode -> true + Grant.PreAuthorizedCodeGrant -> when (client) { + is Client.Attested -> true + is Client.Public -> false + } + } + return client.id.takeIf { useIss } + } } } internal class JwtProofBuilder( clock: Clock, - iss: ClientId, + iss: ClientId?, aud: CredentialIssuerId, nonce: CNonce, popSigner: PopSigner.Jwt, @@ -86,7 +107,7 @@ internal class JwtProofBuilder( private fun claimSet(): JWTClaimsSet = JWTClaimsSet.Builder().apply { - issuer(iss) + iss?.let { issuer(it) } audience(aud.toString()) claim("nonce", nonce.value) issueTime(Date.from(clock.instant())) diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/RequestIssuanceImpl.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/RequestIssuanceImpl.kt index ee6387e5..933f14a7 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/RequestIssuanceImpl.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/RequestIssuanceImpl.kt @@ -41,7 +41,8 @@ internal class RequestIssuanceImpl( // // Update state // - val updatedAuthorizedRequest = this.withCNonceFrom(outcome).withResourceServerDpopNonce(newResourceServerDpopNonce) + val updatedAuthorizedRequest = + this.withCNonceFrom(outcome).withResourceServerDpopNonce(newResourceServerDpopNonce) // // Retry on invalid proof if we begin from NoProofRequired and issuer @@ -104,15 +105,18 @@ internal class RequestIssuanceImpl( } } } - popSigners.map { proofFactory(it, cNonce) } + popSigners.map { proofFactory(it, cNonce, grant) } } } - private fun proofFactory(proofSigner: PopSigner, cNonce: CNonce): ProofFactory = { credentialSupported -> - val iss = config.client.id + private fun proofFactory( + proofSigner: PopSigner, + cNonce: CNonce, + grant: Grant, + ): ProofFactory = { credentialSupported -> val aud = credentialOffer.credentialIssuerMetadata.credentialIssuerIdentifier val proofTypesSupported = credentialSupported.proofTypesSupported - ProofBuilder(proofTypesSupported, config.clock, iss, aud, cNonce, proofSigner).build() + ProofBuilder(proofTypesSupported, config.clock, config.client, grant, aud, cNonce, proofSigner).build() } private suspend fun buildRequest( diff --git a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/DeferredIssuerExtensions.kt b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/DeferredIssuerExtensions.kt index b5783464..1922ea6f 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/DeferredIssuerExtensions.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/DeferredIssuerExtensions.kt @@ -103,6 +103,31 @@ data class AccessTokenTO( } } +@Serializable +enum class GrantTO { + @SerialName("authorization_code") + AuthorizationCode, + + @SerialName("urn:ietf:params:oauth:grant-type:pre-authorized_code") + PreAuthorizedCodeGrant, + + ; + + fun toGrant(): Grant = + when (this) { + AuthorizationCode -> Grant.AuthorizationCode + PreAuthorizedCodeGrant -> Grant.PreAuthorizedCodeGrant + } + + companion object { + fun fromGrant(grant: Grant): GrantTO = + when (grant) { + Grant.AuthorizationCode -> AuthorizationCode + Grant.PreAuthorizedCodeGrant -> PreAuthorizedCodeGrant + } + } +} + @Serializable data class DeferredIssuanceStoredContextTO( @Required @SerialName("credential_issuer") val credentialIssuerId: String, @@ -120,7 +145,8 @@ data class DeferredIssuanceStoredContextTO( @SerialName("transaction_id") val transactionId: String, @SerialName("access_token") val accessToken: AccessTokenTO, @SerialName("refresh_token") val refreshToken: RefreshTokenTO? = null, - @SerialName("authorization_timestamp") val authorizationTimestamp: Long, + @SerialName("authorization_timestamGrantTO.fromGrant(grant)p") val authorizationTimestamp: Long, + @SerialName("grant") val grant: GrantTO, ) { fun toDeferredIssuanceStoredContext( @@ -165,6 +191,7 @@ data class DeferredIssuanceStoredContextTO( timestamp = Instant.ofEpochSecond(authorizationTimestamp), authorizationServerDpopNonce = null, resourceServerDpopNonce = null, + grant = grant.toGrant(), ), transactionId = TransactionId(transactionId), ), @@ -203,6 +230,7 @@ data class DeferredIssuanceStoredContextTO( accessToken = AccessTokenTO.from(authorizedTransaction.authorizedRequest.accessToken), refreshToken = authorizedTransaction.authorizedRequest.refreshToken?.let { RefreshTokenTO.from(it) }, authorizationTimestamp = authorizedTransaction.authorizedRequest.timestamp.epochSecond, + grant = GrantTO.fromGrant(authorizedTransaction.authorizedRequest.grant), ) }