Skip to content

Commit

Permalink
Synchronized with waltid-openid4vc
Browse files Browse the repository at this point in the history
  • Loading branch information
waltkb committed Nov 20, 2023
1 parent bd311e7 commit 8db6059
Show file tree
Hide file tree
Showing 9 changed files with 459 additions and 189 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import id.walt.oid4vc.responses.CredentialResponse
import kotlin.time.Duration

class CredentialError(
credentialRequest: CredentialRequest, val errorCode: CredentialErrorCode,
credentialRequest: CredentialRequest?,
val errorCode: CredentialErrorCode,
val errorUri: String? = null, val cNonce: String? = null, val cNonceExpiresIn: Duration? = null,
override val message: String? = null
) : Exception() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package id.walt.oid4vc.errors

import id.walt.oid4vc.data.CredentialOffer
import id.walt.oid4vc.requests.CredentialOfferRequest

class CredentialOfferError(
val credentialOfferRequest: CredentialOfferRequest, val errorCode: CredentialOfferErrorCode, override val message: String? = null
) : Exception() {
}
val credentialOfferRequest: CredentialOfferRequest?,
val credentialOffer: CredentialOffer?,
val errorCode: CredentialOfferErrorCode,
override val message: String? = null
) : Exception()

enum class CredentialOfferErrorCode {
invalid_request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ abstract class OpenIDCredentialIssuer(
* Override, to use custom path, by default, the path will be: "$baseUrl/credential_offer/<session_id>, e.g.: "https://issuer.myhost.com/api/credential_offer/1234-4567-8900"
* @param issuanceSession The issuance session for which the credential offer uri is created
*/
open protected fun getCredentialOfferByReferenceUri(issuanceSession: IssuanceSession): String {
protected open fun getCredentialOfferByReferenceUri(issuanceSession: IssuanceSession): String {
return URLBuilder(baseUrl).appendPathSegments("credential_offer", issuanceSession.id).buildString()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,32 +160,65 @@ abstract class OpenIDCredentialWallet<S : SIOPSession>(

// ==========================================================
// =============== issuance flow ===========================
open fun getCredentialOffer(credentialOfferRequest: CredentialOfferRequest): CredentialOffer {
open fun resolveCredentialOffer(credentialOfferRequest: CredentialOfferRequest): CredentialOffer {
return credentialOfferRequest.credentialOffer ?: credentialOfferRequest.credentialOfferUri?.let { uri ->
httpGetAsJson(Url(uri))?.jsonObject?.let { CredentialOffer.fromJSON(it) }
} ?: throw CredentialOfferError(
credentialOfferRequest,
null,
CredentialOfferErrorCode.invalid_request,
"No credential offer value found on request, and credential offer could not be fetched by reference from given credential_offer_uri"
)
}

open fun executePreAuthorizedCodeFlow(
credentialOffer: CredentialOffer,
holderDid: String,
client: OpenIDClientConfig,
userPIN: String?
): List<CredentialResponse> {
if (!credentialOffer.grants.containsKey(GrantType.pre_authorized_code.value)) throw CredentialOfferError(
null,
credentialOffer,
CredentialOfferErrorCode.invalid_request,
"Pre-authorized code issuance flow executed, but no pre-authorized_code found on credential offer"
)
val issuerMetadataUrl = getCIProviderMetadataUrl(credentialOffer.credentialIssuer)
val issuerMetadata =
httpGetAsJson(Url(issuerMetadataUrl))?.jsonObject?.let { OpenIDProviderMetadata.fromJSON(it) } ?: throw CredentialOfferError(
null,
credentialOffer,
CredentialOfferErrorCode.invalid_issuer,
"Could not resolve issuer provider metadata from $issuerMetadataUrl"
)
val authorizationServerMetadata = issuerMetadata.authorizationServer?.let { authServer ->
httpGetAsJson(Url(getCommonProviderMetadataUrl(authServer)))?.jsonObject?.let { OpenIDProviderMetadata.fromJSON(it) }
} ?: issuerMetadata
val offeredCredentials = credentialOffer.resolveOfferedCredentials(issuerMetadata)

return executeAuthorizedIssuanceCodeFlow(
authorizationServerMetadata, issuerMetadata, credentialOffer, GrantType.pre_authorized_code,
offeredCredentials, holderDid, client, null, null, userPIN
)
}

@OptIn(ExperimentalEncodingApi::class)
open fun executeFullAuthIssuance(
credentialOfferRequest: CredentialOfferRequest,
credentialOffer: CredentialOffer,
holderDid: String,
client: OpenIDClientConfig
): List<CredentialResponse> {
val credentialOffer = getCredentialOffer(credentialOfferRequest)
if (!credentialOffer.grants.containsKey(GrantType.authorization_code.value)) throw CredentialOfferError(
credentialOfferRequest,
null,
credentialOffer,
CredentialOfferErrorCode.invalid_request,
"Full authorization issuance flow executed, but no authorization_code found on credential offer"
)
val issuerMetadataUrl = getCIProviderMetadataUrl(credentialOffer.credentialIssuer)
val issuerMetadata =
httpGetAsJson(Url(issuerMetadataUrl))?.jsonObject?.let { OpenIDProviderMetadata.fromJSON(it) } ?: throw CredentialOfferError(
credentialOfferRequest,
null,
credentialOffer,
CredentialOfferErrorCode.invalid_issuer,
"Could not resolve issuer provider metadata from $issuerMetadataUrl"
)
Expand Down Expand Up @@ -250,6 +283,8 @@ abstract class OpenIDCredentialWallet<S : SIOPSession>(
location =
if (location.parameters.contains("response_type") && location.parameters["response_type"] == ResponseType.id_token.name) {
executeIdTokenAuthorization(location, holderDid, client)
} else if (location.parameters.contains("response_type") && location.parameters["response_type"] == ResponseType.vp_token.name) {
executeVpTokenAuthorization(location, holderDid, client)
} else location

val code = location.parameters["code"] ?: throw AuthorizationError(
Expand All @@ -258,7 +293,59 @@ abstract class OpenIDCredentialWallet<S : SIOPSession>(
"No authorization code received from server"
)

val tokenReq = TokenRequest(GrantType.authorization_code, client.clientID, config.redirectUri, code, codeVerifier = codeVerifier)
return executeAuthorizedIssuanceCodeFlow(
authorizationServerMetadata, issuerMetadata, credentialOffer,
GrantType.authorization_code, offeredCredentials, holderDid, client, code, codeVerifier
)
}

open fun fetchDeferredCredential(credentialOffer: CredentialOffer, credentialResponse: CredentialResponse): CredentialResponse {
if (credentialResponse.acceptanceToken.isNullOrEmpty()) throw CredentialOfferError(
null,
credentialOffer,
CredentialOfferErrorCode.invalid_request,
"Credential offer has no acceptance token for fetching deferred credential"
)
val issuerMetadataUrl = getCIProviderMetadataUrl(credentialOffer.credentialIssuer)
val issuerMetadata =
httpGetAsJson(Url(issuerMetadataUrl))?.jsonObject?.let { OpenIDProviderMetadata.fromJSON(it) } ?: throw CredentialOfferError(
null,
credentialOffer,
CredentialOfferErrorCode.invalid_issuer,
"Could not resolve issuer provider metadata from $issuerMetadataUrl"
)
if (issuerMetadata.deferredCredentialEndpoint.isNullOrEmpty()) throw CredentialOfferError(
null,
credentialOffer,
CredentialOfferErrorCode.invalid_issuer,
"No deferred credential endpoint found in issuer metadata"
)
val deferredCredResp = httpSubmitForm(Url(issuerMetadata.deferredCredentialEndpoint), parametersOf(), headers {
append(HttpHeaders.Authorization, "Bearer ${credentialResponse.acceptanceToken}")
})
if (!deferredCredResp.status.isSuccess() || deferredCredResp.body.isNullOrEmpty()) throw CredentialError(
null,
CredentialErrorCode.server_error,
"No credential received from deferred credential endpoint, or server responded with error status ${deferredCredResp.status}"
)
return CredentialResponse.fromJSONString(deferredCredResp.body)
}

protected open fun executeAuthorizedIssuanceCodeFlow(
authorizationServerMetadata: OpenIDProviderMetadata, issuerMetadata: OpenIDProviderMetadata,
credentialOffer: CredentialOffer,
grantType: GrantType, offeredCredentials: List<OfferedCredential>, holderDid: String,
client: OpenIDClientConfig, authorizationCode: String? = null, codeVerifier: String? = null, userPIN: String? = null
): List<CredentialResponse> {
val tokenReq = TokenRequest(
grantType,
client.clientID,
config.redirectUri,
authorizationCode,
credentialOffer.grants[grantType.value]?.preAuthorizedCode,
userPIN,
codeVerifier
)
val tokenHttpResp = httpSubmitForm(Url(authorizationServerMetadata.tokenEndpoint!!), parametersOf(tokenReq.toHttpParameters()))
if (!tokenHttpResp.status.isSuccess() || tokenHttpResp.body == null) throw TokenError(
tokenReq,
Expand Down Expand Up @@ -328,7 +415,6 @@ abstract class OpenIDCredentialWallet<S : SIOPSession>(
return CredentialResponse.fromJSONString(httpResp.body)
}

@OptIn(ExperimentalJsExport::class)
protected open fun executeIdTokenAuthorization(idTokenRequestUri: Url, holderDid: String, client: OpenIDClientConfig): Url {
var authReq = AuthorizationRequest.fromHttpQueryString(idTokenRequestUri.encodedQuery).let { authorizationRequest ->
authorizationRequest.customParameters["request"]?.let { AuthorizationJSONRequest.fromJSON(SDJwt.parse(it.first()).fullPayload) }
Expand Down Expand Up @@ -368,4 +454,19 @@ abstract class OpenIDCredentialWallet<S : SIOPSession>(
)
}

open fun executeVpTokenAuthorization(vpTokenRequestUri: Url, holderDid: String, client: OpenIDClientConfig): Url {
val authReq = AuthorizationRequest.fromHttpQueryString(vpTokenRequestUri.encodedQuery)
val tokenResp = processImplicitFlowAuthorization(
authReq.copy(
clientId = client.clientID,
)
)
val httpResp = httpSubmitForm(Url(authReq.responseUri ?: authReq.redirectUri!!), parametersOf(tokenResp.toHttpParameters()))
return when (httpResp.status) {
HttpStatusCode.Found -> httpResp.headers[HttpHeaders.Location]
HttpStatusCode.OK -> httpResp.body?.let { AuthorizationDirectPostResponse.fromJSONString(it) }?.redirectUri
else -> null
}?.let { Url(it) } ?: throw AuthorizationError(authReq, AuthorizationErrorCode.invalid_request, "Request could not be executed")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ data class CredentialResponse private constructor(
@SerialName("error_uri") val errorUri: String? = null,
override val customParameters: Map<String, JsonElement> = mapOf()
) : JsonDataObject() {
val isSuccess get() = format != null
val isDeferred get() = isSuccess && credential == null
val isSuccess get() = (format != null && credential != null) || isDeferred
val isDeferred get() = acceptanceToken != null
override fun toJSON() = Json.encodeToJsonElement(CredentialResponseSerializer, this).jsonObject

companion object : JsonDataObjectFactory<CredentialResponse>() {
Expand Down
Loading

0 comments on commit 8db6059

Please sign in to comment.