Skip to content

Commit d4e6bc8

Browse files
committed
Sync after Keycloak server 26.4.0 release
closes #187 Signed-off-by: mposolda <mposolda@gmail.com>
1 parent 91a0bff commit d4e6bc8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1942
-210
lines changed

.github/workflows/run-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
timeout-minutes: 30
1515
strategy:
1616
matrix:
17-
keycloak_server_version: [ "26.0", "26.2", "26.3", "nightly" ]
17+
keycloak_server_version: [ "26.0", "26.2", "26.4", "nightly" ]
1818
steps:
1919
- uses: actions/checkout@v4
2020

admin-client/src/main/java/org/keycloak/admin/client/Config.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class Config {
3333
private String clientSecret;
3434
private String grantType;
3535
private String scope;
36+
private boolean useDPoP = false;
3637

3738
public Config(String serverUrl, String realm, String username, String password, String clientId, String clientSecret) {
3839
this(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, null);
@@ -125,4 +126,12 @@ public static void checkGrantType(String grantType) {
125126
" (only " + PASSWORD + " and " + CLIENT_CREDENTIALS + " are supported)");
126127
}
127128
}
129+
130+
public boolean isUseDPoP() {
131+
return useDPoP;
132+
}
133+
134+
public void setUseDPoP(boolean useDPoP) {
135+
this.useDPoP = useDPoP;
136+
}
128137
}

admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
*/
1717
package org.keycloak.admin.client;
1818

19+
import jakarta.ws.rs.client.ClientRequestFilter;
1920
import jakarta.ws.rs.client.WebTarget;
2021
import org.keycloak.admin.client.resource.BearerAuthFilter;
22+
import org.keycloak.admin.client.resource.DPoPAuthFilter;
2123
import org.keycloak.admin.client.resource.RealmResource;
2224
import org.keycloak.admin.client.resource.RealmsResource;
2325
import org.keycloak.admin.client.resource.ServerInfoResource;
@@ -84,8 +86,9 @@ public static ResteasyClientProvider getClientProvider() {
8486
private final Client client;
8587
private boolean closed = false;
8688

87-
Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, Client resteasyClient, String authtoken, String scope) {
89+
Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, Client resteasyClient, String authtoken, String scope, boolean useDPoP) {
8890
config = new Config(serverUrl, realm, username, password, clientId, clientSecret, grantType, scope);
91+
config.setUseDPoP(useDPoP);
8992
client = resteasyClient != null ? resteasyClient : newRestEasyClient(null, null, false);
9093
authToken = authtoken;
9194
tokenManager = authtoken == null ? new TokenManager(config, client) : null;
@@ -98,42 +101,87 @@ private static Client newRestEasyClient(Object customJacksonProvider, SSLContext
98101
return CLIENT_PROVIDER.newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager);
99102
}
100103

101-
private BearerAuthFilter newAuthFilter() {
104+
private ClientRequestFilter newAuthFilter() {
105+
if (config.isUseDPoP()) {
106+
if (authToken != null) throw new IllegalArgumentException("Not supported to require DPoP when token is provisioned");
107+
return new DPoPAuthFilter(tokenManager, false);
108+
}
102109
return authToken != null ? new BearerAuthFilter(authToken) : new BearerAuthFilter(tokenManager);
103110
}
104-
111+
112+
/**
113+
*
114+
* Creates the java admin client instance to be used to call admin REST API against Keycloak server.
115+
*
116+
* @param serverUrl Keycloak server URL
117+
* @param realm realm name
118+
* @param username username of the admin user to be used.
119+
* @param password password of the admin user
120+
* @param clientId client ID
121+
* @param clientSecret client secret. Could be left null in case that clientId parameter points to the public client, which does not require client authentication
122+
* @param sslContext ssl context. Could be left null in case that default SSL context should be used.
123+
* @param customJacksonProvider custom Jackson provider. Could be left null in case that Jackson provider will be automatically provided by the admin client. Please see <a href="https://www.keycloak.org/securing-apps/admin-client#_admin_client_compatibility">the documentation</a> for additional details regarding the compatibility
124+
* @param disableTrustManager If to disable trust manager for SSL checks. It is false by default. The value true should be used just for the development purposes, but should not be used in production
125+
* @param authToken access token to be used to call admin REST API. This can be left null if you want admin client to login the user (based on the parameters username, password, clientId and clientSecret) and manage it's own login session. But in case you already have existing session, you can inject the existing access token with the use of this parameter. In that case, it is recommended to leave the properties username, password, clientId or clientSecret empty
126+
* @param scope Custom "scope" parameter to be used. Could be left null in case of default scope should be used. That is sufficient for most of the cases.
127+
* @return Java admin client instance
128+
*/
105129
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext, Object customJacksonProvider, boolean disableTrustManager, String authToken, String scope) {
106-
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager), authToken, scope);
130+
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager), authToken, scope, false);
107131
}
108132

