Skip to content

Commit

Permalink
start pulling oidc-lib into walletkit
Browse files Browse the repository at this point in the history
  • Loading branch information
severinstampler committed Sep 13, 2023
1 parent dfc639c commit 16428d7
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 188 deletions.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ dependencies {
implementation("com.beust:klaxon:5.6")
implementation("com.nimbusds:oauth2-oidc-sdk:10.11")

implementation("id.walt:waltid-openid4vc:1.2309111544.0")

// CLI
implementation("com.github.ajalt.clikt:clikt-jvm:4.2.0")
implementation("com.github.ajalt.clikt:clikt:4.2.0")
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/id/walt/issuer/backend/IssuanceSession.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package id.walt.issuer.backend
import com.nimbusds.oauth2.sdk.AuthorizationRequest
import id.walt.model.oidc.CredentialAuthorizationDetails

data class IssuanceSession(
data class IssuanceSession_(
val id: String,
val credentialDetails: List<CredentialAuthorizationDetails>,
val nonce: String,
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/id/walt/issuer/backend/IssuerController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ object IssuerController {
if (sessionId == null)
ctx.json(IssuerManager.listIssuableCredentials())
else
ctx.json(IssuerManager.getIssuanceSession(sessionId)?.issuables ?: Issuables(credentials = listOf()))
ctx.json(IssuerManager.getSession(sessionId)?.credentialOffer?.credentials ?: Issuables(credentials = listOf()))
}

fun requestIssuance(ctx: Context) {
Expand Down
253 changes: 68 additions & 185 deletions src/main/kotlin/id/walt/issuer/backend/IssuerManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,24 @@ import com.nimbusds.oauth2.sdk.PreAuthorizedCodeGrant
import com.nimbusds.oauth2.sdk.id.Issuer
import com.nimbusds.openid.connect.sdk.SubjectType
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
import id.walt.credentials.w3c.W3CIssuer
import id.walt.credentials.w3c.templates.VcTemplateManager
import id.walt.crypto.LdSignatureType
import id.walt.model.DidMethod
import id.walt.model.DidUrl
import id.walt.model.oidc.*
import id.walt.multitenancy.TenantContext
import id.walt.multitenancy.TenantContextManager
import id.walt.multitenancy.TenantId
import id.walt.multitenancy.TenantType
import id.walt.multitenancy.*
import id.walt.oid4vc.data.CredentialFormat
import id.walt.oid4vc.data.CredentialSupported
import id.walt.oid4vc.definitions.JWTClaims
import id.walt.oid4vc.errors.CredentialError
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.CredentialRequest
import id.walt.oid4vc.responses.CredentialErrorCode
import id.walt.services.did.DidService
import id.walt.services.jwt.JwtService
import id.walt.signatory.ProofConfig
Expand All @@ -26,6 +35,10 @@ import id.walt.signatory.dataproviders.MergingDataProvider
import id.walt.verifier.backend.WalletConfiguration
import io.github.pavleprica.kotlin.cache.time.based.shortTimeBasedCache
import io.javalin.http.BadRequestResponse
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonPrimitive
import mu.KotlinLogging
import java.net.URI
import java.time.Instant
Expand All @@ -37,7 +50,10 @@ fun isSchema(typeOrSchema: String): Boolean {
return Regex(URL_PATTERN).matches(typeOrSchema)
}

object IssuerManager {
object IssuerManager: OpenIDCredentialIssuer(
IssuerTenant.config.issuerApiUrl + "/oidc/",
CredentialIssuerConfig(IssuerTenant.config.credentialTypes?.map { CredentialSupported(id.walt.oid4vc.data.CredentialFormat.jwt_vc_json, it) } ?: listOf())
) {
val log = KotlinLogging.logger { }
val defaultDid: String
get() = IssuerTenant.config.issuerDid
Expand Down Expand Up @@ -82,204 +98,71 @@ object IssuerManager {
return IssuerTenant.state.nonceCache.asMap().keys
}

fun newIssuanceInitiationRequest(
selectedIssuables: Issuables,
preAuthorized: Boolean,
userPin: String? = null,
issuerDid: String? = null
): IssuanceInitiationRequest {
val issuerUri = URI.create("${IssuerTenant.config.issuerApiUrl}/oidc/")
val session = initializeIssuanceSession(
credentialDetails = selectedIssuables.credentials.map { issuable ->
CredentialAuthorizationDetails(issuable.type)
},
preAuthorized = preAuthorized,
authRequest = null,
userPin = userPin,
issuerDid = issuerDid
)
updateIssuanceSession(session, selectedIssuables, issuerDid)
private inline fun <T, R> Iterable<T>.allUniqueBy(transform: (T) -> R) =
HashSet<R>().let { hs ->
all { hs.add(transform(it)) }
}

return IssuanceInitiationRequest(
issuer_url = issuerUri.toString(),
credential_types = selectedIssuables.credentials.map { it.type },
pre_authorized_code = if (preAuthorized) generateAuthorizationCodeFor(session) else null,
user_pin_required = userPin != null,
op_state = if (!preAuthorized) session.id else null
)
}

fun initializeIssuanceSession(
credentialDetails: List<CredentialAuthorizationDetails>,
preAuthorized: Boolean,
authRequest: AuthorizationRequest?,
userPin: String? = null,
issuerDid: String? = null
): IssuanceSession {
val id = UUID.randomUUID().toString()
//TODO: validata/verify PAR request, claims, etc
val session = IssuanceSession(
id,
credentialDetails,
UUID.randomUUID().toString(),
isPreAuthorized = preAuthorized,
authRequest,
Issuables.fromCredentialAuthorizationDetails(credentialDetails),
userPin = userPin,
issuerDid = issuerDid
fun getXDeviceWallet(): WalletConfiguration {
return WalletConfiguration(
id = "x-device",
url = "openid-initiate-issuance://",
presentPath = "",
receivePath = "",
description = "cross device"
)
IssuerTenant.state.sessionCache.put(id, session)
return session
}

fun getIssuanceSession(id: String): IssuanceSession? {
return IssuerTenant.state.sessionCache.getIfPresent(id)
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(defaultDid, 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) }
}

fun updateIssuanceSession(session: IssuanceSession, issuables: Issuables?, issuerDid: String? = null) {
session.issuables = issuables
issuerDid?.let { session.issuerDid = issuerDid }
IssuerTenant.state.sessionCache.put(session.id, session)
private fun resolveDIDFor(keyId: String): String {
return DidUrl.from(keyId).did
}

fun generateAuthorizationCodeFor(session: IssuanceSession): String {
return IssuerTenant.state.authCodeProvider.generateToken(session)
override fun generateCredential(credentialRequest: CredentialRequest): CredentialResult {
return doGenerateCredential(credentialRequest)
}

fun validateAuthorizationCode(code: String): String {
return IssuerTenant.state.authCodeProvider.validateToken(code).map { it.subject }
.orElseThrow { BadRequestResponse("Invalid authorization code given") }
override fun getDeferredCredential(credentialID: String): CredentialResult {
TODO("Not yet implemented")
}

private inline fun <T, R> Iterable<T>.allUniqueBy(transform: (T) -> R) =
HashSet<R>().let { hs ->
all { hs.add(transform(it)) }
}

/**
* For multipleCredentialsOfSameType in session.issuables
*/
private val sessionAccessCounterCache = shortTimeBasedCache<String, HashMap<String, Int>>()
@OptIn(ExperimentalJsExport::class)
fun fulfillIssuanceSession(session: IssuanceSession, credentialRequest: CredentialRequest): String? {
log.debug { "fulfillIssuanceSession for request: $credentialRequest" }
val proof = credentialRequest.proof ?: throw BadRequestResponse("No proof given")
val parsedJwt = SignedJWT.parse(proof.jwt)
if (parsedJwt.header.keyID?.let { DidUrl.isDidUrl(it) } == false) throw BadRequestResponse("Proof is not DID signed")

if (!JwtService.getService().verify(proof.jwt).verified) throw BadRequestResponse("Proof invalid")

val did = DidUrl.from(parsedJwt.header.keyID).did
val now = Instant.now()
val issuables = session.issuables ?: throw BadRequestResponse("No issuables")

log.debug { "Issuance session ${session.id}: Session issuables: ${session.issuables}" }

val sessionLongId = "${session.id}${session.nonce}"


val multipleCredentialsOfSameType = !issuables.credentials.allUniqueBy { it.type }

if (multipleCredentialsOfSameType && sessionAccessCounterCache[sessionLongId].isEmpty) {
log.debug { "Issuance session ${session.id}: Setup multipleCredentialsOfSameType" }
sessionAccessCounterCache[sessionLongId] = HashMap()
}

val requestedType = credentialRequest.type
val credentialsOfRequestedType = issuables.credentials.filter { it.type == requestedType }

val credential = when {
!multipleCredentialsOfSameType -> credentialsOfRequestedType.firstOrNull()
else -> {
val accessCounter = sessionAccessCounterCache[sessionLongId].get()

if (!accessCounter.contains(requestedType))
accessCounter[requestedType] = -1

accessCounter[requestedType] = accessCounter[requestedType]!! + 1

log.info {
"Issuance session ${session.id}: multipleCredentialsOfSameType " +
"request ${accessCounter[requestedType]!! + 1}/${issuables.credentials.size}"
}
override fun getSession(id: String): IssuanceSession? {
return IssuerTenant.state.sessionCache.getIfPresent(id)
}

credentialsOfRequestedType.getOrElse(accessCounter[requestedType]!!) { credentialsOfRequestedType.lastOrNull() }
}
}
override fun putSession(id: String, session: IssuanceSession): IssuanceSession? {
IssuerTenant.state.sessionCache.put(id, session)
return session
}

return credential?.let {
Signatory.getService().issue(it.type,
ProofConfig(
issuerDid = session.issuerDid ?: defaultDid,
proofType = when (credentialRequest.format) {
"jwt_vc" -> ProofType.JWT
else -> ProofType.LD_PROOF
},
subjectDid = did,
issueDate = now,
validDate = now
),
dataProvider = it.credentialData?.let { cd -> MergingDataProvider(cd) })
}
override fun removeSession(id: String): IssuanceSession? {
val prevVal = IssuerTenant.state.sessionCache.getIfPresent(id)
IssuerTenant.state.sessionCache.invalidate(id)
return prevVal
}

fun getXDeviceWallet(): WalletConfiguration {
return WalletConfiguration(
id = "x-device",
url = "openid-initiate-issuance://",
presentPath = "",
receivePath = "",
description = "cross device"
)
override fun signToken(target: TokenTarget, payload: JsonObject, header: JsonObject?, keyId: String?): String {
return JwtService.getService().sign(keyId!!, payload.toString())
}

fun getOidcProviderMetadata() = OIDCProviderMetadata(
Issuer(IssuerTenant.config.issuerApiUrl),
listOf(SubjectType.PUBLIC),
URI("${IssuerTenant.config.issuerApiUrl}/oidc")
).apply {
authorizationEndpointURI = URI("${IssuerTenant.config.issuerApiUrl}/oidc/fulfillPAR")
pushedAuthorizationRequestEndpointURI = URI("${IssuerTenant.config.issuerApiUrl}/oidc/par")
tokenEndpointURI = URI("${IssuerTenant.config.issuerApiUrl}/oidc/token")
grantTypes = listOf(GrantType.AUTHORIZATION_CODE, PreAuthorizedCodeGrant.GRANT_TYPE)
setCustomParameter("credential_endpoint", "${IssuerTenant.config.issuerApiUrl}/oidc/credential")
setCustomParameter(
"credential_issuer", CredentialIssuer(
listOf(
CredentialIssuerDisplay(IssuerTenant.config.issuerApiUrl)
)
)
)
setCustomParameter(
"credentials_supported",
listIssuableCredentials().credentials.map { VcTemplateManager.getTemplate(it.type, true) }
.associateBy({ tmpl -> tmpl.name }) { cred ->
CredentialMetadata(
formats = mapOf(
"ldp_vc" to CredentialFormat(
types = cred.template!!.type,
cryptographic_binding_methods_supported = listOf("did"),
cryptographic_suites_supported = LdSignatureType.values().map { it.name }
),
"jwt_vc" to CredentialFormat(
types = cred.template!!.type,
cryptographic_binding_methods_supported = listOf("did"),
cryptographic_suites_supported = listOf(
JWSAlgorithm.ES256,
JWSAlgorithm.ES256K,
JWSAlgorithm.EdDSA,
JWSAlgorithm.RS256,
JWSAlgorithm.PS256
).map { it.name }
)
),
display = listOf(
CredentialDisplay(
name = cred.name
)
)
)
}
)
override fun verifyTokenSignature(target: TokenTarget, token: String): Boolean {
return JwtService.getService().verify(token).verified
}
}
3 changes: 2 additions & 1 deletion src/main/kotlin/id/walt/issuer/backend/IssuerState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.google.common.cache.CacheBuilder
import id.walt.multitenancy.TenantState
import id.walt.oid4vc.providers.IssuanceSession
import javalinjwt.JWTProvider
import java.time.Duration
import java.util.*
Expand All @@ -21,7 +22,7 @@ class IssuerState : TenantState<IssuerConfig> {
val authCodeProvider = JWTProvider(
algorithm,
{ session: IssuanceSession, alg: Algorithm? ->
JWT.create().withSubject(session.id).withClaim("pre-authorized", session.isPreAuthorized).sign(alg)
JWT.create().withSubject(session.id).withClaim("pre-authorized", session.preAuthUserPin != null).sign(alg)
},
JWT.require(algorithm).build()
)
Expand Down

0 comments on commit 16428d7

Please sign in to comment.