Skip to content

Commit

Permalink
feat(agent): add IssuanceSession and NonceService
Browse files Browse the repository at this point in the history
Signed-off-by: Yurii Shynbuiev <yurii.shynbuiev@iohk.io>
  • Loading branch information
yshyn-iohk authored and patlo-iog committed Apr 29, 2024
1 parent 19c27d0 commit b1d48c6
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ import io.iohk.atala.iam.authorization.DefaultPermissionManagementService
import io.iohk.atala.iam.authorization.core.EntityPermissionManagementService
import io.iohk.atala.iam.entity.http.controller.{EntityController, EntityControllerImpl}
import io.iohk.atala.iam.oidc.controller.CredentialIssuerControllerImpl
import io.iohk.atala.iam.oidc.domain.{OIDCCredentialIssuerService, OIDCCredentialIssuerServiceImpl}
import io.iohk.atala.iam.oidc.service.{OIDCCredentialIssuerService, OIDCCredentialIssuerServiceImpl}
import io.iohk.atala.iam.oidc.storage.InMemoryIssuanceSessionService
import io.iohk.atala.iam.wallet.http.controller.WalletManagementControllerImpl
import io.iohk.atala.issue.controller.IssueControllerImpl
import io.iohk.atala.mercury.*
Expand All @@ -79,12 +80,12 @@ import org.hyperledger.identus.system.controller.SystemControllerImpl
import org.hyperledger.identus.verification.controller.VcVerificationControllerImpl
import io.micrometer.prometheus.{PrometheusConfig, PrometheusMeterRegistry}
import zio.*
import zio.metrics.connectors.micrometer
import zio.metrics.connectors.micrometer.MicrometerConfig
import zio.metrics.jvm.DefaultJvmMetrics
import zio.logging.*
import zio.logging.LogFormat.*
import zio.logging.backend.SLF4J
import zio.metrics.connectors.micrometer
import zio.metrics.connectors.micrometer.MicrometerConfig
import zio.metrics.jvm.DefaultJvmMetrics

import java.security.Security

