From 96424536a79c073687bf9f184dea90ac0459e2cf Mon Sep 17 00:00:00 2001 From: Konstantin Gribov Date: Wed, 30 Mar 2016 21:42:21 +0300 Subject: [PATCH] Add service account support to Keycloak admin client Added grant_type=client_credentials support to keycloak-admin-client so `keycloak-admin-client` can be used with service client account. Fixes #KEYCLOAK-2236 --- .../org/keycloak/admin/client/Config.java | 27 +++++++++- .../org/keycloak/admin/client/Keycloak.java | 29 +++++------ .../admin/client/KeycloakBuilder.java | 43 ++++++++++++---- .../admin/client/token/TokenManager.java | 51 +++++++++++-------- 4 files changed, 103 insertions(+), 47 deletions(-) diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/Config.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/Config.java index ae3edaacc8..4e628e5a4c 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/Config.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/Config.java @@ -17,6 +17,9 @@ package org.keycloak.admin.client; +import static org.keycloak.OAuth2Constants.CLIENT_CREDENTIALS; +import static org.keycloak.OAuth2Constants.PASSWORD; + /** * @author rodrigo.sasaki@icarros.com.br */ @@ -28,14 +31,21 @@ public class Config { private String password; private String clientId; private String clientSecret; + private String grantType; public Config(String serverUrl, String realm, String username, String password, String clientId, String clientSecret) { + this(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD); + } + + public Config(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType) { this.serverUrl = serverUrl; this.realm = realm; this.username = username; this.password = password; this.clientId = clientId; this.clientSecret = clientSecret; + this.grantType = grantType; + checkGrantType(grantType); } public String getServerUrl() { @@ -86,8 +96,23 @@ public class Config { this.clientSecret = clientSecret; } - public boolean isPublicClient(){ + public boolean isPublicClient() { return clientSecret == null; } + public String getGrantType() { + return grantType; + } + + public void setGrantType(String grantType) { + this.grantType = grantType; + checkGrantType(grantType); + } + + public static void checkGrantType(String grantType) { + if (!PASSWORD.equals(grantType) && !CLIENT_CREDENTIALS.equals(grantType)) { + throw new IllegalArgumentException("Unsupported grantType: " + grantType + + " (only " + PASSWORD + " and " + CLIENT_CREDENTIALS + " are supported)"); + } + } } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java index 52c55619e4..5e30a664e4 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java @@ -28,25 +28,25 @@ import org.keycloak.admin.client.token.TokenManager; import java.net.URI; +import static org.keycloak.OAuth2Constants.PASSWORD; + /** * Provides a Keycloak client. By default, this implementation uses a {@link ResteasyClient RESTEasy client} with the * default {@link ResteasyClientBuilder} settings. To customize the underling client, use a {@link KeycloakBuilder} to * create a Keycloak client. * - * @see KeycloakBuilder - * * @author rodrigo.sasaki@icarros.com.br + * @see KeycloakBuilder */ public class Keycloak { - private final Config config; private final TokenManager tokenManager; private final ResteasyWebTarget target; private final ResteasyClient client; - Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, ResteasyClient resteasyClient){ - config = new Config(serverUrl, realm, username, password, clientId, clientSecret); - client = resteasyClient != null ? resteasyClient : new ResteasyClientBuilder().connectionPoolSize(10).build(); + Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, ResteasyClient resteasyClient) { + config = new Config(serverUrl, realm, username, password, clientId, clientSecret, grantType); + client = resteasyClient != null ? resteasyClient : new ResteasyClientBuilder().build(); tokenManager = new TokenManager(config, client); @@ -55,27 +55,27 @@ public class Keycloak { target.register(new BearerAuthFilter(tokenManager)); } - public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret){ - return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, null); + public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret) { + return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, null); } - public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId){ - return new Keycloak(serverUrl, realm, username, password, clientId, null, null); + public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId) { + return new Keycloak(serverUrl, realm, username, password, clientId, null, PASSWORD, null); } - public RealmsResource realms(){ + public RealmsResource realms() { return target.proxy(RealmsResource.class); } - public RealmResource realm(String realmName){ + public RealmResource realm(String realmName) { return realms().realm(realmName); } - public ServerInfoResource serverInfo(){ + public ServerInfoResource serverInfo() { return target.proxy(ServerInfoResource.class); } - public TokenManager tokenManager(){ + public TokenManager tokenManager() { return tokenManager; } @@ -98,5 +98,4 @@ public class Keycloak { public void close() { client.close(); } - } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java index 5a61ffb933..e192d9abce 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java @@ -20,15 +20,17 @@ package org.keycloak.admin.client; import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import static org.keycloak.OAuth2Constants.CLIENT_CREDENTIALS; +import static org.keycloak.OAuth2Constants.PASSWORD; + /** * Provides a {@link Keycloak} client builder with the ability to customize the underlying * {@link ResteasyClient RESTEasy client} used to communicate with the Keycloak server. - * + *

*

Example usage with a connection pool size of 20:

- * *
  *   Keycloak keycloak = KeycloakBuilder.builder()
- *     .serverUrl("https:/sso.example.com/auth")
+ *     .serverUrl("https://sso.example.com/auth")
  *     .realm("realm")
  *     .username("user")
  *     .password("pass")
@@ -37,6 +39,16 @@ import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
  *     .resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build())
  *     .build();
  * 
+ *

