1919
2020package org .keycloak .testsuite .client ;
2121
22+ import jakarta .ws .rs .core .Response ;
2223import java .util .Collections ;
2324import java .util .List ;
2425
2526import org .hamcrest .MatcherAssert ;
27+ import org .hamcrest .Matchers ;
2628import org .junit .Before ;
2729import org .junit .Test ;
30+ import org .keycloak .OAuthErrorException ;
2831import org .keycloak .client .registration .Auth ;
2932import org .keycloak .client .registration .ClientRegistrationException ;
33+ import org .keycloak .client .registration .HttpErrorException ;
3034import org .keycloak .protocol .oidc .OIDCAdvancedConfigWrapper ;
3135import org .keycloak .representations .idm .ClientInitialAccessCreatePresentation ;
3236import org .keycloak .representations .idm .ClientInitialAccessPresentation ;
3337import org .keycloak .representations .idm .ClientRepresentation ;
38+ import org .keycloak .representations .idm .OAuth2ErrorRepresentation ;
3439import org .keycloak .representations .oidc .OIDCClientRepresentation ;
40+ import org .keycloak .util .JsonSerialization ;
3541import org .keycloak .testsuite .Assert ;
3642
3743import static org .hamcrest .Matchers .containsInAnyOrder ;
4147import static org .keycloak .OAuth2Constants .IMPLICIT ;
4248import static org .keycloak .OAuth2Constants .PASSWORD ;
4349import static org .keycloak .OAuth2Constants .REFRESH_TOKEN ;
50+ import static org .keycloak .OAuth2Constants .TOKEN_EXCHANGE_GRANT_TYPE ;
4451import static org .keycloak .models .OAuth2DeviceConfig .OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED ;
4552import static org .keycloak .protocol .oidc .utils .OIDCResponseType .CODE ;
4653import static org .keycloak .protocol .oidc .utils .OIDCResponseType .ID_TOKEN ;
@@ -81,7 +88,7 @@ public void testClientWithoutResponseTypesAndGrantTypes() throws Exception {
8188
8289 assertOIDCResponse (response , List .of (CODE , NONE ), List .of (AUTHORIZATION_CODE , REFRESH_TOKEN ));
8390
84- assertKeycloakClient (response , true , false , false , false , true , false );
91+ assertKeycloakClient (response , true , false , false , false , true , false , false );
8592 }
8693
8794 @ Test
@@ -92,7 +99,7 @@ public void testResponseTypeCodeWithoutGrantTypes() throws Exception {
9299
93100 assertOIDCResponse (response , List .of (CODE , NONE ), List .of (AUTHORIZATION_CODE , REFRESH_TOKEN ));
94101
95- assertKeycloakClient (response , true , false , false , false , true , false );
102+ assertKeycloakClient (response , true , false , false , false , true , false , false );
96103 }
97104
98105 // Limitation of Keycloak switches (Standard flow, Implicit flow) means that enabling any hybrid grant type enables also implicit flow.
@@ -106,7 +113,7 @@ public void testResponseTypeCodeIDTokenWithoutGrantTypes() throws Exception {
106113 assertOIDCResponse (response , List .of (CODE , NONE , ID_TOKEN , "id_token token" , "code id_token" , "code token" , "code id_token token" ),
107114 List .of (AUTHORIZATION_CODE , IMPLICIT , REFRESH_TOKEN ));
108115
109- assertKeycloakClient (response , true , true , false , false , true , false );
116+ assertKeycloakClient (response , true , true , false , false , true , false , false );
110117 }
111118
112119 @ Test
@@ -118,7 +125,7 @@ public void testWithoutResponseTypeClientCredentialsGrant() throws Exception {
118125 assertOIDCResponse (response , Collections .emptyList (),
119126 List .of (CLIENT_CREDENTIALS ));
120127
121- assertKeycloakClient (response , false , false , false , true , false , false );
128+ assertKeycloakClient (response , false , false , false , true , false , false , false );
122129 }
123130
124131 @ Test
@@ -130,7 +137,7 @@ public void testWithoutResponseTypePasswordGrant() throws Exception {
130137 assertOIDCResponse (response , Collections .emptyList (),
131138 List .of (PASSWORD ));
132139
133- assertKeycloakClient (response , false , false , true , false , false , false );
140+ assertKeycloakClient (response , false , false , true , false , false , false , false );
134141 }
135142
136143 @ Test
@@ -142,7 +149,7 @@ public void testWithoutResponseTypePasswordClientCredentialsRefreshTokensGrants(
142149 assertOIDCResponse (response , Collections .emptyList (),
143150 List .of (PASSWORD , CLIENT_CREDENTIALS , REFRESH_TOKEN ));
144151
145- assertKeycloakClient (response , false , false , true , true , true , false );
152+ assertKeycloakClient (response , false , false , true , true , true , false , false );
146153 }
147154
148155 @ Test
@@ -153,7 +160,7 @@ public void testClientWithoutRefreshToken() throws Exception {
153160
154161 assertOIDCResponse (response , List .of (CODE , NONE ), List .of (AUTHORIZATION_CODE ));
155162
156- assertKeycloakClient (response , true , false , false , false , false , false );
163+ assertKeycloakClient (response , true , false , false , false , false , false , false );
157164 }
158165
159166 @ Test
@@ -164,7 +171,7 @@ public void testClientWithRefreshToken() throws Exception {
164171
165172 assertOIDCResponse (response , List .of (CODE , NONE ), List .of (AUTHORIZATION_CODE , REFRESH_TOKEN ));
166173
167- assertKeycloakClient (response , true , false , false , false , true , false );
174+ assertKeycloakClient (response , true , false , false , false , true , false , false );
168175 }
169176
170177 @ Test
@@ -175,7 +182,27 @@ public void testNoResponseTypesWithDeviceGrant() throws Exception {
175182
176183 assertOIDCResponse (response , Collections .emptyList (), List .of (DEVICE_CODE_GRANT_TYPE ));
177184
178- assertKeycloakClient (response , false , false , false , false , false , true );
185+ assertKeycloakClient (response , false , false , false , false , false , true , false );
186+ }
187+
188+ @ Test
189+ public void testGrantTypeTokenExchange () throws Exception {
190+ OIDCClientRepresentation clientRep = createRep (null , List .of (AUTHORIZATION_CODE , REFRESH_TOKEN , TOKEN_EXCHANGE_GRANT_TYPE ));
191+ clientRep .setTokenEndpointAuthMethod ("none" );
192+
193+ ClientRegistrationException clientRegistrationException = Assert .assertThrows (ClientRegistrationException .class , () -> reg .oidc ().create (clientRep ));
194+ MatcherAssert .assertThat (clientRegistrationException .getCause (), Matchers .instanceOf (HttpErrorException .class ));
195+ HttpErrorException httpErrorException = (HttpErrorException ) clientRegistrationException .getCause ();
196+ Assert .assertEquals (Response .Status .BAD_REQUEST .getStatusCode (), httpErrorException .getStatusLine ().getStatusCode ());
197+ OAuth2ErrorRepresentation error = JsonSerialization .readValue (httpErrorException .getErrorResponse (), OAuth2ErrorRepresentation .class );
198+ Assert .assertEquals (OAuthErrorException .INVALID_CLIENT_METADATA , error .getError ());
199+
200+ clientRep .setTokenEndpointAuthMethod (null );
201+ OIDCClientRepresentation response = reg .oidc ().create (clientRep );
202+
203+ assertOIDCResponse (response , List .of (CODE , NONE ), List .of (AUTHORIZATION_CODE , REFRESH_TOKEN , TOKEN_EXCHANGE_GRANT_TYPE ));
204+
205+ assertKeycloakClient (response , true , false , false , false , true , false , true );
179206 }
180207
181208 @ Test
@@ -186,7 +213,7 @@ public void testCodeResponseTypeWithMoreGrants() throws Exception {
186213
187214 assertOIDCResponse (response , List .of (CODE , NONE ), List .of (AUTHORIZATION_CODE , REFRESH_TOKEN , PASSWORD , CLIENT_CREDENTIALS ));
188215
189- assertKeycloakClient (response , true , false , true , true , true , false );
216+ assertKeycloakClient (response , true , false , true , true , true , false , false );
190217 }
191218
192219 // Grant type "authorization_code" added automatically because of response_type "code" .
@@ -201,7 +228,7 @@ public void testCodeResponseTypeWithIncompatibleGrants() throws Exception {
201228
202229 assertOIDCResponse (response , List .of (CODE , NONE ), List .of (AUTHORIZATION_CODE , REFRESH_TOKEN , PASSWORD , CLIENT_CREDENTIALS ));
203230
204- assertKeycloakClient (response , true , false , true , true , true , false );
231+ assertKeycloakClient (response , true , false , true , true , true , false , false );
205232 }
206233
207234 private void assertOIDCResponse (OIDCClientRepresentation response , List <String > expectedResponseTypes , List <String > expectedGrantTypes ) {
@@ -228,7 +255,8 @@ private void assertKeycloakClient(OIDCClientRepresentation response,
228255 boolean expectedDirectGrantFlow ,
229256 boolean expectedServiceAccountsFlow ,
230257 boolean expectedRefreshToken ,
231- boolean expectedDeviceGrant ) {
258+ boolean expectedDeviceGrant ,
259+ boolean expectedTokenExchange ) {
232260 ClientRepresentation kcClient = getClient (response .getClientId ());
233261 OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper .fromClientRepresentation (kcClient );
234262 Assert .assertEquals ("Expected standard flow: " + expectedStandardFlow + " did not match." , expectedStandardFlow , kcClient .isStandardFlowEnabled ());
@@ -239,5 +267,6 @@ private void assertKeycloakClient(OIDCClientRepresentation response,
239267 Assert .assertFalse ("Don't expect refresh token for client credentials grant enabled" , config .isUseRefreshTokenForClientCredentialsGrant ());
240268 boolean deviceEnabled = kcClient .getAttributes () != null && Boolean .parseBoolean (kcClient .getAttributes ().get (OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED ));
241269 Assert .assertEquals ("Expected device: " + expectedDeviceGrant + " did not match." , expectedDeviceGrant , deviceEnabled );
270+ Assert .assertEquals ("Expected Token Exchange: " + expectedTokenExchange + " did not match." , expectedTokenExchange , config .isStandardTokenExchangeEnabled ());
242271 }
243272}
0 commit comments