Skip to content

Commit

Permalink
feat: add oidc4vci protocol MVP (#1182)
Browse files Browse the repository at this point in the history
Signed-off-by: Pat Losoponkul <[email protected]>
Co-authored-by: Yurii Shynbuiev <[email protected]>
  • Loading branch information
patlo-iog and yshyn-iohk authored Jun 14, 2024
1 parent 9ac6e52 commit 3ae91dc
Show file tree
Hide file tree
Showing 100 changed files with 7,044 additions and 17,289 deletions.
20 changes: 15 additions & 5 deletions .mega-linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,19 @@ DISABLE_LINTERS:
- CPP_CPPLINT # For pollux/lib/anoncreds/src/main/c
- JAVA_CHECKSTYLE # For pollux/lib/anoncreds/src/main/java
- GHERKIN_GHERKIN_LINT
- OPENAPI_SPECTRAL
# For python, disable all except PYTHON_BLACK linter
- PYTHON_PYLINT
- PYTHON_FLAKE8
- PYTHON_ISORT
- PYTHON_BANDIT
- PYTHON_MYPY
- PYTHON_PYRIGHT
- PYTHON_RUFF

DISABLE_ERRORS_LINTERS:
- KOTLIN_KTLINT
- PROTOBUF_PROTOLINT
- OPENAPI_SPECTRAL
- MARKDOWN_MARKDOWN_LINK_CHECK

DISABLE: [COPYPASTE, SPELL, CREDENTIALS]
Expand All @@ -45,10 +53,12 @@ PRE_COMMANDS:
cwd: "workspace"

# Linter customisation
MARKDOWN_MARKDOWN_LINK_CHECK_FILTER_REGEX_EXCLUDE: "CHANGELOG.md"
MARKDOWN_MARKDOWNLINT_FILTER_REGEX_EXCLUDE: "CHANGELOG.md"
MARKDOWN_MARKDOWN_LINK_CHECK_FILTER_REGEX_EXCLUDE: CHANGELOG\.md|DEPENDENCIES\.md
MARKDOWN_MARKDOWNLINT_FILTER_REGEX_EXCLUDE: CHANGELOG\.md|DEPENDENCIES\.md
MARKDOWN_MARKDOWN_TABLE_FORMATTER_FILTER_REGEX_EXCLUDE: CHANGELOG\.md|DEPENDENCIES\.md
SQL_SQL_LINT_ARGUMENTS: -d postgres --ignore-errors=postgres-invalid-alter-option,postgres-invalid-create-option,postgres-invalid-drop-option
YAML_YAMLLINT_FILTER_REGEX_EXCLUDE: "infrastructure/charts/agent/*|cloud-agent/service/api/http/*"
YAML_PRETTIER_FILTER_REGEX_EXCLUDE: "infrastructure/charts/agent/*|cloud-agent/service/api/http/*"
YAML_YAMLLINT_FILTER_REGEX_EXCLUDE: "infrastructure/charts/agent/*|cloud-agent/service/api/http/*|examples/*"
YAML_PRETTIER_FILTER_REGEX_EXCLUDE: "infrastructure/charts/agent/*|cloud-agent/service/api/http/*|examples/*"
YAML_V8R_FILTER_REGEX_EXCLUDE: "infrastructure/charts/agent/*"
JAVASCRIPT_STANDARD_FILTER_REGEX_EXCLUDE: "tests/performance-tests/agent-performance-tests-k6/src/k6chaijs.js"
BASH_SHELLCHECK_FILTER_REGEX_EXCLUDE: "infrastructure/*"
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,7 @@ lazy val cloudAgentServer = project
eventNotification
)
.dependsOn(sharedTest % "test->test")
.dependsOn(polluxCore % "compile->compile;test->test")

