diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 1a8952ea31..e71ce467b3 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -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 ------------------------------ @@ -1178,12 +1180,12 @@ webui_developer_user_invitation_email_html_text=\ # 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 diff --git a/obp-api/src/main/scala/code/api/OAuth2.scala b/obp-api/src/main/scala/code/api/OAuth2.scala index 5e2ec6e214..396cd89e19 100644 --- a/obp-api/src/main/scala/code/api/OAuth2.scala +++ b/obp-api/src/main/scala/code/api/OAuth2.scala @@ -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 @@ -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 @@ -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) } @@ -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, @@ -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. diff --git a/obp-api/src/main/scala/code/api/directlogin.scala b/obp-api/src/main/scala/code/api/directlogin.scala index 62b45943c5..576ce7511e 100644 --- a/obp-api/src/main/scala/code/api/directlogin.scala +++ b/obp-api/src/main/scala/code/api/directlogin.scala @@ -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 diff --git a/obp-api/src/main/scala/code/api/openidconnect.scala b/obp-api/src/main/scala/code/api/openidconnect.scala index 3702576705..f05adb7eb3 100644 --- a/obp-api/src/main/scala/code/api/openidconnect.scala +++ b/obp-api/src/main/scala/code/api/openidconnect.scala @@ -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 @@ -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._ @@ -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, diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 29ce832822..86949f8dd4 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -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 diff --git a/obp-api/src/main/scala/code/api/util/Glossary.scala b/obp-api/src/main/scala/code/api/util/Glossary.scala index 7a76579344..2f9219af72 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -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 = diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 70d8be3460..631bac4fdf 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -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 @@ -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 = "", diff --git a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala index eea9a6ace5..3b9d1de5c7 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala @@ -591,7 +591,7 @@ import net.liftweb.util.Helpers._ * Overridden to use the hostname set in the props file */ override def sendPasswordReset(name: String) { - findUserByUsernameLocally(name).toList ::: findUsersByEmailLocally(name) map { + findAuthUserByUsernameLocally(name).toList ::: findUsersByEmailLocally(name) map { // reason of case parameter name is "u" instead of "user": trait AuthUser have constant mumber name is "user" // So if the follow case paramter name is "user" will cause compile warnings case u if u.validated_? => @@ -653,14 +653,14 @@ import net.liftweb.util.Helpers._ generateValidationEmailBodies(user, resetLink) ::: (bccEmail.toList.map(BCC(_))) :_* ) } - + def grantDefaultEntitlementsToAuthUser(user: TheUserType) = { tryo{getResourceUserByUsername(user.getProvider(), user.username.get).head.userId} match { case Full(userId)=>APIUtil.grantDefaultEntitlementsToNewUser(userId) case _ => logger.error("Can not getResourceUserByUsername here, so it breaks the grantDefaultEntitlementsToNewUser process.") } } - + override def validateUser(id: String): NodeSeq = findUserByUniqueId(id) match { case Full(user) if !user.validated_? => user.setValidated(true).resetUniqueId().save @@ -668,7 +668,7 @@ import net.liftweb.util.Helpers._ logUserIn(user, () => { S.notice(S.?("account.validated")) APIUtil.getPropsValue("user_account_validated_redirect_url") match { - case Full(redirectUrl) => + case Full(redirectUrl) => logger.debug(s"user_account_validated_redirect_url = $redirectUrl") S.redirectTo(redirectUrl) case _ => @@ -679,7 +679,7 @@ import net.liftweb.util.Helpers._ case _ => S.error(S.?("invalid.validation.link")); S.redirectTo(homePage) } - + override def actionsAfterSignup(theUser: TheUserType, func: () => Nothing): Nothing = { theUser.setValidated(skipEmailValidation).resetUniqueId() theUser.save @@ -737,7 +737,7 @@ import net.liftweb.util.Helpers._ scala.xml.Unparsed(s"""$agreeTermsHtml""") } } - + def agreePrivacyPolicy = { val webUi = new WebUI val privacyPolicyCheckboxText = Helper.i18n("privacy_policy_checkbox_text", Some("I agree to the above Privacy Policy")) @@ -755,7 +755,7 @@ import net.liftweb.util.Helpers._ |
""".stripMargin scala.xml.Unparsed(agreePrivacyPolicy) - } + } def enableDisableSignUpButton = { val javaScriptCode = """