Skip to content

Commit 5d7c19e

Browse files
longshuicylmarini
andauthored
419 authenticator against keycloak (#420)
* follow cilogon example * add keycloak * keycloak is working now * changelog * revert accidental change * replace the default conf to clowder * typo * use the correct group * check roles also * provide examples of filtering by grouops and roles --------- Co-authored-by: Luigi Marini <[email protected]>
1 parent 2d8b929 commit 5d7c19e

File tree

5 files changed

+117
-0
lines changed

5 files changed

+117
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1313
- Extractors can now specify an extractor_key and an owner (email address) when sending a
1414
registration or heartbeat to Clowder that will restrict use of that extractor to them.
1515
- 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)
16+
- Keycloak provider with secure social [#419](https://github.com/clowder-framework/clowder/issues/419)
1617
- Documentation on how to do easy testing of pull requests
1718

1819
## Fixed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package services
2+
3+
import play.api.libs.ws.WS
4+
import play.api.{Application, Logger}
5+
import play.api.libs.json.JsObject
6+
import securesocial.core._
7+
import scala.collection.JavaConverters._
8+
9+
10+
/**
11+
* A Keycloak OAuth2 Provider
12+
*/
13+
class KeycloakProvider(application: Application) extends OAuth2Provider(application) {
14+
val Error = "error"
15+
val Message = "message"
16+
val Type = "type"
17+
val Sub = "sub"
18+
val Name = "name"
19+
val GivenName = "given_name"
20+
val FamilyName = "family_name"
21+
// todo: picture wont work
22+
val Picture = "picture"
23+
val Email = "email"
24+
val Groups = "groups"
25+
26+
override def id = KeycloakProvider.Keycloak
27+
28+
def fillProfile(user: SocialUser): SocialUser = {
29+
val UserInfoApi = loadProperty("userinfoUrl").getOrElse(throwMissingPropertiesException())
30+
val accessToken = user.oAuth2Info.get.accessToken
31+
val promise = WS.url(UserInfoApi.toString).withHeaders(("Authorization", "Bearer " + accessToken)).get()
32+
33+
try {
34+
val response = awaitResult(promise)
35+
val me = response.json
36+
Logger.debug("Got back from Keycloak : " + me.toString())
37+
(me \ Error).asOpt[JsObject] match {
38+
case Some(error) =>
39+
val message = (error \ Message).as[String]
40+
val errorType = ( error \ Type).as[String]
41+
Logger.error("[securesocial] error retrieving profile information from Keycloak. Error type = %s, message = %s"
42+
.format(errorType,message))
43+
throw new AuthenticationException()
44+
case _ =>
45+
val userId = (me \ Sub).as[String]
46+
val firstName = (me \ GivenName).asOpt[String]
47+
val lastName = (me \ FamilyName).asOpt[String]
48+
val fullName = (me \ Name).asOpt[String]
49+
val avatarUrl = ( me \ Picture).asOpt[String]
50+
val email = ( me \ Email).asOpt[String]
51+
val groups = ( me \ Groups).asOpt[List[String]]
52+
val roles = ( me \ "resource_access" \ "account" \ "roles").asOpt[List[String]]
53+
(application.configuration.getList("securesocial.keycloak.groups"), groups) match {
54+
case (Some(conf), Some(keycloak)) => {
55+
val conflist = conf.unwrapped().asScala.toList
56+
if (keycloak.intersect(conflist).isEmpty) {
57+
throw new AuthenticationException()
58+
}
59+
}
60+
case (Some(_), None) => throw new AuthenticationException()
61+
case (None, _) => Logger.debug("[securesocial] No check needed for groups")
62+
}
63+
(application.configuration.getList("securesocial.keycloak.roles"), roles) match {
64+
case (Some(conf), Some(keycloak)) => {
65+
val conflist = conf.unwrapped().asScala.toList
66+
if (keycloak.intersect(conflist).isEmpty) {
67+
throw new AuthenticationException()
68+
}
69+
}
70+
case (Some(_), None) => throw new AuthenticationException()
71+
case (None, _) => Logger.debug("[securesocial] No check needed for roles")
72+
}
73+
user.copy(
74+
identityId = IdentityId(userId, id),
75+
firstName = firstName.getOrElse(""),
76+
lastName = lastName.getOrElse(""),
77+
fullName = fullName.getOrElse({
78+
if (firstName.isDefined && lastName.isDefined) {
79+
firstName.get + " " + lastName.get
80+
} else if (firstName.isDefined) {
81+
firstName.get
82+
} else if (lastName.isDefined) {
83+
lastName.get
84+
} else {
85+
""
86+
}
87+
}),
88+
avatarUrl = avatarUrl,
89+
email = email
90+
)
91+
}
92+
} catch {
93+
case e: Exception => {
94+
Logger.error( "[securesocial] error retrieving profile information from Keycloak", e)
95+
throw new AuthenticationException()
96+
}
97+
}
98+
}
99+
}
100+
101+
object KeycloakProvider {
102+
val Keycloak = "keycloak"
103+
}

conf/play.plugins

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#10050:services.CrowdProvider
2222
#10051:services.CILogonProvider
2323
#10052:services.LdapProvider
24+
#10053:services.KeycloakProvider
2425
#10090:services.MailerPlugin
2526
#10091:services.AdminsNotifierPlugin
2627
#10100:services.TempFilesPlugin

conf/securesocial.conf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,18 @@ securesocial {
161161
#groups=["cn=org_isda,ou=Groups,dc=ncsa,dc=illinois,dc=edu"]
162162
}
163163

164+
keycloak {
165+
authorizationUrl="http://localhost:8080/keycloak/realms/clowder/protocol/openid-connect/auth"
166+
accessTokenUrl="http://localhost:8080/keycloak/realms/clowder/protocol/openid-connect/token"
167+
userinfoUrl="http://localhost:8080/keycloak/realms/clowder/protocol/openid-connect/userinfo"
168+
clientId=your_client_id
169+
clientSecret=your_client_secret
170+
scope="profile email roles"
171+
# Example of filtering by groups and/or roles
172+
# groups=["group1", "group2"]
173+
# roles=["role1", "role2"]
174+
}
175+
164176
ldap {
165177
url="http://localhost/ldap"
166178
hostname="ldap.example.com"
2.86 KB
Loading

0 commit comments

Comments
 (0)