Merge pull request #2446 from constantine2nd/develop

Keycloak
This commit is contained in:
Simon Redfern 2024-11-18 15:53:44 +01:00 committed by GitHub
commit 27bdfcce3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 288 additions and 83 deletions

View File

@ -499,7 +499,22 @@
<artifactId>amqp-client</artifactId>
<version>5.22.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.testcontainers/rabbitmq -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>rabbitmq</artifactId>
<version>1.20.3</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.testcontainers/kafka -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>kafka</artifactId>
<version>1.20.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -747,9 +747,10 @@ display_internal_errors=false
# allow_oauth2_login=false
# URL of Public server JWK set used for validating bearer JWT access tokens
# It can contain more than one URL i.e. list of uris. Values are comma separated.
# If MITREId URL is present it must be at 1st place in the list
# because MITREId URL can be an appropirate value and we cannot rely on it.
# oauth2.jwk_set.url=http://localhost:8080/jwk.json,https://www.googleapis.com/oauth2/v3/certs
# Keycloak Identity Provider Host
# oauth2.keycloak.host=http://localhost:7070
# oauth2.keycloak.well-known=http://localhost:7070/realms/master/.well-known/openid-configuration
# ------------------------------------------------------------------------------ OAuth 2 ------
## This property is used for documenting at Resource Doc. It may include the port also (but not /obp)

View File