Expand Down Expand Up @@ -221,6 +222,7 @@ object MainApp extends ZIOAppDefault {
RepoModule.polluxContextAwareTransactorLayer ++ RepoModule.polluxTransactorLayer >>> JdbcPresentationRepository.layer,
RepoModule.polluxContextAwareTransactorLayer >>> JdbcVerificationPolicyRepository.layer,
// oidc
InMemoryIssuanceSessionService.layer,
DIDServiceImpl.layer ++ OIDCCredentialIssuerServiceImpl.layer >>> CredentialIssuerControllerImpl.layer,
// event notification service
ZLayer.succeed(500) >>> EventNotificationServiceImpl.layer,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
package io.iohk.atala.iam.oidc

import io.iohk.atala.api.http.{ErrorResponse, RequestContext, EndpointOutputs}
import io.iohk.atala.api.http.{EndpointOutputs, ErrorResponse, RequestContext}
import io.iohk.atala.castor.controller.http.DIDInput
import io.iohk.atala.castor.controller.http.DIDInput.didRefPathSegment
import io.iohk.atala.iam.authentication.apikey.ApiKeyCredentials
import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader
import io.iohk.atala.iam.authentication.oidc.JwtCredentials
import io.iohk.atala.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader
import io.iohk.atala.iam.oidc.http.{
CredentialErrorResponse,
CredentialRequest,
CredentialResponse,
NonceResponse,
CredentialOfferRequest,
CredentialOfferResponse,
NonceRequest
}
import io.iohk.atala.iam.oidc.http.*
import sttp.apispec.Tag
import sttp.model.StatusCode
import sttp.tapir.json.zio.jsonBody
Expand Down Expand Up @@ -120,4 +112,23 @@ object CredentialIssuerEndpoints {
"""The endpoint that returns a `nonce` value for the [Token Endpoint](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-nonce-endpoint)""".stripMargin
)

val issuanceSessionEndpoint: Endpoint[
(ApiKeyCredentials, JwtCredentials),
(RequestContext, String, IssuanceSessionRequest),
ExtendedErrorResponse,
Unit,
Any
] =
baseIssuerFacingEndpoint.post
.in("issuance-session")
.in(jsonBody[IssuanceSessionRequest])
.out(
statusCode(StatusCode.Created).description("Issuance session created successfully"),
)
.errorOut(credentialEndpointErrorOutput)
.name("createIssuanceSession")
.summary("Create Issuance Session")
.description(
"""The endpoint that creates an issuance session for the OIDC VC endpoints""".stripMargin
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import io.iohk.atala.agent.walletapi.model.BaseEntity
import io.iohk.atala.api.http.{ErrorResponse, RequestContext}
import io.iohk.atala.iam.authentication.{Authenticator, Authorizer, DefaultAuthenticator, SecurityLogic}
import io.iohk.atala.iam.oidc.controller.CredentialIssuerController
import io.iohk.atala.iam.oidc.http.{CredentialErrorResponse, CredentialRequest, NonceResponse}
import io.iohk.atala.iam.oidc.http.{CredentialErrorResponse, CredentialRequest, IssuanceSessionRequest, NonceResponse}
import sttp.tapir.ztapir.*
import zio.*

Expand Down Expand Up @@ -57,8 +57,26 @@ case class CredentialIssuerServerEndpoints(
}
}

val all: List[ZServerEndpoint[Any, Any]] =
List(credentialServerEndpoint, createCredentialOfferServerEndpoint, nonceServerEndpoint)
val issuanceSessionServerEndpoint: ZServerEndpoint[Any, Any] =
CredentialIssuerEndpoints.issuanceSessionEndpoint
.zServerSecurityLogic(
// FIXME: how can authorization server authorize itself?
SecurityLogic
.authorizeWalletAccessWith(_)(authenticator, authorizer)
.mapError(Left[ErrorResponse, CredentialErrorResponse])
)
.serverLogic { wac =>
{ case (ctx: RequestContext, didRef: String, issuanceSessionRequest: IssuanceSessionRequest) =>
credentialIssuerController.createIssuanceSession(ctx, didRef, issuanceSessionRequest)
}
}

val all: List[ZServerEndpoint[Any, Any]] = List(
credentialServerEndpoint,
nonceServerEndpoint,
createCredentialOfferServerEndpoint,
issuanceSessionServerEndpoint
)
}

object CredentialIssuerServerEndpoints {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package io.iohk.atala.iam.oidc.controller

import io.iohk.atala.api.http.ErrorResponse
import io.iohk.atala.api.http.ErrorResponse.internalServerError
import io.iohk.atala.api.http.RequestContext
import io.iohk.atala.api.http.{ErrorResponse, RequestContext}
import io.iohk.atala.castor.core.model.did.{CanonicalPrismDID, PrismDID}
import io.iohk.atala.castor.core.service.DIDService
import io.iohk.atala.iam.oidc.CredentialIssuerEndpoints.ExtendedErrorResponse
import io.iohk.atala.iam.oidc.domain.OIDCCredentialIssuerService
import io.iohk.atala.iam.oidc.domain.IssuanceSession
import io.iohk.atala.iam.oidc.http.*
import io.iohk.atala.iam.oidc.http.CredentialErrorCode.*
import io.iohk.atala.iam.oidc.service.OIDCCredentialIssuerService
import io.iohk.atala.shared.models.WalletAccessContext
import zio.{IO, URLayer, ZIO, ZLayer}

Expand All @@ -28,6 +28,12 @@ trait CredentialIssuerController {
didRef: String,
request: NonceRequest
): ZIO[WalletAccessContext, ErrorResponse, NonceResponse]

def createIssuanceSession(
ctx: RequestContext,
didRef: String,
issuanceSessionRequest: IssuanceSessionRequest
): IO[ExtendedErrorResponse, Unit]
}

object CredentialIssuerController {
Expand Down Expand Up @@ -191,6 +197,35 @@ case class CredentialIssuerControllerImpl(didService: DIDService, credentialIssu
)
)
}

