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:
parent
d98cd4235c
commit
96424536a7
4 changed files with 103 additions and 47 deletions
|
@ -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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue