mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Resolve scopes from authenticated client sessions when selecting attributes
Closes #35192 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
e11db03d76
commit
5d6b9c1460
@ -104,4 +104,8 @@ public interface KeycloakContext {
|
||||
void setHttpRequest(HttpRequest httpRequest);
|
||||
|
||||
void setHttpResponse(HttpResponse httpResponse);
|
||||
|
||||
UserSessionModel getUserSession();
|
||||
|
||||
void setUserSession(UserSessionModel session);
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ public enum UserProfileContext {
|
||||
/**
|
||||
* In this context, a user profile is managed by themselves through the account console.
|
||||
*/
|
||||
ACCOUNT(false, false, true),
|
||||
ACCOUNT(false, true, true),
|
||||
|
||||
/**
|
||||
* In this context, a user profile is managed by themselves when authenticating through a broker.
|
||||
|
||||
@ -697,6 +697,10 @@ public class TokenManager {
|
||||
|
||||
/** Return client itself + all default client scopes of client + optional client scopes requested by scope parameter **/
|
||||
public static Stream<ClientScopeModel> getRequestedClientScopes(KeycloakSession session, String scopeParam, ClientModel client, UserModel user) {
|
||||
if (client == null) {
|
||||
return Stream.of();
|
||||
}
|
||||
|
||||
// Add all default client scopes automatically and client itself
|
||||
Stream<ClientScopeModel> clientScopes = Stream.concat(
|
||||
client.getClientScopes(true).values().stream(),
|
||||
|
||||
@ -29,6 +29,7 @@ import org.keycloak.models.KeycloakUriInfo;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.theme.Theme;
|
||||
import org.keycloak.urls.UrlType;
|
||||
@ -37,6 +38,7 @@ import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
@ -54,6 +56,7 @@ public abstract class DefaultKeycloakContext implements KeycloakContext {
|
||||
private Map<UrlType, KeycloakUriInfo> uriInfo;
|
||||
|
||||
private AuthenticationSessionModel authenticationSession;
|
||||
private UserSessionModel userSession;
|
||||
private HttpRequest request;
|
||||
private HttpResponse response;
|
||||
private ClientConnection clientConnection;
|
||||
@ -113,6 +116,11 @@ public abstract class DefaultKeycloakContext implements KeycloakContext {
|
||||
|
||||
@Override
|
||||
public ClientModel getClient() {
|
||||
if (client == null) {
|
||||
client = Optional.ofNullable(authenticationSession)
|
||||
.map(AuthenticationSessionModel::getClient)
|
||||
.orElse(null);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
@ -205,4 +213,13 @@ public abstract class DefaultKeycloakContext implements KeycloakContext {
|
||||
this.response = httpResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserSessionModel getUserSession() {
|
||||
return userSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserSession(UserSessionModel userSession) {
|
||||
this.userSession = userSession;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1532,6 +1532,8 @@ public class AuthenticationManager {
|
||||
}
|
||||
}
|
||||
|
||||
KeycloakContext context = session.getContext();
|
||||
|
||||
if (token.getSessionState() != null && !isSessionValid(realm, userSession)) {
|
||||
// Check if accessToken was for the offline session.
|
||||
if (!isCookie) {
|
||||
@ -1542,6 +1544,8 @@ public class AuthenticationManager {
|
||||
if (!isClientValid(offlineUserSession, client, token)) {
|
||||
return null;
|
||||
}
|
||||
context.setUserSession(offlineUserSession);
|
||||
context.setClient(client);
|
||||
return new AuthResult(user, offlineUserSession, token, client);
|
||||
}
|
||||
}
|
||||
@ -1549,7 +1553,7 @@ public class AuthenticationManager {
|
||||
|
||||
if (userSession != null) {
|
||||
String userSessionId = userSession.getId();
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), session.getContext(), newSession -> {
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), context, newSession -> {
|
||||
RealmModel realmModel = newSession.realms().getRealm(realm.getId());
|
||||
UserSessionModel userSessionModel = newSession.sessions().getUserSession(realmModel, userSessionId);
|
||||
backchannelLogout(newSession, realmModel, userSessionModel, uriInfo, connection, headers, true);
|
||||
@ -1572,7 +1576,11 @@ public class AuthenticationManager {
|
||||
if (!isClientValid(userSession, client, token)) {
|
||||
return null;
|
||||
}
|
||||
context.setClient(client);
|
||||
}
|
||||
|
||||
context.setUserSession(userSession);
|
||||
|
||||
return new AuthResult(user, userSession, token, client);
|
||||
} catch (VerificationException e) {
|
||||
logger.debugf("Failed to verify identity token: %s", e.getMessage());
|
||||
@ -1678,4 +1686,30 @@ public class AuthenticationManager {
|
||||
return HashUtils.sha256UrlEncodedHash(input, StandardCharsets.ISO_8859_1);
|
||||
}
|
||||
|
||||
public static String getRequestedScopes(KeycloakSession session) {
|
||||
return getRequestedScopes(session, session.getContext().getClient());
|
||||
}
|
||||
|
||||
public static String getRequestedScopes(KeycloakSession session, ClientModel client) {
|
||||
KeycloakContext context = session.getContext();
|
||||
AuthenticationSessionModel authenticationSession = context.getAuthenticationSession();
|
||||
|
||||
if (authenticationSession != null) {
|
||||
return authenticationSession.getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
|
||||
}
|
||||
|
||||
UserSessionModel userSession = context.getUserSession();
|
||||
|
||||
if (userSession == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, AuthenticatedClientSessionModel> clientSessions = userSession.getAuthenticatedClientSessions();
|
||||
|
||||
return clientSessions.values().stream().filter(c -> c.getClient().equals(client))
|
||||
.map((c) -> c.getNotes().get(OIDCLoginProtocol.SCOPE_PARAM))
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,8 +13,6 @@ import org.keycloak.authentication.requiredactions.DeleteAccount;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.common.util.Environment;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.utils.PkceUtils;
|
||||
import org.keycloak.utils.SecureContextResolver;
|
||||
@ -33,7 +31,6 @@ import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.Auth;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.resource.AccountResourceProvider;
|
||||
import org.keycloak.services.resources.AbstractSecuredLocalService;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.services.util.ResolveRelative;
|
||||
import org.keycloak.services.util.ViteManifest;
|
||||
@ -146,7 +143,7 @@ public class AccountConsole implements AccountResourceProvider {
|
||||
map.put("resourceCommonUrl", Urls.themeRoot(serverBaseUri).getPath() + "/common/keycloak");
|
||||
map.put("resourceVersion", Version.RESOURCES_VERSION);
|
||||
|
||||
var requestedScopes = getRequestedScopes();
|
||||
var requestedScopes = AuthenticationManager.getRequestedScopes(session, realm.getClientByClientId(Constants.ACCOUNT_CONSOLE_CLIENT_ID));
|
||||
|
||||
if (requestedScopes != null) {
|
||||
map.put(OIDCLoginProtocol.SCOPE_PARAM, requestedScopes);
|
||||
@ -361,26 +358,4 @@ public class AccountConsole implements AccountResourceProvider {
|
||||
|
||||
return new String[]{referrer, referrerName, referrerUri};
|
||||
}
|
||||
|
||||
private String getRequestedScopes() {
|
||||
if (auth == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
UserSessionModel userSession = auth.getSession();
|
||||
|
||||
if (userSession == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (AuthenticatedClientSessionModel c : userSession.getAuthenticatedClientSessions().values()) {
|
||||
ClientModel client = c.getClient();
|
||||
|
||||
if (Constants.ACCOUNT_CONSOLE_CLIENT_ID.equals(client.getClientId())) {
|
||||
return c.getNote(OIDCLoginProtocol.SCOPE_PARAM);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,11 +36,11 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.userprofile.config.DeclarativeUserProfileModel;
|
||||
import org.keycloak.representations.userprofile.config.UPAttribute;
|
||||
import org.keycloak.representations.userprofile.config.UPAttributePermissions;
|
||||
@ -77,17 +77,18 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
|
||||
* @return
|
||||
*/
|
||||
private static boolean requestedScopePredicate(AttributeContext context, Set<String> configuredScopes) {
|
||||
KeycloakSession session = context.getSession();
|
||||
AuthenticationSessionModel authenticationSession = session.getContext().getAuthenticationSession();
|
||||
|
||||
if (authenticationSession == null) {
|
||||
return false;
|
||||
// any attribute is enabled and available when managing through the User Admin API
|
||||
if (UserProfileContext.USER_API.equals(context.getContext())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String requestedScopesString = authenticationSession.getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
|
||||
ClientModel client = authenticationSession.getClient();
|
||||
KeycloakSession session = context.getSession();
|
||||
String requestedScopes = AuthenticationManager.getRequestedScopes(session);
|
||||
ClientModel client = session.getContext().getClient();
|
||||
|
||||
return getRequestedClientScopes(session, requestedScopesString, client, context.getUser()).map((csm) -> csm.getName()).anyMatch(configuredScopes::contains);
|
||||
return getRequestedClientScopes(session, requestedScopes, client, context.getUser())
|
||||
.map(ClientScopeModel::getName)
|
||||
.anyMatch(configuredScopes::contains);
|
||||
}
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
@ -105,7 +105,7 @@ public class AccountRestServiceWithUserProfileTest extends AbstractRestServiceTe
|
||||
assertUserProfileAttributeMetadata(user, "username", "${username}", true, false);
|
||||
assertUserProfileAttributeMetadata(user, "email", "${email}", true, false);
|
||||
|
||||
UserProfileAttributeMetadata uam = assertUserProfileAttributeMetadata(user, "firstName", "${profile.firstName}", false, false);
|
||||
UserProfileAttributeMetadata uam = assertUserProfileAttributeMetadata(user, "firstName", "${profile.firstName}", true, false);
|
||||
assertNull(uam.getAnnotations());
|
||||
Map<String, Object> vc = assertValidatorExists(uam, "length");
|
||||
assertEquals(255, vc.get("max"));
|
||||
@ -121,7 +121,7 @@ public class AccountRestServiceWithUserProfileTest extends AbstractRestServiceTe
|
||||
assertUserProfileAttributeMetadata(user, "attr_required", "attr_required", true, false);
|
||||
assertUserProfileAttributeMetadata(user, "attr_required_by_role", "attr_required_by_role", true, false);
|
||||
|
||||
assertUserProfileAttributeMetadata(user, "attr_required_by_scope", "attr_required_by_scope", false, false);
|
||||
assertUserProfileAttributeMetadata(user, "attr_required_by_scope", "attr_required_by_scope", true, false);
|
||||
|
||||
assertUserProfileAttributeMetadata(user, "attr_not_required_due_to_role", "attr_not_required_due_to_role", false, false);
|
||||
assertUserProfileAttributeMetadata(user, "attr_readonly", "attr_readonly", false, true);
|
||||
@ -229,7 +229,7 @@ public class AccountRestServiceWithUserProfileTest extends AbstractRestServiceTe
|
||||
assertUserProfileAttributeMetadata(user, "username", "${username}", true, true);
|
||||
assertUserProfileAttributeMetadata(user, "email", "${email}", true, false);
|
||||
|
||||
UserProfileAttributeMetadata uam = assertUserProfileAttributeMetadata(user, "firstName", "${profile.firstName}", false, false);
|
||||
UserProfileAttributeMetadata uam = assertUserProfileAttributeMetadata(user, "firstName", "${profile.firstName}", true, false);
|
||||
assertNull(uam.getAnnotations());
|
||||
Map<String, Object> vc = assertValidatorExists(uam, "length");
|
||||
assertEquals(255, vc.get("max"));
|
||||
@ -245,7 +245,7 @@ public class AccountRestServiceWithUserProfileTest extends AbstractRestServiceTe
|
||||
assertUserProfileAttributeMetadata(user, "attr_required", "attr_required", true, false);
|
||||
assertUserProfileAttributeMetadata(user, "attr_required_by_role", "attr_required_by_role", true, false);
|
||||
|
||||
assertUserProfileAttributeMetadata(user, "attr_required_by_scope", "attr_required_by_scope", false, false);
|
||||
assertUserProfileAttributeMetadata(user, "attr_required_by_scope", "attr_required_by_scope", true, false);
|
||||
|
||||
assertUserProfileAttributeMetadata(user, "attr_not_required_due_to_role", "attr_not_required_due_to_role", false, false);
|
||||
assertUserProfileAttributeMetadata(user, "attr_readonly", "attr_readonly", false, true);
|
||||
|
||||
@ -58,7 +58,9 @@ public abstract class AbstractUserProfileTest extends AbstractTestRealmKeycloakT
|
||||
protected static void configureAuthenticationSession(KeycloakSession session, String clientId, Set<String> requestedScopes) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
session.getContext().setAuthenticationSession(createAuthenticationSession(realm.getClientByClientId(clientId), requestedScopes));
|
||||
ClientModel client = realm.getClientByClientId(clientId);
|
||||
session.getContext().setAuthenticationSession(createAuthenticationSession(client, requestedScopes));
|
||||
session.getContext().setClient(client);
|
||||
}
|
||||
|
||||
protected static Optional<ComponentModel> setAndGetDefaultConfiguration(KeycloakSession session) {
|
||||
|
||||
@ -105,7 +105,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("client-a").protocol("openid-connect").build());
|
||||
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("some-optional-scope").protocol("openid-connect").build());
|
||||
ClientRepresentation client = KeycloakModelUtils.createClient(testRealm, "client-a");
|
||||
client.setDefaultClientScopes(Collections.singletonList("customer"));
|
||||
client.setDefaultClientScopes(List.of("customer"));
|
||||
client.setOptionalClientScopes(Collections.singletonList("some-optional-scope"));
|
||||
KeycloakModelUtils.createClient(testRealm, "client-b");
|
||||
}
|
||||
@ -1736,11 +1736,10 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequiredByClientScope() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testRequiredByClientScope);
|
||||
}
|
||||
|
||||
private static void testRequiredByClientScope(KeycloakSession session) {
|
||||
@ModelTest
|
||||
public void testRequiredByClientScope(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName("test");
|
||||
session.getContext().setRealm(realm);
|
||||
UserProfileProvider provider = getUserProfileProvider(session);
|
||||
UPConfig config = UPConfigUtils.parseSystemDefaultConfig();
|
||||
config.addOrReplaceAttribute(new UPAttribute(ATT_ADDRESS, new UPAttributePermissions(Set.of(), Set.of(ROLE_USER)), new UPAttributeRequired(Set.of(), Set.of("client-a"))));
|
||||
@ -1753,33 +1752,22 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
attributes.put(UserModel.EMAIL, "user@email.test");
|
||||
|
||||
// client with default scopes for which is attribute NOT configured as required
|
||||
configureAuthenticationSession(session, "client-b", null);
|
||||
|
||||
// no fail on User API nor Account console as they do not have scopes
|
||||
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
profile.validate();
|
||||
|
||||
// no fail on auth flow scopes when scope is not required
|
||||
profile = provider.create(UserProfileContext.REGISTRATION, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.IDP_REVIEW, attributes);
|
||||
profile.validate();
|
||||
|
||||
// client with default scope for which is attribute configured as required
|
||||
configureAuthenticationSession(session, "client-a", null);
|
||||
|
||||
// no fail on User API nor Account console as they do not have scopes
|
||||
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
// no fail on User API because they don't have access to scopes yet
|
||||
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
|
||||
// fail on auth flow scopes when scope is required
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
@ -1801,7 +1789,6 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -1825,13 +1812,13 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||
// client with default scopes. No address scope included
|
||||
configureAuthenticationSession(session, "client-a", null);
|
||||
|
||||
// No fail on admin and account console as they do not have scopes
|
||||
// no fail on User API because they don't have access to scopes yet
|
||||
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
profile.validate();
|
||||
|
||||
// no fail on auth flow scopes when scope is not required
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.REGISTRATION, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
@ -1845,10 +1832,15 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||
// No fail on admin and account console as they do not have scopes
|
||||
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
profile.validate();
|
||||
|
||||
// fail on auth flow scopes when scope is required
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
@ -37,9 +37,9 @@ public class UPConfigUtilsTest {
|
||||
|
||||
@Test
|
||||
public void canBeAuthFlowContext() {
|
||||
Assert.assertFalse(UserProfileContext.ACCOUNT.canBeAuthFlowContext());
|
||||
Assert.assertFalse(UserProfileContext.USER_API.canBeAuthFlowContext());
|
||||
|
||||
Assert.assertTrue(UserProfileContext.ACCOUNT.canBeAuthFlowContext());
|
||||
Assert.assertTrue(UserProfileContext.IDP_REVIEW.canBeAuthFlowContext());
|
||||
Assert.assertTrue(UserProfileContext.REGISTRATION.canBeAuthFlowContext());
|
||||
Assert.assertTrue(UserProfileContext.UPDATE_PROFILE.canBeAuthFlowContext());
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user