diff --git a/client-api/pom.xml b/client-api/pom.xml
new file mode 100755
index 0000000000..e386849f08
--- /dev/null
+++ b/client-api/pom.xml
@@ -0,0 +1,31 @@
+
+
+
+ keycloak-parent
+ org.keycloak
+ 1.6.0.Final-SNAPSHOT
+
+ 4.0.0
+
+ keycloak-client-api
+ Keycloak Client API
+
+
+
+
+ org.keycloak
+ keycloak-core
+
+
+ org.apache.httpcomponents
+ httpclient
+
+
+ junit
+ junit
+ test
+
+
+
+
diff --git a/client-api/src/main/java/org/keycloak/client/registration/ClientRegistration.java b/client-api/src/main/java/org/keycloak/client/registration/ClientRegistration.java
new file mode 100644
index 0000000000..dae44a4157
--- /dev/null
+++ b/client-api/src/main/java/org/keycloak/client/registration/ClientRegistration.java
@@ -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 Stian Thorgersen
+ */
+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 deserialize(InputStream inputStream, Class 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;
+ }
+ }
+
+}
diff --git a/client-api/src/main/java/org/keycloak/client/registration/ClientRegistrationException.java b/client-api/src/main/java/org/keycloak/client/registration/ClientRegistrationException.java
new file mode 100644
index 0000000000..43f743c05b
--- /dev/null
+++ b/client-api/src/main/java/org/keycloak/client/registration/ClientRegistrationException.java
@@ -0,0 +1,16 @@
+package org.keycloak.client.registration;
+
+/**
+ * @author Stian Thorgersen
+ */
+public class ClientRegistrationException extends Exception {
+
+ public ClientRegistrationException(String s, Throwable throwable) {
+ super(s, throwable);
+ }
+
+ public ClientRegistrationException(String s) {
+ super(s);
+ }
+
+}
diff --git a/client-api/src/main/java/org/keycloak/client/registration/HttpErrorException.java b/client-api/src/main/java/org/keycloak/client/registration/HttpErrorException.java
new file mode 100644
index 0000000000..b25e0330dc
--- /dev/null
+++ b/client-api/src/main/java/org/keycloak/client/registration/HttpErrorException.java
@@ -0,0 +1,22 @@
+package org.keycloak.client.registration;
+
+import org.apache.http.StatusLine;
+
+import java.io.IOException;
+
+/**
+ * @author Stian Thorgersen
+ */
+public class HttpErrorException extends IOException {
+
+ private StatusLine statusLine;
+
+ public HttpErrorException(StatusLine statusLine) {
+ this.statusLine = statusLine;
+ }
+
+ public StatusLine getStatusLine() {
+ return statusLine;
+ }
+
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
index 99f8f3ce3e..ee96b0471f 100755
--- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
@@ -1,14 +1,12 @@
package org.keycloak.representations.idm;
-import java.util.ArrayList;
+import org.codehaus.jackson.annotate.JsonIgnore;
+
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import org.codehaus.jackson.annotate.JsonIgnore;
-import org.keycloak.util.MultivaluedHashMap;
-
/**
* @author Bill Burke
* @version $Revision: 1 $
diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java
index eacce62a87..ac93a4f7a9 100755
--- a/events/api/src/main/java/org/keycloak/events/EventType.java
+++ b/events/api/src/main/java/org/keycloak/events/EventType.java
@@ -69,7 +69,16 @@ public enum EventType {
IMPERSONATE(true),
CUSTOM_REQUIRED_ACTION(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;
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
index 94b06272f2..65ad002ae2 100644
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -89,6 +89,7 @@ personalInfo=Personal Info:
role_admin=Admin
role_realm-admin=Realm Admin
role_create-realm=Create realm
+role_create-client=Create client
role_view-realm=View realm
role_view-users=View users
role_view-applications=View applications
diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java
index d668ae0444..ca47f3e906 100644
--- a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java
+++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java
@@ -70,6 +70,15 @@ public class MigrateTo1_6_0 {
if ((adminConsoleClient != null) && !localeMapperAdded(adminConsoleClient)) {
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);
+ }
}
}
diff --git a/model/api/src/main/java/org/keycloak/models/AdminRoles.java b/model/api/src/main/java/org/keycloak/models/AdminRoles.java
index c067a1d842..2aa91dff9f 100755
--- a/model/api/src/main/java/org/keycloak/models/AdminRoles.java
+++ b/model/api/src/main/java/org/keycloak/models/AdminRoles.java
@@ -13,6 +13,7 @@ public class AdminRoles {
public static String REALM_ADMIN = "realm-admin";
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_USERS = "view-users";
@@ -26,6 +27,6 @@ public class AdminRoles {
public static String MANAGE_CLIENTS = "manage-clients";
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};
}
diff --git a/pom.xml b/pom.xml
index 1ad5e1a1c1..224a32346d 100755
--- a/pom.xml
+++ b/pom.xml
@@ -137,6 +137,7 @@
common
core
core-jaxrs
+ client-api
connections
dependencies
events
@@ -650,6 +651,11 @@
keycloak-core
${project.version}
+
+ org.keycloak
+ keycloak-client-api
+ ${project.version}
+
org.keycloak
keycloak-core-jaxrs
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
index bb17291d85..e476280020 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
@@ -7,6 +7,7 @@ import java.util.List;
import java.util.Map;
import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@@ -44,7 +45,11 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
String clientSecret = null;
String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
- MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters();
+
+ MediaType mediaType = context.getHttpRequest().getHttpHeaders().getMediaType();
+ boolean hasFormData = mediaType != null && mediaType.isCompatible(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
+
+ MultivaluedMap formData = hasFormData ? context.getHttpRequest().getDecodedFormParameters() : null;
if (authorizationHeader != null) {
String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
@@ -54,7 +59,7 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
} 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
- 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();
context.challenge(challengeResponse);
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);
clientSecret = formData.getFirst("client_secret");
}
diff --git a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
index 0156fb6803..3865027ef7 100755
--- a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
@@ -3,6 +3,7 @@ package org.keycloak.services.managers;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.ClientConnection;
+import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@@ -39,6 +40,11 @@ public class AppAuthManager extends AuthenticationManager {
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) {
String tokenString = extractAuthorizationHeaderToken(headers);
if (tokenString == null) return null;
diff --git a/services/src/main/java/org/keycloak/services/resources/ClientRegistrationService.java b/services/src/main/java/org/keycloak/services/resources/ClientRegistrationService.java
index 87e55b8b69..dac9daa93c 100644
--- a/services/src/main/java/org/keycloak/services/resources/ClientRegistrationService.java
+++ b/services/src/main/java/org/keycloak/services/resources/ClientRegistrationService.java
@@ -3,21 +3,27 @@ package org.keycloak.services.resources;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.BadRequestException;
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.EventType;
import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.exportimport.KeycloakClientDescriptionConverter;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ModelDuplicateException;
-import org.keycloak.models.RealmModel;
+import org.keycloak.models.*;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
+import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
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.core.Context;
+import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
@@ -36,6 +42,8 @@ public class ClientRegistrationService {
@Context
private KeycloakSession session;
+ private AppAuthManager authManager = new AppAuthManager();
+
public ClientRegistrationService(RealmModel realm, EventBuilder event) {
this.realm = realm;
this.event = event;
@@ -44,6 +52,10 @@ public class ClientRegistrationService {
@POST
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN })
public Response create(String description, @QueryParam("format") String format) {
+ event.event(EventType.CLIENT_REGISTER);
+
+ authenticate(true, null);
+
if (format == null) {
format = KeycloakClientDescriptionConverter.ID;
}
@@ -58,6 +70,10 @@ public class ClientRegistrationService {
ClientModel clientModel = RepresentationToModel.createClient(session, realm, rep, true);
rep = ModelToRepresentation.toRepresentation(clientModel);
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();
} catch (ModelDuplicateException e) {
return ErrorResponse.exists("Client " + rep.getClientId() + " already exists");
@@ -67,34 +83,79 @@ public class ClientRegistrationService {
@GET
@Path("{clientId}")
@Produces(MediaType.APPLICATION_JSON)
- public ClientRepresentation get(@PathParam("clientId") String clientId) {
- AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event, realm);
- ClientModel client = clientAuth.getClient();
+ public Response get(@PathParam("clientId") String clientId) {
+ event.event(EventType.CLIENT_INFO);
+
+ ClientModel client = authenticate(false, clientId);
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
@Path("{clientId}")
@Consumes(MediaType.APPLICATION_JSON)
- public void update(@PathParam("clientId") String clientId, ClientRepresentation rep) {
- ClientModel client = realm.getClientByClientId(clientId);
- if (client == null) {
- throw new NotFoundException("Client not found");
- }
+ public Response update(@PathParam("clientId") String clientId, ClientRepresentation rep) {
+ event.event(EventType.CLIENT_UPDATE).client(clientId);
+
+ ClientModel client = authenticate(false, clientId);
RepresentationToModel.updateClient(rep, client);
+
+ logger.infov("Updated client {0}", rep.getClientId());
+
+ event.success();
+ return Response.status(Response.Status.OK).build();
}
@DELETE
@Path("{clientId}")
- public void delete(@PathParam("clientId") String clientId) {
- ClientModel client = realm.getClientByClientId(clientId);
- if (client == null) {
- throw new NotFoundException("Client not found");
+ public Response delete(@PathParam("clientId") String clientId) {
+ event.event(EventType.CLIENT_DELETE).client(clientId);
+
+ 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();
}
}
diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index 52f49df1ce..dda825fdf3 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -112,14 +112,14 @@ public class RealmsResource {
return service;
}
-// @Path("{realm}/client-registration")
-// public ClientRegistrationService getClientsService(final @PathParam("realm") String name) {
-// RealmModel realm = init(name);
-// EventBuilder event = new EventBuilder(realm, session, clientConnection);
-// ClientRegistrationService service = new ClientRegistrationService(realm, event);
-// ResteasyProviderFactory.getInstance().injectProperties(service);
-// return service;
-// }
+ @Path("{realm}/client-registration")
+ public ClientRegistrationService getClientsService(final @PathParam("realm") String name) {
+ RealmModel realm = init(name);
+ EventBuilder event = new EventBuilder(realm, session, clientConnection);
+ ClientRegistrationService service = new ClientRegistrationService(realm, event);
+ ResteasyProviderFactory.getInstance().injectProperties(service);
+ return service;
+ }
@Path("{realm}/clients-managements")
public ClientsManagementService getClientsManagementService(final @PathParam("realm") String name) {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainersTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainersTestEnricher.java
index ffb3e0c6ab..257fb55689 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainersTestEnricher.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainersTestEnricher.java
@@ -20,6 +20,8 @@ import org.keycloak.admin.client.Keycloak;
import org.keycloak.models.Constants;
import org.keycloak.testsuite.arquillian.annotation.AdapterLibsLocationProperty;
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.MASTER;
@@ -55,6 +57,10 @@ public class ContainersTestEnricher {
@ClassScoped
private InstanceProducer adminClient;
+ @Inject
+ @ClassScoped
+ private InstanceProducer oauthClient;
+
private ContainerController controller;
private final boolean migrationTests = System.getProperty("migration", "false").equals("true");
@@ -92,6 +98,7 @@ public class ContainersTestEnricher {
initializeTestContext(testClass);
initializeAdminClient();
+ initializeOAuthClient();
}
private void initializeTestContext(Class testClass) {
@@ -116,6 +123,10 @@ public class ContainersTestEnricher {
MASTER, ADMIN, ADMIN, Constants.ADMIN_CONSOLE_CLIENT_ID));
}
+ private void initializeOAuthClient() {
+ oauthClient.set(new OAuthClient(getAuthServerContextRootFromSystemProperty() + "/auth"));
+ }
+
/**
*
* @param testClass
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
index cf25d05159..73583cf489 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
@@ -1,8 +1,6 @@
package org.keycloak.testsuite.arquillian;
-import org.keycloak.testsuite.arquillian.provider.URLProvider;
-import org.keycloak.testsuite.arquillian.provider.SuiteContextProvider;
-import org.keycloak.testsuite.arquillian.provider.TestContextProvider;
+import org.keycloak.testsuite.arquillian.provider.*;
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.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.execution.TestExecutionDecider;
import org.keycloak.testsuite.arquillian.jira.JiraTestExecutionDecider;
-import org.keycloak.testsuite.arquillian.provider.AdminClientProvider;
import org.keycloak.testsuite.arquillian.undertow.CustomUndertowContainer;
/**
@@ -27,7 +24,8 @@ public class KeycloakArquillianExtension implements LoadableExtension {
builder
.service(ResourceProvider.class, SuiteContextProvider.class)
.service(ResourceProvider.class, TestContextProvider.class)
- .service(ResourceProvider.class, AdminClientProvider.class);
+ .service(ResourceProvider.class, AdminClientProvider.class)
+ .service(ResourceProvider.class, OAuthClientProvider.class);
builder
.service(DeploymentScenarioGenerator.class, DeploymentTargetModifier.class)
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/OAuthClientProvider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/OAuthClientProvider.java
new file mode 100644
index 0000000000..4f54d18cf7
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/OAuthClientProvider.java
@@ -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 Stian Thorgersen
+ */
+public class OAuthClientProvider implements ResourceProvider {
+
+ @Inject
+ Instance oauthClient;
+
+ @Override
+ public boolean canProvide(Class> type) {
+ return OAuthClient.class.isAssignableFrom(type);
+ }
+
+ @Override
+ public Object lookup(ArquillianResource resource, Annotation... qualifiers) {
+ return oauthClient.get();
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
new file mode 100644
index 0000000000..3e52bbbc5a
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -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 Stian Thorgersen
+ */
+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 parameters = new LinkedList();
+ 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);
+ }
+ }
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
index 33991151ce..99c5d670ce 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
@@ -20,6 +20,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
import org.keycloak.testsuite.arquillian.SuiteContext;
+import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.WebDriver;
import org.keycloak.testsuite.auth.page.AuthServer;
import org.keycloak.testsuite.auth.page.AuthServerContextRoot;
@@ -51,6 +52,9 @@ public abstract class AbstractKeycloakTest {
@ArquillianResource
protected Keycloak adminClient;
+ @ArquillianResource
+ protected OAuthClient oauthClient;
+
protected List testRealmReps;
@Drone
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java
new file mode 100644
index 0000000000..1d4b01129b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java
@@ -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 Stian Thorgersen
+ */
+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 testRealms) {
+ RealmRepresentation rep = new RealmRepresentation();
+ rep.setEnabled(true);
+ rep.setRealm(REALM_NAME);
+ rep.setUsers(new LinkedList());
+
+ LinkedList 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();
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 8f900b9553..58d32e4ba2 100644
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -201,6 +201,10 @@
org.keycloak
keycloak-admin-client
+
+ org.keycloak
+ keycloak-client-api
+
org.keycloak
keycloak-services