KEYCLOAK-17284 Evaluate ID-Token and UserInfo-Endpoint:
- add additional REST endpoints for evaluation: - for ID Token: GET /realm/clients/id/evaluate-scopes/generate-example-id-token - for UserInfo-Endpoint: GET /realm/clients/id/evaluate-scopes/generate-example-userinfo - extend UI: add additional tabs "Generated ID Token" and "Generated User Info" to the client scopes evaluation screen Co-authored-by: Daniel Fesenmeyer <daniel.fesenmeyer@bosch.io>
This commit is contained in:
parent
65c48a4183
commit
b75648bda2
7 changed files with 194 additions and 77 deletions
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.keycloak.protocol.oidc;
|
||||
|
||||
import java.util.HashMap;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
|
@ -657,6 +658,31 @@ public class TokenManager {
|
|||
return finalToken.get();
|
||||
}
|
||||
|
||||
public Map<String, Object> generateUserInfoClaims(AccessToken userInfo, UserModel userModel) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("sub", userModel.getId());
|
||||
claims.putAll(userInfo.getOtherClaims());
|
||||
|
||||
if (userInfo.getRealmAccess() != null) {
|
||||
Map<String, Set<String>> realmAccess = new HashMap<>();
|
||||
realmAccess.put("roles", userInfo.getRealmAccess().getRoles());
|
||||
claims.put("realm_access", realmAccess);
|
||||
}
|
||||
|
||||
if (userInfo.getResourceAccess() != null && !userInfo.getResourceAccess().isEmpty()) {
|
||||
Map<String, Map<String, Set<String>>> resourceAccessMap = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, AccessToken.Access> resourceAccessMapEntry : userInfo.getResourceAccess()
|
||||
.entrySet()) {
|
||||
Map<String, Set<String>> resourceAccess = new HashMap<>();
|
||||
resourceAccess.put("roles", resourceAccessMapEntry.getValue().getRoles());
|
||||
resourceAccessMap.put(resourceAccessMapEntry.getKey(), resourceAccess);
|
||||
}
|
||||
claims.put("resource_access", resourceAccessMap);
|
||||
}
|
||||
return claims;
|
||||
}
|
||||
|
||||
public void transformIDToken(KeycloakSession session, IDToken token,
|
||||
UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
|
||||
|
||||
|
|
|
@ -69,9 +69,7 @@ import javax.ws.rs.core.HttpHeaders;
|
|||
import javax.ws.rs.core.Response;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author pedroigor
|
||||
|
@ -229,28 +227,7 @@ public class UserInfoEndpoint {
|
|||
AccessToken userInfo = new AccessToken();
|
||||
|
||||
tokenManager.transformUserInfoAccessToken(session, userInfo, userSession, clientSessionCtx);
|
||||
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("sub", userModel.getId());
|
||||
claims.putAll(userInfo.getOtherClaims());
|
||||
|
||||
if (userInfo.getRealmAccess() != null) {
|
||||
Map<String, Set<String>> realmAccess = new HashMap<>();
|
||||
realmAccess.put("roles", userInfo.getRealmAccess().getRoles());
|
||||
claims.put("realm_access", realmAccess);
|
||||
}
|
||||
|
||||
if (userInfo.getResourceAccess() != null && !userInfo.getResourceAccess().isEmpty()) {
|
||||
Map<String, Map<String, Set<String>>> resourceAccessMap = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, AccessToken.Access> resourceAccessMapEntry : userInfo.getResourceAccess()
|
||||
.entrySet()) {
|
||||
Map<String, Set<String>> resourceAccess = new HashMap<>();
|
||||
resourceAccess.put("roles", resourceAccessMapEntry.getValue().getRoles());
|
||||
resourceAccessMap.put(resourceAccessMapEntry.getKey(), resourceAccess);
|
||||
}
|
||||
claims.put("resource_access", resourceAccessMap);
|
||||
}
|
||||
Map<String, Object> claims = tokenManager.generateUserInfoClaims(userInfo, userModel);
|
||||
|
||||
Response.ResponseBuilder responseBuilder;
|
||||
OIDCAdvancedConfigWrapper cfg = OIDCAdvancedConfigWrapper.fromClientModel(clientModel);
|
||||
|
|
|
@ -19,7 +19,9 @@ package org.keycloak.services.resources.admin;
|
|||
|
||||
import static org.keycloak.protocol.ProtocolMapperUtils.isEnabled;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
|
@ -47,6 +49,7 @@ import org.keycloak.models.UserSessionModel;
|
|||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
|
@ -144,6 +147,55 @@ public class ClientScopeEvaluateResource {
|
|||
return rep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create JSON with payload of example user info
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Path("generate-example-userinfo")
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Map<String, Object> generateExampleUserinfo(@QueryParam("scope") String scopeParam, @QueryParam("userId") String userId) {
|
||||
auth.clients().requireView(client);
|
||||
|
||||
UserModel user = getUserModel(userId);
|
||||
|
||||
logger.debugf("generateExampleUserinfo invoked. User: %s", user.getUsername());
|
||||
|
||||
return sessionAware(user, scopeParam, (userSession, clientSessionCtx) -> {
|
||||
AccessToken userInfo = new AccessToken();
|
||||
TokenManager tokenManager = new TokenManager();
|
||||
|
||||
tokenManager.transformUserInfoAccessToken(session, userInfo, userSession, clientSessionCtx);
|
||||
return tokenManager.generateUserInfoClaims(userInfo, user);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create JSON with payload of example id token
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Path("generate-example-id-token")
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public IDToken generateExampleIdToken(@QueryParam("scope") String scopeParam, @QueryParam("userId") String userId) {
|
||||
auth.clients().requireView(client);
|
||||
|
||||
UserModel user = getUserModel(userId);
|
||||
|
||||
logger.debugf("generateExampleIdToken invoked. User: %s, Scope param: %s", user.getUsername(), scopeParam);
|
||||
|
||||
return sessionAware(user, scopeParam, (userSession, clientSessionCtx) ->
|
||||
{
|
||||
TokenManager tokenManager = new TokenManager();
|
||||
return tokenManager.responseBuilder(realm, client, null, session, userSession, clientSessionCtx)
|
||||
.generateAccessToken().generateIDToken().getIdToken();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create JSON with payload of example access token
|
||||
*
|
||||
|
@ -156,25 +208,20 @@ public class ClientScopeEvaluateResource {
|
|||
public AccessToken generateExampleAccessToken(@QueryParam("scope") String scopeParam, @QueryParam("userId") String userId) {
|
||||
auth.clients().requireView(client);
|
||||
|
||||
if (userId == null) {
|
||||
throw new NotFoundException("No userId provided");
|
||||
}
|
||||
|
||||
UserModel user = session.users().getUserById(realm, userId);
|
||||
if (user == null) {
|
||||
throw new NotFoundException("No user found");
|
||||
}
|
||||
UserModel user = getUserModel(userId);
|
||||
|
||||
logger.debugf("generateExampleAccessToken invoked. User: %s, Scope param: %s", user.getUsername(), scopeParam);
|
||||
|
||||
AccessToken token = generateToken(user, scopeParam);
|
||||
return token;
|
||||
return sessionAware(user, scopeParam, (userSession, clientSessionCtx) ->
|
||||
{
|
||||
TokenManager tokenManager = new TokenManager();
|
||||
return tokenManager.responseBuilder(realm, client, null, session, userSession, clientSessionCtx)
|
||||
.generateAccessToken().getAccessToken();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private AccessToken generateToken(UserModel user, String scopeParam) {
|
||||
private<R> R sessionAware(UserModel user, String scopeParam, BiFunction<UserSessionModel, ClientSessionContext,R> function) {
|
||||
AuthenticationSessionModel authSession = null;
|
||||
UserSessionModel userSession = null;
|
||||
AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(session);
|
||||
|
||||
try {
|
||||
|
@ -186,18 +233,13 @@ public class ClientScopeEvaluateResource {
|
|||
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
|
||||
authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scopeParam);
|
||||
|
||||
userSession = session.sessions().createUserSession(authSession.getParentSession().getId(), realm, user, user.getUsername(),
|
||||
UserSessionModel userSession = session.sessions().createUserSession(authSession.getParentSession().getId(), realm, user, user.getUsername(),
|
||||
clientConnection.getRemoteAddr(), "example-auth", false, null, null, UserSessionModel.SessionPersistenceState.TRANSIENT);
|
||||
|
||||
AuthenticationManager.setClientScopesInSession(authSession);
|
||||
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(session, userSession, authSession);
|
||||
|
||||
TokenManager tokenManager = new TokenManager();
|
||||
|
||||
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, client, null, session, userSession, clientSessionCtx)
|
||||
.generateAccessToken();
|
||||
|
||||
return responseBuilder.getAccessToken();
|
||||
return function.apply(userSession, clientSessionCtx);
|
||||
|
||||
} finally {
|
||||
if (authSession != null) {
|
||||
|
@ -206,6 +248,17 @@ public class ClientScopeEvaluateResource {
|
|||
}
|
||||
}
|
||||
|
||||
private UserModel getUserModel(String userId) {
|
||||
if (userId == null) {
|
||||
throw new NotFoundException("No userId provided");
|
||||
}
|
||||
|
||||
UserModel user = session.users().getUserById(realm, userId);
|
||||
if (user == null) {
|
||||
throw new NotFoundException("No user found");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
public static class ProtocolMapperEvaluationRepresentation {
|
||||
|
||||
|
|
|
@ -1044,7 +1044,11 @@ client-scopes.evaluate.granted-realm-effective-roles=Granted Effective Realm Rol
|
|||
client-scopes.evaluate.granted-realm-effective-roles.tooltip=Client has scope mappings for these roles. Those roles will be in the access token issued to this client if the authenticated user is a member of them
|
||||
client-scopes.evaluate.granted-client-effective-roles=Granted Effective Client Roles
|
||||
generated-access-token=Generated Access Token
|
||||
generated-access-token.tooltip=See the example token, which will be generated and sent to the client when selected user is authenticated. You can see claims and roles that the token will contain based on the effective protocol mappers and role scope mappings and also based on the claims/roles assigned to user himself
|
||||
generated-access-token.tooltip=See the example access token, which will be generated and sent to the client when selected user is authenticated. You can see claims and roles that the token will contain based on the effective protocol mappers and role scope mappings and also based on the claims/roles assigned to user himself
|
||||
generated-id-token=Generated ID Token
|
||||
generated-id-token.tooltip=See the example ID Token, which will be generated and sent to the client when selected user is authenticated. You can see claims and roles that the token will contain based on the effective protocol mappers and role scope mappings and also based on the claims/roles assigned to user himself
|
||||
generated-user-info=Generated User Info
|
||||
generated-user-info.tooltip=See the example User Info, which will be provided by the User Info Endpoint
|
||||
|
||||
manage=Manage
|
||||
authentication=Authentication
|
||||
|
|
|
@ -2573,8 +2573,9 @@ module.controller('ClientClientScopesSetupCtrl', function($scope, realm, Realm,
|
|||
});
|
||||
|
||||
module.controller('ClientClientScopesEvaluateCtrl', function($scope, Realm, User, ClientEvaluateProtocolMappers, ClientEvaluateGrantedRoles,
|
||||
ClientEvaluateNotGrantedRoles, ClientEvaluateGenerateExampleToken, realm, client, clients, clientScopes, serverInfo,
|
||||
ComponentUtils, clientOptionalClientScopes, clientDefaultClientScopes, $route, $routeParams, $http, Notifications, $location,
|
||||
ClientEvaluateNotGrantedRoles, ClientEvaluateGenerateExampleAccessToken, ClientEvaluateGenerateExampleIDToken,
|
||||
ClientEvaluateGenerateExampleUserInfo, realm, client, clients, clientScopes, serverInfo, ComponentUtils,
|
||||
clientOptionalClientScopes, clientDefaultClientScopes, $route, $routeParams, $http, Notifications, $location,
|
||||
Client) {
|
||||
|
||||
console.log('ClientClientScopesEvaluateCtrl');
|
||||
|
@ -2610,6 +2611,8 @@ module.controller('ClientClientScopesEvaluateCtrl', function($scope, Realm, User
|
|||
$scope.notGrantedClientRoles = null;
|
||||
$scope.targetClient = null;
|
||||
$scope.oidcAccessToken = null;
|
||||
$scope.oidcIDToken = null;
|
||||
$scope.oidcUserInfo = null;
|
||||
|
||||
$scope.selectedTab = 0;
|
||||
}
|
||||
|
@ -2743,49 +2746,75 @@ module.controller('ClientClientScopesEvaluateCtrl', function($scope, Realm, User
|
|||
|
||||
// Send request for retrieve accessToken (in case user was selected)
|
||||
if (client.protocol === 'openid-connect' && $scope.userId != null && $scope.userId !== '') {
|
||||
var url = ClientEvaluateGenerateExampleToken.url({
|
||||
var exampleRequestParams = {
|
||||
realm: realm.realm,
|
||||
client: client.id,
|
||||
userId: $scope.userId,
|
||||
scopeParam: $scope.scopeParam
|
||||
};
|
||||
|
||||
var accessTokenUrl = ClientEvaluateGenerateExampleAccessToken.url(exampleRequestParams);
|
||||
getPrettyJsonResponse(accessTokenUrl).then(function (result) {
|
||||
$scope.oidcAccessToken = result;
|
||||
});
|
||||
|
||||
$http.get(url).then(function (response) {
|
||||
if (response.data) {
|
||||
var oidcAccessToken = angular.fromJson(response.data);
|
||||
oidcAccessToken = angular.toJson(oidcAccessToken, true);
|
||||
$scope.oidcAccessToken = oidcAccessToken;
|
||||
} else {
|
||||
$scope.oidcAccessToken = null;
|
||||
}
|
||||
var idTokenUrl = ClientEvaluateGenerateExampleIDToken.url(exampleRequestParams);
|
||||
getPrettyJsonResponse(idTokenUrl).then(function (result) {
|
||||
$scope.oidcIDToken = result;
|
||||
});
|
||||
|
||||
var userInfoUrl = ClientEvaluateGenerateExampleUserInfo.url(exampleRequestParams);
|
||||
getPrettyJsonResponse(userInfoUrl).then(function (result) {
|
||||
$scope.oidcUserInfo = result;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.showTab(1);
|
||||
};
|
||||
|
||||
function getPrettyJsonResponse(url) {
|
||||
return $http.get(url).then(function (response) {
|
||||
if (response.data) {
|
||||
var responseJson = angular.fromJson(response.data);
|
||||
return angular.toJson(responseJson, true);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.isResponseAvailable = function () {
|
||||
return $scope.protocolMappers != null;
|
||||
}
|
||||
|
||||
$scope.isTokenAvailable = function () {
|
||||
$scope.isAccessTokenAvailable = function () {
|
||||
return $scope.oidcAccessToken != null;
|
||||
}
|
||||
|
||||
$scope.isIDTokenAvailable = function () {
|
||||
return $scope.oidcIDToken != null;
|
||||
}
|
||||
|
||||
$scope.isUserInfoAvailable = function () {
|
||||
return $scope.oidcUserInfo != null;
|
||||
}
|
||||
|
||||
$scope.showTab = function (tab) {
|
||||
$scope.selectedTab = tab;
|
||||
|
||||
// Check if there is more clever way to do it... :/
|
||||
if (tab === 1) {
|
||||
$scope.tabCss = { tab1: 'active', tab2: '', tab3: '' }
|
||||
} else if (tab === 2) {
|
||||
$scope.tabCss = { tab1: '', tab2: 'active', tab3: '' }
|
||||
} else if (tab === 3) {
|
||||
$scope.tabCss = { tab1: '', tab2: '', tab3: 'active' }
|
||||
$scope.tabCss = {
|
||||
tab1: getTabCssClass(1, tab),
|
||||
tab2: getTabCssClass(2, tab),
|
||||
tab3: getTabCssClass(3, tab),
|
||||
tab4: getTabCssClass(4, tab),
|
||||
tab5: getTabCssClass(5, tab)
|
||||
}
|
||||
}
|
||||
|
||||
function getTabCssClass(tabNo, selectedTab) {
|
||||
return (tabNo === selectedTab) ? 'active' : '';
|
||||
}
|
||||
|
||||
$scope.protocolMappersShown = function () {
|
||||
return $scope.selectedTab === 1;
|
||||
}
|
||||
|
@ -2794,8 +2823,17 @@ module.controller('ClientClientScopesEvaluateCtrl', function($scope, Realm, User
|
|||
return $scope.selectedTab === 2;
|
||||
}
|
||||
|
||||
$scope.tokenShown = function () {
|
||||
return $scope.selectedTab === 3;
|
||||
$scope.exampleTabInfo = function() {
|
||||
switch ($scope.selectedTab) {
|
||||
case 3:
|
||||
return { isShown: true, value: $scope.oidcAccessToken}
|
||||
case 4:
|
||||
return { isShown: true, value: $scope.oidcIDToken}
|
||||
case 5:
|
||||
return { isShown: true, value: $scope.oidcUserInfo}
|
||||
default:
|
||||
return { isShown: false, value: null}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.sortMappersByPriority = function(mapper) {
|
||||
|
|
|
@ -1239,19 +1239,30 @@ module.factory('ClientEvaluateNotGrantedRoles', function($resource) {
|
|||
});
|
||||
});
|
||||
|
||||
module.factory('ClientEvaluateGenerateExampleToken', function($resource) {
|
||||
var url = authUrl + '/admin/realms/:realm/clients/:client/evaluate-scopes/generate-example-access-token?scope=:scopeParam&userId=:userId';
|
||||
module.factory('ClientEvaluateGenerateExampleAccessToken', function($resource) {
|
||||
return buildClientEvaluateGenerateExampleUrl('generate-example-access-token');
|
||||
});
|
||||
|
||||
module.factory('ClientEvaluateGenerateExampleIDToken', function($resource) {
|
||||
return buildClientEvaluateGenerateExampleUrl('generate-example-id-token');
|
||||
});
|
||||
|
||||
module.factory('ClientEvaluateGenerateExampleUserInfo', function($resource) {
|
||||
return buildClientEvaluateGenerateExampleUrl('generate-example-userinfo');
|
||||
});
|
||||
|
||||
function buildClientEvaluateGenerateExampleUrl(subPath) {
|
||||
var urlTemplate = authUrl + '/admin/realms/:realm/clients/:client/evaluate-scopes/' + subPath + '?scope=:scopeParam&userId=:userId';
|
||||
return {
|
||||
url : function(parameters)
|
||||
{
|
||||
return url
|
||||
url: function (parameters) {
|
||||
return urlTemplate
|
||||
.replace(':realm', parameters.realm)
|
||||
.replace(':client', parameters.client)
|
||||
.replace(':scopeParam', parameters.scopeParam)
|
||||
.replace(':userId', parameters.userId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.factory('ClientProtocolMappersByProtocol', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/clients/:client/protocol-mappers/protocol/:protocol', {
|
||||
|
|
|
@ -126,10 +126,18 @@
|
|||
<a href="">{{:: 'evaluated-roles' | translate}}</a>
|
||||
<kc-tooltip>{{:: 'evaluated-roles.tooltip' | translate}}</kc-tooltip>
|
||||
</li>
|
||||
<li class="{{tabCss.tab3}}" data-ng-click="showTab(3)" data-ng-show="isTokenAvailable()">
|
||||
<li class="{{tabCss.tab3}}" data-ng-click="showTab(3)" data-ng-show="isAccessTokenAvailable()">
|
||||
<a href="">{{:: 'generated-access-token' | translate}}</a>
|
||||
<kc-tooltip>{{:: 'generated-access-token.tooltip' | translate}}</kc-tooltip>
|
||||
</li>
|
||||
<li class="{{tabCss.tab4}}" data-ng-click="showTab(4)" data-ng-show="isIDTokenAvailable()">
|
||||
<a href="">{{:: 'generated-id-token' | translate}}</a>
|
||||
<kc-tooltip>{{:: 'generated-id-token.tooltip' | translate}}</kc-tooltip>
|
||||
</li>
|
||||
<li class="{{tabCss.tab5}}" data-ng-click="showTab(5)" data-ng-show="isUserInfoAvailable()">
|
||||
<a href="">{{:: 'generated-user-info' | translate}}</a>
|
||||
<kc-tooltip>{{:: 'generated-user-info.tooltip' | translate}}</kc-tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Effective protocol mappers -->
|
||||
|
@ -246,11 +254,11 @@
|
|||
</form>
|
||||
|
||||
|
||||
<!-- Access token -->
|
||||
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.viewClients">
|
||||
<!-- Example content: One of Access token, ID token or User Info -->
|
||||
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.viewClients" data-ng-show="exampleTabInfo().isShown">
|
||||
<div class="form-group">
|
||||
<div class="col-md-10 col-md-offset-1" data-ng-show="tokenShown()">
|
||||
<textarea class="form-control" rows="20" kc-select-action="click" readonly>{{oidcAccessToken}}</textarea>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<textarea class="form-control" rows="20" kc-select-action="click" readonly>{{exampleTabInfo().value}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
Loading…
Reference in a new issue