keycloak-aplcache
Changes
client-api/pom.xml 31(+31 -0)
client-api/src/main/java/org/keycloak/client/registration/ClientRegistrationException.java 16(+16 -0)
examples/js-console/README.md 2(+1 -1)
forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties 13(+13 -0)
forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties 13(+13 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-provider-config.html 12(+6 -6)
pom.xml 6(+6 -0)
services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java 11(+8 -3)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainersTestEnricher.java 11(+11 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java 8(+3 -5)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/OAuthClientProvider.java 29(+29 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java 77(+77 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java 4(+4 -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/docbook/auth-server-docs/reference/en/en-US/modules/javascript-adapter.xml b/docbook/auth-server-docs/reference/en/en-US/modules/javascript-adapter.xml
index 2e9f941..33d62dc 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/javascript-adapter.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/javascript-adapter.xml
@@ -209,6 +209,7 @@ new Keycloak({ url: 'http://localhost/auth', realm: 'myrealm', clientId: 'myApp'
<listitem>redirectUri - specifies the uri to redirect to after login</listitem>
<listitem>prompt - can be set to 'none' to check if the user is logged in already (if not logged in, a login form is not displayed)</listitem>
<listitem>loginHint - used to pre-fill the username/email field on the login form</listitem>
+ <listitem>action - if value is 'register' then user is redirected to registration page, otherwise to login page</listitem>
</itemizedlist>
</para>
</simplesect>
@@ -247,6 +248,20 @@ new Keycloak({ url: 'http://localhost/auth', realm: 'myrealm', clientId: 'myApp'
</simplesect>
<simplesect>
+ <title>register(options)</title>
+
+ <para>Redirects to registration form. It's a shortcut for doing login with option action = 'register'</para>
+ <para>Options are same as login method but 'action' is overwritten to 'register'</para>
+ </simplesect>
+
+ <simplesect>
+ <title>createRegisterUrl(options)</title>
+
+ <para>Returns the url to registration page. It's a shortcut for doing createRegisterUrl with option action = 'register'</para>
+ <para>Options are same as createLoginUrl method but 'action' is overwritten to 'register'</para>
+ </simplesect>
+
+ <simplesect>
<title>accountManagement()</title>
<para>Redirects to account management</para>
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;
examples/js-console/README.md 2(+1 -1)
diff --git a/examples/js-console/README.md b/examples/js-console/README.md
index 749c0f3..fa9a02d 100644
--- a/examples/js-console/README.md
+++ b/examples/js-console/README.md
@@ -12,6 +12,6 @@ Open the Keycloak admin console, click on Add Realm, click on 'Choose a JSON fil
Deploy the JS Console to Keycloak by running:
- mvn install jboss-as:deploy
+ mvn install wildfly:deploy
Open the console at http://localhost:8080/js-console and login with username: 'user', and password: 'password'.
diff --git a/examples/js-console/src/main/webapp/index.html b/examples/js-console/src/main/webapp/index.html
index d85b63b..2cca0f8 100644
--- a/examples/js-console/src/main/webapp/index.html
+++ b/examples/js-console/src/main/webapp/index.html
@@ -7,6 +7,7 @@
<div>
<button onclick="keycloak.login()">Login</button>
<button onclick="keycloak.logout()">Logout</button>
+ <button onclick="keycloak.register()">Register</button>
<button onclick="refreshToken(9999)">Refresh Token</button>
<button onclick="refreshToken(30)">Refresh Token (if <30s validity)</button>
<button onclick="loadProfile()">Get Profile</button>
@@ -18,6 +19,7 @@
<button onclick="output(keycloak)">Show Details</button>
<button onclick="output(keycloak.createLoginUrl())">Show Login URL</button>
<button onclick="output(keycloak.createLogoutUrl())">Show Logout URL</button>
+ <button onclick="output(keycloak.createRegisterUrl())">Show Register URL</button>
</div>
<h2>Result</h2>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties
index 6442e6e..beaeb4d 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties
+++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties
@@ -7,6 +7,7 @@ onText=AN
offText=AUS
client=de Client
clear=de Clear
+selectOne=de Select One...
# Realm settings
realm-detail.enabled.tooltip=de Users and clients can only access a realm if it's enabled
@@ -114,3 +115,15 @@ not-before.tooltip=de Revoke any tokens issued before this date.
set-to-now=de Set To Now
push=de Push
push.tooltip=de For every client that has an admin URL, notify them of the new revocation policy.
+
+#Protocol Mapper
+usermodel.prop.label=de Property
+usermodel.prop.tooltip=de Name of the property method in the UserModel interface. For example, a value of 'email' would reference the UserModel.getEmail() method.
+usermodel.attr.label=de User Attribute
+usermodel.attr.tooltip=de Name of stored user attribute which is the name of an attribute within the UserModel.attribute map.
+userSession.modelNote.label=de User Session Note
+userSession.modelNote.tooltip=de Name of stored user session note within the UserSessionModel.note map.
+multivalued.label=de Multivalued
+multivalued.tooltip=de Indicates if attribute supports multiple values. If true, then the list of all values of this attribute will be set as claim. If false, then just first value will be set as claim
+selectRole.label=de Select Role
+selectRole.tooltip=de Enter role in the textbox to the left, or click this button to browse and select the role you want
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index be3ef2d..5404458 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -7,6 +7,7 @@ onText=ON
offText=OFF
client=Client
clear=Clear
+selectOne=Select One...
# Realm settings
realm-detail.enabled.tooltip=Users and clients can only access a realm if it's enabled
@@ -114,3 +115,15 @@ not-before.tooltip=Revoke any tokens issued before this date.
set-to-now=Set To Now
push=Push
push.tooltip=For every client that has an admin URL, notify them of the new revocation policy.
+
+#Protocol Mapper
+usermodel.prop.label=Property
+usermodel.prop.tooltip=Name of the property method in the UserModel interface. For example, a value of 'email' would reference the UserModel.getEmail() method.
+usermodel.attr.label=User Attribute
+usermodel.attr.tooltip=Name of stored user attribute which is the name of an attribute within the UserModel.attribute map.
+userSession.modelNote.label=User Session Note
+userSession.modelNote.tooltip=Name of stored user session note within the UserSessionModel.note map.
+multivalued.label=Multivalued
+multivalued.tooltip=Indicates if attribute supports multiple values. If true, then the list of all values of this attribute will be set as claim. If false, then just first value will be set as claim
+selectRole.label=Select Role
+selectRole.tooltip=Enter role in the textbox to the left, or click this button to browse and select the role you want
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-provider-config.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-provider-config.html
index 08b76a8..54ebdae 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-provider-config.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-provider-config.html
@@ -1,16 +1,16 @@
<div>
<div data-ng-repeat="option in properties" class="form-group" data-ng-controller="ProviderConfigCtrl">
- <label class="col-md-2 control-label">{{option.label}}</label>
+ <label class="col-md-2 control-label">{{:: option.label | translate}}</label>
<div class="col-sm-6" data-ng-hide="option.type == 'boolean' || option.type == 'List' || option.type == 'Role' || option.type == 'ClientList'">
<input class="form-control" type="text" data-ng-model="config[ option.name ]" >
</div>
<div class="col-sm-6" data-ng-show="option.type == 'boolean'">
- <input ng-model="config[ option.name ]" value="'true'" name="option.name" id="option.name" onoffswitchstring />
+ <input ng-model="config[ option.name ]" value="'true'" name="option.name" id="option.name" onoffswitchstring on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<div class="col-sm-6" data-ng-show="option.type == 'List'">
<select ng-model="config[ option.name ]" ng-options="data for data in option.defaultValue">
- <option value="" selected> Select one... </option>
+ <option value="" selected> {{:: 'selectOne' | translate}} </option>
</select>
</div>
<div class="col-sm-6" data-ng-show="option.type == 'Role'">
@@ -19,16 +19,16 @@
<input class="form-control" type="text" data-ng-model="config[ option.name ]" >
</div>
<div class="col-sm-2">
- <button type="submit" data-ng-click="openRoleSelector(option.name, config)" class="btn btn-default" tooltip-placement="top" tooltip-trigger="mouseover mouseout" tooltip="Enter role in the textbox to the left, or click this button to browse and select the role you want">Select Role</button>
+ <button type="submit" data-ng-click="openRoleSelector(option.name, config)" class="btn btn-default" tooltip-placement="top" tooltip-trigger="mouseover mouseout" tooltip="{{:: 'selectRole.tooltip' | translate}}">{{:: 'selectRole.label' | translate}}</button>
</div>
</div>
</div>
<div class="col-sm-4" data-ng-show="option.type == 'ClientList'">
<select ng-model="config[ option.name ]" ng-options="client.clientId as client.clientId for client in clients">
- <option value="" selected> Select one... </option>
+ <option value="" selected> {{:: 'selectOne' | translate}} </option>
</select>
</div>
- <kc-tooltip>{{option.helpText}}</kc-tooltip>
+ <kc-tooltip>{{:: option.helpText | translate}}</kc-tooltip>
</div>
</div>
\ No newline at end of file
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/integration/js/src/main/resources/keycloak.js b/integration/js/src/main/resources/keycloak.js
index f384e7b..d189a36 100755
--- a/integration/js/src/main/resources/keycloak.js
+++ b/integration/js/src/main/resources/keycloak.js
@@ -183,6 +183,18 @@
return url;
}
+ kc.register = function (options) {
+ return adapter.register(options);
+ }
+
+ kc.createRegisterUrl = function(options) {
+ if (!options) {
+ options = {};
+ }
+ options.action = 'register';
+ return kc.createLoginUrl(options);
+ }
+
kc.createAccountUrl = function(options) {
var url = getRealmUrl()
+ '/account'
@@ -760,6 +772,11 @@
return createPromise().promise;
},
+ register: function(options) {
+ window.location.href = kc.createRegisterUrl(options);
+ return createPromise().promise;
+ },
+
accountManagement : function() {
window.location.href = kc.createAccountUrl();
return createPromise().promise;
@@ -858,6 +875,16 @@
return promise.promise;
},
+ register : function() {
+ var registerUrl = kc.createRegisterUrl();
+ var ref = window.open(registerUrl, '_blank', 'location=no');
+ ref.addEventListener('loadstart', function(event) {
+ if (event.url.indexOf('http://localhost') == 0) {
+ ref.close();
+ }
+ });
+ },
+
accountManagement : function() {
var accountUrl = kc.createAccountUrl();
var ref = window.open(accountUrl, '_blank', 'location=no');
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 46e5433..61cfa08 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/protocol/ProtocolMapperUtils.java b/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java
index c1a9938..2b86773 100755
--- a/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java
@@ -17,14 +17,14 @@ public class ProtocolMapperUtils {
public static final String USER_ATTRIBUTE = "user.attribute";
public static final String USER_SESSION_NOTE = "user.session.note";
public static final String MULTIVALUED = "multivalued";
- public static final String USER_MODEL_PROPERTY_LABEL = "User Property";
- public static final String USER_MODEL_PROPERTY_HELP_TEXT = "Name of the property method in the UserModel interface. For example, a value of 'email' would reference the UserModel.getEmail() method.";
- public static final String USER_MODEL_ATTRIBUTE_LABEL = "User Attribute";
- public static final String USER_MODEL_ATTRIBUTE_HELP_TEXT = "Name of stored user attribute which is the name of an attribute within the UserModel.attribute map.";
- public static final String USER_SESSION_MODEL_NOTE_LABEL = "User Session Note";
- public static final String USER_SESSION_MODEL_NOTE_HELP_TEXT = "Name of stored user session note within the UserSessionModel.note map.";
- public static final String MULTIVALUED_LABEL = "Multivalued";
- public static final String MULTIVALUED_HELP_TEXT = "Indicates if attribute supports multiple values. If true, then the list of all values of this attribute will be set as claim. If false, then just first value will be set as claim";
+ public static final String USER_MODEL_PROPERTY_LABEL = "usermodel.prop.label";
+ public static final String USER_MODEL_PROPERTY_HELP_TEXT = "usermodel.prop.tooltip";
+ public static final String USER_MODEL_ATTRIBUTE_LABEL = "usermodel.attr.label";
+ public static final String USER_MODEL_ATTRIBUTE_HELP_TEXT = "usermodel.attr.tooltip";
+ public static final String USER_SESSION_MODEL_NOTE_LABEL = "userSession.modelNote.label";
+ public static final String USER_SESSION_MODEL_NOTE_HELP_TEXT = "userSession.modelNote.tooltip";
+ public static final String MULTIVALUED_LABEL = "multivalued.label";
+ public static final String MULTIVALUED_HELP_TEXT = "multivalued.tooltip";
public static String getUserModelValue(UserModel user, String propertyName) {
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>