Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ public ClientBuilder consentRequired(Boolean enabled) {
return this;
}

public ClientBuilder implicitFlowEnabled(Boolean enabled) {
rep.setImplicitFlowEnabled(enabled);
return this;
}

public ClientBuilder webOrigins(String... webOrigins) {
rep.setWebOrigins(combine(rep.getWebOrigins(), webOrigins));
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
*/
package org.keycloak.testframework.realm;

import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;

import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;

/**
* @author <a href="mailto:yoshiyuki.tabata.jy@hitachi.com">Yoshiyuki Tabata</a>
Expand Down Expand Up @@ -57,4 +60,12 @@ public ClientScopeBuilder attribute(String key, String value) {
rep.getAttributes().put(key, value);
return this;
}

public ClientScopeBuilder mappers(ProtocolMapperRepresentation... mappers) {
if (mappers != null) {
rep.setProtocolMappers(Builder.createIfNull(rep.getProtocolMappers(), LinkedList::new));
rep.getProtocolMappers().addAll(Arrays.asList(mappers));
}
return this;
}
Comment thread
rmartinc marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.keycloak.testframework.realm;

import java.util.HashMap;

import org.keycloak.representations.idm.ProtocolMapperRepresentation;

/**
* Builder to help with protocol mappers in the test-suite.
*
* @author rmartinc
*/
public class ProtocolMapperBuilder extends Builder<ProtocolMapperRepresentation> {
Comment thread
rmartinc marked this conversation as resolved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JFYI - the instantiation of the protocol mappers is quite fragile even in our core logic, and this PR should resolve it and might be used in the testsuite as well if we decide to go that way: #50974


private ProtocolMapperBuilder(ProtocolMapperRepresentation rep) {
super(rep);
}

public static ProtocolMapperBuilder create() {
return new ProtocolMapperBuilder(new ProtocolMapperRepresentation());
}

public static ProtocolMapperBuilder update(ProtocolMapperRepresentation rep) {
return new ProtocolMapperBuilder(rep);
}

public ProtocolMapperBuilder name(String name) {
rep.setName(name);
return this;
}

public ProtocolMapperBuilder protocol(String protocol) {
rep.setProtocol(protocol);
return this;
}

public ProtocolMapperBuilder protocolMapper(String protocolMapper) {
rep.setProtocolMapper(protocolMapper);
return this;
}

public ProtocolMapperBuilder config(String key, String value) {
rep.setConfig(Builder.createIfNull(rep.getConfig(), HashMap::new));
rep.getConfig().put(key, value);
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.keycloak.models.ClientScopeModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.testframework.realm.ClientScopeBuilder;

public class ParameterizedScopeBuilder {
Expand Down Expand Up @@ -55,6 +56,11 @@ public ParameterizedScopeBuilder regexp(String regexp) {
return this;
}

public ParameterizedScopeBuilder mappers(ProtocolMapperRepresentation... mappers) {
builder.mappers(mappers);
return this;
}

public ClientScopeRepresentation build() {
return builder.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,28 @@

import jakarta.ws.rs.core.Response;

import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.common.Profile;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.CibaConfig;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.Constants;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.grants.ciba.CibaGrantTypeFactory;
import org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelResponse;
import org.keycloak.protocol.oidc.grants.ciba.endpoints.ClientNotificationEndpointRequest;
import org.keycloak.protocol.oidc.mappers.HardcodedClaim;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.protocol.oidc.scope.ParameterizedScopeTypeProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.testframework.annotations.InjectClient;
import org.keycloak.testframework.annotations.InjectEvents;
import org.keycloak.testframework.annotations.InjectRealm;
Expand All @@ -35,6 +42,7 @@
import org.keycloak.testframework.realm.ClientConfig;
import org.keycloak.testframework.realm.ManagedClient;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.ProtocolMapperBuilder;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.testframework.realm.RealmConfig;
import org.keycloak.testframework.realm.UserBuilder;
Expand All @@ -44,6 +52,7 @@
import org.keycloak.testframework.ui.page.OAuthGrantPage;
import org.keycloak.testframework.util.ApiUtil;
import org.keycloak.tests.suites.DatabaseTest;
import org.keycloak.tests.utils.admin.AdminApiUtil;
import org.keycloak.testsuite.util.AccountHelper;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse;
Expand All @@ -65,6 +74,7 @@ public class ParameterizedScopesOAuthGrantTest {

private static final String THIRD_PARTY_APP = "third-party";
private static final String DEFAULT_USERNAME = "test-user@localhost";
private static final String DEFAULT_ADMIN_USERNAME = "administrator@localhost";
private static final String DEFAULT_PASSWORD = "password";

private static String PARAMETERIZED_SCOPE_ID;
Expand Down Expand Up @@ -106,6 +116,7 @@ public void afterEach() {
if (userConsents.stream().anyMatch(m -> THIRD_PARTY_APP.equals(m.get("clientId")))) {
AccountHelper.revokeConsents(realm.admin(), DEFAULT_USERNAME, THIRD_PARTY_APP);
}
oauth.responseType(OAuth2Constants.CODE);
}

@Test
Expand Down Expand Up @@ -673,6 +684,76 @@ public void booleanScopeRejectsExcessivelyLongParameter() {
assertLongParameterRejected("length-scope");
}

@Test
public void delegationScopeWithImplicitFlow() {
// create impersonation parameterized role with a hardcoded claim
createAndAssignOptionalScope(ParameterizedScopeBuilder.create("delegated-act")
.parameterizedScopeType("delegation")
.mappers(ProtocolMapperBuilder.create().name("Hardcoded Claim")
.protocol(OIDCLoginProtocol.LOGIN_PROTOCOL)
.protocolMapper(HardcodedClaim.PROVIDER_ID)
.config(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "matched_scope")
.config(HardcodedClaim.CLAIM_VALUE, "implicit-delegation")
.config(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, Boolean.TRUE.toString())
.build())
.build());

// change the app to allow implicit flow and no consent required
ClientRepresentation rep = thirdParty.admin().toRepresentation();
rep.setConsentRequired(Boolean.FALSE);
rep.setImplicitFlowEnabled(Boolean.TRUE);
thirdParty.admin().update(rep);
realm.cleanup().add(r -> {
rep.setConsentRequired(Boolean.TRUE);
rep.setImplicitFlowEnabled(Boolean.FALSE);
r.clients().get(thirdParty.getId()).update(rep);
});

// add impersonation to the admin user
final ClientResource realmManagement = AdminApiUtil.findClientByClientId(realm.admin(), Constants.REALM_MANAGEMENT_CLIENT_ID);
final String clientUUID = realmManagement.toRepresentation().getId();
final RoleRepresentation impersonation = realmManagement.roles().get(AdminRoles.IMPERSONATION).toRepresentation();
AdminApiUtil.findUserByUsernameId(realm.admin(), DEFAULT_ADMIN_USERNAME).roles()
.clientLevel(clientUUID).add(List.of(impersonation));

// implicit flow login with the delegation scope granted
String requestedScope = "delegated-act:" + DEFAULT_ADMIN_USERNAME;
oauth.client(THIRD_PARTY_APP)
.responseType(OAuth2Constants.TOKEN)
.scope(requestedScope)
.openLoginForm();
Comment thread
rmartinc marked this conversation as resolved.

oauth.fillLoginForm(DEFAULT_USERNAME, DEFAULT_PASSWORD);
AuthorizationEndpointResponse res = new AuthorizationEndpointResponse(oauth);

Assertions.assertTrue(res.isRedirected());
Assertions.assertNotNull(res.getAccessToken());

AccessToken token = oauth.verifyToken(res.getAccessToken());
MatcherAssert.assertThat(List.of(token.getScope().split(" ")), Matchers.hasItem(requestedScope));
Assertions.assertEquals("implicit-delegation", token.getOtherClaims().get("matched_scope"));

// logout and remove the permission
AccountHelper.logout(realm.admin(), DEFAULT_USERNAME);
AdminApiUtil.findUserByUsernameId(realm.admin(), DEFAULT_ADMIN_USERNAME).roles()
.clientLevel(clientUUID).remove(List.of(impersonation));

// implicit flow login with the delegation scope not granted
oauth.client(THIRD_PARTY_APP)
.responseType(OAuth2Constants.TOKEN)
.scope(requestedScope)
.openLoginForm();
oauth.fillLoginForm(DEFAULT_USERNAME, DEFAULT_PASSWORD);
res = new AuthorizationEndpointResponse(oauth);

Assertions.assertTrue(res.isRedirected());
Assertions.assertNotNull(res.getAccessToken());

token = oauth.verifyToken(res.getAccessToken());
MatcherAssert.assertThat(List.of(token.getScope().split(" ")), Matchers.not(Matchers.hasItem(requestedScope)));
Assertions.assertNull(token.getOtherClaims().get("matched_scope"));
}

private String createAndAssignOptionalScope(ClientScopeRepresentation scope) {
String scopeId = ApiUtil.getCreatedId(realm.admin().clientScopes().create(scope));
thirdParty.admin().addOptionalClientScope(scopeId);
Expand Down Expand Up @@ -717,6 +798,12 @@ public RealmBuilder configure(RealmBuilder realm) {
.name("Test", "User")
.emailVerified(true)
.password(DEFAULT_PASSWORD)
.enabled(true),
UserBuilder.create(DEFAULT_ADMIN_USERNAME)
.email(DEFAULT_ADMIN_USERNAME)
.name("Admin", "User")
.emailVerified(true)
.password(DEFAULT_PASSWORD)
.enabled(true));
return realm;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.protocol.oidc.mappers.ParameterizedScopeUserPropertyMapper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
Expand Down Expand Up @@ -70,6 +71,7 @@
import org.keycloak.tests.utils.admin.AdminApiUtil;
import org.keycloak.testsuite.util.AccountHelper;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse;
import org.keycloak.testsuite.util.oauth.IntrospectionResponse;
import org.keycloak.testsuite.util.oauth.LogoutResponse;
import org.keycloak.testsuite.util.oauth.ciba.AuthenticationRequestAcknowledgement;
Expand Down Expand Up @@ -123,6 +125,7 @@ public void afterEach() {
if (consents.stream().anyMatch(m -> oauth.getClientId().equals(m.get("clientId")))) {
AccountHelper.revokeConsents(realm.admin(), USERNAME, oauth.getClientId());
}
oauth.responseType(OAuth2Constants.CODE);
}

@Test
Expand Down Expand Up @@ -246,6 +249,59 @@ public void delegationFGAP() throws Exception {
Assertions.assertTrue(logout.isSuccess(), logout.getError() + " - " + logout.getErrorDescription());
}

@Test
public void delegationImplicit() {
final ClientResource realmManagement = AdminApiUtil.findClientByClientId(realm.admin(), Constants.REALM_MANAGEMENT_CLIENT_ID);
final String clientUUID = realmManagement.toRepresentation().getId();
final RoleRepresentation impersonation = realmManagement.roles().get(AdminRoles.IMPERSONATION).toRepresentation();
administrator.admin().roles().clientLevel(clientUUID).add(List.of(impersonation));

// enable implicit flow for the client
ClientRepresentation clientRep = oauth.clientResource().toRepresentation();
clientRep.setImplicitFlowEnabled(Boolean.TRUE);
oauth.clientResource().update(clientRep);
realm.cleanup().add(r -> {
clientRep.setImplicitFlowEnabled(Boolean.FALSE);
r.clients().get(clientRep.getId()).update(clientRep);
});

// request the delegation to administrator and accept the delegation using implicit
final String scope = OIDCLoginProtocolFactory.DELEGATION_SCOPE + ClientScopeModel.VALUE_SEPARATOR + administrator.getUsername();
oauth.scope(scope).responseType(OAuth2Constants.TOKEN).openLoginForm();
oauth.fillLoginForm(USERNAME, PASSWORD);
grantPage.assertCurrent();
List<String> grants = grantPage.getDisplayedGrants();
MatcherAssert.assertThat(grants, Matchers.hasItem("Delegate token to administrator administrator?"));
grantPage.accept();

AuthorizationEndpointResponse res = new AuthorizationEndpointResponse(oauth);

Assertions.assertTrue(res.isRedirected());
Assertions.assertNotNull(res.getAccessToken());
AccessToken token = oauth.verifyToken(res.getAccessToken());
assertScopeContains(token.getScope(), scope);
assertMayActPresent(token, administrator.getId());

// logout
AdminApiUtil.findUserByUsernameId(realm.admin(), USERNAME).logout();

// remove the impersonation from the user and try again
administrator.admin().roles().clientLevel(clientUUID).remove(List.of(impersonation));
Comment thread
rmartinc marked this conversation as resolved.

oauth.scope(scope).responseType(OAuth2Constants.TOKEN).openLoginForm();
oauth.fillLoginForm(USERNAME, PASSWORD);
res = new AuthorizationEndpointResponse(oauth);

Assertions.assertTrue(res.isRedirected());
Assertions.assertNotNull(res.getAccessToken());
token = oauth.verifyToken(res.getAccessToken());
assertScopeNotContains(token.getScope(), scope);
assertMayActNotPresent(token);

// logout
AdminApiUtil.findUserByUsernameId(realm.admin(), USERNAME).logout();
}

@Test
public void delegationWithPreferredUsernameMapper() {
final ClientResource realmManagement = AdminApiUtil.findClientByClientId(realm.admin(), Constants.REALM_MANAGEMENT_CLIENT_ID);
Expand Down
Loading