Skip to content

Commit

Permalink
Merge pull request #2370 from constantine2nd/develop
Browse files Browse the repository at this point in the history
ORY Hydra as OIDC Provider
  • Loading branch information
simonredfern committed Apr 4, 2024
2 parents 3344a78 + 345deb4 commit f90bbad
Show file tree
Hide file tree
Showing 13 changed files with 179 additions and 111 deletions.
6 changes: 4 additions & 2 deletions obp-api/src/main/resources/props/sample.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,8 @@ outboundAdapterCallContext.generalContext
# There are 2 ways of authenticating OAuth 2.0 Clients at the /oauth2/token we support: private_key_jwt and client_secret_post
# hydra_token_endpoint_auth_method=private_key_jwt
# hydra_supported_token_endpoint_auth_methods=client_secret_basic,client_secret_post,private_key_jwt
## ORY Hydra login url is "obp-api-hostname/user_mgt/login" implies "true" in order to avoid creation of a new user during OIDC flow
# hydra_uses_obp_user_credentials=true
# ------------------------------ Hydra oauth2 props end ------------------------------

# ------------------------------ default entitlements ------------------------------
Expand Down Expand Up @@ -1178,12 +1180,12 @@ webui_developer_user_invitation_email_html_text=<!DOCTYPE html>\
# List of countries where consent is not required for the collection of personal data
personal_data_collection_consent_country_waiver_list = Austria, Belgium, Bulgaria, Croatia, Republic of Cyprus, Czech Republic, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Ireland, Italy, Latvia, Lithuania, Luxembourg, Malta, Netherlands, Poland, Portugal, Romania, Slovakia, Slovenia, Spain, Sweden, England, Scotland, Wales, Northern Ireland

# Sngle Sign On/Off
# Single Sign On/Off
# sso.enabled=false

# Local identity provider url
# it defaults to the hostname props value
# local_identity_provider=strongly recomended to use top level domain name so that all nodes in the cluster share same provider name
# local_identity_provider=strongly recommended to use top level domain name so that all nodes in the cluster share same provider name


# enable dynamic code sandbox, default is false, this will make sandbox works for code running in Future, will make performance lower than disable
Expand Down
39 changes: 26 additions & 13 deletions obp-api/src/main/scala/code/api/OAuth2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ package code.api

import java.net.URI
import java.util

