diff --git a/.gitignore b/.gitignore index 050735f5d..10a35c1c8 100644 --- a/.gitignore +++ b/.gitignore @@ -185,3 +185,5 @@ iOSInjectionProject/ /waltid-openid4vc/root-ca-cert.pem /waltid-openid4vc/root-ca-priv.pem /waltid-openid4vc/root-ca-pub.pem +/waltid-services/waltid-issuer-api/k8s/cert-device-potential.pem +/waltid-services/waltid-issuer-api/k8s/ec_device_key.pem diff --git a/docker-compose/docker-compose.yaml b/docker-compose/docker-compose.yaml index cbca0f497..7ebc8f356 100644 --- a/docker-compose/docker-compose.yaml +++ b/docker-compose/docker-compose.yaml @@ -14,6 +14,7 @@ services: - .env extra_hosts: - "host.docker.internal:host-gateway" + - "wase:host-gateway" volumes: - ./wallet-api/config:/waltid-wallet-api/config - ./wallet-api/walt.yaml:/waltid-wallet-api/walt.yaml diff --git a/waltid-libraries/protocols/waltid-openid4vc/README.md b/waltid-libraries/protocols/waltid-openid4vc/README.md index 841e00e32..12b9b8999 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/README.md +++ b/waltid-libraries/protocols/waltid-openid4vc/README.md @@ -72,9 +72,9 @@ business logic, for processing the OpenID4VC protocols. The examples are based on **JVM** and make use of the following libraries: - [**ktor**](https://ktor.io/) for the HTTP server endpoints and client-side request handling -- [**waltid-crypto**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/waltid-crypto) for cryptographic operations +- [**waltid-crypto**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/crypto/waltid-crypto) for cryptographic operations - [**waltid-did**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/waltid-did) for DID-related operations -- [**waltid-verifiable-credentials**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/waltid-verifiable-credentials) for credential and presentation handling +- [**waltid-verifiable-credentials**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/credentials/waltid-verifiable-credentials) for credential and presentation handling ### Issuer @@ -84,7 +84,7 @@ For the full demo issuer implementation, refer to `/src/jvmTest/kotlin/id/walt/o For the OpenID4VCI issuance protocol, implement the following endpoints: -**Well-defined endpoints:** +##### Well-defined endpoints: These endpoints are well-defined, and need to be available under this exact path, relative to your issuer base URL: @@ -95,9 +95,11 @@ These endpoints are well-defined, and need to be available under this exact path Returns the issuer [provider metadata](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata). -https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L147-L152 +https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L167-L172 -**Other required endpoints** +See also [here](#configuration-of-issuance-provider) for details about **creating the provider metadata**, required for these endpoints. + +#### Other required endpoints These endpoints can have any path, according to your requirements or preferences, but need to be referenced in the provider metadata, returned by the well-defined configuration endpoints listed above. @@ -108,7 +110,7 @@ returned by the well-defined configuration endpoints listed above. Endpoint to receive [pushed authorization requests](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-pushed-authorization-reques), referenced in the provider metadata as `pushed_authorization_request_endpoint`, see [here](https://www.rfc-editor.org/rfc/rfc9126.html#name-authorization-server-metada). - https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L153-L161 + https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L173-L181 * `GET /authorize` @@ -117,28 +119,31 @@ in provider metadata as `authorization_endpoint`, see [here](https://www.rfc-edi Not required for the pre-authorized issuance flow. -https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L162-L206 +https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L182-L226 * `POST /token` [Token endpoint](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-token-endpoint), referenced in provider metadata as `token_endpoint`, see [here](https://www.rfc-editor.org/rfc/rfc8414.html#section-2). -https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L207-L216 +https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L227-L236 * `POST /credential` [Credential endpoint](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-endpoint) to fetch the issued credential, after authorization flow is completed. Referenced in provider metadata as `credential_endpoint`, as defined [here](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p). -https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L217-L229 +https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L237-L249 + +See also [here](#crypto-operations-and-credential-issuance) for details about **generating credentials** using the library. * `POST /credential_deferred` [Deferred credential endpoint](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-deferred-credential-endpoin), -to fetch issued credential if issuance is deferred. Referenced in provider metadata as `deferred_credential_endpoint` (missing in spec). +to fetch issued credential if issuance is deferred. Referenced in provider metadata as `deferred_credential_endpoint`, as +defined [here](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p). -https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L230-L245 +https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L250-L265 * `POST /batch_credential` @@ -146,7 +151,9 @@ https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/waltid-ope fetch multiple issued credentials. Referenced in provider metadata as `batch_credential_endpoint`, as defined [here](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p). -https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L246-L258 +**Note:** The batch credential endpoint has been removed from the latest OpenID4VCI specification. Support for the new specification (credentials array in `/credential` response object) is yet to be implemented. + +https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L266-L278 #### Business logic @@ -154,21 +161,39 @@ For the business logic, implement the abstract issuance provider in `src/commonMain/kotlin/id/walt/oid4vc/providers/OpenIDCredentialIssuer.kt`, providing session and cache management, as well, as cryptographic operations for issuing credentials. -* **Configuration of issuance provider** +##### Configuration of issuance provider + +https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L49-L66 + +**Provider metadata** -https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L54-L71 +To **create the provider metadata** object for the well-defined [metadata endpoints](#well-defined-endpoints), you may make use of the helper function in the OpenID4VCI utility object: +[OpenID4VCI::createDefaultProviderMetadata](https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt#L307), +which creates the metadata based on the issuer base URL, describing the standard API endpoints, response types and signing algorithms. -* **Simple session management example** +**Note**, that this utility function does NOT add supported credential types, as it is up to the implementer, which credential types they can support. +See [here](https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/OpenIDCredentialIssuer.kt#L42) for an example how to load the list of supported credentials from a configuration. + + +##### Simple session management example Here we implement a simplistic in-memory session management: -https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L73-L78 +https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L68-L81 + +##### Crypto operations and credential issuance + +Token signing and credential issuance based on [**waltid-crypto**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/crypto/waltid-crypto), [**waltid-did**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/waltid-did) and [**waltid-verifiable-credentials**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/credentials/waltid-verifiable-credentials). + +https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L83-L160 -* **Crypto operations and credential issuance** +**Credential generation** -Token signing and credential issuance based on [**waltid-crypto**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/waltid-crypto), [**waltid-did**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/waltid-did) and [**waltid-verifiable-credentials**](https://github.com/walt-id/waltid-identity/tree/main/waltid-libraries/waltid-verifiable-credentials). +For generating W3C or SD-Jwt-VC credentials, as required for the `/credential` endpoint, the library provides two helper functions in the OpenID4VCI utility object: +* [OpenID4VCI.generateSdJwtVC](https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt#L386) +* [OpenID4VCI.generateW3CJwtVC](https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt#L439) -https://github.com/walt-id/waltid-identity/blob/main/waltid-libraries/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt#L80-L139 +For an example how to use the utility functions, see [here](https://github.com/walt-id/waltid-identity/blob/main/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt#L266-L271). ### Verifier diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/OpenIDCredentialIssuer.kt b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/OpenIDCredentialIssuer.kt deleted file mode 100644 index 71420c218..000000000 --- a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/OpenIDCredentialIssuer.kt +++ /dev/null @@ -1,387 +0,0 @@ -package id.walt.oid4vc.providers - -import cbor.Cbor -import id.walt.crypto.utils.Base64Utils.base64UrlDecode -import id.walt.mdoc.cose.COSESign1 -import id.walt.mdoc.dataelement.ByteStringElement -import id.walt.mdoc.dataelement.MapKey -import id.walt.mdoc.dataelement.StringElement -import id.walt.oid4vc.OpenID4VCI -import id.walt.oid4vc.data.* -import id.walt.oid4vc.definitions.CROSS_DEVICE_CREDENTIAL_OFFER_URL -import id.walt.oid4vc.definitions.JWTClaims -import id.walt.oid4vc.definitions.OPENID_CREDENTIAL_AUTHORIZATION_TYPE -import id.walt.oid4vc.errors.* -import id.walt.oid4vc.interfaces.CredentialResult -import id.walt.oid4vc.interfaces.ICredentialProvider -import id.walt.oid4vc.requests.* -import id.walt.oid4vc.responses.* -import id.walt.oid4vc.util.randomUUID -import io.github.oshai.kotlinlogging.KotlinLogging -import io.ktor.http.* -import kotlinx.datetime.Clock -import kotlinx.serialization.decodeFromByteArray -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes - -/** - * Base object for a service, providing issuance of verifiable credentials via the OpenID4CI issuance protocol - * e.g.: Credential issuer - */ -abstract class OpenIDCredentialIssuer( - baseUrl: String, - override val config: CredentialIssuerConfig, -) : OpenIDProvider(baseUrl), ICredentialProvider { - - private val log = KotlinLogging.logger { } - - override val metadata - get() = createDefaultProviderMetadata().copy( - credentialConfigurationsSupported = config.credentialConfigurationsSupported - ) - private var _supportedCredentialFormats: Set? = null - val supportedCredentialFormats - get() = _supportedCredentialFormats ?: (metadata.credentialConfigurationsSupported?.values?.map { it.format }?.toSet() - ?: setOf()).also { - _supportedCredentialFormats = it - } - - private fun isCredentialTypeSupported(format: CredentialFormat, types: List?, docType: String?): Boolean { - if (types.isNullOrEmpty() && docType.isNullOrEmpty()) - return false - return config.credentialConfigurationsSupported.values.any { cred -> - format == cred.format && ( - (docType != null && cred.docType == docType) || - (types != null && cred.credentialDefinition?.type != null && cred.credentialDefinition.type.containsAll(types)) - ) - } - } - - private fun isSupportedAuthorizationDetails(authorizationDetails: AuthorizationDetails): Boolean { - return authorizationDetails.type == OPENID_CREDENTIAL_AUTHORIZATION_TYPE && - config.credentialConfigurationsSupported.values.any { credentialSupported -> - credentialSupported.format == authorizationDetails.format && - ((authorizationDetails.credentialDefinition?.type != null && credentialSupported.credentialDefinition?.type?.containsAll( - authorizationDetails.credentialDefinition.type - ) == true) || - (authorizationDetails.docType != null && credentialSupported.docType == authorizationDetails.docType) - ) - // TODO: check other supported credential parameters - } - } - - override fun validateAuthorizationRequest(authorizationRequest: AuthorizationRequest): Boolean { - return authorizationRequest.authorizationDetails != null && authorizationRequest.authorizationDetails.any { - isSupportedAuthorizationDetails(it) - } - } - - override fun initializeAuthorization( - authorizationRequest: AuthorizationRequest, - expiresIn: Duration, - authServerState: String?, //the state used for additional authentication with pwd, id_token or vp_token. - ): IssuanceSession { - return if (authorizationRequest.issuerState.isNullOrEmpty()) { - if (!validateAuthorizationRequest(authorizationRequest)) { - throw AuthorizationError( - authorizationRequest, AuthorizationErrorCode.invalid_request, - "No valid authorization details for credential issuance found on authorization request" - ) - } - IssuanceSession( - randomUUID(), authorizationRequest, - Clock.System.now().plus(expiresIn), authServerState = authServerState - ) - } else { - getVerifiedSession(authorizationRequest.issuerState)?.copy(authorizationRequest = authorizationRequest) - ?: throw AuthorizationError( - authorizationRequest, AuthorizationErrorCode.invalid_request, - "No valid issuance session found for given issuer state" - ) - }.also { - val updatedSession = IssuanceSession( - id = it.id, - authorizationRequest = authorizationRequest, - expirationTimestamp = Clock.System.now().plus(5.minutes), - authServerState = authServerState, - txCode = it.txCode, - txCodeValue = it.txCodeValue, - credentialOffer = it.credentialOffer, - cNonce = it.cNonce, - customParameters = it.customParameters - ) - putSession(it.id, updatedSession) - } - } - - - open fun initializeCredentialOffer( - credentialOfferBuilder: CredentialOffer.Builder, - expiresIn: Duration, - allowPreAuthorized: Boolean, - txCode: TxCode? = null, txCodeValue: String? = null, - ): IssuanceSession { - val sessionId = randomUUID() - credentialOfferBuilder.addAuthorizationCodeGrant(sessionId) - if (allowPreAuthorized) - credentialOfferBuilder.addPreAuthorizedCodeGrant( - generateToken(sessionId, TokenTarget.TOKEN), - txCode - ) - return IssuanceSession( - id = sessionId, - authorizationRequest = null, - expirationTimestamp = Clock.System.now().plus(expiresIn), - txCode = txCode, - txCodeValue = txCodeValue, - credentialOffer = credentialOfferBuilder.build() - ).also { - putSession(it.id, it) - } - } - - private fun generateProofOfPossessionNonceFor(session: IssuanceSession): IssuanceSession { - return session.copy( - cNonce = randomUUID() - ).also { - putSession(it.id, it) - } - } - - override fun generateTokenResponse(session: IssuanceSession, tokenRequest: TokenRequest): TokenResponse { - if (tokenRequest.grantType == GrantType.pre_authorized_code && session.txCode != null && - session.txCodeValue != tokenRequest.txCode - ) { - throw TokenError( - tokenRequest, - TokenErrorCode.invalid_grant, - message = "User PIN required for this issuance session has not been provided or PIN is wrong." - ) - } - return super.generateTokenResponse(session, tokenRequest).copy( - cNonce = generateProofOfPossessionNonceFor(session).cNonce, - cNonceExpiresIn = session.expirationTimestamp - Clock.System.now() - // TODO: authorization_pending, interval - ) - } - - private fun createCredentialError( - credReq: CredentialRequest, session: IssuanceSession, - errorCode: CredentialErrorCode, message: String?, - ) = - CredentialError( - credReq, errorCode, null, - // renew c_nonce for this session, if the error was invalid_or_missing_proof - cNonce = if (errorCode == CredentialErrorCode.invalid_or_missing_proof) generateProofOfPossessionNonceFor( - session - ).cNonce else null, - cNonceExpiresIn = if (errorCode == CredentialErrorCode.invalid_or_missing_proof) session.expirationTimestamp - Clock.System.now() else null, - message = message - ) - - open fun generateCredentialResponse(credentialRequest: CredentialRequest, accessToken: String): CredentialResponse { - val accessInfo = verifyAndParseToken(accessToken, TokenTarget.ACCESS) ?: throw CredentialError( - credentialRequest, - CredentialErrorCode.invalid_token, - message = "Invalid access token" - ) - val sessionId = accessInfo[JWTClaims.Payload.subject]!!.jsonPrimitive.content - val session = getVerifiedSession(sessionId) ?: throw CredentialError( - credentialRequest, - CredentialErrorCode.invalid_token, - "Session not found for given access token, or session expired." - ) - return doGenerateCredentialResponseFor(credentialRequest, session) - } - - private fun doGenerateCredentialResponseFor( - credentialRequest: CredentialRequest, - session: IssuanceSession, - ): CredentialResponse { - val nonce = session.cNonce ?: throw createCredentialError( - credentialRequest, - session, - CredentialErrorCode.invalid_request, - "Session invalid" - ) - log.debug { "Credential request to validate: $credentialRequest" } - if (credentialRequest.proof == null || !validateProofOfPossession(credentialRequest, nonce)) { - throw createCredentialError( - credentialRequest, - session, - CredentialErrorCode.invalid_or_missing_proof, - "Invalid proof of possession" - ) - } - - if (!supportedCredentialFormats.contains(credentialRequest.format)) - throw createCredentialError( - credentialRequest, - session, - CredentialErrorCode.unsupported_credential_format, - "Credential format not supported" - ) - - // check types, credential_definition.types, docType, one of them must be supported - /*val types = credentialRequest.types ?: credentialRequest.credentialDefinition?.types - if (!isCredentialTypeSupported(credentialRequest.format, types, credentialRequest.docType)) - throw createCredentialError( - credentialRequest, - session, - CredentialErrorCode.unsupported_credential_type, - "Credential type not supported: format=${credentialRequest.format}, types=$types, docType=${credentialRequest.docType}" - ) - - */ - - // TODO: validate if requested credential was authorized - // (by authorization details, or credential offer, or scope) - - // issue credential for credential request - return createCredentialResponseFor(generateCredential(credentialRequest), session) - } - - open fun generateDeferredCredentialResponse(acceptanceToken: String): CredentialResponse { - val accessInfo = - verifyAndParseToken(acceptanceToken, TokenTarget.DEFERRED_CREDENTIAL) ?: throw DeferredCredentialError( - CredentialErrorCode.invalid_token, - message = "Invalid acceptance token" - ) - val sessionId = accessInfo[JWTClaims.Payload.subject]!!.jsonPrimitive.content - val credentialId = accessInfo[JWTClaims.Payload.jwtID]!!.jsonPrimitive.content - val session = getVerifiedSession(sessionId) ?: throw DeferredCredentialError( - CredentialErrorCode.invalid_token, - "Session not found for given access token, or session expired." - ) - // issue credential for credential request - return createCredentialResponseFor(getDeferredCredential(credentialId), session) - } - - open fun generateBatchCredentialResponse( - batchCredentialRequest: BatchCredentialRequest, - accessToken: String, - ): BatchCredentialResponse { - val accessInfo = verifyAndParseToken(accessToken, TokenTarget.ACCESS) ?: throw BatchCredentialError( - batchCredentialRequest, - CredentialErrorCode.invalid_token, - message = "Invalid access token" - ) - val sessionId = accessInfo[JWTClaims.Payload.subject]!!.jsonPrimitive.content - val session = getVerifiedSession(sessionId) ?: throw BatchCredentialError( - batchCredentialRequest, - CredentialErrorCode.invalid_token, - "Session not found for given access token, or session expired." - ) - - try { - val responses = batchCredentialRequest.credentialRequests.map { - doGenerateCredentialResponseFor(it, session) - } - return generateProofOfPossessionNonceFor(session).let { updatedSession -> - BatchCredentialResponse.success( - responses, - updatedSession.cNonce, - updatedSession.expirationTimestamp - Clock.System.now() - ) - } - } catch (error: CredentialError) { - throw BatchCredentialError( - batchCredentialRequest, - error.errorCode, - error.errorUri, - error.cNonce, - error.cNonceExpiresIn, - error.message - ) - } - } - - override fun verifyAndParseToken(token: String, target: TokenTarget): JsonObject? { - return super.verifyAndParseToken(token, target)?.let { - if (target == TokenTarget.DEFERRED_CREDENTIAL && !it.containsKey(JWTClaims.Payload.jwtID)) - null - else it - } - } - - private fun createDeferredCredentialToken(session: AuthorizationSession, credentialResult: CredentialResult) = - generateToken( - session.id, TokenTarget.DEFERRED_CREDENTIAL, - credentialResult.credentialId - ?: throw Exception("credentialId must not be null, if credential issuance is deferred.") - ) - - private fun createCredentialResponseFor( - credentialResult: CredentialResult, - session: IssuanceSession, - ): CredentialResponse { - return credentialResult.credential?.let { - CredentialResponse.success(credentialResult.format, it, customParameters = credentialResult.customParameters) - } ?: generateProofOfPossessionNonceFor(session).let { updatedSession -> - CredentialResponse.deferred( - credentialResult.format, - createDeferredCredentialToken(session, credentialResult), - updatedSession.cNonce, - updatedSession.expirationTimestamp - Clock.System.now() - ) - } - } - - private fun validateProofOfPossession(credentialRequest: CredentialRequest, nonce: String): Boolean { - log.debug { "VALIDATING: ${credentialRequest.proof} with nonce $nonce" } - log.debug { "VERIFYING ITS SIGNATURE" } - if (credentialRequest.proof == null) return false - return when { - credentialRequest.proof.isJwtProofType -> verifyTokenSignature( - TokenTarget.PROOF_OF_POSSESSION, credentialRequest.proof.jwt!! - ) && OpenID4VCI.getNonceFromProof(credentialRequest.proof) == nonce - - credentialRequest.proof.isCwtProofType -> verifyCOSESign1Signature( - TokenTarget.PROOF_OF_POSSESSION, credentialRequest.proof.cwt!! - ) && OpenID4VCI.getNonceFromProof(credentialRequest.proof) == nonce - - else -> false - } - } - - fun getCIProviderMetadataUrl(): String { - return URLBuilder(baseUrl).apply { - pathSegments = listOf(".well-known", "openid-credential-issuer") - }.buildString() - } - - open fun getCredentialOfferRequestUrl( - offerRequest: CredentialOfferRequest, - walletCredentialOfferEndpoint: String = CROSS_DEVICE_CREDENTIAL_OFFER_URL, - ): String { - val url = URLBuilder(walletCredentialOfferEndpoint).apply { - parameters.appendAll(parametersOf(offerRequest.toHttpParameters())) - }.buildString() - - log.debug { "CREATED URL: $url" } - - return url - } - - /** - * Returns the URI on which the credential offer object can be retrieved for this issuance session, if the request object is passed by reference. - * The returned URI will be used for the credential_offer_uri parameter of the credential offer request. - * Override, to use custom path, by default, the path will be: "$baseUrl/credential_offer/, 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 - */ - protected open fun getCredentialOfferByReferenceUri(issuanceSession: IssuanceSession): String { - return URLBuilder(baseUrl).appendPathSegments("credential_offer", issuanceSession.id).buildString() - } - - open fun getCredentialOfferRequest( - issuanceSession: IssuanceSession, byReference: Boolean = false, - ): CredentialOfferRequest { - return if (byReference) { - CredentialOfferRequest(null, getCredentialOfferByReferenceUri(issuanceSession)) - } else { - CredentialOfferRequest(issuanceSession.credentialOffer) - } - } -} diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt b/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt deleted file mode 100644 index 6216eb239..000000000 --- a/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt +++ /dev/null @@ -1,292 +0,0 @@ -package id.walt.oid4vc - -import com.nimbusds.jose.JWSHeader -import com.nimbusds.jwt.JWTParser -import id.walt.credentials.CredentialBuilder -import id.walt.credentials.CredentialBuilderType -import id.walt.credentials.issuance.Issuer.baseIssue -import id.walt.crypto.keys.Key -import id.walt.crypto.keys.KeyType -import id.walt.crypto.keys.jwk.JWKKey -import id.walt.did.dids.DidService -import id.walt.mdoc.dataelement.MapElement -import id.walt.oid4vc.data.* -import id.walt.oid4vc.definitions.JWTClaims -import id.walt.oid4vc.errors.* -import id.walt.oid4vc.interfaces.CredentialResult -import id.walt.oid4vc.providers.CredentialIssuerConfig -import id.walt.oid4vc.providers.IssuanceSession -import id.walt.oid4vc.providers.OpenIDCredentialIssuer -import id.walt.oid4vc.providers.TokenTarget -import id.walt.oid4vc.requests.AuthorizationRequest -import id.walt.oid4vc.requests.BatchCredentialRequest -import id.walt.oid4vc.requests.CredentialRequest -import id.walt.oid4vc.requests.TokenRequest -import id.walt.oid4vc.responses.AuthorizationErrorCode -import id.walt.oid4vc.responses.CredentialErrorCode -import id.walt.oid4vc.util.randomUUID -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* -import io.ktor.server.application.* -import io.ktor.server.engine.* -import io.ktor.server.netty.* -import io.ktor.server.plugins.contentnegotiation.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.ktor.util.* -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.jsonPrimitive -import kotlin.time.Duration.Companion.minutes - -const val CI_PROVIDER_PORT = 9001 -const val CI_PROVIDER_BASE_URL = "http://localhost:$CI_PROVIDER_PORT" - -class CITestProvider : OpenIDCredentialIssuer( - baseUrl = CI_PROVIDER_BASE_URL, - config = CredentialIssuerConfig( - credentialConfigurationsSupported = mapOf( - "VerifiableId" to CredentialSupported( - CredentialFormat.jwt_vc_json, - cryptographicBindingMethodsSupported = setOf("did"), - credentialSigningAlgValuesSupported = setOf("ES256K"), - credentialDefinition = CredentialDefinition(type = listOf("VerifiableCredential", "VerifiableId")), - customParameters = mapOf("foo" to JsonPrimitive("bar")) - ), - "VerifiableDiploma" to CredentialSupported( - CredentialFormat.jwt_vc_json, - cryptographicBindingMethodsSupported = setOf("did"), - credentialSigningAlgValuesSupported = setOf("ES256K"), - credentialDefinition = CredentialDefinition(type = listOf("VerifiableCredential", "VerifiableAttestation", "VerifiableDiploma")) - ) - ) - ) -) { - - // session management - private val authSessions: MutableMap = mutableMapOf() - - override fun getSession(id: String): IssuanceSession? = authSessions[id] - override fun putSession(id: String, session: IssuanceSession) { - authSessions[id] = session - } - override fun getSessionByAuthServerState(authServerState: String): IssuanceSession? { - TODO("Not yet implemented") - } - - override fun removeSession(id: String) { - authSessions.remove(id) - } - - // crypto operations and credential issuance - val CI_TOKEN_KEY = runBlocking { JWKKey.generate(KeyType.RSA) } - private val CI_DID_KEY = runBlocking { JWKKey.generate(KeyType.Ed25519) } - val CI_ISSUER_DID = runBlocking { DidService.registerByKey("key", CI_DID_KEY).did } - val deferredCredentialRequests = mutableMapOf() - var deferIssuance = false - - override fun signToken(target: TokenTarget, payload: JsonObject, header: JsonObject?, keyId: String?, privKey: Key?) = - runBlocking { CI_TOKEN_KEY.signJws(payload.toString().toByteArray()) } - - override fun signCWTToken( - target: TokenTarget, - payload: MapElement, - header: MapElement?, - keyId: String?, - privKey: Key? - ): String { - TODO("Not yet implemented") - } - - fun getKeyFor(token: String): Key { - return runBlocking { DidService.resolveToKey((JWTParser.parse(token).header as JWSHeader).keyID.substringBefore("#")) }.getOrThrow() - } - - override fun verifyTokenSignature(target: TokenTarget, token: String) = - runBlocking { (if (target == TokenTarget.PROOF_OF_POSSESSION) getKeyFor(token) else CI_TOKEN_KEY).verifyJws(token).isSuccess } - - override fun verifyCOSESign1Signature(target: TokenTarget, token: String): Boolean { - TODO("Not yet implemented") - } - - override fun generateCredential(credentialRequest: CredentialRequest): CredentialResult { - if (deferIssuance) return CredentialResult(credentialRequest.format, null, randomUUID()).also { - deferredCredentialRequests[it.credentialId!!] = credentialRequest - } - return doGenerateCredential(credentialRequest).also { - // for testing purposes: defer next credential if multiple credentials are issued - deferIssuance = !deferIssuance - } - } - - override fun getDeferredCredential(credentialID: String): CredentialResult { - if (deferredCredentialRequests.containsKey(credentialID)) { - return doGenerateCredential(deferredCredentialRequests[credentialID]!!) - } - throw DeferredCredentialError(CredentialErrorCode.invalid_request, message = "Invalid credential ID given") - } - - private fun doGenerateCredential(credentialRequest: CredentialRequest): CredentialResult { - if (credentialRequest.format == CredentialFormat.mso_mdoc) throw CredentialError( - credentialRequest, - CredentialErrorCode.unsupported_credential_format - ) - val types = credentialRequest.credentialDefinition?.type ?: throw CredentialError( - credentialRequest, - CredentialErrorCode.unsupported_credential_type - ) - val proofHeader = credentialRequest.proof?.jwt?.let { parseTokenHeader(it) } ?: throw CredentialError( - credentialRequest, - CredentialErrorCode.invalid_or_missing_proof, - message = "Proof must be JWT proof" - ) - val holderKid = proofHeader[JWTClaims.Header.keyID]?.jsonPrimitive?.content ?: throw CredentialError( - credentialRequest, - CredentialErrorCode.invalid_or_missing_proof, - message = "Proof JWT header must contain kid claim" - ) - return runBlocking { - CredentialBuilder(CredentialBuilderType.W3CV2CredentialBuilder).apply { - type = credentialRequest.credentialDefinition?.type ?: listOf("VerifiableCredential") - issuerDid = CI_ISSUER_DID - subjectDid = holderKid - }.buildW3C().baseIssue(CI_DID_KEY, CI_ISSUER_DID, holderKid, mapOf(), mapOf(), mapOf(), mapOf()) - }.let { - CredentialResult(CredentialFormat.jwt_vc_json, JsonPrimitive(it)) - } - } - - fun start() { - embeddedServer(Netty, port = CI_PROVIDER_PORT) { - install(ContentNegotiation) { - json() - } - routing { - get("/.well-known/openid-configuration") { - call.respond(metadata.toJSON()) - } - get("/.well-known/openid-credential-issuer") { - call.respond(metadata.toJSON()) - } - post("/par") { - val authReq = AuthorizationRequest.fromHttpParameters(call.receiveParameters().toMap()) - try { - val session = initializeAuthorization(authReq, 5.minutes, null) - call.respond(getPushedAuthorizationSuccessResponse(session).toJSON()) - } catch (exc: AuthorizationError) { - call.respond(HttpStatusCode.BadRequest, exc.toPushedAuthorizationErrorResponse().toJSON()) - } - } - get("/authorize") { - val authReq = AuthorizationRequest.fromHttpParameters(call.parameters.toMap()) - try { - val authResp = if (authReq.responseType.contains(ResponseType.Code)) { - processCodeFlowAuthorization(authReq) - } else if (authReq.responseType.contains(ResponseType.Token)) { - processImplicitFlowAuthorization(authReq) - } else { - throw AuthorizationError( - authReq, - AuthorizationErrorCode.unsupported_response_type, - "Response type not supported" - ) - } - val redirectUri = if (authReq.isReferenceToPAR) { - getPushedAuthorizationSession(authReq).authorizationRequest?.redirectUri - } else { - authReq.redirectUri - } ?: throw AuthorizationError( - authReq, - AuthorizationErrorCode.invalid_request, - "No redirect_uri found for this authorization request" - ) - call.response.apply { - status(HttpStatusCode.Found) - val defaultResponseMode = - if (authReq.responseType.contains(ResponseType.Code)) ResponseMode.query else ResponseMode.fragment - header( - HttpHeaders.Location, - authResp.toRedirectUri(redirectUri, authReq.responseMode ?: defaultResponseMode) - ) - } - } catch (authExc: AuthorizationError) { - call.response.apply { - status(HttpStatusCode.Found) - header(HttpHeaders.Location, URLBuilder(authExc.authorizationRequest.redirectUri!!).apply { - parameters.appendAll( - parametersOf( - authExc.toAuthorizationErrorResponse().toHttpParameters() - ) - ) - }.buildString()) - } - } - } - post("/token") { - val params = call.receiveParameters().toMap() - val tokenReq = TokenRequest.fromHttpParameters(params) - try { - val tokenResp = processTokenRequest(tokenReq) - call.respond(tokenResp.toJSON()) - } catch (exc: TokenError) { - call.respond(HttpStatusCode.BadRequest, exc.toAuthorizationErrorResponse().toJSON()) - } - } - post("/credential") { - val accessToken = call.request.header(HttpHeaders.Authorization)?.substringAfter(" ") - if (accessToken.isNullOrEmpty() || !verifyTokenSignature(TokenTarget.ACCESS, accessToken)) { - call.respond(HttpStatusCode.Unauthorized) - } else { - val credReq = CredentialRequest.fromJSON(call.receive()) - try { - call.respond(generateCredentialResponse(credReq, accessToken).toJSON()) - } catch (exc: CredentialError) { - call.respond(HttpStatusCode.BadRequest, exc.toCredentialErrorResponse().toJSON()) - } - } - } - post("/credential_deferred") { - val accessToken = call.request.header(HttpHeaders.Authorization)?.substringAfter(" ") - if (accessToken.isNullOrEmpty() || !verifyTokenSignature( - TokenTarget.DEFERRED_CREDENTIAL, - accessToken - ) - ) { - call.respond(HttpStatusCode.Unauthorized) - } else { - try { - call.respond(generateDeferredCredentialResponse(accessToken).toJSON()) - } catch (exc: DeferredCredentialError) { - call.respond(HttpStatusCode.BadRequest, exc.toCredentialErrorResponse().toJSON()) - } - } - } - post("/batch_credential") { - val accessToken = call.request.header(HttpHeaders.Authorization)?.substringAfter(" ") - if (accessToken.isNullOrEmpty() || !verifyTokenSignature(TokenTarget.ACCESS, accessToken)) { - call.respond(HttpStatusCode.Unauthorized) - } else { - val req = BatchCredentialRequest.fromJSON(call.receive()) - try { - call.respond(generateBatchCredentialResponse(req, accessToken).toJSON()) - } catch (exc: BatchCredentialError) { - call.respond(HttpStatusCode.BadRequest, exc.toBatchCredentialErrorResponse().toJSON()) - } - } - } - get("/credential_offer/{session_id}") { - val sessionId = call.parameters["session_id"]!! - val credentialOffer = getSession(sessionId)?.credentialOffer - if (credentialOffer != null) { - call.respond(HttpStatusCode.Created, credentialOffer.toJSON()) - } else { - call.respond(HttpStatusCode.NotFound, "Issuance session with given ID not found") - } - } - } - }.start() - } -} - diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CI_JVM_Test.kt b/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CI_JVM_Test.kt index 004491eaf..d341c740e 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CI_JVM_Test.kt +++ b/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/CI_JVM_Test.kt @@ -117,7 +117,6 @@ class CI_JVM_Test { private val testCIClientConfig = OpenIDClientConfig("test-client", null, redirectUri = "http://blank") companion object { - private lateinit var ciTestProvider: CITestProvider private lateinit var credentialWallet: TestCredentialWallet @BeforeAll @@ -125,9 +124,7 @@ class CI_JVM_Test { fun init() = runTest { DidService.minimalInit() assertContains(DidService.registrarMethods.keys, "web") - ciTestProvider = CITestProvider() credentialWallet = TestCredentialWallet(CredentialWalletConfig("http://blank")) - ciTestProvider.start() } } @@ -205,20 +202,6 @@ class CI_JVM_Test { println("metadataParsed: $metadataParsed") } - @Test - fun testFetchAndParseMetadata() = runTest { - val response = ktorClient.get("${CI_PROVIDER_BASE_URL}/.well-known/openid-configuration") - println("response: $response") - assertEquals(expected = HttpStatusCode.OK, actual = response.status) - val respText = response.bodyAsText() - val metadata: OpenIDProviderMetadata = OpenIDProviderMetadata.fromJSONString(respText) - println("metadata: $metadata") - assertEquals( - expected = Json.parseToJsonElement(ciTestProvider.metadata.toJSONString()), - actual = Json.parseToJsonElement(metadata.toJSONString()) - ) - } - @Test fun testAuthorizationRequestSerialization() { val authorizationReq = "response_type=code" + @@ -254,33 +237,6 @@ class CI_JVM_Test { ) } - @Test - fun testInvalidAuthorizationRequest() = runTest { - // 0. get issuer metadata - val providerMetadata = - ktorClient.get(ciTestProvider.getCIProviderMetadataUrl()).call.body() - assertNotNull(actual = providerMetadata.pushedAuthorizationRequestEndpoint) - - // 1. send pushed authorization request with authorization details, containing info of credentials to be issued, receive session id - val authReq = AuthorizationRequest( - responseType = setOf(ResponseType.Code), - clientId = testCIClientConfig.clientID, - redirectUri = credentialWallet.config.redirectUri, - authorizationDetails = listOf( - AuthorizationDetails( - type = OPENID_CREDENTIAL_AUTHORIZATION_TYPE - ) - ) - ) - val parResp = ktorClient.submitForm( - providerMetadata.pushedAuthorizationRequestEndpoint!!, - formParameters = parametersOf(authReq.toHttpParameters()) - ).body().let { PushedAuthorizationResponse.fromJSON(it) } - - assertFalse(actual = parResp.isSuccess) - assertEquals(expected = "invalid_request", actual = parResp.error) - } - private fun verifyIssuerAndSubjectId(credential: JsonObject, issuerId: String, subjectId: String) { assertEquals(expected = issuerId, actual = credential["issuer"]?.jsonPrimitive?.contentOrNull) //credential["credentialSubject"]?.jsonObject?.get("id")?.jsonPrimitive?.contentOrNull shouldBe subjectId // TODO <-- use this @@ -290,1719 +246,6 @@ class CI_JVM_Test { ) // FIXME <-- remove } - @Test - fun testFullAuthCodeFlow() = runTest { - println("// 0. get issuer metadata") - val providerMetadata = - ktorClient.get(ciTestProvider.getCIProviderMetadataUrl()).call.body() - println("providerMetadata: $providerMetadata") - assertNotNull(actual = providerMetadata.pushedAuthorizationRequestEndpoint) - - println("// 1. send pushed authorization request with authorization details, containing info of credentials to be issued, receive session id") - val pushedAuthReq = AuthorizationRequest( - responseType = setOf(ResponseType.Code), - clientId = testCIClientConfig.clientID, - redirectUri = credentialWallet.config.redirectUri, - authorizationDetails = listOf( - AuthorizationDetails( - type = OPENID_CREDENTIAL_AUTHORIZATION_TYPE, - format = CredentialFormat.jwt_vc_json, - credentialDefinition = CredentialDefinition(type = listOf("VerifiableCredential", "VerifiableId")) - ), AuthorizationDetails( - type = OPENID_CREDENTIAL_AUTHORIZATION_TYPE, - format = CredentialFormat.jwt_vc_json, - credentialDefinition = CredentialDefinition(type = listOf("VerifiableCredential", "VerifiableAttestation", "VerifiableDiploma")) - ) - ) - ) - println("pushedAuthReq: $pushedAuthReq") - - val pushedAuthResp = ktorClient.submitForm( - providerMetadata.pushedAuthorizationRequestEndpoint!!, - formParameters = parametersOf(pushedAuthReq.toHttpParameters()) - ).body().let { PushedAuthorizationResponse.fromJSON(it) } - println("pushedAuthResp: $pushedAuthResp") - - assertTrue(actual = pushedAuthResp.isSuccess) - assertTrue(actual = pushedAuthResp.requestUri!!.startsWith("urn:ietf:params:oauth:request_uri:")) - - println("// 2. call authorize endpoint with request uri, receive HTTP redirect (302 Found) with Location header") - assertNotNull(actual = providerMetadata.authorizationEndpoint) - val authReq = AuthorizationRequest( - responseType = setOf(ResponseType.Code), - clientId = testCIClientConfig.clientID, - requestUri = pushedAuthResp.requestUri - ) - println("authReq: $authReq") - val authResp = ktorClient.get(providerMetadata.authorizationEndpoint!!) { - url { - parameters.appendAll(parametersOf(authReq.toHttpParameters())) - } - } - println("authResp: $authResp") - assertEquals(expected = HttpStatusCode.Found, actual = authResp.status) - assertContains(iterable = authResp.headers.names(), element = HttpHeaders.Location) - val location = Url(authResp.headers[HttpHeaders.Location]!!) - println("location: $location") - assertTrue(actual = location.toString().startsWith(credentialWallet.config.redirectUri!!)) - assertContains(iterable = location.parameters.names(), element = ResponseType.Code.name.lowercase()) - - println("// 3. Parse code response parameter from authorization redirect URI") - assertNotNull(actual = providerMetadata.tokenEndpoint) - - val tokenReq = TokenRequest( - grantType = GrantType.authorization_code, - clientId = testCIClientConfig.clientID, - redirectUri = credentialWallet.config.redirectUri, - code = location.parameters["code"]!! - ) - println("tokenReq: $tokenReq") - - println("// 4. Call token endpoint with code from authorization response, receive access token from response") - val tokenResp = ktorClient.submitForm( - providerMetadata.tokenEndpoint!!, - formParameters = parametersOf(tokenReq.toHttpParameters()) - ).body().let { TokenResponse.fromJSON(it) } - println("tokenResp: $tokenResp") - assertTrue(actual = tokenResp.isSuccess) - assertNotNull(actual = tokenResp.accessToken) - assertNotNull(actual = tokenResp.cNonce) - - println("// 5a. Call credential endpoint with access token, to receive credential (synchronous issuance)") - assertNotNull(actual = providerMetadata.credentialEndpoint) - ciTestProvider.deferIssuance = false - var nonce = tokenResp.cNonce!! - - val credReq = CredentialRequest.forAuthorizationDetails( - pushedAuthReq.authorizationDetails!!.first(), - credentialWallet.generateDidProof(credentialWallet.TEST_DID, ciTestProvider.baseUrl, nonce) - ) - println("credReq: $credReq") - - val credentialResp = ktorClient.post(providerMetadata.credentialEndpoint!!) { - contentType(ContentType.Application.Json) - bearerAuth(tokenResp.accessToken!!) - setBody(credReq.toJSON()) - }.body().let { CredentialResponse.fromJSON(it) } - println("credentialResp: $credentialResp") - - assertTrue(actual = credentialResp.isSuccess) - assertFalse(actual = credentialResp.isDeferred) - assertEquals(expected = CredentialFormat.jwt_vc_json, actual = credentialResp.format!!) - assertTrue(actual = credentialResp.credential!!.instanceOf(JsonPrimitive::class)) - val credential = credentialResp.credential!!.jsonPrimitive.content - println(">>> Issued credential: $credential") - //credential.issuer?.id shouldBe ciTestProvider.baseUrl - //credential.credentialSubject?.id shouldBe credentialWallet.TEST_DID - assertTrue(actual = JwtSignaturePolicy().verify(credential, null, mapOf()).isSuccess) - //Auditor.getService().verify(credential, listOf(SignaturePolicy())).result shouldBe true - - nonce = credentialResp.cNonce ?: nonce - - println("// 5b. test deferred (asynchronous) credential issuance") - assertNotNull(actual = providerMetadata.deferredCredentialEndpoint) - ciTestProvider.deferIssuance = true - - val deferredCredResp = ktorClient.post(providerMetadata.credentialEndpoint!!) { - contentType(ContentType.Application.Json) - bearerAuth(tokenResp.accessToken!!) - setBody(credReq.toJSON()) - }.body().let { CredentialResponse.fromJSON(it) } - println("deferredCredResp: $deferredCredResp") - - assertTrue(actual = deferredCredResp.isSuccess) - assertTrue(actual = deferredCredResp.isDeferred) - assertNotNull(actual = deferredCredResp.acceptanceToken) - assertNull(actual = deferredCredResp.credential) - - nonce = deferredCredResp.cNonce ?: nonce - - val deferredCredResp2 = ktorClient.post(providerMetadata.deferredCredentialEndpoint!!) { - bearerAuth(deferredCredResp.acceptanceToken!!) - }.body().let { CredentialResponse.fromJSON(it) } - println("deferredCredResp2: $deferredCredResp2") - - assertTrue(actual = deferredCredResp2.isSuccess) - assertFalse(actual = deferredCredResp2.isDeferred) - - val deferredCredential = deferredCredResp2.credential!!.jsonPrimitive.content - println(">>> Issued deferred credential: $deferredCredential") - - verifyIssuerAndSubjectId( - SDJwt.parse(deferredCredential).fullPayload["vc"]?.jsonObject!!, - ciTestProvider.CI_ISSUER_DID, credentialWallet.TEST_DID - ) - assertTrue(actual = JwtSignaturePolicy().verify(deferredCredential, null, mapOf()).isSuccess) - - nonce = deferredCredResp2.cNonce ?: nonce - - println("// 5c. test batch credential issuance (with one synchronous and one deferred credential)") - assertNotNull(actual = providerMetadata.batchCredentialEndpoint) - ciTestProvider.deferIssuance = false - - val proof = credentialWallet.generateDidProof(credentialWallet.TEST_DID, ciTestProvider.baseUrl, nonce) - println("proof: $proof") - - val batchReq = BatchCredentialRequest(pushedAuthReq.authorizationDetails!!.map { - CredentialRequest.forAuthorizationDetails(it, proof) - }) - println("batchReq: $batchReq") - - val batchResp = ktorClient.post(providerMetadata.batchCredentialEndpoint!!) { - contentType(ContentType.Application.Json) - bearerAuth(tokenResp.accessToken!!) - setBody(batchReq.toJSON()) - }.body().let { BatchCredentialResponse.fromJSON(it) } - println("batchResp: $batchResp") - - assertTrue(actual = batchResp.isSuccess) - assertEquals(expected = 2, actual = batchResp.credentialResponses!!.size) - assertFalse(actual = batchResp.credentialResponses!![0].isDeferred) - assertNotNull(actual = batchResp.credentialResponses!![0].credential) - assertTrue(actual = batchResp.credentialResponses!![1].isDeferred) - assertNotNull(actual = batchResp.credentialResponses!![1].acceptanceToken) - - val batchCred1 = - batchResp.credentialResponses!![0].credential!!.jsonPrimitive.content - assertEquals( - expected = "VerifiableId", - actual = SDJwt.parse(batchCred1).fullPayload["vc"]?.jsonObject!!["type"]?.jsonArray?.last()?.jsonPrimitive?.contentOrNull - ) - assertTrue(actual = JwtSignaturePolicy().verify(batchCred1, null, mapOf()).isSuccess) - println("batchCred1: $batchCred1") - - val batchResp2 = ktorClient.post(providerMetadata.deferredCredentialEndpoint!!) { - bearerAuth(batchResp.credentialResponses!![1].acceptanceToken!!) - }.body().let { CredentialResponse.fromJSON(it) } - println("batchResp2: $batchResp2") - - assertTrue(actual = batchResp2.isSuccess) - assertFalse(actual = batchResp2.isDeferred) - assertNotNull(actual = batchResp2.credential) - val batchCred2 = batchResp2.credential!!.jsonPrimitive.content - assertEquals( - expected = "VerifiableDiploma", - actual = SDJwt.parse(batchCred2).fullPayload["vc"]?.jsonObject!!["type"]?.jsonArray?.last()?.jsonPrimitive?.contentOrNull - ) - assertTrue(actual = JwtSignaturePolicy().verify(batchCred2, null, mapOf()).isSuccess) - } - - @Test - fun testCredentialIssuanceIsolatedFunctions() = runTest { - // TODO: consider re-implementing CITestProvider, making use of new lib functions - println("// -------- CREDENTIAL ISSUER ----------") - // init credential offer for full authorization code flow - val credOffer = CredentialOffer.Builder(ciTestProvider.baseUrl) - .addOfferedCredential("VerifiableId") - .addAuthorizationCodeGrant("test-state") - .build() - val issueReqUrl = OpenID4VCI.getCredentialOfferRequestUrl(credOffer) - - // Show credential offer request as QR code - println(issueReqUrl) - - println("// -------- WALLET ----------") -// val parsedCredOffer = runBlocking { OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) } - val parsedCredOffer = OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) - assertEquals(expected = credOffer.toJSONString(), actual = parsedCredOffer.toJSONString()) - -// val providerMetadata = runBlocking { OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) } - val providerMetadata = OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) - assertEquals(expected = parsedCredOffer.credentialIssuer, actual = providerMetadata.credentialIssuer) - - println("// resolve offered credentials") - val offeredCredentials = OpenID4VCI.resolveOfferedCredentials(parsedCredOffer, providerMetadata) - println("offeredCredentials: $offeredCredentials") - assertEquals(expected = 1, actual = offeredCredentials.size) - assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) - assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().credentialDefinition?.type?.last()) - val offeredCredential = offeredCredentials.first() - println("offeredCredentials[0]: $offeredCredential") - - println("// go through full authorization code flow to receive offered credential") - println("// auth request (short-cut, without pushed authorization request)") - val authReq = AuthorizationRequest( - setOf(ResponseType.Code), testCIClientConfig.clientID, - redirectUri = credentialWallet.config.redirectUri, - issuerState = parsedCredOffer.grants[GrantType.authorization_code.value]!!.issuerState - ) - println("authReq: $authReq") - - println("// -------- CREDENTIAL ISSUER ----------") - - // create issuance session and generate authorization code - val authCodeResponse: AuthorizationCodeResponse = AuthorizationCodeResponse.success("test-code") - val redirectUri = authCodeResponse.toRedirectUri(authReq.redirectUri ?: TODO(), authReq.responseMode ?: ResponseMode.query) - Url(redirectUri).let { - assertContains(iterable = it.parameters.names(), element = ResponseType.Code.name.lowercase()) - assertEquals( - expected = authCodeResponse.code, - actual = it.parameters[ResponseType.Code.name.lowercase()] - ) - } - - println("// -------- WALLET ----------") - println("// token req") - val tokenReq = - TokenRequest( - GrantType.authorization_code, - testCIClientConfig.clientID, - code = authCodeResponse.code!! - ) - println("tokenReq: $tokenReq") - - println("// -------- CREDENTIAL ISSUER ----------") - - // TODO: Validate authorization code - // TODO: generate access token - val accessToken = ciTestProvider.signToken( - target = TokenTarget.ACCESS, - payload = buildJsonObject { - put(JWTClaims.Payload.subject, "test-issuance-session") - put(JWTClaims.Payload.issuer, ciTestProvider.baseUrl) - put(JWTClaims.Payload.audience, TokenTarget.ACCESS.name) - put(JWTClaims.Payload.jwtID, "token-id") - }) - - val cNonce = "pop-nonce" - val tokenResponse: TokenResponse = TokenResponse.success(accessToken, "bearer", cNonce = cNonce) - - println("// -------- WALLET ----------") - assertTrue(actual = tokenResponse.isSuccess) - assertNotNull(actual = tokenResponse.accessToken) - assertNotNull(actual = tokenResponse.cNonce) - - println("// receive credential") - val nonce = tokenResponse.cNonce!! - val holderDid = TEST_WALLET_DID_WEB1 -// val holderKey = runBlocking { JWKKey.importJWK(TEST_WALLET_KEY1) }.getOrThrow() - val holderKey = JWKKey.importJWK(TEST_WALLET_KEY1).getOrThrow() -// val holderKeyId = runBlocking { holderKey.getKeyId() } - val holderKeyId = holderKey.getKeyId() - val proofKeyId = "$holderDid#$holderKeyId" - val proofOfPossession = - ProofOfPossession.JWTProofBuilder(ciTestProvider.baseUrl, null, nonce, proofKeyId).build(holderKey) - - val credReq = CredentialRequest.forOfferedCredential(offeredCredential, proofOfPossession) - println("credReq: $credReq") - - println("// -------- CREDENTIAL ISSUER ----------") - val parsedHolderKeyId = credReq.proof?.jwt?.let { JwtUtils.parseJWTHeader(it) }?.get("kid")?.jsonPrimitive?.content - assertNotNull(actual = parsedHolderKeyId) - assertTrue(actual = parsedHolderKeyId.startsWith("did:")) - val parsedHolderDid = parsedHolderKeyId.substringBefore("#") -// val resolvedKeyForHolderDid = runBlocking { DidService.resolveToKey(parsedHolderDid) }.getOrThrow() - val resolvedKeyForHolderDid = DidService.resolveToKey(parsedHolderDid).getOrThrow() - - val validPoP = credReq.proof?.validateJwtProof(resolvedKeyForHolderDid, ciTestProvider.baseUrl,null, nonce, parsedHolderKeyId) - assertTrue(actual = validPoP!!) - - val generatedCredential = ciTestProvider.generateCredential(credReq).credential - assertNotNull(generatedCredential) - val credentialResponse: CredentialResponse = CredentialResponse.success(credReq.format, generatedCredential) - - println("// -------- WALLET ----------") - assertTrue(actual = credentialResponse.isSuccess) - assertFalse(actual = credentialResponse.isDeferred) - assertEquals(expected = CredentialFormat.jwt_vc_json, actual = credentialResponse.format!!) - assertTrue(actual = credentialResponse.credential!!.instanceOf(JsonPrimitive::class)) - - println("// parse and verify credential") - val credential = credentialResponse.credential!!.jsonPrimitive.content - println(">>> Issued credential: $credential") - verifyIssuerAndSubjectId( - SDJwt.parse(credential).fullPayload["vc"]?.jsonObject!!, - ciTestProvider.CI_ISSUER_DID, credentialWallet.TEST_DID - ) - assertTrue(actual = JwtSignaturePolicy().verify(credential, null, mapOf()).isSuccess) - } - - // Test case for available authentication methods are: NONE, ID_TOKEN, VP_TOKEN, PRE_AUTHORIZED PWD(Handled by third party authorization server) - @Test - fun testCredentialIssuanceIsolatedFunctionsAuthCodeFlow() = runTest { - // TODO: consider re-implementing CITestProvider, making use of new lib functions - // is it ok to generate the credential offer using the ciTestProvider (OpenIDCredentialIssuer class) ? - val issuedCredentialId = "VerifiableId" - val baseUrl = ciTestProvider.baseUrl - ciTestProvider.deferIssuance = false - - println("// -------- CREDENTIAL ISSUER ----------") - // Init credential offer for full authorization code flow - - // Issuer Client stores the authentication method in session. - // Available authentication methods are: NONE, ID_TOKEN, VP_TOKEN, PWD(Handled by third party authorization server), PRE_AUTHORIZED. The response for each method is a redirect to the proper location. - println("// --Authentication method is NONE--") - var issuerState = "test-state-none-auth" - var issueReqUrl = testIsolatedFunctionsCreateCredentialOffer(baseUrl, issuerState, issuedCredentialId) - - // Issuer Client shows credential offer request as QR code - println(issueReqUrl) - - println("// -------- WALLET ----------") - var parsedCredOffer = OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) - var providerMetadata = OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) - assertEquals(expected = parsedCredOffer.credentialIssuer, actual = providerMetadata.credentialIssuer) - - println("// resolve offered credentials") - var offeredCredentials = OpenID4VCI.resolveOfferedCredentials(parsedCredOffer, providerMetadata) - println("offeredCredentials: $offeredCredentials") - assertEquals(expected = 1, actual = offeredCredentials.size) - assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) - assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().credentialDefinition?.type?.last()) - var offeredCredential = offeredCredentials.first() - println("offeredCredentials[0]: $offeredCredential") - - - println("// go through authorization code flow to receive offered credential") - println("// auth request (short-cut, without pushed authorization request)") - var authReqWalletState = "secured_state" - var authReqWallet = AuthorizationRequest( - setOf(ResponseType.Code), testCIClientConfig.clientID, - redirectUri = credentialWallet.config.redirectUri, - issuerState = parsedCredOffer.grants[GrantType.authorization_code.value]!!.issuerState, - state = authReqWalletState - ) - println("authReq: $authReqWallet") - - // Wallet client calls /authorize endpoint - - - println("// -------- CREDENTIAL ISSUER ----------") - var authReq = OpenID4VCI.validateAuthorizationRequestQueryString(authReqWallet.toHttpQueryString()) - - // Issuer Client retrieves issuance session based on issuer state and stores the credential request, including authReqWallet state - // Available authentication methods are: NONE, ID_TOKEN, VP_TOKEN, PWD(Handled by third party authorization server). The response for each method is a redirect to the proper location. - // Issuer Client checks the authentication method of the session - - println("// --Authentication method is NONE--") - // Issuer Client generates authorization code - var authorizationCode = "secured_code" - var authCodeResponse: AuthorizationCodeResponse = AuthorizationCodeResponse.success(authorizationCode, mapOf("state" to listOf(authReqWallet.state!!))) - var redirectUri = testCredentialIssuanceIsolatedFunctionsAuthCodeFlowRedirectWithCode(authCodeResponse, authReqWallet) - - // Issuer client redirects the request to redirectUri - - println("// -------- WALLET ----------") - println("// token req") - var tokenReq = - TokenRequest( - GrantType.authorization_code, - testCIClientConfig.clientID, - code = authCodeResponse.code!! - ) - println("tokenReq: $tokenReq") - - - println("// -------- CREDENTIAL ISSUER ----------") - // Validate token request against authorization code - OpenID4VCI.validateTokenRequestRaw(tokenReq.toHttpParameters(), authorizationCode) - - // Generate Access Token - var expirationTime = (Clock.System.now().epochSeconds + 864000L) // ten days in milliseconds - - var accessToken = OpenID4VCI.signToken( - privateKey = ciTestProvider.CI_TOKEN_KEY, - payload = buildJsonObject { - put(JWTClaims.Payload.audience, ciTestProvider.baseUrl) - put(JWTClaims.Payload.subject, authReq.clientId) - put(JWTClaims.Payload.issuer, ciTestProvider.baseUrl) - put(JWTClaims.Payload.expirationTime, expirationTime) - put(JWTClaims.Payload.notBeforeTime, Clock.System.now().epochSeconds) - } - ) - - // Issuer client creates cPoPnonce - var cPoPNonce = "secured_cPoPnonce" - var tokenResponse: TokenResponse = TokenResponse.success(accessToken, "bearer", cNonce = cPoPNonce, expiresIn = expirationTime) - - // Issuer client sends successful response with tokenResponse - - - println("// -------- WALLET ----------") - assertTrue(actual = tokenResponse.isSuccess) - assertNotNull(actual = tokenResponse.accessToken) - assertNotNull(actual = tokenResponse.cNonce) - - println("// receive credential") - var nonce = tokenResponse.cNonce!! - val holderDid = TEST_WALLET_DID_WEB1 -// val holderKey = runBlocking { JWKKey.importJWK(TEST_WALLET_KEY1) }.getOrThrow() - val holderKey = JWKKey.importJWK(TEST_WALLET_KEY1).getOrThrow() -// val holderKeyId = runBlocking { holderKey.getKeyId() } - val holderKeyId = holderKey.getKeyId() - val proofKeyId = "$holderDid#$holderKeyId" - var proofOfPossession = - ProofOfPossession.JWTProofBuilder(ciTestProvider.baseUrl, null, nonce, proofKeyId).build(holderKey) - - var credReq = CredentialRequest.forOfferedCredential(offeredCredential, proofOfPossession) - println("credReq: $credReq") - - println("// -------- CREDENTIAL ISSUER ----------") - // Issuer Client extracts Access Token from header - OpenID4VCI.verifyToken(tokenResponse.accessToken.toString(), ciTestProvider.CI_TOKEN_KEY.getPublicKey()) - - //Then VC Stuff - - - - // ---------------------------------- - // Authentication Method is ID_TOKEN - // ---------------------------------- - println("// --Authentication method is ID_TOKEN--") - issuerState = "test-state-idtoken-auth" - issueReqUrl = testIsolatedFunctionsCreateCredentialOffer(baseUrl, issuerState, issuedCredentialId) - - // Issuer Client shows credential offer request as QR code - println(issueReqUrl) - - println("// -------- WALLET ----------") - parsedCredOffer = OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) - providerMetadata = OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) - assertEquals(expected = parsedCredOffer.credentialIssuer, actual = providerMetadata.credentialIssuer) - - println("// resolve offered credentials") - offeredCredentials = OpenID4VCI.resolveOfferedCredentials(parsedCredOffer, providerMetadata) - println("offeredCredentials: $offeredCredentials") - assertEquals(expected = 1, actual = offeredCredentials.size) - assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) - assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().credentialDefinition?.type?.last()) - offeredCredential = offeredCredentials.first() - println("offeredCredentials[0]: $offeredCredential") - - - authReqWalletState = "secured_state_idtoken" - authReqWallet = AuthorizationRequest( - setOf(ResponseType.Code), testCIClientConfig.clientID, - redirectUri = credentialWallet.config.redirectUri, - issuerState = parsedCredOffer.grants[GrantType.authorization_code.value]!!.issuerState, - state = authReqWalletState - ) - println("authReq: $authReqWallet") - - // Wallet client calls /authorize endpoint - - - println("// -------- CREDENTIAL ISSUER ----------") - authReq = OpenID4VCI.validateAuthorizationRequestQueryString(authReqWallet.toHttpQueryString()) - - // Issuer Client retrieves issuance session based on issuer state and stores the credential request, including authReqWallet state - // Available authentication methods are: NONE, ID_TOKEN, VP_TOKEN, PWD(Handled by third party authorization server). The response for each method is a redirect to the proper location. - // Issuer Client checks the authentication method of the session - - // Issuer Client generates authorization code - // Issuer client creates state and nonce for the id token authorization request - var authReqIssuerState = "secured_state_issuer_idtoken" - var authReqIssuerNonce = "secured_nonce_issue_idtoken" - - var authReqIssuer = OpenID4VCI.generateAuthorizationRequest(authReq, ciTestProvider.baseUrl, ciTestProvider.CI_TOKEN_KEY, ResponseType.IdToken, authReqIssuerState, authReqIssuerNonce) - - // Redirect uri is located in the client_metadata.authorization_endpoint or "openid://" - var redirectUriReq = authReqIssuer.toRedirectUri(authReq.clientMetadata?.customParameters?.get("authorization_endpoint")?.jsonPrimitive?.content ?: "openid://", authReq.responseMode ?: ResponseMode.query) - Url(redirectUriReq).let { - assertContains(iterable = it.parameters.names(), element = "request") - assertContains(iterable = it.parameters.names(), element = "redirect_uri") - assertEquals(expected = ciTestProvider.baseUrl + "/direct_post", actual = it.parameters["redirect_uri"]) - } - - // Issuer Client redirects the request to redirectUri - - - println("// -------- WALLET ----------") - // wallet creates id token - val idToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDprZXk6ejJkbXpEODFjZ1B4OFZraTdKYnV1TW1GWXJXUGdZb3l0eWtVWjNleXFodDFqOUtib2o3ZzlQZlhKeGJiczRLWWVneXI3RUxuRlZucERNemJKSkRETlpqYXZYNmp2dERtQUxNYlhBR1c2N3BkVGdGZWEyRnJHR1NGczhFanhpOTZvRkxHSGNMNFA2YmpMRFBCSkV2UlJIU3JHNExzUG5lNTJmY3p0Mk1XakhMTEpCdmhBQyN6MmRtekQ4MWNnUHg4VmtpN0pidXVNbUZZcldQZ1lveXR5a1VaM2V5cWh0MWo5S2JvajdnOVBmWEp4YmJzNEtZZWd5cjdFTG5GVm5wRE16YkpKREROWmphdlg2anZ0RG1BTE1iWEFHVzY3cGRUZ0ZlYTJGckdHU0ZzOEVqeGk5Nm9GTEdIY0w0UDZiakxEUEJKRXZSUkhTckc0THNQbmU1MmZjenQyTVdqSExMSkJ2aEFDIn0.eyJub25jZSI6ImE4YWE1NDYwLTRmN2UtNDRmNy05ZGE3LWU1NmQ0YjIxMWE1MSIsInN1YiI6ImRpZDprZXk6ejJkbXpEODFjZ1B4OFZraTdKYnV1TW1GWXJXUGdZb3l0eWtVWjNleXFodDFqOUtib2o3ZzlQZlhKeGJiczRLWWVneXI3RUxuRlZucERNemJKSkRETlpqYXZYNmp2dERtQUxNYlhBR1c2N3BkVGdGZWEyRnJHR1NGczhFanhpOTZvRkxHSGNMNFA2YmpMRFBCSkV2UlJIU3JHNExzUG5lNTJmY3p0Mk1XakhMTEpCdmhBQyIsImlzcyI6ImRpZDprZXk6ejJkbXpEODFjZ1B4OFZraTdKYnV1TW1GWXJXUGdZb3l0eWtVWjNleXFodDFqOUtib2o3ZzlQZlhKeGJiczRLWWVneXI3RUxuRlZucERNemJKSkRETlpqYXZYNmp2dERtQUxNYlhBR1c2N3BkVGdGZWEyRnJHR1NGczhFanhpOTZvRkxHSGNMNFA2YmpMRFBCSkV2UlJIU3JHNExzUG5lNTJmY3p0Mk1XakhMTEpCdmhBQyIsImF1ZCI6Imh0dHBzOi8vMDFiYi01LTIwMy0xNzQtNjcubmdyb2stZnJlZS5hcHAiLCJpYXQiOjE3MjExNDQ3MzYsImV4cCI6MTcyMTE0NTAzNn0.VPWyLkMQAlcc40WCNSRH-Vxaj4LHi-wf2P9kcEKDvcdyVec2xJIwkg0JF4INMbLCkF0Y89lT0oswALd345wdUg" - // wallet calls POST /direct_post (e.g. redirect_uri of Issuer Auth Req) providing the id_token - - - println("// -------- CREDENTIAL ISSUER ----------") - // Create validateIdTokenResponse() - val idTokenPayload = OpenID4VCI.validateAuthorizationRequestToken(idToken) - - // Issuer Client validates states and nonces based on idTokenPayload - - // Issuer client generates authorization code - authorizationCode = "secured_code_idtoken" - authCodeResponse = AuthorizationCodeResponse.success(authorizationCode, mapOf("state" to listOf(authReqWallet.state!!))) - - redirectUri = authCodeResponse.toRedirectUri(authReq.redirectUri ?: TODO(), authReq.responseMode ?: ResponseMode.query) - Url(redirectUri).let { - assertContains(iterable = it.parameters.names(), element = ResponseType.Code.name.lowercase()) - assertEquals( - expected = authCodeResponse.code, - actual = it.parameters[ResponseType.Code.name.lowercase()] - ) - } - - - println("// -------- WALLET ----------") - println("// token req") - tokenReq = - TokenRequest( - GrantType.authorization_code, - testCIClientConfig.clientID, - code = authCodeResponse.code!! - ) - println("tokenReq: $tokenReq") - - - println("// -------- CREDENTIAL ISSUER ----------") - // Validate token request against authorization code - OpenID4VCI.validateTokenRequestRaw(tokenReq.toHttpParameters(), authorizationCode) - - // Generate Access Token - expirationTime = (Clock.System.now().epochSeconds + 864000L) // ten days in milliseconds - - accessToken = OpenID4VCI.signToken( - privateKey = ciTestProvider.CI_TOKEN_KEY, - payload = buildJsonObject { - put(JWTClaims.Payload.audience, ciTestProvider.baseUrl) - put(JWTClaims.Payload.subject, authReq.clientId) - put(JWTClaims.Payload.issuer, ciTestProvider.baseUrl) - put(JWTClaims.Payload.expirationTime, expirationTime) - put(JWTClaims.Payload.notBeforeTime, Clock.System.now().epochSeconds) - } - ) - - // Issuer client creates cPoPnonce - cPoPNonce = "secured_cPoPnonce_idtoken" - tokenResponse = TokenResponse.success(accessToken, "bearer", cNonce = cPoPNonce, expiresIn = expirationTime) - - // Issuer client sends successful response with tokenResponse - - println("// -------- WALLET ----------") - assertTrue(actual = tokenResponse.isSuccess) - assertNotNull(actual = tokenResponse.accessToken) - assertNotNull(actual = tokenResponse.cNonce) - - println("// receive credential") - nonce = tokenResponse.cNonce!! - proofOfPossession = ProofOfPossession.JWTProofBuilder(ciTestProvider.baseUrl, null, nonce, proofKeyId).build(holderKey) - - credReq = CredentialRequest.forOfferedCredential(offeredCredential, proofOfPossession) - println("credReq: $credReq") - - println("// -------- CREDENTIAL ISSUER ----------") - // Issuer Client extracts Access Token from header - OpenID4VCI.verifyToken(tokenResponse.accessToken.toString(), ciTestProvider.CI_TOKEN_KEY.getPublicKey()) - - //Then VC Stuff - - - - // ---------------------------------- - // Authentication Method is VP_TOKEN - // ---------------------------------- - println("// --Authentication method is VP_TOKEN--") - issuerState = "test-state-vptoken-auth" - issueReqUrl = testIsolatedFunctionsCreateCredentialOffer(baseUrl, issuerState, issuedCredentialId) - - // Issuer Client shows credential offer request as QR code - println(issueReqUrl) - - println("// -------- WALLET ----------") - parsedCredOffer = OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) - providerMetadata = OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) - assertEquals(expected = parsedCredOffer.credentialIssuer, actual = providerMetadata.credentialIssuer) - - println("// resolve offered credentials") - offeredCredentials = OpenID4VCI.resolveOfferedCredentials(parsedCredOffer, providerMetadata) - println("offeredCredentials: $offeredCredentials") - assertEquals(expected = 1, actual = offeredCredentials.size) - assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) - assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().credentialDefinition?.type?.last()) - offeredCredential = offeredCredentials.first() - println("offeredCredentials[0]: $offeredCredential") - - - authReqWalletState = "secured_state_vptoken" - authReqWallet = AuthorizationRequest( - setOf(ResponseType.Code), testCIClientConfig.clientID, - redirectUri = credentialWallet.config.redirectUri, - issuerState = parsedCredOffer.grants[GrantType.authorization_code.value]!!.issuerState, - state = authReqWalletState - ) - println("authReq: $authReqWallet") - - // Wallet client calls /authorize endpoint - - - println("// -------- CREDENTIAL ISSUER ----------") - authReq = OpenID4VCI.validateAuthorizationRequestQueryString(authReqWallet.toHttpQueryString()) - - // Issuer Client retrieves issuance session based on issuer state and stores the credential request, including authReqWallet state - // Available authentication methods are: NONE, ID_TOKEN, VP_TOKEN, PWD(Handled by third party authorization server). The response for each method is a redirect to the proper location. - val requestedCredentialId = "OpenBadgeCredential" - val vpProfile = OpenId4VPProfile.EBSIV3 - val requestCredentialsArr = buildJsonArray { add(requestedCredentialId) } - val requestedTypes = requestCredentialsArr.map { - when (it) { - is JsonPrimitive -> it.contentOrNull - is JsonObject -> it["credential"]?.jsonPrimitive?.contentOrNull - else -> throw IllegalArgumentException("Invalid JSON type for requested credential: $it") - } ?: throw IllegalArgumentException("Invalid VC type for requested credential: $it") - } - - val presentationDefinition = PresentationDefinition.defaultGenerationFromVcTypesForCredentialFormat(requestedTypes, CredentialFormat.jwt_vc) - - // Issuer Client creates state and nonce for the vp_token authorization request - authReqIssuerState = "secured_state_issuer_vptoken" - authReqIssuerNonce = "secured_nonce_issuer_vptoken" - - authReqIssuer = OpenID4VCI.generateAuthorizationRequest(authReq, ciTestProvider.baseUrl, ciTestProvider.CI_TOKEN_KEY, ResponseType.VpToken, authReqIssuerState, authReqIssuerNonce, true, presentationDefinition) - - // Redirect uri is located in the client_metadata.authorization_endpoint or "openid://" - redirectUriReq = authReqIssuer.toRedirectUri(authReq.clientMetadata?.customParameters?.get("authorization_endpoint")?.jsonPrimitive?.content ?: "openid://", authReq.responseMode ?: ResponseMode.query) - Url(redirectUriReq).let { - assertContains(iterable = it.parameters.names(), element = "request") - assertContains(iterable = it.parameters.names(), element = "redirect_uri") - assertContains(iterable = it.parameters.names(), element = "presentation_definition") - assertEquals(expected = ciTestProvider.baseUrl + "/direct_post", actual = it.parameters["redirect_uri"]) - } - // Issuer Client redirects the request to redirectUri - - - println("// -------- WALLET ----------") - // wallet creates vp token - val vpToken = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa3A3QVZ3dld4bnNORHVTU2JmMTlzZ0t6cngyMjNXWTk1QXFaeUFHaWZGVnlWI3o2TWtwN0FWd3ZXeG5zTkR1U1NiZjE5c2dLenJ4MjIzV1k5NUFxWnlBR2lmRlZ5ViJ9.eyJzdWIiOiJkaWQ6a2V5Ono2TWtwN0FWd3ZXeG5zTkR1U1NiZjE5c2dLenJ4MjIzV1k5NUFxWnlBR2lmRlZ5ViIsIm5iZiI6MTcyMDc2NDAxOSwiaWF0IjoxNzIwNzY0MDc5LCJqdGkiOiJ1cm46dXVpZDpiNzE2YThlOC0xNzVlLTRhMTYtODZlMC0xYzU2Zjc4NTFhZDEiLCJpc3MiOiJkaWQ6a2V5Ono2TWtwN0FWd3ZXeG5zTkR1U1NiZjE5c2dLenJ4MjIzV1k5NUFxWnlBR2lmRlZ5ViIsIm5vbmNlIjoiNDY0YTAwMTUtNzQ1OS00Y2Y4LWJmNjgtNDg0ODQyYTE5Y2FmIiwiYXVkIjoiZGlkOmtleTp6Nk1rcDdBVnd2V3huc05EdVNTYmYxOXNnS3pyeDIyM1dZOTVBcVp5QUdpZkZWeVYiLCJ2cCI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iXSwiaWQiOiJ1cm46dXVpZDpiNzE2YThlOC0xNzVlLTRhMTYtODZlMC0xYzU2Zjc4NTFhZDEiLCJob2xkZXIiOiJkaWQ6a2V5Ono2TWtwN0FWd3ZXeG5zTkR1U1NiZjE5c2dLenJ4MjIzV1k5NUFxWnlBR2lmRlZ5ViIsInZlcmlmaWFibGVDcmVkZW50aWFsIjpbImV5SmhiR2NpT2lKRlpFUlRRU0lzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW1ScFpEcHJaWGs2ZWpaTmEzQTNRVlozZGxkNGJuTk9SSFZUVTJKbU1UbHpaMHQ2Y25neU1qTlhXVGsxUVhGYWVVRkhhV1pHVm5sV0luMC5leUpwYzNNaU9pSmthV1E2YTJWNU9ubzJUV3R3TjBGV2QzWlhlRzV6VGtSMVUxTmlaakU1YzJkTGVuSjRNakl6VjFrNU5VRnhXbmxCUjJsbVJsWjVWaUlzSW5OMVlpSTZJbVJwWkRwclpYazZlalpOYTJwdE1tZGhSM052WkVkamFHWkhOR3M0VURaTGQwTklXbk5XUlZCYWFHODFWblZGWWxrNU5IRnBRa0k1SWl3aWRtTWlPbnNpUUdOdmJuUmxlSFFpT2xzaWFIUjBjSE02THk5M2QzY3Vkek11YjNKbkx6SXdNVGd2WTNKbFpHVnVkR2xoYkhNdmRqRWlMQ0pvZEhSd2N6b3ZMM0IxY213dWFXMXpaMnh2WW1Gc0xtOXlaeTl6Y0dWakwyOWlMM1l6Y0RBdlkyOXVkR1Y0ZEM1cWMyOXVJbDBzSW1sa0lqb2lkWEp1T25WMWFXUTZNVEl6SWl3aWRIbHdaU0k2V3lKV1pYSnBabWxoWW14bFEzSmxaR1Z1ZEdsaGJDSXNJazl3Wlc1Q1lXUm5aVU55WldSbGJuUnBZV3dpWFN3aWJtRnRaU0k2SWtwR1JpQjRJSFpqTFdWa2RTQlFiSFZuUm1WemRDQXpJRWx1ZEdWeWIzQmxjbUZpYVd4cGRIa2lMQ0pwYzNOMVpYSWlPbnNpZEhsd1pTSTZXeUpRY205bWFXeGxJbDBzSW1sa0lqb2laR2xrT21WNFlXMXdiR1U2TVRJeklpd2libUZ0WlNJNklrcHZZbk1nWm05eUlIUm9aU0JHZFhSMWNtVWdLRXBHUmlraUxDSjFjbXdpT2lKb2RIUndjem92TDNkM2R5NXFabVl1YjNKbkx5SXNJbWx0WVdkbElqcDdJbWxrSWpvaWFIUjBjSE02THk5M00yTXRZMk5uTG1kcGRHaDFZaTVwYnk5Mll5MWxaQzl3YkhWblptVnpkQzB4TFRJd01qSXZhVzFoWjJWekwwcEdSbDlNYjJkdlRHOWphM1Z3TG5CdVp5SXNJblI1Y0dVaU9pSkpiV0ZuWlNKOWZTd2lhWE56ZFdGdVkyVkVZWFJsSWpvaU1qQXlNeTB3TnkweU1GUXdOem93TlRvME5Gb2lMQ0psZUhCcGNtRjBhVzl1UkdGMFpTSTZJakl3TXpNdE1EY3RNakJVTURjNk1EVTZORFJhSWl3aVkzSmxaR1Z1ZEdsaGJGTjFZbXBsWTNRaU9uc2lhV1FpT2lKa2FXUTZaWGhoYlhCc1pUb3hNak1pTENKMGVYQmxJanBiSWtGamFHbGxkbVZ0Wlc1MFUzVmlhbVZqZENKZExDSmhZMmhwWlhabGJXVnVkQ0k2ZXlKcFpDSTZJblZ5YmpwMWRXbGtPbUZqTWpVMFltUTFMVGhtWVdRdE5HSmlNUzA1WkRJNUxXVm1aRGt6T0RVek5qa3lOaUlzSW5SNWNHVWlPbHNpUVdOb2FXVjJaVzFsYm5RaVhTd2libUZ0WlNJNklrcEdSaUI0SUhaakxXVmtkU0JRYkhWblJtVnpkQ0F6SUVsdWRHVnliM0JsY21GaWFXeHBkSGtpTENKa1pYTmpjbWx3ZEdsdmJpSTZJbFJvYVhNZ2QyRnNiR1YwSUhOMWNIQnZjblJ6SUhSb1pTQjFjMlVnYjJZZ1Z6TkRJRlpsY21sbWFXRmliR1VnUTNKbFpHVnVkR2xoYkhNZ1lXNWtJR2hoY3lCa1pXMXZibk4wY21GMFpXUWdhVzUwWlhKdmNHVnlZV0pwYkdsMGVTQmtkWEpwYm1jZ2RHaGxJSEJ5WlhObGJuUmhkR2x2YmlCeVpYRjFaWE4wSUhkdmNtdG1iRzkzSUdSMWNtbHVaeUJLUmtZZ2VDQldReTFGUkZVZ1VHeDFaMFpsYzNRZ015NGlMQ0pqY21sMFpYSnBZU0k2ZXlKMGVYQmxJam9pUTNKcGRHVnlhV0VpTENKdVlYSnlZWFJwZG1VaU9pSlhZV3hzWlhRZ2MyOXNkWFJwYjI1eklIQnliM1pwWkdWeWN5QmxZWEp1WldRZ2RHaHBjeUJpWVdSblpTQmllU0JrWlcxdmJuTjBjbUYwYVc1bklHbHVkR1Z5YjNCbGNtRmlhV3hwZEhrZ1pIVnlhVzVuSUhSb1pTQndjbVZ6Wlc1MFlYUnBiMjRnY21WeGRXVnpkQ0IzYjNKclpteHZkeTRnVkdocGN5QnBibU5zZFdSbGN5QnpkV05qWlhOelpuVnNiSGtnY21WalpXbDJhVzVuSUdFZ2NISmxjMlZ1ZEdGMGFXOXVJSEpsY1hWbGMzUXNJR0ZzYkc5M2FXNW5JSFJvWlNCb2IyeGtaWElnZEc4Z2MyVnNaV04wSUdGMElHeGxZWE4wSUhSM2J5QjBlWEJsY3lCdlppQjJaWEpwWm1saFlteGxJR055WldSbGJuUnBZV3h6SUhSdklHTnlaV0YwWlNCaElIWmxjbWxtYVdGaWJHVWdjSEpsYzJWdWRHRjBhVzl1TENCeVpYUjFjbTVwYm1jZ2RHaGxJSEJ5WlhObGJuUmhkR2x2YmlCMGJ5QjBhR1VnY21WeGRXVnpkRzl5TENCaGJtUWdjR0Z6YzJsdVp5QjJaWEpwWm1sallYUnBiMjRnYjJZZ2RHaGxJSEJ5WlhObGJuUmhkR2x2YmlCaGJtUWdkR2hsSUdsdVkyeDFaR1ZrSUdOeVpXUmxiblJwWVd4ekxpSjlMQ0pwYldGblpTSTZleUpwWkNJNkltaDBkSEJ6T2k4dmR6TmpMV05qWnk1bmFYUm9kV0l1YVc4dmRtTXRaV1F2Y0d4MVoyWmxjM1F0TXkweU1ESXpMMmx0WVdkbGN5OUtSa1l0VmtNdFJVUlZMVkJNVlVkR1JWTlVNeTFpWVdSblpTMXBiV0ZuWlM1d2JtY2lMQ0owZVhCbElqb2lTVzFoWjJVaWZYMTlMQ0pqY21Wa1pXNTBhV0ZzVTJOb1pXMWhJanA3SW1sa0lqb2lhSFIwY0hNNkx5OXdkWEpzTG1sdGMyZHNiMkpoYkM1dmNtY3ZjM0JsWXk5dllpOTJNM0F3TDNOamFHVnRZUzlxYzI5dUwyOWlYM1l6Y0RCZllXTm9hV1YyWlcxbGJuUmpjbVZrWlc1MGFXRnNYM05qYUdWdFlTNXFjMjl1SWl3aWRIbHdaU0k2SWtaMWJHeEtjMjl1VTJOb1pXMWhWbUZzYVdSaGRHOXlNakF5TVNKOWZTd2lhblJwSWpvaWRYSnVPblYxYVdRNk1USXpJaXdpWlhod0lqb3lNREExTkRVMU9UUTBMQ0pwWVhRaU9qRTJPRGs0TXpZM05EUXNJbTVpWmlJNk1UWTRPVGd6TmpjME5IMC5PRHZUQXVMN2JrME1pX3hNLVFualg4azByZ3VUeWtiYzJ6bFdFMVU2SGlmVXFjWTdFVU5GcUdUZWFUWHRESkxrODBuZWN6YkNNTGh1YlZseEFkdl9DdyJdfX0.zTXluOVIP0sQzc5GzNvtVvWRiaC-x9qMZg0d-EvCuRIg7QSgY0hmrfVlAzh2IDEvaXZ1ahM3hSVDx_YI74ToAw" - // wallet calls POST /direct_post (e.g. redirect_uri of Issuer Auth Req) providing the vp_token and presentation submission - - println("// -------- CREDENTIAL ISSUER ----------") - val vpTokenPayload = OpenID4VCI.validateAuthorizationRequestToken(vpToken) - - // Issuer Client validates states and nonces based on vpTokenPayload - - // Issuer client generates authorization code - authorizationCode = "secured_code_vptoken" - authCodeResponse = AuthorizationCodeResponse.success(authorizationCode, mapOf("state" to listOf(authReqWallet.state!!))) - - redirectUri = authCodeResponse.toRedirectUri(authReq.redirectUri ?: TODO(), authReq.responseMode ?: ResponseMode.query) - Url(redirectUri).let { - assertContains(iterable = it.parameters.names(), element = ResponseType.Code.name.lowercase()) - assertEquals( - expected = authCodeResponse.code, - actual = it.parameters[ResponseType.Code.name.lowercase()] - ) - } - - - println("// -------- WALLET ----------") - println("// token req") - tokenReq = - TokenRequest( - GrantType.authorization_code, - testCIClientConfig.clientID, - code = authCodeResponse.code!! - ) - println("tokenReq: $tokenReq") - - - println("// -------- CREDENTIAL ISSUER ----------") - // Validate token request against authorization code - OpenID4VCI.validateTokenRequestRaw(tokenReq.toHttpParameters(), authorizationCode) - - // Generate Access Token - expirationTime = (Clock.System.now().epochSeconds + 864000L) // ten days in milliseconds - - accessToken = OpenID4VCI.signToken( - privateKey = ciTestProvider.CI_TOKEN_KEY, - payload = buildJsonObject { - put(JWTClaims.Payload.audience, ciTestProvider.baseUrl) - put(JWTClaims.Payload.subject, authReq.clientId) - put(JWTClaims.Payload.issuer, ciTestProvider.baseUrl) - put(JWTClaims.Payload.expirationTime, expirationTime) - put(JWTClaims.Payload.notBeforeTime, Clock.System.now().epochSeconds) - } - ) - - // Issuer client creates cPoPnonce - cPoPNonce = "secured_cPoPnonce_idtoken" - tokenResponse = TokenResponse.success(accessToken, "bearer", cNonce = cPoPNonce, expiresIn = expirationTime) - - // Issuer client sends successful response with tokenResponse - - println("// -------- WALLET ----------") - assertTrue(actual = tokenResponse.isSuccess) - assertNotNull(actual = tokenResponse.accessToken) - assertNotNull(actual = tokenResponse.cNonce) - - println("// receive credential") - nonce = tokenResponse.cNonce!! - proofOfPossession = ProofOfPossession.JWTProofBuilder(ciTestProvider.baseUrl, null, nonce, proofKeyId).build(holderKey) - - credReq = CredentialRequest.forOfferedCredential(offeredCredential, proofOfPossession) - println("credReq: $credReq") - - println("// -------- CREDENTIAL ISSUER ----------") - // Issuer Client extracts Access Token from header - OpenID4VCI.verifyToken(tokenResponse.accessToken.toString(), ciTestProvider.CI_TOKEN_KEY.getPublicKey()) - - //Then VC Stuff - - - // ---------------------------------- - // Authentication Method is PRE_AUTHORIZED - // ---------------------------------- - println("// --Authentication method is PRE_AUTHORIZED--") - val preAuthCode = "test-state-pre_auth" - val credOffer = CredentialOffer.Builder(baseUrl) - .addOfferedCredential(issuedCredentialId) - .addPreAuthorizedCodeGrant(preAuthCode) - .build() - - issueReqUrl = OpenID4VCI.getCredentialOfferRequestUrl(credOffer) - // Issuer Client shows credential offer request as QR code - println(issueReqUrl) - - println("// -------- WALLET ----------") - parsedCredOffer = OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) - providerMetadata = OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) - assertEquals(expected = parsedCredOffer.credentialIssuer, actual = providerMetadata.credentialIssuer) - - println("// resolve offered credentials") - offeredCredentials = OpenID4VCI.resolveOfferedCredentials(parsedCredOffer, providerMetadata) - println("offeredCredentials: $offeredCredentials") - assertEquals(expected = 1, actual = offeredCredentials.size) - assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) - assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().credentialDefinition?.type?.last()) - offeredCredential = offeredCredentials.first() - println("offeredCredentials[0]: $offeredCredential") - assertNotNull(actual = parsedCredOffer.grants[GrantType.pre_authorized_code.value]?.preAuthorizedCode) - - - println("// token req") - tokenReq = TokenRequest( - grantType = GrantType.pre_authorized_code, - //clientId = testCIClientConfig.clientID, - redirectUri = credentialWallet.config.redirectUri, - preAuthorizedCode = parsedCredOffer.grants[GrantType.pre_authorized_code.value]!!.preAuthorizedCode, - txCode = null - ) - - - println("// -------- CREDENTIAL ISSUER ----------") - // Validate token request against authorization code - OpenID4VCI.validateTokenRequestRaw(tokenReq.toHttpParameters(), preAuthCode) - - // Generate Access Token - expirationTime = (Clock.System.now().epochSeconds + 864000L) // ten days in milliseconds - - accessToken = OpenID4VCI.signToken( - privateKey = ciTestProvider.CI_TOKEN_KEY, - payload = buildJsonObject { - put(JWTClaims.Payload.audience, ciTestProvider.baseUrl) - put(JWTClaims.Payload.subject, authReq.clientId) - put(JWTClaims.Payload.issuer, ciTestProvider.baseUrl) - put(JWTClaims.Payload.expirationTime, expirationTime) - put(JWTClaims.Payload.notBeforeTime, Clock.System.now().epochSeconds) - } - ) - - // Issuer client creates cPoPnonce - cPoPNonce = "secured_cPoPnonce_preauthorized" - tokenResponse = TokenResponse.success(accessToken, "bearer", cNonce = cPoPNonce, expiresIn = expirationTime) - - // Issuer client sends successful response with tokenResponse - - - println("// -------- WALLET ----------") - assertTrue(actual = tokenResponse.isSuccess) - assertNotNull(actual = tokenResponse.accessToken) - assertNotNull(actual = tokenResponse.cNonce) - - println("// receive credential") - nonce = tokenResponse.cNonce!! - proofOfPossession = ProofOfPossession.JWTProofBuilder(ciTestProvider.baseUrl, null, nonce, proofKeyId).build(holderKey) - - credReq = CredentialRequest.forOfferedCredential(offeredCredential, proofOfPossession) - println("credReq: $credReq") - - println("// -------- CREDENTIAL ISSUER ----------") - // Issuer Client extracts Access Token from header - OpenID4VCI.verifyToken(tokenResponse.accessToken.toString(), ciTestProvider.CI_TOKEN_KEY.getPublicKey()) - - //Then VC Stuff - - - } - -// -// @Test -// fun testCredentialIssuanceIsolatedFunctionsAuthCodeFlowWithNoneAuth() = runTest { -// // TODO: consider re-implementing CITestProvider, making use of new lib functions -// // is it ok to generate the credential offer using the ciTestProvider (OpenIDCredentialIssuer class) ? -// -// println("// -------- CREDENTIAL ISSUER ----------") -// // init credential offer for full authorization code flow -// // Issuer Client stores the authentication method in session, NONE in this case (PRE_AUTHORIZED, PWD, ID_TOKEN, VP_TOKEN, NONE) -// val credOffer = CredentialOffer.Builder(ciTestProvider.baseUrl) -// .addOfferedCredential("VerifiableId") -// .addAuthorizationCodeGrant("test-state-none-auth") -// .build() -// val issueReqUrl = OpenID4VCI.getCredentialOfferRequestUrl(credOffer) -// ciTestProvider.deferIssuance = false -// -// // Show credential offer request as QR code -// println(issueReqUrl) -// -// -// println("// -------- WALLET ----------") -//// val parsedCredOffer = runBlocking { OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) } -// val parsedCredOffer = OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) -// assertEquals(expected = credOffer.toJSONString(), actual = parsedCredOffer.toJSONString()) -// -//// val providerMetadata = runBlocking { OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) } -// val providerMetadata = OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) -// assertEquals(expected = parsedCredOffer.credentialIssuer, actual = providerMetadata.credentialIssuer) -// -// println("// resolve offered credentials") -// val offeredCredentials = OpenID4VCI.resolveOfferedCredentials(parsedCredOffer, providerMetadata) -// println("offeredCredentials: $offeredCredentials") -// assertEquals(expected = 1, actual = offeredCredentials.size) -// assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) -// assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().types?.last()) -// val offeredCredential = offeredCredentials.first() -// println("offeredCredentials[0]: $offeredCredential") -// -// println("// go through authorization code flow to receive offered credential") -// println("// auth request (short-cut, without pushed authorization request)") -// val authReqWalletState = "secured_state" -// val authReqWallet = AuthorizationRequest( -// setOf(ResponseType.Code), testCIClientConfig.clientID, -// redirectUri = credentialWallet.config.redirectUri, -// issuerState = parsedCredOffer.grants[GrantType.authorization_code.value]!!.issuerState, -// state = authReqWalletState -// ) -// println("authReq: $authReqWallet") -// -// // Wallet client calls /authorize endpoint -// -// -// println("// -------- CREDENTIAL ISSUER ----------") -// val authReq = validateAuthorizationRequestQueryString(authReqWallet.toHttpQueryString()) -// -// // Issuer client retrieves issuance session based on issuer state and stores the credential request, including authReqWallet state -// // Available authentication methods are: NONE, ID_TOKEN, VP_TOKEN, PWD(Handled by third party authorization server). The response for each method is a redirect to the proper location. -// -// // Issuer client checks the authentication method of the session -// // A) authentication method is NONE -// -// // Issuer client generates authorization code -// -// val authorizationCode = "secured_code" -// val authCodeResponse: AuthorizationCodeResponse = AuthorizationCodeResponse.success(authorizationCode, mapOf("state" to listOf(authReqWallet.state!!))) -// -// val redirectUri = authCodeResponse.toRedirectUri(authReq.redirectUri ?: TODO(), authReq.responseMode ?: ResponseMode.query) -// Url(redirectUri).let { -// assertContains(iterable = it.parameters.names(), element = ResponseType.Code.name.lowercase()) -// assertEquals( -// expected = authCodeResponse.code, -// actual = it.parameters[ResponseType.Code.name.lowercase()] -// ) -// } -// -// // Issuer client redirects the request to redirectUri -// -// println("// -------- WALLET ----------") -// println("// token req") -// val tokenReq = -// TokenRequest( -// GrantType.authorization_code, -// testCIClientConfig.clientID, -// code = authCodeResponse.code!! -// ) -// println("tokenReq: $tokenReq") -// -// -// println("// -------- CREDENTIAL ISSUER ----------") -// // Validate token request against authorization code -// validateTokenRequestRaw(tokenReq.toHttpParameters(), authorizationCode) -// -// // Generate Access Token -// val expirationTime = (Clock.System.now().epochSeconds + 864000L) // ten days in milliseconds -// -// val accessToken = signToken( -// privateKey = ciTestProvider.CI_TOKEN_KEY, -// payload = buildJsonObject { -// put(JWTClaims.Payload.audience, ciTestProvider.baseUrl) -// put(JWTClaims.Payload.subject, authReq.clientId) -// put(JWTClaims.Payload.issuer, ciTestProvider.baseUrl) -// put(JWTClaims.Payload.expirationTime, expirationTime) -// put(JWTClaims.Payload.notBeforeTime, Clock.System.now().epochSeconds) -// } -// ) -// -// // Issuer client creates cPoPnonce -// val cPoPNonce = "secured_cPoPnonce" -// val tokenResponse: TokenResponse = TokenResponse.success(accessToken, "bearer", cNonce = cPoPNonce, expiresIn = expirationTime) -// -// // Issuer client sends successful response with tokenResponse -// -// -// println("// -------- WALLET ----------") -// assertTrue(actual = tokenResponse.isSuccess) -// assertNotNull(actual = tokenResponse.accessToken) -// assertNotNull(actual = tokenResponse.cNonce) -// -// println("// receive credential") -// val nonce = tokenResponse.cNonce!! -// val holderDid = TEST_WALLET_DID_WEB1 -//// val holderKey = runBlocking { JWKKey.importJWK(TEST_WALLET_KEY1) }.getOrThrow() -// val holderKey = JWKKey.importJWK(TEST_WALLET_KEY1).getOrThrow() -//// val holderKeyId = runBlocking { holderKey.getKeyId() } -// val holderKeyId = holderKey.getKeyId() -// val proofKeyId = "$holderDid#$holderKeyId" -// val proofOfPossession = -// ProofOfPossession.JWTProofBuilder(ciTestProvider.baseUrl, null, nonce, proofKeyId).build(holderKey) -// -// val credReq = CredentialRequest.forOfferedCredential(offeredCredential, proofOfPossession) -// println("credReq: $credReq") -// -// -// println("// -------- CREDENTIAL ISSUER ----------") -// // Issuer Client extracts Access Token from header -// verifyToken(tokenResponse.accessToken.toString(), ciTestProvider.CI_TOKEN_KEY.getPublicKey()) -// -// val parsedHolderKeyId = credReq.proof?.jwt?.let { JwtUtils.parseJWTHeader(it) }?.get("kid")?.jsonPrimitive?.content -// assertNotNull(actual = parsedHolderKeyId) -// assertTrue(actual = parsedHolderKeyId.startsWith("did:")) -// val parsedHolderDid = parsedHolderKeyId.substringBefore("#") -//// val resolvedKeyForHolderDid = runBlocking { DidService.resolveToKey(parsedHolderDid) }.getOrThrow() -// val resolvedKeyForHolderDid = DidService.resolveToKey(parsedHolderDid).getOrThrow() -// -// val validPoP = credReq.proof?.validateJwtProof(resolvedKeyForHolderDid, ciTestProvider.baseUrl,null, nonce, parsedHolderKeyId) -// assertTrue(actual = validPoP!!) -// val generatedCredential = ciTestProvider.generateCredential(credReq).credential -// assertNotNull(generatedCredential) -// val credentialResponse: CredentialResponse = CredentialResponse.success(credReq.format, generatedCredential) -// -// println("// -------- WALLET ----------") -// assertTrue(actual = credentialResponse.isSuccess) -// assertFalse(actual = credentialResponse.isDeferred) -// assertEquals(expected = CredentialFormat.jwt_vc_json, actual = credentialResponse.format!!) -// assertTrue(actual = credentialResponse.credential!!.instanceOf(JsonPrimitive::class)) -// -// println("// parse and verify credential") -// val credential = credentialResponse.credential!!.jsonPrimitive.content -// println(">>> Issued credential: $credential") -// verifyIssuerAndSubjectId( -// SDJwt.parse(credential).fullPayload["vc"]?.jsonObject!!, -// ciTestProvider.CI_ISSUER_DID, credentialWallet.TEST_DID -// ) -// assertTrue(actual = JwtSignaturePolicy().verify(credential, null, mapOf()).isSuccess) -// } -// -// @Test -// fun testCredentialIssuanceIsolatedFunctionsAuthCodeFlowWithIdTokenAuth() = runTest { -// // TODO: consider re-implementing CITestProvider, making use of new lib functions -// // is it ok to generate the credential offer using the ciTestProvider (OpenIDCredentialIssuer class) ? -// -// println("// -------- CREDENTIAL ISSUER ----------") -// // init credential offer for full authorization code flow -// // Issuer Client stores the authentication method in session, ID_TOKEN in this case (PRE_AUTHORIZED, PWD, ID_TOKEN, VP_TOKEN, NONE) -// -// val credOffer = CredentialOffer.Builder(ciTestProvider.baseUrl) -// .addOfferedCredential("VerifiableId") -// .addAuthorizationCodeGrant("test-state-idtoken-auth") -// .build() -// val issueReqUrl = OpenID4VCI.getCredentialOfferRequestUrl(credOffer) -// ciTestProvider.deferIssuance = false -// -// // Show credential offer request as QR code -// println(issueReqUrl) -// -// -// println("// -------- WALLET ----------") -//// val parsedCredOffer = runBlocking { OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) } -// val parsedCredOffer = OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) -// assertEquals(expected = credOffer.toJSONString(), actual = parsedCredOffer.toJSONString()) -// -//// val providerMetadata = runBlocking { OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) } -// val providerMetadata = OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) -// assertEquals(expected = parsedCredOffer.credentialIssuer, actual = providerMetadata.credentialIssuer) -// -// println("// resolve offered credentials") -// val offeredCredentials = OpenID4VCI.resolveOfferedCredentials(parsedCredOffer, providerMetadata) -// println("offeredCredentials: $offeredCredentials") -// assertEquals(expected = 1, actual = offeredCredentials.size) -// assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) -// assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().types?.last()) -// val offeredCredential = offeredCredentials.first() -// println("offeredCredentials[0]: $offeredCredential") -// -// println("// go through authorization code flow to receive offered credential") -// println("// auth request (short-cut, without pushed authorization request)") -// val authReqWalletState = "secured_state_wallet" -// val authReqWallet = AuthorizationRequest( -// setOf(ResponseType.Code), testCIClientConfig.clientID, -// redirectUri = credentialWallet.config.redirectUri, -// issuerState = parsedCredOffer.grants[GrantType.authorization_code.value]!!.issuerState, -// state = authReqWalletState, -// clientMetadata = OpenIDClientMetadata(customParameters=mapOf("authorization_endpoint" to "wallet-api.com/callback".toJsonElement())) -// ) -// println("authReq: $authReqWallet") -// -// // Wallet client calls /authorize endpoint -// -// println("// -------- CREDENTIAL ISSUER ----------") -// val authReq = OpenID4VCI.validateAuthorizationRequestQueryString(authReqWallet.toHttpQueryString()) -// -// // Issuer client retrieves issuance session based on issuer state and stores the credential request, including authReqWallet state -// // Issuer client checks the authentication method of the session and find out that authentication method is ID_TOKEN -// // Issuer client creates state and nonce for the id token authorization request -// val authReqIssuerState = "secured_state_issuer" -// val authReqIssuerNonce = "secured_nonce_issuer" -// -// val authReqIssuer = OpenID4VCI.generateAuthorizationRequest(authReq, ciTestProvider.baseUrl, ciTestProvider.CI_TOKEN_KEY, ResponseType.IdToken, authReqIssuerState, authReqIssuerNonce) -// -// // Redirect uri is located in the client_metadata.authorization_endpoint or "openid://" -// val redirectUriReq = authReqIssuer.toRedirectUri(authReq.clientMetadata?.customParameters?.get("authorization_endpoint")?.jsonPrimitive?.content ?: "openid://", authReq.responseMode ?: ResponseMode.query) -// Url(redirectUriReq).let { -// assertContains(iterable = it.parameters.names(), element = "request") -// assertContains(iterable = it.parameters.names(), element = "redirect_uri") -// assertEquals(expected = ciTestProvider.baseUrl + "/direct_post", actual = it.parameters["redirect_uri"]) -// } -// -// // Issuer client redirects the request to redirectUri -// println("// -------- WALLET ----------") -// // wallet creates id token -// val idToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDprZXk6ejJkbXpEODFjZ1B4OFZraTdKYnV1TW1GWXJXUGdZb3l0eWtVWjNleXFodDFqOUtib2o3ZzlQZlhKeGJiczRLWWVneXI3RUxuRlZucERNemJKSkRETlpqYXZYNmp2dERtQUxNYlhBR1c2N3BkVGdGZWEyRnJHR1NGczhFanhpOTZvRkxHSGNMNFA2YmpMRFBCSkV2UlJIU3JHNExzUG5lNTJmY3p0Mk1XakhMTEpCdmhBQyN6MmRtekQ4MWNnUHg4VmtpN0pidXVNbUZZcldQZ1lveXR5a1VaM2V5cWh0MWo5S2JvajdnOVBmWEp4YmJzNEtZZWd5cjdFTG5GVm5wRE16YkpKREROWmphdlg2anZ0RG1BTE1iWEFHVzY3cGRUZ0ZlYTJGckdHU0ZzOEVqeGk5Nm9GTEdIY0w0UDZiakxEUEJKRXZSUkhTckc0THNQbmU1MmZjenQyTVdqSExMSkJ2aEFDIn0.eyJub25jZSI6ImE4YWE1NDYwLTRmN2UtNDRmNy05ZGE3LWU1NmQ0YjIxMWE1MSIsInN1YiI6ImRpZDprZXk6ejJkbXpEODFjZ1B4OFZraTdKYnV1TW1GWXJXUGdZb3l0eWtVWjNleXFodDFqOUtib2o3ZzlQZlhKeGJiczRLWWVneXI3RUxuRlZucERNemJKSkRETlpqYXZYNmp2dERtQUxNYlhBR1c2N3BkVGdGZWEyRnJHR1NGczhFanhpOTZvRkxHSGNMNFA2YmpMRFBCSkV2UlJIU3JHNExzUG5lNTJmY3p0Mk1XakhMTEpCdmhBQyIsImlzcyI6ImRpZDprZXk6ejJkbXpEODFjZ1B4OFZraTdKYnV1TW1GWXJXUGdZb3l0eWtVWjNleXFodDFqOUtib2o3ZzlQZlhKeGJiczRLWWVneXI3RUxuRlZucERNemJKSkRETlpqYXZYNmp2dERtQUxNYlhBR1c2N3BkVGdGZWEyRnJHR1NGczhFanhpOTZvRkxHSGNMNFA2YmpMRFBCSkV2UlJIU3JHNExzUG5lNTJmY3p0Mk1XakhMTEpCdmhBQyIsImF1ZCI6Imh0dHBzOi8vMDFiYi01LTIwMy0xNzQtNjcubmdyb2stZnJlZS5hcHAiLCJpYXQiOjE3MjExNDQ3MzYsImV4cCI6MTcyMTE0NTAzNn0.VPWyLkMQAlcc40WCNSRH-Vxaj4LHi-wf2P9kcEKDvcdyVec2xJIwkg0JF4INMbLCkF0Y89lT0oswALd345wdUg" -// // wallet calls POST /direct_post (e.g. redirect_uri of Issuer Auth Req) providing the id_token -// -// println("// -------- CREDENTIAL ISSUER ----------") -// // Create validateIdTokenResponse() -// val idTokenPayload = validateAuthorizationRequestToken(idToken) -// -// // Issuer Client validates states and nonces based on idTokenPayload -// -// // Issuer client generates authorization code -// val authorizationCode = "secured_code" -// val authCodeResponse: AuthorizationCodeResponse = AuthorizationCodeResponse.success(authorizationCode, mapOf("state" to listOf(authReqWallet.state!!))) -// -// val redirectUri = authCodeResponse.toRedirectUri(authReq.redirectUri ?: TODO(), authReq.responseMode ?: ResponseMode.query) -// Url(redirectUri).let { -// assertContains(iterable = it.parameters.names(), element = ResponseType.Code.name.lowercase()) -// assertEquals( -// expected = authCodeResponse.code, -// actual = it.parameters[ResponseType.Code.name.lowercase()] -// ) -// } -// -// // Issuer client redirects the request to redirectUri -// -// println("// -------- WALLET ----------") -// println("// token req") -// val tokenReq = -// TokenRequest( -// GrantType.authorization_code, -// testCIClientConfig.clientID, -// code = authCodeResponse.code!! -// ) -// println("tokenReq: $tokenReq") -// -// -// println("// -------- CREDENTIAL ISSUER ----------") -// // Validate token request against authorization code -// validateTokenRequestRaw(tokenReq.toHttpParameters(), authorizationCode) -// -// // Generate Access Token -// val expirationTime = (Clock.System.now().epochSeconds + 864000L) // ten days in milliseconds -// val accessToken = signToken( -// privateKey = ciTestProvider.CI_TOKEN_KEY, -// payload = buildJsonObject { -// put(JWTClaims.Payload.audience, ciTestProvider.baseUrl) -// put(JWTClaims.Payload.subject, authReq.clientId) -// put(JWTClaims.Payload.issuer, ciTestProvider.baseUrl) -// put(JWTClaims.Payload.expirationTime, expirationTime) -// put(JWTClaims.Payload.notBeforeTime, Clock.System.now().epochSeconds) -// } -// ) -// // Issuer client creates cPoPnonce -// val cPoPNonce = "secured_cPoPnonce" -// val tokenResponse: TokenResponse = TokenResponse.success(accessToken, "bearer", cNonce = cPoPNonce, expiresIn = expirationTime) -// -// // Issuer client sends successful response with tokenResponse -// -// -// // -// println("// -------- WALLET ----------") -// assertTrue(actual = tokenResponse.isSuccess) -// assertNotNull(actual = tokenResponse.accessToken) -// assertNotNull(actual = tokenResponse.cNonce) -// -// println("// receive credential") -// val nonce = tokenResponse.cNonce!! -// val holderDid = TEST_WALLET_DID_WEB1 -//// val holderKey = runBlocking { JWKKey.importJWK(TEST_WALLET_KEY1) }.getOrThrow() -// val holderKey = JWKKey.importJWK(TEST_WALLET_KEY1).getOrThrow() -//// val holderKeyId = runBlocking { holderKey.getKeyId() } -// val holderKeyId = holderKey.getKeyId() -// val proofKeyId = "$holderDid#$holderKeyId" -// val proofOfPossession = -// ProofOfPossession.JWTProofBuilder(ciTestProvider.baseUrl, null, nonce, proofKeyId).build(holderKey) -// -// val credReq = CredentialRequest.forOfferedCredential(offeredCredential, proofOfPossession) -// println("credReq: $credReq") -// -// -// println("// -------- CREDENTIAL ISSUER ----------") -// // Issuer Client extracts Access Token from header -// verifyToken(tokenResponse.accessToken.toString(), ciTestProvider.CI_TOKEN_KEY) -// -// val parsedHolderKeyId = credReq.proof?.jwt?.let { JwtUtils.parseJWTHeader(it) }?.get("kid")?.jsonPrimitive?.content -// assertNotNull(actual = parsedHolderKeyId) -// assertTrue(actual = parsedHolderKeyId.startsWith("did:")) -// val parsedHolderDid = parsedHolderKeyId.substringBefore("#") -//// val resolvedKeyForHolderDid = runBlocking { DidService.resolveToKey(parsedHolderDid) }.getOrThrow() -// val resolvedKeyForHolderDid = DidService.resolveToKey(parsedHolderDid).getOrThrow() -// -// val validPoP = credReq.proof?.validateJwtProof(resolvedKeyForHolderDid, ciTestProvider.baseUrl,null, nonce, parsedHolderKeyId) -// assertTrue(actual = validPoP!!) -// val generatedCredential = ciTestProvider.generateCredential(credReq).credential -// assertNotNull(generatedCredential) -// val credentialResponse: CredentialResponse = CredentialResponse.success(credReq.format, generatedCredential) -// -// println("// -------- WALLET ----------") -// assertTrue(actual = credentialResponse.isSuccess) -// assertFalse(actual = credentialResponse.isDeferred) -// assertEquals(expected = CredentialFormat.jwt_vc_json, actual = credentialResponse.format!!) -// assertTrue(actual = credentialResponse.credential!!.instanceOf(JsonPrimitive::class)) -// -// println("// parse and verify credential") -// val credential = credentialResponse.credential!!.jsonPrimitive.content -// println(">>> Issued credential: $credential") -// verifyIssuerAndSubjectId( -// SDJwt.parse(credential).fullPayload["vc"]?.jsonObject!!, -// ciTestProvider.CI_ISSUER_DID, credentialWallet.TEST_DID -// ) -// assertTrue(actual = JwtSignaturePolicy().verify(credential, null, mapOf()).isSuccess) -// } -// -// @Test -// fun testCredentialIssuanceIsolatedFunctionsAuthCodeFlowWithVpTokenAuth() = runTest { -// // TODO: consider re-implementing CITestProvider, making use of new lib functions -// // is it ok to generate the credential offer using the ciTestProvider (OpenIDCredentialIssuer class) ? -// -// println("// -------- CREDENTIAL ISSUER ----------") -// // init credential offer for full authorization code flow -// // Issuer Client stores the authentication method in session, ID_TOKEN in this case (PRE_AUTHORIZED, PWD, ID_TOKEN, VP_TOKEN, NONE) -// -// val credOffer = CredentialOffer.Builder(ciTestProvider.baseUrl) -// .addOfferedCredential("VerifiableId") -// .addAuthorizationCodeGrant("test-state-vptoken-auth") -// .build() -// val issueReqUrl = OpenID4VCI.getCredentialOfferRequestUrl(credOffer) -// ciTestProvider.deferIssuance = false -// -// // Show credential offer request as QR code -// println(issueReqUrl) -// -// -// println("// -------- WALLET ----------") -//// val parsedCredOffer = runBlocking { OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) } -// val parsedCredOffer = OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) -// assertEquals(expected = credOffer.toJSONString(), actual = parsedCredOffer.toJSONString()) -// -//// val providerMetadata = runBlocking { OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) } -// val providerMetadata = OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) -// assertEquals(expected = parsedCredOffer.credentialIssuer, actual = providerMetadata.credentialIssuer) -// -// println("// resolve offered credentials") -// val offeredCredentials = OpenID4VCI.resolveOfferedCredentials(parsedCredOffer, providerMetadata) -// println("offeredCredentials: $offeredCredentials") -// assertEquals(expected = 1, actual = offeredCredentials.size) -// assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) -// assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().types?.last()) -// val offeredCredential = offeredCredentials.first() -// println("offeredCredentials[0]: $offeredCredential") -// -// println("// go through authorization code flow to receive offered credential") -// println("// auth request (short-cut, without pushed authorization request)") -// val authReqWalletState = "secured_state_wallet" -// val authReqWallet = AuthorizationRequest( -// setOf(ResponseType.Code), testCIClientConfig.clientID, -// redirectUri = credentialWallet.config.redirectUri, -// issuerState = parsedCredOffer.grants[GrantType.authorization_code.value]!!.issuerState, -// state = authReqWalletState, -// clientMetadata = OpenIDClientMetadata(customParameters=mapOf("authorization_endpoint" to "wallet-api.com/callback".toJsonElement())) -// ) -// println("authReq: $authReqWallet") -// -// // Wallet client calls /authorize endpoint -// -// println("// -------- CREDENTIAL ISSUER ----------") -// val authReq = validateAuthorizationRequestQueryString(authReqWallet.toHttpQueryString()) -// -// // Issuer client retrieves issuance session based on issuer state and stores the credential request, including authReqWallet state -// // Issuer client checks the authentication method of the session and find out that authentication method is VP_TOKEN -// // Issuer client generates presentation definition -// val requestedCredentialId = "OpenBadgeCredential" -// val vpProfile = OpenId4VPProfile.EBSIV3 -// val requestCredentialsArr = buildJsonArray { add(requestedCredentialId) } -// val requestedTypes = requestCredentialsArr.map { -// when (it) { -// is JsonPrimitive -> it.contentOrNull -// is JsonObject -> it["credential"]?.jsonPrimitive?.contentOrNull -// else -> throw IllegalArgumentException("Invalid JSON type for requested credential: $it") -// } ?: throw IllegalArgumentException("Invalid VC type for requested credential: $it") -// } -// -// val presentationDefinition = PresentationDefinition.primitiveGenerationFromVcTypes(requestedTypes, vpProfile) -// -// // Issuer client creates state and nonce for the vp token authorization request -// val authReqIssuerState = "secured_state_issuer" -// val authReqIssuerNonce = "secured_nonce_issuer" -// -// val authReqIssuer = generateAuthorizationRequest(authReq, ciTestProvider.baseUrl, ciTestProvider.CI_TOKEN_KEY, ResponseType.VpToken, authReqIssuerState, authReqIssuerNonce, true, presentationDefinition) -// -// // Redirect uri is located in the client_metadata.authorization_endpoint or "openid://" -// val redirectUriReq = authReqIssuer.toRedirectUri(authReq.clientMetadata?.customParameters?.get("authorization_endpoint")?.jsonPrimitive?.content ?: "openid://", authReq.responseMode ?: ResponseMode.query) -// Url(redirectUriReq).let { -// assertContains(iterable = it.parameters.names(), element = "request") -// assertContains(iterable = it.parameters.names(), element = "redirect_uri") -// assertContains(iterable = it.parameters.names(), element = "presentation_definition") -// assertEquals(expected = ciTestProvider.baseUrl + "/direct_post", actual = it.parameters["redirect_uri"]) -// } -// -// // Issuer client redirects the request to redirectUri -// println("// -------- WALLET ----------") -// // wallet creates vp token -// val vpToken = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa3A3QVZ3dld4bnNORHVTU2JmMTlzZ0t6cngyMjNXWTk1QXFaeUFHaWZGVnlWI3o2TWtwN0FWd3ZXeG5zTkR1U1NiZjE5c2dLenJ4MjIzV1k5NUFxWnlBR2lmRlZ5ViJ9.eyJzdWIiOiJkaWQ6a2V5Ono2TWtwN0FWd3ZXeG5zTkR1U1NiZjE5c2dLenJ4MjIzV1k5NUFxWnlBR2lmRlZ5ViIsIm5iZiI6MTcyMDc2NDAxOSwiaWF0IjoxNzIwNzY0MDc5LCJqdGkiOiJ1cm46dXVpZDpiNzE2YThlOC0xNzVlLTRhMTYtODZlMC0xYzU2Zjc4NTFhZDEiLCJpc3MiOiJkaWQ6a2V5Ono2TWtwN0FWd3ZXeG5zTkR1U1NiZjE5c2dLenJ4MjIzV1k5NUFxWnlBR2lmRlZ5ViIsIm5vbmNlIjoiNDY0YTAwMTUtNzQ1OS00Y2Y4LWJmNjgtNDg0ODQyYTE5Y2FmIiwiYXVkIjoiZGlkOmtleTp6Nk1rcDdBVnd2V3huc05EdVNTYmYxOXNnS3pyeDIyM1dZOTVBcVp5QUdpZkZWeVYiLCJ2cCI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iXSwiaWQiOiJ1cm46dXVpZDpiNzE2YThlOC0xNzVlLTRhMTYtODZlMC0xYzU2Zjc4NTFhZDEiLCJob2xkZXIiOiJkaWQ6a2V5Ono2TWtwN0FWd3ZXeG5zTkR1U1NiZjE5c2dLenJ4MjIzV1k5NUFxWnlBR2lmRlZ5ViIsInZlcmlmaWFibGVDcmVkZW50aWFsIjpbImV5SmhiR2NpT2lKRlpFUlRRU0lzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW1ScFpEcHJaWGs2ZWpaTmEzQTNRVlozZGxkNGJuTk9SSFZUVTJKbU1UbHpaMHQ2Y25neU1qTlhXVGsxUVhGYWVVRkhhV1pHVm5sV0luMC5leUpwYzNNaU9pSmthV1E2YTJWNU9ubzJUV3R3TjBGV2QzWlhlRzV6VGtSMVUxTmlaakU1YzJkTGVuSjRNakl6VjFrNU5VRnhXbmxCUjJsbVJsWjVWaUlzSW5OMVlpSTZJbVJwWkRwclpYazZlalpOYTJwdE1tZGhSM052WkVkamFHWkhOR3M0VURaTGQwTklXbk5XUlZCYWFHODFWblZGWWxrNU5IRnBRa0k1SWl3aWRtTWlPbnNpUUdOdmJuUmxlSFFpT2xzaWFIUjBjSE02THk5M2QzY3Vkek11YjNKbkx6SXdNVGd2WTNKbFpHVnVkR2xoYkhNdmRqRWlMQ0pvZEhSd2N6b3ZMM0IxY213dWFXMXpaMnh2WW1Gc0xtOXlaeTl6Y0dWakwyOWlMM1l6Y0RBdlkyOXVkR1Y0ZEM1cWMyOXVJbDBzSW1sa0lqb2lkWEp1T25WMWFXUTZNVEl6SWl3aWRIbHdaU0k2V3lKV1pYSnBabWxoWW14bFEzSmxaR1Z1ZEdsaGJDSXNJazl3Wlc1Q1lXUm5aVU55WldSbGJuUnBZV3dpWFN3aWJtRnRaU0k2SWtwR1JpQjRJSFpqTFdWa2RTQlFiSFZuUm1WemRDQXpJRWx1ZEdWeWIzQmxjbUZpYVd4cGRIa2lMQ0pwYzNOMVpYSWlPbnNpZEhsd1pTSTZXeUpRY205bWFXeGxJbDBzSW1sa0lqb2laR2xrT21WNFlXMXdiR1U2TVRJeklpd2libUZ0WlNJNklrcHZZbk1nWm05eUlIUm9aU0JHZFhSMWNtVWdLRXBHUmlraUxDSjFjbXdpT2lKb2RIUndjem92TDNkM2R5NXFabVl1YjNKbkx5SXNJbWx0WVdkbElqcDdJbWxrSWpvaWFIUjBjSE02THk5M00yTXRZMk5uTG1kcGRHaDFZaTVwYnk5Mll5MWxaQzl3YkhWblptVnpkQzB4TFRJd01qSXZhVzFoWjJWekwwcEdSbDlNYjJkdlRHOWphM1Z3TG5CdVp5SXNJblI1Y0dVaU9pSkpiV0ZuWlNKOWZTd2lhWE56ZFdGdVkyVkVZWFJsSWpvaU1qQXlNeTB3TnkweU1GUXdOem93TlRvME5Gb2lMQ0psZUhCcGNtRjBhVzl1UkdGMFpTSTZJakl3TXpNdE1EY3RNakJVTURjNk1EVTZORFJhSWl3aVkzSmxaR1Z1ZEdsaGJGTjFZbXBsWTNRaU9uc2lhV1FpT2lKa2FXUTZaWGhoYlhCc1pUb3hNak1pTENKMGVYQmxJanBiSWtGamFHbGxkbVZ0Wlc1MFUzVmlhbVZqZENKZExDSmhZMmhwWlhabGJXVnVkQ0k2ZXlKcFpDSTZJblZ5YmpwMWRXbGtPbUZqTWpVMFltUTFMVGhtWVdRdE5HSmlNUzA1WkRJNUxXVm1aRGt6T0RVek5qa3lOaUlzSW5SNWNHVWlPbHNpUVdOb2FXVjJaVzFsYm5RaVhTd2libUZ0WlNJNklrcEdSaUI0SUhaakxXVmtkU0JRYkhWblJtVnpkQ0F6SUVsdWRHVnliM0JsY21GaWFXeHBkSGtpTENKa1pYTmpjbWx3ZEdsdmJpSTZJbFJvYVhNZ2QyRnNiR1YwSUhOMWNIQnZjblJ6SUhSb1pTQjFjMlVnYjJZZ1Z6TkRJRlpsY21sbWFXRmliR1VnUTNKbFpHVnVkR2xoYkhNZ1lXNWtJR2hoY3lCa1pXMXZibk4wY21GMFpXUWdhVzUwWlhKdmNHVnlZV0pwYkdsMGVTQmtkWEpwYm1jZ2RHaGxJSEJ5WlhObGJuUmhkR2x2YmlCeVpYRjFaWE4wSUhkdmNtdG1iRzkzSUdSMWNtbHVaeUJLUmtZZ2VDQldReTFGUkZVZ1VHeDFaMFpsYzNRZ015NGlMQ0pqY21sMFpYSnBZU0k2ZXlKMGVYQmxJam9pUTNKcGRHVnlhV0VpTENKdVlYSnlZWFJwZG1VaU9pSlhZV3hzWlhRZ2MyOXNkWFJwYjI1eklIQnliM1pwWkdWeWN5QmxZWEp1WldRZ2RHaHBjeUJpWVdSblpTQmllU0JrWlcxdmJuTjBjbUYwYVc1bklHbHVkR1Z5YjNCbGNtRmlhV3hwZEhrZ1pIVnlhVzVuSUhSb1pTQndjbVZ6Wlc1MFlYUnBiMjRnY21WeGRXVnpkQ0IzYjNKclpteHZkeTRnVkdocGN5QnBibU5zZFdSbGN5QnpkV05qWlhOelpuVnNiSGtnY21WalpXbDJhVzVuSUdFZ2NISmxjMlZ1ZEdGMGFXOXVJSEpsY1hWbGMzUXNJR0ZzYkc5M2FXNW5JSFJvWlNCb2IyeGtaWElnZEc4Z2MyVnNaV04wSUdGMElHeGxZWE4wSUhSM2J5QjBlWEJsY3lCdlppQjJaWEpwWm1saFlteGxJR055WldSbGJuUnBZV3h6SUhSdklHTnlaV0YwWlNCaElIWmxjbWxtYVdGaWJHVWdjSEpsYzJWdWRHRjBhVzl1TENCeVpYUjFjbTVwYm1jZ2RHaGxJSEJ5WlhObGJuUmhkR2x2YmlCMGJ5QjBhR1VnY21WeGRXVnpkRzl5TENCaGJtUWdjR0Z6YzJsdVp5QjJaWEpwWm1sallYUnBiMjRnYjJZZ2RHaGxJSEJ5WlhObGJuUmhkR2x2YmlCaGJtUWdkR2hsSUdsdVkyeDFaR1ZrSUdOeVpXUmxiblJwWVd4ekxpSjlMQ0pwYldGblpTSTZleUpwWkNJNkltaDBkSEJ6T2k4dmR6TmpMV05qWnk1bmFYUm9kV0l1YVc4dmRtTXRaV1F2Y0d4MVoyWmxjM1F0TXkweU1ESXpMMmx0WVdkbGN5OUtSa1l0VmtNdFJVUlZMVkJNVlVkR1JWTlVNeTFpWVdSblpTMXBiV0ZuWlM1d2JtY2lMQ0owZVhCbElqb2lTVzFoWjJVaWZYMTlMQ0pqY21Wa1pXNTBhV0ZzVTJOb1pXMWhJanA3SW1sa0lqb2lhSFIwY0hNNkx5OXdkWEpzTG1sdGMyZHNiMkpoYkM1dmNtY3ZjM0JsWXk5dllpOTJNM0F3TDNOamFHVnRZUzlxYzI5dUwyOWlYM1l6Y0RCZllXTm9hV1YyWlcxbGJuUmpjbVZrWlc1MGFXRnNYM05qYUdWdFlTNXFjMjl1SWl3aWRIbHdaU0k2SWtaMWJHeEtjMjl1VTJOb1pXMWhWbUZzYVdSaGRHOXlNakF5TVNKOWZTd2lhblJwSWpvaWRYSnVPblYxYVdRNk1USXpJaXdpWlhod0lqb3lNREExTkRVMU9UUTBMQ0pwWVhRaU9qRTJPRGs0TXpZM05EUXNJbTVpWmlJNk1UWTRPVGd6TmpjME5IMC5PRHZUQXVMN2JrME1pX3hNLVFualg4azByZ3VUeWtiYzJ6bFdFMVU2SGlmVXFjWTdFVU5GcUdUZWFUWHRESkxrODBuZWN6YkNNTGh1YlZseEFkdl9DdyJdfX0.zTXluOVIP0sQzc5GzNvtVvWRiaC-x9qMZg0d-EvCuRIg7QSgY0hmrfVlAzh2IDEvaXZ1ahM3hSVDx_YI74ToAw" -// // wallet calls POST /direct_post (e.g. redirect_uri of Issuer Auth Req) providing the id_token -// -// println("// -------- CREDENTIAL ISSUER ----------") -// val vpTokenPayload = validateAuthorizationRequestToken(vpToken) -// -// // Issuer Client validates states and nonces based on vpTokenPayload -// -// // Issuer client generates authorization code -// val authorizationCode = "secured_code" -// val authCodeResponse: AuthorizationCodeResponse = AuthorizationCodeResponse.success(authorizationCode, mapOf("state" to listOf(authReqWallet.state!!))) -// -// val redirectUri = authCodeResponse.toRedirectUri(authReq.redirectUri ?: TODO(), authReq.responseMode ?: ResponseMode.query) -// Url(redirectUri).let { -// assertContains(iterable = it.parameters.names(), element = ResponseType.Code.name.lowercase()) -// assertEquals( -// expected = authCodeResponse.code, -// actual = it.parameters[ResponseType.Code.name.lowercase()] -// ) -// } -// -// // Issuer client redirects the request to redirectUri -// -// println("// -------- WALLET ----------") -// println("// token req") -// val tokenReq = -// TokenRequest( -// GrantType.authorization_code, -// testCIClientConfig.clientID, -// code = authCodeResponse.code!! -// ) -// println("tokenReq: $tokenReq") -// -// -// println("// -------- CREDENTIAL ISSUER ----------") -// // Validate token request against authorization code -// validateTokenRequestRaw(tokenReq.toHttpParameters(), authorizationCode) -// -// // Generate Access Token -// val expirationTime = (Clock.System.now().epochSeconds + 864000L) // ten days in milliseconds -// val accessToken = signToken( -// privateKey = ciTestProvider.CI_TOKEN_KEY, -// payload = buildJsonObject { -// put(JWTClaims.Payload.audience, ciTestProvider.baseUrl) -// put(JWTClaims.Payload.subject, authReq.clientId) -// put(JWTClaims.Payload.issuer, ciTestProvider.baseUrl) -// put(JWTClaims.Payload.expirationTime, expirationTime) -// put(JWTClaims.Payload.notBeforeTime, Clock.System.now().epochSeconds) -// } -// ) -// // Issuer client creates cPoPnonce -// val cPoPNonce = "secured_cPoPnonce" -// val tokenResponse: TokenResponse = TokenResponse.success(accessToken, "bearer", cNonce = cPoPNonce, expiresIn = expirationTime) -// -// // Issuer client sends successful response with tokenResponse -// -// -// // -// println("// -------- WALLET ----------") -// assertTrue(actual = tokenResponse.isSuccess) -// assertNotNull(actual = tokenResponse.accessToken) -// assertNotNull(actual = tokenResponse.cNonce) -// -// println("// receive credential") -// val nonce = tokenResponse.cNonce!! -// val holderDid = TEST_WALLET_DID_WEB1 -//// val holderKey = runBlocking { JWKKey.importJWK(TEST_WALLET_KEY1) }.getOrThrow() -// val holderKey = JWKKey.importJWK(TEST_WALLET_KEY1).getOrThrow() -//// val holderKeyId = runBlocking { holderKey.getKeyId() } -// val holderKeyId = holderKey.getKeyId() -// val proofKeyId = "$holderDid#$holderKeyId" -// val proofOfPossession = -// ProofOfPossession.JWTProofBuilder(ciTestProvider.baseUrl, null, nonce, proofKeyId).build(holderKey) -// -// val credReq = CredentialRequest.forOfferedCredential(offeredCredential, proofOfPossession) -// println("credReq: $credReq") -// -// -// println("// -------- CREDENTIAL ISSUER ----------") -// // Issuer Client extracts Access Token from header -// verifyToken(tokenResponse.accessToken.toString(), ciTestProvider.CI_TOKEN_KEY) -// -// val parsedHolderKeyId = credReq.proof?.jwt?.let { JwtUtils.parseJWTHeader(it) }?.get("kid")?.jsonPrimitive?.content -// assertNotNull(actual = parsedHolderKeyId) -// assertTrue(actual = parsedHolderKeyId.startsWith("did:")) -// val parsedHolderDid = parsedHolderKeyId.substringBefore("#") -//// val resolvedKeyForHolderDid = runBlocking { DidService.resolveToKey(parsedHolderDid) }.getOrThrow() -// val resolvedKeyForHolderDid = DidService.resolveToKey(parsedHolderDid).getOrThrow() -// -// val validPoP = credReq.proof?.validateJwtProof(resolvedKeyForHolderDid, ciTestProvider.baseUrl,null, nonce, parsedHolderKeyId) -// assertTrue(actual = validPoP!!) -// val generatedCredential = ciTestProvider.generateCredential(credReq).credential -// assertNotNull(generatedCredential) -// val credentialResponse: CredentialResponse = CredentialResponse.success(credReq.format, generatedCredential) -// -// println("// -------- WALLET ----------") -// assertTrue(actual = credentialResponse.isSuccess) -// assertFalse(actual = credentialResponse.isDeferred) -// assertEquals(expected = CredentialFormat.jwt_vc_json, actual = credentialResponse.format!!) -// assertTrue(actual = credentialResponse.credential!!.instanceOf(JsonPrimitive::class)) -// -// println("// parse and verify credential") -// val credential = credentialResponse.credential!!.jsonPrimitive.content -// println(">>> Issued credential: $credential") -// verifyIssuerAndSubjectId( -// SDJwt.parse(credential).fullPayload["vc"]?.jsonObject!!, -// ciTestProvider.CI_ISSUER_DID, credentialWallet.TEST_DID -// ) -// assertTrue(actual = JwtSignaturePolicy().verify(credential, null, mapOf()).isSuccess) -// } - - @Test - fun testCredentialOfferFullAuth() = runTest { - println("// -------- CREDENTIAL ISSUER ----------") - println("// as CI provider, initialize credential offer for user") - val issuanceSession = ciTestProvider.initializeCredentialOffer( - CredentialOffer.Builder(ciTestProvider.baseUrl).addOfferedCredential("VerifiableId"), - 5.minutes, allowPreAuthorized = false - ) - println("issuanceSession: $issuanceSession") - assertNotNull(actual = issuanceSession.credentialOffer) - val offerRequest = CredentialOfferRequest(issuanceSession.credentialOffer!!) - val offerUri = ciTestProvider.getCredentialOfferRequestUrl(offerRequest) - println(">>> Offer URI: $offerUri") - - println("// -------- WALLET ----------") - println("// as WALLET: receive credential offer, either being called via deeplink or by scanning QR code") - println("// parse credential URI") - val parsedOfferReq = CredentialOfferRequest.fromHttpParameters(Url(offerUri).parameters.toMap()) - println("parsedOfferReq: $parsedOfferReq") - - assertNotNull(actual = parsedOfferReq.credentialOffer) - assertNotNull(actual = parsedOfferReq.credentialOffer!!.credentialIssuer) - assertEquals( - expected = setOf(GrantType.authorization_code.value), - actual = parsedOfferReq.credentialOffer!!.grants.keys - ) - - println("// get issuer metadata") - val providerMetadataUri = - credentialWallet.getCIProviderMetadataUrl(parsedOfferReq.credentialOffer!!.credentialIssuer) - val providerMetadata = ktorClient.get(providerMetadataUri).call.body() - println("providerMetadata: $providerMetadata") - - assertNotNull(actual = providerMetadata.credentialConfigurationsSupported) - - println("// resolve offered credentials") - val offeredCredentials = OpenID4VCI.resolveOfferedCredentials(parsedOfferReq.credentialOffer!!, providerMetadata) - println("offeredCredentials: $offeredCredentials") - - assertEquals(expected = 1, actual = offeredCredentials.size) - assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) - assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().credentialDefinition?.type?.last()) - val offeredCredential = offeredCredentials.first() - println("offeredCredentials[0]: $offeredCredential") - - println("// go through full authorization code flow to receive offered credential") - println("// auth request (short-cut, without pushed authorization request)") - val authReq = AuthorizationRequest( - setOf(ResponseType.Code), testCIClientConfig.clientID, - redirectUri = credentialWallet.config.redirectUri, - issuerState = parsedOfferReq.credentialOffer!!.grants[GrantType.authorization_code.value]!!.issuerState - ) - println("authReq: $authReq") - - val authResp = ktorClient.get(providerMetadata.authorizationEndpoint!!) { - url { - parameters.appendAll(parametersOf(authReq.toHttpParameters())) - } - } - println("authResp: $authResp") - - assertEquals(expected = HttpStatusCode.Found, actual = authResp.status) - val location = Url(authResp.headers[HttpHeaders.Location]!!) - assertContains(iterable = location.parameters.names(), element = ResponseType.Code.name.lowercase()) - - println("// token req") - val tokenReq = - TokenRequest( - GrantType.authorization_code, - testCIClientConfig.clientID, - code = location.parameters[ResponseType.Code.name]!! - ) - println("tokenReq: $tokenReq") - - val tokenResp = ktorClient.submitForm( - providerMetadata.tokenEndpoint!!, - formParameters = parametersOf(tokenReq.toHttpParameters()) - ).body().let { TokenResponse.fromJSON(it) } - println("tokenResp: $tokenResp") - - assertTrue(actual = tokenResp.isSuccess) - assertNotNull(actual = tokenResp.accessToken) - assertNotNull(actual = tokenResp.cNonce) - - println("// receive credential") - ciTestProvider.deferIssuance = false - val nonce = tokenResp.cNonce!! - - val credReq = CredentialRequest.forOfferedCredential( - offeredCredential, - credentialWallet.generateDidProof(credentialWallet.TEST_DID, ciTestProvider.baseUrl, nonce) - ) - println("credReq: $credReq") - - val credentialResp = ktorClient.post(providerMetadata.credentialEndpoint!!) { - contentType(ContentType.Application.Json) - bearerAuth(tokenResp.accessToken!!) - setBody(credReq.toJSON()) - }.body().let { CredentialResponse.fromJSON(it) } - println("credentialResp: $credentialResp") - - assertTrue(actual = credentialResp.isSuccess) - assertFalse(actual = credentialResp.isDeferred) - assertEquals(expected = CredentialFormat.jwt_vc_json, actual = credentialResp.format!!) - assertTrue(actual = credentialResp.credential!!.instanceOf(JsonPrimitive::class)) - - println("// parse and verify credential") - val credential = credentialResp.credential!!.jsonPrimitive.content - println(">>> Issued credential: $credential") - verifyIssuerAndSubjectId( - SDJwt.parse(credential).fullPayload["vc"]?.jsonObject!!, - ciTestProvider.CI_ISSUER_DID, credentialWallet.TEST_DID - ) - assertTrue(JwtSignaturePolicy().verify(credential, null, mapOf()).isSuccess) - } - - @Test - fun testPreAuthCodeFlow() = runTest { - println("// -------- CREDENTIAL ISSUER ----------") - println("// as CI provider, initialize credential offer for user, this time providing full offered credential object, and allowing pre-authorized code flow with user pin") - val issuanceSession = ciTestProvider.initializeCredentialOffer( - CredentialOffer.Builder(ciTestProvider.baseUrl) - .addOfferedCredential(ciTestProvider.metadata.credentialConfigurationsSupported!!.keys.first()), - 5.minutes, allowPreAuthorized = true, txCode = TxCode(TxInputMode.numeric), txCodeValue = "1234" - ) - println("issuanceSession: $issuanceSession") - - assertNotNull(actual = issuanceSession.credentialOffer) - assertEquals( - expected = ciTestProvider.metadata.credentialConfigurationsSupported!!.keys.first(), - actual = issuanceSession.credentialOffer!!.credentialConfigurationIds.first() - ) - - val offerRequest = CredentialOfferRequest(issuanceSession.credentialOffer!!) - println("offerRequest: $offerRequest") - - println("// create credential offer request url (this time cross-device)") - val offerUri = ciTestProvider.getCredentialOfferRequestUrl(offerRequest) - println("Offer URI: $offerUri") - - println("// -------- WALLET ----------") - println("// as WALLET: receive credential offer, either being called via deeplink or by scanning QR code") - println("// parse credential URI") - val parsedOfferReq = CredentialOfferRequest.fromHttpParameters(Url(offerUri).parameters.toMap()) - println("parsedOfferReq: $parsedOfferReq") - - assertNotNull(actual = parsedOfferReq.credentialOffer) - assertNotNull(actual = parsedOfferReq.credentialOffer!!.credentialIssuer) - assertContains( - iterable = parsedOfferReq.credentialOffer!!.grants.keys, - element = GrantType.pre_authorized_code.value - ) - assertNotNull(actual = parsedOfferReq.credentialOffer!!.grants[GrantType.pre_authorized_code.value]?.preAuthorizedCode) - assertNotNull(actual = parsedOfferReq.credentialOffer!!.grants[GrantType.pre_authorized_code.value]?.txCode) - - println("// get issuer metadata") - val providerMetadataUri = - credentialWallet.getCIProviderMetadataUrl(parsedOfferReq.credentialOffer!!.credentialIssuer) - val providerMetadata = ktorClient.get(providerMetadataUri).call.body() - println("providerMetadata: $providerMetadata") - - assertNotNull(actual = providerMetadata.credentialConfigurationsSupported) - - println("// resolve offered credentials") - val offeredCredentials = OpenID4VCI.resolveOfferedCredentials(parsedOfferReq.credentialOffer!!, providerMetadata) - println("offeredCredentials: $offeredCredentials") - assertEquals(expected = 1, actual = offeredCredentials.size) - assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) - assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().credentialDefinition?.type?.last()) - val offeredCredential = offeredCredentials.first() - println("offeredCredentials[0]: $offeredCredential") - - println("// fetch access token using pre-authorized code (skipping authorization step)") - println("// try without user PIN, should be rejected!") - var tokenReq = TokenRequest( - grantType = GrantType.pre_authorized_code, - //clientId = testCIClientConfig.clientID, - redirectUri = credentialWallet.config.redirectUri, - preAuthorizedCode = parsedOfferReq.credentialOffer!!.grants[GrantType.pre_authorized_code.value]!!.preAuthorizedCode, - txCode = null - ) - println("tokenReq: $tokenReq") - - var tokenResp = ktorClient.submitForm( - providerMetadata.tokenEndpoint!!, formParameters = parametersOf(tokenReq.toHttpParameters()) - ).body().let { TokenResponse.fromJSON(it) } - println("tokenResp: $tokenResp") - - assertFalse(actual = tokenResp.isSuccess) - assertEquals(expected = TokenErrorCode.invalid_grant.name, actual = tokenResp.error) - - println("// try with user PIN, should work:") - tokenReq = TokenRequest( - grantType = GrantType.pre_authorized_code, - clientId = testCIClientConfig.clientID, - redirectUri = credentialWallet.config.redirectUri, - preAuthorizedCode = parsedOfferReq.credentialOffer!!.grants[GrantType.pre_authorized_code.value]!!.preAuthorizedCode, - txCode = issuanceSession.txCodeValue - ) - println("tokenReq: $tokenReq") - - tokenResp = ktorClient.submitForm( - providerMetadata.tokenEndpoint!!, formParameters = parametersOf(tokenReq.toHttpParameters()) - ).body().let { TokenResponse.fromJSON(it) } - println("tokenResp: $tokenResp") - - assertTrue(actual = tokenResp.isSuccess) - assertNotNull(actual = tokenResp.accessToken) - assertNotNull(actual = tokenResp.cNonce) - - println("// receive credential") - ciTestProvider.deferIssuance = false - val nonce = tokenResp.cNonce!! - - val credReq = CredentialRequest.forOfferedCredential( - offeredCredential, - credentialWallet.generateDidProof(credentialWallet.TEST_DID, ciTestProvider.baseUrl, nonce) - ) - println("credReq: $credReq") - - val credentialResp = ktorClient.post(providerMetadata.credentialEndpoint!!) { - contentType(ContentType.Application.Json) - bearerAuth(tokenResp.accessToken!!) - setBody(credReq.toJSON()) - }.body().let { CredentialResponse.fromJSON(it) } - println("credentialResp: $credentialResp") - - assertTrue(actual = credentialResp.isSuccess) - assertFalse(actual = credentialResp.isDeferred) - assertEquals(expected = CredentialFormat.jwt_vc_json, actual = credentialResp.format!!) - assertTrue(actual = credentialResp.credential!!.instanceOf(JsonPrimitive::class)) - - println("// parse and verify credential") - val credential = credentialResp.credential!!.jsonPrimitive.content - println(">>> Issued credential: $credential") - - verifyIssuerAndSubjectId( - SDJwt.parse(credential).fullPayload["vc"]?.jsonObject!!, - ciTestProvider.CI_ISSUER_DID, credentialWallet.TEST_DID - ) - assertTrue(actual = JwtSignaturePolicy().verify(credential, null, mapOf()).isSuccess) - } - - @Test - fun testFullAuthImplicitFlow() = runTest { - println("// 0. get issuer metadata") - val providerMetadata = - ktorClient.get(ciTestProvider.getCIProviderMetadataUrl()).call.body() - println("providerMetadata: $providerMetadata") - - println("// 1. send pushed authorization request with authorization details, containing info of credentials to be issued, receive session id") - val implicitAuthReq = AuthorizationRequest( - responseType = setOf(ResponseType.Token), - responseMode = ResponseMode.fragment, - clientId = testCIClientConfig.clientID, - redirectUri = credentialWallet.config.redirectUri, - authorizationDetails = listOf( - AuthorizationDetails( - type = OPENID_CREDENTIAL_AUTHORIZATION_TYPE, - format = CredentialFormat.jwt_vc_json, - credentialDefinition = CredentialDefinition(type = listOf("VerifiableCredential", "VerifiableId")) - ), AuthorizationDetails( - type = OPENID_CREDENTIAL_AUTHORIZATION_TYPE, - format = CredentialFormat.jwt_vc_json, - credentialDefinition = CredentialDefinition(type = listOf("VerifiableCredential", "VerifiableAttestation", "VerifiableDiploma")) - ) - ) - ) - println("implicitAuthReq: $implicitAuthReq") - - println("// 2. call authorize endpoint with request uri, receive HTTP redirect (302 Found) with Location header") - assertNotNull(actual = providerMetadata.authorizationEndpoint) - val authResp = ktorClient.get(providerMetadata.authorizationEndpoint!!) { - url { - parameters.appendAll(parametersOf(implicitAuthReq.toHttpParameters())) - } - } - println("authResp: $authResp") - - assertEquals(expected = HttpStatusCode.Found, actual = authResp.status) - assertContains(iterable = authResp.headers.names(), element = HttpHeaders.Location) - - val location = Url(authResp.headers[HttpHeaders.Location]!!) - println("location: $location") - assertTrue(actual = location.toString().startsWith(credentialWallet.config.redirectUri!!)) - assertFalse(actual = location.fragment.isEmpty()) - - val locationWithQueryParams = Url("http://blank?${location.fragment}") - val tokenResp = TokenResponse.fromHttpParameters(locationWithQueryParams.parameters.toMap()) - println("tokenResp: $tokenResp") - - assertTrue(actual = tokenResp.isSuccess) - assertNotNull(actual = tokenResp.accessToken) - assertNotNull(actual = tokenResp.cNonce) - - println("// 3a. Call credential endpoint with access token, to receive credential (synchronous issuance)") - assertNotNull(actual = providerMetadata.credentialEndpoint) - ciTestProvider.deferIssuance = false - - val credReq = CredentialRequest.forAuthorizationDetails( - implicitAuthReq.authorizationDetails!!.first(), - credentialWallet.generateDidProof(credentialWallet.TEST_DID, ciTestProvider.baseUrl, tokenResp.cNonce!!) - ) - println("credReq: $credReq") - - val credentialResp = ktorClient.post(providerMetadata.credentialEndpoint!!) { - contentType(ContentType.Application.Json) - bearerAuth(tokenResp.accessToken!!) - setBody(credReq.toJSON()) - }.body().let { CredentialResponse.fromJSON(it) } - println("credentialResp: $credentialResp") - - assertTrue(actual = credentialResp.isSuccess) - assertFalse(actual = credentialResp.isDeferred) - assertEquals(expected = CredentialFormat.jwt_vc_json, actual = credentialResp.format!!) - assertTrue(actual = credentialResp.credential!!.instanceOf(JsonPrimitive::class)) - - val credential = credentialResp.credential!!.jsonPrimitive.content - println(">>> Issued credential: $credential") - - verifyIssuerAndSubjectId( - SDJwt.parse(credential).fullPayload["vc"]?.jsonObject!!, - ciTestProvider.CI_ISSUER_DID, credentialWallet.TEST_DID - ) - assertTrue(actual = JwtSignaturePolicy().verify(credential, null, mapOf()).isSuccess) - } - val issuerPortalRequest = "openid-credential-offer://issuer.portal.walt.id/?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fw3c-ccg.github.io%2Fvc-ed%2Fplugfest-1-2022%2Fjff-vc-edu-plugfest-1-context.json%22%2C%22https%3A%2F%2Fw3id.org%2Fsecurity%2Fsuites%2Fed25519-2020%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22c7228046-1a8e-4e27-a7b1-cd6479e1455f%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJjNzIyODA0Ni0xYThlLTRlMjctYTdiMS1jZDY0NzllMTQ1NWYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.On2_7P4vr5caTHKbWv2i0a604HQ-FaiuVZHH9kzEKK7mOdVHtNHoAZADpDJtowNCkhMQxruLbnqB7WvRQzufCg%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D" @@ -2041,7 +284,7 @@ class CI_JVM_Test { // make credential request val credReq = CredentialRequest.forOfferedCredential( offeredCredentials.first(), - credentialWallet.generateDidProof(credentialWallet.TEST_DID, ciTestProvider.baseUrl, tokenResp.cNonce!!) + credentialWallet.generateDidProof(credentialWallet.TEST_DID, providerMetadata.issuer!!, tokenResp.cNonce!!) ) println("credReq: $credReq") @@ -2169,134 +412,6 @@ class CI_JVM_Test { println(SDJwt.parse(credentialResp.credential!!.jsonPrimitive.content).fullPayload.toString()) } - // issuance by reference - - //@Test - suspend fun testCredentialOfferByReference() { - println("// -------- CREDENTIAL ISSUER ----------") - println("// as CI provider, initialize credential offer for user, this time providing full offered credential object, and allowing pre-authorized code flow with user pin") - val issuanceSession = ciTestProvider.initializeCredentialOffer( - CredentialOffer.Builder(ciTestProvider.baseUrl) - .addOfferedCredential(ciTestProvider.metadata.credentialConfigurationsSupported!!.keys.first()), - 5.minutes, allowPreAuthorized = true, TxCode(TxInputMode.numeric), txCodeValue = "1234" - ) - println("issuanceSession: $issuanceSession") - - assertNotNull(actual = issuanceSession.credentialOffer) - assertEquals( - expected = ciTestProvider.metadata.credentialConfigurationsSupported!!.keys.first(), - actual = issuanceSession.credentialOffer!!.credentialConfigurationIds.first() - ) - - val offerRequest = ciTestProvider.getCredentialOfferRequest(issuanceSession, byReference = true) - println("offerRequest: $offerRequest") - assertNull(actual = offerRequest.credentialOffer) - assertNotNull(actual = offerRequest.credentialOfferUri) - - println("// create credential offer request url (this time cross-device)") - val offerUri = ciTestProvider.getCredentialOfferRequestUrl(offerRequest) - println("Offer URI: $offerUri") - - println("// -------- WALLET ----------") - println("// as WALLET: receive credential offer, either being called via deeplink or by scanning QR code") - println("// parse credential URI") - - val credentialOffer = - credentialWallet.resolveCredentialOffer(CredentialOfferRequest.fromHttpParameters(Url(offerUri).parameters.toMap())) - - assertNotNull(actual = credentialOffer.credentialIssuer) - assertContains(iterable = credentialOffer.grants.keys, element = GrantType.pre_authorized_code.value) - assertNotNull(actual = credentialOffer.grants[GrantType.pre_authorized_code.value]?.preAuthorizedCode) - assertNotNull(actual = credentialOffer.grants[GrantType.pre_authorized_code.value]?.txCode) - - println("// get issuer metadata") - val providerMetadataUri = - credentialWallet.getCIProviderMetadataUrl(credentialOffer.credentialIssuer) - val providerMetadata = ktorClient.get(providerMetadataUri).call.body() - println("providerMetadata: $providerMetadata") - - assertNotNull(actual = providerMetadata.credentialConfigurationsSupported) - - println("// resolve offered credentials") - val offeredCredentials = OpenID4VCI.resolveOfferedCredentials(credentialOffer, providerMetadata) - println("offeredCredentials: $offeredCredentials") - assertEquals(expected = 1, actual = offeredCredentials.size) - assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) - assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().credentialDefinition?.type?.last()) - val offeredCredential = offeredCredentials.first() - println("offeredCredentials[0]: $offeredCredential") - - println("// fetch access token using pre-authorized code (skipping authorization step)") - println("// try without user PIN, should be rejected!") - var tokenReq = TokenRequest( - grantType = GrantType.pre_authorized_code, - //clientId = testCIClientConfig.clientID, - redirectUri = credentialWallet.config.redirectUri, - preAuthorizedCode = credentialOffer.grants[GrantType.pre_authorized_code.value]!!.preAuthorizedCode, - txCode = null - ) - println("tokenReq: $tokenReq") - - var tokenResp = ktorClient.submitForm( - providerMetadata.tokenEndpoint!!, formParameters = parametersOf(tokenReq.toHttpParameters()) - ).body().let { TokenResponse.fromJSON(it) } - println("tokenResp: $tokenResp") - - assertFalse(actual = tokenResp.isSuccess) - assertEquals(expected = TokenErrorCode.invalid_grant.name, actual = tokenResp.error) - - println("// try with user PIN, should work:") - tokenReq = TokenRequest( - grantType = GrantType.pre_authorized_code, - clientId = testCIClientConfig.clientID, - redirectUri = credentialWallet.config.redirectUri, - preAuthorizedCode = credentialOffer.grants[GrantType.pre_authorized_code.value]!!.preAuthorizedCode, - txCode = issuanceSession.txCodeValue - ) - println("tokenReq: $tokenReq") - - tokenResp = ktorClient.submitForm( - providerMetadata.tokenEndpoint!!, formParameters = parametersOf(tokenReq.toHttpParameters()) - ).body().let { TokenResponse.fromJSON(it) } - println("tokenResp: $tokenResp") - - assertTrue(actual = tokenResp.isSuccess) - assertNotNull(actual = tokenResp.accessToken) - assertNotNull(actual = tokenResp.cNonce) - - println("// receive credential") - ciTestProvider.deferIssuance = false - val nonce = tokenResp.cNonce!! - - val credReq = CredentialRequest.forOfferedCredential( - offeredCredential, - credentialWallet.generateDidProof(credentialWallet.TEST_DID, ciTestProvider.baseUrl, nonce) - ) - println("credReq: $credReq") - - val credentialResp = ktorClient.post(providerMetadata.credentialEndpoint!!) { - contentType(ContentType.Application.Json) - bearerAuth(tokenResp.accessToken!!) - setBody(credReq.toJSON()) - }.body().let { CredentialResponse.fromJSON(it) } - println("credentialResp: $credentialResp") - - assertTrue(actual = credentialResp.isSuccess) - assertFalse(actual = credentialResp.isDeferred) - assertEquals(expected = CredentialFormat.jwt_vc_json, actual = credentialResp.format!!) - assertTrue(actual = credentialResp.credential!!.instanceOf(JsonPrimitive::class)) - - println("// parse and verify credential") - val credential = credentialResp.credential!!.jsonPrimitive.content - println(">>> Issued credential: $credential") - verifyIssuerAndSubjectId( - SDJwt.parse(credential).fullPayload["vc"]?.jsonObject!!, - ciTestProvider.CI_ISSUER_DID, credentialWallet.TEST_DID - ) - assertTrue(actual = JwtSignaturePolicy().verify(credential, null, mapOf()).isSuccess) - } - - val http = HttpClient { install(ContentNegotiation) { json() diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/OpenID4VCI_Test.kt b/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/OpenID4VCI_Test.kt index 763fa82bb..c241851b2 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/OpenID4VCI_Test.kt +++ b/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/OpenID4VCI_Test.kt @@ -11,6 +11,7 @@ import id.walt.oid4vc.data.dif.PresentationDefinition import id.walt.oid4vc.definitions.JWTClaims import id.walt.oid4vc.providers.TokenTarget import id.walt.oid4vc.requests.AuthorizationRequest +import id.walt.oid4vc.requests.CredentialOfferRequest import id.walt.oid4vc.requests.CredentialRequest import id.walt.oid4vc.requests.TokenRequest import id.walt.oid4vc.responses.AuthorizationCodeResponse @@ -67,7 +68,6 @@ class OpenID4VCI_Test { @Test fun testCredentialIssuanceIsolatedFunctions() = runTest { - // TODO: consider re-implementing CITestProvider, making use of new lib functions println("// -------- CREDENTIAL ISSUER ----------") // init credential offer for full authorization code flow val credOffer = CredentialOffer.Builder(ISSUER_BASE_URL) @@ -104,7 +104,6 @@ class OpenID4VCI_Test { // create issuance session and generate authorization code val authCodeResponse = OpenID4VC.processCodeFlowAuthorization(authReq, authReq.issuerState!!, ISSUER_METADATA, ISSUER_TOKEN_KEY) - //val authCodeResponse: AuthorizationCodeResponse = AuthorizationCodeResponse.success("test-code") val redirectUri = authCodeResponse.toRedirectUri(authReq.redirectUri ?: TODO(), authReq.responseMode ?: ResponseMode.query) Url(redirectUri).let { assertContains(iterable = it.parameters.names(), element = ResponseType.Code.name.lowercase()) @@ -193,9 +192,8 @@ class OpenID4VCI_Test { } // Test case for available authentication methods are: NONE, ID_TOKEN, VP_TOKEN, PRE_AUTHORIZED PWD(Handled by third party authorization server) - //@Test + @Test fun testCredentialIssuanceIsolatedFunctionsAuthCodeFlow() = runTest { - // TODO: consider re-implementing CITestProvider, making use of new lib functions // is it ok to generate the credential offer using the ciTestProvider (OpenIDCredentialIssuer class) ? val issuedCredentialId = "VerifiableId" @@ -321,7 +319,8 @@ class OpenID4VCI_Test { // ---------------------------------- println("// --Authentication method is ID_TOKEN--") issuerState = "test-state-idtoken-auth" - credOffer = CredentialOffer.fromJSONString(testIsolatedFunctionsCreateCredentialOffer(ISSUER_BASE_URL, issuerState, issuedCredentialId)) + val credOfferUrl = testIsolatedFunctionsCreateCredentialOffer(ISSUER_BASE_URL, issuerState, issuedCredentialId) + credOffer = CredentialOfferRequest.fromHttpQueryString(Url(credOfferUrl).encodedQuery).credentialOffer!! // Issuer Client shows credential offer request as QR code println(OpenID4VCI.getCredentialOfferRequestUrl(credOffer)) diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/wallettest.kt b/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/wallettest.kt index b06a8a71d..138dc97ba 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/wallettest.kt +++ b/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/wallettest.kt @@ -40,13 +40,11 @@ class wallettest { followRedirects = false } - private lateinit var ciTestProvider: CITestProvider private lateinit var credentialWallet: TestCredentialWallet private val testCIClientConfig = OpenIDClientConfig("test-client", null, redirectUri = "http://blank") //@BeforeAll /* Uncomment me */ fun init() { - ciTestProvider = CITestProvider() credentialWallet = TestCredentialWallet(CredentialWalletConfig("http://blank")) //ciTestProvider.start() } @@ -142,12 +140,11 @@ class wallettest { assertNotNull(actual = tokenResp.cNonce) println("// receive credential") - ciTestProvider.deferIssuance = false val nonce = tokenResp.cNonce!! val credReq = CredentialRequest.forOfferedCredential( offeredCredential = offeredCredential, - proof = credentialWallet.generateDidProof(credentialWallet.TEST_DID, ciTestProvider.baseUrl, nonce) + proof = credentialWallet.generateDidProof(credentialWallet.TEST_DID, providerMetadata.issuer!!, nonce) ) println("credReq: $credReq") diff --git a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt index a5683dd91..05bfb08aa 100644 --- a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt +++ b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt @@ -8,23 +8,15 @@ import cbor.Cbor import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.util.X509CertUtils import com.sksamuel.hoplite.ConfigException -import com.upokecenter.cbor.CBORObject import id.walt.commons.config.ConfigManager import id.walt.commons.config.ConfigurationException import id.walt.commons.persistence.ConfiguredPersistence -import id.walt.credentials.issuance.Issuer.mergingJwtIssue -import id.walt.credentials.issuance.Issuer.mergingSdJwtIssue -import id.walt.credentials.issuance.dataFunctions -import id.walt.credentials.utils.CredentialDataMergeUtils.mergeSDJwtVCPayloadWithMapping -import id.walt.credentials.vc.vcs.W3CVC import id.walt.crypto.keys.* import id.walt.crypto.keys.jwk.JWKKey import id.walt.crypto.utils.Base64Utils.base64UrlDecode import id.walt.crypto.utils.Base64Utils.encodeToBase64Url -import id.walt.crypto.utils.JsonUtils.toJsonElement import id.walt.crypto.utils.JsonUtils.toJsonObject import id.walt.did.dids.DidService -import id.walt.did.dids.DidUtils import id.walt.issuer.config.CredentialTypeConfig import id.walt.issuer.config.OIDCIssuerServiceConfig import id.walt.mdoc.COSECryptoProviderKeyInfo @@ -50,7 +42,6 @@ import id.walt.oid4vc.responses.* import id.walt.oid4vc.util.COSESign1Utils import id.walt.oid4vc.util.JwtUtils import id.walt.oid4vc.util.randomUUID -import id.walt.sdjwt.* import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.* import io.ktor.client.plugins.* @@ -64,11 +55,9 @@ import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.plus import kotlinx.serialization.* import kotlinx.serialization.json.* -import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid /** * OIDC for Verifiable Credential Issuance service provider, implementing abstract service provider from OIDC4VC library. @@ -98,11 +87,22 @@ open class CIProvider( val exampleIssuerKey by lazy { runBlocking { JWKKey.generate(KeyType.Ed25519) } } val exampleIssuerDid by lazy { runBlocking { DidService.registerByKey("jwk", exampleIssuerKey).did } } - // TODO: make configurable -// private val CI_TOKEN_KEY by lazy { KeyManager.resolveSerializedKeyBlocking("""""") } val CI_TOKEN_KEY = runBlocking { KeyManager.resolveSerializedKey(ConfigManager.getConfig().ciTokenKey) } -// private val CI_TOKEN_KEY by lazy { runBlocking { JWKKey.generate(KeyType.Ed25519) } } + + suspend fun sendCallback(sessionId: String, type: String, data: JsonObject, callbackUrl: String) { + try { + http.post(callbackUrl.replace("\$id", sessionId)) { + setBody(buildJsonObject { + put("id", sessionId) + put("type", type) + put("data", data) + }) + } + } catch (ex: Exception) { + throw IllegalArgumentException("Error sending HTTP POST request to issuer callback url.", ex) + } + } } // ------------------------------- @@ -166,7 +166,7 @@ open class CIProvider( // ------------------------------------- // Implementation of abstract issuer service provider interface - fun generateCredential(credentialRequest: CredentialRequest): CredentialResult { + fun generateCredential(credentialRequest: CredentialRequest, session: IssuanceSession): CredentialResult { log.debug { "GENERATING CREDENTIAL:" } log.debug { "Credential request: $credentialRequest" } log.debug { "CREDENTIAL REQUEST JSON -------:" } @@ -177,16 +177,16 @@ open class CIProvider( } return when (credentialRequest.format) { - CredentialFormat.mso_mdoc -> runBlocking { doGenerateMDoc(credentialRequest) } - else -> doGenerateCredential(credentialRequest) + CredentialFormat.mso_mdoc -> runBlocking { doGenerateMDoc(credentialRequest, session) } + else -> doGenerateCredential(credentialRequest, session) } } - fun getDeferredCredential(credentialID: String): CredentialResult { + fun getDeferredCredential(credentialID: String, session: IssuanceSession): CredentialResult { return deferredCredentialRequests[credentialID]?.let { when (it.format) { - CredentialFormat.mso_mdoc -> runBlocking { doGenerateMDoc(it) } - else -> doGenerateCredential(it) + CredentialFormat.mso_mdoc -> runBlocking { doGenerateMDoc(it, session) } + else -> doGenerateCredential(it, session) } } ?: throw DeferredCredentialError(CredentialErrorCode.invalid_request, message = "Invalid credential ID given") @@ -195,6 +195,7 @@ open class CIProvider( @OptIn(ExperimentalSerializationApi::class, ExperimentalStdlibApi::class) private fun doGenerateCredential( credentialRequest: CredentialRequest, + issuanceSession: IssuanceSession ): CredentialResult { if (credentialRequest.format == CredentialFormat.mso_mdoc) throw CredentialError( credentialRequest, CredentialErrorCode.unsupported_credential_format @@ -212,63 +213,34 @@ open class CIProvider( CredentialErrorCode.invalid_or_missing_proof, message = "Proof JWT header must contain kid or jwk claim" ) - val holderDid = if (!holderKid.isNullOrEmpty() && DidUtils.isDidUrl(holderKid)) holderKid.substringBefore("#") else null - val nonce = OpenID4VCI.getNonceFromProof(credentialRequest.proof!!) ?: throw CredentialError( - credentialRequest, - CredentialErrorCode.invalid_or_missing_proof, message = "Proof must contain nonce" - ) - val data: IssuanceSessionData = if (!tokenCredentialMapping.contains(nonce)) { - repeat(10) { - log.debug { "WARNING: RETURNING DEMO/EXAMPLE (= BOGUS) CREDENTIAL: nonce is not mapped to issuance request data (was deferred issuance tried?)" } - } - findMatchingSessionData( - credentialRequest, - listOf( - IssuanceSessionData( - id = Uuid.random().toString(), - exampleIssuerKey, - exampleIssuerDid, - IssuanceRequest( - issuerKey = Json.parseToJsonElement(KeySerialization.serializeKey(exampleIssuerKey)).jsonObject, - credentialConfigurationId = "OpenBadgeCredential_${credentialRequest.format.value}", - credentialData = Json.parseToJsonElement(IssuanceExamples.openBadgeCredentialData).jsonObject, - mdocData = null, - issuerDid = exampleIssuerDid - ) - ) - ) - ) ?: throw IllegalArgumentException("No matching issuance session data for nonce: $nonce") - } else { - log.debug { "RETRIEVING VC FROM TOKEN MAPPING: $nonce" } - findMatchingSessionData( - credentialRequest, - tokenCredentialMapping[nonce] - ?: throw IllegalArgumentException("No matching issuance session data found for nonce: $nonce!") - ) ?: throw IllegalArgumentException("No matching issuance session data found for nonce: $nonce!") - } + log.debug { "RETRIEVING ISSUANCE REQUEST FOR CREDENTIAL REQUEST" } + val request = findMatchingIssuanceRequest( + credentialRequest, issuanceSession.issuanceRequests + ) ?: throw IllegalArgumentException("No matching issuance request found for this session: ${issuanceSession.id}!") return CredentialResult(format = credentialRequest.format, credential = JsonPrimitive(runBlocking { - val vc = data.request.credentialData ?: throw MissingFieldException(listOf("credentialData"), "credentialData") + val vc = request.credentialData ?: throw MissingFieldException(listOf("credentialData"), "credentialData") + val resolvedIssuerKey = KeyManager.resolveSerializedKey(request.issuerKey) - data.run { - var issuerKid = issuerDid ?: data.issuerKey.key.getKeyId() + request.run { + var issuerKid = issuerDid ?: resolvedIssuerKey.getKeyId() if(!issuerDid.isNullOrEmpty()) { if (issuerDid.startsWith("did:key") && issuerDid.length == 186) // EBSI conformance corner case when issuer uses did:key instead of did:ebsi and no trust framework is defined issuerKid = issuerDid + "#" + issuerDid.removePrefix("did:key:") else if (issuerDid.startsWith("did:ebsi")) - issuerKid = issuerDid + "#" + issuerKey.key.getKeyId() + issuerKid = issuerDid + "#" + resolvedIssuerKey.getKeyId() } val holderKeyJWK = JWKKey.importJWK(holderKey.toString()).getOrNull()?.exportJWKObject()?.plus("kid" to JWKKey.importJWK(holderKey.toString()).getOrThrow().getKeyId())?.toJsonObject() - when (data.request.credentialFormat) { + when (credentialFormat) { CredentialFormat.sd_jwt_vc -> OpenID4VCI.generateSdJwtVC(credentialRequest, vc, request.mapping, request.selectiveDisclosure, vct = metadata.credentialConfigurationsSupported?.get(request.credentialConfigurationId)?.vct ?: throw ConfigurationException( ConfigException("No vct configured for given credential configuration id: ${request.credentialConfigurationId}") - ), issuerDid, issuerKid, request.x5Chain, data.issuerKey.key).toString() + ), issuerDid, issuerKid, request.x5Chain, resolvedIssuerKey).toString() else -> OpenID4VCI.generateW3CJwtVC(credentialRequest, vc, request.mapping, request.selectiveDisclosure, - issuerDid, issuerKid, request.x5Chain, data.issuerKey.key) + issuerDid, issuerKid, request.x5Chain, resolvedIssuerKey) } }.also { log.debug { "Respond VC: $it" } } })) @@ -277,6 +249,7 @@ open class CIProvider( @OptIn(ExperimentalSerializationApi::class) private suspend fun doGenerateMDoc( credentialRequest: CredentialRequest, + issuanceSession: IssuanceSession ): CredentialResult { val coseSign1 = Cbor.decodeFromByteArray( credentialRequest.proof?.cwt?.base64UrlDecode() ?: throw CredentialError( @@ -290,21 +263,19 @@ open class CIProvider( CredentialErrorCode.invalid_or_missing_proof, message = "No nonce found on proof" ) - println("RETRIEVING VC FROM TOKEN MAPPING: $nonce") - val data: IssuanceSessionData = tokenCredentialMapping[nonce]?.first() - ?: throw CredentialError( - credentialRequest, - CredentialErrorCode.invalid_request, - "The issuanceIdCredentialMapping does not contain a mapping for: $nonce!" - ) - val issuerSignedItems = data.request.mdocData ?: throw MissingFieldException(listOf("mdocData"), "mdocData") - val issuerKey = JWK.parse(runBlocking { data.issuerKey.key.exportJWK() }).toECKey() - val keyId = runBlocking { data.issuerKey.key.getKeyId() } + println("RETRIEVING ISSUANCE REQUEST FOR CREDENTIAL REQUEST: $nonce") + val request = findMatchingIssuanceRequest( + credentialRequest, issuanceSession.issuanceRequests + ) ?: throw IllegalArgumentException("No matching issuance request found for this session: ${issuanceSession.id}!") + val issuerSignedItems = request.mdocData ?: throw MissingFieldException(listOf("mdocData"), "mdocData") + val resolvedIssuerKey = KeyManager.resolveSerializedKey(request.issuerKey) + val issuerKey = JWK.parse(runBlocking { resolvedIssuerKey.exportJWK() }).toECKey() + val keyID = resolvedIssuerKey.getKeyId() val cryptoProvider = SimpleCOSECryptoProvider(listOf( COSECryptoProviderKeyInfo( - keyId, AlgorithmID.ECDSA_256, issuerKey.toECPublicKey(), issuerKey.toECPrivateKey(), - x5Chain = data.request.x5Chain?.map { X509CertUtils.parse(it) } ?: listOf(), - trustedRootCAs = data.request.trustedRootCAs?.map { X509CertUtils.parse(it) } ?: listOf() + keyID, AlgorithmID.ECDSA_256, issuerKey.toECPublicKey(), issuerKey.toECPrivateKey(), + x5Chain = request.x5Chain?.map { X509CertUtils.parse(it) } ?: listOf(), + trustedRootCAs = request.trustedRootCAs?.map { X509CertUtils.parse(it) } ?: listOf() ) )) val mdoc = MDocBuilder( @@ -326,9 +297,10 @@ open class CIProvider( DataElement.fromCBOR( OneKey(holderKey.publicKey, null).AsCBOR().EncodeToBytes() ) - ), cryptoProvider, keyId + ), cryptoProvider, keyID ).also { - data.sendCallback("generated_mdoc", buildJsonObject { put("mdoc", it.toCBORHex()) }) + if(!issuanceSession.callbackUrl.isNullOrEmpty()) + sendCallback(issuanceSession.id, "generated_mdoc", buildJsonObject { put("mdoc", it.toCBORHex()) }, issuanceSession.callbackUrl) } return CredentialResult( CredentialFormat.mso_mdoc, JsonPrimitive(mdoc.issuerSigned.toMapElement().toCBOR().encodeToBase64Url()), @@ -336,26 +308,6 @@ open class CIProvider( ) } - - @OptIn(ExperimentalEncodingApi::class) - fun parseFromJwt(jwt: String): Pair { - val jwtParts = jwt.split(".") - - fun decodeJwtPart(idx: Int) = - Json.parseToJsonElement(jwtParts[idx].base64UrlDecode().decodeToString()).jsonObject - - val header = decodeJwtPart(0) - val payload = decodeJwtPart(1) - - val subjectDid = - header["kid"]?.jsonPrimitive?.contentOrNull - ?: throw IllegalArgumentException("No kid in proof.jwt header!") - val nonce = payload["nonce"]?.jsonPrimitive?.contentOrNull - ?: throw IllegalArgumentException("No nonce in proof.jwt payload!") - - return Pair(subjectDid, nonce) - } - @OptIn(ExperimentalSerializationApi::class) fun generateBatchCredentialResponse( batchCredentialRequest: BatchCredentialRequest, @@ -384,83 +336,6 @@ open class CIProvider( ) } - - @Serializable - data class IssuanceSessionData( - val id: String, - val issuerKey: DirectSerializedKey, - val issuerDid: String?, - val request: IssuanceRequest, - val callbackUrl: String? = null, - ) { - constructor( - id: String, - issuerKey: Key, - issuerDid: String?, - request: IssuanceRequest, - callbackUrl: String? = null, - ) : this(id, DirectSerializedKey(issuerKey), issuerDid, request, callbackUrl) - - suspend fun sendCallback(type: String, data: JsonObject) { - if (callbackUrl != null) { - try { - http.post(callbackUrl.replace("\$id", id)) { - setBody(buildJsonObject { - put("id", id) - put("type", type) - put("data", data) - }) - } - } catch (ex: Exception) { - throw IllegalArgumentException("Error sending HTTP POST request to issuer callback url.", ex) - } - } - } - - val jwtCryptoProvider - get() = SingleKeyJWTCryptoProvider(issuerKey.key) - } - - // TODO: Hack as this is non stateless because of oidc4vc lib API - val sessionCredentialPreMapping = ConfiguredPersistence>( - // session id -> VC - "sessionid_vc", defaultExpiration = 5.minutes, - encoding = { Json.encodeToString(it) }, - decoding = { Json.decodeFromString(it) }, - ) - - // TODO: Hack as this is non stateless because of oidc4vc lib API - private val tokenCredentialMapping = ConfiguredPersistence>( - // token -> VC - "token_vc", defaultExpiration = 5.minutes, - encoding = { Json.encodeToString(it) }, - decoding = { Json.decodeFromString(it) }, - ) - - //private val sessionTokenMapping = HashMap() // session id -> token - - // TODO: Hack as this is non stateless because of oidc4vc lib API - suspend fun setIssuanceDataForIssuanceId(issuanceId: String, data: List) { - log.debug { "DEPOSITED CREDENTIAL FOR ISSUANCE ID: $issuanceId" } - sessionCredentialPreMapping[issuanceId] = data - } - - // TODO: Hack as this is non stateless because of oidc4vc lib API - suspend fun mapSessionIdToToken(sessionId: String, token: String) { - log.debug { "MAPPING SESSION ID TO TOKEN: $sessionId -->> $token" } - val premappedVc = sessionCredentialPreMapping[sessionId] - ?: throw IllegalArgumentException("No credential pre-mapped with any such session id: $sessionId (for use with token: $token)") - sessionCredentialPreMapping.remove(sessionId) - log.debug { "SWAPPING PRE-MAPPED VC FROM SESSION ID TO NEW TOKEN: $token" } - tokenCredentialMapping[token] = premappedVc - - premappedVc.first().let { - it.sendCallback("requested_token", buildJsonObject { - put("request", Json.encodeToJsonElement(it.request).jsonObject) - }) - } - } - suspend fun getJwksSessions() : JsonObject{ var jwksList = buildJsonObject { put("keys", buildJsonArray { add(buildJsonObject { @@ -470,15 +345,16 @@ open class CIProvider( put("kid", CI_TOKEN_KEY.getKeyId()) }) }) } - sessionCredentialPreMapping.getAll().forEach { - it.forEach { + authSessions.getAll().forEach { session -> + session.issuanceRequests.forEach { + val resolvedIssuerKey = KeyManager.resolveSerializedKey(it.issuerKey) jwksList = buildJsonObject { put("keys", buildJsonArray { val jwkWithKid = buildJsonObject { - it.issuerKey.key.getPublicKey().exportJWKObject().forEach { + resolvedIssuerKey.getPublicKey().exportJWKObject().forEach { put(it.key, it.value) } - put("kid", it.issuerKey.key.getPublicKey().getKeyId()) + put("kid", resolvedIssuerKey.getPublicKey().getKeyId()) } add(jwkWithKid) jwksList.forEach { it.value.jsonArray.forEach { @@ -499,12 +375,12 @@ open class CIProvider( fun getDocTypeByCredentialConfigurationId(id: String) = metadata.credentialConfigurationsSupported?.get(id)?.docType // Use format, type, vct and docType checks to filter matching entries - private fun findMatchingSessionData( + private fun findMatchingIssuanceRequest( credentialRequest: CredentialRequest, - sessionDataList: List - ): IssuanceSessionData? { - return sessionDataList.find { sessionData -> - val credentialConfigurationId = sessionData.request.credentialConfigurationId + issuanceRequests: List + ): IssuanceRequest? { + return issuanceRequests.find { sessionData -> + val credentialConfigurationId = sessionData.credentialConfigurationId val credentialFormat = getFormatByCredentialConfigurationId(credentialConfigurationId) require(credentialFormat == credentialRequest.format) { "Format does not match" } // Depending on the format, perform specific checks @@ -569,7 +445,7 @@ open class CIProvider( } IssuanceSession( randomUUID(), authorizationRequest, - Clock.System.now().plus(expiresIn), authServerState = authServerState + Clock.System.now().plus(expiresIn), listOf(), authServerState = authServerState ) } else { getVerifiedSession(authorizationRequest.issuerState!!)?.copy(authorizationRequest = authorizationRequest) @@ -582,11 +458,13 @@ open class CIProvider( id = it.id, authorizationRequest = authorizationRequest, expirationTimestamp = Clock.System.now().plus(5.minutes), + issuanceRequests = it.issuanceRequests, authServerState = authServerState, txCode = it.txCode, txCodeValue = it.txCodeValue, credentialOffer = it.credentialOffer, cNonce = it.cNonce, + callbackUrl = it.callbackUrl, customParameters = it.customParameters ) putSession(it.id, updatedSession) @@ -594,12 +472,15 @@ open class CIProvider( } open fun initializeCredentialOffer( - credentialOfferBuilder: CredentialOffer.Builder, + issuanceRequests: List, expiresIn: Duration, allowPreAuthorized: Boolean, + callbackUrl: String? = null, txCode: TxCode? = null, txCodeValue: String? = null, ): IssuanceSession = runBlocking { val sessionId = randomUUID() + val credentialOfferBuilder = + OidcIssuance.issuanceRequestsToCredentialOfferBuilder(issuanceRequests) credentialOfferBuilder.addAuthorizationCodeGrant(sessionId) if (allowPreAuthorized) credentialOfferBuilder.addPreAuthorizedCodeGrant( @@ -610,9 +491,11 @@ open class CIProvider( id = sessionId, authorizationRequest = null, expirationTimestamp = Clock.System.now().plus(expiresIn), + issuanceRequests= issuanceRequests, txCode = txCode, txCodeValue = txCodeValue, - credentialOffer = credentialOfferBuilder.build() + credentialOffer = credentialOfferBuilder.build(), + callbackUrl = callbackUrl ).also { putSession(it.id, it) } @@ -642,7 +525,7 @@ open class CIProvider( if(!validationResult.success) throw CredentialError(credentialRequest, CredentialErrorCode.invalid_request, message = validationResult.message) // create credential result - val credentialResult = generateCredential(credentialRequest) + val credentialResult = generateCredential(credentialRequest, session) return@runBlocking createCredentialResponseFor(credentialResult, session) } @@ -659,7 +542,7 @@ open class CIProvider( "Session not found for given access token, or session expired." ) // issue credential for credential request - return@runBlocking createCredentialResponseFor(getDeferredCredential(credentialId), session) + return@runBlocking createCredentialResponseFor(getDeferredCredential(credentialId, session), session) } fun processTokenRequest(tokenRequest: TokenRequest): TokenResponse = runBlocking { diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/IssuanceSession.kt b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/IssuanceSession.kt similarity index 82% rename from waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/IssuanceSession.kt rename to waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/IssuanceSession.kt index 99c403033..b9369aea7 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/IssuanceSession.kt +++ b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/IssuanceSession.kt @@ -1,7 +1,8 @@ -package id.walt.oid4vc.providers +package id.walt.issuer.issuance import id.walt.oid4vc.data.CredentialOffer import id.walt.oid4vc.data.TxCode +import id.walt.oid4vc.providers.AuthorizationSession import id.walt.oid4vc.requests.AuthorizationRequest import kotlinx.datetime.Instant import kotlinx.serialization.Serializable @@ -12,10 +13,12 @@ data class IssuanceSession( override val id: String, override val authorizationRequest: AuthorizationRequest?, override val expirationTimestamp: Instant, + val issuanceRequests: List, val txCode: TxCode? = null, val txCodeValue: String? = null, override val authServerState: String? = null, //the state used for additional authentication with pwd, id_token or vp_token. val credentialOffer: CredentialOffer? = null, val cNonce: String? = null, + val callbackUrl: String? = null, val customParameters: Map? = null, ) : AuthorizationSession() diff --git a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/IssuerApi.kt b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/IssuerApi.kt index b579d893a..ce6891754 100644 --- a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/IssuerApi.kt +++ b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/IssuerApi.kt @@ -42,30 +42,16 @@ suspend fun createCredentialOfferUri( vct = if (credentialFormat == CredentialFormat.sd_jwt_vc) OidcApi.metadata.getVctByCredentialConfigurationId(it.credentialConfigurationId) ?: throw IllegalArgumentException("VCT not found") else null) } - val credentialOfferBuilder = - OidcIssuance.issuanceRequestsToCredentialOfferBuilder(overwrittenIssuanceRequests) - val issuanceSession = OidcApi.initializeCredentialOffer( - credentialOfferBuilder = credentialOfferBuilder, + issuanceRequests = overwrittenIssuanceRequests, expiresIn, allowPreAuthorized = when (overwrittenIssuanceRequests[0].authenticationMethod) { AuthenticationMethod.PRE_AUTHORIZED -> true else -> false - } + }, + callbackUrl = callbackUrl ) - OidcApi.setIssuanceDataForIssuanceId(issuanceSession.id, overwrittenIssuanceRequests.map { - val key = KeyManager.resolveSerializedKey(it.issuerKey) - - CIProvider.IssuanceSessionData( - id = issuanceSession.id, - issuerKey = key, - issuerDid = it.issuerDid, - request = it, - callbackUrl = callbackUrl - ) - }) // TODO: Hack as this is non stateless because of oidc4vc lib API - logger.debug { "issuanceSession: $issuanceSession" } val offerRequest = @@ -529,8 +515,11 @@ fun Application.issuerApi() { val credentialOffer = issuanceSession.credentialOffer ?: throw BadRequestException("Session has no credential offer set") - OidcApi.sessionCredentialPreMapping[sessionId]?.first() - ?.sendCallback("resolved_credential_offer", credentialOffer.toJSON()) + issuanceSession.callbackUrl?.let { + CIProvider.sendCallback( + sessionId, "resolved_credential_offer", credentialOffer.toJSON(), it + ) + } context.respond(credentialOffer.toJSON()) } diff --git a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/OidcApi.kt b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/OidcApi.kt index bcf476a38..8014e4f00 100644 --- a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/OidcApi.kt +++ b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/OidcApi.kt @@ -3,14 +3,12 @@ package id.walt.issuer.issuance import id.walt.policies.Verifier import id.walt.policies.models.PolicyRequest.Companion.parsePolicyRequests -import id.walt.crypto.utils.Base64Utils.base64UrlDecode import id.walt.oid4vc.OpenID4VC import id.walt.oid4vc.data.* import id.walt.oid4vc.data.dif.PresentationDefinition import id.walt.oid4vc.data.dif.PresentationSubmission import id.walt.oid4vc.definitions.JWTClaims import id.walt.oid4vc.errors.* -import id.walt.oid4vc.providers.IssuanceSession import id.walt.oid4vc.providers.TokenTarget import id.walt.oid4vc.requests.AuthorizationRequest import id.walt.oid4vc.requests.BatchCredentialRequest @@ -120,9 +118,8 @@ object OidcApi : CIProvider() { get("/authorize") { val authReq = runBlocking { AuthorizationRequest.fromHttpParametersAuto(call.parameters.toMap()) } try { - val issuanceSessionData = - OidcApi.sessionCredentialPreMapping[authReq.issuerState!!] ?: error("No such pre mapping: ${authReq.issuerState}") - val authMethod = issuanceSessionData.first().request.authenticationMethod ?: AuthenticationMethod.NONE + val issuanceSession = authReq.issuerState?.let { getSession(it) } ?: error("No issuance session found for given issuer state, or issuer state was empty: ${authReq.issuerState}") + val authMethod = issuanceSession.issuanceRequests.firstOrNull()?.authenticationMethod ?: AuthenticationMethod.NONE val authResp: Any = when { ResponseType.Code in authReq.responseType -> { when (authMethod) { @@ -138,27 +135,23 @@ object OidcApi : CIProvider() { } AuthenticationMethod.ID_TOKEN -> { - val idTokenRequestJwtKid = issuanceSessionData.first().issuerKey.key.getKeyId() - val idTokenRequestJwtPrivKey = issuanceSessionData.first().issuerKey OpenID4VC.processCodeFlowAuthorizationWithAuthorizationRequest( authReq, ResponseType.IdToken, metadata, CI_TOKEN_KEY, - issuanceSessionData.first().request.useJar + issuanceSession.issuanceRequests.first().useJar ) } AuthenticationMethod.VP_TOKEN -> { - val vpTokenRequestJwtKid = issuanceSessionData.first().issuerKey.key.getKeyId() - val vpTokenRequestJwtPrivKey = issuanceSessionData.first().issuerKey - val vpProfile = issuanceSessionData.first().request.vpProfile ?: OpenId4VPProfile.DEFAULT - val credFormat = issuanceSessionData.first().request.credentialFormat ?: when(vpProfile) { + val vpProfile = issuanceSession.issuanceRequests.first().vpProfile ?: OpenId4VPProfile.DEFAULT + val credFormat = issuanceSession.issuanceRequests.first().credentialFormat ?: when(vpProfile) { OpenId4VPProfile.HAIP -> CredentialFormat.sd_jwt_vc OpenId4VPProfile.ISO_18013_7_MDOC -> CredentialFormat.mso_mdoc OpenId4VPProfile.EBSIV3 -> CredentialFormat.jwt_vc else -> CredentialFormat.jwt_vc_json } - val vpRequestValue = issuanceSessionData.first().request.vpRequestValue + val vpRequestValue = issuanceSession.issuanceRequests.first().vpRequestValue ?: throw IllegalArgumentException("missing vpRequestValue parameter") // Generate Presentation Definition @@ -176,13 +169,13 @@ object OidcApi : CIProvider() { OpenID4VC.processCodeFlowAuthorizationWithAuthorizationRequest( authReq, ResponseType.VpToken, metadata, CI_TOKEN_KEY, - issuanceSessionData.first().request.useJar, + issuanceSession.issuanceRequests.first().useJar, presentationDefinition ) } AuthenticationMethod.NONE -> OpenID4VC.processCodeFlowAuthorization( - authReq, issuanceSessionData.first().id, metadata, CI_TOKEN_KEY) + authReq, issuanceSession.id, metadata, CI_TOKEN_KEY) else -> { throw AuthorizationError( authReq, @@ -194,7 +187,7 @@ object OidcApi : CIProvider() { } ResponseType.Token in authReq.responseType -> OpenID4VC.processImplicitFlowAuthorization( - authReq, issuanceSessionData.first().id, metadata, CI_TOKEN_KEY) + authReq, issuanceSession.id, metadata, CI_TOKEN_KEY) else -> { throw AuthorizationError( @@ -314,21 +307,6 @@ object OidcApi : CIProvider() { try { val tokenResp = processTokenRequest(tokenReq) logger.info { "/token tokenResp: $tokenResp" } - - val sessionId = Json.parseToJsonElement( - (tokenResp.accessToken - ?: throw IllegalArgumentException("No access token was responded with tokenResp?")).split( - "." - )[1].base64UrlDecode().decodeToString() - ).jsonObject["sub"]?.jsonPrimitive?.contentOrNull - ?: throw IllegalArgumentException("Could not get session ID from token response!") - val nonceToken = tokenResp.cNonce - ?: throw IllegalArgumentException("No nonce token was responded with the tokenResp?") - OidcApi.mapSessionIdToToken( - sessionId, - nonceToken - ) // TODO: Hack as this is non stateless because of oidc4vc lib API - call.respond(tokenResp.toJSON()) } catch (exc: TokenError) { logger.error(exc) { "Token error: " } @@ -463,64 +441,4 @@ object OidcApi : CIProvider() { "Authorization request does not refer to a pushed authorization session" ) } - - /* - private val sessionCache = mutableMapOf() - - override fun generateCredential(credentialRequest: CredentialRequest): CredentialResult { - return doGenerateCredential(credentialRequest) - } - - override fun getDeferredCredential(credentialID: String): CredentialResult { - TODO("Not yet implemented") - } - - override fun getSession(id: String): IssuanceSession? { - return sessionCache[id] - } - - override fun putSession(id: String, session: IssuanceSession): IssuanceSession? { - return sessionCache.put(id, session) - } - - override fun removeSession(id: String): IssuanceSession? { - return sessionCache.remove(id) - } - - //private val CI_TOKEN_KEY = KeyService.getService().generate(KeyAlgorithm.RSA) - //private val CI_DID_KEY = KeyService.getService().generate(KeyAlgorithm.EdDSA_Ed25519) - //val CI_ISSUER_DID = DidService.create(DidMethod.key, CI_DID_KEY.id) - override fun signToken(target: TokenTarget, payload: JsonObject, header: JsonObject?, keyId: String?): String { - - TODO() - //return JwtService.getService().sign(keyId ?: CI_TOKEN_KEY.id, payload.toString()) - } - - override fun verifyTokenSignature(target: TokenTarget, token: String): Boolean = TODO() //JwtService.getService().verify(token).verified - - private fun doGenerateCredential(credentialRequest: CredentialRequest): CredentialResult { - TODO() - /*if(credentialRequest.format == CredentialFormat.mso_mdoc) throw CredentialError(credentialRequest, CredentialErrorCode.unsupported_credential_format) - val types = credentialRequest.types ?: credentialRequest.credentialDefinition?.types ?: throw CredentialError(credentialRequest, CredentialErrorCode.unsupported_credential_type) - val proofHeader = credentialRequest.proof?.jwt?.let { parseTokenHeader(it) } ?: throw CredentialError(credentialRequest, CredentialErrorCode.invalid_or_missing_proof, message = "Proof must be JWT proof") - val holderKid = proofHeader[JWTClaims.Header.keyID]?.jsonPrimitive?.content ?: throw CredentialError(credentialRequest, CredentialErrorCode.invalid_or_missing_proof, message = "Proof JWT header must contain kid claim") - return Signatory.getService().issue( - types.last(), - ProofConfig(CI_ISSUER_DID, subjectDid = resolveDIDFor(holderKid)), - issuer = W3CIssuer(baseUrl), - storeCredential = false).let { - when(credentialRequest.format) { - CredentialFormat.ldp_vc -> Json.decodeFromString(it) - else -> JsonPrimitive(it) - } - }.let { CredentialResult(credentialRequest.format, it) }*/ - } - - private fun resolveDIDFor(keyId: String): String { - TODO() - - //return DidUrl.from(keyId).did - } - - */ } diff --git a/waltid-services/waltid-issuer-api/src/test/kotlin/id/walt/LocalIssuerApiTest.kt b/waltid-services/waltid-issuer-api/src/test/kotlin/id/walt/LocalIssuerApiTest.kt index 64e38db16..1f9b5b870 100644 --- a/waltid-services/waltid-issuer-api/src/test/kotlin/id/walt/LocalIssuerApiTest.kt +++ b/waltid-services/waltid-issuer-api/src/test/kotlin/id/walt/LocalIssuerApiTest.kt @@ -15,7 +15,7 @@ import kotlin.test.Test import kotlin.test.assertEquals class IssuerApiTest { - +companion object { @Language("JSON") val TEST_KEY = """{ "type": "jwk", @@ -126,13 +126,12 @@ class IssuerApiTest { "issuanceDate": "\u003ctimestamp\u003e", "expirationDate": "\u003ctimestamp-in:365d\u003e" }""" - + val jsonKeyObj = Json.decodeFromString(TEST_KEY) + val jsonVCObj = Json.decodeFromString(TEST_W3VC) + val jsonMappingObj = Json.decodeFromString(TEST_MAPPING) +} @Test fun testJwt() = runTest { - val jsonKeyObj = Json.decodeFromString(TEST_KEY) - val jsonVCObj = Json.decodeFromString(TEST_W3VC) - val jsonMappingObj = Json.decodeFromString(TEST_MAPPING) - val issueRequest = IssuanceRequest( issuerKey = jsonKeyObj, diff --git a/waltid-services/waltid-issuer-api/src/test/kotlin/id/walt/OidcIssuanceTest.kt b/waltid-services/waltid-issuer-api/src/test/kotlin/id/walt/OidcIssuanceTest.kt index 5cd8b0071..46379ab14 100644 --- a/waltid-services/waltid-issuer-api/src/test/kotlin/id/walt/OidcIssuanceTest.kt +++ b/waltid-services/waltid-issuer-api/src/test/kotlin/id/walt/OidcIssuanceTest.kt @@ -1,7 +1,9 @@ package id.walt +import id.walt.IssuerApiTest.Companion.TEST_ISSUER_DID import id.walt.commons.config.ConfigManager import id.walt.issuer.issuance.CIProvider +import id.walt.issuer.issuance.IssuanceRequest import id.walt.oid4vc.OpenID4VCI import id.walt.oid4vc.data.CredentialOffer import id.walt.oid4vc.requests.CredentialOfferRequest @@ -18,7 +20,13 @@ class OidcIssuanceTest { // -------- CREDENTIAL ISSUER ---------- // as CI provider, initialize credential offer for user val issuanceSession = ciTestProvider.initializeCredentialOffer( - CredentialOffer.Builder(ciTestProvider.baseUrl).addOfferedCredential("VerifiableId"), + listOf(IssuanceRequest( + issuerKey = IssuerApiTest.jsonKeyObj, + credentialData = IssuerApiTest.jsonVCObj, + credentialConfigurationId = "VerifiableId", + mapping = IssuerApiTest.jsonMappingObj, + issuerDid = TEST_ISSUER_DID + )), 5.minutes, allowPreAuthorized = false )