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
This commit is contained in:
Konstantin Gribov 2016-03-30 21:42:21 +03:00
parent d98cd4235c
commit 96424536a7
4 changed files with 103 additions and 47 deletions

View file

@ -17,6 +17,9 @@
package org.keycloak.admin.client; package org.keycloak.admin.client;
import static org.keycloak.OAuth2Constants.CLIENT_CREDENTIALS;
import static org.keycloak.OAuth2Constants.PASSWORD;
/** /**
* @author rodrigo.sasaki@icarros.com.br * @author rodrigo.sasaki@icarros.com.br
*/ */
@ -28,14 +31,21 @@ public class Config {
private String password; private String password;
private String clientId; private String clientId;
private String clientSecret; private String clientSecret;
private String grantType;
public Config(String serverUrl, String realm, String username, String password, String clientId, String clientSecret) { 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.serverUrl = serverUrl;
this.realm = realm; this.realm = realm;
this.username = username; this.username = username;
this.password = password; this.password = password;
this.clientId = clientId; this.clientId = clientId;
this.clientSecret = clientSecret; this.clientSecret = clientSecret;
this.grantType = grantType;
checkGrantType(grantType);
} }
public String getServerUrl() { public String getServerUrl() {
@ -86,8 +96,23 @@ public class Config {
this.clientSecret = clientSecret; this.clientSecret = clientSecret;
} }
public boolean isPublicClient(){ public boolean isPublicClient() {
return clientSecret == null; 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)");
}
}
} }

View file

@ -28,25 +28,25 @@ import org.keycloak.admin.client.token.TokenManager;
import java.net.URI; 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 * 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 * default {@link ResteasyClientBuilder} settings. To customize the underling client, use a {@link KeycloakBuilder} to
* create a Keycloak client. * create a Keycloak client.
* *
* @see KeycloakBuilder
*
* @author rodrigo.sasaki@icarros.com.br * @author rodrigo.sasaki@icarros.com.br
* @see KeycloakBuilder
*/ */
public class Keycloak { public class Keycloak {
private final Config config; private final Config config;
private final TokenManager tokenManager; private final TokenManager tokenManager;
private final ResteasyWebTarget target; private final ResteasyWebTarget target;
private final ResteasyClient client; private final ResteasyClient client;
Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, ResteasyClient resteasyClient){ 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); config = new Config(serverUrl, realm, username, password, clientId, clientSecret, grantType);
client = resteasyClient != null ? resteasyClient : new ResteasyClientBuilder().connectionPoolSize(10).build(); client = resteasyClient != null ? resteasyClient : new ResteasyClientBuilder().build();
tokenManager = new TokenManager(config, client); tokenManager = new TokenManager(config, client);
@ -55,27 +55,27 @@ public class Keycloak {
target.register(new BearerAuthFilter(tokenManager)); target.register(new BearerAuthFilter(tokenManager));
} }
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret){ 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); 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){ public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId) {
return new Keycloak(serverUrl, realm, username, password, clientId, null, null); return new Keycloak(serverUrl, realm, username, password, clientId, null, PASSWORD, null);
} }
public RealmsResource realms(){ public RealmsResource realms() {
return target.proxy(RealmsResource.class); return target.proxy(RealmsResource.class);
} }
public RealmResource realm(String realmName){ public RealmResource realm(String realmName) {
return realms().realm(realmName); return realms().realm(realmName);
} }
public ServerInfoResource serverInfo(){ public ServerInfoResource serverInfo() {
return target.proxy(ServerInfoResource.class); return target.proxy(ServerInfoResource.class);
} }
public TokenManager tokenManager(){ public TokenManager tokenManager() {
return tokenManager; return tokenManager;
} }
@ -98,5 +98,4 @@ public class Keycloak {
public void close() { public void close() {
client.close(); client.close();
} }
} }

View file