// ############################
// #### Release process #####
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import org.hyperledger.identus.iam.entity.http.EntityServerEndpoints
import org.hyperledger.identus.iam.wallet.http.WalletManagementServerEndpoints
import org.hyperledger.identus.issue.controller.IssueServerEndpoints
import org.hyperledger.identus.mercury.{DidOps, HttpClient}
import org.hyperledger.identus.oid4vci.CredentialIssuerServerEndpoints
import org.hyperledger.identus.pollux.core.service.{CredentialService, PresentationService}
import org.hyperledger.identus.pollux.credentialdefinition.CredentialDefinitionRegistryServerEndpoints
import org.hyperledger.identus.pollux.credentialschema.{
Expand Down Expand Up @@ -135,6 +136,7 @@ object AgentHttpServer {
allEntityEndpoints <- EntityServerEndpoints.all
allWalletManagementEndpoints <- WalletManagementServerEndpoints.all
allEventEndpoints <- EventServerEndpoints.all
allOIDCEndpoints <- CredentialIssuerServerEndpoints.all
} yield allCredentialDefinitionRegistryEndpoints ++
allSchemaRegistryEndpoints ++
allVerificationPolicyEndpoints ++
Expand All @@ -148,7 +150,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 @@ -27,14 +27,17 @@ import org.hyperledger.identus.credentialstatus.controller.CredentialStatusContr
import org.hyperledger.identus.didcomm.controller.DIDCommControllerImpl
import org.hyperledger.identus.event.controller.EventControllerImpl
import org.hyperledger.identus.event.notification.EventNotificationServiceImpl
import org.hyperledger.identus.iam.authentication.{DefaultAuthenticator, Oid4vciAuthenticatorFactory}
import org.hyperledger.identus.iam.authentication.apikey.JdbcAuthenticationRepository
import org.hyperledger.identus.iam.authentication.DefaultAuthenticator
import org.hyperledger.identus.iam.authorization.core.EntityPermissionManagementService
import org.hyperledger.identus.iam.authorization.DefaultPermissionManagementService
import org.hyperledger.identus.iam.entity.http.controller.{EntityController, EntityControllerImpl}
import org.hyperledger.identus.iam.wallet.http.controller.WalletManagementControllerImpl
import org.hyperledger.identus.issue.controller.IssueControllerImpl
import org.hyperledger.identus.mercury.*
import org.hyperledger.identus.oid4vci.controller.CredentialIssuerControllerImpl
import org.hyperledger.identus.oid4vci.service.OIDCCredentialIssuerServiceImpl
import org.hyperledger.identus.oid4vci.storage.InMemoryIssuanceSessionService
import org.hyperledger.identus.pollux.core.service.*
import org.hyperledger.identus.pollux.core.service.verification.VcVerificationServiceImpl
import org.hyperledger.identus.pollux.credentialdefinition.controller.CredentialDefinitionControllerImpl
Expand All @@ -48,6 +51,7 @@ import org.hyperledger.identus.pollux.sql.repository.{
JdbcCredentialRepository,
JdbcCredentialSchemaRepository,
JdbcCredentialStatusListRepository,
JdbcOID4VCIIssuerMetadataRepository,
JdbcPresentationRepository,
JdbcVerificationPolicyRepository,
Migrations as PolluxMigrations
Expand Down Expand Up @@ -191,6 +195,7 @@ object MainApp extends ZIOAppDefault {
DefaultAuthenticator.layer,
DefaultPermissionManagementService.layer,
EntityPermissionManagementService.layer,
Oid4vciAuthenticatorFactory.layer,
// grpc
GrpcModule.prismNodeStubLayer,
// storage
Expand All @@ -205,7 +210,13 @@ object MainApp extends ZIOAppDefault {
RepoModule.polluxContextAwareTransactorLayer ++ RepoModule.polluxTransactorLayer >>> JdbcCredentialSchemaRepository.layer,
RepoModule.polluxContextAwareTransactorLayer ++ RepoModule.polluxTransactorLayer >>> JdbcCredentialDefinitionRepository.layer,
RepoModule.polluxContextAwareTransactorLayer ++ RepoModule.polluxTransactorLayer >>> JdbcPresentationRepository.layer,
RepoModule.polluxContextAwareTransactorLayer ++ RepoModule.polluxTransactorLayer >>> JdbcOID4VCIIssuerMetadataRepository.layer,
RepoModule.polluxContextAwareTransactorLayer >>> JdbcVerificationPolicyRepository.layer,
// oidc
CredentialIssuerControllerImpl.layer,
InMemoryIssuanceSessionService.layer,
OID4VCIIssuerMetadataServiceImpl.layer,
OIDCCredentialIssuerServiceImpl.layer,
// event notification service
ZLayer.succeed(500) >>> EventNotificationServiceImpl.layer,
// HTTP client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import org.hyperledger.identus.agent.walletapi.model.{ManagedDIDState, Publicati
import org.hyperledger.identus.agent.walletapi.model.error.DIDSecretStorageError.{KeyNotFoundError, WalletNotFoundError}
import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService
import org.hyperledger.identus.agent.walletapi.storage.DIDNonSecretStorage
import org.hyperledger.identus.castor.core.model.did.{LongFormPrismDID, PrismDID, VerificationRelationship}
import org.hyperledger.identus.castor.core.model.did.EllipticCurve
import org.hyperledger.identus.castor.core.model.did.{
EllipticCurve,
LongFormPrismDID,
PrismDID,
VerificationRelationship
}
import org.hyperledger.identus.castor.core.service.DIDService
import org.hyperledger.identus.mercury.{AgentPeerService, DidAgent}
import org.hyperledger.identus.mercury.model.DidId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import sttp.tapir.json.zio.jsonBody
import sttp.tapir.EndpointOutput.OneOfVariant

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
Expand Up @@ -51,8 +51,8 @@ trait Authorizer[E <: BaseEntity] {
.mapError(msg =>
AuthenticationError.UnexpectedError(s"Unable to retrieve entity role for entity id ${entity.id}. $msg")
)
.filterOrFail(_ != EntityRole.Admin)(
AuthenticationError.InvalidRole("Admin role is not allowed to access the tenant's wallet.")
.filterOrFail(_ == EntityRole.Tenant)(
AuthenticationError.InvalidRole("Only Tenant role is allowed to access the tenant's wallet.")
)
.flatMap(_ => authorizeWalletAccessLogic(entity))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package org.hyperledger.identus.iam.authentication

import org.hyperledger.identus.agent.walletapi.model.{BaseEntity, EntityRole}
import org.hyperledger.identus.iam.authentication.oidc.{
AccessToken,
JwtAuthenticationError,
JwtCredentials,
Oauth2TokenIntrospector,
RemoteOauth2TokenIntrospector
}
import org.hyperledger.identus.oid4vci.service.OIDCCredentialIssuerService
import org.hyperledger.identus.pollux.core.service.OID4VCIIssuerMetadataService
import zio.*
import zio.http.Client

import java.util.UUID

final case class ExternalEntity(id: UUID) extends BaseEntity {
override def role: Either[String, EntityRole] = Right(EntityRole.ExternalParty)
}

case class Oid4vciAuthenticator(tokenIntrospector: Oauth2TokenIntrospector) extends Authenticator[ExternalEntity] {

override def isEnabled: Boolean = true

def authenticate(credentials: Credentials): IO[AuthenticationError, ExternalEntity] = {
credentials match {
case JwtCredentials(Some(token)) if token.nonEmpty => authenticate(token)
case JwtCredentials(Some(_)) => ZIO.fail(JwtAuthenticationError.emptyToken)
case JwtCredentials(None) => ZIO.fail(AuthenticationError.InvalidCredentials("Bearer token is not provided"))
case other => ZIO.fail(AuthenticationError.InvalidCredentials("Bearer token is not provided"))
}
}

private def authenticate(token: String): IO[AuthenticationError, ExternalEntity] = {
for {
accessToken <- ZIO
.fromEither(AccessToken.fromString(token))
.mapError(AuthenticationError.InvalidCredentials.apply)
introspection <- tokenIntrospector
.introspectToken(accessToken)
.mapError(e => AuthenticationError.UnexpectedError(e.getMessage))
_ <- ZIO
.fail(AuthenticationError.InvalidCredentials("The accessToken is invalid."))
.unless(introspection.active)
entityId <- ZIO
.fromOption(introspection.sub)
.mapError(_ => AuthenticationError.UnexpectedError("Subject ID is not found in the accessToken."))
.flatMap { id =>
ZIO
.attempt(UUID.fromString(id))
.mapError(e => AuthenticationError.UnexpectedError(s"Subject ID in accessToken is not a UUID. $e"))
}
} yield ExternalEntity(entityId)
}
}

class Oid4vciAuthenticatorFactory(
httpClient: Client,
issuerService: OIDCCredentialIssuerService,
metadataService: OID4VCIIssuerMetadataService
) {
def make(issuerState: String): IO[AuthenticationError, Oid4vciAuthenticator] =
issuerService
.getIssuanceSessionByIssuerState(issuerState)
.mapError(e =>
AuthenticationError.UnexpectedError(s"Unable to get issuanceSession from issuerState: $issuerState")
)
.flatMap(session => make(session.issuerId))

def make(issuerId: UUID): IO[AuthenticationError, Oid4vciAuthenticator] =
for {
issuer <- metadataService
.getCredentialIssuer(issuerId)
.mapError(e => AuthenticationError.UnexpectedError(s"Unable to get issuer from issuerId: $issuerId"))
tokenIntrospector <- RemoteOauth2TokenIntrospector
.fromAuthorizationServer(
httpClient,
issuer.authorizationServer,
issuer.authorizationServerClientId,
issuer.authorizationServerClientSecret
)
.mapError(e => AuthenticationError.UnexpectedError(s"Unable to create token introspector: $e"))
} yield Oid4vciAuthenticator(tokenIntrospector)
}

object Oid4vciAuthenticatorFactory {
def layer: URLayer[Client & OIDCCredentialIssuerService & OID4VCIIssuerMetadataService, Oid4vciAuthenticatorFactory] =
ZLayer.fromFunction(Oid4vciAuthenticatorFactory(_, _, _))
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ final class AccessToken private (token: String, claims: JwtClaim, rolesClaimPath
}

object AccessToken {
def fromString(token: String, rolesClaimPath: Seq[String]): Either[String, AccessToken] =
def fromString(token: String, rolesClaimPath: Seq[String] = Nil): Either[String, AccessToken] =
JwtCirce
.decode(token, JwtOptions(false, false, false))
.map(claims => AccessToken(token, claims, rolesClaimPath))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ class KeycloakAuthenticatorImpl(
ctx <- role match {
case EntityRole.Admin => ZIO.succeed(WalletAdministrationContext.Admin())
case EntityRole.Tenant => selfServiceCtx
case EntityRole.ExternalParty =>
ZIO.fail(AuthenticationError.InvalidRole("External party cannot access the wallet."))
}
} yield ctx
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,8 @@ import zio.*
import zio.http.*
import zio.json.*

import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import scala.jdk.CollectionConverters.*

final case class TokenIntrospection(active: Boolean, sub: Option[String])

object TokenIntrospection {
given JsonEncoder[TokenIntrospection] = JsonEncoder.derived
given JsonDecoder[TokenIntrospection] = JsonDecoder.derived
}

final case class TokenResponse(access_token: String, refresh_token: String)

object TokenResponse {
Expand Down Expand Up @@ -50,51 +41,19 @@ trait KeycloakClient {
class KeycloakClientImpl(client: AuthzClient, httpClient: Client, override val keycloakConfig: KeycloakConfig)
extends KeycloakClient {

private val introspectionUrl = client.getServerConfiguration().getIntrospectionEndpoint()
private val introspector: Oauth2TokenIntrospector = RemoteOauth2TokenIntrospector(
client.getServerConfiguration().getIntrospectionEndpoint(),
httpClient,
keycloakConfig.clientId,
keycloakConfig.clientSecret
)
private val tokenUrl = client.getServerConfiguration().getTokenEndpoint()

private val baseFormHeaders = Headers(Header.ContentType(MediaType.application.`x-www-form-urlencoded`))

// TODO: support offline introspection
// https://www.keycloak.org/docs/22.0.4/securing_apps/#_token_introspection_endpoint
override def introspectToken(token: AccessToken): IO[KeycloakClientError, TokenIntrospection] = {
(for {
url <- ZIO.fromEither(URL.decode(introspectionUrl)).orDie
response <- httpClient
.request(
Request(
url = url,
method = Method.POST,
headers = baseFormHeaders ++ Headers(
Header.Authorization.Basic(
username = URLEncoder.encode(keycloakConfig.clientId, StandardCharsets.UTF_8),
password = URLEncoder.encode(keycloakConfig.clientSecret, StandardCharsets.UTF_8)
)
),
body = Body.fromURLEncodedForm(
Form(
FormField.simpleField("token", token.toString)
)
)
)
)
.logError("Fail to introspect token on keycloak.")
.mapError(e => KeycloakClientError.UnexpectedError("Fail to introspect the token on keycloak."))
body <- response.body.asString
.logError("Fail parse keycloak introspection response.")
.mapError(e => KeycloakClientError.UnexpectedError("Fail parse keycloak introspection response."))
result <-
if (response.status.code == 200) {
ZIO
.fromEither(body.fromJson[TokenIntrospection])
.logError("Fail to decode keycloak token introspection response")
.mapError(e => KeycloakClientError.UnexpectedError(e))
} else {
ZIO.logError(s"Keycloak token introspection was unsucessful. Status: ${response.status}. Response: $body") *>
ZIO.fail(KeycloakClientError.UnexpectedError("Token introspection was unsuccessful."))
}
} yield result).provide(Scope.default)
}
override def introspectToken(token: AccessToken): IO[KeycloakClientError, TokenIntrospection] =
introspector.introspectToken(token).mapError(e => KeycloakClientError.UnexpectedError(e.getMessage))

override def getAccessToken(username: String, password: String): IO[KeycloakClientError, TokenResponse] = {
(for {
Expand Down
Loading

0 comments on commit 3ae91dc

Please sign in to comment.