keycloak-aplcache

Changes

client-api/pom.xml 31(+31 -0)

pom.xml 6(+6 -0)

Details

client-api/pom.xml 31(+31 -0)

diff --git a/client-api/pom.xml b/client-api/pom.xml
new file mode 100755
index 0000000..e386849
--- /dev/null
+++ b/client-api/pom.xml
@@ -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>
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 0000000..dae44a4
--- /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 <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;
+        }
+    }
+
+}
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 0000000..43f743c
--- /dev/null
+++ b/client-api/src/main/java/org/keycloak/client/registration/ClientRegistrationException.java
@@ -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);
+    }
+
+}
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 0000000..b25e033
--- /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 <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;
+    }
+
+}
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 99f8f3c..ee96b04 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @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 eacce62..ac93a4f 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 94b0627..65ad002 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 d668ae0..ca47f3e 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 c067a1d..2aa91df 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};
 
 }

pom.xml 6(+6 -0)

diff --git a/pom.xml b/pom.xml
index 1ad5e1a..224a323 100755
--- a/pom.xml
+++ b/pom.xml
@@ -137,6 +137,7 @@
         <module>common</module>
         <module>core</module>
         <module>core-jaxrs</module>
+        <module>client-api</module>
         <module>connections</module>
         <module>dependencies</module>
         <module>events</module>
@@ -652,6 +653,11 @@
             </dependency>
             <dependency>
                 <groupId>org.keycloak</groupId>
+                <artifactId>keycloak-client-api</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.keycloak</groupId>
                 <artifactId>keycloak-core-jaxrs</artifactId>
                 <version>${project.version}</version>
             </dependency>
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 bb17291..e476280 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<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) {
             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 0156fb6..3865027 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 87e55b8..dac9daa 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 52f49df..dda825f 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 ffb3e0c..257fb55 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<Keycloak> adminClient;
 
+    @Inject
+    @ClassScoped
+    private InstanceProducer<OAuthClient> 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 cf25d05..73583cf 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 0000000..4f54d18
--- /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 <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();
+    }
+
+}
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 0000000..3e52bbb
--- /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 <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);
+            }
+        }
+    }
+
+}
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 3399115..99c5d67 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<RealmRepresentation> 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 0000000..1d4b011
--- /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 <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();
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 8f900b9..58d32e4 100644
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -203,6 +203,10 @@
                 </dependency>
                 <dependency>
                     <groupId>org.keycloak</groupId>
+                    <artifactId>keycloak-client-api</artifactId>
+                </dependency>
+                <dependency>
+                    <groupId>org.keycloak</groupId>
                     <artifactId>keycloak-services</artifactId>
                 </dependency>
                 <dependency>