2121
2222import jakarta .ws .rs .core .Response ;
2323import org .hamcrest .MatcherAssert ;
24+ import org .jboss .arquillian .graphene .page .Page ;
2425import org .junit .Rule ;
2526import org .junit .Test ;
2627import org .keycloak .OAuth2Constants ;
2728import org .keycloak .OAuthErrorException ;
2829import org .keycloak .TokenVerifier ;
2930import org .keycloak .admin .client .resource .ClientResource ;
31+ import org .keycloak .admin .client .resource .UserResource ;
3032import org .keycloak .common .Profile ;
3133import org .keycloak .protocol .oidc .OIDCConfigAttributes ;
3234import org .keycloak .representations .AccessToken ;
3941import org .keycloak .testsuite .admin .ApiUtil ;
4042import org .keycloak .testsuite .arquillian .annotation .EnableFeature ;
4143import org .keycloak .testsuite .arquillian .annotation .UncaughtServerErrorExpected ;
44+ import org .keycloak .testsuite .pages .ConsentPage ;
45+ import org .keycloak .testsuite .updaters .ClientAttributeUpdater ;
4246import org .keycloak .testsuite .util .oauth .AccessTokenResponse ;
4347import org .keycloak .util .TokenUtil ;
4448
@@ -62,6 +66,9 @@ public class StandardTokenExchangeV2Test extends AbstractKeycloakTest {
6266 @ Rule
6367 public AssertEvents events = new AssertEvents (this );
6468
69+ @ Page
70+ protected ConsentPage consentPage ;
71+
6572 @ Override
6673 public void addTestRealms (List <RealmRepresentation > testRealms ) {
6774 RealmRepresentation testRealm = loadJson (getClass ().getResourceAsStream ("/token-exchange/testrealm-token-exchange-v2.json" ), RealmRepresentation .class );
@@ -81,8 +88,21 @@ private String resourceOwnerLogin(String username, String password, String clien
8188 oauth .scope (null );
8289 oauth .openid (false );
8390 AccessTokenResponse response = oauth .doGrantAccessTokenRequest (username , password );
91+ assertEquals (Response .Status .OK .getStatusCode (), response .getStatusCode ());
8492 TokenVerifier <AccessToken > accessTokenVerifier = TokenVerifier .create (response .getAccessToken (), AccessToken .class );
85- AccessToken token = accessTokenVerifier .parse ().getToken ();
93+ accessTokenVerifier .parse ();
94+ return response .getAccessToken ();
95+ }
96+
97+ private String loginWithConsents (String username , String password , String clientId , String secret ) throws Exception {
98+ oauth .client (clientId , secret ).doLogin (username , password );
99+ consentPage .assertCurrent ();
100+ consentPage .confirm ();
101+ assertNotNull (oauth .getCurrentQuery ().get (OAuth2Constants .CODE ));
102+ AccessTokenResponse response = oauth .doAccessTokenRequest (oauth .getCurrentQuery ().get (OAuth2Constants .CODE ));
103+ assertEquals (Response .Status .OK .getStatusCode (), response .getStatusCode ());
104+ TokenVerifier <AccessToken > accessTokenVerifier = TokenVerifier .create (response .getAccessToken (), AccessToken .class );
105+ accessTokenVerifier .parse ();
86106 return response .getAccessToken ();
87107 }
88108
@@ -234,22 +254,18 @@ public void testClientExchangeToItselfWithConsents() throws Exception {
234254 oauth .realm (TEST );
235255 String accessToken = resourceOwnerLogin ("john" , "password" ,"subject-client" , "secret" );
236256
237- ClientResource client = ApiUtil .findClientByClientId (adminClient .realm (TEST ), "subject-client" );
238- ClientRepresentation clientRepresentation = client .toRepresentation ();
239- clientRepresentation .setConsentRequired (Boolean .TRUE );
240- client .update (clientRepresentation );
241-
242- AccessTokenResponse response = tokenExchange (accessToken , "subject-client" , "secret" , null , null );
243- assertEquals (Response .Status .BAD_REQUEST .getStatusCode (), response .getStatusCode ());
244- assertEquals (OAuthErrorException .INVALID_CLIENT , response .getError ());
245- assertEquals ("Client requires user consent" , response .getErrorDescription ());
246-
247- response = tokenExchange (accessToken , "subject-client" , "secret" , List .of ("subject-client" ), null );
248- assertEquals (OAuthErrorException .INVALID_CLIENT , response .getError ());
249- assertEquals ("Client requires user consent" , response .getErrorDescription ());
250-
251- clientRepresentation .setConsentRequired (Boolean .FALSE );
252- client .update (clientRepresentation );
257+ try (ClientAttributeUpdater clientUpdater = ClientAttributeUpdater .forClient (adminClient , TEST , "subject-client" )
258+ .setConsentRequired (Boolean .TRUE )
259+ .update ()) {
260+ AccessTokenResponse response = tokenExchange (accessToken , "subject-client" , "secret" , null , null );
261+ assertEquals (Response .Status .BAD_REQUEST .getStatusCode (), response .getStatusCode ());
262+ assertEquals (OAuthErrorException .INVALID_SCOPE , response .getError ());
263+ assertEquals ("Missing consents for Token Exchange in client subject-client" , response .getErrorDescription ());
264+
265+ response = tokenExchange (accessToken , "subject-client" , "secret" , List .of ("subject-client" ), null );
266+ assertEquals (OAuthErrorException .INVALID_SCOPE , response .getError ());
267+ assertEquals ("Missing consents for Token Exchange in client subject-client" , response .getErrorDescription ());
268+ }
253269 }
254270
255271 @ Test
@@ -281,14 +297,14 @@ public void testUnavailableAudienceRequested() throws Exception {
281297 String accessToken = resourceOwnerLogin ("john" , "password" ,"subject-client" , "secret" );
282298 // request invalid client audience
283299 AccessTokenResponse response = tokenExchange (accessToken , "requester-client" , "secret" , List .of ("target-client1" , "invalid-client" ), null );
284- org . junit . Assert . assertEquals (Response .Status .BAD_REQUEST .getStatusCode (), response .getStatusCode ());
285- org . junit . Assert . assertEquals (OAuthErrorException .INVALID_CLIENT , response .getError ());
286- org . junit . Assert . assertEquals ("Audience not found" , response .getErrorDescription ());
300+ assertEquals (Response .Status .BAD_REQUEST .getStatusCode (), response .getStatusCode ());
301+ assertEquals (OAuthErrorException .INVALID_CLIENT , response .getError ());
302+ assertEquals ("Audience not found" , response .getErrorDescription ());
287303 // The "target-client3" is valid client, but audience unavailable to the user. Request not allowed
288304 response = tokenExchange (accessToken , "requester-client" , "secret" , List .of ("target-client1" , "target-client3" ), null );
289- org . junit . Assert . assertEquals (Response .Status .BAD_REQUEST .getStatusCode (), response .getStatusCode ());
290- org . junit . Assert . assertEquals (OAuthErrorException .INVALID_REQUEST , response .getError ());
291- org . junit . Assert . assertEquals ("Requested audience not available: target-client3" , response .getErrorDescription ());
305+ assertEquals (Response .Status .BAD_REQUEST .getStatusCode (), response .getStatusCode ());
306+ assertEquals (OAuthErrorException .INVALID_REQUEST , response .getError ());
307+ assertEquals ("Requested audience not available: target-client3" , response .getErrorDescription ());
292308 }
293309
294310 @ Test
@@ -299,24 +315,24 @@ public void testScopeNotAllowed() throws Exception {
299315 oauth .scope ("optional-scope3" );
300316 AccessTokenResponse response = tokenExchange (accessToken , "requester-client" , "secret" , List .of ("target-client1" , "target-client3" ), null );
301317 assertEquals (Response .Status .BAD_REQUEST .getStatusCode (), response .getStatusCode ());
302- org . junit . Assert . assertEquals (OAuthErrorException .INVALID_SCOPE , response .getError ());
303- org . junit . Assert . assertEquals ("Invalid scopes: optional-scope3" , response .getErrorDescription ());
318+ assertEquals (OAuthErrorException .INVALID_SCOPE , response .getError ());
319+ assertEquals ("Invalid scopes: optional-scope3" , response .getErrorDescription ());
304320
305321 //scope that doesn't exist
306322 oauth .scope ("bad-scope" );
307323 response = tokenExchange (accessToken , "requester-client" , "secret" , List .of ("target-client1" , "target-client3" ), null );
308324 assertEquals (Response .Status .BAD_REQUEST .getStatusCode (), response .getStatusCode ());
309- org . junit . Assert . assertEquals (OAuthErrorException .INVALID_SCOPE , response .getError ());
310- org . junit . Assert . assertEquals ("Invalid scopes: bad-scope" , response .getErrorDescription ());
325+ assertEquals (OAuthErrorException .INVALID_SCOPE , response .getError ());
326+ assertEquals ("Invalid scopes: bad-scope" , response .getErrorDescription ());
311327 }
312328
313329 @ Test
314330 public void testScopeFilter () throws Exception {
315- String accessToken = resourceOwnerLogin ("john" , "password" ,"subject-client" , "secret" );
331+ String accessToken = resourceOwnerLogin ("john" , "password" , "subject-client" , "secret" );
316332 AccessTokenResponse response = tokenExchange (accessToken , "requester-client" , "secret" , List .of ("target-client2" ), null );
317- org . junit . Assert . assertEquals (Response .Status .BAD_REQUEST .getStatusCode (), response .getStatusCode ());
318- org . junit . Assert . assertEquals (OAuthErrorException .INVALID_REQUEST , response .getError ());
319- org . junit . Assert . assertEquals ("Requested audience not available: target-client2" , response .getErrorDescription ());
333+ assertEquals (Response .Status .BAD_REQUEST .getStatusCode (), response .getStatusCode ());
334+ assertEquals (OAuthErrorException .INVALID_REQUEST , response .getError ());
335+ assertEquals ("Requested audience not available: target-client2" , response .getErrorDescription ());
320336
321337 oauth .scope ("optional-scope2" );
322338 response = tokenExchange (accessToken , "requester-client" , "secret" , List .of ("target-client2" ), null );
@@ -340,11 +356,11 @@ public void testScopeFilter() throws Exception {
340356
341357 @ Test
342358 public void testScopeParamIncludedAudienceIncludedRefreshToken () throws Exception {
343- String accessToken = resourceOwnerLogin ("mike" , "password" ,"subject-client" , "secret" );
359+ String accessToken = resourceOwnerLogin ("mike" , "password" , "subject-client" , "secret" );
344360 oauth .scope ("optional-scope2" );
345361 AccessTokenResponse response = tokenExchange (accessToken , "requester-client" , "secret" , List .of ("target-client1" ), Collections .singletonMap (OAuth2Constants .REQUESTED_TOKEN_TYPE , OAuth2Constants .REFRESH_TOKEN_TYPE ));
346362 assertAudiencesAndScopes (response , List .of ("target-client1" ), List .of ("default-scope1" , "optional-scope2" ));
347- org . junit . Assert . assertNotNull (response .getRefreshToken ());
363+ assertNotNull (response .getRefreshToken ());
348364
349365 oauth .client ("requester-client" , "secret" );
350366 response = oauth .doRefreshTokenRequest (response .getRefreshToken ());
@@ -363,6 +379,44 @@ public void testExchangeWithDynamicScopesEnabled() throws Exception {
363379 testingClient .disableFeature (Profile .Feature .DYNAMIC_SCOPES );
364380 }
365381
382+ @ Test
383+ public void testConsents () throws Exception {
384+ try (ClientAttributeUpdater clientUpdater = ClientAttributeUpdater .forClient (adminClient , TEST , "requester-client" )
385+ .setConsentRequired (Boolean .TRUE )
386+ .update ()) {
387+ // initial TE without any consent should fail
388+ String accessToken = resourceOwnerLogin ("mike" , "password" , "subject-client" , "secret" );
389+ AccessTokenResponse response = tokenExchange (accessToken , "requester-client" , "secret" , null , null );
390+ assertEquals (Response .Status .BAD_REQUEST .getStatusCode (), response .getStatusCode ());
391+ assertEquals (OAuthErrorException .INVALID_SCOPE , response .getError ());
392+ assertEquals ("Missing consents for Token Exchange in client requester-client" , response .getErrorDescription ());
393+
394+ // logout
395+ UserResource mike = ApiUtil .findUserByUsernameId (adminClient .realm (TEST ), "mike" );
396+ mike .logout ();
397+
398+ // perform a login and allow consent for default scopes, TE should work now
399+ accessToken = loginWithConsents ("mike" , "password" , "requester-client" , "secret" );
400+ response = tokenExchange (accessToken , "requester-client" , "secret" , null , null );
401+ assertAudiencesAndScopes (response , List .of ("target-client1" ), List .of ("default-scope1" ));
402+
403+ // request TE with optional-scope2 whose consent is missing, should fail
404+ oauth .scope ("optional-scope2" );
405+ response = tokenExchange (accessToken , "requester-client" , "secret" , null , null );
406+ assertEquals (Response .Status .BAD_REQUEST .getStatusCode (), response .getStatusCode ());
407+ assertEquals (OAuthErrorException .INVALID_SCOPE , response .getError ());
408+ assertEquals ("Missing consents for Token Exchange in client requester-client" , response .getErrorDescription ());
409+
410+ // logout
411+ mike .logout ();
412+
413+ // consent the additional scope, TE should work now
414+ accessToken = loginWithConsents ("mike" , "password" , "requester-client" , "secret" );
415+ response = tokenExchange (accessToken , "requester-client" , "secret" , null , null );
416+ assertAudiencesAndScopes (response , List .of ("target-client1" ), List .of ("default-scope1" , "optional-scope2" ));
417+ }
418+ }
419+
366420 private void assertAudiences (AccessToken token , List <String > expectedAudiences ) {
367421 MatcherAssert .assertThat ("Incompatible audiences" , token .getAudience () == null ? List .of () : List .of (token .getAudience ()), containsInAnyOrder (expectedAudiences .toArray ()));
368422 MatcherAssert .assertThat ("Incompatible resource access" , token .getResourceAccess ().keySet (), containsInAnyOrder (expectedAudiences .toArray ()));
@@ -373,10 +427,11 @@ private void assertScopes(AccessToken token, List<String> expectedScopes) {
373427 }
374428
375429 private void assertAudiencesAndScopes (AccessTokenResponse tokenExchangeResponse , List <String > expectedAudiences , List <String > expectedScopes ) throws Exception {
430+ assertEquals (Response .Status .OK .getStatusCode (), tokenExchangeResponse .getStatusCode ());
376431 TokenVerifier <AccessToken > accessTokenVerifier = TokenVerifier .create (tokenExchangeResponse .getAccessToken (), AccessToken .class );
377432 AccessToken token = accessTokenVerifier .parse ().getToken ();
378433 if (expectedAudiences == null ) {
379- org . junit . Assert . assertNull ("Expected token to not contain audience" , token .getAudience ());
434+ assertNull ("Expected token to not contain audience" , token .getAudience ());
380435 } else {
381436 assertAudiences (token , expectedAudiences );
382437 }
0 commit comments