Skip to content

Commit

Permalink
feat(agent): define the OAS for CredentialIssuerEndpoints
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 30, 2024
1 parent da7bc75 commit c35103b
Show file tree
Hide file tree
Showing 7 changed files with 526 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,32 @@ import org.hyperledger.identus.resolvers.DIDResolver
import org.hyperledger.identus.shared.utils.DurationOps.toMetricsSeconds
import org.hyperledger.identus.system.controller.SystemServerEndpoints
import org.hyperledger.identus.verification.controller.VcVerificationServerEndpoints
import io.iohk.atala.agent.walletapi.model.{Entity, Wallet, WalletSeed}
import io.iohk.atala.agent.walletapi.service.{EntityService, ManagedDIDService, WalletManagementService}
import io.iohk.atala.agent.walletapi.storage.DIDNonSecretStorage
import io.iohk.atala.castor.controller.{DIDRegistrarServerEndpoints, DIDServerEndpoints}
import io.iohk.atala.castor.core.service.DIDService
import io.iohk.atala.connect.controller.ConnectionServerEndpoints
import io.iohk.atala.connect.core.service.ConnectionService
import io.iohk.atala.credentialstatus.controller.CredentialStatusServiceEndpoints
import io.iohk.atala.event.controller.EventServerEndpoints
import io.iohk.atala.event.notification.EventNotificationConfig
import io.iohk.atala.iam.authentication.apikey.ApiKeyAuthenticator
import io.iohk.atala.iam.entity.http.EntityServerEndpoints
import io.iohk.atala.iam.oidc.CredentialIssuerServerEndpoints
import io.iohk.atala.iam.wallet.http.WalletManagementServerEndpoints
import io.iohk.atala.issue.controller.IssueServerEndpoints
import io.iohk.atala.mercury.{DidOps, HttpClient}
import io.iohk.atala.pollux.core.service.{CredentialService, PresentationService}
import io.iohk.atala.pollux.credentialdefinition.CredentialDefinitionRegistryServerEndpoints
import io.iohk.atala.pollux.credentialschema.{SchemaRegistryServerEndpoints, VerificationPolicyServerEndpoints}
import io.iohk.atala.pollux.vc.jwt.DidResolver as JwtDidResolver
import io.iohk.atala.presentproof.controller.PresentProofServerEndpoints
import io.iohk.atala.resolvers.DIDResolver
import io.iohk.atala.shared.models.WalletAdministrationContext
import io.iohk.atala.shared.models.{HexString, WalletAccessContext, WalletId}
import io.iohk.atala.shared.utils.DurationOps.toMetricsSeconds
import io.iohk.atala.system.controller.SystemServerEndpoints
import zio.*
import zio.metrics.*

