Skip to content

Commit

Permalink
feat: applied oid4vci isolated functions to wallet-api
Browse files Browse the repository at this point in the history
  • Loading branch information
chsavvaidis committed Feb 26, 2025
1 parent 462e8bf commit 05f4025
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 104 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
package id.walt.webwallet.service.exchange

import id.walt.oid4vc.OpenID4VCI
import id.walt.oid4vc.data.OpenIDProviderMetadata
import id.walt.oid4vc.requests.BatchCredentialRequest
import id.walt.oid4vc.requests.CredentialRequest
import id.walt.oid4vc.responses.BatchCredentialResponse
import id.walt.oid4vc.responses.CredentialResponse
import id.walt.webwallet.utils.WalletHttpClients
import io.klogging.logger
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.serialization.json.JsonObject

object CredentialOfferProcessor {
private val http = WalletHttpClients.getHttpClient()
Expand All @@ -30,21 +25,18 @@ object CredentialOfferProcessor {
accessToken: String,
): List<ProcessedCredentialOffer> {

providerMetadata as OpenIDProviderMetadata.Draft13

val batchCredentialRequest = BatchCredentialRequest(credReqs)

val batchResponse = http.post(providerMetadata.batchCredentialEndpoint!!) {
contentType(ContentType.Application.Json)
bearerAuth(accessToken)
setBody(batchCredentialRequest.toJSON())
}.body<JsonObject>().let { BatchCredentialResponse.fromJSON(it) }
logger.debug { "credential batch response: $batchResponse" }
val batchCredentialResponse = OpenID4VCI.sendBatchCredentialRequest(
providerMetadata = providerMetadata,
accessToken = accessToken,
batchCredentialRequest = batchCredentialRequest,
)

return (batchResponse.credentialResponses
return (batchCredentialResponse.credentialResponses
?: throw IllegalArgumentException("No credential responses returned")).indices.map {
ProcessedCredentialOffer(
batchResponse.credentialResponses!![it],
batchCredentialResponse.credentialResponses!![it],
batchCredentialRequest.credentialRequests[it]
)
}
Expand All @@ -56,14 +48,16 @@ object CredentialOfferProcessor {
accessToken: String,
): List<ProcessedCredentialOffer> {

val credReq = credReqs.first()

val credentialResponse = http.post(providerMetadata.credentialEndpoint!!) {
contentType(ContentType.Application.Json)
bearerAuth(accessToken)
setBody(credReq.toJSON())
}.body<JsonObject>().let { ProcessedCredentialOffer(CredentialResponse.fromJSON(it), credReq) }
logger.debug { "credentialResponse: $credentialResponse" }
val credentialResponse = OpenID4VCI.sendCredentialRequest(
providerMetadata = providerMetadata,
accessToken = accessToken,
credentialRequest = credReqs.first()
).let {
ProcessedCredentialOffer(
credentialResponse = it,
credentialRequest = credReqs.first()
)
}

return listOf(credentialResponse)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,18 @@ object IssuanceService : IssuanceServiceBase() {
val isEntra = EntraIssuanceRequest.isEntraIssuanceRequestUri(offer)
val processedCredentialOffers = if (isEntra) {
processMSEntraIssuanceRequest(
EntraIssuanceRequest.fromAuthorizationRequest(
entraIssuanceRequest = EntraIssuanceRequest.fromAuthorizationRequest(
AuthorizationRequest.fromHttpParametersAuto(
reqParams
)
), credentialWallet
),
credentialWallet = credentialWallet
)
} else {
processCredentialOffer(
credentialWallet.resolveCredentialOffer(CredentialOfferRequest.fromHttpParameters(reqParams)),
credentialWallet,
clientId
credentialOffer = OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(offer),
credentialWallet = credentialWallet,
clientId = clientId
)
}
// === original ===
Expand All @@ -67,16 +68,15 @@ object IssuanceService : IssuanceServiceBase() {
credentialWallet: TestCredentialWallet,
clientId: String,
): List<ProcessedCredentialOffer> {
val providerMetadata = getCredentialIssuerOpenIDMetadata(
credentialOffer.credentialIssuer,
credentialWallet,
)

val providerMetadata = OpenID4VCI.resolveCIProviderMetadata(credentialOffer)

logger.debug { "providerMetadata: $providerMetadata" }

logger.debug { "// resolve offered credentials" }
val offeredCredentials = OpenID4VCI.resolveOfferedCredentials(credentialOffer, providerMetadata)
logger.debug { "offeredCredentials: $offeredCredentials" }

require(offeredCredentials.isNotEmpty()) { "Resolved an empty list of offered credentials" }

logger.debug { "// fetch access token using pre-authorized code (skipping authorization step)" }
Expand All @@ -86,12 +86,14 @@ object IssuanceService : IssuanceServiceBase() {
clientId = clientId
)

val tokenResp = issueTokenRequest(
providerMetadata.tokenEndpoint!!,
tokenReq,
val tokenResp = OpenID4VCI.sendTokenRequest(
providerMetadata = providerMetadata,
tokenRequest = tokenReq
)
logger.debug { ">>> Token response is: $tokenResp" }
validateTokenResponse(tokenResp)

OpenID4VCI.validateTokenResponse(tokenResp)

//we know for a fact that there is an access token in the response
//due to the validation call above
val accessToken = tokenResp.accessToken!!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,12 @@ import id.walt.mdoc.doc.MDoc
import id.walt.mdoc.issuersigned.IssuerSigned
import id.walt.oid4vc.data.CredentialFormat
import id.walt.oid4vc.data.OfferedCredential
import id.walt.oid4vc.data.OpenIDProviderMetadata
import id.walt.oid4vc.requests.TokenRequest
import id.walt.oid4vc.responses.TokenResponse
import id.walt.oid4vc.util.randomUUID
import id.walt.sdjwt.SDJWTVCTypeMetadata
import id.walt.webwallet.service.oidc4vc.TestCredentialWallet
import id.walt.webwallet.utils.WalletHttpClients
import io.klogging.Klogger
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.util.*
import kotlinx.serialization.ExperimentalSerializationApi
Expand All @@ -35,48 +29,6 @@ abstract class IssuanceServiceBase {

protected fun parseOfferParams(offerURL: String) = Url(offerURL).parameters.toMap()

protected suspend fun getCredentialIssuerOpenIDMetadata(
issuerURL: String,
credentialWallet: TestCredentialWallet,
): OpenIDProviderMetadata {
logger.debug { "// get issuer metadata" }
val providerMetadataUri =
credentialWallet.getCIProviderMetadataUrl(issuerURL)
logger.debug { "Getting provider metadata from: $providerMetadataUri" }
val providerMetadataResult = http.get(providerMetadataUri)
logger.debug { "Provider metadata returned: ${providerMetadataResult.bodyAsText()}" }
return providerMetadataResult
.body<JsonObject>()
.let {
OpenIDProviderMetadata.fromJSON(it)
}
}

protected suspend fun issueTokenRequest(
tokenURL: String,
req: TokenRequest,
) = http.submitForm(
tokenURL, formParameters = parametersOf(req.toHttpParameters())
).let { rawResponse ->
logger.debug { "Raw TokenResponse: $rawResponse" }
rawResponse.body<JsonObject>().let {
TokenResponse.fromJSON(it)
}
}

protected fun validateTokenResponse(
tokenResponse: TokenResponse,
) {
require(tokenResponse.isSuccess) {
"token request failed: ${tokenResponse.error} ${tokenResponse.errorDescription}"
}
//there has to be an access token in the response, otherwise we are unable
//to invoke the credential endpoint
requireNotNull(tokenResponse.accessToken) {
"invalid Authorization Server token response: no access token included in the response: $tokenResponse "
}
}

protected suspend fun getCredentialData(
processedOffer: ProcessedCredentialOffer,
manifest: JsonObject?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package id.walt.webwallet.service.exchange
import id.walt.crypto.keys.Key
import id.walt.oid4vc.OpenID4VCI
import id.walt.oid4vc.data.*
import id.walt.oid4vc.requests.CredentialOfferRequest
import id.walt.oid4vc.requests.CredentialRequest
import id.walt.oid4vc.requests.EntraIssuanceRequest
import id.walt.oid4vc.requests.TokenRequest
Expand All @@ -18,14 +17,12 @@ object IssuanceServiceExternalSignatures : IssuanceServiceBase() {

suspend fun prepareExternallySignedOfferRequest(
offerURL: String,
credentialWallet: TestCredentialWallet,
publicKey: Key,
did: String,
didAuthKeyId: String,
): PrepareExternalClaimResult {
logger.debug { "// -------- WALLET: PREPARE STEP FOR OID4VCI WITH EXTERNAL SIGNATURES ----------" }
logger.debug { "// parse credential URI" }
val reqParams = parseOfferParams(offerURL)

// entra or openid4vc credential offer
val isEntra = EntraIssuanceRequest.isEntraIssuanceRequestUri(offerURL)
Expand All @@ -34,8 +31,7 @@ object IssuanceServiceExternalSignatures : IssuanceServiceBase() {
throw UnsupportedOperationException("MS Entra credential issuance requests with externally provided signatures are not supported yet")
} else {
processPrepareCredentialOffer(
credentialWallet.resolveCredentialOffer(CredentialOfferRequest.fromHttpParameters(reqParams)),
credentialWallet,
OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(offerURL),
did,
didAuthKeyId,
publicKey,
Expand All @@ -45,15 +41,11 @@ object IssuanceServiceExternalSignatures : IssuanceServiceBase() {

private suspend fun processPrepareCredentialOffer(
credentialOffer: CredentialOffer,
credentialWallet: TestCredentialWallet,
did: String,
didAuthKeyId: String,
publicKey: Key,
): PrepareExternalClaimResult {
val providerMetadata = getCredentialIssuerOpenIDMetadata(
credentialOffer.credentialIssuer,
credentialWallet,
) as OpenIDProviderMetadata.Draft13
val providerMetadata = OpenID4VCI.resolveCIProviderMetadata(credentialOffer)

logger.debug { "providerMetadata: $providerMetadata" }

Expand All @@ -69,12 +61,13 @@ object IssuanceServiceExternalSignatures : IssuanceServiceBase() {
clientId = did
)

val tokenResp = issueTokenRequest(
providerMetadata.tokenEndpoint!!,
tokenReq,
val tokenResp = OpenID4VCI.sendTokenRequest(
providerMetadata = providerMetadata,
tokenRequest = tokenReq
)

logger.debug { ">>> Token response is: $tokenResp" }
validateTokenResponse(tokenResp)
OpenID4VCI.validateTokenResponse(tokenResp)

val offeredCredentialsProofRequests = offeredCredentials.map { offeredCredential ->
OfferedCredentialProofOfPossessionParameters(
Expand Down Expand Up @@ -147,10 +140,8 @@ object IssuanceServiceExternalSignatures : IssuanceServiceBase() {
accessToken: String?,
): List<ProcessedCredentialOffer> {
logger.debug { "// get issuer metadata" }
val providerMetadata = getCredentialIssuerOpenIDMetadata(
credentialIssuerURL,
credentialWallet,
)
val providerMetadata = OpenID4VCI.resolveCIProviderMetadata(credentialIssuerURL)

logger.debug { "providerMetadata: $providerMetadata" }
logger.debug { "Using issuer URL: $credentialIssuerURL" }
val credReqs = offeredCredentialProofsOfPossession.map { offeredCredentialProofOfPossession ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class ExternalSignatureClaimStrategy(
did = did,
didAuthKeyId = didAuthKeyId,
publicKey = publicKey,
credentialWallet = SSIKit2WalletService.getCredentialWallet(did),
)

suspend fun submitCredentialClaim(
Expand Down

0 comments on commit 05f4025

Please sign in to comment.