@ -28,13 +28,12 @@ package code.api
import java.net.URI
import java.util
import code.api.util.ErrorMessages._
import code.api.util.{APIUtil, CallContext, CertificateUtil, JwtUtil}
import code.consumer.Consumers
import code.consumer.Consumers.consumers
import code.loginattempts.LoginAttempt
import code.model.Consumer
import code.model.{AppType, Consumer}
import code.util.HydraUtil._
import code.users.Users
import code.util.Helper.MdcLoggable
@ -75,11 +74,13 @@ object OAuth2Login extends RestHelper with MdcLoggable {
case true =>
val value = getValueOfOAuh2HeaderField(cc)
if (Google.isIssuer(value)) {
Google.applyRules(value, cc)
Google.applyIdTokenRules(value, cc)
} else if (Yahoo.isIssuer(value)) {
Yahoo.applyRules(value, cc)
Yahoo.applyIdTokenRules(value, cc)
} else if (Azure.isIssuer(value)) {
Azure.applyRules(value, cc)
Azure.applyIdTokenRules(value, cc)
} else if (Keycloak.isIssuer(value)) {
Keycloak.applyRules(value, cc)
} else {
Hydra.applyRules(value, cc)
}
@ -95,11 +96,13 @@ object OAuth2Login extends RestHelper with MdcLoggable {
case true =>
val value = getValueOfOAuh2HeaderField(cc)
if (Google.isIssuer(value)) {
Google.applyRulesFuture(value, cc)
Google.applyIdTokenRulesFuture(value, cc)
} else if (Yahoo.isIssuer(value)) {
Yahoo.applyRulesFuture(value, cc)
Yahoo.applyIdTokenRulesFuture(value, cc)
} else if (Azure.isIssuer(value)) {
Azure.applyRulesFuture(value, cc)
Azure.applyIdTokenRulesFuture(value, cc)
} else if (Keycloak.isIssuer(value)) {
Keycloak.applyRulesFuture(value, cc)
} else {
Hydra.applyRulesFuture(value, cc)
}
@ -108,12 +111,12 @@ object OAuth2Login extends RestHelper with MdcLoggable {
}
}
object Hydra extends OAuth2Util {
override def wellKnownOpenidConfiguration: URI = new URI(hydraPublicUrl)
override def urlOfJwkSets: Box[String] = checkUrlOfJwkSets(identityProvider = hydraPublicUrl)
private def applyAccessTokenRules(value: String, cc: CallContext): (Box[User], Some[CallContext]) = {
override def applyAccessTokenRules(value: String, cc: CallContext): (Box[User], Some[CallContext]) = {
// In case of Hydra issued access tokens are not self-encoded/self-contained like JWT tokens are.
// It implies the access token can be revoked at any time.
val introspectOAuth2Token: OAuth2TokenIntrospection = hydraAdmin.introspectOAuth2Token(value, null)
@ -135,7 +138,7 @@ object OAuth2Login extends RestHelper with MdcLoggable {
val errorMessage = Oauth2TokenEndpointAuthMethodForbidden + hydraClient.getTokenEndpointAuthMethod()
return (Failure(errorMessage), Some(cc.copy(consumer = Failure(errorMessage))))
}
// check access token binding with client certificate
{
if(consumer.isEmpty) {
@ -163,7 +166,7 @@ object OAuth2Login extends RestHelper with MdcLoggable {
// hydra update client endpoint have bug, So here delete and create to do update
hydraAdmin.deleteOAuth2Client(clientId)
hydraAdmin.createOAuth2Client(oAuth2Client)
} else if(!CertificateUtil.comparePemX509Certificates(certInConsumer, cert)) {
} else if(!CertificateUtil.comparePemX509Certificates(certInConsumer, cert)) {
// Cannot mat.ch the value from PSD2-CERT header and the database value Consumer.clientCertificate
logger.debug(s"Cert in Consumer with the name ***${foundConsumer.name}*** : " + certInConsumer)
logger.debug("Cert in Request: " + cert)
@ -178,7 +181,7 @@ object OAuth2Login extends RestHelper with MdcLoggable {
}
}
}
// In case a user is created via OpenID Connect flow implies provider = hydraPublicUrl
// In case a user is created via GUI of OBP-API implies provider = Constant.localIdentityProvider
val user = Users.users.vend.getUserByProviderAndUsername(introspectOAuth2Token.getIss, introspectOAuth2Token.getSub).or(
@ -193,28 +196,24 @@ object OAuth2Login extends RestHelper with MdcLoggable {
case _ => (user, Some(cc.copy(consumer = consumer)))
}
}
private def applyIdTokenRules(value: String, cc: CallContext): (Box[User], Some[CallContext]) = {
super.applyRules(value, cc)
}
override def applyRules(value: String, cc: CallContext): (Box[User], Some[CallContext]) = {
isIssuer(jwtToken=value, identityProvider = hydraPublicUrl) match {
case true => applyIdTokenRules(value, cc)
case false => applyAccessTokenRules(value, cc)
def applyRules(token: String, cc: CallContext): (Box[User], Some[CallContext]) = {
isIssuer(jwtToken=token, identityProvider = hydraPublicUrl) match {
case true => super.applyIdTokenRules(token, cc)
case false => applyAccessTokenRules(token, cc)
}
}
override def applyRulesFuture(value: String, cc: CallContext): Future[(Box[User], Some[CallContext])] = Future {
def applyRulesFuture(value: String, cc: CallContext): Future[(Box[User], Some[CallContext])] = Future {
applyRules(value, cc)
}
}
trait OAuth2Util {
def wellKnownOpenidConfiguration: URI
def urlOfJwkSets: Box[String] = APIUtil.getPropsValue(nameOfProperty = "oauth2.jwk_set.url")
def checkUrlOfJwkSets(identityProvider: String) = {
@ -226,7 +225,7 @@ object OAuth2Login extends RestHelper with MdcLoggable {
case Nil => Failure(Oauth2CannotMatchIssuerAndJwksUriException)
}
}
private def getClaim(name: String, idToken: String): Option[String] = {
val claim = JwtUtil.getClaim(name = name, jwtToken = idToken)
claim match {
@ -249,6 +248,18 @@ object OAuth2Login extends RestHelper with MdcLoggable {
Failure(Oauth2ThereIsNoUrlOfJwkSet)
}
}
def validateAccessToken(accessToken: String): Box[JWTClaimsSet] = {
urlOfJwkSets match {
case Full(url) =>
JwtUtil.validateAccessToken(accessToken, url)
case ParamFailure(a, b, c, apiFailure : APIFailure) =>
ParamFailure(a, b, c, apiFailure : APIFailure)
case Failure(msg, t, c) =>
Failure(msg, t, c)
case _ =>
Failure(Oauth2ThereIsNoUrlOfJwkSet)
}
}
/** New Style Endpoints
* This function creates user based on "iss" and "sub" fields
* It is mapped in next way:
@ -256,11 +267,11 @@ object OAuth2Login extends RestHelper with MdcLoggable {
* sub => ResourceUser.providerId
* @param idToken Google's response example:
* {
* "access_token": "ya29.GluUBg5DflrJciFikW5hqeKEp9r1whWnU5x2JXCm9rKkRMs2WseXX8O5UugFMDsIKuKCZlE7tTm1fMII_YYpvcMX6quyR5DXNHH8Lbx5TrZN__fA92kszHJEVqPc",
* "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA4ZDMyNDVjNjJmODZiNjM2MmFmY2JiZmZlMWQwNjk4MjZkZDFkYzEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTM5NjY4NTQyNDU3ODA4OTI5NTkiLCJlbWFpbCI6Im1hcmtvLm1pbGljLnNyYmlqYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6Im5HS1JUb0tOblZBMjhINk1od1hCeHciLCJuYW1lIjoiTWFya28gTWlsacSHIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tWGQ0NGhuSjZURG8vQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQUt4cndjYWR3emhtNE40dFdrNUU4QXZ4aS1aSzZrczRxZy9zOTYtYy9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiTWFya28iLCJmYW1pbHlfbmFtZSI6Ik1pbGnEhyIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNTQ3NzA1NjkxLCJleHAiOjE1NDc3MDkyOTF9.iUxhF_SU2vi76zPuRqAKJvFOzpb_EeP3lc5u9FO9o5xoXzVq3QooXexTfK2f1YAcWEy9LSftA34PB0QTuCZpkQChZVM359n3a3hplf6oWWkBXZN2_IG10NwEH4g0VVBCsjWBDMp6lvepN_Zn15x8opUB7272m4-smAou_WmUPTeivXRF8yPcp4J55DigcY31YP59dMQr2X-6Rr1vCRnJ6niqqJ1UDldfsgt4L7dXmUCnkDdXHwEQAZwbKbR4dUoEha3QeylCiBErmLdpIyqfKECphC6piGXZB-rRRqLz41WNfuF-3fswQvGmIkzTJDR7lQaletMp7ivsfVw8N5jFxg",
* "expires_in": 3600,
* "token_type": "Bearer",
* "scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
* "access_token": "ya29.GluUBg5DflrJciFikW5hqeKEp9r1whWnU5x2JXCm9rKkRMs2WseXX8O5UugFMDsIKuKCZlE7tTm1fMII_YYpvcMX6quyR5DXNHH8Lbx5TrZN__fA92kszHJEVqPc",
* "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA4ZDMyNDVjNjJmODZiNjM2MmFmY2JiZmZlMWQwNjk4MjZkZDFkYzEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTM5NjY4NTQyNDU3ODA4OTI5NTkiLCJlbWFpbCI6Im1hcmtvLm1pbGljLnNyYmlqYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6Im5HS1JUb0tOblZBMjhINk1od1hCeHciLCJuYW1lIjoiTWFya28gTWlsacSHIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tWGQ0NGhuSjZURG8vQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQUt4cndjYWR3emhtNE40dFdrNUU4QXZ4aS1aSzZrczRxZy9zOTYtYy9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiTWFya28iLCJmYW1pbHlfbmFtZSI6Ik1pbGnEhyIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNTQ3NzA1NjkxLCJleHAiOjE1NDc3MDkyOTF9.iUxhF_SU2vi76zPuRqAKJvFOzpb_EeP3lc5u9FO9o5xoXzVq3QooXexTfK2f1YAcWEy9LSftA34PB0QTuCZpkQChZVM359n3a3hplf6oWWkBXZN2_IG10NwEH4g0VVBCsjWBDMp6lvepN_Zn15x8opUB7272m4-smAou_WmUPTeivXRF8yPcp4J55DigcY31YP59dMQr2X-6Rr1vCRnJ6niqqJ1UDldfsgt4L7dXmUCnkDdXHwEQAZwbKbR4dUoEha3QeylCiBErmLdpIyqfKECphC6piGXZB-rRRqLz41WNfuF-3fswQvGmIkzTJDR7lQaletMp7ivsfVw8N5jFxg",
* "expires_in": 3600,
* "token_type": "Bearer",
* "scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
* "refresh_token": "1/HkTtUahtUTdG7D6urpPNz6g-_qufF-Y1YppcBf0v3Cs"
* }
* @return an existing or a new user
@ -271,7 +282,7 @@ object OAuth2Login extends RestHelper with MdcLoggable {
Users.users.vend.getOrCreateUserByProviderIdFuture(
provider = provider,
idGivenByProvider = uniqueIdGivenByProvider,
consentId = None,
consentId = None,
name = getClaim(name = "given_name", idToken = idToken).orElse(Some(uniqueIdGivenByProvider)),
email = getClaim(name = "email", idToken = idToken)
).map(_._1)
@ -283,11 +294,11 @@ object OAuth2Login extends RestHelper with MdcLoggable {
* sub => ResourceUser.providerId
* @param idToken Google's response example:
* {
* "access_token": "ya29.GluUBg5DflrJciFikW5hqeKEp9r1whWnU5x2JXCm9rKkRMs2WseXX8O5UugFMDsIKuKCZlE7tTm1fMII_YYpvcMX6quyR5DXNHH8Lbx5TrZN__fA92kszHJEVqPc",
* "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA4ZDMyNDVjNjJmODZiNjM2MmFmY2JiZmZlMWQwNjk4MjZkZDFkYzEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTM5NjY4NTQyNDU3ODA4OTI5NTkiLCJlbWFpbCI6Im1hcmtvLm1pbGljLnNyYmlqYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6Im5HS1JUb0tOblZBMjhINk1od1hCeHciLCJuYW1lIjoiTWFya28gTWlsacSHIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tWGQ0NGhuSjZURG8vQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQUt4cndjYWR3emhtNE40dFdrNUU4QXZ4aS1aSzZrczRxZy9zOTYtYy9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiTWFya28iLCJmYW1pbHlfbmFtZSI6Ik1pbGnEhyIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNTQ3NzA1NjkxLCJleHAiOjE1NDc3MDkyOTF9.iUxhF_SU2vi76zPuRqAKJvFOzpb_EeP3lc5u9FO9o5xoXzVq3QooXexTfK2f1YAcWEy9LSftA34PB0QTuCZpkQChZVM359n3a3hplf6oWWkBXZN2_IG10NwEH4g0VVBCsjWBDMp6lvepN_Zn15x8opUB7272m4-smAou_WmUPTeivXRF8yPcp4J55DigcY31YP59dMQr2X-6Rr1vCRnJ6niqqJ1UDldfsgt4L7dXmUCnkDdXHwEQAZwbKbR4dUoEha3QeylCiBErmLdpIyqfKECphC6piGXZB-rRRqLz41WNfuF-3fswQvGmIkzTJDR7lQaletMp7ivsfVw8N5jFxg",
* "expires_in": 3600,
* "token_type": "Bearer",
* "scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
* "access_token": "ya29.GluUBg5DflrJciFikW5hqeKEp9r1whWnU5x2JXCm9rKkRMs2WseXX8O5UugFMDsIKuKCZlE7tTm1fMII_YYpvcMX6quyR5DXNHH8Lbx5TrZN__fA92kszHJEVqPc",
* "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA4ZDMyNDVjNjJmODZiNjM2MmFmY2JiZmZlMWQwNjk4MjZkZDFkYzEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTM5NjY4NTQyNDU3ODA4OTI5NTkiLCJlbWFpbCI6Im1hcmtvLm1pbGljLnNyYmlqYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6Im5HS1JUb0tOblZBMjhINk1od1hCeHciLCJuYW1lIjoiTWFya28gTWlsacSHIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tWGQ0NGhuSjZURG8vQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQUt4cndjYWR3emhtNE40dFdrNUU4QXZ4aS1aSzZrczRxZy9zOTYtYy9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiTWFya28iLCJmYW1pbHlfbmFtZSI6Ik1pbGnEhyIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNTQ3NzA1NjkxLCJleHAiOjE1NDc3MDkyOTF9.iUxhF_SU2vi76zPuRqAKJvFOzpb_EeP3lc5u9FO9o5xoXzVq3QooXexTfK2f1YAcWEy9LSftA34PB0QTuCZpkQChZVM359n3a3hplf6oWWkBXZN2_IG10NwEH4g0VVBCsjWBDMp6lvepN_Zn15x8opUB7272m4-smAou_WmUPTeivXRF8yPcp4J55DigcY31YP59dMQr2X-6Rr1vCRnJ6niqqJ1UDldfsgt4L7dXmUCnkDdXHwEQAZwbKbR4dUoEha3QeylCiBErmLdpIyqfKECphC6piGXZB-rRRqLz41WNfuF-3fswQvGmIkzTJDR7lQaletMp7ivsfVw8N5jFxg",
* "expires_in": 3600,
* "token_type": "Bearer",
* "scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
* "refresh_token": "1/HkTtUahtUTdG7D6urpPNz6g-_qufF-Y1YppcBf0v3Cs"
* }
* @return an existing or a new user
@ -330,22 +341,22 @@ object OAuth2Login extends RestHelper with MdcLoggable {
* We can find consumer by sub and azp => Get
* @param idToken Google's response example:
* {
* "access_token": "ya29.GluUBg5DflrJciFikW5hqeKEp9r1whWnU5x2JXCm9rKkRMs2WseXX8O5UugFMDsIKuKCZlE7tTm1fMII_YYpvcMX6quyR5DXNHH8Lbx5TrZN__fA92kszHJEVqPc",
* "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA4ZDMyNDVjNjJmODZiNjM2MmFmY2JiZmZlMWQwNjk4MjZkZDFkYzEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTM5NjY4NTQyNDU3ODA4OTI5NTkiLCJlbWFpbCI6Im1hcmtvLm1pbGljLnNyYmlqYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6Im5HS1JUb0tOblZBMjhINk1od1hCeHciLCJuYW1lIjoiTWFya28gTWlsacSHIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tWGQ0NGhuSjZURG8vQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQUt4cndjYWR3emhtNE40dFdrNUU4QXZ4aS1aSzZrczRxZy9zOTYtYy9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiTWFya28iLCJmYW1pbHlfbmFtZSI6Ik1pbGnEhyIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNTQ3NzA1NjkxLCJleHAiOjE1NDc3MDkyOTF9.iUxhF_SU2vi76zPuRqAKJvFOzpb_EeP3lc5u9FO9o5xoXzVq3QooXexTfK2f1YAcWEy9LSftA34PB0QTuCZpkQChZVM359n3a3hplf6oWWkBXZN2_IG10NwEH4g0VVBCsjWBDMp6lvepN_Zn15x8opUB7272m4-smAou_WmUPTeivXRF8yPcp4J55DigcY31YP59dMQr2X-6Rr1vCRnJ6niqqJ1UDldfsgt4L7dXmUCnkDdXHwEQAZwbKbR4dUoEha3QeylCiBErmLdpIyqfKECphC6piGXZB-rRRqLz41WNfuF-3fswQvGmIkzTJDR7lQaletMp7ivsfVw8N5jFxg",
* "expires_in": 3600,
* "token_type": "Bearer",
* "scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
* "access_token": "ya29.GluUBg5DflrJciFikW5hqeKEp9r1whWnU5x2JXCm9rKkRMs2WseXX8O5UugFMDsIKuKCZlE7tTm1fMII_YYpvcMX6quyR5DXNHH8Lbx5TrZN__fA92kszHJEVqPc",
* "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA4ZDMyNDVjNjJmODZiNjM2MmFmY2JiZmZlMWQwNjk4MjZkZDFkYzEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTM5NjY4NTQyNDU3ODA4OTI5NTkiLCJlbWFpbCI6Im1hcmtvLm1pbGljLnNyYmlqYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6Im5HS1JUb0tOblZBMjhINk1od1hCeHciLCJuYW1lIjoiTWFya28gTWlsacSHIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tWGQ0NGhuSjZURG8vQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQUt4cndjYWR3emhtNE40dFdrNUU4QXZ4aS1aSzZrczRxZy9zOTYtYy9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiTWFya28iLCJmYW1pbHlfbmFtZSI6Ik1pbGnEhyIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNTQ3NzA1NjkxLCJleHAiOjE1NDc3MDkyOTF9.iUxhF_SU2vi76zPuRqAKJvFOzpb_EeP3lc5u9FO9o5xoXzVq3QooXexTfK2f1YAcWEy9LSftA34PB0QTuCZpkQChZVM359n3a3hplf6oWWkBXZN2_IG10NwEH4g0VVBCsjWBDMp6lvepN_Zn15x8opUB7272m4-smAou_WmUPTeivXRF8yPcp4J55DigcY31YP59dMQr2X-6Rr1vCRnJ6niqqJ1UDldfsgt4L7dXmUCnkDdXHwEQAZwbKbR4dUoEha3QeylCiBErmLdpIyqfKECphC6piGXZB-rRRqLz41WNfuF-3fswQvGmIkzTJDR7lQaletMp7ivsfVw8N5jFxg",
* "expires_in": 3600,
* "token_type": "Bearer",
* "scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
* "refresh_token": "1/HkTtUahtUTdG7D6urpPNz6g-_qufF-Y1YppcBf0v3Cs"
* }
* @return an existing or a new consumer
*/
def getOrCreateConsumer(idToken: String, userId: Box[String]): Box[Consumer] = {
def getOrCreateConsumer(idToken: String, userId: Box[String], description: Option[String]): Box[Consumer] = {
val aud = Some(JwtUtil.getAudience(idToken).mkString(","))
val azp = getClaim(name = "azp", idToken = idToken)
val iss = getClaim(name = "iss", idToken = idToken)
val sub = getClaim(name = "sub", idToken = idToken)
val email = getClaim(name = "email", idToken = idToken)
val name = getClaim(name = "name", idToken = idToken)
val name = getClaim(name = "name", idToken = idToken).orElse(description)
Consumers.consumers.vend.getOrCreateConsumer(
consumerId = None,
key = Some(Helpers.randomString(40).toLowerCase),
@ -356,19 +367,19 @@ object OAuth2Login extends RestHelper with MdcLoggable {
sub = sub,
Some(true),
name = name,
appType = None,
description = Some(OpenIdConnect.openIdConnect),
appType = Some(AppType.Confidential),
description = description,
developerEmail = email,
redirectURL = None,
createdByUserId = userId.toOption
)
}
def applyRules(value: String, cc: CallContext): (Box[User], Some[CallContext]) = {
validateIdToken(value) match {
def applyIdTokenRules(token: String, cc: CallContext): (Box[User], Some[CallContext]) = {
validateIdToken(token) match {
case Full(_) =>
val user = IdentityProviderCommon.getOrCreateResourceUser(value)
val consumer = IdentityProviderCommon.getOrCreateConsumer(value, user.map(_.userId))
val user = getOrCreateResourceUser(token)
val consumer = getOrCreateConsumer(token, user.map(_.userId), Some(OpenIdConnect.openIdConnect))
LoginAttempt.userIsLocked(user.map(_.provider).getOrElse(""), user.map(_.name).getOrElse("")) match {
case true => ((Failure(UsernameHasBeenLocked), Some(cc.copy(consumer = consumer))))
case false => (user, Some(cc.copy(consumer = consumer)))
@ -381,62 +392,61 @@ object OAuth2Login extends RestHelper with MdcLoggable {
(Failure(Oauth2IJwtCannotBeVerified), Some(cc))
}
}
def applyRulesFuture(value: String, cc: CallContext): Future[(Box[User], Some[CallContext])] = {
validateIdToken(value) match {
def applyIdTokenRulesFuture(value: String, cc: CallContext): Future[(Box[User], Some[CallContext])] = Future {
applyIdTokenRules(value, cc)
}
def applyAccessTokenRules(token: String, cc: CallContext): (Box[User], Some[CallContext]) = {
validateAccessToken(token) match {
case Full(_) =>
for {
user <- IdentityProviderCommon.getOrCreateResourceUserFuture(value)
consumer <- Future{IdentityProviderCommon.getOrCreateConsumer(value, user.map(_.userId))}
} yield {
LoginAttempt.userIsLocked(user.map(_.provider).getOrElse(""), user.map(_.name).getOrElse("")) match {
case true => ((Failure(UsernameHasBeenLocked), Some(cc.copy(consumer = consumer))))
case false => (user, Some(cc.copy(consumer = consumer)))
}
val user = getOrCreateResourceUser(token)
val consumer = getOrCreateConsumer(token, user.map(_.userId), Some("OAuth 2.0"))
LoginAttempt.userIsLocked(user.map(_.provider).getOrElse(""), user.map(_.name).getOrElse("")) match {
case true => ((Failure(UsernameHasBeenLocked), Some(cc.copy(consumer = consumer))))
case false => (user, Some(cc.copy(consumer = consumer)))
}
case ParamFailure(a, b, c, apiFailure : APIFailure) =>
Future((ParamFailure(a, b, c, apiFailure : APIFailure), Some(cc)))
case ParamFailure(a, b, c, apiFailure: APIFailure) =>
(ParamFailure(a, b, c, apiFailure: APIFailure), Some(cc))
case Failure(msg, t, c) =>
Future((Failure(msg, t, c), Some(cc)))
(Failure(msg, t, c), Some(cc))
case _ =>
Future((Failure(Oauth2IJwtCannotBeVerified), Some(cc)))
(Failure(Oauth2IJwtCannotBeVerified), Some(cc))
}
}
}
object IdentityProviderCommon extends OAuth2Util {
override def wellKnownOpenidConfiguration: URI = new URI("")
override def urlOfJwkSets: Box[String] = checkUrlOfJwkSets(identityProvider = "common")
def applyAccessTokenRulesFuture(value: String, cc: CallContext): Future[(Box[User], Some[CallContext])] = Future {
applyAccessTokenRules(value, cc)
}
}
object Google extends OAuth2Util {
val google = "google"
/**
* OpenID Connect Discovery.
* Google exposes OpenID Connect discovery documents ( https://YOUR_DOMAIN/.well-known/openid-configuration ).
* Google exposes OpenID Connect discovery documents ( https://YOUR_DOMAIN/.well-known/openid-configuration ).
* These can be used to automatically configure applications.
*/
override def wellKnownOpenidConfiguration: URI = new URI("https://accounts.google.com/.well-known/openid-configuration")
override def urlOfJwkSets: Box[String] = checkUrlOfJwkSets(identityProvider = google)
def isIssuer(jwt: String): Boolean = isIssuer(jwtToken=jwt, identityProvider = google)
}
object Yahoo extends OAuth2Util {
val yahoo = "yahoo"
/**
* OpenID Connect Discovery.
* Yahoo exposes OpenID Connect discovery documents ( https://YOUR_DOMAIN/.well-known/openid-configuration ).
* Yahoo exposes OpenID Connect discovery documents ( https://YOUR_DOMAIN/.well-known/openid-configuration ).
* These can be used to automatically configure applications.
*/
override def wellKnownOpenidConfiguration: URI = new URI("https://login.yahoo.com/.well-known/openid-configuration")
override def urlOfJwkSets: Box[String] = checkUrlOfJwkSets(identityProvider = yahoo)
def isIssuer(jwt: String): Boolean = isIssuer(jwtToken=jwt, identityProvider = yahoo)
}
}
object Azure extends OAuth2Util {
val microsoft = "microsoft"
/**
* OpenID Connect Discovery.
* Yahoo exposes OpenID Connect discovery documents ( https://YOUR_DOMAIN/.well-known/openid-configuration ).
* Yahoo exposes OpenID Connect discovery documents ( https://YOUR_DOMAIN/.well-known/openid-configuration ).
* These can be used to automatically configure applications.
*/
override def wellKnownOpenidConfiguration: URI = new URI("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration")
@ -444,4 +454,30 @@ object OAuth2Login extends RestHelper with MdcLoggable {
def isIssuer(jwt: String): Boolean = isIssuer(jwtToken=jwt, identityProvider = microsoft)
}
object Keycloak extends OAuth2Util {
val keycloakHost = APIUtil.getPropsValue(nameOfProperty = "oauth2.keycloak.host", "http://localhost:7070")
/**
* OpenID Connect Discovery.
* Yahoo exposes OpenID Connect discovery documents ( https://YOUR_DOMAIN/.well-known/openid-configuration ).
* These can be used to automatically configure applications.
*/
override def wellKnownOpenidConfiguration: URI =
new URI(
APIUtil.getPropsValue(nameOfProperty = "oauth2.keycloak.well-known", "http://localhost:7070/realms/master/.well-known/openid-configuration")
)
override def urlOfJwkSets: Box[String] = checkUrlOfJwkSets(identityProvider = keycloakHost)
def isIssuer(jwt: String): Boolean = isIssuer(jwtToken=jwt, identityProvider = keycloakHost)
def applyRules(token: String, cc: CallContext): (Box[User], Some[CallContext]) = {
JwtUtil.getClaim("typ", token) match {
case "ID" => super.applyIdTokenRules(token, cc)
case "Bearer" => super.applyAccessTokenRules(token, cc)
case "" => super.applyAccessTokenRules(token, cc)
}
}
def applyRulesFuture(value: String, cc: CallContext): Future[(Box[User], Some[CallContext])] = Future {
applyRules(value, cc)
}
}
}

View File

@ -430,7 +430,12 @@ object MappedConsumersProvider extends ConsumersProvider with MdcLoggable {
case None =>
}
name match {
case Some(v) => c.name(v)
case Some(v) =>
val count = Consumer.findAll(By(Consumer.name, v)).size
if (count == 0)
c.name(v)
else
c.name(v + "_" + Helpers.randomString(10).toLowerCase)
case None =>
}
appType match {

View File

@ -34,12 +34,14 @@ object HydraUtil extends MdcLoggable{
val hydraSupportedTokenEndpointAuthMethods =
APIUtil.getPropsValue("hydra_supported_token_endpoint_auth_methods", "client_secret_basic,client_secret_post,private_key_jwt")
lazy val hydraPublicUrl = APIUtil.getPropsValue("hydra_public_url")
lazy val hydraPublicUrl: String = APIUtil.getPropsValue("hydra_public_url")
.openOrThrowException(s"If props $INTEGRATE_WITH_HYDRA is true, hydra_public_url value should not be blank")
// This method is a regular expression operation that removes a trailing slash (/) from a string if one is present.
.replaceFirst("/$", "")
lazy val hydraAdminUrl = APIUtil.getPropsValue("hydra_admin_url")
lazy val hydraAdminUrl: String = APIUtil.getPropsValue("hydra_admin_url")
.openOrThrowException(s"If props $INTEGRATE_WITH_HYDRA is true, hydra_admin_url value should not be blank")
// This method is a regular expression operation that removes a trailing slash (/) from a string if one is present.
.replaceFirst("/$", "")
lazy val hydraConsents = APIUtil.getPropsValue("hydra_consents")

View File

@ -0,0 +1,79 @@
package code.container
import code.api.v5_0_0.V500ServerSetup
import code.setup.DefaultUsers
import org.apache.kafka.clients.consumer.{ConsumerConfig, KafkaConsumer}
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerConfig, ProducerRecord}
import org.apache.kafka.common.serialization.{StringDeserializer, StringSerializer}
import org.scalatest.Ignore
import org.testcontainers.kafka.KafkaContainer
import java.util.{Collections, Properties}
import scala.jdk.CollectionConverters._
@Ignore
class EmbeddedKafka extends V500ServerSetup with DefaultUsers {
val kafkaContainer: KafkaContainer = new KafkaContainer("apache/kafka-native:3.8.0")
// It registers a shutdown hook, which is a block of code (or function) that runs when the application terminates,
// - either normally(e.g., when the main method completes)
// - or due to an external signal(e.g., Ctrl + C or termination by the operating system).
sys.addShutdownHook {
kafkaContainer.stop()
}
override def beforeAll(): Unit = {
super.beforeAll()
// Start RabbitMQ container
kafkaContainer.start()
}
override def afterAll(): Unit = {
super.afterAll()
kafkaContainer.stop()
}
feature(s"test EmbeddedKafka") {
scenario("Publish and Consume Message") {
val bootstrapServers: String = kafkaContainer.getBootstrapServers
// Kafka producer properties
val producerProps = new Properties()
producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers)
producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer].getName)
producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer].getName)
// Kafka consumer properties
val consumerProps = new Properties()
consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers)
consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, "test-group")
consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, classOf[StringDeserializer].getName)
consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, classOf[StringDeserializer].getName)
consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest")
// Create a producer
val producer = new KafkaProducer[String, String](producerProps)
val topic = "test-topic"
val key = "test-key"
val value = "Hello, Kafka!"
// Produce a message
producer.send(new ProducerRecord[String, String](topic, key, value))
producer.close()
// Create a consumer
val consumer = new KafkaConsumer[String, String](consumerProps)
consumer.subscribe(Collections.singletonList(topic))
// Consume the message
val records = consumer.poll(5000L)
consumer.close()
val messages = records.asScala.map(record => record.value())
messages should contain(value)
}
}
}

View File

@ -0,0 +1,67 @@
package code.container
import code.api.v5_0_0.V500ServerSetup
import code.setup.DefaultUsers
import com.rabbitmq.client.{Channel, Connection, ConnectionFactory}
import org.scalatest.Ignore
import org.testcontainers.containers.RabbitMQContainer
@Ignore
class EmbeddedRabbitMQ extends V500ServerSetup with DefaultUsers {
val rabbitMQContainer = new RabbitMQContainer("rabbitmq:3.7.25-management-alpine")
// It registers a shutdown hook, which is a block of code (or function) that runs when the application terminates,
// - either normally(e.g., when the main method completes)
// - or due to an external signal(e.g., Ctrl + C or termination by the operating system).
sys.addShutdownHook {
rabbitMQContainer.stop()
}
override def beforeAll(): Unit = {
super.beforeAll()
// Start RabbitMQ container
rabbitMQContainer.start()
}
override def afterAll(): Unit = {
super.afterAll()
rabbitMQContainer.stop()
}
feature(s"test EmbeddedRabbitMQ") {
scenario("Publish and Consume Message") {
val rabbitHost = rabbitMQContainer.getHost
val rabbitPort = rabbitMQContainer.getAmqpPort
// Set up RabbitMQ connection
val factory = new ConnectionFactory()
factory.setHost(rabbitHost)
factory.setPort(rabbitPort)
val connection: Connection = factory.newConnection()
val channel: Channel = connection.createChannel()
// Declare a queue
val queueName = "test-queue"
channel.queueDeclare(queueName, false, false, false, null)
// Publish a message
val message = "Hello, RabbitMQ!"
channel.basicPublish("", queueName, null, message.getBytes)
println(s"Published message: $message")
// Consume the message
val delivery = channel.basicGet(queueName, true)
val consumedMessage = new String(delivery.getBody)
println(s"Consumed message: $consumedMessage")
consumedMessage shouldBe message
// Clean up
channel.close()
connection.close()
}
}
}