Merge pull request #1687 from stianst/client-reg2
KEYCLOAK-1749 Client registration service and client java api
This commit is contained in:
commit
4c554b4af6
21 changed files with 910 additions and 41 deletions
31
client-api/pom.xml
Executable file
31
client-api/pom.xml
Executable file
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-parent</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>1.6.0.Final-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-client-api</artifactId>
|
||||||
|
<name>Keycloak Client API</name>
|
||||||
|
<description/>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>httpclient</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,275 @@
|
||||||
|
package org.keycloak.client.registration;
|
||||||
|
|
||||||
|
import org.apache.http.HttpHeaders;
|
||||||
|
import org.apache.http.HttpRequest;
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.client.methods.HttpDelete;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
|
import org.apache.http.client.methods.HttpPost;
|
||||||
|
import org.apache.http.client.methods.HttpPut;
|
||||||
|
import org.apache.http.entity.StringEntity;
|
||||||
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClients;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.util.Base64;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class ClientRegistration {
|
||||||
|
|
||||||
|
private String clientRegistrationUrl;
|
||||||
|
private HttpClient httpClient;
|
||||||
|
private Auth auth;
|
||||||
|
|
||||||
|
public static ClientRegistrationBuilder create() {
|
||||||
|
return new ClientRegistrationBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientRegistration() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientRepresentation create(ClientRepresentation client) throws ClientRegistrationException {
|
||||||
|
String content = serialize(client);
|
||||||
|
InputStream resultStream = doPost(content);
|
||||||
|
return deserialize(resultStream, ClientRepresentation.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientRepresentation get() throws ClientRegistrationException {
|
||||||
|
if (auth instanceof ClientIdSecretAuth) {
|
||||||
|
String clientId = ((ClientIdSecretAuth) auth).clientId;
|
||||||
|
return get(clientId);
|
||||||
|
} else {
|
||||||
|
throw new ClientRegistrationException("Requires client authentication");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientRepresentation get(String clientId) throws ClientRegistrationException {
|
||||||
|
InputStream resultStream = doGet(clientId);
|
||||||
|
return resultStream != null ? deserialize(resultStream, ClientRepresentation.class) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(ClientRepresentation client) throws ClientRegistrationException {
|
||||||
|
String content = serialize(client);
|
||||||
|
doPut(content, client.getClientId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete() throws ClientRegistrationException {
|
||||||
|
if (auth instanceof ClientIdSecretAuth) {
|
||||||
|
String clientId = ((ClientIdSecretAuth) auth).clientId;
|
||||||
|
delete(clientId);
|
||||||
|
} else {
|
||||||
|
throw new ClientRegistrationException("Requires client authentication");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(String clientId) throws ClientRegistrationException {
|
||||||
|
doDelete(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() throws ClientRegistrationException {
|
||||||
|
if (httpClient instanceof CloseableHttpClient) {
|
||||||
|
try {
|
||||||
|
((CloseableHttpClient) httpClient).close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ClientRegistrationException("Failed to close http client", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream doPost(String content) throws ClientRegistrationException {
|
||||||
|
try {
|
||||||
|
HttpPost request = new HttpPost(clientRegistrationUrl);
|
||||||
|
|
||||||
|
request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
|
||||||
|
request.setHeader(HttpHeaders.ACCEPT, "application/json");
|
||||||
|
request.setEntity(new StringEntity(content));
|
||||||
|
|
||||||
|
auth.addAuth(request);
|
||||||
|
|
||||||
|
HttpResponse response = httpClient.execute(request);
|
||||||
|
InputStream responseStream = null;
|
||||||
|
if (response.getEntity() != null) {
|
||||||
|
responseStream = response.getEntity().getContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.getStatusLine().getStatusCode() == 201) {
|
||||||
|
return responseStream;
|
||||||
|
} else {
|
||||||
|
responseStream.close();
|
||||||
|
throw new HttpErrorException(response.getStatusLine());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ClientRegistrationException("Failed to send request", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream doGet(String endpoint) throws ClientRegistrationException {
|
||||||
|
try {
|
||||||
|
HttpGet request = new HttpGet(clientRegistrationUrl + "/" + endpoint);
|
||||||
|
|
||||||
|
request.setHeader(HttpHeaders.ACCEPT, "application/json");
|
||||||
|
|
||||||
|
auth.addAuth(request);
|
||||||
|
|
||||||
|
HttpResponse response = httpClient.execute(request);
|
||||||
|
InputStream responseStream = null;
|
||||||
|
if (response.getEntity() != null) {
|
||||||
|
responseStream = response.getEntity().getContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.getStatusLine().getStatusCode() == 200) {
|
||||||
|
return responseStream;
|
||||||
|
} else if (response.getStatusLine().getStatusCode() == 404) {
|
||||||
|
responseStream.close();
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
responseStream.close();
|
||||||
|
throw new HttpErrorException(response.getStatusLine());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ClientRegistrationException("Failed to send request", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doPut(String content, String endpoint) throws ClientRegistrationException {
|
||||||
|
try {
|
||||||
|
HttpPut request = new HttpPut(clientRegistrationUrl + "/" + endpoint);
|
||||||
|
|
||||||
|
request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
|
||||||
|
request.setHeader(HttpHeaders.ACCEPT, "application/json");
|
||||||
|
request.setEntity(new StringEntity(content));
|
||||||
|
|
||||||
|
auth.addAuth(request);
|
||||||
|
|
||||||
|
HttpResponse response = httpClient.execute(request);
|
||||||
|
if (response.getEntity() != null) {
|
||||||
|
response.getEntity().getContent().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.getStatusLine().getStatusCode() != 200) {
|
||||||
|
throw new HttpErrorException(response.getStatusLine());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ClientRegistrationException("Failed to send request", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doDelete(String endpoint) throws ClientRegistrationException {
|
||||||
|
try {
|
||||||
|
HttpDelete request = new HttpDelete(clientRegistrationUrl + "/" + endpoint);
|
||||||
|
|
||||||
|
auth.addAuth(request);
|
||||||
|
|
||||||
|
HttpResponse response = httpClient.execute(request);
|
||||||
|
if (response.getEntity() != null) {
|
||||||
|
response.getEntity().getContent().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.getStatusLine().getStatusCode() != 200) {
|
||||||
|
throw new HttpErrorException(response.getStatusLine());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ClientRegistrationException("Failed to send request", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serialize(ClientRepresentation client) throws ClientRegistrationException {
|
||||||
|
try {
|
||||||
|
return JsonSerialization.writeValueAsString(client);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ClientRegistrationException("Failed to write json object", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T deserialize(InputStream inputStream, Class<T> clazz) throws ClientRegistrationException {
|
||||||
|
try {
|
||||||
|
return JsonSerialization.readValue(inputStream, clazz);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ClientRegistrationException("Failed to read json object", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ClientRegistrationBuilder {
|
||||||
|
|
||||||
|
private String realm;
|
||||||
|
|
||||||
|
private String authServerUrl;
|
||||||
|
|
||||||
|
private Auth auth;
|
||||||
|
|
||||||
|
private HttpClient httpClient;
|
||||||
|
|
||||||
|
public ClientRegistrationBuilder realm(String realm) {
|
||||||
|
this.realm = realm;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public ClientRegistrationBuilder authServerUrl(String authServerUrl) {
|
||||||
|
this.authServerUrl = authServerUrl;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientRegistrationBuilder auth(String token) {
|
||||||
|
this.auth = new TokenAuth(token);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientRegistrationBuilder auth(String clientId, String clientSecret) {
|
||||||
|
this.auth = new ClientIdSecretAuth(clientId, clientSecret);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientRegistrationBuilder httpClient(HttpClient httpClient) {
|
||||||
|
this.httpClient = httpClient;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientRegistration build() {
|
||||||
|
ClientRegistration clientRegistration = new ClientRegistration();
|
||||||
|
clientRegistration.clientRegistrationUrl = authServerUrl + "/realms/" + realm + "/client-registration";
|
||||||
|
|
||||||
|
clientRegistration.httpClient = httpClient != null ? httpClient : HttpClients.createDefault();
|
||||||
|
clientRegistration.auth = auth;
|
||||||
|
|
||||||
|
return clientRegistration;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Auth {
|
||||||
|
void addAuth(HttpRequest httpRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AuthorizationHeaderAuth implements Auth {
|
||||||
|
private String credentials;
|
||||||
|
|
||||||
|
public AuthorizationHeaderAuth(String credentials) {
|
||||||
|
this.credentials = credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAuth(HttpRequest httpRequest) {
|
||||||
|
httpRequest.setHeader(HttpHeaders.AUTHORIZATION, credentials);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TokenAuth extends AuthorizationHeaderAuth {
|
||||||
|
public TokenAuth(String token) {
|
||||||
|
super("Bearer " + token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ClientIdSecretAuth extends AuthorizationHeaderAuth {
|
||||||
|
private String clientId;
|
||||||
|
|
||||||
|
public ClientIdSecretAuth(String clientId, String clientSecret) {
|
||||||
|
super("Basic " + Base64.encodeBytes((clientId + ":" + clientSecret).getBytes()));
|
||||||
|
this.clientId = clientId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package org.keycloak.client.registration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class ClientRegistrationException extends Exception {
|
||||||
|
|
||||||
|
public ClientRegistrationException(String s, Throwable throwable) {
|
||||||
|
super(s, throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientRegistrationException(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package org.keycloak.client.registration;
|
||||||
|
|
||||||
|
import org.apache.http.StatusLine;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class HttpErrorException extends IOException {
|
||||||
|
|
||||||
|
private StatusLine statusLine;
|
||||||
|
|
||||||
|
public HttpErrorException(StatusLine statusLine) {
|
||||||
|
this.statusLine = statusLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StatusLine getStatusLine() {
|
||||||
|
return statusLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,14 +1,12 @@
|
||||||
package org.keycloak.representations.idm;
|
package org.keycloak.representations.idm;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import org.codehaus.jackson.annotate.JsonIgnore;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.codehaus.jackson.annotate.JsonIgnore;
|
|
||||||
import org.keycloak.util.MultivaluedHashMap;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
|
|
|
@ -69,7 +69,16 @@ public enum EventType {
|
||||||
IMPERSONATE(true),
|
IMPERSONATE(true),
|
||||||
CUSTOM_REQUIRED_ACTION(true),
|
CUSTOM_REQUIRED_ACTION(true),
|
||||||
CUSTOM_REQUIRED_ACTION_ERROR(true),
|
CUSTOM_REQUIRED_ACTION_ERROR(true),
|
||||||
EXECUTE_ACTIONS(true);
|
EXECUTE_ACTIONS(true),
|
||||||
|
|
||||||
|
CLIENT_INFO(false),
|
||||||
|
CLIENT_INFO_ERROR(false),
|
||||||
|
CLIENT_REGISTER(true),
|
||||||
|
CLIENT_REGISTER_ERROR(true),
|
||||||
|
CLIENT_UPDATE(true),
|
||||||
|
CLIENT_UPDATE_ERROR(true),
|
||||||
|
CLIENT_DELETE(true),
|
||||||
|
CLIENT_DELETE_ERROR(true);
|
||||||
|
|
||||||
private boolean saveByDefault;
|
private boolean saveByDefault;
|
||||||
|
|
||||||
|
|
|
@ -89,6 +89,7 @@ personalInfo=Personal Info:
|
||||||
role_admin=Admin
|
role_admin=Admin
|
||||||
role_realm-admin=Realm Admin
|
role_realm-admin=Realm Admin
|
||||||
role_create-realm=Create realm
|
role_create-realm=Create realm
|
||||||
|
role_create-client=Create client
|
||||||
role_view-realm=View realm
|
role_view-realm=View realm
|
||||||
role_view-users=View users
|
role_view-users=View users
|
||||||
role_view-applications=View applications
|
role_view-applications=View applications
|
||||||
|
|
|
@ -70,6 +70,15 @@ public class MigrateTo1_6_0 {
|
||||||
if ((adminConsoleClient != null) && !localeMapperAdded(adminConsoleClient)) {
|
if ((adminConsoleClient != null) && !localeMapperAdded(adminConsoleClient)) {
|
||||||
adminConsoleClient.addProtocolMapper(localeMapper);
|
adminConsoleClient.addProtocolMapper(localeMapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClientModel client = realm.getMasterAdminClient();
|
||||||
|
if (client.getRole(AdminRoles.CREATE_CLIENT) == null) {
|
||||||
|
RoleModel role = client.addRole(AdminRoles.CREATE_CLIENT);
|
||||||
|
role.setDescription("${role_" + AdminRoles.CREATE_CLIENT + "}");
|
||||||
|
role.setScopeParamRequired(false);
|
||||||
|
|
||||||
|
realm.getRole(AdminRoles.ADMIN).addCompositeRole(role);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ public class AdminRoles {
|
||||||
public static String REALM_ADMIN = "realm-admin";
|
public static String REALM_ADMIN = "realm-admin";
|
||||||
|
|
||||||
public static String CREATE_REALM = "create-realm";
|
public static String CREATE_REALM = "create-realm";
|
||||||
|
public static String CREATE_CLIENT = "create-client";
|
||||||
|
|
||||||
public static String VIEW_REALM = "view-realm";
|
public static String VIEW_REALM = "view-realm";
|
||||||
public static String VIEW_USERS = "view-users";
|
public static String VIEW_USERS = "view-users";
|
||||||
|
@ -26,6 +27,6 @@ public class AdminRoles {
|
||||||
public static String MANAGE_CLIENTS = "manage-clients";
|
public static String MANAGE_CLIENTS = "manage-clients";
|
||||||
public static String MANAGE_EVENTS = "manage-events";
|
public static String MANAGE_EVENTS = "manage-events";
|
||||||
|
|
||||||
public static String[] ALL_REALM_ROLES = {VIEW_REALM, VIEW_USERS, VIEW_CLIENTS, VIEW_EVENTS, VIEW_IDENTITY_PROVIDERS, MANAGE_REALM, MANAGE_USERS, MANAGE_CLIENTS, MANAGE_EVENTS, MANAGE_IDENTITY_PROVIDERS};
|
public static String[] ALL_REALM_ROLES = {CREATE_CLIENT, VIEW_REALM, VIEW_USERS, VIEW_CLIENTS, VIEW_EVENTS, VIEW_IDENTITY_PROVIDERS, MANAGE_REALM, MANAGE_USERS, MANAGE_CLIENTS, MANAGE_EVENTS, MANAGE_IDENTITY_PROVIDERS};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
6
pom.xml
6
pom.xml
|
@ -137,6 +137,7 @@
|
||||||
<module>common</module>
|
<module>common</module>
|
||||||
<module>core</module>
|
<module>core</module>
|
||||||
<module>core-jaxrs</module>
|
<module>core-jaxrs</module>
|
||||||
|
<module>client-api</module>
|
||||||
<module>connections</module>
|
<module>connections</module>
|
||||||
<module>dependencies</module>
|
<module>dependencies</module>
|
||||||
<module>events</module>
|
<module>events</module>
|
||||||
|
@ -650,6 +651,11 @@
|
||||||
<artifactId>keycloak-core</artifactId>
|
<artifactId>keycloak-core</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-client-api</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-core-jaxrs</artifactId>
|
<artifactId>keycloak-core-jaxrs</artifactId>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
@ -44,7 +45,11 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
|
||||||
String clientSecret = null;
|
String clientSecret = null;
|
||||||
|
|
||||||
String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
|
String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
|
||||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
|
||||||
|
MediaType mediaType = context.getHttpRequest().getHttpHeaders().getMediaType();
|
||||||
|
boolean hasFormData = mediaType != null && mediaType.isCompatible(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
|
||||||
|
|
||||||
|
MultivaluedMap<String, String> formData = hasFormData ? context.getHttpRequest().getDecodedFormParameters() : null;
|
||||||
|
|
||||||
if (authorizationHeader != null) {
|
if (authorizationHeader != null) {
|
||||||
String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
|
String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
|
||||||
|
@ -54,7 +59,7 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Don't send 401 if client_id parameter was sent in request. For example IE may automatically send "Authorization: Negotiate" in XHR requests even for public clients
|
// Don't send 401 if client_id parameter was sent in request. For example IE may automatically send "Authorization: Negotiate" in XHR requests even for public clients
|
||||||
if (!formData.containsKey(OAuth2Constants.CLIENT_ID)) {
|
if (formData != null && !formData.containsKey(OAuth2Constants.CLIENT_ID)) {
|
||||||
Response challengeResponse = Response.status(Response.Status.UNAUTHORIZED).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + context.getRealm().getName() + "\"").build();
|
Response challengeResponse = Response.status(Response.Status.UNAUTHORIZED).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + context.getRealm().getName() + "\"").build();
|
||||||
context.challenge(challengeResponse);
|
context.challenge(challengeResponse);
|
||||||
return;
|
return;
|
||||||
|
@ -62,7 +67,7 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client_id == null) {
|
if (formData != null && client_id == null) {
|
||||||
client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
|
client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
|
||||||
clientSecret = formData.getFirst("client_secret");
|
clientSecret = formData.getFirst("client_secret");
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.keycloak.services.managers;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.spi.UnauthorizedException;
|
import org.jboss.resteasy.spi.UnauthorizedException;
|
||||||
import org.keycloak.ClientConnection;
|
import org.keycloak.ClientConnection;
|
||||||
|
import org.keycloak.models.KeycloakContext;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
|
||||||
|
@ -39,6 +40,11 @@ public class AppAuthManager extends AuthenticationManager {
|
||||||
return tokenString;
|
return tokenString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AuthResult authenticateBearerToken(KeycloakSession session, RealmModel realm) {
|
||||||
|
KeycloakContext ctx = session.getContext();
|
||||||
|
return authenticateBearerToken(session, realm, ctx.getUri(), ctx.getConnection(), ctx.getRequestHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
public AuthResult authenticateBearerToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
|
public AuthResult authenticateBearerToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
|
||||||
String tokenString = extractAuthorizationHeaderToken(headers);
|
String tokenString = extractAuthorizationHeaderToken(headers);
|
||||||
if (tokenString == null) return null;
|
if (tokenString == null) return null;
|
||||||
|
|
|
@ -3,21 +3,27 @@ package org.keycloak.services.resources;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.spi.BadRequestException;
|
import org.jboss.resteasy.spi.BadRequestException;
|
||||||
import org.jboss.resteasy.spi.NotFoundException;
|
import org.jboss.resteasy.spi.NotFoundException;
|
||||||
|
import org.jboss.resteasy.spi.UnauthorizedException;
|
||||||
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.exportimport.ClientDescriptionConverter;
|
import org.keycloak.exportimport.ClientDescriptionConverter;
|
||||||
import org.keycloak.exportimport.KeycloakClientDescriptionConverter;
|
import org.keycloak.exportimport.KeycloakClientDescriptionConverter;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.*;
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.ModelDuplicateException;
|
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
import org.keycloak.models.utils.RepresentationToModel;
|
import org.keycloak.models.utils.RepresentationToModel;
|
||||||
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
|
import org.keycloak.services.ErrorResponseException;
|
||||||
|
import org.keycloak.services.ForbiddenException;
|
||||||
|
import org.keycloak.services.managers.AppAuthManager;
|
||||||
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
|
||||||
import javax.ws.rs.*;
|
import javax.ws.rs.*;
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -36,6 +42,8 @@ public class ClientRegistrationService {
|
||||||
@Context
|
@Context
|
||||||
private KeycloakSession session;
|
private KeycloakSession session;
|
||||||
|
|
||||||
|
private AppAuthManager authManager = new AppAuthManager();
|
||||||
|
|
||||||
public ClientRegistrationService(RealmModel realm, EventBuilder event) {
|
public ClientRegistrationService(RealmModel realm, EventBuilder event) {
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
this.event = event;
|
this.event = event;
|
||||||
|
@ -44,6 +52,10 @@ public class ClientRegistrationService {
|
||||||
@POST
|
@POST
|
||||||
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN })
|
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN })
|
||||||
public Response create(String description, @QueryParam("format") String format) {
|
public Response create(String description, @QueryParam("format") String format) {
|
||||||
|
event.event(EventType.CLIENT_REGISTER);
|
||||||
|
|
||||||
|
authenticate(true, null);
|
||||||
|
|
||||||
if (format == null) {
|
if (format == null) {
|
||||||
format = KeycloakClientDescriptionConverter.ID;
|
format = KeycloakClientDescriptionConverter.ID;
|
||||||
}
|
}
|
||||||
|
@ -58,6 +70,10 @@ public class ClientRegistrationService {
|
||||||
ClientModel clientModel = RepresentationToModel.createClient(session, realm, rep, true);
|
ClientModel clientModel = RepresentationToModel.createClient(session, realm, rep, true);
|
||||||
rep = ModelToRepresentation.toRepresentation(clientModel);
|
rep = ModelToRepresentation.toRepresentation(clientModel);
|
||||||
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
|
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
|
||||||
|
|
||||||
|
logger.infov("Created client {0}", rep.getClientId());
|
||||||
|
|
||||||
|
event.client(rep.getClientId()).success();
|
||||||
return Response.created(uri).entity(rep).build();
|
return Response.created(uri).entity(rep).build();
|
||||||
} catch (ModelDuplicateException e) {
|
} catch (ModelDuplicateException e) {
|
||||||
return ErrorResponse.exists("Client " + rep.getClientId() + " already exists");
|
return ErrorResponse.exists("Client " + rep.getClientId() + " already exists");
|
||||||
|
@ -67,34 +83,79 @@ public class ClientRegistrationService {
|
||||||
@GET
|
@GET
|
||||||
@Path("{clientId}")
|
@Path("{clientId}")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public ClientRepresentation get(@PathParam("clientId") String clientId) {
|
public Response get(@PathParam("clientId") String clientId) {
|
||||||
AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event, realm);
|
event.event(EventType.CLIENT_INFO);
|
||||||
ClientModel client = clientAuth.getClient();
|
|
||||||
|
ClientModel client = authenticate(false, clientId);
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
throw new NotFoundException("Client not found");
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
}
|
}
|
||||||
return ModelToRepresentation.toRepresentation(client);
|
return Response.ok(ModelToRepresentation.toRepresentation(client)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@Path("{clientId}")
|
@Path("{clientId}")
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public void update(@PathParam("clientId") String clientId, ClientRepresentation rep) {
|
public Response update(@PathParam("clientId") String clientId, ClientRepresentation rep) {
|
||||||
ClientModel client = realm.getClientByClientId(clientId);
|
event.event(EventType.CLIENT_UPDATE).client(clientId);
|
||||||
if (client == null) {
|
|
||||||
throw new NotFoundException("Client not found");
|
ClientModel client = authenticate(false, clientId);
|
||||||
}
|
|
||||||
RepresentationToModel.updateClient(rep, client);
|
RepresentationToModel.updateClient(rep, client);
|
||||||
|
|
||||||
|
logger.infov("Updated client {0}", rep.getClientId());
|
||||||
|
|
||||||
|
event.success();
|
||||||
|
return Response.status(Response.Status.OK).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("{clientId}")
|
@Path("{clientId}")
|
||||||
public void delete(@PathParam("clientId") String clientId) {
|
public Response delete(@PathParam("clientId") String clientId) {
|
||||||
ClientModel client = realm.getClientByClientId(clientId);
|
event.event(EventType.CLIENT_DELETE).client(clientId);
|
||||||
if (client == null) {
|
|
||||||
throw new NotFoundException("Client not found");
|
ClientModel client = authenticate(false, clientId);
|
||||||
|
if (realm.removeClient(client.getId())) {
|
||||||
|
event.success();
|
||||||
|
return Response.ok().build();
|
||||||
|
} else {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
}
|
}
|
||||||
realm.removeClient(client.getId());
|
}
|
||||||
|
|
||||||
|
private ClientModel authenticate(boolean create, String clientId) {
|
||||||
|
String authorizationHeader = session.getContext().getRequestHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
|
boolean bearer = authorizationHeader != null && authorizationHeader.split(" ")[0].equalsIgnoreCase("Bearer");
|
||||||
|
|
||||||
|
if (bearer) {
|
||||||
|
AuthenticationManager.AuthResult authResult = authManager.authenticateBearerToken(session, realm);
|
||||||
|
AccessToken.Access realmAccess = authResult.getToken().getResourceAccess(Constants.REALM_MANAGEMENT_CLIENT_ID);
|
||||||
|
if (realmAccess != null) {
|
||||||
|
if (realmAccess.isUserInRole(AdminRoles.MANAGE_CLIENTS)) {
|
||||||
|
return create ? null : realm.getClientByClientId(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (create && realmAccess.isUserInRole(AdminRoles.CREATE_CLIENT)) {
|
||||||
|
return create ? null : realm.getClientByClientId(clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!create) {
|
||||||
|
ClientModel client;
|
||||||
|
|
||||||
|
try {
|
||||||
|
AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event, realm);
|
||||||
|
client = clientAuth.getClient();
|
||||||
|
|
||||||
|
if (client != null && !client.isPublicClient() && client.getClientId().equals(clientId)) {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.error(Errors.NOT_ALLOWED);
|
||||||
|
|
||||||
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,14 +112,14 @@ public class RealmsResource {
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Path("{realm}/client-registration")
|
@Path("{realm}/client-registration")
|
||||||
// public ClientRegistrationService getClientsService(final @PathParam("realm") String name) {
|
public ClientRegistrationService getClientsService(final @PathParam("realm") String name) {
|
||||||
// RealmModel realm = init(name);
|
RealmModel realm = init(name);
|
||||||
// EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
||||||
// ClientRegistrationService service = new ClientRegistrationService(realm, event);
|
ClientRegistrationService service = new ClientRegistrationService(realm, event);
|
||||||
// ResteasyProviderFactory.getInstance().injectProperties(service);
|
ResteasyProviderFactory.getInstance().injectProperties(service);
|
||||||
// return service;
|
return service;
|
||||||
// }
|
}
|
||||||
|
|
||||||
@Path("{realm}/clients-managements")
|
@Path("{realm}/clients-managements")
|
||||||
public ClientsManagementService getClientsManagementService(final @PathParam("realm") String name) {
|
public ClientsManagementService getClientsManagementService(final @PathParam("realm") String name) {
|
||||||
|
|
|
@ -20,6 +20,8 @@ import org.keycloak.admin.client.Keycloak;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AdapterLibsLocationProperty;
|
import org.keycloak.testsuite.arquillian.annotation.AdapterLibsLocationProperty;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
|
||||||
import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN;
|
import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN;
|
||||||
import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
|
import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
|
||||||
|
|
||||||
|
@ -55,6 +57,10 @@ public class ContainersTestEnricher {
|
||||||
@ClassScoped
|
@ClassScoped
|
||||||
private InstanceProducer<Keycloak> adminClient;
|
private InstanceProducer<Keycloak> adminClient;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ClassScoped
|
||||||
|
private InstanceProducer<OAuthClient> oauthClient;
|
||||||
|
|
||||||
private ContainerController controller;
|
private ContainerController controller;
|
||||||
|
|
||||||
private final boolean migrationTests = System.getProperty("migration", "false").equals("true");
|
private final boolean migrationTests = System.getProperty("migration", "false").equals("true");
|
||||||
|
@ -92,6 +98,7 @@ public class ContainersTestEnricher {
|
||||||
|
|
||||||
initializeTestContext(testClass);
|
initializeTestContext(testClass);
|
||||||
initializeAdminClient();
|
initializeAdminClient();
|
||||||
|
initializeOAuthClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeTestContext(Class testClass) {
|
private void initializeTestContext(Class testClass) {
|
||||||
|
@ -116,6 +123,10 @@ public class ContainersTestEnricher {
|
||||||
MASTER, ADMIN, ADMIN, Constants.ADMIN_CONSOLE_CLIENT_ID));
|
MASTER, ADMIN, ADMIN, Constants.ADMIN_CONSOLE_CLIENT_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initializeOAuthClient() {
|
||||||
|
oauthClient.set(new OAuthClient(getAuthServerContextRootFromSystemProperty() + "/auth"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param testClass
|
* @param testClass
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package org.keycloak.testsuite.arquillian;
|
package org.keycloak.testsuite.arquillian;
|
||||||
|
|
||||||
import org.keycloak.testsuite.arquillian.provider.URLProvider;
|
import org.keycloak.testsuite.arquillian.provider.*;
|
||||||
import org.keycloak.testsuite.arquillian.provider.SuiteContextProvider;
|
|
||||||
import org.keycloak.testsuite.arquillian.provider.TestContextProvider;
|
|
||||||
import org.jboss.arquillian.container.spi.client.container.DeployableContainer;
|
import org.jboss.arquillian.container.spi.client.container.DeployableContainer;
|
||||||
import org.jboss.arquillian.container.test.impl.enricher.resource.URLResourceProvider;
|
import org.jboss.arquillian.container.test.impl.enricher.resource.URLResourceProvider;
|
||||||
import org.jboss.arquillian.container.test.spi.client.deployment.ApplicationArchiveProcessor;
|
import org.jboss.arquillian.container.test.spi.client.deployment.ApplicationArchiveProcessor;
|
||||||
|
@ -12,7 +10,6 @@ import org.jboss.arquillian.graphene.location.CustomizableURLResourceProvider;
|
||||||
import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider;
|
import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider;
|
||||||
import org.jboss.arquillian.test.spi.execution.TestExecutionDecider;
|
import org.jboss.arquillian.test.spi.execution.TestExecutionDecider;
|
||||||
import org.keycloak.testsuite.arquillian.jira.JiraTestExecutionDecider;
|
import org.keycloak.testsuite.arquillian.jira.JiraTestExecutionDecider;
|
||||||
import org.keycloak.testsuite.arquillian.provider.AdminClientProvider;
|
|
||||||
import org.keycloak.testsuite.arquillian.undertow.CustomUndertowContainer;
|
import org.keycloak.testsuite.arquillian.undertow.CustomUndertowContainer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,7 +24,8 @@ public class KeycloakArquillianExtension implements LoadableExtension {
|
||||||
builder
|
builder
|
||||||
.service(ResourceProvider.class, SuiteContextProvider.class)
|
.service(ResourceProvider.class, SuiteContextProvider.class)
|
||||||
.service(ResourceProvider.class, TestContextProvider.class)
|
.service(ResourceProvider.class, TestContextProvider.class)
|
||||||
.service(ResourceProvider.class, AdminClientProvider.class);
|
.service(ResourceProvider.class, AdminClientProvider.class)
|
||||||
|
.service(ResourceProvider.class, OAuthClientProvider.class);
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.service(DeploymentScenarioGenerator.class, DeploymentTargetModifier.class)
|
.service(DeploymentScenarioGenerator.class, DeploymentTargetModifier.class)
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package org.keycloak.testsuite.arquillian.provider;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.core.api.Instance;
|
||||||
|
import org.jboss.arquillian.core.api.annotation.Inject;
|
||||||
|
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||||
|
import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class OAuthClientProvider implements ResourceProvider {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Instance<OAuthClient> oauthClient;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canProvide(Class<?> type) {
|
||||||
|
return OAuthClient.class.isAssignableFrom(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object lookup(ArquillianResource resource, Annotation... qualifiers) {
|
||||||
|
return oauthClient.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package org.keycloak.testsuite.util;
|
||||||
|
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.apache.http.NameValuePair;
|
||||||
|
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
|
import org.apache.http.client.methods.HttpPost;
|
||||||
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClients;
|
||||||
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||||
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
|
import org.keycloak.util.BasicAuthHelper;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class OAuthClient {
|
||||||
|
|
||||||
|
private String baseUrl;
|
||||||
|
|
||||||
|
public OAuthClient(String baseUrl) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessTokenResponse getToken(String realm, String clientId, String clientSecret, String username, String password) {
|
||||||
|
CloseableHttpClient httpclient = HttpClients.createDefault();
|
||||||
|
try {
|
||||||
|
HttpPost post = new HttpPost(OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(baseUrl)).build(realm));
|
||||||
|
|
||||||
|
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
|
||||||
|
parameters.add(new BasicNameValuePair("username", username));
|
||||||
|
parameters.add(new BasicNameValuePair("password", password));
|
||||||
|
if (clientSecret != null) {
|
||||||
|
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
|
||||||
|
post.setHeader("Authorization", authorization);
|
||||||
|
} else {
|
||||||
|
parameters.add(new BasicNameValuePair("client_id", clientId));
|
||||||
|
}
|
||||||
|
|
||||||
|
UrlEncodedFormEntity formEntity;
|
||||||
|
try {
|
||||||
|
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
post.setEntity(formEntity);
|
||||||
|
|
||||||
|
CloseableHttpResponse response = httpclient.execute(post);
|
||||||
|
|
||||||
|
if (response.getStatusLine().getStatusCode() != 200) {
|
||||||
|
throw new RuntimeException("Failed to retrieve token: " + response.getStatusLine().toString() + " / " + IOUtils.toString(response.getEntity().getContent()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonSerialization.readValue(response.getEntity().getContent(), AccessTokenResponse.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
try {
|
||||||
|
httpclient.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
|
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
|
||||||
import org.keycloak.testsuite.arquillian.SuiteContext;
|
import org.keycloak.testsuite.arquillian.SuiteContext;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
import org.keycloak.testsuite.auth.page.AuthServer;
|
import org.keycloak.testsuite.auth.page.AuthServer;
|
||||||
import org.keycloak.testsuite.auth.page.AuthServerContextRoot;
|
import org.keycloak.testsuite.auth.page.AuthServerContextRoot;
|
||||||
|
@ -51,6 +52,9 @@ public abstract class AbstractKeycloakTest {
|
||||||
@ArquillianResource
|
@ArquillianResource
|
||||||
protected Keycloak adminClient;
|
protected Keycloak adminClient;
|
||||||
|
|
||||||
|
@ArquillianResource
|
||||||
|
protected OAuthClient oauthClient;
|
||||||
|
|
||||||
protected List<RealmRepresentation> testRealmReps;
|
protected List<RealmRepresentation> testRealmReps;
|
||||||
|
|
||||||
@Drone
|
@Drone
|
||||||
|
|
|
@ -0,0 +1,306 @@
|
||||||
|
package org.keycloak.testsuite.client;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.client.registration.ClientRegistration;
|
||||||
|
import org.keycloak.client.registration.ClientRegistrationException;
|
||||||
|
import org.keycloak.client.registration.HttpErrorException;
|
||||||
|
import org.keycloak.models.AdminRoles;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class ClientRegistrationTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
|
private static final String REALM_NAME = "test";
|
||||||
|
private static final String CLIENT_ID = "test-client";
|
||||||
|
private static final String CLIENT_SECRET = "test-client-secret";
|
||||||
|
|
||||||
|
private ClientRegistration clientRegistrationAsAdmin;
|
||||||
|
private ClientRegistration clientRegistrationAsClient;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() throws ClientRegistrationException {
|
||||||
|
clientRegistrationAsAdmin = clientBuilder().auth(getToken("manage-clients", "password")).build();
|
||||||
|
clientRegistrationAsClient = clientBuilder().auth(CLIENT_ID, CLIENT_SECRET).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void after() throws ClientRegistrationException {
|
||||||
|
clientRegistrationAsAdmin.close();
|
||||||
|
clientRegistrationAsClient.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
|
RealmRepresentation rep = new RealmRepresentation();
|
||||||
|
rep.setEnabled(true);
|
||||||
|
rep.setRealm(REALM_NAME);
|
||||||
|
rep.setUsers(new LinkedList<UserRepresentation>());
|
||||||
|
|
||||||
|
LinkedList<CredentialRepresentation> credentials = new LinkedList<>();
|
||||||
|
CredentialRepresentation password = new CredentialRepresentation();
|
||||||
|
password.setType(CredentialRepresentation.PASSWORD);
|
||||||
|
password.setValue("password");
|
||||||
|
credentials.add(password);
|
||||||
|
|
||||||
|
UserRepresentation user = new UserRepresentation();
|
||||||
|
user.setEnabled(true);
|
||||||
|
user.setUsername("manage-clients");
|
||||||
|
user.setCredentials(credentials);
|
||||||
|
user.setClientRoles(Collections.singletonMap(Constants.REALM_MANAGEMENT_CLIENT_ID, Collections.singletonList(AdminRoles.MANAGE_CLIENTS)));
|
||||||
|
|
||||||
|
rep.getUsers().add(user);
|
||||||
|
|
||||||
|
UserRepresentation user2 = new UserRepresentation();
|
||||||
|
user2.setEnabled(true);
|
||||||
|
user2.setUsername("create-clients");
|
||||||
|
user2.setCredentials(credentials);
|
||||||
|
user2.setClientRoles(Collections.singletonMap(Constants.REALM_MANAGEMENT_CLIENT_ID, Collections.singletonList(AdminRoles.CREATE_CLIENT)));
|
||||||
|
|
||||||
|
rep.getUsers().add(user2);
|
||||||
|
|
||||||
|
UserRepresentation user3 = new UserRepresentation();
|
||||||
|
user3.setEnabled(true);
|
||||||
|
user3.setUsername("no-access");
|
||||||
|
user3.setCredentials(credentials);
|
||||||
|
|
||||||
|
rep.getUsers().add(user3);
|
||||||
|
|
||||||
|
testRealms.add(rep);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerClient(ClientRegistration clientRegistration) throws ClientRegistrationException {
|
||||||
|
ClientRepresentation client = new ClientRepresentation();
|
||||||
|
client.setClientId(CLIENT_ID);
|
||||||
|
client.setSecret(CLIENT_SECRET);
|
||||||
|
|
||||||
|
ClientRepresentation createdClient = clientRegistration.create(client);
|
||||||
|
assertEquals(CLIENT_ID, createdClient.getClientId());
|
||||||
|
|
||||||
|
client = adminClient.realm(REALM_NAME).clients().get(createdClient.getId()).toRepresentation();
|
||||||
|
assertEquals(CLIENT_ID, client.getClientId());
|
||||||
|
|
||||||
|
AccessTokenResponse token2 = oauthClient.getToken(REALM_NAME, CLIENT_ID, CLIENT_SECRET, "manage-clients", "password");
|
||||||
|
assertNotNull(token2.getToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerClientAsAdmin() throws ClientRegistrationException {
|
||||||
|
registerClient(clientRegistrationAsAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerClientAsAdminWithCreateOnly() throws ClientRegistrationException {
|
||||||
|
ClientRegistration clientRegistration = clientBuilder().auth(getToken("create-clients", "password")).build();
|
||||||
|
try {
|
||||||
|
registerClient(clientRegistration);
|
||||||
|
} finally {
|
||||||
|
clientRegistration.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerClientAsAdminWithNoAccess() throws ClientRegistrationException {
|
||||||
|
ClientRegistration clientRegistration = clientBuilder().auth(getToken("no-access", "password")).build();
|
||||||
|
try {
|
||||||
|
registerClient(clientRegistration);
|
||||||
|
fail("Expected 403");
|
||||||
|
} catch (ClientRegistrationException e) {
|
||||||
|
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
|
||||||
|
} finally {
|
||||||
|
clientRegistration.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getClientAsAdminWithCreateOnly() throws ClientRegistrationException {
|
||||||
|
registerClient(clientRegistrationAsAdmin);
|
||||||
|
ClientRegistration clientRegistration = clientBuilder().auth(getToken("create-clients", "password")).build();
|
||||||
|
try {
|
||||||
|
clientRegistration.get(CLIENT_ID);
|
||||||
|
fail("Expected 403");
|
||||||
|
} catch (ClientRegistrationException e) {
|
||||||
|
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
|
||||||
|
} finally {
|
||||||
|
clientRegistration.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wrongClient() throws ClientRegistrationException {
|
||||||
|
registerClient(clientRegistrationAsAdmin);
|
||||||
|
|
||||||
|
ClientRepresentation client = new ClientRepresentation();
|
||||||
|
client.setClientId("test-client-2");
|
||||||
|
client.setSecret("test-client-2-secret");
|
||||||
|
|
||||||
|
clientRegistrationAsAdmin.create(client);
|
||||||
|
|
||||||
|
ClientRegistration clientRegistration = clientBuilder().auth("test-client-2", "test-client-2-secret").build();
|
||||||
|
|
||||||
|
client = clientRegistration.get("test-client-2");
|
||||||
|
assertNotNull(client);
|
||||||
|
assertEquals("test-client-2", client.getClientId());
|
||||||
|
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
clientRegistration.get(CLIENT_ID);
|
||||||
|
fail("Expected 403");
|
||||||
|
} catch (ClientRegistrationException e) {
|
||||||
|
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
client = clientRegistrationAsAdmin.get(CLIENT_ID);
|
||||||
|
try {
|
||||||
|
clientRegistration.update(client);
|
||||||
|
fail("Expected 403");
|
||||||
|
} catch (ClientRegistrationException e) {
|
||||||
|
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
clientRegistration.delete(CLIENT_ID);
|
||||||
|
fail("Expected 403");
|
||||||
|
} catch (ClientRegistrationException e) {
|
||||||
|
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
clientRegistration.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getClientAsAdminWithNoAccess() throws ClientRegistrationException {
|
||||||
|
registerClient(clientRegistrationAsAdmin);
|
||||||
|
ClientRegistration clientRegistration = clientBuilder().auth(getToken("no-access", "password")).build();
|
||||||
|
try {
|
||||||
|
clientRegistration.get(CLIENT_ID);
|
||||||
|
fail("Expected 403");
|
||||||
|
} catch (ClientRegistrationException e) {
|
||||||
|
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
|
||||||
|
} finally {
|
||||||
|
clientRegistration.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateClient(ClientRegistration clientRegistration) throws ClientRegistrationException {
|
||||||
|
ClientRepresentation client = clientRegistration.get(CLIENT_ID);
|
||||||
|
client.setRedirectUris(Collections.singletonList("http://localhost:8080/app"));
|
||||||
|
|
||||||
|
clientRegistration.update(client);
|
||||||
|
|
||||||
|
ClientRepresentation updatedClient = clientRegistration.get(CLIENT_ID);
|
||||||
|
|
||||||
|
assertEquals(1, updatedClient.getRedirectUris().size());
|
||||||
|
assertEquals("http://localhost:8080/app", updatedClient.getRedirectUris().get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updateClientAsAdmin() throws ClientRegistrationException {
|
||||||
|
registerClient(clientRegistrationAsAdmin);
|
||||||
|
updateClient(clientRegistrationAsAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updateClientAsAdminWithCreateOnly() throws ClientRegistrationException {
|
||||||
|
ClientRegistration clientRegistration = clientBuilder().auth(getToken("create-clients", "password")).build();
|
||||||
|
try {
|
||||||
|
updateClient(clientRegistration);
|
||||||
|
fail("Expected 403");
|
||||||
|
} catch (ClientRegistrationException e) {
|
||||||
|
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
|
||||||
|
} finally {
|
||||||
|
clientRegistration.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updateClientAsAdminWithNoAccess() throws ClientRegistrationException {
|
||||||
|
ClientRegistration clientRegistration = clientBuilder().auth(getToken("no-access", "password")).build();
|
||||||
|
try {
|
||||||
|
updateClient(clientRegistration);
|
||||||
|
fail("Expected 403");
|
||||||
|
} catch (ClientRegistrationException e) {
|
||||||
|
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
|
||||||
|
} finally {
|
||||||
|
clientRegistration.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updateClientAsClient() throws ClientRegistrationException {
|
||||||
|
registerClient(clientRegistrationAsAdmin);
|
||||||
|
updateClient(clientRegistrationAsClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteClient(ClientRegistration clientRegistration) throws ClientRegistrationException {
|
||||||
|
clientRegistration.delete(CLIENT_ID);
|
||||||
|
|
||||||
|
// Can't authenticate as client after client is deleted
|
||||||
|
ClientRepresentation client = clientRegistrationAsAdmin.get(CLIENT_ID);
|
||||||
|
assertNull(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deleteClientAsAdmin() throws ClientRegistrationException {
|
||||||
|
registerClient(clientRegistrationAsAdmin);
|
||||||
|
deleteClient(clientRegistrationAsAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deleteClientAsAdminWithCreateOnly() throws ClientRegistrationException {
|
||||||
|
ClientRegistration clientRegistration = clientBuilder().auth(getToken("create-clients", "password")).build();
|
||||||
|
try {
|
||||||
|
deleteClient(clientRegistration);
|
||||||
|
fail("Expected 403");
|
||||||
|
} catch (ClientRegistrationException e) {
|
||||||
|
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
|
||||||
|
} finally {
|
||||||
|
clientRegistration.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deleteClientAsAdminWithNoAccess() throws ClientRegistrationException {
|
||||||
|
ClientRegistration clientRegistration = clientBuilder().auth(getToken("no-access", "password")).build();
|
||||||
|
try {
|
||||||
|
deleteClient(clientRegistration);
|
||||||
|
fail("Expected 403");
|
||||||
|
} catch (ClientRegistrationException e) {
|
||||||
|
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
|
||||||
|
} finally {
|
||||||
|
clientRegistration.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deleteClientAsClient() throws ClientRegistrationException {
|
||||||
|
registerClient(clientRegistrationAsAdmin);
|
||||||
|
deleteClient(clientRegistrationAsClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientRegistration.ClientRegistrationBuilder clientBuilder() {
|
||||||
|
return ClientRegistration.create().realm("test").authServerUrl(testContext.getAuthServerContextRoot() + "/auth");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getToken(String username, String password) {
|
||||||
|
return oauthClient.getToken(REALM_NAME, "security-admin-console", null, username, password).getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -201,6 +201,10 @@
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-admin-client</artifactId>
|
<artifactId>keycloak-admin-client</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-client-api</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-services</artifactId>
|
<artifactId>keycloak-services</artifactId>
|
||||||
|
|
Loading…
Reference in a new issue