133+
/**
134+
* See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values
135+
*/
109136
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext, Object customJacksonProvider, boolean disableTrustManager, String authToken) {
110-
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager), authToken, null);
137+
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager), authToken, null, false);
111138
}
112139

140+
/**
141+
* See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values
142+
*/
113143
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret) {
114144
return getInstance(serverUrl, realm, username, password, clientId, clientSecret, null, null, false, null);
115145
}
116146

147+
/**
148+
* See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values
149+
*/
117150
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext) {
118151
return getInstance(serverUrl, realm, username, password, clientId, clientSecret, sslContext, null, false, null);
119152
}
120153

154+
/**
155+
* See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values
156+
*/
121157
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext, Object customJacksonProvider) {
122158
return getInstance(serverUrl, realm, username, password, clientId, clientSecret, sslContext, customJacksonProvider, false, null);
123159
}
124160

161+
/**
162+
* See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values
163+
*/
125164
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId) {
126165
return getInstance(serverUrl, realm, username, password, clientId, null, null, null, false, null);
127166
}
128167

168+
/**
169+
* See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values
170+
*/
129171
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, SSLContext sslContext) {
130172
return getInstance(serverUrl, realm, username, password, clientId, null, sslContext, null, false, null);
131173
}
132174

175+
/**
176+
* See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values
177+
*/
133178
public static Keycloak getInstance(String serverUrl, String realm, String clientId, String authToken) {
134179
return getInstance(serverUrl, realm, null, null, clientId, null, null, null, false, authToken);
135180
}
136181

182+
/**
183+
* See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values
184+
*/
137185
public static Keycloak getInstance(String serverUrl, String realm, String clientId, String authToken, SSLContext sllSslContext) {
138186
return getInstance(serverUrl, realm, null, null, clientId, null, sllSslContext, null, false, authToken);
139187
}

admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
package org.keycloak.admin.client;
1919

20-
import static org.keycloak.OAuth2Constants.CLIENT_CREDENTIALS;
2120
import static org.keycloak.OAuth2Constants.PASSWORD;
2221

2322
import jakarta.ws.rs.client.Client;
@@ -35,7 +34,10 @@
3534
* .password("pass")
3635
* .clientId("client")
3736
* .clientSecret("secret")
38-
* .resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build())
37+
* .resteasyClient(new ResteasyClientBuilderImpl()
38+
* .connectionPoolSize(20)
39+
* .build()
40+
* .register(org.keycloak.admin.client.JacksonProvider.class, 100))
3941
* .build();
4042
* </pre>
4143
* <p>Example usage with grant_type=client_credentials</p>
@@ -63,6 +65,7 @@ public class KeycloakBuilder {
6365
private Client resteasyClient;
6466
private String authorization;
6567
private String scope;
68+
private boolean useDPoP = false;
6669

6770
public KeycloakBuilder serverUrl(String serverUrl) {
6871
this.serverUrl = serverUrl;
@@ -105,6 +108,12 @@ public KeycloakBuilder clientSecret(String clientSecret) {
105108
return this;
106109
}
107110

111+
/**
112+
* Custom instance of resteasy client. Please see <a href="https://www.keycloak.org/securing-apps/admin-client#_admin_client_compatibility">the documentation</a> for additional details regarding the compatibility
113+
*
114+
* @param resteasyClient Custom RestEasy client
115+
* @return admin client builder
116+
*/
108117
public KeycloakBuilder resteasyClient(Client resteasyClient) {
109118
this.resteasyClient = resteasyClient;
110119
return this;
@@ -115,6 +124,17 @@ public KeycloakBuilder authorization(String auth) {
115124
return this;
116125
}
117126

127+
/**
128+
* @param useDPoP If true, then admin-client will add DPoP proofs to the token-requests and to the admin REST API requests. DPoP feature must be
129+
* enabled on Keycloak server side to work properly. It is false by default. Parameter is supposed to be used with Keycloak server 26.4.0 or later as
130+
* earlier versions did not support DPoP requests for admin REST API
131+
* @return admin client builder
132+
*/
133+
public KeycloakBuilder useDPoP(boolean useDPoP) {
134+
this.useDPoP = useDPoP;
135+
return this;
136+
}
137+
118138
/**
119139
* Builds a new Keycloak client from this builder.
120140
*/
@@ -145,7 +165,7 @@ public Keycloak build() {
145165
throw new IllegalStateException("clientId required");
146166
}
147167

148-
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, grantType, resteasyClient, authorization, scope);
168+
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, grantType, resteasyClient, authorization, scope, useDPoP);
149169
}
150170

