Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

419 authenticator against keycloak #420

Merged
merged 11 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Extractors can now specify an extractor_key and an owner (email address) when sending a
registration or heartbeat to Clowder that will restrict use of that extractor to them.
- Added a dropdown menu to select all spaces, your spaces and also the spaces you have access to. [#374](https://github.com/clowder-framework/clowder/issues/374)
- Keycloak provider with secure social [#419](https://github.com/clowder-framework/clowder/issues/419)

## Fixed
- Updated lastModifiesDate when updating file or metadata to a dataset, added lastModified to UI [386](https://github.com/clowder-framework/clowder/issues/386)
Expand Down
103 changes: 103 additions & 0 deletions app/services/KeycloakProvider.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package services

import play.api.libs.ws.WS
import play.api.{Application, Logger}
import play.api.libs.json.JsObject
import securesocial.core._
import scala.collection.JavaConverters._


/**
* A Keycloak OAuth2 Provider
*/
class KeycloakProvider(application: Application) extends OAuth2Provider(application) {
val Error = "error"
val Message = "message"
val Type = "type"
val Sub = "sub"
val Name = "name"
val GivenName = "given_name"
val FamilyName = "family_name"
// todo: picture wont work
val Picture = "picture"
val Email = "email"
val Groups = "groups"

override def id = KeycloakProvider.Keycloak

def fillProfile(user: SocialUser): SocialUser = {
val UserInfoApi = loadProperty("userinfoUrl").getOrElse(throwMissingPropertiesException())
val accessToken = user.oAuth2Info.get.accessToken
val promise = WS.url(UserInfoApi.toString).withHeaders(("Authorization", "Bearer " + accessToken)).get()

try {
val response = awaitResult(promise)
val me = response.json
Logger.debug("Got back from Keycloak : " + me.toString())
(me \ Error).asOpt[JsObject] match {
case Some(error) =>
val message = (error \ Message).as[String]
val errorType = ( error \ Type).as[String]
Logger.error("[securesocial] error retrieving profile information from Keycloak. Error type = %s, message = %s"
.format(errorType,message))
throw new AuthenticationException()
case _ =>
val userId = (me \ Sub).as[String]
val firstName = (me \ GivenName).asOpt[String]
val lastName = (me \ FamilyName).asOpt[String]
val fullName = (me \ Name).asOpt[String]
val avatarUrl = ( me \ Picture).asOpt[String]
val email = ( me \ Email).asOpt[String]
val groups = ( me \ Groups).asOpt[List[String]]
robkooper marked this conversation as resolved.
Show resolved Hide resolved
val roles = ( me \ "resource_access" \ "account" \ "roles").asOpt[List[String]]
(application.configuration.getList("securesocial.keycloak.groups"), groups) match {
case (Some(conf), Some(keycloak)) => {
val conflist = conf.unwrapped().asScala.toList
if (keycloak.intersect(conflist).isEmpty) {
throw new AuthenticationException()
}
}
case (Some(_), None) => throw new AuthenticationException()
case (None, _) => Logger.debug("[securesocial] No check needed for groups")
}
(application.configuration.getList("securesocial.keycloak.roles"), roles) match {
case (Some(conf), Some(keycloak)) => {
val conflist = conf.unwrapped().asScala.toList
if (keycloak.intersect(conflist).isEmpty) {
throw new AuthenticationException()
}
}
case (Some(_), None) => throw new AuthenticationException()
case (None, _) => Logger.debug("[securesocial] No check needed for roles")
}
user.copy(
identityId = IdentityId(userId, id),
firstName = firstName.getOrElse(""),
lastName = lastName.getOrElse(""),
fullName = fullName.getOrElse({
if (firstName.isDefined && lastName.isDefined) {
firstName.get + " " + lastName.get
} else if (firstName.isDefined) {
firstName.get
} else if (lastName.isDefined) {
lastName.get
} else {
""
}
}),
avatarUrl = avatarUrl,
email = email
)
}
} catch {
case e: Exception => {
Logger.error( "[securesocial] error retrieving profile information from Keycloak", e)
throw new AuthenticationException()
}
}
}
}

object KeycloakProvider {
val Keycloak = "keycloak"
}
1 change: 1 addition & 0 deletions conf/play.plugins
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#10050:services.CrowdProvider
#10051:services.CILogonProvider
#10052:services.LdapProvider
#10053:services.KeycloakProvider
#10090:services.MailerPlugin
#10091:services.AdminsNotifierPlugin
#10100:services.TempFilesPlugin
Expand Down
9 changes: 9 additions & 0 deletions conf/securesocial.conf
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,15 @@ securesocial {
#groups=["cn=org_isda,ou=Groups,dc=ncsa,dc=illinois,dc=edu"]
}

keycloak {
authorizationUrl="http://localhost:8080/keycloak/realms/clowder/protocol/openid-connect/auth"
accessTokenUrl="http://localhost:8080/keycloak/realms/clowder/protocol/openid-connect/token"
userinfoUrl="http://localhost:8080/keycloak/realms/clowder/protocol/openid-connect/userinfo"
clientId=your_client_id
clientSecret=your_client_secret
scope="profile email roles"
}
longshuicy marked this conversation as resolved.
Show resolved Hide resolved

ldap {
url="http://localhost/ldap"
hostname="ldap.example.com"
Expand Down
Binary file added public/securesocial/images/providers/keycloak.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.