@ -20,15 +20,17 @@ package org.keycloak.admin.client;
import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; 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 * Provides a {@link Keycloak} client builder with the ability to customize the underlying
* {@link ResteasyClient RESTEasy client} used to communicate with the Keycloak server. * {@link ResteasyClient RESTEasy client} used to communicate with the Keycloak server.
* * <p>
* <p>Example usage with a connection pool size of 20:</p> * <p>Example usage with a connection pool size of 20:</p>
*
* <pre> * <pre>
* Keycloak keycloak = KeycloakBuilder.builder() * Keycloak keycloak = KeycloakBuilder.builder()
* .serverUrl("https:/sso.example.com/auth") * .serverUrl("https://sso.example.com/auth")
* .realm("realm") * .realm("realm")
* .username("user") * .username("user")
* .password("pass") * .password("pass")
@ -37,6 +39,16 @@ import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
* .resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build()) * .resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build())
* .build(); * .build();
* </pre> * </pre>
* <p>Example usage with grant_type=client_credentials</p>
* <pre>
* Keycloak keycloak = KeycloakBuilder.builder()
* .serverUrl("https://sso.example.com/auth")
* .realm("example")
* .grantType(OAuth2Constants.CLIENT_CREDENTIALS)
* .clientId("client")
* .clientSecret("secret")
* .build();
* </pre>
* *
* @author Scott Rossillo * @author Scott Rossillo
* @see ResteasyClientBuilder * @see ResteasyClientBuilder
@ -48,6 +60,7 @@ public class KeycloakBuilder {
private String password; private String password;
private String clientId; private String clientId;
private String clientSecret; private String clientSecret;
private String grantType = PASSWORD;
private ResteasyClient resteasyClient; private ResteasyClient resteasyClient;
public KeycloakBuilder serverUrl(String serverUrl) { public KeycloakBuilder serverUrl(String serverUrl) {
@ -60,6 +73,12 @@ public class KeycloakBuilder {
return this; return this;
} }
public KeycloakBuilder grantType(String grantType) {
Config.checkGrantType(grantType);
this.grantType = grantType;
return this;
}
public KeycloakBuilder username(String username) { public KeycloakBuilder username(String username) {
this.username = username; this.username = username;
return this; return this;
@ -97,19 +116,25 @@ public class KeycloakBuilder {
throw new IllegalStateException("realm required"); throw new IllegalStateException("realm required");
} }
if (username == null) { if (PASSWORD.equals(grantType)) {
throw new IllegalStateException("username required"); if (username == null) {
} throw new IllegalStateException("username required");
}
if (password == null) { if (password == null) {
throw new IllegalStateException("password required"); 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) { if (clientId == null) {
throw new IllegalStateException("clientId required"); 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() { private KeycloakBuilder() {

View file

@ -27,11 +27,12 @@ import org.keycloak.representations.AccessTokenResponse;
import javax.ws.rs.BadRequestException; import javax.ws.rs.BadRequestException;
import javax.ws.rs.core.Form; import javax.ws.rs.core.Form;
import static org.keycloak.OAuth2Constants.*;
/** /**
* @author rodrigo.sasaki@icarros.com.br * @author rodrigo.sasaki@icarros.com.br
*/ */
public class TokenManager { public class TokenManager {
private static final long DEFAULT_MIN_VALIDITY = 30; private static final long DEFAULT_MIN_VALIDITY = 30;
private AccessTokenResponse currentToken; private AccessTokenResponse currentToken;
@ -39,61 +40,67 @@ public class TokenManager {
private long minTokenValidity = DEFAULT_MIN_VALIDITY; private long minTokenValidity = DEFAULT_MIN_VALIDITY;
private final Config config; private final Config config;
private final TokenService tokenService; private final TokenService tokenService;
private final String accessTokenGrantType;
public TokenManager(Config config, ResteasyClient client){ public TokenManager(Config config, ResteasyClient client) {
this.config = config; this.config = config;
ResteasyWebTarget target = client.target(config.getServerUrl()); ResteasyWebTarget target = client.target(config.getServerUrl());
if(!config.isPublicClient()){ if (!config.isPublicClient()) {
target.register(new BasicAuthFilter(config.getClientId(), config.getClientSecret())); 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(); return getAccessToken().getToken();
} }
public synchronized AccessTokenResponse getAccessToken(){ public synchronized AccessTokenResponse getAccessToken() {
if(currentToken == null){ if (currentToken == null) {
grantToken(); grantToken();
}else if(tokenExpired()){ } else if (tokenExpired()) {
refreshToken(); refreshToken();
} }
return currentToken; return currentToken;
} }
public AccessTokenResponse grantToken(){ public AccessTokenResponse grantToken() {
Form form = new Form() Form form = new Form().param(GRANT_TYPE, accessTokenGrantType);
.param("grant_type", "password") if (PASSWORD.equals(accessTokenGrantType)) {
.param("username", config.getUsername()) form.param("username", config.getUsername())
.param("password", config.getPassword()); .param("password", config.getPassword());
}
if(config.isPublicClient()){ if (config.isPublicClient()) {
form.param("client_id", config.getClientId()); form.param(CLIENT_ID, config.getClientId());
} }
int requestTime = Time.currentTime(); int requestTime = Time.currentTime();
synchronized (this) { synchronized (this) {
currentToken = tokenService.grantToken( config.getRealm(), form.asMap() ); currentToken = tokenService.grantToken(config.getRealm(), form.asMap());
expirationTime = requestTime + currentToken.getExpiresIn(); expirationTime = requestTime + currentToken.getExpiresIn();
} }
return currentToken; return currentToken;
} }
public AccessTokenResponse refreshToken(){ public AccessTokenResponse refreshToken() {
Form form = new Form() Form form = new Form().param(GRANT_TYPE, REFRESH_TOKEN)
.param("grant_type", "refresh_token") .param(REFRESH_TOKEN, currentToken.getRefreshToken());
.param("refresh_token", currentToken.getRefreshToken());
if(config.isPublicClient()){ if (config.isPublicClient()) {
form.param("client_id", config.getClientId()); form.param(CLIENT_ID, config.getClientId());
} }
try { try {
int requestTime = Time.currentTime(); int requestTime = Time.currentTime();
synchronized (this) { synchronized (this) {
currentToken = tokenService.refreshToken( config.getRealm(), form.asMap() ); currentToken = tokenService.refreshToken(config.getRealm(), form.asMap());
expirationTime = requestTime + currentToken.getExpiresIn(); expirationTime = requestTime + currentToken.getExpiresIn();
} }
return currentToken; return currentToken;