151171
private KeycloakBuilder() {

admin-client/src/main/java/org/keycloak/admin/client/resource/BearerAuthFilter.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class BearerAuthFilter implements ClientRequestFilter, ClientResponseFilt
3434

3535
public static final String AUTH_HEADER_PREFIX = "Bearer ";
3636
private final String tokenString;
37-
private final TokenManager tokenManager;
37+
protected final TokenManager tokenManager;
3838

3939
public BearerAuthFilter(String tokenString) {
4040
this.tokenString = tokenString;
@@ -66,12 +66,17 @@ public void filter(ClientRequestContext requestContext, ClientResponseContext re
6666
for (Object authHeader : authHeaders) {
6767
if (authHeader instanceof String) {
6868
String headerValue = (String) authHeader;
69-
if (headerValue.startsWith(AUTH_HEADER_PREFIX)) {
70-
String token = headerValue.substring( AUTH_HEADER_PREFIX.length() );
69+
String authHeaderPrefix = getAuthHeaderPrefix();
70+
if (headerValue.startsWith(authHeaderPrefix)) {
71+
String token = headerValue.substring( authHeaderPrefix.length() );
7172
tokenManager.invalidate( token );
7273
}
7374
}
7475
}
7576
}
7677
}
78+
79+
protected String getAuthHeaderPrefix() {
80+
return AUTH_HEADER_PREFIX;
81+
}
7782
}

admin-client/src/main/java/org/keycloak/admin/client/resource/ClientsResource.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.keycloak.representations.idm.ClientRepresentation;
2121

2222
import jakarta.ws.rs.Consumes;
23+
import jakarta.ws.rs.DELETE;
2324
import jakarta.ws.rs.GET;
2425
import jakarta.ws.rs.POST;
2526
import jakarta.ws.rs.Path;
@@ -66,4 +67,8 @@ List<ClientRepresentation> findAll(@QueryParam("clientId") String clientId,
6667
@Produces(MediaType.APPLICATION_JSON)
6768
List<ClientRepresentation> query(@QueryParam("q") String searchQuery);
6869

70+
@Path("{id}")
71+
@DELETE
72+
Response delete(@PathParam("id") String id);
73+
6974
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2025 Red Hat, Inc. and/or its affiliates
3+
* and other contributors as indicated by the @author tags.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.keycloak.admin.client.resource;
19+
20+
import java.io.IOException;
21+
22+
import jakarta.ws.rs.client.ClientRequestContext;
23+
import jakarta.ws.rs.core.HttpHeaders;
24+
import org.keycloak.admin.client.token.TokenManager;
25+
import org.keycloak.util.DPoPGenerator;
26+
27+
import static org.keycloak.OAuth2Constants.DPOP_HTTP_HEADER;
28+
29+
/**
30+
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
31+
*/
32+
public class DPoPAuthFilter extends BearerAuthFilter {
33+
34+
private final boolean tokenRequest;
35+
36+
public DPoPAuthFilter(TokenManager tokenManager, boolean tokenRequest) {
37+
super(tokenManager);
38+
this.tokenRequest = tokenRequest;
39+
}
40+
41+
@Override
42+
public void filter(ClientRequestContext requestContext) throws IOException {
43+
String requestUri = requestContext.getUri().toString();
44+
if (tokenRequest) {
45+
if (requestUri.endsWith("/token")) {
46+
// Request for obtain new accessToken or refresh-token request
47+
String dpop = DPoPGenerator.generateRsaSignedDPoPProof(tokenManager.getDpopKeyPair(), requestContext.getMethod(), requestUri, null);
48+
requestContext.getHeaders().add(DPOP_HTTP_HEADER, dpop);
49+
}
50+
} else {
51+
// Regular request to admin REST API
52+
String accessToken = tokenManager.getAccessTokenString();
53+
String dpop = DPoPGenerator.generateRsaSignedDPoPProof(tokenManager.getDpopKeyPair(), requestContext.getMethod(), requestUri, accessToken);
54+
requestContext.getHeaders().add(DPOP_HTTP_HEADER, dpop);
55+
56+
String authHeader = DPOP_HTTP_HEADER + " " + accessToken;
57+
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
58+
}
59+
}
60+
61+
62+
@Override
63+
protected String getAuthHeaderPrefix() {
64+
return DPOP_HTTP_HEADER;
65+
}
66+
}

admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,4 +420,7 @@ Response testLDAPConnection(@FormParam("action") String action, @FormParam("conn
420420

421421
@Path("client-types")
422422
ClientTypesResource clientTypes();
423+
424+
@Path("workflows")
425+
WorkflowsResource workflows();
423426
}

0 commit comments

Comments
 (0)