8000 Verification of user consents to scopes requested by the target clien… by cavuugroup · Pull Request #26514 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
b3bf467
Update teams.yml (#26684)
stianst Feb 1, 2024
b9513cc
Verification of user consents to scopes requested by the target clien…
kw-omega Jan 25, 2024
58b2c7d
Renamed messages in localization/realm overrides (#26663)
agagancarczyk Feb 1, 2024
70288eb
Allow formating numbers when rendering attributes
pedroigor Jan 21, 2024
90c5d76
Fix Keycloak operator setting for cache-config-file (#26666)
mhajas Feb 1, 2024
98976b4
Update teams.yml
stianst Feb 1, 2024
8273f0f
Update Maven dependency versions for docs
mabartos Feb 1, 2024
64b5920
Add authentication test to cypress (#26337) (#26563)
Aboullos Feb 1, 2024
eb46a2f
Update AuthenticationManager.java (#26586)
alcalin Feb 1, 2024
9280111
Update teams.yml
stianst Feb 1, 2024
23998de
fixed disabling unassign on cancel (#26693)
agagancarczyk Feb 1, 2024
2e2d5ca
Bump @types/uuid from 9.0.7 to 9.0.8 in /js (#26522)
dependabot[bot] Feb 1, 2024
01c6d93
account library (#26373)
edewit Feb 1, 2024
a7157f6
Documentation for SuppressRefreshTokenRotationExecutor
mposolda Jan 31, 2024
9f4eef2
Bump @types/node from 20.11.13 to 20.11.15 in /js (#26705)
dependabot[bot] Feb 1, 2024
bee5afa
Bump i18next-http-backend from 2.4.2 to 2.4.3 in /js (#26672)
dependabot[bot] Feb 1, 2024
7982a65
Bump vite-plugin-checker from 0.6.2 to 0.6.4 in /js (#26704)
dependabot[bot] Feb 1, 2024
3fb203d
Bump lint-staged from 15.2.0 to 15.2.1 in /js (#26674)
dependabot[bot] Feb 1, 2024
22dc5f9
Bump i18next from 23.8.1 to 23.8.2 in /js (#26675)
dependabot[bot] Feb 1, 2024
1184271
Upgrade Husky to latest version (#26711)
jonkoops Feb 1, 2024
1ded464
Fixes typo in Server Administration guide (#26543)
christian-2 Feb 1, 2024
d4a4cb5
Remove un-maintained domain-extension example (#26709)
stianst Feb 2, 2024
9ff366f
Bump @testing-library/jest-dom from 6.4.0 to 6.4.1 in /js (#26726)
dependabot[bot] Feb 2, 2024
6f33c17
Bump @types/react from 18.2.48 to 18.2.51 in /js (#26724)
dependabot[bot] Feb 2, 2024
3978233
Bump @playwright/test from 1.41.1 to 1.41.2 in /js (#26722)
dependabot[bot] Feb 2, 2024
f55d16b
Bump react-hook-form from 7.49.3 to 7.50.0 in /js (#26721)
dependabot[bot] Feb 2, 2024
54145ca
Bump husky from 9.0.9 to 9.0.10 in /js (#26719)
dependabot[bot] Feb 2, 2024
0f0d2ec
Bump @types/node from 20.11.15 to 20.11.16 in /js (#26720)
dependabot[bot] Feb 2, 2024
36f79bb
removing duplicate open id scope (#26542)
ShefeeqPM Feb 2, 2024
7478a57
Update CODEOWNERS
stianst Feb 2, 2024
9ffdc61
Improve ScheduledTask task-name handling
thomasdarimont Sep 26, 2023
698bbce
Remove RealmModel from authorization services interfaces (#26708)
mhajas Feb 2, 2024
af854a8
Upgrade to Quarkus 3.7.1 (#26736)
vmuzikar Feb 2, 2024
1b2648a
Bump @vitejs/plugin-react-swc from 3.5.0 to 3.6.0 in /js (#26676)
dependabot[bot] Feb 2, 2024
d04ab27
Reviewing the user profile documentation
pedroigor Feb 1, 2024
5bab790
Supported option to specify maximum threads used to handle HTTP reque…
Pepo48 Feb 2, 2024
bb54227
Tooltip for attribute name in user-profile configuration is incorrect
mposolda Feb 2, 2024
7447674
Bump prettier from 3.2.4 to 3.2.5 in /js
dependabot[bot] Feb 5, 2024
16e70ea
Bump react-i18next from 14.0.1 to 14.0.2 in /js
dependabot[bot] Feb 5, 2024
1f88dfe
Bump @types/react from 18.2.51 to 18.2.52 in /js
dependabot[bot] Feb 5, 2024
ea9ca42
Bump @testing-library/react from 14.2.0 to 14.2.1 in /js
dependabot[bot] Feb 5, 2024
af901a4
Keep time units upon time entry
hmlnarik Feb 2, 2024
4bda70a
added comment about MySQL Server parameter sql_generate_invisible_pri…
ppaulis Feb 5, 2024
01a1dc6
Update HA guides to use the new ISPN config options
mhajas Feb 5, 2024
68b6824
Bump react-router-dom from 6.21.3 to 6.22.0 in /js
dependabot[bot] Feb 5, 2024
2242035
add Bundesagentur für Arbeit to ADOPTERS.md (#26784)
opdt Feb 5, 2024
c547460
update cpu sizing based on the hashing changes
kami619 Feb 1, 2024
a97b018
Update migration guide
ahus1 Feb 1, 2024
0638560
Fix Operator tests on ARM-based machines (#26786)
vmuzikar Feb 5, 2024
9ddcdea
Add actions for flaky Admin UI tests (#26778)
Aboullos Feb 5, 2024
b4a99b5
Upgrade liquibase to version 4.25.1
sguilhen Feb 2, 2024
68b2d79
Align JDBC drivers with Quarkus upstream
ahus1 Feb 5, 2024
ca0ea33
fix: using iteration to allow recursive structures (#26789)
shawkins Feb 5, 2024
c2348d0
Upgrade arquilliam bom to match org.apache.maven dependency versions …
pedroigor Feb 5, 2024
304e039
Bump lint-staged from 15.2.1 to 15.2.2 in /js (#26805)
dependabot[bot] Feb 6, 2024
a0af039
Bump @typescript-eslint/eslint-plugin from 6.20.0 to 6.21.0 in /js (#…
dependabot[bot] Feb 6, 2024
3e8d937
Bump @types/react from 18.2.52 to 18.2.54 in /js (#26800)
dependabot[bot] Feb 6, 2024
6b5a1d4
Bump react-i18next from 14.0.2 to 14.0.3 in /js (#26802)
dependabot[bot] Feb 6, 2024
50e0237
Bump @testing-library/jest-dom from 6.4.1 to 6.4.2 in /js (#26801)
dependabot[bot] Feb 6, 2024
7a69591
Fixed/improved issues with effective message bundles tab (#26760)
agagancarczyk Feb 6, 2024
7becb71
Add Ukrainian localization (#26670)
Zernov-A Feb 6, 2024
7c3e817
Bump @typescript-eslint/parser from 6.20.0 to 6.21.0 in /js (#26803)
dependabot[bot] Feb 6, 2024
9efa86a
Update Keycloak operator example with correct hostname syntax (#26796)
BadgerOps Feb 6, 2024
51936b0
PKCE should return error if code_verifier sent but no code_challenge …
rmartinc Feb 5, 2024
3ccbb01
Update messages_pt_BR.properties
hmlnarik Feb 5, 2024
1f5fcab
Introduce index.ftl into base account theme
hmlnarik Jan 30, 2024
99a48ba
added refresh button (#26599)
edewit Feb 6, 2024
d2c5129
Use code from RestEasy to create and set cookies (#26558)
stianst Feb 6, 2024
a845945
fixed tabbing issues in dropdown forms (#26827)
agagancarczyk Feb 6, 2024
eb88b71
Ignore empty attribute values when retriveing boolean/int/long (#2672…
stianst Feb 6, 2024
f09ecb8
Fixed token expiration help text (#26831)
agagancarczyk Feb 6, 2024
e19d41b
Make label for Keycloak container images configurable
ahus1 Feb 6, 2024
947d3d1
Empty error message when validation issue due the PersonNameProhibite…
mposolda Feb 2, 2024
a8488b3
Improvements for test connection and authentication in the LDAP provider
rmartinc Jan 31, 2024
cfb92e4
CORS SPI
dteleguin Jan 27, 2024
a6efda9
CORS SPI - code review
dteleguin Feb 2, 2024
07e49fb
User profile migration documentation for default validations and stra…
mposolda Feb 2, 2024
0aa428e
Support custom common path (#22717)
kamontat Feb 7, 2024
68d3b70
fixed search results after removing chips from effective message bund…
agagancarczyk Feb 7, 2024
c059977
Bump @types/react from 18.2.54 to 18.2.55 in /js
dependabot[bot] Feb 7, 2024
5b16de6
Bump react-hook-form from 7.50.0 to 7.50.1 in /js
dependabot[bot] Feb 7, 2024
cc2a685
chore: clarify default creds in readme, add .vscode to gitignore (#26…
gitdallas Feb 7, 2024
11f6a88
Bump react-i18next from 14.0.3 to 14.0.5 in /js
dependabot[bot] Feb 7, 2024
caafab2
Removed duplicate refresh buttons (#26857)
agagancarczyk Feb 7, 2024
a015941
Add cookie type builder (#26848)
stianst Feb 7, 2024
39ba795
Update custom rest endpoint documentation and example
zak905 Jan 12, 2024
f14265c
Update examples/providers/rest/README.md
zak905 Jan 14, 2024
6332bb4
rephrase sentence in changes-22_0_0.adoc for more clarity
zak905 Jan 24, 2024
e6212f6
Added event for temporary lockout for brute force protector (#26630)
tsaarni Feb 7, 2024
c7f7ad1
fix documentation for resetting OTP in "reset credentials" flow (#26834)
mschnitzler Feb 7, 2024
9b76100
task: adds event diagnostics for failed tests (#26864)
shawkins Feb 7, 2024
880d470
Update HA guide about non-blocking probes (#26783)
ahus1 Feb 7, 2024
fe1f6a7
task: upgrade to osgi 6.0.0
shawkins Feb 6, 2024
8d75c72
Update pom.xml
shawkins Feb 6, 2024
a8aef62
Removing version overrides and further aligning with quarkus versions…
shawkins Feb 7, 2024
9a34b9a
Brute Force Detection is disabled when updating frontenUrl via admin …
douglaspalmer Jan 31, 2024
273d612
blah
douglaspalmer Feb 2, 2024
0be7466
Remove section on adding custom attributes with account v1 and custom…
stianst Feb 8, 2024
3664c64
Bump @types/react-dom from 18.2.18 to 18.2.19 in /js
dependabot[bot] Feb 8, 2024
45e2a49
check consents for scopes from subject token
cavuugroup Feb 8, 2024
5c5c568
Fix confusing SAML NameId mapper format tooltip
mhajas Jan 26, 2024
2f29856
Fix for freemarker template URL sanitization in template.ftl (#26838)
dominiktopp Feb 8, 2024
d18f263
Merge branch 'keycloak:main' into issue/20702
cavuugroup Feb 8, 2024
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 @@ -20,6 +20,7 @@
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.TokenVerifier;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.ExchangeExternalToken;
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
Expand All @@ -29,6 +30,7 @@
import org.keycloak.broker.provider.IdentityProviderMapperSyncModeDelegate;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.Profile;
import org.keycloak.common.VerificationException;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.common.util.Base64Url;
import org.keycloak.events.Details;
Expand All @@ -38,6 +40,7 @@
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderMapperModel;
Expand Down Expand Up @@ -84,7 +87,9 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
Expand Down Expand Up @@ -126,7 +131,7 @@ public Response exchange(TokenExchangeContext context) {
this.event = context.getEvent();
this.clientConnection = context.getClientConnection();
this.headers = context.getHeaders();
this.tokenManager = (TokenManager)context.getTokenManager();
this.tokenManager = (TokenManager) context.getTokenManager();
this.clientAuthAttributes = context.getClientAuthAttributes();
return tokenExchange();
}
Expand Down Expand Up @@ -255,7 +260,7 @@ protected Response tokenExchange() {
}
}
}
}
}
}

protected Response exchangeToIdentityProvider(UserModel targetUser, UserSessionModel targetUserSession, String requestedIssuer) {
Expand Down Expand Up @@ -311,10 +316,28 @@ protected Response exchangeClientToClient(UserModel targetUser, UserSessionModel
}
}

if (targetClient.isConsentRequired()) {
event.detail(Details.REASON, "audience requires consent");
event.error(Errors.CONSENT_DENIED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
String scopesFromSubjectToken = null;
String subjectToken = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN);
if (subjectToken != null) {
try {
scopesFromSubjectToken = TokenVerifier.create(subjectToken, AccessToken.class).parse().getToken().getScope();
} catch (VerificationException e) {
event.detail(Details.REASON, "unable to parse jwt subject_token");
event.error(Errors.INVALID_TOKEN);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_TOKEN, "Invalid subject token", Response.Status.BAD_REQUEST);
}
}

// verification of existing users consents for requested scopes of target client
if (!TokenManager.verifyConsentStillAvailable(session, targetUser, targetClient, TokenManager.getRequestedClientScopes(scopesFromSubjectToken, client))) {
event.error(Errors.NOT_ALLOWED);
event.detail(Details.REASON, String.format(
"Missing some consents of [%s] for audience [%s]",
TokenManager.getRequestedClientScopes(scopesFromSubjectToken, client)
.map(ClientScopeModel::getName)
.collect(Collectors.joining(",")),
targetClient.getClientId()));
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_SCOPE, "Client requires user consent", Response.Status.BAD_REQUEST);
}

boolean isClientTheAudience = client.equals(targetClient);
Expand Down Expand Up @@ -430,7 +453,7 @@ protected Response exchangeClientToOIDCClient(UserModel targetUser, UserSessionM
}

if (requestedTokenType.equals(OAuth2Constants.REFRESH_TOKEN_TYPE)
&& OIDCAdvancedConfigWrapper.fromClientModel(client).isUseRefreshToken()) {
&& OIDCAdvancedConfigWrapper.fromClientModel(client).isUseRefreshToken()) {
responseBuilder.generateRefreshToken();
responseBuilder.getRefreshToken().issuedFor(client.getClientId());
}
Expand All @@ -442,7 +465,7 @@ protected Response exchangeClientToOIDCClient(UserModel targetUser, UserSessionM

AccessTokenResponse res = responseBuilder.build();
event.detail(Details.AUDIENCE, targetClient.getClientId())
.user(targetUser);
.user(targetUser);

event.success();

Expand Down Expand Up @@ -492,7 +515,7 @@ protected Response exchangeClientToSAML2Client(UserModel targetUser, UserSession
res.setOtherClaims(OAuth2Constants.ISSUED_TOKEN_TYPE, requestedTokenType);

event.detail(Details.AUDIENCE, targetClient.getClientId())
.user(targetUser);
.user(targetUser);

event.success();

Expand Down Expand Up @@ -557,12 +580,12 @@ protected UserModel importUserFromExternalIdentity(BrokeredIdentityContext conte
.collect(Collectors.toSet());
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
for (IdentityProviderMapperModel mapper : mappers) {
IdentityProviderMapper target = (IdentityProviderMapper)sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
IdentityProviderMapper target = (IdentityProviderMapper) sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
target.preprocessFederatedIdentity(session, realm, mapper, context);
}

UserModel user = null;
if (! context.getIdpConfig().isTransientUsers()) {
if (!context.getIdpConfig().isTransientUsers()) {
FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(providerId, context.getId(),
context.getUsername(), context.getToken());

Expand Down Expand Up @@ -610,7 +633,7 @@ protected UserModel importUserFromExternalIdentity(BrokeredIdentityContext conte
user.setLastName(context.getLastName());


if (! context.getIdpConfig().isTransientUsers()) {
if (!context.getIdpConfig().isTransientUsers()) {
FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(context.getIdpConfig().getAlias(), context.getId(),
context.getModelUsername(), context.getToken());
session.users().addFederatedIdentity(realm, user, federatedIdentityModel);
Expand All @@ -619,7 +642,7 @@ protected UserModel importUserFromExternalIdentity(BrokeredIdentityContext conte
context.getIdp().importNewUser(session, realm, user, context);

for (IdentityProviderMapperModel mapper : mappers) {
IdentityProviderMapper target = (IdentityProviderMapper)sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
IdentityProviderMapper target = (IdentityProviderMapper) sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
target.importNewUser(session, realm, user, mapper, context);
}

Expand Down Expand Up @@ -650,7 +673,7 @@ protected UserModel importUserFromExternalIdentity(BrokeredIdentityContext conte
context.getIdp().updateBrokeredUser(session, realm, user, context);

for (IdentityProviderMapperModel mapper : mappers) {
IdentityProviderMapper target = (IdentityProviderMapper)sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
IdentityProviderMapper target = (IdentityProviderMapper) sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
IdentityProviderMapperSyncModeDelegate.delegateUpdateBrokeredUser(session, realm, user, mapper, context, target);
}
}
Expand Down
< CBA5 td class="blob-code blob-code-addition js-file-line">
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
Expand All @@ -52,6 +53,7 @@
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected;
import org.keycloak.testsuite.util.AccountHelper;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.util.BasicAuthHelper;

Expand All @@ -63,6 +65,9 @@
import jakarta.ws.rs.core.Response;

import java.util.Arrays;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -241,6 +246,7 @@ public static void setupRealm(KeycloakSession session) {
ResourceServer server = management.realmResourceServer();
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(server, clientRep);
management.clients().exchangeToPermission(target).addAssociatedPolicy(clientPolicy);
management.clients().exchangeToPermission(realm.getClientByClientId("target-with-consent")).addAssociatedPolicy(clientPolicy);

// permission for user impersonation for a client

Expand Down Expand Up @@ -279,6 +285,19 @@ public static void setUpUserImpersonatePermissions(KeycloakSession session) {
userImpersonationPermission.addAssociatedPolicy(clientsAllowedToImpersonate);
}

public static void setUpTokenExchangeConsent(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName(TEST);
ClientModel targetClientRequiredConsent = session.clients().getClientByClientId(realm, "target-with-consent");
UserConsentModel userConsentModel = new UserConsentModel(targetClientRequiredConsent);
userConsentModel.setCreatedDate(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());

session.clients().getClientScopes(realm, targetClientRequiredConsent, true).values().forEach(userConsentModel::addGrantedClientScope);
session.clients().getClientScopes(realm, targetClientRequiredConsent, false).values().forEach(userConsentModel::addGrantedClientScope);
userConsentModel.setLastUpdatedDate(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());

session.users().addConsent(realm, session.users().getUserByUsername(realm, "user").getId(), userConsentModel);
}

@Override
protected boolean isImportAfterEachMethod() {
return true;
Expand Down Expand Up @@ -967,6 +986,43 @@ public void testClientExchange() throws Exception {
assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode());
}

@Test
public void testClientExchangeBadRequestWhenNoConsent() throws Exception {
testingClient.server().run(ClientTokenExchangeTest::setupRealm);

oauth.realm(TEST);
oauth.clientId("direct-legal");
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password");
String accessToken = response.getAccessToken();
TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));

response = oauth.doTokenExchange(TEST, accessToken, "target-with-consent", "direct-legal", "secret");
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());

}
@Test
public void testClientExchangeOKWhenNoConsentExists() throws Exception {
testingClient.server().run(ClientTokenExchangeTest::setupRealm);
testingClient.server().run(ClientTokenExchangeTest::setUpTokenExchangeConsent);

oauth.realm(TEST);
oauth.clientId("direct-legal");
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password");
String accessToken = response.getAccessToken();
TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));

response = oauth.doTokenExchange(TEST, accessToken, "target-with-consent", "direct-legal", "secret");
assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode());

}

@Test
public void testPublicClientNotAllowed() throws Exception {
testingClient.server().run(ClientTokenExchangeTest::setupRealm);
Expand Down Expand Up @@ -1025,6 +1081,17 @@ private static void addDirectExchanger(KeycloakSession session) {
target.setFullScopeAllowed(false);
target.addScopeMapping(exampleRole);

ClientModel targetWithConsentRequired = realm.addClient("target-with-consent");
targetWithConsentRequired.setName("target-with-consent");
targetWithConsentRequired.setClientId("target-with-consent");
targetWithConsentRequired.setDirectAccessGrantsEnabled(true);
targetWithConsentRequired.setEnabled(true);
targetWithConsentRequired.setSecret("secret");
targetWithConsentRequired.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
targetWithConsentRequired.setFullScopeAllowed(false);
targetWithConsentRequired.addScopeMapping(exampleRole);
targetWithConsentRequired.setConsentRequired(true);

ClientModel directExchanger = realm.addClient("direct-exchanger");
directExchanger.setName("direct-exchanger");
directExchanger.setClientId("direct-exchanger");
Expand All @@ -1037,6 +1104,7 @@ private static void addDirectExchanger(KeycloakSession session) {

// permission for client to client exchange to "target" client
management.clients().setPermissionsEnabled(target, true);
management.clients().setPermissionsEnabled(targetWithConsentRequired, true);

ClientPolicyRepresentation clientImpersonateRep = new ClientPolicyRepresentation();
clientImpersonateRep.setName("clientImpersonatorsDirect");
Expand Down
0