walt-id / waltid-openid4vc

Kotlin multiplatform library implementing the data models and protocols of the OID4VC specifications, including OID4VCI, OID4VP and SIOPv2.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[moved]

We're currently redesigning our products to make them more efficient and easy to work with. Future versions of this library will be realised via a new repo, which we will publish soon. You can rest assured that the structure and features supported in this repo will be exactly the same in the new one.

You can find more info about the redesign and the introduction of the community stack here. In the meantime, if you have any questions or concerns, please reach out to us.

OpenID4VC - Kotlin multiplatform library

by walt.id

Multiplatform library implementing the data models and protocols of the OpenID for Verifiable Credentials specifications, including OID4VCI, OID4VP and SIOPv2.

CI/CD Workflow for walt.id OpenID4VC Join community! Follow @walt_id

Getting Started

What it provides

  • Request and response data objects
    • Parse and serialize to/from HTTP URI query parameters and/or HTTP form data or JSON data from request bodies
  • Data structures defined by OpenID and DIF specifications
  • Error handling
  • Interfaces for state management and cryptographic operations
  • Abstract base objects for issuer, verifier and wallet providers, implementing common business logic

How to use it

To use it, depending on the kind of service provider you want to implement,

  • Implement the abstract base class of the type of service provider you want to create (Issuer, Verifier or Wallet)
  • Implement the interfaces for session management and cryptographic operations
  • Implement a REST API providing the HTTP endpoints defined by the respective specification

Architecture

architecture

Examples

The following examples show how to use the library, with simple, minimal implementations of Issuer, Verifier and Wallet REST endpoints and business logic, for processing the OpenID4VC protocols.

The examples are based on JVM and make use of ktor for the HTTP server endpoints and client-side request handling, and the waltid-ssikit for the cryptographic operations and credential and presentation handling.

Issuer

For the full demo issuer implementation, refer to /src/jvmTest/kotlin/id/walt/oid4vc/CITestProvider.kt

REST endpoints

For the OpenID4VCI issuance protocol, implement the following endpoints:

Well-defined endpoints:

This endpoints are well-defined, and need to be available under this exact path, relative to your issuer base URL:

  • GET /.well-known/openid-configuration

  • GET /.well-known/openid-credential-issuer

Returns the issuer provider metadata.

get("/.well-known/openid-configuration") {
call.respond(metadata.toJSON())
}
get("/.well-known/openid-credential-issuer") {
call.respond(metadata.toJSON())
}

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.

  • POST /par

Endpoint to receive pushed authorization requests, referenced in the provider metadata as pushed_authorization_request_endpoint, see also here.

post("/par") {
val authReq = AuthorizationRequest.fromHttpParameters(call.receiveParameters().toMap())
try {
val session = initializeAuthorization(authReq, 600)
call.respond(getPushedAuthorizationSuccessResponse(session).toJSON())
} catch (exc: AuthorizationError) {
call.respond(HttpStatusCode.BadRequest, exc.toPushedAuthorizationErrorResponse().toJSON())
}
}

  • GET /authorize

Authorization endpoint, referenced in provider metadata as authorization_endpoint, see here

Not required for the pre-authorized issuance flow.

get("/authorize") {
val authReq = AuthorizationRequest.fromHttpParameters(call.parameters.toMap())
try {
val authResp = if(authReq.responseType == ResponseType.code.name) {
processCodeFlowAuthorization(authReq)
} else if(authReq.responseType.contains(ResponseType.token.name)) {
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 == ResponseType.code.name) 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

Token endpoint, referenced in provider metadata as token_endpoint, see here

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

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.

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<JsonObject>())
try {
call.respond(generateCredentialResponse(credReq, accessToken).toJSON())
} catch (exc: CredentialError) {
call.respond(HttpStatusCode.BadRequest, exc.toCredentialErrorResponse().toJSON())
}
}
}

  • POST /credential_deferred

Deferred credential endpoint, to fetch issued credential if issuance is deferred. Referenced in provider metadata as deferred_credential_endpoint (missing in spec).

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

