-
Notifications
You must be signed in to change notification settings - Fork 6.4k
/
DefaultClientSessionContext.java
309 lines (250 loc) · 12.4 KB
/
DefaultClientSessionContext.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.util;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.Profile;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.rar.AuthorizationRequestContext;
import org.keycloak.rar.AuthorizationRequestSource;
import org.keycloak.util.TokenUtil;
/**
* Not thread safe. It's per-request object
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DefaultClientSessionContext implements ClientSessionContext {
private static Logger logger = Logger.getLogger(DefaultClientSessionContext.class);
private final AuthenticatedClientSessionModel clientSession;
private final Set<String> clientScopeIds;
private final KeycloakSession session;
private Set<ClientScopeModel> clientScopes;
//
private Set<RoleModel> roles;
private Set<ProtocolMapperModel> protocolMappers;
// All roles of user expanded. It doesn't yet take into account permitted clientScopes
private Set<RoleModel> userRoles;
private Map<String, Object> attributes = new HashMap<>();
private DefaultClientSessionContext(AuthenticatedClientSessionModel clientSession, Set<String> clientScopeIds, KeycloakSession session) {
this.clientScopeIds = clientScopeIds;
this.clientSession = clientSession;
this.session = session;
}
/**
* Useful if we want to "re-compute" client scopes based on the scope parameter
*/
public static DefaultClientSessionContext fromClientSessionScopeParameter(AuthenticatedClientSessionModel clientSession, KeycloakSession session) {
return fromClientSessionAndScopeParameter(clientSession, clientSession.getNote(OAuth2Constants.SCOPE), session);
}
public static DefaultClientSessionContext fromClientSessionAndScopeParameter(AuthenticatedClientSessionModel clientSession, String scopeParam, KeycloakSession session) {
Stream<ClientScopeModel> requestedClientScopes;
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
session.getContext().setClient(clientSession.getClient());
requestedClientScopes = AuthorizationContextUtil.getClientScopesStreamFromAuthorizationRequestContextWithClient(session, scopeParam);
} else {
requestedClientScopes = TokenManager.getRequestedClientScopes(scopeParam, clientSession.getClient());
}
return fromClientSessionAndClientScopes(clientSession, requestedClientScopes, session);
}
public static DefaultClientSessionContext fromClientSessionAndClientScopeIds(AuthenticatedClientSessionModel clientSession, Set<String> clientScopeIds, KeycloakSession session) {
return new DefaultClientSessionContext(clientSession, clientScopeIds, session);
}
// in order to standardize the way we create this object and with that data, it's better to compute the client scopes internally instead of relying on external sources
// i.e: the TokenManager.getRequestedClientScopes was being called in many places to obtain the ClientScopeModel stream.
// by changing this method to private, we'll only call it in this class, while also having a single place to put the DYNAMIC_SCOPES feature flag condition
private static DefaultClientSessionContext fromClientSessionAndClientScopes(AuthenticatedClientSessionModel clientSession,
Stream<ClientScopeModel> clientScopes,
KeycloakSession session) {
Set<String> clientScopeIds = clientScopes.map(ClientScopeModel::getId).collect(Collectors.toSet());
return new DefaultClientSessionContext(clientSession, clientScopeIds, session);
}
@Override
public AuthenticatedClientSessionModel getClientSession() {
return clientSession;
}
@Override
public Set<String> getClientScopeIds() {
return clientScopeIds;
}
@Override
public Stream<ClientScopeModel> getClientScopesStream() {
// Load client scopes if not yet present
if (clientScopes == null) {
clientScopes = loadClientScopes();
}
return clientScopes.stream();
}
@Override
public Stream<RoleModel> getRolesStream() {
// Load roles if not yet present
if (roles == null) {
roles = loadRoles();
}
return roles.stream();
}
@Override
public Stream<ProtocolMapperModel> getProtocolMappersStream() {
// Load protocolMappers if not yet present
if (protocolMappers == null) {
protocolMappers = loadProtocolMappers();
}
return protocolMappers.stream();
}
private Set<RoleModel> getUserRoles() {
// Load userRoles if not yet present
if (userRoles == null) {
userRoles = loadUserRoles();
}
return userRoles;
}
@Override
public String getScopeString() {
return getScopeString(false);
}
@Override
public String getScopeString(boolean ignoreIncludeInTokenScope) {
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
String scopeParam = buildScopesStringFromAuthorizationRequest(ignoreIncludeInTokenScope);
logger.tracef("Generated scope param with Dynamic Scopes enabled: %1s", scopeParam);
String scopeSent = clientSession.getNote(OAuth2Constants.SCOPE);
if (TokenUtil.isOIDCRequest(scopeSent)) {
scopeParam = TokenUtil.attachOIDCScope(scopeParam);
}
return scopeParam;
}
// Add both default and optional scopes to scope parameter. Don't add client itself
String scopeParam = getClientScopesStream()
.filter(((Predicate<ClientScopeModel>) ClientModel.class::isInstance).negate())
.filter(scope-> scope.isIncludeInTokenScope() || ignoreIncludeInTokenScope)
.map(ClientScopeModel::getName)
.collect(Collectors.joining(" "));
// See if "openid" scope is requested
String scopeSent = clientSession.getNote(OAuth2Constants.SCOPE);
if (TokenUtil.isOIDCRequest(scopeSent)) {
scopeParam = TokenUtil.attachOIDCScope(scopeParam);
}
return scopeParam;
}
/**
* Get all the scopes from the {@link AuthorizationRequestContext} by filtering entries by Source and by whether
* they should be included in tokens or not.
* Then return the scope name from the data stored in the RAR object representation.
*
* @param ignoreIncludeInTokenScope ignore include in token scope from client scope options
*
* @return see description
*/
private String buildScopesStringFromAuthorizationRequest(boolean ignoreIncludeInTokenScope) {
return AuthorizationContextUtil.getAuthorizationRequestContextFromScopes(session, clientSession.getNote(OAuth2Constants.SCOPE)).getAuthorizationDetailEntries().stream()
.filter(authorizationDetails -> authorizationDetails.getSource().equals(AuthorizationRequestSource.SCOPE))
.filter(authorizationDetails -> authorizationDetails.getClientScope().isIncludeInTokenScope() || ignoreIncludeInTokenScope)
.filter(authorizationDetails -> isClientScopePermittedForUser(authorizationDetails.getClientScope()))
.map(authorizationDetails -> authorizationDetails.getAuthorizationDetails().getScopeNameFromCustomData())
.collect(Collectors.joining(" "));
}
@Override
public void setAttribute(String name, Object value) {
attributes.put(name, value);
}
@Override
public <T> T getAttribute(String name, Class<T> clazz) {
Object value = attributes.get(name);
return clazz.cast(value);
}
@Override
public AuthorizationRequestContext getAuthorizationRequestContext() {
return AuthorizationContextUtil.getAuthorizationRequestContextFromScopes(session, clientSession.getNote(OAuth2Constants.SCOPE));
}
// Loading data
private Set<ClientScopeModel> loadClientScopes() {
Set<ClientScopeModel> clientScopes = new HashSet<>();
for (String scopeId : clientScopeIds) {
ClientScopeModel clientScope = KeycloakModelUtils.findClientScopeById(clientSession.getClient().getRealm(), getClientSession().getClient(), scopeId);
if (clientScope != null) {
if (isClientScopePermittedForUser(clientScope)) {
clientScopes.add(clientScope);
} else {
if (logger.isTraceEnabled()) {
logger.tracef("User '%s' not permitted to have client scope '%s'",
clientSession.getUserSession().getUser().getUsername(), clientScope.getName());
}
}
}
}
return clientScopes;
}
// Return true if clientScope can be used by the user.
private boolean isClientScopePermittedForUser(ClientScopeModel clientScope) {
if (clientScope instanceof ClientModel) {
return true;
}
Set<RoleModel> clientScopeRoles = clientScope.getScopeMappingsStream().collect(Collectors.toSet());
// Client scope is automatically permitted if it doesn't have any role scope mappings
if (clientScopeRoles.isEmpty()) {
return true;
}
// Expand (resolve composite roles)
clientScopeRoles = RoleUtils.expandCompositeRoles(clientScopeRoles);
// Check if expanded roles of clientScope has any intersection with expanded roles of user. If not, it is not permitted
clientScopeRoles.retainAll(getUserRoles());
return !clientScopeRoles.isEmpty();
}
private Set<RoleModel> loadRoles() {
UserModel user = clientSession.getUserSession().getUser();
ClientModel client = clientSession.getClient();
return TokenManager.getAccess(user, client, getClientScopesStream());
}
private Set<ProtocolMapperModel> loadProtocolMappers() {
String protocol = clientSession.getClient().getProtocol();
// Being rather defensive. But protocol should normally always be there
if (protocol == null) {
logger.warnf("Client '%s' doesn't have protocol set. Fallback to openid-connect. Please fix client configuration",
clientSession.getClient().getClientId());
protocol = OIDCLoginProtocol.LOGIN_PROTOCOL;
}
String finalProtocol = protocol;
return getClientScopesStream()
.flatMap(clientScope -> clientScope.getProtocolMappersStream()
.filter(mapper -> Objects.equals(finalProtocol, mapper.getProtocol()) &&
ProtocolMapperUtils.isEnabled(session, mapper)))
.collect(Collectors.toSet());
}
private Set<RoleModel> loadUserRoles() {
UserModel user = clientSession.getUserSession().getUser();
return RoleUtils.getDeepUserRoleMappings(user);
}
}