Skip to content

Commit

Permalink
- Obtain c_nonce via Nonce Endpoint
Browse files Browse the repository at this point in the history
- Remove AuthorizedRequest.ProofRequired and AuthorizedRequest.NoProofRequired
- Removed c_nonce from token and credential endpoint responses
- Removed retry of issuance request
  • Loading branch information
vafeini committed Feb 21, 2025
1 parent 747e99e commit 649547f
Show file tree
Hide file tree
Showing 23 changed files with 796 additions and 696 deletions.
131 changes: 24 additions & 107 deletions src/main/kotlin/eu/europa/ec/eudi/openid4vci/AuthorizeIssuance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,125 +45,42 @@ enum class Grant : java.io.Serializable {
/**
* 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.
*
* @param accessToken Access token authorizing the request(s) to issue credential(s)
* @param refreshToken Refresh token to refresh the access token, if needed
* @param credentialIdentifiers authorization details, if provided by the token endpoint
* @param authorizationServerDpopNonce Nonce value for DPoP provided by the Authorization Server
* @param timestamp the point in time of the authorization (when tokens were issued)
* @param resourceServerDpopNonce Nonce value for DPoP provided by the Resource Server
* @param grant The Grant through which the authorization was obtained
*/
sealed interface AuthorizedRequest : java.io.Serializable {

/**
* Access token authorizing the request(s) to issue credential(s)
*/
val accessToken: AccessToken
val refreshToken: RefreshToken?
val credentialIdentifiers: Map<CredentialConfigurationIdentifier, List<CredentialIdentifier>>?
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
data class AuthorizedRequest(
val accessToken: AccessToken,
val refreshToken: RefreshToken?,
val credentialIdentifiers: Map<CredentialConfigurationIdentifier, List<CredentialIdentifier>>?,
val timestamp: Instant,
val authorizationServerDpopNonce: Nonce?,
val resourceServerDpopNonce: Nonce?,
val grant: Grant,
) : java.io.Serializable {

fun isAccessTokenExpired(at: Instant): Boolean = accessToken.isExpired(timestamp, at)

/**
* In case an 'invalid_proof' error response was received from issuer with
* fresh c_nonce
*
* @param cNonce The c_nonce provided from issuer along the 'invalid_proof' error code.
* @return The new state of the request.
*/
fun withCNonce(cNonce: CNonce): ProofRequired =
ProofRequired(
accessToken = accessToken,
refreshToken = refreshToken,
cNonce = cNonce,
credentialIdentifiers = credentialIdentifiers,
timestamp = timestamp,
authorizationServerDpopNonce = authorizationServerDpopNonce,
resourceServerDpopNonce = resourceServerDpopNonce,
grant = grant,
)

fun withRefreshedAccessToken(
refreshedAccessToken: AccessToken,
newRefreshToken: RefreshToken?,
at: Instant,
newAuthorizationServerDpopNonce: Nonce?,
): AuthorizedRequest =
when (this) {
is NoProofRequired -> copy(
accessToken = refreshedAccessToken,
refreshToken = newRefreshToken ?: refreshToken,
timestamp = at,
authorizationServerDpopNonce = newAuthorizationServerDpopNonce,
)

is ProofRequired -> copy(
accessToken = refreshedAccessToken,
refreshToken = newRefreshToken ?: refreshToken,
timestamp = at,
authorizationServerDpopNonce = newAuthorizationServerDpopNonce,
)
}
copy(
accessToken = refreshedAccessToken,
refreshToken = newRefreshToken ?: refreshToken,
timestamp = at,
authorizationServerDpopNonce = newAuthorizationServerDpopNonce,
)

fun withResourceServerDpopNonce(newResourceServerDpopNonce: Nonce?): AuthorizedRequest =
when (this) {
is NoProofRequired -> copy(resourceServerDpopNonce = newResourceServerDpopNonce)
is ProofRequired -> copy(resourceServerDpopNonce = newResourceServerDpopNonce)
}

/**
* Issuer authorized issuance
*
* @param accessToken Access token authorizing credential issuance
* @param refreshToken Refresh token to refresh the access token, if needed
* @param credentialIdentifiers authorization details, if provided by the token endpoint
* @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,
override val refreshToken: RefreshToken?,
override val credentialIdentifiers: Map<CredentialConfigurationIdentifier, List<CredentialIdentifier>>?,
override val timestamp: Instant,
override val authorizationServerDpopNonce: Nonce?,
override val resourceServerDpopNonce: Nonce?,
override val grant: Grant,
) : AuthorizedRequest

/**
* Issuer authorized issuance and required the provision of proof of holder's binding to be provided
* along with the request
*
* @param accessToken Access token authorizing certificate issuance
* @param refreshToken Refresh token to refresh the access token, if needed
* @param cNonce Nonce value provided by issuer to be included in proof of holder's binding
* @param credentialIdentifiers authorization details, if provided by the token endpoint
* @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,
override val refreshToken: RefreshToken?,
val cNonce: CNonce,
override val credentialIdentifiers: Map<CredentialConfigurationIdentifier, List<CredentialIdentifier>>?,
override val timestamp: Instant,
override val authorizationServerDpopNonce: Nonce?,
override val resourceServerDpopNonce: Nonce?,
override val grant: Grant,
) : AuthorizedRequest
copy(resourceServerDpopNonce = newResourceServerDpopNonce)
}

sealed interface AccessTokenOption {
Expand Down
21 changes: 2 additions & 19 deletions src/main/kotlin/eu/europa/ec/eudi/openid4vci/DeferredIssuer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,25 +53,9 @@ data class DeferredIssuerConfig(
* @param transactionId the id returned by deferred endpoint
*/
data class AuthorizedTransaction(
val authorizedRequest: AuthorizedRequest.NoProofRequired,
val authorizedRequest: AuthorizedRequest,
val transactionId: TransactionId,
) {
constructor(authorizedRequest: AuthorizedRequest, transactionId: TransactionId) : this(
authorizedRequest = when (authorizedRequest) {
is AuthorizedRequest.NoProofRequired -> authorizedRequest
is AuthorizedRequest.ProofRequired -> AuthorizedRequest.NoProofRequired(
accessToken = authorizedRequest.accessToken,
refreshToken = authorizedRequest.refreshToken,
credentialIdentifiers = authorizedRequest.credentialIdentifiers,
timestamp = authorizedRequest.timestamp,
authorizationServerDpopNonce = authorizedRequest.authorizationServerDpopNonce,
resourceServerDpopNonce = authorizedRequest.resourceServerDpopNonce,
grant = authorizedRequest.grant,
)
},
transactionId = transactionId,
)
}
)

/**
* Represents what a wallet needs to keep to be
Expand Down Expand Up @@ -128,7 +112,6 @@ interface DeferredIssuer : QueryForDeferredCredential {
val newCtx = when (outcome) {
is DeferredCredentialQueryOutcome.IssuancePending, is DeferredCredentialQueryOutcome.Errored -> {
if (newAuthorized != ctx.authorizedTransaction.authorizedRequest) {
check(newAuthorized is AuthorizedRequest.NoProofRequired)
val newAuthorizedTransaction = ctx.authorizedTransaction.copy(authorizedRequest = newAuthorized)
ctx.copy(authorizedTransaction = newAuthorizedTransaction)
} else {
Expand Down
25 changes: 10 additions & 15 deletions src/main/kotlin/eu/europa/ec/eudi/openid4vci/Issuance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -135,19 +135,11 @@ interface RequestIssuance {

/**
* Places a request to the credential issuance endpoint.
* Method will attempt to automatically retry submission in case
* - Initial authorization state is [AuthorizedRequest.NoProofRequired] and
* - one or more [popSigners] haven been provided
*
* @receiver the current authorization state
* @param requestPayload the payload of the request
* @param popSigners one or more signers for the proofs to be sent.
* Although this is an optional parameter, only required in case the present authorization state is [AuthorizedRequest.ProofRequired],
* caller is advised to provide it, to allow the method to automatically retry
* in case of [CredentialIssuanceError.InvalidProof]
*
* @return the possibly updated [AuthorizedRequest] (if updated it will contain a fresh c_nonce and/or
* updated Resource-Server DPoP Nonce) and the [SubmissionOutcome]
* @param popSigners one or more signers for the proofs to be sent. Required when requested credential's configuration requires proof.
* @return the possibly updated [AuthorizedRequest] (if updated it will contain a fresh updated Resource-Server DPoP Nonce)
* and the [SubmissionOutcome]
*/
suspend fun AuthorizedRequest.request(
requestPayload: IssuanceRequestPayload,
Expand Down Expand Up @@ -245,12 +237,10 @@ sealed class CredentialIssuanceError(message: String) : Throwable(message) {
) : CredentialIssuanceError(message)

/**
* Issuer rejected the issuance request because no c_nonce was provided along with the proof.
* A fresh c_nonce is provided by the issuer.
* Issuer rejected the issuance request because no or invalid proof(s) were provided or at least one of the key proofs does
* not contain a c_nonce value.
*/
data class InvalidProof(
val cNonce: String,
val cNonceExpiresIn: Long? = 5,
val errorDescription: String? = null,
) : CredentialIssuanceError("Invalid Proof")

Expand Down Expand Up @@ -331,6 +321,11 @@ sealed class CredentialIssuanceError(message: String) : Throwable(message) {
*/
data class ResponseUnparsable(val error: String) : CredentialIssuanceError("ResponseUnparsable")

/**
* Request to nonce endpoint of issuer failed
*/
data class CNonceRequestFailed(val error: String) : CredentialIssuanceError("CNonceRequestFailed")

/**
* Sealed hierarchy of errors related to proof generation
*/
Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/eu/europa/ec/eudi/openid4vci/Issuer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,17 @@ interface Issuer :
dPoPJwtFactory,
ktorHttpClientFactory,
)
val nonceEndpointClient = credentialOffer.credentialIssuerMetadata.nonceEndpoint?.let {
NonceEndpointClient(
credentialOffer.credentialIssuerMetadata.nonceEndpoint,
ktorHttpClientFactory,
)
}
RequestIssuanceImpl(
credentialOffer,
config,
credentialEndpointClient,
nonceEndpointClient,
credentialOffer.credentialIssuerMetadata.batchCredentialIssuance,
responseEncryptionSpec,
)
Expand Down
9 changes: 3 additions & 6 deletions src/main/kotlin/eu/europa/ec/eudi/openid4vci/Types.kt
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,12 @@ value class AuthorizationCode(val code: String) {
}

/**
* A c_nonce related information as provided from issuance server.
* A c_nonce as provided from issuance server's nonce endpoint.
*
* @param value The c_nonce value
* @param expiresInSeconds Nonce time to live in seconds.
*/
data class CNonce(
val value: String,
val expiresInSeconds: Long? = 5,
) : java.io.Serializable {
@JvmInline
value class CNonce(val value: String) : java.io.Serializable {
init {
require(value.isNotEmpty()) { "Value cannot be empty" }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ package eu.europa.ec.eudi.openid4vci.internal
import eu.europa.ec.eudi.openid4vci.*
import eu.europa.ec.eudi.openid4vci.AccessTokenOption.AsRequested
import eu.europa.ec.eudi.openid4vci.AccessTokenOption.Limited
import eu.europa.ec.eudi.openid4vci.AuthorizedRequest.NoProofRequired
import eu.europa.ec.eudi.openid4vci.AuthorizedRequest.ProofRequired
import eu.europa.ec.eudi.openid4vci.CredentialIssuanceError.InvalidAuthorizationState
import eu.europa.ec.eudi.openid4vci.internal.http.AuthorizationEndpointClient
import eu.europa.ec.eudi.openid4vci.internal.http.TokenEndpointClient
Expand All @@ -29,7 +27,6 @@ import com.nimbusds.oauth2.sdk.id.State as NimbusState
internal data class TokenResponse(
val accessToken: AccessToken,
val refreshToken: RefreshToken?,
val cNonce: CNonce?,
val authorizationDetails: Map<CredentialConfigurationIdentifier, List<CredentialIdentifier>> = emptyMap(),
val timestamp: Instant,
)
Expand Down Expand Up @@ -87,19 +84,27 @@ internal class AuthorizeIssuanceImpl(
authorizationCode: AuthorizationCode,
serverState: String,
authDetailsOption: AccessTokenOption,
): Result<AuthorizedRequest> =
runCatching {
ensure(serverState == state) { InvalidAuthorizationState() }
val credConfigIdsAsAuthDetails = identifiersSentAsAuthDetails.filter(authDetailsOption)
val (tokenResponse, newDpopNonce) =
tokenEndpointClient.requestAccessTokenAuthFlow(
authorizationCode,
pkceVerifier,
credConfigIdsAsAuthDetails,
dpopNonce,
).getOrThrow()
authorizedRequest(credentialOffer, tokenResponse, newDpopNonce, Grant.AuthorizationCode)
}
): Result<AuthorizedRequest> = runCatching {
ensure(serverState == state) { InvalidAuthorizationState() }
val credConfigIdsAsAuthDetails = identifiersSentAsAuthDetails.filter(authDetailsOption)
val (tokenResponse, newDpopNonce) =
tokenEndpointClient.requestAccessTokenAuthFlow(
authorizationCode,
pkceVerifier,
credConfigIdsAsAuthDetails,
dpopNonce,
).getOrThrow()

AuthorizedRequest(
accessToken = tokenResponse.accessToken,
refreshToken = tokenResponse.refreshToken,
credentialIdentifiers = tokenResponse.authorizationDetails,
timestamp = tokenResponse.timestamp,
authorizationServerDpopNonce = newDpopNonce,
resourceServerDpopNonce = null,
grant = Grant.AuthorizationCode,
)
}

override suspend fun authorizeWithPreAuthorizationCode(
txCode: String?,
Expand All @@ -122,7 +127,16 @@ internal class AuthorizeIssuanceImpl(
credConfigIdsAsAuthDetails,
dpopNonce = null,
).getOrThrow()
authorizedRequest(credentialOffer, tokenResponse, newDpopNonce, Grant.PreAuthorizedCodeGrant)

AuthorizedRequest(
accessToken = tokenResponse.accessToken,
refreshToken = tokenResponse.refreshToken,
credentialIdentifiers = tokenResponse.authorizationDetails,
timestamp = tokenResponse.timestamp,
authorizationServerDpopNonce = newDpopNonce,
resourceServerDpopNonce = null,
grant = Grant.PreAuthorizedCodeGrant,
)
}
}

Expand All @@ -149,43 +163,6 @@ private fun TxCode.validate(txCode: String?) {
}
}

private fun authorizedRequest(
offer: CredentialOffer,
tokenResponse: TokenResponse,
newDpopNonce: Nonce?,
grant: Grant,
): AuthorizedRequest {
val offerRequiresProofs = offer.credentialConfigurationIdentifiers.any {
val credentialConfiguration = offer.credentialIssuerMetadata.credentialConfigurationsSupported[it]
credentialConfiguration != null && credentialConfiguration.proofTypesSupported.values.isNotEmpty()
}
val (accessToken, refreshToken, cNonce, authorizationDetails, timestamp) = tokenResponse
return when {
cNonce != null && offerRequiresProofs ->
ProofRequired(
accessToken,
refreshToken,
cNonce = cNonce,
authorizationDetails,
timestamp,
authorizationServerDpopNonce = newDpopNonce,
resourceServerDpopNonce = null,
grant = grant,
)

else ->
NoProofRequired(
accessToken,
refreshToken,
authorizationDetails,
timestamp,
authorizationServerDpopNonce = newDpopNonce,
resourceServerDpopNonce = null,
grant = grant,
)
}
}

private fun Iterable<CredentialConfigurationIdentifier>.filter(
accessTokenOption: AccessTokenOption,
): List<CredentialConfigurationIdentifier> =
Expand Down
Loading

0 comments on commit 649547f

Please sign in to comment.