From 054987944d91fe2021092c30b337e880421533ff Mon Sep 17 00:00:00 2001 From: Arnab Dutta Date: Wed, 16 Nov 2022 13:53:44 +0530 Subject: [PATCH] feat: add feature to include custom-claims in user-info endpoint of admin-ui plugin #2969 (#2970) * feat: add feature to include custom-claims in user-info endpoint of admin-ui plugin #2969 * feat: add feature to include custom-claims in user-info endpoint of admin-ui plugin #2969 * feat: add feature to include custom-claims in user-info endpoint of admin-ui plugin #2969 * feat: add feature to include custom-claims in user-info endpoint of admin-ui plugin #2969 --- .../introspection_github_claims.py | 87 ++++++ .../GithubExternalAuthenticator.py | 255 ++++++++++++++++++ .../adminui/model/auth/UserInfoResponse.java | 4 + .../adminui/service/auth/OAuth2Service.java | 26 +- .../jans_setup/templates/scripts.ldif | 34 +++ 5 files changed, 405 insertions(+), 1 deletion(-) create mode 100644 docs/script-catalog/introspection/introspection-github-claims/introspection_github_claims.py create mode 100644 docs/script-catalog/person_authentication/github-external-authenticator/GithubExternalAuthenticator.py diff --git a/docs/script-catalog/introspection/introspection-github-claims/introspection_github_claims.py b/docs/script-catalog/introspection/introspection-github-claims/introspection_github_claims.py new file mode 100644 index 00000000000..309d38b26a7 --- /dev/null +++ b/docs/script-catalog/introspection/introspection-github-claims/introspection_github_claims.py @@ -0,0 +1,87 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2019, Janssen +# +# + +from io.jans.model.custom.script.type.introspection import IntrospectionType +from io.jans.as.server.model.common import AuthorizationGrantList +from io.jans.as.server.service import SessionIdService +from io.jans.service.cdi.util import CdiUtil +from java.lang import String + +class Introspection(IntrospectionType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Github. Introspection script. Initializing ..." + print "Github. Introspection script. Initialized successfully" + + return True + + def destroy(self, configurationAttributes): + print "Github. Introspection script. Destroying ..." + print "Github. Introspection script. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + # Returns boolean, true - apply introspection method, false - ignore it. + # This method is called after introspection response is ready. This method can modify introspection response. + # Note : + # responseAsJsonObject - is org.codehaus.jettison.json.JSONObject, you can use any method to manipulate json + # context is reference of io.jans.as.service.external.context.ExternalIntrospectionContext (in https://github.com/JanssenFederation/oxauth project, ) + def modifyResponse(self, responseAsJsonObject, context): + print "Github. Checking for saved parameters in session ..." + try: + token = context.getHttpRequest().getParameter("token") + if token is None: + print "Github. Introspection. There is no token in request" + return True + + authorizationGrantList = CdiUtil.bean(AuthorizationGrantList) + authorizationGrant = authorizationGrantList.getAuthorizationGrantByAccessToken(token) + if authorizationGrant is None: + print "Github. Introspection. Failed to load authorization grant by token" + return False + + # Put user_id into response + responseAsJsonObject.accumulate("user_id", authorizationGrant.getUser().getUserId()) + + # Put custom parameters into response + sessionDn = authorizationGrant.getSessionDn(); + print "sessionDn '%s'" % sessionDn + if sessionDn is None: + print "There is no session" + return True + + sessionIdService = CdiUtil.bean(SessionIdService) + session = sessionIdService.getSessionByDn(sessionDn, False) + if sessionDn is None: + print "Github. Introspection. Failed to load session '%s'" % sessionDn + return False + + # Return session_id + responseAsJsonObject.accumulate("session_id", sessionDn) + + sessionAttributes = session.getSessionAttributes() + if sessionAttributes is None: + print "There is no session attributes" + return True + + # Append custom claims + customClaims = {} + + if sessionAttributes.containsKey("gihub_username"): + customClaims["gihub_username"] = sessionAttributes.get("gihub_username") + if sessionAttributes.containsKey("gihub_access_token"): + customClaims["gihub_access_token"] = sessionAttributes.get("gihub_access_token") + + responseAsJsonObject.accumulate("customClaims", customClaims) + except Exception as e: + print "Exception occured. Unable to resolve role/scope mapping." + print e + + return True + diff --git a/docs/script-catalog/person_authentication/github-external-authenticator/GithubExternalAuthenticator.py b/docs/script-catalog/person_authentication/github-external-authenticator/GithubExternalAuthenticator.py new file mode 100644 index 00000000000..9e0f259e585 --- /dev/null +++ b/docs/script-catalog/person_authentication/github-external-authenticator/GithubExternalAuthenticator.py @@ -0,0 +1,255 @@ +# Janssen Project software is available under the Apache 2.0 License (2004). See http://www.apache.org/licenses/ for full text. +# Copyright (c) 2020, Janssen Project +# +# Author: Yuriy Movchan +# + +from io.jans.as.common.model.common import User +from io.jans.as.model.jwt import Jwt +from io.jans.as.server.service import AuthenticationService +from io.jans.as.common.service.common import UserService +from io.jans.as.server.service.net import HttpService +from io.jans.as.server.security import Identity +from io.jans.as.server.util import ServerUtil +from io.jans.orm import PersistenceEntryManager +from io.jans.as.persistence.model.configuration import GluuConfiguration +from io.jans.model.custom.script.type.auth import PersonAuthenticationType +from io.jans.service.cdi.util import CdiUtil +from io.jans.util import StringHelper + +from io.jans.jsf2.service import FacesService +from java.util import Arrays, UUID + +import json +import sys +import datetime +import urllib + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "GitHub. Initialization" + print "GitHub. Initialized successfully" + + # read config from github_creds_file + github_creds_file = configurationAttributes.get("github_creds_file").getValue2() + f = open(github_creds_file, 'r') + try: + creds = json.loads(f.read()) + print creds + except: + print "GitHub: Initialization. Failed to load creds from file:", github_creds_file + print "Exception: ", sys.exc_info()[1] + return False + finally: + f.close() + + self.op_server = str(creds["op_server"]) + self.client_id = str(creds["client_id"]) + self.client_secret = str(creds["client_secret"]) + self.authorization_uri = str(creds["authorization_uri"]) + self.token_uri = str(creds["token_uri"]) + self.userinfo_uri = str(creds["userinfo_uri"]) + self.redirect_uri = str(creds["redirect_uri"]) + self.scope = str(creds["scope"]) + self.title = str(creds["title"]) + self.auto_redirect = creds["auto_redirect"] + + print "GitHub: Initialized successfully" + + return True + + def destroy(self, configurationAttributes): + print "GitHub. Destroy" + print "GitHub. Destroyed successfully" + return True + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def getApiVersion(self): + return 11 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + print "GitHub: authenticate called for step %s" % str(step) + identity = CdiUtil.bean(Identity) + authenticationService = CdiUtil.bean(AuthenticationService) + + if step == 1: + # Get Access Token + tokenResponse = self.getToken(requestParameters) + if tokenResponse is None: + return False + + # Get User Info + userInfo = self.getUserInfo(tokenResponse["access_token"]) + foundUser = self.addUser(userInfo) + if foundUser is None: + return False + + identity.setWorkingParameter("gihub_username", userInfo["login"]) + identity.setWorkingParameter("gihub_access_token", tokenResponse["access_token"]) + + print "GitHub: Successfully authenticated" + + loggedIn = authenticationService.authenticate(foundUser.getUserId()) + print "GitHub: Authentication: %s" % str(loggedIn) + return loggedIn + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "GitHub: prepareForStep called for step %s" % str(step) + if step == 1: + # redirect to external OIDC server + + redirect_url_elements = [self.authorization_uri, + "?response_type=code id_token", + "&client_id=", self.client_id, + "&scope=", self.scope, + "&redirect_uri=", self.redirect_uri] + redirect_url = "".join(redirect_url_elements) + + identity = CdiUtil.bean(Identity) + + if self.auto_redirect: + facesService = CdiUtil.bean(FacesService) + facesService.redirectToExternalURL(redirect_url) + else: + identity.setWorkingParameter("oidc_redirect_uri", redirect_url) + identity.setWorkingParameter("oidc_title", self.title) + + return True + + def getExtraParametersForStep(self, configurationAttributes, step): + print "GitHub: getExtraParametersForStep called for step %s" % str(step) + return Arrays.asList("gihub_username", "gihub_access_token") + + def getCountAuthenticationSteps(self, configurationAttributes): + print "GitHub: getCountAuthenticationSteps called" + return 1 + + def getPageForStep(self, configurationAttributes, step): + print "GitHub: getPageForStep called for step %s" % str(step) + if(step == 1): + return "/auth/github/github.xhtml" + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + print "GitHub: getNextStep called for step %s" % str(step) + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "GitHub: Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + def generalLogin(self, identity, authenticationService): + print "GitHub: general login" + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + logged_in = authenticationService.authenticate(user_name, user_password) + + return logged_in + + def getLocalPrimaryKey(self): + entryManager = CdiUtil.bean(PersistenceEntryManager) + config = GluuConfiguration() + config = entryManager.find(config.getClass(), "ou=configuration,o=jans") + # Pick (one) attribute where user id is stored (e.g. uid/mail) + # primaryKey is the primary key on the backend AD / LDAP Server + # localPrimaryKey is the primary key on Janssen. This attr value has been mapped with the primary key attr of the backend AD / LDAP when configuring cache refresh + uid_attr = config.getIdpAuthn().get(0).getConfig().findValue("localPrimaryKey").asText() + print "GitHub: init. uid attribute is '%s'" % uid_attr + return uid_attr + + def getToken(self, requestParameters): + print "GitHub: Get Access Token" + oidcCode = ServerUtil.getFirstValue(requestParameters, "code") + httpService = CdiUtil.bean(HttpService) + httpclient = httpService.getHttpsClient() + tokenRequestData = urllib.urlencode({ + "code" : oidcCode, + "grant_type" : "authorization_code", + "redirect_uri": self.redirect_uri, + "client_id": self.client_id, + "client_secret": self.client_secret + }) + + tokenRequestHeaders = { "Content-type" : "application/x-www-form-urlencoded", "Accept" : "application/json" } + + resultResponse = httpService.executePost(httpclient, self.token_uri, None, tokenRequestHeaders, tokenRequestData) + httpResponse = resultResponse.getHttpResponse() + httpResponseStatusCode = httpResponse.getStatusLine().getStatusCode() + print "OIDC: token response status code: %s" % httpResponseStatusCode + if str(httpResponseStatusCode) != "200": + print "OIDC: Failed to get token, status code %s" % httpResponseStatusCode + return None + + responseBytes = httpService.getResponseContent(httpResponse) + responseString = httpService.convertEntityToString(responseBytes) + tokenResponse = json.loads(responseString) + + return tokenResponse + + def addUser(self, user): + try: + print "GitHub: Adding user" + userId = user["login"] + userService = CdiUtil.bean(UserService) + foundUser = userService.getUserByAttribute("jansExtUid", "github:"+userId) + + if foundUser is None: + print "GitHub: User not found, adding new" + foundUser = User() + foundUser.setAttribute("jansExtUid", "github:"+userId) + foundUser.setAttribute("jansEmail", user["email"]) + foundUser.setAttribute("mail", user["email"]) + foundUser.setAttribute("displayName", "github:"+userId) + foundUser.setAttribute("givenName", "github:"+userId) + foundUser.setAttribute(self.getLocalPrimaryKey(), userId) + foundUser = userService.addUser(foundUser, True) + + return foundUser + except Exception as e: + print e + print "GitHub: Add user Exception: ", sys.exc_info()[1] + return None + + def getUserInfo(self, accessToken): + try: + print "GitHub: Get Userinfo" + httpService = CdiUtil.bean(HttpService) + httpclient = httpService.getHttpsClient() + tokenRequestHeaders = { "Authorization" : "Bearer %s" % accessToken, "Accept" : "application/json" } + + resultResponse = httpService.executeGet(httpclient, self.userinfo_uri, tokenRequestHeaders) + httpResponse = resultResponse.getHttpResponse() + httpResponseStatusCode = httpResponse.getStatusLine().getStatusCode() + print "GitHub: userinfo response status code: %s" % httpResponseStatusCode + if str(httpResponseStatusCode) != "200": + print "GitHub: Failed to get userinfo, status code %s" % httpResponseStatusCode + return None + + responseBytes = httpService.getResponseContent(httpResponse) + responseString = httpService.convertEntityToString(responseBytes) + userinfoResponse = json.loads(responseString) + + print userinfoResponse + + return userinfoResponse + except Exception as e: + print e + return None \ No newline at end of file diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/model/auth/UserInfoResponse.java b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/model/auth/UserInfoResponse.java index 64e3dabf4d0..06be3756e31 100644 --- a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/model/auth/UserInfoResponse.java +++ b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/model/auth/UserInfoResponse.java @@ -14,6 +14,10 @@ public void setClaims(Map claims) { this.claims = claims; } + public void addClaims(String key, Object value) { + this.claims.put(key, value); + } + public String getJwtUserInfo() { return jwtUserInfo; } diff --git a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/service/auth/OAuth2Service.java b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/service/auth/OAuth2Service.java index a979cdc729e..737da21daa1 100644 --- a/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/service/auth/OAuth2Service.java +++ b/jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/service/auth/OAuth2Service.java @@ -134,6 +134,26 @@ public TokenResponse getApiProtectionToken(String userInfoJwt) throws Applicatio } } + public Map introspectToken(String accessToken) { + log.info("Token introspection from auth-server."); + AUIConfiguration auiConfiguration = auiConfigurationService.getAUIConfiguration(); + Invocation.Builder request = ClientFactory.instance().getClientBuilder(auiConfiguration.getAuthServerIntrospectionEndpoint()); + request.header("Authorization", "Bearer " + accessToken); + + MultivaluedMap body = new MultivaluedHashMap<>(); + body.putSingle("token", accessToken); + + Response response = request.post(Entity.form(body)); + + log.info("Introspection response status code: {}", response.getStatus()); + + if (response.getStatus() == 200) { + Map entity = response.readEntity(Map.class); + log.info("Introspection response entity: {}", entity); + return entity; + } + return null; + } public UserInfoResponse getUserInfo(UserInfoRequest userInfoRequest) throws ApplicationException { try { log.debug("Getting User-Info from auth-server: {}", userInfoRequest.getAccessToken()); @@ -151,6 +171,8 @@ public UserInfoResponse getUserInfo(UserInfoRequest userInfoRequest) throws Appl accessToken = tokenResponse.getAccessToken(); } log.debug("Access Token : {}", accessToken); + Map introspectionResponse = introspectToken(accessToken); + MultivaluedMap body = new MultivaluedHashMap<>(); body.putSingle("access_token", accessToken); @@ -172,9 +194,11 @@ public UserInfoResponse getUserInfo(UserInfoRequest userInfoRequest) throws Appl UserInfoResponse userInfoResponse = new UserInfoResponse(); userInfoResponse.setClaims(getClaims(jwtUserInfo)); userInfoResponse.setJwtUserInfo(entity); + if(introspectionResponse.get("customClaims") != null) { + userInfoResponse.addClaims("customClaims", introspectionResponse.get("customClaims")); + } log.debug("User-Info response userInfoResponse: {}", userInfoResponse); - return userInfoResponse; } diff --git a/jans-linux-setup/jans_setup/templates/scripts.ldif b/jans-linux-setup/jans_setup/templates/scripts.ldif index 2500de9a00c..292e06095a9 100644 --- a/jans-linux-setup/jans_setup/templates/scripts.ldif +++ b/jans-linux-setup/jans_setup/templates/scripts.ldif @@ -577,3 +577,37 @@ jansScr::%(update_token_updatetoken)s jansScrTyp: update_token jansProgLng: python +dn: inum=E706-F8A8,ou=scripts,o=jans +objectClass: top +objectClass: jansCustomScr +description: GitHub Inbound Authentication script +displayName: github +inum: E706-F8A8 +jansConfProperty: {"value1":"hide","value2":"true","hide":false,"description":""} +jansConfProperty: {"value1":"github_creds_file","value2":"/opt/github.json","hide":false,"description":""} +jansConfProperty: {"value1":"description","value2":"GitHub Login","hide":false,"description":""} +jansEnabled: true +jansLevel: 10 +jansModuleProperty: {"value1":"location_type","value2":"ldap","description":""} +jansModuleProperty: {"value1":"usage_type","value2":"interactive","description":""} +jansProgLng: python +jansRevision: 0 +jansScr::%(person_authentication_github_external_authenticator_githubexternalauthenticator)s +jansScrTyp: person_authentication + +dn: inum=8333-DFBC,ou=scripts,o=jans +objectClass: jansCustomScr +objectClass: top +description: Add GitHub claims to introspection response +displayName: github-claims-introspection-script +inum: 8333-DFBC +jansEnabled: true +jansLevel: 10 +jansModuleProperty: {"value1":"location_type","value2":"ldap","description":""} +jansProgLng: python +jansRevision: 0 +jansScr::%(introspection_introspection_github_claims_introspection_github_claims)s +jansScrTyp: introspection + + +