Batch credential endpoint to 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.

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())
}
}

Business logic

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

class CITestProvider(): OpenIDCredentialIssuer(
baseUrl = CI_PROVIDER_BASE_URL,
config = CredentialIssuerConfig(
credentialsSupported = listOf(
CredentialSupported(
CredentialFormat.jwt_vc_json, "VerifiableId",
cryptographicBindingMethodsSupported = setOf("did"), cryptographicSuitesSupported = setOf("ES256K"),
types = listOf("VerifiableCredential", "VerifiableId"),
customParameters = mapOf("foo" to JsonPrimitive("bar"))
),
CredentialSupported(
CredentialFormat.jwt_vc_json, "VerifiableDiploma",
cryptographicBindingMethodsSupported = setOf("did"), cryptographicSuitesSupported = setOf("ES256K"),
types = listOf("VerifiableCredential", "VerifiableAttestation", "VerifiableDiploma")
)
)
)
) {

  • Simple session management example

Here we implement a simplistic in-memory session management:

// session management
private val authSessions: MutableMap<String, IssuanceSession> = mutableMapOf()
override fun getSession(id: String): IssuanceSession? = authSessions[id]
override fun putSession(id: String, session: IssuanceSession) = authSessions.put(id, session)
override fun removeSession(id: String) = authSessions.remove(id)

  • Crypto operations and credential issuance

Token signing and credential issuance based on waltid-ssikit

// crypto operations and credential issuance
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)
val deferredCredentialRequests = mutableMapOf<String, CredentialRequest>()
var deferIssuance = false
override fun signToken(target: TokenTarget, payload: JsonObject, header: JsonObject?, keyId: String?)
= JwtService.getService().sign(keyId ?: CI_TOKEN_KEY.id, payload.toString())
override fun verifyTokenSignature(target: TokenTarget, token: String)
= JwtService.getService().verify(token).verified
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.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<JsonObject>(it)
else -> JsonPrimitive(it)
}
}.let { CredentialResult(credentialRequest.format, it) }
}
private fun resolveDIDFor(keyId: String): String {
return DidUrl.from(keyId).did
}

Verifier

For the full demo verifier implementation, refer to /src/jvmTest/kotlin/id/walt/oid4vc/VPTestVerifier.kt

REST endpoints

Business logic

Wallet

REST endpoints

Business logic

License

Licensed under the Apache License, Version 2.0

Example flows:

EBSI conformance test: Credential issuance:

sequenceDiagram
Issuer -->> Wallet: Issuance request (QR, or by request)
Wallet ->> Issuer: Resolve credential offer
Issuer -->> Wallet: Credential offer
Wallet ->> Issuer: fetch OpenID Credential Issuer metadata
Issuer -->> Wallet: Credential issuer metadata
Wallet ->> Wallet: Check if external authorization service (AS)
Wallet ->> AS: fetch OpenID provider metadata
AS -->> Wallet: OpenID provider metadata
Wallet ->> Wallet: resolve offered credential metainfo
Wallet ->> Wallet: Generate code verifier and code challenge
Wallet ->> AS: Authorization request, auth details and code challenge
AS -->> Wallet: Redirect to wallet with id_token request
Wallet ->> Wallet: Generate id_token
Wallet ->> AS: POST id_token response to redirect_uri
AS -->> Wallet: Redirect to wallet with authorzation code
Wallet ->> AS: POST token request with code and code verifier
AS -->> Wallet: Respond with access_token and c_nonce
loop Fetch offered credentials
Wallet ->> Wallet: generate DID proof
Wallet ->> Issuer: Fetch credentials from credential endpoint or batch-credential endpoint, with DID proof
Issuer -->> Wallet: Credential (and updated c_nonce)
end

About

Kotlin multiplatform library implementing the data models and protocols of the OID4VC specifications, including OID4VCI, OID4VP and SIOPv2.

License:Apache License 2.0


Languages

Language:Kotlin 100.0%