-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(agent): define the OAS for CredentialIssuerEndpoints
Signed-off-by: Yurii Shynbuiev <yurii.shynbuiev@iohk.io>
- Loading branch information
1 parent
da7bc75
commit c35103b
Showing
7 changed files
with
526 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
89 changes: 89 additions & 0 deletions
89
...gent/service/server/src/main/scala/io/iohk/atala/iam/oidc/CredentialIssuerEndpoints.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) | ||
|
||
} |
37 changes: 37 additions & 0 deletions
37
...ervice/server/src/main/scala/io/iohk/atala/iam/oidc/CredentialIssuerServerEndpoints.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
99 changes: 99 additions & 0 deletions
99
...agent/service/server/src/main/scala/io/iohk/atala/iam/oidc/http/AuthorizationErrors.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
|
||
} |
Oops, something went wrong.