Expand Down Expand Up @@ -135,6 +161,7 @@ object AgentHttpServer {
allEntityEndpoints <- EntityServerEndpoints.all
allWalletManagementEndpoints <- WalletManagementServerEndpoints.all
allEventEndpoints <- EventServerEndpoints.all
allOIDCEndpoints <- CredentialIssuerServerEndpoints.all
} yield allCredentialDefinitionRegistryEndpoints ++
allSchemaRegistryEndpoints ++
allVerificationPolicyEndpoints ++
Expand All @@ -148,7 +175,8 @@ object AgentHttpServer {
allSystemEndpoints ++
allEntityEndpoints ++
allWalletManagementEndpoints ++
allEventEndpoints
allEventEndpoints ++
allOIDCEndpoints
def run =
for {
allEndpoints <- agentRESTServiceEndpoints
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import sttp.model.StatusCode
import sttp.tapir.json.zio.jsonBody
import sttp.tapir.{oneOfVariantValueMatcher, *}
import sttp.tapir.EndpointOutput.OneOfVariant
import zio.json.JsonEncoder

object EndpointOutputs {
private def statusCodeMatcher(
def statusCodeMatcher(
statusCode: StatusCode
): PartialFunction[Any, Boolean] = {
case ErrorResponse(status, _, _, _, _) if status == statusCode.code => true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.iohk.atala.iam.oidc

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,
DeferredCredentialResponse,
ImmediateCredentialResponse,
JwtCredentialRequest
}
import sttp.apispec.Tag
import sttp.tapir.{
Endpoint,
EndpointInput,
endpoint,
extractFromRequest,
model,
oneOf,
path,
query,
statusCode,
stringToPath,
oneOfVariantValueMatcher
}
import sttp.model.StatusCode
import sttp.tapir.json.zio.jsonBody

object CredentialIssuerEndpoints {

private val tagName = "OIDC Credential Issuer"
private val tagDescription =
s"""
|The __${tagName}__ is a service that issues credentials to users by implementing the [OIDC for Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html) specification.
|It exposes the following endpoints:
|- Credential Endpoint
|- Credential Issuer Metadata Endpoint
|- Credential Offer Endpoint
|""".stripMargin

val tag = Tag(tagName, Some(tagDescription))

private val baseEndpoint = endpoint
.tag(tagName)
.securityIn(jwtAuthHeader)
.in(extractFromRequest[RequestContext](RequestContext.apply))
.in("oidc" / didRefPathSegment / "credential-issuer")

val credentialEndpointErrorOutput = oneOf[Either[ErrorResponse, CredentialErrorResponse]](
oneOfVariantValueMatcher(StatusCode.BadRequest, jsonBody[Either[ErrorResponse, CredentialErrorResponse]]) {
case Right(CredentialErrorResponse(code, _, _, _)) if code.toHttpStatusCode == StatusCode.BadRequest => true
},
oneOfVariantValueMatcher(StatusCode.Unauthorized, jsonBody[Either[ErrorResponse, CredentialErrorResponse]]) {
case Right(CredentialErrorResponse(code, _, _, _)) if code.toHttpStatusCode == StatusCode.Unauthorized => true
},
oneOfVariantValueMatcher(StatusCode.Forbidden, jsonBody[Either[ErrorResponse, CredentialErrorResponse]]) {
case Right(CredentialErrorResponse(code, _, _, _)) if code.toHttpStatusCode == StatusCode.Forbidden => true
},
oneOfVariantValueMatcher(StatusCode.InternalServerError, jsonBody[Either[ErrorResponse, CredentialErrorResponse]]) {
case Left(ErrorResponse(status, _, _, _, _)) if status == StatusCode.InternalServerError.code => true
}
)

val credentialEndpoint: Endpoint[
JwtCredentials,
(RequestContext, String, CredentialRequest),
Either[ErrorResponse, CredentialErrorResponse],
CredentialResponse,
Any
] = baseEndpoint.post
.in(jsonBody[CredentialRequest])
.out(
statusCode(StatusCode.Ok).description("Credential issued successfully"),
)
.out(jsonBody[CredentialResponse])
.errorOut(credentialEndpointErrorOutput)
.name("issueCredential")
.summary("Credential Endpoint")
.description(
"""OIDC for VC [Credential Endpoint](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-endpoint)""".stripMargin
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.iohk.atala.iam.oidc

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.http.{CredentialErrorResponse, CredentialRequest, ImmediateCredentialResponse}
import sttp.tapir.ztapir.*
import zio.*

class CredentialIssuerServerEndpoints(
authenticator: Authenticator[BaseEntity]
) {

val credentialServersEndpoint: ZServerEndpoint[Any, Any] =
CredentialIssuerEndpoints.credentialEndpoint
.zServerSecurityLogic(
SecurityLogic // TODO: add OIDC client authenticator
.authenticate(_)(authenticator)
.mapError(Left[ErrorResponse, CredentialErrorResponse])
)
.serverLogic { wac =>
{ case (ctx: RequestContext, didRef: String, request: CredentialRequest) =>
ZIO.succeed(ImmediateCredentialResponse("credential"))
}
}

val all: List[ZServerEndpoint[Any, Any]] = List(credentialServersEndpoint)
}

object CredentialIssuerServerEndpoints {
def all: URIO[DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = {
for {
authenticator <- ZIO.service[DefaultAuthenticator]
oidcEndpoints = new CredentialIssuerServerEndpoints(authenticator)
} yield oidcEndpoints.all
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package io.iohk.atala.iam.oidc.http

import io.iohk.atala.api.http.EndpointOutputs.statusCodeMatcher
import io.iohk.atala.api.http.ErrorResponse
import sttp.model.StatusCode
import sttp.tapir.Schema.annotations.encodedName
import sttp.tapir.json.zio.jsonBody
import sttp.tapir.{Schema, oneOfVariantValueMatcher}
import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder, jsonField}

// According to OIDC spec and RFC6750, the following errors are expected to be returned by the authorization server
// https://www.rfc-editor.org/rfc/rfc6750.html#section-3.1
object AuthorizationErrors {

val invalidRequest = oneOfVariantValueMatcher(
StatusCode.BadRequest,
jsonBody[CredentialErrorResponse].description(
"""The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats the same parameter, uses more than one method for including an access token, or is otherwise malformed.""".stripMargin
)
)(statusCodeMatcher(StatusCode.BadRequest))

val invalidToken = oneOfVariantValueMatcher(
StatusCode.Unauthorized,
jsonBody[CredentialErrorResponse].description(
"The access token provided is expired, revoked, malformed, or invalid for other reason"
)
)(statusCodeMatcher(StatusCode.Unauthorized))

val insufficientScope = oneOfVariantValueMatcher(
StatusCode.Forbidden,
jsonBody[CredentialErrorResponse].description(
"The request requires higher privileges than provided by the access token"
)
)(statusCodeMatcher(StatusCode.Forbidden))
}

case class CredentialErrorResponse(
error: CredentialErrorCode,
@jsonField("error_description")
@encodedName("error_description")
errorDescription: Option[String] = None,
@jsonField("c_nonce")
@encodedName("c_nonce")
nonce: Option[String] = None,
@jsonField("c_nonce_expires_in")
@encodedName("c_nonce_expires_in")
nonceExpiresIs: Option[Long] = None
)

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

enum CredentialErrorCode {
case invalid_request
case invalid_token
case insufficient_scope
case invalid_credential_request
case unsupported_credential_type
case invalid_proof
case invalid_encryption_parameters
}

object CredentialErrorCode {
given schema: Schema[CredentialErrorCode] = Schema.derivedEnumeration.defaultStringBased
given encoder: JsonEncoder[CredentialErrorCode] = DeriveJsonEncoder.gen
given decoder: JsonDecoder[CredentialErrorCode] = DeriveJsonDecoder.gen

implicit class CredentialErrorCodeOps(val credentialErrorCode: CredentialErrorCode) extends AnyVal {
def toHttpStatusCode: StatusCode = CredentialErrorCode.toHttpStatusCode(credentialErrorCode)
}
val toHttpStatusCode: PartialFunction[CredentialErrorCode, StatusCode] = {
case CredentialErrorCode.invalid_request => StatusCode.BadRequest
case CredentialErrorCode.invalid_token => StatusCode.Unauthorized
case CredentialErrorCode.insufficient_scope => StatusCode.Forbidden
case CredentialErrorCode.invalid_credential_request => StatusCode.BadRequest
case CredentialErrorCode.unsupported_credential_type => StatusCode.BadRequest
case CredentialErrorCode.invalid_proof => StatusCode.BadRequest
case CredentialErrorCode.invalid_encryption_parameters => StatusCode.BadRequest
}
}

object CredentialRequestErrors {
def errorCodeMatcher(
credentialErrorCode: CredentialErrorCode
): PartialFunction[Any, Boolean] = {
case CredentialErrorResponse(code, _, _, _) if code == credentialErrorCode => true
}

val badRequest = oneOfVariantValueMatcher(
StatusCode.BadRequest,
jsonBody[ErrorResponse].description(
"The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats the same parameter, or is otherwise malformed"
)
)(statusCodeMatcher(StatusCode.BadRequest))

}

0 comments on commit c35103b

Please sign in to comment.