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:
Christoph Leistert 2021-03-05 16:45:19 +01:00 committed by Marek Posolda
parent 65c48a4183
commit b75648bda2
7 changed files with 194 additions and 77 deletions

View file

@ -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) {

View file

@ -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);

View file

@ -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 {

View file

@ -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

View file

@ -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) {

View file

@ -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', {

View file

@ -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>