import code.api.util.ErrorMessages._
import code.api.util.{APIUtil, CallContext, JwtUtil}
import code.consumer.Consumers
Expand All @@ -38,6 +37,7 @@ import code.model.Consumer
import code.util.HydraUtil._
import code.users.Users
import code.util.Helper.MdcLoggable
import code.util.HydraUtil
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet
import com.openbankproject.commons.ExecutionContext.Implicits.global
Expand Down Expand Up @@ -274,13 +274,13 @@ object OAuth2Login extends RestHelper with MdcLoggable {
* @return an existing or a new user
*/
def getOrCreateResourceUserFuture(idToken: String): Future[Box[User]] = {
val subject = JwtUtil.getSubject(idToken).getOrElse("")
val issuer = JwtUtil.getIssuer(idToken).getOrElse("")
val uniqueIdGivenByProvider = JwtUtil.getSubject(idToken).getOrElse("")
val provider = resolveProvider(idToken)
Users.users.vend.getOrCreateUserByProviderIdFuture(
provider = issuer,
idGivenByProvider = subject,
provider = provider,
idGivenByProvider = uniqueIdGivenByProvider,
consentId = None,
name = getClaim(name = "given_name", idToken = idToken).orElse(Some(subject)),
name = getClaim(name = "given_name", idToken = idToken).orElse(Some(uniqueIdGivenByProvider)),
email = getClaim(name = "email", idToken = idToken)
).map(_._1)
}
Expand All @@ -301,14 +301,14 @@ object OAuth2Login extends RestHelper with MdcLoggable {
* @return an existing or a new user
*/
def getOrCreateResourceUser(idToken: String): Box[User] = {
val subject = JwtUtil.getSubject(idToken).getOrElse("")
val issuer = JwtUtil.getIssuer(idToken).getOrElse("")
Users.users.vend.getUserByProviderId(provider = issuer, idGivenByProvider = subject).or { // Find a user
val uniqueIdGivenByProvider = JwtUtil.getSubject(idToken).getOrElse("")
val provider = resolveProvider(idToken)
Users.users.vend.getUserByProviderId(provider = provider, idGivenByProvider = uniqueIdGivenByProvider).or { // Find a user
Users.users.vend.createResourceUser( // Otherwise create a new one
provider = issuer,
providerId = Some(subject),
provider = provider,
providerId = Some(uniqueIdGivenByProvider),
None,
name = getClaim(name = "given_name", idToken = idToken).orElse(Some(subject)),
name = getClaim(name = "given_name", idToken = idToken).orElse(Some(uniqueIdGivenByProvider)),
email = getClaim(name = "email", idToken = idToken),
userId = None,
createdByUserInvitationId = None,
Expand All @@ -317,7 +317,20 @@ object OAuth2Login extends RestHelper with MdcLoggable {
)
}
}
/**

def resolveProvider(idToken: String) = {
isIssuer(jwtToken = idToken, identityProvider = hydraPublicUrl) match {
case true if HydraUtil.hydraUsesObpUserCredentials => // Case that source of the truth of Hydra user management is the OBP-API mapper DB
// In case that ORY Hydra login url is "hostname/user_mgt/login" we MUST override hydraPublicUrl as provider
// in order to avoid creation of a new user
Constant.localIdentityProvider
case _ => // All other cases implies a new user creation
// TODO raise exception in case of else case
JwtUtil.getIssuer(idToken).getOrElse("")
}
}

/**
* This function creates a consumer based on "azp", "sub", "iss", "name" and "email" fields
* Please note that a user must be created before consumer.
* Unique criteria to decide do we create or get a consumer is pair o values: < sub : azp > i.e.
Expand Down
2 changes: 1 addition & 1 deletion obp-api/src/main/scala/code/api/directlogin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ object DirectLogin extends RestHelper with MdcLoggable {
def grantEntitlementsToUseDynamicEndpointsInSpacesInDirectLogin(userId:Long) = {
try {
val resourceUser = UserX.findByResourceUserId(userId).openOrThrowException(s"$InvalidDirectLoginParameters can not find the resourceUser!")
val authUser = AuthUser.findUserByUsernameLocally(resourceUser.name).openOrThrowException(s"$InvalidDirectLoginParameters can not find the auth user!")
val authUser = AuthUser.findAuthUserByPrimaryKey(resourceUser.userPrimaryKey.value).openOrThrowException(s"$InvalidDirectLoginParameters can not find the auth user!")
AuthUser.grantEntitlementsToUseDynamicEndpointsInSpaces(authUser)
AuthUser.grantEmailDomainEntitlementsToUser(authUser)
// User init actions
Expand Down
16 changes: 9 additions & 7 deletions obp-api/src/main/scala/code/api/openidconnect.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ TESOBE (http://www.tesobe.com/)
package code.api

import java.net.HttpURLConnection

import code.api.OAuth2Login.Hydra
import code.api.util.APIUtil._
import code.api.util.{APIUtil, AfterApiAuth, ErrorMessages, JwtUtil}
import code.consumer.Consumers
Expand All @@ -38,7 +40,7 @@ import code.token.{OpenIDConnectToken, TokensOpenIDConnect}
import code.users.Users
import code.util.Helper.{MdcLoggable, ObpS}
import com.openbankproject.commons.model.User
import com.openbankproject.commons.util.{ApiVersion,ApiVersionStatus}
import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus}
import javax.net.ssl.HttpsURLConnection
import net.liftweb.common._
import net.liftweb.http._
Expand Down Expand Up @@ -195,14 +197,14 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable {
}

private def getOrCreateResourceUser(idToken: String): Box[User] = {
val subject = JwtUtil.getSubject(idToken)
val issuer = JwtUtil.getIssuer(idToken).getOrElse("")
Users.users.vend.getUserByProviderId(provider = issuer, idGivenByProvider = subject.getOrElse("")).or { // Find a user
val uniqueIdGivenByProvider = JwtUtil.getSubject(idToken)
val provider = Hydra.resolveProvider(idToken)
Users.users.vend.getUserByProviderId(provider = provider, idGivenByProvider = uniqueIdGivenByProvider.getOrElse("")).or { // Find a user
Users.users.vend.createResourceUser( // Otherwise create a new one
provider = issuer,
providerId = subject,
provider = provider,
providerId = uniqueIdGivenByProvider,
createdByConsentId = None,
name = subject,
name = uniqueIdGivenByProvider,
email = getClaim(name = "email", idToken = idToken),
userId = None,
createdByUserInvitationId = None,
Expand Down
2 changes: 1 addition & 1 deletion obp-api/src/main/scala/code/api/util/ConsentUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ object Consent {
// 1. Get or Create a User
getOrCreateUser(consent.sub, consent.iss, Some(consent.toConsent().consentId), None, None) map {
case (Full(user), newUser) =>
// 2. Assign entitlements to the User
// 2. Assign entitlements (Roles) to the User
addEntitlements(user, consent) match {
case Full(user) =>
// 3. Copy Auth Context to the User
Expand Down
13 changes: 13 additions & 0 deletions obp-api/src/main/scala/code/api/util/Glossary.scala
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,19 @@ object Glossary extends MdcLoggable {
|Each Consumer has a consumer key and secrect which allows it to enter into secure communication with the API server.
""")

glossaryItems += GlossaryItem(
title = "Consumer.consumer_key (Consumer Key)",
description =
s"""
|The client identifier issued to the client during the registration process. It is a unique string representing the registration information provided by the client.
|At the time the consumer_key was introduced OAuth 1.0a was only available. The OAuth 2.0 counterpart for this value is client_id
|""".stripMargin)

glossaryItems += GlossaryItem(
title = "client_id (Client ID)",
description =
s"""Please take a look at a Consumer.consumer_key""".stripMargin)

glossaryItems += GlossaryItem(
title = "Customer",
description =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import code.metadata.transactionimages.TransactionImages
import code.metadata.wheretags.WhereTags
import code.metrics.MappedMetric
import code.model._
import code.model.dataAccess.AuthUser.findUserByUsernameLocally
import code.model.dataAccess.AuthUser.findAuthUserByUsernameLocally
import code.model.dataAccess._
import code.productAttributeattribute.MappedProductAttribute
import code.productattribute.ProductAttributeX
Expand Down Expand Up @@ -5793,7 +5793,7 @@ object LocalMappedConnector extends Connector with MdcLoggable {
//NOTE: this method is not for mapped connector, we put it here for the star default implementation.
// : we call that method only when we set external authentication and provider is not OBP-API
override def checkExternalUserExists(username: String, callContext: Option[CallContext]): Box[InboundExternalUser] = {
findUserByUsernameLocally(username).map( user =>
findAuthUserByUsernameLocally(username).map(user =>
InboundExternalUser(aud = "",
exp = "",
iat = "",
Expand Down
Loading

0 comments on commit f90bbad

Please sign in to comment.