keycloak-uncached
KEYCLOAK-1937 OpenID Connect Dynamic Client Registration KEYCLOAK-1938 Register …
Changes
client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistration.java 68(+61 -7)
client-registration/api/src/main/java/org/keycloak/client/registration/OIDCClientRepresentationMixIn.java 22(+22 -0)
client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java 2(+2 -0)
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java 70(+21 -49)
services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java 12(+11 -1)
services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java 125(+125 -0)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationService.java 2(+1 -1)
services/src/main/java/org/keycloak/services/clientregistration/DefaultClientRegistrationProvider.java 91(+14 -77)
services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java 18(+7 -11)
services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java 110(+56 -54)
services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProviderFactory.java 4(+3 -1)
services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientResponseRepresentation.java 77(+0 -77)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java 85(+85 -0)
Details
diff --git a/client-registration/api/src/main/java/org/keycloak/client/registration/Auth.java b/client-registration/api/src/main/java/org/keycloak/client/registration/Auth.java
index 31ad4e8..c3dd313 100644
--- a/client-registration/api/src/main/java/org/keycloak/client/registration/Auth.java
+++ b/client-registration/api/src/main/java/org/keycloak/client/registration/Auth.java
@@ -5,6 +5,7 @@ import org.apache.http.HttpRequest;
import org.keycloak.common.util.Base64;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -21,11 +22,14 @@ public abstract class Auth {
return new BearerTokenAuth(initialAccess.getToken());
}
-
public static Auth token(ClientRepresentation client) {
return new BearerTokenAuth(client.getRegistrationAccessToken());
}
+ public static Auth token(OIDCClientRepresentation client) {
+ return new BearerTokenAuth(client.getRegistrationAccessToken());
+ }
+
public static Auth client(String clientId, String clientSecret) {
return new BasicAuth(clientId, clientSecret);
}
diff --git a/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistration.java b/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistration.java
index 2b1a991..f2215f1 100644
--- a/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistration.java
+++ b/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistration.java
@@ -3,8 +3,10 @@ package org.keycloak.client.registration;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;
import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
@@ -18,10 +20,17 @@ public class ClientRegistration {
public static final ObjectMapper outputMapper = new ObjectMapper();
static {
outputMapper.getSerializationConfig().addMixInAnnotations(ClientRepresentation.class, ClientRepresentationMixIn.class);
+ outputMapper.getSerializationConfig().addMixInAnnotations(OIDCClientRepresentation.class, OIDCClientRepresentationMixIn.class);
+ outputMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
}
+ private final String JSON = "application/json";
+ private final String XML = "application/xml";
+
private final String DEFAULT = "default";
private final String INSTALLATION = "install";
+ private final String OIDC = "openid-connect";
+ private final String SAML = "saml2-entity-descriptor";
private HttpUtil httpUtil;
@@ -47,23 +56,23 @@ public class ClientRegistration {
public ClientRepresentation create(ClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client);
- InputStream resultStream = httpUtil.doPost(content, DEFAULT);
+ InputStream resultStream = httpUtil.doPost(content, JSON, JSON, DEFAULT);
return deserialize(resultStream, ClientRepresentation.class);
}
public ClientRepresentation get(String clientId) throws ClientRegistrationException {
- InputStream resultStream = httpUtil.doGet(DEFAULT, clientId);
+ InputStream resultStream = httpUtil.doGet(JSON, DEFAULT, clientId);
return resultStream != null ? deserialize(resultStream, ClientRepresentation.class) : null;
}
public AdapterConfig getAdapterConfig(String clientId) throws ClientRegistrationException {
- InputStream resultStream = httpUtil.doGet(INSTALLATION, clientId);
+ InputStream resultStream = httpUtil.doGet(JSON, INSTALLATION, clientId);
return resultStream != null ? deserialize(resultStream, AdapterConfig.class) : null;
}
public ClientRepresentation update(ClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client);
- InputStream resultStream = httpUtil.doPut(content, DEFAULT, client.getClientId());
+ InputStream resultStream = httpUtil.doPut(content, JSON, JSON, DEFAULT, client.getClientId());
return resultStream != null ? deserialize(resultStream, ClientRepresentation.class) : null;
}
@@ -75,10 +84,17 @@ public class ClientRegistration {
httpUtil.doDelete(DEFAULT, clientId);
}
- public static String serialize(ClientRepresentation client) throws ClientRegistrationException {
- try {
+ public OIDCClientRegistration oidc() {
+ return new OIDCClientRegistration();
+ }
- return outputMapper.writeValueAsString(client);
+ public SAMLClientRegistration saml() {
+ return new SAMLClientRegistration();
+ }
+
+ public static String serialize(Object obj) throws ClientRegistrationException {
+ try {
+ return outputMapper.writeValueAsString(obj);
} catch (IOException e) {
throw new ClientRegistrationException("Failed to write json object", e);
}
@@ -92,6 +108,44 @@ public class ClientRegistration {
}
}
+ public class OIDCClientRegistration {
+
+ public OIDCClientRepresentation create(OIDCClientRepresentation client) throws ClientRegistrationException {
+ String content = serialize(client);
+ InputStream resultStream = httpUtil.doPost(content, JSON, JSON, OIDC);
+ return deserialize(resultStream, OIDCClientRepresentation.class);
+ }
+
+ public OIDCClientRepresentation get(String clientId) throws ClientRegistrationException {
+ InputStream resultStream = httpUtil.doGet(JSON, OIDC, clientId);
+ return resultStream != null ? deserialize(resultStream, OIDCClientRepresentation.class) : null;
+ }
+
+ public OIDCClientRepresentation update(OIDCClientRepresentation client) throws ClientRegistrationException {
+ String content = serialize(client);
+ InputStream resultStream = httpUtil.doPut(content, JSON, JSON, OIDC, client.getClientId());
+ return resultStream != null ? deserialize(resultStream, OIDCClientRepresentation.class) : null;
+ }
+
+ public void delete(OIDCClientRepresentation client) throws ClientRegistrationException {
+ delete(client.getClientId());
+ }
+
+ public void delete(String clientId) throws ClientRegistrationException {
+ httpUtil.doDelete(OIDC, clientId);
+ }
+
+ }
+
+ public class SAMLClientRegistration {
+
+ public ClientRepresentation create(String entityDescriptor) throws ClientRegistrationException {
+ InputStream resultStream = httpUtil.doPost(entityDescriptor, XML, JSON, SAML);
+ return deserialize(resultStream, ClientRepresentation.class);
+ }
+
+ }
+
public static class ClientRegistrationBuilder {
private String url;
diff --git a/client-registration/api/src/main/java/org/keycloak/client/registration/HttpUtil.java b/client-registration/api/src/main/java/org/keycloak/client/registration/HttpUtil.java
index 6444749..4d44710 100644
--- a/client-registration/api/src/main/java/org/keycloak/client/registration/HttpUtil.java
+++ b/client-registration/api/src/main/java/org/keycloak/client/registration/HttpUtil.java
@@ -33,12 +33,12 @@ class HttpUtil {
this.auth = auth;
}
- InputStream doPost(String content, String... path) throws ClientRegistrationException {
+ InputStream doPost(String content, String contentType, String acceptType, String... path) throws ClientRegistrationException {
try {
HttpPost request = new HttpPost(getUrl(baseUri, path));
- request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
- request.setHeader(HttpHeaders.ACCEPT, "application/json");
+ request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
+ request.setHeader(HttpHeaders.ACCEPT, acceptType);
request.setEntity(new StringEntity(content));
addAuth(request);
@@ -60,11 +60,11 @@ class HttpUtil {
}
}
- InputStream doGet(String... path) throws ClientRegistrationException {
+ InputStream doGet(String acceptType, String... path) throws ClientRegistrationException {
try {
HttpGet request = new HttpGet(getUrl(baseUri, path));
- request.setHeader(HttpHeaders.ACCEPT, "application/json");
+ request.setHeader(HttpHeaders.ACCEPT, acceptType);
addAuth(request);
@@ -90,12 +90,12 @@ class HttpUtil {
}
}
- InputStream doPut(String content, String... path) throws ClientRegistrationException {
+ InputStream doPut(String content, String contentType, String acceptType, String... path) throws ClientRegistrationException {
try {
HttpPut request = new HttpPut(getUrl(baseUri, path));
- request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
- request.setHeader(HttpHeaders.ACCEPT, "application/json");
+ request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
+ request.setHeader(HttpHeaders.ACCEPT, acceptType);
request.setEntity(new StringEntity(content));
addAuth(request);
@@ -134,7 +134,7 @@ class HttpUtil {
response.getEntity().getContent().close();
}
- if (response.getStatusLine().getStatusCode() != 200) {
+ if (response.getStatusLine().getStatusCode() != 204) {
throw new HttpErrorException(response.getStatusLine());
}
} catch (IOException e) {
diff --git a/client-registration/api/src/main/java/org/keycloak/client/registration/OIDCClientRepresentationMixIn.java b/client-registration/api/src/main/java/org/keycloak/client/registration/OIDCClientRepresentationMixIn.java
new file mode 100644
index 0000000..b0dfed6
--- /dev/null
+++ b/client-registration/api/src/main/java/org/keycloak/client/registration/OIDCClientRepresentationMixIn.java
@@ -0,0 +1,22 @@
+package org.keycloak.client.registration;
+
+import org.codehaus.jackson.annotate.JsonIgnore;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+abstract class OIDCClientRepresentationMixIn {
+
+ @JsonIgnore
+ private Integer client_id_issued_at;
+
+ @JsonIgnore
+ private Integer client_secret_expires_at;
+
+ @JsonIgnore
+ private String registration_client_uri;
+
+ @JsonIgnore
+ private String registration_access_token;
+
+}
diff --git a/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java
index f0b0857..986faac 100644
--- a/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java
+++ b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java
@@ -62,6 +62,8 @@ public class ClientRegistrationCLI {
CommandContainer command = registry.getCommand(args[0], null);
ParserGenerator.parseAndPopulate(command, args[0], Arrays.copyOfRange(args, 1, args.length));
}*/
+
+ //commandInvocation.getCommandRegistry().getAllCommandNames()
}
}
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml b/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml
index 0070cc0..fab7119 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml
@@ -10,15 +10,15 @@
<para>
The Client Registration Service provides built-in support for Keycloak Client Representations, OpenID Connect
Client Meta Data and SAML Entity Descriptors. It's also possible to plugin custom client registration providers
- if required. The Client Registration Service endpoint is <literal><KEYCLOAK URL>/clients/<provider></literal>.
+ if required. The Client Registration Service endpoint is <literal><KEYCLOAK URL>/realms/<realm>/clients/<provider></literal>.
</para>
<para>
The built-in supported <literal>providers</literal> are:
<itemizedlist>
<listitem><literal>default</literal> Keycloak Representations</listitem>
<listitem><literal>install</literal> Keycloak Adapter Configuration</listitem>
- <!--<listitem><literal>openid-connect</literal> OpenID Connect Dynamic Client Registration</listitem>-->
- <!--<listitem><literal>saml-ed</literal> SAML Entity Descriptors</listitem>-->
+ <listitem><literal>openid-connect</literal> OpenID Connect Dynamic Client Registration</listitem>
+ <listitem><literal>saml2-entity-descriptor</literal> SAML Entity Descriptors</listitem>
</itemizedlist>
The following sections will describe how to use the different providers.
</para>
@@ -106,30 +106,30 @@ Authorization: bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJmMjJmNzQyYy04ZjNlLTQ2M....
</para>
<para>
To create a client create a Client Representation (JSON) then do a HTTP POST to:
- <literal><KEYCLOAK URL>/clients/<provider>/default</literal>. It will return a Client Representation
+ <literal><KEYCLOAK URL>/realms/<realm>/clients/<provider>/default</literal>. It will return a Client Representation
that also includes the registration access token. You should save the registration access token somewhere
if you want to retrieve the config, update or delete the client later.
</para>
<para>
To retrieve the Client Representation then do a HTTP GET to:
- <literal><KEYCLOAK URL>/clients/<provider>/default/<client id></literal>. It will also
+ <literal><KEYCLOAK URL>/realms/<realm>clients/<provider>/default/<client id></literal>. It will also
return a new registration access token.
</para>
<para>
To update the Client Representation then do a HTTP PUT to with the updated Client Representation to:
- <literal><KEYCLOAK URL>/clients/<provider>/default/<client id></literal>. It will also
+ <literal><KEYCLOAK URL>/realms/<realm>/clients/<provider>/default/<client id></literal>. It will also
return a new registration access token.
</para>
<para>
To delete the Client Representation then do a HTTP DELETE to:
- <literal><KEYCLOAK URL>/clients/<provider>/default/<client id></literal>
+ <literal><KEYCLOAK URL>/realms/<realm>/clients/<provider>/default/<client id></literal>
</para>
</section>
<section>
<title>Keycloak Adapter Configuration</title>
<para>
- The <default>installation</default> client registration provider can be used to retrieve the adapter configuration
+ The <literal>installation</literal> client registration provider can be used to retrieve the adapter configuration
for a client. In addition to token authentication you can also authenticate with client credentials using
HTTP basic authentication. To do this include the following header in the request:
<programlisting><![CDATA[
@@ -138,7 +138,7 @@ Authorization: basic BASE64(client-id + ':' + client-secret)
</para>
<para>
To retrieve the Adapter Configuration then do a HTTP GET to:
- <literal><KEYCLOAK URL>/clients/<provider>/installation/<client id></literal>
+ <literal><KEYCLOAK URL>//realms/<realm>clients/<provider>/installation/<client id></literal>
</para>
<para>
No authentication is required for public clients. This means that for the JavaScript adapter you can
@@ -146,23 +146,36 @@ Authorization: basic BASE64(client-id + ':' + client-secret)
</para>
</section>
- <!--
<section>
<title>OpenID Connect Dynamic Client Registration</title>
<para>
- TODO
+ Keycloak implements <ulink url="https://openid.net/specs/openid-connect-registration-1_0.html">OpenID Connect Dynamic Client Registration</ulink>,
+ which extends <ulink url="https://tools.ietf.org/html/rfc7591">OAuth 2.0 Dynamic Client Registration Protocol</ulink> and
+ <ulink url="https://tools.ietf.org/html/rfc7592">OAuth 2.0 Dynamic Client Registration Management Protocol</ulink>.
+ </para>
+ <para>
+ The endpoint to use these specifications to register clients in Keycloak is:
+ <literal><KEYCLOAK URL>/realms/<realm>/clients/<provider>/oidc[/<client id>]</literal>.
+ </para>
+ <para>
+ This endpoints can also be found in the OpenID Connect Discovery endpoint for the realm:
+ <literal><KEYCLOAK URL>/realms/<realm>/.well-known/openid-configuration</literal>.
</para>
</section>
- -->
- <!--
<section>
<title>SAML Entity Descriptors</title>
<para>
- TODO
+ The SAML Entity Descriptor endpoint only supports using SAML v2 Entity Descriptors to create clients. It
+ doesn't support retrieving, updating or deleting clients. For those operations the Keycloak representation
+ endpoints should be used. When creating a client a Keycloak Client Representation is returned with details
+ about the created client, including a registration access token.
+ </para>
+ <para>
+ To create a client do a HTTP POST with the SAML Entity Descriptor to:
+ <literal><KEYCLOAK URL>/realms/<realm>/clients/<provider>/saml2-entity-descriptor</literal>.
</para>
</section>
- -->
<section>
<title>Client Registration Java API</title>
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java
index 81c2df4..1ee3cd2 100644
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java
@@ -1,63 +1,35 @@
package org.keycloak.protocol.saml.clientregistration;
-import org.jboss.logging.Logger;
-import org.keycloak.events.EventBuilder;
+import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.services.clientregistration.ClientRegistrationAuth;
-import org.keycloak.services.clientregistration.ClientRegistrationProvider;
+import org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.net.URI;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class EntityDescriptorClientRegistrationProvider implements ClientRegistrationProvider {
-
- private static final Logger logger = Logger.getLogger(EntityDescriptorClientRegistrationProvider.class);
-
- private KeycloakSession session;
- private EventBuilder event;
- private ClientRegistrationAuth auth;
+public class EntityDescriptorClientRegistrationProvider extends AbstractClientRegistrationProvider {
public EntityDescriptorClientRegistrationProvider(KeycloakSession session) {
- this.session = session;
- }
-
-// @POST
-// @Consumes(MediaType.APPLICATION_XML)
-// @Produces(MediaType.APPLICATION_JSON)
-// public Response create(String descriptor) {
-// event.event(EventType.CLIENT_REGISTER);
-//
-// auth.requireCreate();
-//
-// ClientRepresentation client = session.getProvider(ClientDescriptionConverter.class, EntityDescriptorDescriptionConverter.ID).convertToInternal(descriptor);
-//
-// try {
-// ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
-// client = ModelToRepresentation.toRepresentation(clientModel);
-// URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
-//
-// logger.infov("Created client {0}", client.getClientId());
-//
-// event.client(client.getClientId()).success();
-//
-// return Response.created(uri).entity(client).build();
-// } catch (ModelDuplicateException e) {
-// return ErrorResponse.exists("Client " + client.getClientId() + " already exists");
-// }
-// }
-
- @Override
- public void close() {
- }
-
- @Override
- public void setAuth(ClientRegistrationAuth auth) {
- this.auth = auth;
+ super(session);
}
- @Override
- public void setEvent(EventBuilder event) {
- this.event = event;
+ @POST
+ @Consumes(MediaType.APPLICATION_XML)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response createSaml(String descriptor) {
+ ClientRepresentation client = session.getProvider(ClientDescriptionConverter.class, EntityDescriptorDescriptionConverter.ID).convertToInternal(descriptor);
+ client = create(client);
+ URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
+ return Response.created(uri).entity(client).build();
}
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java
index 955dfe4..6caacd6 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java
@@ -5,8 +5,7 @@ import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.exportimport.ClientDescriptionConverterFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
-import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.protocol.oidc.representations.OIDCClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.clientregistration.oidc.DescriptionConverter;
import org.keycloak.util.JsonSerialization;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
index 714c0d1..60e9f9d 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
@@ -4,6 +4,8 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
+import org.keycloak.services.clientregistration.ClientRegistrationService;
+import org.keycloak.services.clientregistration.oidc.OIDCClientRegistrationProviderFactory;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.Urls;
import org.keycloak.wellknown.WellKnownProvider;
@@ -48,6 +50,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
config.setUserinfoEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "issueUserInfo").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
config.setLogoutEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "logout").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
config.setJwksUri(uriBuilder.clone().path(OIDCLoginProtocolService.class, "certs").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
+ config.setRegistrationEndpoint(RealmsResource.clientRegistrationUrl(uriInfo).path(ClientRegistrationService.class, "provider").build(realm.getName(), OIDCClientRegistrationProviderFactory.ID).toString());
config.setIdTokenSigningAlgValuesSupported(DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED);
config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
index 9245e58..0226331 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
@@ -7,7 +7,6 @@ import org.codehaus.jackson.annotate.JsonProperty;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -48,6 +47,9 @@ public class OIDCConfigurationRepresentation {
@JsonProperty("response_modes_supported")
private List<String> responseModesSupported;
+ @JsonProperty("registration_endpoint")
+ private String registrationEndpoint;
+
protected Map<String, Object> otherClaims = new HashMap<String, Object>();
public String getIssuer() {
@@ -138,6 +140,14 @@ public class OIDCConfigurationRepresentation {
this.responseModesSupported = responseModesSupported;
}
+ public String getRegistrationEndpoint() {
+ return registrationEndpoint;
+ }
+
+ public void setRegistrationEndpoint(String registrationEndpoint) {
+ this.registrationEndpoint = registrationEndpoint;
+ }
+
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
new file mode 100644
index 0000000..0c95a37
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
@@ -0,0 +1,125 @@
+package org.keycloak.services.clientregistration;
+
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.ClientInitialAccessModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.ForbiddenException;
+
+import javax.ws.rs.core.Response;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public abstract class AbstractClientRegistrationProvider implements ClientRegistrationProvider {
+
+ protected KeycloakSession session;
+ protected EventBuilder event;
+ protected ClientRegistrationAuth auth;
+
+ public AbstractClientRegistrationProvider(KeycloakSession session) {
+ this.session = session;
+ }
+
+ public ClientRepresentation create(ClientRepresentation client) {
+ event.event(EventType.CLIENT_REGISTER);
+
+ auth.requireCreate();
+
+ try {
+ ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
+ if (client.getClientId() == null) {
+ clientModel.setClientId(clientModel.getId());
+ }
+
+ client = ModelToRepresentation.toRepresentation(clientModel);
+
+ String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, clientModel);
+
+ client.setRegistrationAccessToken(registrationAccessToken);
+
+ if (auth.isInitialAccessToken()) {
+ ClientInitialAccessModel initialAccessModel = auth.getInitialAccessModel();
+ initialAccessModel.decreaseRemainingCount();
+ }
+
+ event.client(client.getClientId()).success();
+ return client;
+ } catch (ModelDuplicateException e) {
+ throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client Identifier in use", Response.Status.BAD_REQUEST);
+ }
+ }
+
+ public ClientRepresentation get(String clientId) {
+ event.event(EventType.CLIENT_INFO);
+
+ ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
+ auth.requireView(client);
+
+ ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
+
+ if (auth.isRegistrationAccessToken()) {
+ String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client);
+ rep.setRegistrationAccessToken(registrationAccessToken);
+ }
+
+ event.client(client.getClientId()).success();
+ return rep;
+ }
+
+ public ClientRepresentation update(String clientId, ClientRepresentation rep) {
+ event.event(EventType.CLIENT_UPDATE).client(clientId);
+
+ ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
+ auth.requireUpdate(client);
+
+ if (!client.getClientId().equals(rep.getClientId())) {
+ throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client Identifier modified", Response.Status.BAD_REQUEST);
+ }
+
+ RepresentationToModel.updateClient(rep, client);
+ rep = ModelToRepresentation.toRepresentation(client);
+
+ if (auth.isRegistrationAccessToken()) {
+ String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client);
+ rep.setRegistrationAccessToken(registrationAccessToken);
+ }
+
+ event.client(client.getClientId()).success();
+ return rep;
+ }
+
+ public void delete(String clientId) {
+ event.event(EventType.CLIENT_DELETE).client(clientId);
+
+ ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
+ auth.requireUpdate(client);
+
+ if (session.getContext().getRealm().removeClient(client.getId())) {
+ event.client(client.getClientId()).success();
+ } else {
+ throw new ForbiddenException();
+ }
+ }
+
+ @Override
+ public void setAuth(ClientRegistrationAuth auth) {
+ this.auth = auth;
+ }
+
+ @Override
+ public void setEvent(EventBuilder event) {
+ this.event = event;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationService.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationService.java
index 0581388..621d5fb 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationService.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationService.java
@@ -25,7 +25,7 @@ public class ClientRegistrationService {
}
@Path("{provider}")
- public Object getProvider(@PathParam("provider") String providerId) {
+ public Object provider(@PathParam("provider") String providerId) {
checkSsl();
ClientRegistrationProvider provider = session.getProvider(ClientRegistrationProvider.class, providerId);
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/DefaultClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/DefaultClientRegistrationProvider.java
index 38d71f2..126d00a 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/DefaultClientRegistrationProvider.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/DefaultClientRegistrationProvider.java
@@ -2,14 +2,11 @@ package org.keycloak.services.clientregistration;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
-import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.services.ErrorResponse;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
@@ -19,101 +16,41 @@ import java.net.URI;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class DefaultClientRegistrationProvider implements ClientRegistrationProvider {
-
- private KeycloakSession session;
- private EventBuilder event;
- private ClientRegistrationAuth auth;
+public class DefaultClientRegistrationProvider extends AbstractClientRegistrationProvider {
public DefaultClientRegistrationProvider(KeycloakSession session) {
- this.session = session;
+ super(session);
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
- public Response create(ClientRepresentation client) {
- event.event(EventType.CLIENT_REGISTER);
-
- auth.requireCreate();
-
- try {
- ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
- client = ModelToRepresentation.toRepresentation(clientModel);
-
- String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, clientModel);
-
- client.setRegistrationAccessToken(registrationAccessToken);
-
- URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
-
- if (auth.isInitialAccessToken()) {
- ClientInitialAccessModel initialAccessModel = auth.getInitialAccessModel();
- initialAccessModel.decreaseRemainingCount();
- }
-
- event.client(client.getClientId()).success();
- return Response.created(uri).entity(client).build();
- } catch (ModelDuplicateException e) {
- return ErrorResponse.exists("Client " + client.getClientId() + " already exists");
- }
+ public Response createDefault(ClientRepresentation client) {
+ client = create(client);
+ URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
+ return Response.created(uri).entity(client).build();
}
@GET
@Path("{clientId}")
@Produces(MediaType.APPLICATION_JSON)
- public Response get(@PathParam("clientId") String clientId) {
- event.event(EventType.CLIENT_INFO);
-
- ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
- auth.requireView(client);
-
- ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
-
- if (auth.isRegistrationAccessToken()) {
- String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client);
- rep.setRegistrationAccessToken(registrationAccessToken);
- }
-
- event.client(client.getClientId()).success();
- return Response.ok(rep).build();
+ public Response getDefault(@PathParam("clientId") String clientId) {
+ ClientRepresentation client = get(clientId);
+ return Response.ok(client).build();
}
@PUT
@Path("{clientId}")
@Consumes(MediaType.APPLICATION_JSON)
- public Response update(@PathParam("clientId") String clientId, ClientRepresentation rep) {
- event.event(EventType.CLIENT_UPDATE).client(clientId);
-
- ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
- auth.requireUpdate(client);
-
- RepresentationToModel.updateClient(rep, client);
- rep = ModelToRepresentation.toRepresentation(client);
-
- if (auth.isRegistrationAccessToken()) {
- String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client);
- rep.setRegistrationAccessToken(registrationAccessToken);
- }
-
- event.client(client.getClientId()).success();
- return Response.ok(rep).build();
+ public Response updateDefault(@PathParam("clientId") String clientId, ClientRepresentation client) {
+ client = update(clientId, client);
+ return Response.ok(client).build();
}
@DELETE
@Path("{clientId}")
- public Response delete(@PathParam("clientId") String clientId) {
- event.event(EventType.CLIENT_DELETE).client(clientId);
-
- ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
- auth.requireUpdate(client);
-
- if (session.getContext().getRealm().removeClient(client.getId())) {
- event.client(client.getClientId()).success();
- return Response.ok().build();
- } else {
- return Response.status(Response.Status.NOT_FOUND).build();
- }
+ public void deleteDefault(@PathParam("clientId") String clientId) {
+ delete(clientId);
}
@Override
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ErrorCodes.java b/services/src/main/java/org/keycloak/services/clientregistration/ErrorCodes.java
new file mode 100644
index 0000000..ed491de
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ErrorCodes.java
@@ -0,0 +1,12 @@
+package org.keycloak.services.clientregistration;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface ErrorCodes {
+
+ String INVALID_REDIRECT_URI = "invalid_redirect_uri";
+
+ String INVALID_CLIENT_METADATA = "invalid_client_metadata";
+
+}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
index 1e0784c..a7f9f2c 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
@@ -1,8 +1,9 @@
package org.keycloak.services.clientregistration.oidc;
-import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.protocol.oidc.representations.OIDCClientRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+
+import java.net.URI;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -11,27 +12,22 @@ public class DescriptionConverter {
public static ClientRepresentation toInternal(OIDCClientRepresentation clientOIDC) {
ClientRepresentation client = new ClientRepresentation();
- client.setClientId(KeycloakModelUtils.generateId());
+ client.setClientId(clientOIDC.getClientId());
client.setName(clientOIDC.getClientName());
client.setRedirectUris(clientOIDC.getRedirectUris());
client.setBaseUrl(clientOIDC.getClientUri());
return client;
}
- public static OIDCClientResponseRepresentation toExternalResponse(ClientRepresentation client) {
- OIDCClientResponseRepresentation response = new OIDCClientResponseRepresentation();
+ public static OIDCClientRepresentation toExternalResponse(ClientRepresentation client, URI uri) {
+ OIDCClientRepresentation response = new OIDCClientRepresentation();
response.setClientId(client.getClientId());
-
response.setClientName(client.getName());
response.setClientUri(client.getBaseUrl());
-
response.setClientSecret(client.getSecret());
- response.setClientSecretExpiresAt(0);
-
response.setRedirectUris(client.getRedirectUris());
-
response.setRegistrationAccessToken(client.getRegistrationAccessToken());
-
+ response.setRegistrationClientUri(uri.toString());
return response;
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java
index 961f28d..e60720b 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java
@@ -1,72 +1,70 @@
package org.keycloak.services.clientregistration.oidc;
-import org.jboss.logging.Logger;
+import org.keycloak.common.util.Time;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
import org.keycloak.services.clientregistration.ClientRegistrationAuth;
-import org.keycloak.services.clientregistration.ClientRegistrationProvider;
+import org.keycloak.services.clientregistration.ErrorCodes;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.net.URI;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class OIDCClientRegistrationProvider implements ClientRegistrationProvider {
+public class OIDCClientRegistrationProvider extends AbstractClientRegistrationProvider {
- private static final Logger logger = Logger.getLogger(OIDCClientRegistrationProvider.class);
+ public OIDCClientRegistrationProvider(KeycloakSession session) {
+ super(session);
+ }
- private KeycloakSession session;
- private EventBuilder event;
- private ClientRegistrationAuth auth;
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response createOIDC(OIDCClientRepresentation clientOIDC) {
+ if (clientOIDC.getClientId() != null) {
+ throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client Identifier included", Response.Status.BAD_REQUEST);
+ }
- public OIDCClientRegistrationProvider(KeycloakSession session) {
- this.session = session;
+ ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
+ client = create(client);
+ URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
+ clientOIDC = DescriptionConverter.toExternalResponse(client, uri);
+ clientOIDC.setClientIdIssuedAt(Time.currentTime());
+ return Response.created(uri).entity(clientOIDC).build();
}
-// @POST
-// @Consumes(MediaType.APPLICATION_JSON)
-// @Produces(MediaType.APPLICATION_JSON)
-// public Response create(OIDCClientRepresentation clientOIDC) {
-// event.event(EventType.CLIENT_REGISTER);
-//
-// auth.requireCreate();
-//
-// ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
-//
-// try {
-// ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
-//
-// client = ModelToRepresentation.toRepresentation(clientModel);
-//
-// String registrationAccessToken = TokenGenerator.createRegistrationAccessToken();
-//
-// clientModel.setRegistrationToken(registrationAccessToken);
-//
-// URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
-//
-// logger.infov("Created client {0}", client.getClientId());
-//
-// event.client(client.getClientId()).success();
-//
-// OIDCClientResponseRepresentation response = DescriptionConverter.toExternalResponse(client);
-//
-// response.setClientName(client.getName());
-// response.setClientUri(client.getBaseUrl());
-//
-// response.setClientSecret(client.getSecret());
-// response.setClientSecretExpiresAt(0);
-//
-// response.setRedirectUris(client.getRedirectUris());
-//
-// response.setRegistrationAccessToken(registrationAccessToken);
-// response.setRegistrationClientUri(uri.toString());
-//
-// return Response.created(uri).entity(response).build();
-// } catch (ModelDuplicateException e) {
-// return ErrorResponse.exists("Client " + client.getClientId() + " already exists");
-// }
-// }
+ @GET
+ @Path("{clientId}")
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response getOIDC(@PathParam("clientId") String clientId) {
+ ClientRepresentation client = get(clientId);
+ OIDCClientRepresentation clientOIDC = DescriptionConverter.toExternalResponse(client, session.getContext().getUri().getRequestUri());
+ return Response.ok(clientOIDC).build();
+ }
- @Override
- public void close() {
+ @PUT
+ @Path("{clientId}")
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response updateOIDC(@PathParam("clientId") String clientId, OIDCClientRepresentation clientOIDC) {
+ ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
+ client = update(clientId, client);
+ URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
+ clientOIDC = DescriptionConverter.toExternalResponse(client, uri);
+ return Response.ok(clientOIDC).build();
+ }
+
+ @DELETE
+ @Path("{clientId}")
+ public void deleteOIDC(@PathParam("clientId") String clientId) {
+ delete(clientId);
}
@Override
@@ -79,4 +77,8 @@ public class OIDCClientRegistrationProvider implements ClientRegistrationProvide
this.event = event;
}
+ @Override
+ public void close() {
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProviderFactory.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProviderFactory.java
index 6f112f8..0144e79 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProviderFactory.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProviderFactory.java
@@ -11,6 +11,8 @@ import org.keycloak.services.clientregistration.ClientRegistrationProviderFactor
*/
public class OIDCClientRegistrationProviderFactory implements ClientRegistrationProviderFactory {
+ public static final String ID = "openid-connect";
+
@Override
public ClientRegistrationProvider create(KeycloakSession session) {
return new OIDCClientRegistrationProvider(session);
@@ -30,7 +32,7 @@ public class OIDCClientRegistrationProviderFactory implements ClientRegistration
@Override
public String getId() {
- return "openid-connect";
+ return ID;
}
}
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 4b5e4f2..cc1e49a 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -59,6 +59,10 @@ public class RealmsResource {
return uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getProtocol");
}
+ public static UriBuilder clientRegistrationUrl(UriInfo uriInfo) {
+ return uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getClientsService");
+ }
+
public static UriBuilder brokerUrl(UriInfo uriInfo) {
return uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getBrokerService");
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java
index bb33515..42870a6 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java
@@ -180,7 +180,6 @@ public class RealmTest extends AbstractClientTest {
String description = IOUtils.toString(getClass().getResourceAsStream("/client-descriptions/client-oidc.json"));
ClientRepresentation converted = realm.convertClientDescription(description);
- assertEquals(36, converted.getClientId().length());
assertEquals(1, converted.getRedirectUris().size());
assertEquals("http://localhost", converted.getRedirectUris().get(0));
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
new file mode 100644
index 0000000..9696274
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
@@ -0,0 +1,85 @@
+package org.keycloak.testsuite.client;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.client.registration.Auth;
+import org.keycloak.client.registration.ClientRegistrationException;
+import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
+import org.keycloak.representations.idm.ClientInitialAccessPresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+
+import java.util.Collections;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
+
+ @Before
+ public void before() throws Exception {
+ super.before();
+
+ ClientInitialAccessPresentation token = adminClient.realm(REALM_NAME).clientInitialAccess().create(new ClientInitialAccessCreatePresentation(0, 10));
+ reg.auth(Auth.token(token));
+ }
+
+ public OIDCClientRepresentation create() throws ClientRegistrationException {
+ OIDCClientRepresentation client = new OIDCClientRepresentation();
+ client.setClientName("RegistrationAccessTokenTest");
+ client.setClientUri("http://root");
+ client.setRedirectUris(Collections.singletonList("http://redirect"));
+
+ OIDCClientRepresentation response = reg.oidc().create(client);
+
+ return response;
+ }
+
+ @Test
+ public void createClient() throws ClientRegistrationException {
+ OIDCClientRepresentation response = create();
+
+ assertNotNull(response.getRegistrationAccessToken());
+ assertNotNull(response.getClientIdIssuedAt());
+ assertNotNull(response.getClientId());
+ assertNull(response.getClientSecretExpiresAt());
+ assertNotNull(response.getRegistrationClientUri());
+ assertEquals("RegistrationAccessTokenTest", response.getClientName());
+ assertEquals("http://root", response.getClientUri());
+ assertEquals(1, response.getRedirectUris().size());
+ assertEquals("http://redirect", response.getRedirectUris().get(0));
+ }
+
+ @Test
+ public void getClient() throws ClientRegistrationException {
+ OIDCClientRepresentation response = create();
+ reg.auth(Auth.token(response));
+
+ OIDCClientRepresentation rep = reg.oidc().get(response.getClientId());
+ assertNotNull(rep);
+ assertNotEquals(response.getRegistrationAccessToken(), rep.getRegistrationAccessToken());
+ }
+
+ @Test
+ public void updateClient() throws ClientRegistrationException {
+ OIDCClientRepresentation response = create();
+ reg.auth(Auth.token(response));
+
+ response.setRedirectUris(Collections.singletonList("http://newredirect"));
+
+ OIDCClientRepresentation updated = reg.oidc().update(response);
+
+ assertEquals(1, updated.getRedirectUris().size());
+ assertEquals("http://newredirect", updated.getRedirectUris().get(0));
+ }
+
+ @Test
+ public void deleteClient() throws ClientRegistrationException {
+ OIDCClientRepresentation response = create();
+ reg.auth(Auth.token(response));
+
+ reg.oidc().delete(response);
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java
new file mode 100644
index 0000000..1f5eab8
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java
@@ -0,0 +1,42 @@
+package org.keycloak.testsuite.client;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.client.registration.Auth;
+import org.keycloak.client.registration.ClientRegistrationException;
+import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
+import org.keycloak.representations.idm.ClientInitialAccessPresentation;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class SAMLClientRegistrationTest extends AbstractClientRegistrationTest {
+
+ @Before
+ public void before() throws Exception {
+ super.before();
+
+ ClientInitialAccessPresentation token = adminClient.realm(REALM_NAME).clientInitialAccess().create(new ClientInitialAccessCreatePresentation(0, 10));
+ reg.auth(Auth.token(token));
+ }
+
+ @Test
+ public void createClient() throws ClientRegistrationException, IOException {
+ String entityDescriptor = IOUtils.toString(getClass().getResourceAsStream("/clientreg-test/saml-entity-descriptor.xml"));
+ ClientRepresentation response = reg.saml().create(entityDescriptor);
+
+ assertNotNull(response.getRegistrationAccessToken());
+ assertEquals("loadbalancer-9.siroe.com", response.getClientId());
+ assertEquals(1, response.getRedirectUris().size());
+ assertEquals("https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp", response.getRedirectUris().get(0));
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml
new file mode 100644
index 0000000..b00ab25
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml
@@ -0,0 +1,82 @@
+<EntityDescriptor
+ xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+ entityID="loadbalancer-9.siroe.com">
+ <SPSSODescriptor
+ AuthnRequestsSigned="false"
+ WantAssertionsSigned="false"
+ protocolSupportEnumeration=
+ "urn:oasis:names:tc:SAML:2.0:protocol">
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>
+MIICYDCCAgqgAwIBAgICBoowDQYJKoZIhvcNAQEEBQAwgZIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRQwEgYDVQQHEwtTYW50YSBDbGFyYTEeMBwGA1UEChMVU3VuIE1pY3Jvc3lz
+dGVtcyBJbmMuMRowGAYDVQQLExFJZGVudGl0eSBTZXJ2aWNlczEcMBoGA1UEAxMTQ2VydGlmaWNh
+dGUgTWFuYWdlcjAeFw0wNjExMDIxOTExMzRaFw0xMDA3MjkxOTExMzRaMDcxEjAQBgNVBAoTCXNp
+cm9lLmNvbTEhMB8GA1UEAxMYbG9hZGJhbGFuY2VyLTkuc2lyb2UuY29tMIGfMA0GCSqGSIb3DQEB
+AQUAA4GNADCBiQKBgQCjOwa5qoaUuVnknqf5pdgAJSEoWlvx/jnUYbkSDpXLzraEiy2UhvwpoBgB
+EeTSUaPPBvboCItchakPI6Z/aFdH3Wmjuij9XD8r1C+q//7sUO0IGn0ORycddHhoo0aSdnnxGf9V
+tREaqKm9dJ7Yn7kQHjo2eryMgYxtr/Z5Il5F+wIDAQABo2AwXjARBglghkgBhvhCAQEEBAMCBkAw
+DgYDVR0PAQH/BAQDAgTwMB8GA1UdIwQYMBaAFDugITflTCfsWyNLTXDl7cMDUKuuMBgGA1UdEQQR
+MA+BDW1hbGxhQHN1bi5jb20wDQYJKoZIhvcNAQEEBQADQQB/6DOB6sRqCZu2OenM9eQR0gube85e
+nTTxU4a7x1naFxzYXK1iQ1vMARKMjDb19QEJIEJKZlDK4uS7yMlf1nFS
+ </X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <KeyDescriptor use="encryption">
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>
+MIICTDCCAfagAwIBAgICBo8wDQYJKoZIhvcNAQEEBQAwgZIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRQwEgYDVQQHEwtTYW50YSBDbGFyYTEeMBwGA1UEChMVU3VuIE1pY3Jvc3lz
+dGVtcyBJbmMuMRowGAYDVQQLExFJZGVudGl0eSBTZXJ2aWNlczEcMBoGA1UEAxMTQ2VydGlmaWNh
+dGUgTWFuYWdlcjAeFw0wNjExMDcyMzU2MTdaFw0xMDA4MDMyMzU2MTdaMCMxITAfBgNVBAMTGGxv
+YWRiYWxhbmNlci05LnNpcm9lLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAw574iRU6
+HsSO4LXW/OGTXyfsbGv6XRVOoy3v+J1pZ51KKejcDjDJXNkKGn3/356AwIaqbcymWd59T0zSqYfR
+Hn+45uyjYxRBmVJseLpVnOXLub9jsjULfGx0yjH4w+KsZSZCXatoCHbj/RJtkzuZY6V9to/hkH3S
+InQB4a3UAgMCAwEAAaNgMF4wEQYJYIZIAYb4QgEBBAQDAgZAMA4GA1UdDwEB/wQEAwIE8DAfBgNV
+HSMEGDAWgBQ7oCE35Uwn7FsjS01w5e3DA1CrrjAYBgNVHREEETAPgQ1tYWxsYUBzdW4uY29tMA0G
+CSqGSIb3DQEBBAUAA0EAMlbfBg/ff0Xkv4DOR5LEqmfTZKqgdlD81cXynfzlF7XfnOqI6hPIA90I
+x5Ql0ejivIJAYcMGUyA+/YwJg2FGoA==
+ </X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ <EncryptionMethod Algorithm=
+ "https://www.w3.org/2001/04/xmlenc#aes128-cbc">
+ <KeySize xmlns="https://www.w3.org/2001/04/xmlenc#">128</KeySize>
+ </EncryptionMethod>
+ </KeyDescriptor>
+ <SingleLogoutService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp"
+ ResponseLocation="https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp"/>
+ <SingleLogoutService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/SPSloSoap/metaAlias/sp"/>
+ <ManageNameIDService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/SPMniRedirect/metaAlias/sp"
+ ResponseLocation="https://LoadBalancer-9.siroe.com:3443/federation/SPMniRedirect/metaAlias/sp"/>
+ <ManageNameIDService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/SPMniSoap/metaAlias/sp"
+ ResponseLocation="https://LoadBalancer-9.siroe.com:3443/federation/SPMniSoap/metaAlias/sp"/>
+ <NameIDFormat>
+ urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
+ </NameIDFormat>
+ <NameIDFormat>
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+ </NameIDFormat>
+ <AssertionConsumerService
+ isDefault="true"
+ index="0"
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp"/>
+ <AssertionConsumerService
+ index="1"
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp"/>
+ </SPSSODescriptor>
+</EntityDescriptor>