private def buildIssuanceSession(
canonicalPrismDID: CanonicalPrismDID,
issuanceSessionRequest: IssuanceSessionRequest
): IssuanceSession = {
IssuanceSession(
nonce = issuanceSessionRequest.nonce,
issuableCredentials = issuanceSessionRequest.issuableCredentials,
isPreAuthorized = issuanceSessionRequest.isPreAuthorized,
did = issuanceSessionRequest.did,
issuerDid = canonicalPrismDID,
userPin = issuanceSessionRequest.userPin
)
}

override def createIssuanceSession(
ctx: RequestContext,
didRef: String,
issuanceSessionRequest: IssuanceSessionRequest
): IO[ExtendedErrorResponse, Unit] = {
for {
canonicalPrismDID <- resolveIssuerDID(didRef)
issuanceSession = buildIssuanceSession(canonicalPrismDID, issuanceSessionRequest)
_ <- credentialIssuerService
.createIssuanceSession(issuanceSession)
.mapError(ue => serverError(Some(s"Unexpected error while creating issuance session: ${ue.message}")))
} yield ()

}
}

object CredentialIssuerControllerImpl {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.iohk.atala.iam.oidc.domain

import io.iohk.atala.castor.core.model.did.CanonicalPrismDID
import io.iohk.atala.iam.oidc.http.IssuableCredential

case class IssuanceSession(
nonce: String,
issuableCredentials: Seq[IssuableCredential],
isPreAuthorized: Boolean,
did: Option[String],
issuerDid: CanonicalPrismDID,
userPin: Option[String]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.iohk.atala.iam.oidc.http

import sttp.tapir.Schema
import sttp.tapir.json.zio.schemaForZioJsonValue
import zio.json.ast.Json
import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder}

case class IssuanceSessionRequest(
nonce: String,
issuableCredentials: Seq[IssuableCredential],
isPreAuthorized: Boolean,
did: Option[String],
issuerDid: Option[String],
userPin: Option[String]
)

object IssuanceSessionRequest {
given schema: Schema[IssuanceSessionRequest] = Schema.derived
given encoder: JsonEncoder[IssuanceSessionRequest] = DeriveJsonEncoder.gen
given decoder: JsonDecoder[IssuanceSessionRequest] = DeriveJsonDecoder.gen
}

case class IssuableCredential(`type`: String, claims: Json)

object IssuableCredential {
given schema: Schema[IssuableCredential] = Schema.derived
given encoder: JsonEncoder[IssuableCredential] = DeriveJsonEncoder.gen
given decoder: JsonDecoder[IssuableCredential] = DeriveJsonDecoder.gen
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.iohk.atala.iam.oidc.service

import io.iohk.atala.iam.oidc.service.NonceService.NonceGenerator
import zio.Task

import java.time.Instant
import scala.collection.concurrent.TrieMap

trait NonceService {
def generateNonce()(implicit gen: NonceGenerator): String = gen()
def validateNonce(nonce: String): Task[Boolean]
def storeNonce(nonce: String, expireAt: Long): Task[Unit]
}

object NonceService {
type NonceGenerator = () => String
given randomUUID: NonceGenerator = () => java.util.UUID.randomUUID().toString
}

case class InMemoryNonceService() extends NonceService {
import zio.{Task, ZIO}
private case class NonceRecord(nonce: String, expireAt: Long, fired: Boolean = false)

private val nonces: TrieMap[String, NonceRecord] = TrieMap.empty

override def validateNonce(nonce: String): Task[Boolean] = {
nonces.get(nonce) match {
case None =>
ZIO.succeed(false)
case Some(n) if !n.fired && n.expireAt > Instant.now().toEpochMilli =>
nonces.replace(nonce, n, n.copy(fired = true))
ZIO.succeed(true)
}
}

override def storeNonce(nonce: String, expireAt: Long): Task[Unit] = {
nonces.putIfAbsent(nonce, NonceRecord(nonce, expireAt)) match {
case Some(_) => ZIO.fail(new RuntimeException(s"Nonce $nonce already exists"))
case None => ZIO.succeed(())
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package io.iohk.atala.iam.oidc.domain
package io.iohk.atala.iam.oidc.service

import io.circe.Json
import io.iohk.atala.agent.walletapi.storage.DIDNonSecretStorage
import io.iohk.atala.castor.core.model.did.{PrismDID, VerificationRelationship}
import io.iohk.atala.castor.core.service.DIDService
import io.iohk.atala.iam.oidc.http.CredentialOffer
import io.iohk.atala.iam.oidc.http.CredentialOfferAuthorizationGrant
import io.iohk.atala.iam.oidc.http.CredentialOfferGrant
import io.iohk.atala.iam.oidc.http.{CredentialDefinition, CredentialSubject}
import io.iohk.atala.iam.oidc.domain.IssuanceSession
import io.iohk.atala.iam.oidc.http.*
import io.iohk.atala.iam.oidc.storage.IssuanceSessionStorage
import io.iohk.atala.pollux.core.service.CredentialService
import io.iohk.atala.pollux.vc.jwt.{DID, Issuer, JWT, JwtCredential, W3cCredentialPayload}
import io.iohk.atala.shared.models.{WalletAccessContext, WalletId}
Expand Down Expand Up @@ -42,6 +41,8 @@ trait OIDCCredentialIssuerService {
): ZIO[WalletAccessContext, Error, CredentialOffer]

def getIssuanceSessionNonce(issuerState: String): ZIO[WalletAccessContext, Error, UUID]

def createIssuanceSession(issuanceSession: IssuanceSession): IO[Error, IssuanceSession]
}

object OIDCCredentialIssuerService {
Expand All @@ -65,7 +66,8 @@ object OIDCCredentialIssuerService {
case class OIDCCredentialIssuerServiceImpl(
didService: DIDService,
didNonSecretStorage: DIDNonSecretStorage,
credentialService: CredentialService
credentialService: CredentialService,
issuanceSessionStorage: IssuanceSessionStorage
) extends OIDCCredentialIssuerService {

import OIDCCredentialIssuerService.Error
Expand Down Expand Up @@ -98,7 +100,7 @@ case class OIDCCredentialIssuerServiceImpl(
.provideSomeLayer(ZLayer.succeed(wac))

jwtVC <- buildJwtVerifiableCredential(jwtIssuer.did, credentialIdentifier, credentialDefinition)
jwt <- issueJwtVS(jwtIssuer, jwtVC)
jwt <- issueJwtVC(jwtIssuer, jwtVC)
} yield jwt
}

Expand Down Expand Up @@ -139,12 +141,18 @@ case class OIDCCredentialIssuerServiceImpl(
claimsOpt.fold(Json.obj())(claims => Json.obj(claims: _*))
}

def issueJwtVS(issuer: Issuer, payload: W3cCredentialPayload): IO[Error, JWT] = {
def issueJwtVC(issuer: Issuer, payload: W3cCredentialPayload): IO[Error, JWT] = {
ZIO
.fromTry(Try(JwtCredential.encodeJwt(payload.toJwtCredentialPayload, issuer)))
.mapError(e => ServiceError(s"Failed to issue JWT: ${e.getMessage}"))
}

override def createIssuanceSession(issuanceSession: IssuanceSession): IO[Error, IssuanceSession] = {
issuanceSessionStorage
.start(issuanceSession)
.mapError(e => ServiceError(s"Failed to start issuance session: ${e.message}"))
}

override def getIssuanceSessionNonce(
issuerState: String
): ZIO[WalletAccessContext, OIDCCredentialIssuerService.Error, UUID] =
Expand All @@ -170,6 +178,9 @@ case class OIDCCredentialIssuerServiceImpl(
}

object OIDCCredentialIssuerServiceImpl {
val layer: URLayer[DIDService & DIDNonSecretStorage & CredentialService, OIDCCredentialIssuerService] =
ZLayer.fromFunction(OIDCCredentialIssuerServiceImpl(_, _, _))
val layer: URLayer[
DIDService & DIDNonSecretStorage & CredentialService & IssuanceSessionStorage,
OIDCCredentialIssuerService
] =
ZLayer.fromFunction(OIDCCredentialIssuerServiceImpl(_, _, _, _))
}

0 comments on commit b1d48c6

Please sign in to comment.