Example usage with grant_type=client_credentials

+ *
+ *   Keycloak keycloak = KeycloakBuilder.builder()
+ *     .serverUrl("https://sso.example.com/auth")
+ *     .realm("example")
+ *     .grantType(OAuth2Constants.CLIENT_CREDENTIALS)
+ *     .clientId("client")
+ *     .clientSecret("secret")
+ *     .build();
+ * 
* * @author Scott Rossillo * @see ResteasyClientBuilder @@ -48,6 +60,7 @@ public class KeycloakBuilder { private String password; private String clientId; private String clientSecret; + private String grantType = PASSWORD; private ResteasyClient resteasyClient; public KeycloakBuilder serverUrl(String serverUrl) { @@ -60,6 +73,12 @@ public class KeycloakBuilder { return this; } + public KeycloakBuilder grantType(String grantType) { + Config.checkGrantType(grantType); + this.grantType = grantType; + return this; + } + public KeycloakBuilder username(String username) { this.username = username; return this; @@ -97,19 +116,25 @@ public class KeycloakBuilder { throw new IllegalStateException("realm required"); } - if (username == null) { - throw new IllegalStateException("username required"); - } + if (PASSWORD.equals(grantType)) { + if (username == null) { + throw new IllegalStateException("username required"); + } - if (password == null) { - throw new IllegalStateException("password required"); + if (password == null) { + throw new IllegalStateException("password required"); + } + } else if (CLIENT_CREDENTIALS.equals(grantType)) { + if (clientSecret == null) { + throw new IllegalStateException("clientSecret required with grant_type=client_credentials"); + } } if (clientId == null) { throw new IllegalStateException("clientId required"); } - return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, resteasyClient); + return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, grantType, resteasyClient); } private KeycloakBuilder() { diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/token/TokenManager.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/token/TokenManager.java index e32568139c..bb32daea90 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/token/TokenManager.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/token/TokenManager.java @@ -27,11 +27,12 @@ import org.keycloak.representations.AccessTokenResponse; import javax.ws.rs.BadRequestException; import javax.ws.rs.core.Form; +import static org.keycloak.OAuth2Constants.*; + /** * @author rodrigo.sasaki@icarros.com.br */ public class TokenManager { - private static final long DEFAULT_MIN_VALIDITY = 30; private AccessTokenResponse currentToken; @@ -39,61 +40,67 @@ public class TokenManager { private long minTokenValidity = DEFAULT_MIN_VALIDITY; private final Config config; private final TokenService tokenService; + private final String accessTokenGrantType; - public TokenManager(Config config, ResteasyClient client){ + public TokenManager(Config config, ResteasyClient client) { this.config = config; ResteasyWebTarget target = client.target(config.getServerUrl()); - if(!config.isPublicClient()){ + if (!config.isPublicClient()) { target.register(new BasicAuthFilter(config.getClientId(), config.getClientSecret())); } - tokenService = target.proxy(TokenService.class); + this.tokenService = target.proxy(TokenService.class); + this.accessTokenGrantType = config.getGrantType(); + + if (CLIENT_CREDENTIALS.equals(accessTokenGrantType) && config.isPublicClient()) { + throw new IllegalArgumentException("Can't use " + GRANT_TYPE + "=" + CLIENT_CREDENTIALS + " with public client"); + } } - public String getAccessTokenString(){ + public String getAccessTokenString() { return getAccessToken().getToken(); } - public synchronized AccessTokenResponse getAccessToken(){ - if(currentToken == null){ + public synchronized AccessTokenResponse getAccessToken() { + if (currentToken == null) { grantToken(); - }else if(tokenExpired()){ + } else if (tokenExpired()) { refreshToken(); } return currentToken; } - public AccessTokenResponse grantToken(){ - Form form = new Form() - .param("grant_type", "password") - .param("username", config.getUsername()) + public AccessTokenResponse grantToken() { + Form form = new Form().param(GRANT_TYPE, accessTokenGrantType); + if (PASSWORD.equals(accessTokenGrantType)) { + form.param("username", config.getUsername()) .param("password", config.getPassword()); + } - if(config.isPublicClient()){ - form.param("client_id", config.getClientId()); + if (config.isPublicClient()) { + form.param(CLIENT_ID, config.getClientId()); } int requestTime = Time.currentTime(); synchronized (this) { - currentToken = tokenService.grantToken( config.getRealm(), form.asMap() ); + currentToken = tokenService.grantToken(config.getRealm(), form.asMap()); expirationTime = requestTime + currentToken.getExpiresIn(); } return currentToken; } - public AccessTokenResponse refreshToken(){ - Form form = new Form() - .param("grant_type", "refresh_token") - .param("refresh_token", currentToken.getRefreshToken()); + public AccessTokenResponse refreshToken() { + Form form = new Form().param(GRANT_TYPE, REFRESH_TOKEN) + .param(REFRESH_TOKEN, currentToken.getRefreshToken()); - if(config.isPublicClient()){ - form.param("client_id", config.getClientId()); + if (config.isPublicClient()) { + form.param(CLIENT_ID, config.getClientId()); } try { int requestTime = Time.currentTime(); synchronized (this) { - currentToken = tokenService.refreshToken( config.getRealm(), form.asMap() ); + currentToken = tokenService.refreshToken(config.getRealm(), form.asMap()); expirationTime = requestTime + currentToken.getExpiresIn(); } return currentToken;