From 4f2b97de7f09be8c012055ebbfa346eeaaadd3fd Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Thu, 19 Nov 2015 21:04:35 +0100 Subject: [PATCH] KEYCLOAK-1937 OpenID Connect Dynamic Client Registration KEYCLOAK-1938 Register clients from SAML Entity Descriptors --- .../keycloak/client/registration/Auth.java | 6 +- .../registration/ClientRegistration.java | 68 +++++- .../client/registration/HttpUtil.java | 18 +- .../OIDCClientRepresentationMixIn.java | 22 ++ .../cli/ClientRegistrationCLI.java | 2 + .../oidc/OIDCClientRepresentation.java | 223 ++++++++++++++++++ .../en/en-US/modules/client-registration.xml | 43 ++-- ...yDescriptorClientRegistrationProvider.java | 70 ++---- .../oidc/OIDCClientDescriptionConverter.java | 3 +- .../protocol/oidc/OIDCWellKnownProvider.java | 3 + .../OIDCClientRepresentation.java | 177 -------------- .../OIDCConfigurationRepresentation.java | 12 +- .../AbstractClientRegistrationProvider.java | 125 ++++++++++ .../ClientRegistrationService.java | 2 +- .../DefaultClientRegistrationProvider.java | 91 ++----- .../clientregistration/ErrorCodes.java | 12 + .../oidc/DescriptionConverter.java | 18 +- .../oidc/OIDCClientRegistrationProvider.java | 112 ++++----- ...OIDCClientRegistrationProviderFactory.java | 4 +- .../OIDCClientResponseRepresentation.java | 77 ------ .../services/resources/RealmsResource.java | 4 + .../client/OIDCClientRegistrationTest.java | 85 +++++++ .../client/SAMLClientRegistrationTest.java | 42 ++++ .../clientreg-test/saml-entity-descriptor.xml | 82 +++++++ .../keycloak/testsuite/admin/RealmTest.java | 1 - 25 files changed, 818 insertions(+), 484 deletions(-) create mode 100644 client-registration/api/src/main/java/org/keycloak/client/registration/OIDCClientRepresentationMixIn.java create mode 100644 core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java delete mode 100644 services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCClientRepresentation.java create mode 100644 services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java create mode 100644 services/src/main/java/org/keycloak/services/clientregistration/ErrorCodes.java delete mode 100644 services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientResponseRepresentation.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml 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 31ad4e882f..c3dd313921 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 Stian Thorgersen @@ -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 2b1a991c6f..f2215f19b9 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 6444749782..4d44710f5b 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 0000000000..b0dfed637e --- /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 Stian Thorgersen + */ +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 f0b0857760..986faac648 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/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java b/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java new file mode 100644 index 0000000000..1ff32fc096 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java @@ -0,0 +1,223 @@ +package org.keycloak.representations.oidc; + +import org.codehaus.jackson.annotate.JsonAutoDetect; + +import java.util.List; + +/** + * @author Stian Thorgersen + */ +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) +public class OIDCClientRepresentation { + + private List redirect_uris; + + private String token_endpoint_auth_method; + + private String grant_types; + + private String response_types; + + private String client_id; + + private String client_secret; + + private String client_name; + + private String client_uri; + + private String logo_uri; + + private String scope; + + private String contacts; + + private String tos_uri; + + private String policy_uri; + + private String jwks_uri; + + private String jwks; + + private String software_id; + + private String software_version; + + private Integer client_id_issued_at; + + private Integer client_secret_expires_at; + + private String registration_client_uri; + + private String registration_access_token; + + public List getRedirectUris() { + return redirect_uris; + } + + public void setRedirectUris(List redirectUris) { + this.redirect_uris = redirectUris; + } + + public String getTokenEndpointAuthMethod() { + return token_endpoint_auth_method; + } + + public void setTokenEndpointAuthMethod(String token_endpoint_auth_method) { + this.token_endpoint_auth_method = token_endpoint_auth_method; + } + + public String getGrantTypes() { + return grant_types; + } + + public void setGrantTypes(String grantTypes) { + this.grant_types = grantTypes; + } + + public String getResponseTypes() { + return response_types; + } + + public void setResponseTypes(String responseTypes) { + this.response_types = responseTypes; + } + + public String getClientId() { + return client_id; + } + + public void setClientId(String clientId) { + this.client_id = clientId; + } + + public String getClientSecret() { + return client_secret; + } + + public void setClientSecret(String clientSecret) { + this.client_secret = clientSecret; + } + + public String getClientName() { + return client_name; + } + + public void setClientName(String client_name) { + this.client_name = client_name; + } + + public String getClientUri() { + return client_uri; + } + + public void setClientUri(String client_uri) { + this.client_uri = client_uri; + } + + public String getLogoUri() { + return logo_uri; + } + + public void setLogoUri(String logo_uri) { + this.logo_uri = logo_uri; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public String getContacts() { + return contacts; + } + + public void setContacts(String contacts) { + this.contacts = contacts; + } + + public String getTosUri() { + return tos_uri; + } + + public void setTosUri(String tos_uri) { + this.tos_uri = tos_uri; + } + + public String getPolicyUri() { + return policy_uri; + } + + public void setPolicyUri(String policy_uri) { + this.policy_uri = policy_uri; + } + + public String getJwksUri() { + return jwks_uri; + } + + public void setJwksUri(String jwks_uri) { + this.jwks_uri = jwks_uri; + } + + public String getJwks() { + return jwks; + } + + public void setJwks(String jwks) { + this.jwks = jwks; + } + + public String getSoftwareId() { + return software_id; + } + + public void setSoftwareId(String softwareId) { + this.software_id = softwareId; + } + + public String getSoftwareVersion() { + return software_version; + } + + public void setSoftwareVersion(String softwareVersion) { + this.software_version = softwareVersion; + } + + public Integer getClientIdIssuedAt() { + return client_id_issued_at; + } + + public void setClientIdIssuedAt(Integer clientIdIssuedAt) { + this.client_id_issued_at = clientIdIssuedAt; + } + + public Integer getClientSecretExpiresAt() { + return client_secret_expires_at; + } + + public void setClientSecretExpiresAt(Integer client_secret_expires_at) { + this.client_secret_expires_at = client_secret_expires_at; + } + + public String getRegistrationClientUri() { + return registration_client_uri; + } + + public void setRegistrationClientUri(String registrationClientUri) { + this.registration_client_uri = registrationClientUri; + } + + public String getRegistrationAccessToken() { + return registration_access_token; + } + + public void setRegistrationAccessToken(String registrationAccessToken) { + this.registration_access_token = registrationAccessToken; + } + +} 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 0070cc004b..fab7119e4f 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 @@ 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 <KEYCLOAK URL>/clients/<provider>. + if required. The Client Registration Service endpoint is <KEYCLOAK URL>/realms/<realm>/clients/<provider>. The built-in supported providers are: default Keycloak Representations install Keycloak Adapter Configuration - - + openid-connect OpenID Connect Dynamic Client Registration + saml2-entity-descriptor SAML Entity Descriptors The following sections will describe how to use the different providers. @@ -106,30 +106,30 @@ Authorization: bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJmMjJmNzQyYy04ZjNlLTQ2M.... To create a client create a Client Representation (JSON) then do a HTTP POST to: - <KEYCLOAK URL>/clients/<provider>/default. It will return a Client Representation + <KEYCLOAK URL>/realms/<realm>/clients/<provider>/default. 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. To retrieve the Client Representation then do a HTTP GET to: - <KEYCLOAK URL>/clients/<provider>/default/<client id>. It will also + <KEYCLOAK URL>/realms/<realm>clients/<provider>/default/<client id>. It will also return a new registration access token. To update the Client Representation then do a HTTP PUT to with the updated Client Representation to: - <KEYCLOAK URL>/clients/<provider>/default/<client id>. It will also + <KEYCLOAK URL>/realms/<realm>/clients/<provider>/default/<client id>. It will also return a new registration access token. To delete the Client Representation then do a HTTP DELETE to: - <KEYCLOAK URL>/clients/<provider>/default/<client id> + <KEYCLOAK URL>/realms/<realm>/clients/<provider>/default/<client id>
Keycloak Adapter Configuration - The installation client registration provider can be used to retrieve the adapter configuration + The installation 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: To retrieve the Adapter Configuration then do a HTTP GET to: - <KEYCLOAK URL>/clients/<provider>/installation/<client id> + <KEYCLOAK URL>//realms/<realm>clients/<provider>/installation/<client id> 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)
- -
Client Registration Java API 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 81c2df4d9e..1ee3cd2bff 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 Stian Thorgersen */ -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; + super(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; - } - - @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 955dfe4769..6caacd6f31 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 714c0d1da9..60e9f9d36a 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/OIDCClientRepresentation.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCClientRepresentation.java deleted file mode 100644 index 4a83ebddce..0000000000 --- a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCClientRepresentation.java +++ /dev/null @@ -1,177 +0,0 @@ -package org.keycloak.protocol.oidc.representations; - -import org.codehaus.jackson.annotate.JsonProperty; - -import java.util.List; - -/** - * @author Stian Thorgersen - */ -public class OIDCClientRepresentation { - - @JsonProperty("redirect_uris") - private List redirectUris; - - @JsonProperty("token_endpoint_auth_method") - private String tokenEndpointAuthMethod; - - @JsonProperty("grant_types") - private String grantTypes; - - @JsonProperty("response_types") - private String responseTypes; - - @JsonProperty("client_name") - private String clientName; - - @JsonProperty("client_uri") - private String clientUri; - - @JsonProperty("logo_uri") - private String logoUri; - - @JsonProperty("scope") - private String scope; - - @JsonProperty("contacts") - private String contacts; - - @JsonProperty("tos_uri") - private String tos_uri; - - @JsonProperty("policy_uri") - private String policy_uri; - - @JsonProperty("jwks_uri") - private String jwks_uri; - - @JsonProperty("jwks") - private String jwks; - - @JsonProperty("software_id") - private String softwareId; - - @JsonProperty("software_version") - private String softwareVersion; - - public List getRedirectUris() { - return redirectUris; - } - - public void setRedirectUris(List redirectUris) { - this.redirectUris = redirectUris; - } - - public String getTokenEndpointAuthMethod() { - return tokenEndpointAuthMethod; - } - - public void setTokenEndpointAuthMethod(String tokenEndpointAuthMethod) { - this.tokenEndpointAuthMethod = tokenEndpointAuthMethod; - } - - public String getGrantTypes() { - return grantTypes; - } - - public void setGrantTypes(String grantTypes) { - this.grantTypes = grantTypes; - } - - public String getResponseTypes() { - return responseTypes; - } - - public void setResponseTypes(String responseTypes) { - this.responseTypes = responseTypes; - } - - public String getClientName() { - return clientName; - } - - public void setClientName(String clientName) { - this.clientName = clientName; - } - - public String getClientUri() { - return clientUri; - } - - public void setClientUri(String clientUri) { - this.clientUri = clientUri; - } - - public String getLogoUri() { - return logoUri; - } - - public void setLogoUri(String logoUri) { - this.logoUri = logoUri; - } - - public String getScope() { - return scope; - } - - public void setScope(String scope) { - this.scope = scope; - } - - public String getContacts() { - return contacts; - } - - public void setContacts(String contacts) { - this.contacts = contacts; - } - - public String getTos_uri() { - return tos_uri; - } - - public void setTos_uri(String tos_uri) { - this.tos_uri = tos_uri; - } - - public String getPolicy_uri() { - return policy_uri; - } - - public void setPolicy_uri(String policy_uri) { - this.policy_uri = policy_uri; - } - - public String getJwks_uri() { - return jwks_uri; - } - - public void setJwks_uri(String jwks_uri) { - this.jwks_uri = jwks_uri; - } - - public String getJwks() { - return jwks; - } - - public void setJwks(String jwks) { - this.jwks = jwks; - } - - public String getSoftwareId() { - return softwareId; - } - - public void setSoftwareId(String softwareId) { - this.softwareId = softwareId; - } - - public String getSoftwareVersion() { - return softwareVersion; - } - - public void setSoftwareVersion(String softwareVersion) { - this.softwareVersion = softwareVersion; - } - -} 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 9245e588b9..02263317ca 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 Stian Thorgersen @@ -48,6 +47,9 @@ public class OIDCConfigurationRepresentation { @JsonProperty("response_modes_supported") private List responseModesSupported; + @JsonProperty("registration_endpoint") + private String registrationEndpoint; + protected Map otherClaims = new HashMap(); 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 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 0000000000..0c95a37ce1 --- /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 Stian Thorgersen + */ +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 0581388a1c..621d5fb91c 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 38d71f227c..126d00af49 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 Stian Thorgersen */ -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 0000000000..ed491decaf --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientregistration/ErrorCodes.java @@ -0,0 +1,12 @@ +package org.keycloak.services.clientregistration; + +/** + * @author Stian Thorgersen + */ +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 1e0784c288..a7f9f2c82f 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 Stian Thorgersen @@ -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 961f28de78..e60720bc87 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 Stian Thorgersen */ -public class OIDCClientRegistrationProvider implements ClientRegistrationProvider { - - private static final Logger logger = Logger.getLogger(OIDCClientRegistrationProvider.class); - - private KeycloakSession session; - private EventBuilder event; - private ClientRegistrationAuth auth; +public class OIDCClientRegistrationProvider extends AbstractClientRegistrationProvider { public OIDCClientRegistrationProvider(KeycloakSession session) { - this.session = session; + super(session); } -// @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"); -// } -// } + @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); + } - @Override - public void close() { + 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(); + } + + @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(); + } + + @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 6f112f8816..0144e790a8 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/clientregistration/oidc/OIDCClientResponseRepresentation.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientResponseRepresentation.java deleted file mode 100644 index cd4eea4949..0000000000 --- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientResponseRepresentation.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.keycloak.services.clientregistration.oidc; - -import org.codehaus.jackson.annotate.JsonProperty; -import org.keycloak.protocol.oidc.representations.OIDCClientRepresentation; - -/** - * @author Stian Thorgersen - */ -public class OIDCClientResponseRepresentation extends OIDCClientRepresentation { - - @JsonProperty("client_id") - private String clientId; - - @JsonProperty("client_secret") - private String clientSecret; - - @JsonProperty("client_id_issued_at") - private int clientIdIssuedAt; - - @JsonProperty("client_secret_expires_at") - private int clientSecretExpiresAt; - - @JsonProperty("registration_client_uri") - private String registrationClientUri; - - @JsonProperty("registration_access_token") - private String registrationAccessToken; - - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public String getClientSecret() { - return clientSecret; - } - - public void setClientSecret(String clientSecret) { - this.clientSecret = clientSecret; - } - - public int getClientIdIssuedAt() { - return clientIdIssuedAt; - } - - public void setClientIdIssuedAt(int clientIdIssuedAt) { - this.clientIdIssuedAt = clientIdIssuedAt; - } - - public int getClientSecretExpiresAt() { - return clientSecretExpiresAt; - } - - public void setClientSecretExpiresAt(int clientSecretExpiresAt) { - this.clientSecretExpiresAt = clientSecretExpiresAt; - } - - public String getRegistrationClientUri() { - return registrationClientUri; - } - - public void setRegistrationClientUri(String registrationClientUri) { - this.registrationClientUri = registrationClientUri; - } - - public String getRegistrationAccessToken() { - return registrationAccessToken; - } - - public void setRegistrationAccessToken(String registrationAccessToken) { - this.registrationAccessToken = registrationAccessToken; - } - -} 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 4b5e4f201b..cc1e49ae38 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-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 0000000000..9696274241 --- /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 Stian Thorgersen + */ +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 0000000000..1f5eab8a98 --- /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 Stian Thorgersen + */ +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 0000000000..b00ab251a4 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml @@ -0,0 +1,82 @@ + + + + + + +MIICYDCCAgqgAwIBAgICBoowDQYJKoZIhvcNAQEEBQAwgZIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI +EwpDYWxpZm9ybmlhMRQwEgYDVQQHEwtTYW50YSBDbGFyYTEeMBwGA1UEChMVU3VuIE1pY3Jvc3lz +dGVtcyBJbmMuMRowGAYDVQQLExFJZGVudGl0eSBTZXJ2aWNlczEcMBoGA1UEAxMTQ2VydGlmaWNh +dGUgTWFuYWdlcjAeFw0wNjExMDIxOTExMzRaFw0xMDA3MjkxOTExMzRaMDcxEjAQBgNVBAoTCXNp +cm9lLmNvbTEhMB8GA1UEAxMYbG9hZGJhbGFuY2VyLTkuc2lyb2UuY29tMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQCjOwa5qoaUuVnknqf5pdgAJSEoWlvx/jnUYbkSDpXLzraEiy2UhvwpoBgB +EeTSUaPPBvboCItchakPI6Z/aFdH3Wmjuij9XD8r1C+q//7sUO0IGn0ORycddHhoo0aSdnnxGf9V +tREaqKm9dJ7Yn7kQHjo2eryMgYxtr/Z5Il5F+wIDAQABo2AwXjARBglghkgBhvhCAQEEBAMCBkAw +DgYDVR0PAQH/BAQDAgTwMB8GA1UdIwQYMBaAFDugITflTCfsWyNLTXDl7cMDUKuuMBgGA1UdEQQR +MA+BDW1hbGxhQHN1bi5jb20wDQYJKoZIhvcNAQEEBQADQQB/6DOB6sRqCZu2OenM9eQR0gube85e +nTTxU4a7x1naFxzYXK1iQ1vMARKMjDb19QEJIEJKZlDK4uS7yMlf1nFS + + + + + + + + +MIICTDCCAfagAwIBAgICBo8wDQYJKoZIhvcNAQEEBQAwgZIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI +EwpDYWxpZm9ybmlhMRQwEgYDVQQHEwtTYW50YSBDbGFyYTEeMBwGA1UEChMVU3VuIE1pY3Jvc3lz +dGVtcyBJbmMuMRowGAYDVQQLExFJZGVudGl0eSBTZXJ2aWNlczEcMBoGA1UEAxMTQ2VydGlmaWNh +dGUgTWFuYWdlcjAeFw0wNjExMDcyMzU2MTdaFw0xMDA4MDMyMzU2MTdaMCMxITAfBgNVBAMTGGxv +YWRiYWxhbmNlci05LnNpcm9lLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAw574iRU6 +HsSO4LXW/OGTXyfsbGv6XRVOoy3v+J1pZ51KKejcDjDJXNkKGn3/356AwIaqbcymWd59T0zSqYfR +Hn+45uyjYxRBmVJseLpVnOXLub9jsjULfGx0yjH4w+KsZSZCXatoCHbj/RJtkzuZY6V9to/hkH3S +InQB4a3UAgMCAwEAAaNgMF4wEQYJYIZIAYb4QgEBBAQDAgZAMA4GA1UdDwEB/wQEAwIE8DAfBgNV +HSMEGDAWgBQ7oCE35Uwn7FsjS01w5e3DA1CrrjAYBgNVHREEETAPgQ1tYWxsYUBzdW4uY29tMA0G +CSqGSIb3DQEBBAUAA0EAMlbfBg/ff0Xkv4DOR5LEqmfTZKqgdlD81cXynfzlF7XfnOqI6hPIA90I +x5Ql0ejivIJAYcMGUyA+/YwJg2FGoA== + + + + + 128 + + + + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + + + 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 bb3351542e..42870a6642 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)); }