From 764c20d74821a9c04c078def652a19257229d86a Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Tue, 17 Nov 2015 20:35:09 +0100 Subject: [PATCH 1/4] KEYCLOAK-2085 Initial access tokens for client registration --- .../keycloak/client/registration/Auth.java | 6 + .../registration/ClientRegistration.java | 13 +- .../ClientRepresentationMixIn.java | 13 ++ .../META-INF/jpa-changelog-1.7.0.xml | 2 +- ...ClientInitialAccessCreatePresentation.java | 36 ++++ .../idm/ClientInitialAccessPresentation.java | 67 +++++++ .../java/org/keycloak/util/TokenUtil.java | 1 + .../resource/ClientInitialAccessResource.java | 31 +++ .../admin/client/resource/RealmResource.java | 5 +- .../models/ClientInitialAccessModel.java | 22 +++ .../java/org/keycloak/models/ClientModel.java | 4 +- .../keycloak/models/UserSessionProvider.java | 5 + .../models/entities/ClientEntity.java | 10 +- .../models/utils/KeycloakModelUtils.java | 12 -- .../models/utils/ModelToRepresentation.java | 1 - .../models/utils/RepresentationToModel.java | 3 - .../cache/infinispan/ClientAdapter.java | 10 +- .../models/cache/entities/CachedClient.java | 8 +- .../keycloak/models/jpa/ClientAdapter.java | 8 +- .../models/jpa/entities/ClientEntity.java | 12 +- .../keycloak/adapters/ClientAdapter.java | 8 +- .../ClientInitialAccessAdapter.java | 69 +++++++ .../InfinispanUserSessionProvider.java | 90 +++++++-- .../compat/ClientInitialAccessAdapter.java | 55 ++++++ .../compat/MemUserSessionProvider.java | 65 +++++-- .../compat/MemUserSessionProviderFactory.java | 8 +- .../entities/ClientInitialAccessEntity.java | 68 +++++++ .../entities/ClientInitialAccessEntity.java | 48 +++++ .../mapreduce/ClientInitialAccessMapper.java | 80 ++++++++ ...yDescriptorClientRegistrationProvider.java | 22 +-- ...nstallationClientRegistrationProvider.java | 5 +- .../clientregistration/ClientRegAuth.java | 134 ------------- .../ClientRegistrationAuth.java | 182 ++++++++++++++++++ .../ClientRegistrationProvider.java | 3 +- .../ClientRegistrationService.java | 2 +- .../ClientRegistrationTokenUtils.java | 99 ++++++++++ .../DefaultClientRegistrationProvider.java | 31 ++- .../oidc/OIDCClientRegistrationProvider.java | 25 +-- .../admin/ClientInitialAccessResource.java | 102 ++++++++++ .../resources/admin/ClientResource.java | 6 +- .../resources/admin/RealmAdminResource.java | 12 ++ .../AbstractClientRegistrationTest.java | 30 ++- .../client/ClientRegistrationTest.java | 16 -- .../client/InitialAccessTokenTest.java | 101 ++++++++++ .../client/RegistrationAccessTokenTest.java | 19 +- .../admin/ClientInitialAccessTest.java | 58 ++++++ 46 files changed, 1292 insertions(+), 315 deletions(-) create mode 100644 client-api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java create mode 100644 core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessCreatePresentation.java create mode 100644 core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessPresentation.java create mode 100644 integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientInitialAccessResource.java create mode 100755 model/api/src/main/java/org/keycloak/models/ClientInitialAccessModel.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientInitialAccessAdapter.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/ClientInitialAccessAdapter.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/entities/ClientInitialAccessEntity.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientInitialAccessEntity.java create mode 100644 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientInitialAccessMapper.java delete mode 100644 services/src/main/java/org/keycloak/services/clientregistration/ClientRegAuth.java create mode 100644 services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java create mode 100644 services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java create mode 100644 services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/InitialAccessTokenTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientInitialAccessTest.java diff --git a/client-api/src/main/java/org/keycloak/client/registration/Auth.java b/client-api/src/main/java/org/keycloak/client/registration/Auth.java index 5b0e85fa8d..31ad4e882f 100644 --- a/client-api/src/main/java/org/keycloak/client/registration/Auth.java +++ b/client-api/src/main/java/org/keycloak/client/registration/Auth.java @@ -3,6 +3,7 @@ package org.keycloak.client.registration; import org.apache.http.HttpHeaders; import org.apache.http.HttpRequest; import org.keycloak.common.util.Base64; +import org.keycloak.representations.idm.ClientInitialAccessPresentation; import org.keycloak.representations.idm.ClientRepresentation; /** @@ -16,6 +17,11 @@ public abstract class Auth { return new BearerTokenAuth(token); } + public static Auth token(ClientInitialAccessPresentation initialAccess) { + return new BearerTokenAuth(initialAccess.getToken()); + } + + public static Auth token(ClientRepresentation client) { return new BearerTokenAuth(client.getRegistrationAccessToken()); } diff --git a/client-api/src/main/java/org/keycloak/client/registration/ClientRegistration.java b/client-api/src/main/java/org/keycloak/client/registration/ClientRegistration.java index f932226440..3be763f9d9 100644 --- a/client-api/src/main/java/org/keycloak/client/registration/ClientRegistration.java +++ b/client-api/src/main/java/org/keycloak/client/registration/ClientRegistration.java @@ -2,6 +2,7 @@ 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.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.util.JsonSerialization; @@ -14,6 +15,11 @@ import java.io.InputStream; */ public class ClientRegistration { + public static final ObjectMapper outputMapper = new ObjectMapper(); + static { + outputMapper.getSerializationConfig().addMixInAnnotations(ClientRepresentation.class, ClientRepresentationMixIn.class); + } + private final String DEFAULT = "default"; private final String INSTALLATION = "install"; @@ -69,15 +75,16 @@ public class ClientRegistration { httpUtil.doDelete(DEFAULT, clientId); } - private String serialize(ClientRepresentation client) throws ClientRegistrationException { + public static String serialize(ClientRepresentation client) throws ClientRegistrationException { try { - return JsonSerialization.writeValueAsString(client); + + return outputMapper.writeValueAsString(client); } catch (IOException e) { throw new ClientRegistrationException("Failed to write json object", e); } } - private T deserialize(InputStream inputStream, Class clazz) throws ClientRegistrationException { + private static T deserialize(InputStream inputStream, Class clazz) throws ClientRegistrationException { try { return JsonSerialization.readValue(inputStream, clazz); } catch (IOException e) { diff --git a/client-api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java b/client-api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java new file mode 100644 index 0000000000..ba382f6717 --- /dev/null +++ b/client-api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java @@ -0,0 +1,13 @@ +package org.keycloak.client.registration; + +import org.codehaus.jackson.annotate.JsonIgnore; + +/** + * @author Stian Thorgersen + */ +abstract class ClientRepresentationMixIn { + + @JsonIgnore + String registrationAccessToken; + +} diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml index aed99fcbe1..c53eb4b44b 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml @@ -59,7 +59,7 @@ - + diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessCreatePresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessCreatePresentation.java new file mode 100644 index 0000000000..4c18b3d143 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessCreatePresentation.java @@ -0,0 +1,36 @@ +package org.keycloak.representations.idm; + +/** + * @author Stian Thorgersen + */ +public class ClientInitialAccessCreatePresentation { + + private Integer expiration; + + private Integer count; + + public ClientInitialAccessCreatePresentation() { + } + + public ClientInitialAccessCreatePresentation(Integer expiration, Integer count) { + this.expiration = expiration; + this.count = count; + } + + public Integer getExpiration() { + return expiration; + } + + public void setExpiration(Integer expiration) { + this.expiration = expiration; + } + + public Integer getCount() { + return count; + } + + public void setCount(Integer count) { + this.count = count; + } + +} diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessPresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessPresentation.java new file mode 100644 index 0000000000..d8021ad9f9 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessPresentation.java @@ -0,0 +1,67 @@ +package org.keycloak.representations.idm; + +/** + * @author Stian Thorgersen + */ +public class ClientInitialAccessPresentation { + + private String id; + + private String token; + + private Integer timestamp; + + private Integer expiration; + + private Integer count; + + private Integer remainingCount; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Integer getTimestamp() { + return timestamp; + } + + public void setTimestamp(Integer timestamp) { + this.timestamp = timestamp; + } + + public Integer getExpiration() { + return expiration; + } + + public void setExpiration(Integer expiration) { + this.expiration = expiration; + } + + public Integer getCount() { + return count; + } + + public void setCount(Integer count) { + this.count = count; + } + + public Integer getRemainingCount() { + return remainingCount; + } + + public void setRemainingCount(Integer remainingCount) { + this.remainingCount = remainingCount; + } +} diff --git a/core/src/main/java/org/keycloak/util/TokenUtil.java b/core/src/main/java/org/keycloak/util/TokenUtil.java index 0d103a6b9b..10b6c20ae6 100644 --- a/core/src/main/java/org/keycloak/util/TokenUtil.java +++ b/core/src/main/java/org/keycloak/util/TokenUtil.java @@ -19,6 +19,7 @@ public class TokenUtil { public static final String TOKEN_TYPE_OFFLINE = "Offline"; + public static boolean isOfflineTokenRequested(String scopeParam) { if (scopeParam == null) { return false; diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientInitialAccessResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientInitialAccessResource.java new file mode 100644 index 0000000000..8875a4ccec --- /dev/null +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientInitialAccessResource.java @@ -0,0 +1,31 @@ +package org.keycloak.admin.client.resource; + +import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; +import org.keycloak.representations.idm.ClientInitialAccessPresentation; + +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public interface ClientInitialAccessResource { + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + ClientInitialAccessPresentation create(ClientInitialAccessCreatePresentation rep); + + @GET + @Produces(MediaType.APPLICATION_JSON) + List list(); + + @DELETE + @Path("{id}") + void delete(final @PathParam("id") String id); + +} diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java index 87513ec2ab..82b023b73e 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java @@ -45,5 +45,8 @@ public interface RealmResource { @Path("client-session-stats") @GET List> getClientSessionStats(); - + + @Path("clients-initial-access") + ClientInitialAccessResource clientInitialAccess(); + } diff --git a/model/api/src/main/java/org/keycloak/models/ClientInitialAccessModel.java b/model/api/src/main/java/org/keycloak/models/ClientInitialAccessModel.java new file mode 100755 index 0000000000..7447319f97 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/ClientInitialAccessModel.java @@ -0,0 +1,22 @@ +package org.keycloak.models; + +/** + * @author Stian Thorgersen + */ +public interface ClientInitialAccessModel { + + String getId(); + + RealmModel getRealm(); + + int getTimestamp(); + + int getExpiration(); + + int getCount(); + + int getRemainingCount(); + + void decreaseRemainingCount(); + +} diff --git a/model/api/src/main/java/org/keycloak/models/ClientModel.java b/model/api/src/main/java/org/keycloak/models/ClientModel.java index c421aeab1e..051493e90f 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java @@ -90,8 +90,8 @@ public interface ClientModel extends RoleContainerModel { String getSecret(); public void setSecret(String secret); - String getRegistrationSecret(); - void setRegistrationSecret(String registrationSecret); + String getRegistrationToken(); + void setRegistrationToken(String registrationToken); boolean isFullScopeAllowed(); void setFullScopeAllowed(boolean value); diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java index 1a59f4ffef..0c1df9cc1f 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -63,6 +63,11 @@ public interface UserSessionProvider extends Provider { UserSessionModel importUserSession(UserSessionModel persistentUserSession, boolean offline); ClientSessionModel importClientSession(ClientSessionModel persistentClientSession, boolean offline); + ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count); + ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id); + void removeClientInitialAccessModel(RealmModel realm, String id); + List listClientInitialAccess(RealmModel realm); + void close(); } diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java index f15614fe4b..d04699f16c 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java @@ -17,7 +17,7 @@ public class ClientEntity extends AbstractIdentifiableEntity { private boolean enabled; private String clientAuthenticatorType; private String secret; - private String registrationSecret; + private String registrationToken; private String protocol; private int notBefore; private boolean publicClient; @@ -91,12 +91,12 @@ public class ClientEntity extends AbstractIdentifiableEntity { this.secret = secret; } - public String getRegistrationSecret() { - return registrationSecret; + public String getRegistrationToken() { + return registrationToken; } - public void setRegistrationSecret(String registrationSecret) { - this.registrationSecret = registrationSecret; + public void setRegistrationToken(String registrationToken) { + this.registrationToken = registrationToken; } public int getNotBefore() { diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index c35c58ecc8..02caa74c95 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -43,8 +43,6 @@ import java.util.UUID; */ public final class KeycloakModelUtils { - private static final int RANDOM_PASSWORD_BYTES = 32; - private KeycloakModelUtils() { } @@ -182,16 +180,6 @@ public final class KeycloakModelUtils { return secret; } - public static void generateRegistrationAccessToken(ClientModel client) { - client.setRegistrationSecret(generatePassword()); - } - - public static String generatePassword() { - byte[] buf = new byte[RANDOM_PASSWORD_BYTES]; - new SecureRandom().nextBytes(buf); - return Base64Url.encode(buf); - } - public static String getDefaultClientAuthenticatorType() { return "client-secret"; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 0df3516980..30736167eb 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -387,7 +387,6 @@ public class ModelToRepresentation { rep.setNotBefore(clientModel.getNotBefore()); rep.setNodeReRegistrationTimeout(clientModel.getNodeReRegistrationTimeout()); rep.setClientAuthenticatorType(clientModel.getClientAuthenticatorType()); - rep.setRegistrationAccessToken(clientModel.getRegistrationSecret()); Set redirectUris = clientModel.getRedirectUris(); if (redirectUris != null) { diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index a46ee57503..a31d35592b 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -737,8 +737,6 @@ public class RepresentationToModel { KeycloakModelUtils.generateSecret(client); } - client.setRegistrationSecret(resourceRep.getRegistrationAccessToken()); - if (resourceRep.getAttributes() != null) { for (Map.Entry entry : resourceRep.getAttributes().entrySet()) { client.setAttribute(entry.getKey(), entry.getValue()); @@ -815,7 +813,6 @@ public class RepresentationToModel { if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired()); if (rep.getNodeReRegistrationTimeout() != null) resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout()); if (rep.getClientAuthenticatorType() != null) resource.setClientAuthenticatorType(rep.getClientAuthenticatorType()); - if (rep.getRegistrationAccessToken() != null) resource.setRegistrationSecret(rep.getRegistrationAccessToken()); resource.updateClient(); if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol()); diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java index 7a873735bc..e03452ad2b 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java @@ -120,14 +120,14 @@ public class ClientAdapter implements ClientModel { getDelegateForUpdate(); updated.setSecret(secret); } - public String getRegistrationSecret() { - if (updated != null) return updated.getRegistrationSecret(); - return cached.getRegistrationSecret(); + public String getRegistrationToken() { + if (updated != null) return updated.getRegistrationToken(); + return cached.getRegistrationToken(); } - public void setRegistrationSecret(String registrationsecret) { + public void setRegistrationToken(String registrationToken) { getDelegateForUpdate(); - updated.setRegistrationSecret(registrationsecret); + updated.setRegistrationToken(registrationToken); } public boolean isPublicClient() { diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java index 1c04b9df10..7c7b97d796 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java @@ -31,7 +31,7 @@ public class CachedClient implements Serializable { private boolean enabled; private String clientAuthenticatorType; private String secret; - private String registrationSecret; + private String registrationToken; private String protocol; private Map attributes = new HashMap(); private boolean publicClient; @@ -58,7 +58,7 @@ public class CachedClient implements Serializable { id = model.getId(); clientAuthenticatorType = model.getClientAuthenticatorType(); secret = model.getSecret(); - registrationSecret = model.getRegistrationSecret(); + registrationToken = model.getRegistrationToken(); clientId = model.getClientId(); name = model.getName(); description = model.getDescription(); @@ -131,8 +131,8 @@ public class CachedClient implements Serializable { return secret; } - public String getRegistrationSecret() { - return registrationSecret; + public String getRegistrationToken() { + return registrationToken; } public boolean isPublicClient() { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java index 5ea0b11619..a7dc0f46d3 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java @@ -178,13 +178,13 @@ public class ClientAdapter implements ClientModel { } @Override - public String getRegistrationSecret() { - return entity.getRegistrationSecret(); + public String getRegistrationToken() { + return entity.getRegistrationToken(); } @Override - public void setRegistrationSecret(String registrationSecret) { - entity.setRegistrationSecret(registrationSecret); + public void setRegistrationToken(String registrationToken) { + entity.setRegistrationToken(registrationToken); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java index 881b12967f..6218e26d4b 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java @@ -42,8 +42,8 @@ public class ClientEntity { private boolean enabled; @Column(name="SECRET") private String secret; - @Column(name="REGISTRATION_SECRET") - private String registrationSecret; + @Column(name="REGISTRATION_TOKEN") + private String registrationToken; @Column(name="CLIENT_AUTHENTICATOR_TYPE") private String clientAuthenticatorType; @Column(name="NOT_BEFORE") @@ -203,12 +203,12 @@ public class ClientEntity { this.secret = secret; } - public String getRegistrationSecret() { - return registrationSecret; + public String getRegistrationToken() { + return registrationToken; } - public void setRegistrationSecret(String registrationSecret) { - this.registrationSecret = registrationSecret; + public void setRegistrationToken(String registrationToken) { + this.registrationToken = registrationToken; } public int getNotBefore() { diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java index cbacd096d2..8eb562f96a 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java @@ -178,13 +178,13 @@ public class ClientAdapter extends AbstractMongoAdapter imple } @Override - public String getRegistrationSecret() { - return getMongoEntity().getRegistrationSecret(); + public String getRegistrationToken() { + return getMongoEntity().getRegistrationToken(); } @Override - public void setRegistrationSecret(String registrationSecretsecret) { - getMongoEntity().setRegistrationSecret(registrationSecretsecret); + public void setRegistrationToken(String registrationToken) { + getMongoEntity().setRegistrationToken(registrationToken); updateMongoEntity(); } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientInitialAccessAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientInitialAccessAdapter.java new file mode 100644 index 0000000000..7d753359a2 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientInitialAccessAdapter.java @@ -0,0 +1,69 @@ +package org.keycloak.models.sessions.infinispan; + +import org.infinispan.Cache; +import org.keycloak.models.ClientInitialAccessModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +/** + * @author Stian Thorgersen + */ +public class ClientInitialAccessAdapter implements ClientInitialAccessModel { + + private final KeycloakSession session; + private final InfinispanUserSessionProvider provider; + private final Cache cache; + private final RealmModel realm; + private final ClientInitialAccessEntity entity; + + public ClientInitialAccessAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache cache, RealmModel realm, ClientInitialAccessEntity entity) { + this.session = session; + this.provider = provider; + this.cache = cache; + this.realm = realm; + this.entity = entity; + } + + @Override + public String getId() { + return entity.getId(); + } + + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public int getTimestamp() { + return entity.getTimestamp(); + } + + @Override + public int getExpiration() { + return entity.getExpiration(); + } + + @Override + public int getCount() { + return entity.getCount(); + } + + @Override + public int getRemainingCount() { + return entity.getRemainingCount(); + } + + @Override + public void decreaseRemainingCount() { + entity.setRemainingCount(entity.getRemainingCount() - 1); + update(); + } + + void update() { + provider.getTx().replace(cache, entity.getId(), entity); + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index 9ba15878a4..c819259849 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -3,28 +3,10 @@ package org.keycloak.models.sessions.infinispan; import org.infinispan.Cache; import org.infinispan.distexec.mapreduce.MapReduceTask; import org.jboss.logging.Logger; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakTransaction; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; -import org.keycloak.models.UserSessionProvider; -import org.keycloak.models.UsernameLoginFailureModel; +import org.keycloak.models.*; import org.keycloak.models.session.UserSessionPersisterProvider; -import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; -import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; -import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; -import org.keycloak.models.sessions.infinispan.entities.SessionEntity; -import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; -import org.keycloak.models.sessions.infinispan.mapreduce.ClientSessionMapper; -import org.keycloak.models.sessions.infinispan.mapreduce.FirstResultReducer; -import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer; -import org.keycloak.models.sessions.infinispan.mapreduce.SessionMapper; -import org.keycloak.models.sessions.infinispan.mapreduce.UserLoginFailureMapper; -import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionMapper; -import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionNoteMapper; +import org.keycloak.models.sessions.infinispan.entities.*; +import org.keycloak.models.sessions.infinispan.mapreduce.*; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RealmInfoUtil; import org.keycloak.common.util.Time; @@ -355,6 +337,15 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { persister.removeClientSession(clientSessionId, true); } + // Remove expired client initial access + map = new MapReduceTask(sessionCache) + .mappedWith(ClientInitialAccessMapper.create(realm.getId()).time(Time.currentTime()).remainingCount(0).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String id : map.keySet()) { + tx.remove(sessionCache, id); + } } @Override @@ -538,11 +529,24 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return models; } + List wrapClientInitialAccess(RealmModel realm, Collection entities) { + List models = new LinkedList<>(); + for (ClientInitialAccessEntity e : entities) { + models.add(wrap(realm, e)); + } + return models; + } + ClientSessionAdapter wrap(RealmModel realm, ClientSessionEntity entity, boolean offline) { Cache cache = getCache(offline); return entity != null ? new ClientSessionAdapter(session, this, cache, realm, entity, offline) : null; } + ClientInitialAccessAdapter wrap(RealmModel realm, ClientInitialAccessEntity entity) { + Cache cache = getCache(false); + return entity != null ? new ClientInitialAccessAdapter(session, this, cache, realm, entity) : null; + } + UsernameLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) { return entity != null ? new UsernameLoginFailureAdapter(this, loginFailureCache, key, entity) : null; @@ -680,6 +684,50 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return wrap(clientSession.getRealm(), entity, offline); } + @Override + public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) { + String id = KeycloakModelUtils.generateId(); + + ClientInitialAccessEntity entity = new ClientInitialAccessEntity(); + entity.setId(id); + entity.setRealm(realm.getId()); + entity.setTimestamp(Time.currentTime()); + entity.setExpiration(expiration); + entity.setCount(count); + entity.setRemainingCount(count); + + tx.put(sessionCache, id, entity); + + return wrap(realm, entity); + } + + @Override + public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) { + Cache cache = getCache(false); + ClientInitialAccessEntity entity = (ClientInitialAccessEntity) cache.get(id); + + // If created in this transaction + if (entity == null) { + entity = (ClientInitialAccessEntity) tx.get(cache, id); + } + + return wrap(realm, entity); + } + + @Override + public void removeClientInitialAccessModel(RealmModel realm, String id) { + tx.remove(getCache(false), id); + } + + @Override + public List listClientInitialAccess(RealmModel realm) { + Map entities = new MapReduceTask(sessionCache) + .mappedWith(ClientInitialAccessMapper.create(realm.getId())) + .reducedWith(new FirstResultReducer()) + .execute(); + return wrapClientInitialAccess(realm, entities.values()); + } + class InfinispanKeycloakTransaction implements KeycloakTransaction { private boolean active; diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/ClientInitialAccessAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/ClientInitialAccessAdapter.java new file mode 100644 index 0000000000..3398efff4a --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/ClientInitialAccessAdapter.java @@ -0,0 +1,55 @@ +package org.keycloak.models.sessions.infinispan.compat; + +import org.keycloak.models.ClientInitialAccessModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.sessions.infinispan.compat.entities.ClientInitialAccessEntity; + +/** + * @author Stian Thorgersen + */ +public class ClientInitialAccessAdapter implements ClientInitialAccessModel { + + private final RealmModel realm; + private final ClientInitialAccessEntity entity; + + public ClientInitialAccessAdapter(RealmModel realm, ClientInitialAccessEntity entity) { + this.realm = realm; + this.entity = entity; + } + + @Override + public String getId() { + return entity.getId(); + } + + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public int getTimestamp() { + return entity.getTimestamp(); + } + + @Override + public int getExpiration() { + return entity.getExpires(); + } + + @Override + public int getCount() { + return entity.getCount(); + } + + @Override + public int getRemainingCount() { + return entity.getRemainingCount(); + } + + @Override + public void decreaseRemainingCount() { + entity.setRemainingCount(entity.getRemainingCount() - 1); + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java index f45edf162c..db20ef8940 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java @@ -1,19 +1,8 @@ package org.keycloak.models.sessions.infinispan.compat; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ModelDuplicateException; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; -import org.keycloak.models.UserSessionProvider; -import org.keycloak.models.UsernameLoginFailureModel; +import org.keycloak.models.*; import org.keycloak.models.session.UserSessionPersisterProvider; -import org.keycloak.models.sessions.infinispan.compat.entities.ClientSessionEntity; -import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity; -import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureEntity; -import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureKey; +import org.keycloak.models.sessions.infinispan.compat.entities.*; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RealmInfoUtil; import org.keycloak.common.util.Time; @@ -41,11 +30,12 @@ public class MemUserSessionProvider implements UserSessionProvider { private final ConcurrentHashMap offlineUserSessions; private final ConcurrentHashMap offlineClientSessions; + private ConcurrentHashMap clientInitialAccess; public MemUserSessionProvider(KeycloakSession session, ConcurrentHashMap userSessions, ConcurrentHashMap userSessionsByBrokerSessionId, ConcurrentHashMap> userSessionsByBrokerUserId, ConcurrentHashMap clientSessions, ConcurrentHashMap loginFailures, - ConcurrentHashMap offlineUserSessions, ConcurrentHashMap offlineClientSessions) { + ConcurrentHashMap offlineUserSessions, ConcurrentHashMap offlineClientSessions, ConcurrentHashMap clientInitialAccess) { this.session = session; this.userSessions = userSessions; this.clientSessions = clientSessions; @@ -54,6 +44,7 @@ public class MemUserSessionProvider implements UserSessionProvider { this.userSessionsByBrokerUserId = userSessionsByBrokerUserId; this.offlineUserSessions = offlineUserSessions; this.offlineClientSessions = offlineClientSessions; + this.clientInitialAccess = clientInitialAccess; } @Override @@ -341,6 +332,15 @@ public class MemUserSessionProvider implements UserSessionProvider { persister.removeClientSession(s.getId(), true); } } + + // Remove expired initial access + Iterator iaitr = clientInitialAccess.values().iterator(); + while (iaitr.hasNext()) { + ClientInitialAccessEntity e = iaitr.next(); + if (e.getRealmId().equals(realm.getId()) && (e.getExpires() < Time.currentTime())) { + iaitr.remove(); + } + } } @Override @@ -574,6 +574,43 @@ public class MemUserSessionProvider implements UserSessionProvider { return getUserSessions(realm, client, first, max, true); } + @Override + public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) { + String id = KeycloakModelUtils.generateId(); + + ClientInitialAccessEntity entity = new ClientInitialAccessEntity(); + entity.setId(id); + entity.setRealmId(realm.getId()); + entity.setTimestamp(Time.currentTime()); + entity.setExpiration(expiration); + entity.setCount(count); + entity.setRemainingCount(count); + + clientInitialAccess.put(id, entity); + + return new ClientInitialAccessAdapter(realm, entity); + } + + @Override + public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) { + ClientInitialAccessEntity entity = clientInitialAccess.get(id); + return entity != null ? new ClientInitialAccessAdapter(realm, entity) : null; + } + + @Override + public void removeClientInitialAccessModel(RealmModel realm, String id) { + clientInitialAccess.remove(id); + } + + @Override + public List listClientInitialAccess(RealmModel realm) { + List models = new LinkedList<>(); + for (ClientInitialAccessEntity e : clientInitialAccess.values()) { + models.add(new ClientInitialAccessAdapter(realm, e)); + } + return models; + } + @Override public void close() { } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java index 187a33ffbe..451fcb0ecf 100644 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java @@ -1,14 +1,12 @@ package org.keycloak.models.sessions.infinispan.compat; -import org.keycloak.Config; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.UserSessionProvider; -import org.keycloak.models.UserSessionProviderFactory; import org.keycloak.models.sessions.infinispan.compat.entities.ClientSessionEntity; import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureEntity; import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureKey; +import org.keycloak.models.sessions.infinispan.compat.entities.ClientInitialAccessEntity; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -29,9 +27,11 @@ public class MemUserSessionProviderFactory { private ConcurrentHashMap offlineUserSessions = new ConcurrentHashMap(); private ConcurrentHashMap offlineClientSessions = new ConcurrentHashMap(); + private ConcurrentHashMap clientInitialAccess = new ConcurrentHashMap<>(); + public UserSessionProvider create(KeycloakSession session) { return new MemUserSessionProvider(session, userSessions, userSessionsByBrokerSessionId, userSessionsByBrokerUserId, clientSessions, loginFailures, - offlineUserSessions, offlineClientSessions); + offlineUserSessions, offlineClientSessions, clientInitialAccess); } public void close() { diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/entities/ClientInitialAccessEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/entities/ClientInitialAccessEntity.java new file mode 100644 index 0000000000..ed0aeac494 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/entities/ClientInitialAccessEntity.java @@ -0,0 +1,68 @@ +package org.keycloak.models.sessions.infinispan.compat.entities; + +/** + * @author Stian Thorgersen + */ +public class ClientInitialAccessEntity { + + private String id; + + private String realmId; + + private int timestamp; + + private int expires; + + private int count; + + private int remainingCount; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + + public int getExpires() { + return expires; + } + + public void setExpiration(int expires) { + this.expires = expires; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public int getRemainingCount() { + return remainingCount; + } + + public void setRemainingCount(int remainingCount) { + this.remainingCount = remainingCount; + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientInitialAccessEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientInitialAccessEntity.java new file mode 100644 index 0000000000..05daf1e291 --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientInitialAccessEntity.java @@ -0,0 +1,48 @@ +package org.keycloak.models.sessions.infinispan.entities; + +/** + * @author Stian Thorgersen + */ +public class ClientInitialAccessEntity extends SessionEntity { + + private int timestamp; + + private int expires; + + private int count; + + private int remainingCount; + + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + + public int getExpiration() { + return expires; + } + + public void setExpiration(int expires) { + this.expires = expires; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public int getRemainingCount() { + return remainingCount; + } + + public void setRemainingCount(int remainingCount) { + this.remainingCount = remainingCount; + } + +} diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientInitialAccessMapper.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientInitialAccessMapper.java new file mode 100644 index 0000000000..87c9b3c24a --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientInitialAccessMapper.java @@ -0,0 +1,80 @@ +package org.keycloak.models.sessions.infinispan.mapreduce; + +import org.infinispan.distexec.mapreduce.Collector; +import org.infinispan.distexec.mapreduce.Mapper; +import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +import java.io.Serializable; + +/** + * @author Stian Thorgersen + */ +public class ClientInitialAccessMapper implements Mapper, Serializable { + + public ClientInitialAccessMapper(String realm) { + this.realm = realm; + } + + private enum EmitValue { + KEY, ENTITY + } + + private String realm; + + private EmitValue emit = EmitValue.ENTITY; + + private Integer time; + private Integer remainingCount; + + public static ClientInitialAccessMapper create(String realm) { + return new ClientInitialAccessMapper(realm); + } + + public ClientInitialAccessMapper emitKey() { + emit = EmitValue.KEY; + return this; + } + + public ClientInitialAccessMapper time(int time) { + this.time = time; + return this; + } + + + public ClientInitialAccessMapper remainingCount(int remainingCount) { + this.remainingCount = remainingCount; + return this; + } + + @Override + public void map(String key, SessionEntity e, Collector collector) { + if (!realm.equals(e.getRealm())) { + return; + } + + if (!(e instanceof ClientInitialAccessEntity)) { + return; + } + + ClientInitialAccessEntity entity = (ClientInitialAccessEntity) e; + + if (time != null && entity.getExpiration() > 0 && (entity.getTimestamp() + entity.getExpiration()) < time) { + return; + } + + if (remainingCount != null && entity.getRemainingCount() == remainingCount) { + return; + } + + switch (emit) { + case KEY: + collector.emit(key, key); + break; + case ENTITY: + collector.emit(key, entity); + break; + } + } + +} 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 298623d582..81c2df4d9e 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 @@ -2,26 +2,10 @@ package org.keycloak.protocol.saml.clientregistration; import org.jboss.logging.Logger; import org.keycloak.events.EventBuilder; -import org.keycloak.events.EventType; -import org.keycloak.exportimport.ClientDescriptionConverter; -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.protocol.saml.EntityDescriptorDescriptionConverter; -import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.services.ErrorResponse; -import org.keycloak.services.clientregistration.ClientRegAuth; +import org.keycloak.services.clientregistration.ClientRegistrationAuth; import org.keycloak.services.clientregistration.ClientRegistrationProvider; -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 */ @@ -31,7 +15,7 @@ public class EntityDescriptorClientRegistrationProvider implements ClientRegistr private KeycloakSession session; private EventBuilder event; - private ClientRegAuth auth; + private ClientRegistrationAuth auth; public EntityDescriptorClientRegistrationProvider(KeycloakSession session) { this.session = session; @@ -67,7 +51,7 @@ public class EntityDescriptorClientRegistrationProvider implements ClientRegistr } @Override - public void setAuth(ClientRegAuth auth) { + public void setAuth(ClientRegistrationAuth auth) { this.auth = auth; } diff --git a/services/src/main/java/org/keycloak/services/clientregistration/AdapterInstallationClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/AdapterInstallationClientRegistrationProvider.java index feffc5f495..c667b5ea65 100644 --- a/services/src/main/java/org/keycloak/services/clientregistration/AdapterInstallationClientRegistrationProvider.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/AdapterInstallationClientRegistrationProvider.java @@ -1,6 +1,5 @@ package org.keycloak.services.clientregistration; -import org.jboss.resteasy.spi.UnauthorizedException; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; @@ -23,7 +22,7 @@ public class AdapterInstallationClientRegistrationProvider implements ClientRegi private KeycloakSession session; private EventBuilder event; - private ClientRegAuth auth; + private ClientRegistrationAuth auth; public AdapterInstallationClientRegistrationProvider(KeycloakSession session) { this.session = session; @@ -51,7 +50,7 @@ public class AdapterInstallationClientRegistrationProvider implements ClientRegi } @Override - public void setAuth(ClientRegAuth auth) { + public void setAuth(ClientRegistrationAuth auth) { this.auth = auth; } diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegAuth.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegAuth.java deleted file mode 100644 index cf13235cfc..0000000000 --- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegAuth.java +++ /dev/null @@ -1,134 +0,0 @@ -package org.keycloak.services.clientregistration; - -import org.jboss.resteasy.spi.UnauthorizedException; -import org.keycloak.events.Errors; -import org.keycloak.events.EventBuilder; -import org.keycloak.models.*; -import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; -import org.keycloak.representations.AccessToken; -import org.keycloak.services.ForbiddenException; -import org.keycloak.services.managers.AppAuthManager; -import org.keycloak.services.managers.AuthenticationManager; - -import javax.ws.rs.core.HttpHeaders; - -/** - * @author Stian Thorgersen - */ -public class ClientRegAuth { - - private KeycloakSession session; - private EventBuilder event; - - private String token; - private AccessToken.Access bearerRealmAccess; - - private boolean authenticated = false; - private boolean registrationAccessToken = false; - - public ClientRegAuth(KeycloakSession session, EventBuilder event) { - this.session = session; - this.event = event; - - init(); - } - - private void init() { - RealmModel realm = session.getContext().getRealm(); - - String authorizationHeader = session.getContext().getRequestHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); - if (authorizationHeader == null) { - return; - } - - String[] split = authorizationHeader.split(" "); - if (!split[0].equalsIgnoreCase("bearer")) { - return; - } - - if (split[1].indexOf('.') == -1) { - token = split[1]; - authenticated = true; - registrationAccessToken = true; - } else { - AuthenticationManager.AuthResult authResult = new AppAuthManager().authenticateBearerToken(session, realm); - bearerRealmAccess = authResult.getToken().getResourceAccess(Constants.REALM_MANAGEMENT_CLIENT_ID); - authenticated = true; - } - } - - public boolean isAuthenticated() { - return authenticated; - } - - public boolean isRegistrationAccessToken() { - return registrationAccessToken; - } - - public void requireCreate() { - if (!authenticated) { - event.error(Errors.NOT_ALLOWED); - throw new UnauthorizedException(); - } - - if (bearerRealmAccess != null) { - if (bearerRealmAccess.isUserInRole(AdminRoles.MANAGE_CLIENTS) || bearerRealmAccess.isUserInRole(AdminRoles.CREATE_CLIENT)) { - return; - } - } - - event.error(Errors.NOT_ALLOWED); - throw new ForbiddenException(); - } - - public void requireView(ClientModel client) { - if (!authenticated) { - event.error(Errors.NOT_ALLOWED); - throw new UnauthorizedException(); - } - - if (client == null) { - event.error(Errors.NOT_ALLOWED); - throw new ForbiddenException(); - } - - if (bearerRealmAccess != null) { - if (bearerRealmAccess.isUserInRole(AdminRoles.MANAGE_CLIENTS) || bearerRealmAccess.isUserInRole(AdminRoles.VIEW_CLIENTS)) { - return; - } - } else if (token != null) { - if (client.getRegistrationSecret() != null && client.getRegistrationSecret().equals(token)) { - return; - } - } - - event.error(Errors.NOT_ALLOWED); - throw new ForbiddenException(); - } - - public void requireUpdate(ClientModel client) { - if (!authenticated) { - event.error(Errors.NOT_ALLOWED); - throw new UnauthorizedException(); - } - - if (client == null) { - event.error(Errors.NOT_ALLOWED); - throw new ForbiddenException(); - } - - if (bearerRealmAccess != null) { - if (bearerRealmAccess.isUserInRole(AdminRoles.MANAGE_CLIENTS) || bearerRealmAccess.isUserInRole(AdminRoles.VIEW_CLIENTS)) { - return; - } - } else if (token != null) { - if (client.getRegistrationSecret() != null && client.getRegistrationSecret().equals(token)) { - return; - } - } - - event.error(Errors.NOT_ALLOWED); - throw new ForbiddenException(); - } - -} diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java new file mode 100644 index 0000000000..e5e71c5891 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java @@ -0,0 +1,182 @@ +package org.keycloak.services.clientregistration; + +import org.jboss.resteasy.spi.UnauthorizedException; +import org.keycloak.common.util.Time; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.models.*; +import org.keycloak.representations.JsonWebToken; +import org.keycloak.services.ForbiddenException; +import org.keycloak.util.TokenUtil; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.UriInfo; +import java.util.List; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class ClientRegistrationAuth { + + private KeycloakSession session; + private EventBuilder event; + + private JsonWebToken jwt; + private ClientInitialAccessModel initialAccessModel; + + public ClientRegistrationAuth(KeycloakSession session, EventBuilder event) { + this.session = session; + this.event = event; + + init(); + } + + private void init() { + RealmModel realm = session.getContext().getRealm(); + UriInfo uri = session.getContext().getUri(); + + String authorizationHeader = session.getContext().getRequestHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); + if (authorizationHeader == null) { + return; + } + + String[] split = authorizationHeader.split(" "); + if (!split[0].equalsIgnoreCase("bearer")) { + return; + } + + jwt = ClientRegistrationTokenUtils.parseToken(realm, uri, split[1]); + + if (isInitialAccessToken()) { + initialAccessModel = session.sessions().getClientInitialAccessModel(session.getContext().getRealm(), jwt.getId()); + if (initialAccessModel == null) { + throw new ForbiddenException(); + } + } + } + + public boolean isAuthenticated() { + return jwt != null; + } + + public boolean isBearerToken() { + return TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType()); + } + + public boolean isInitialAccessToken() { + return ClientRegistrationTokenUtils.TYPE_INITIAL_ACCESS_TOKEN.equals(jwt.getType()); + } + + public boolean isRegistrationAccessToken() { + return ClientRegistrationTokenUtils.TYPE_REGISTRATION_ACCESS_TOKEN.equals(jwt.getType()); + } + + public void requireCreate() { + if (!isAuthenticated()) { + event.error(Errors.NOT_ALLOWED); + throw new UnauthorizedException(); + } + + if (isBearerToken()) { + if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.CREATE_CLIENT)) { + return; + } + } else if (isInitialAccessToken()) { + if (initialAccessModel.getRemainingCount() > 0) { + if (initialAccessModel.getExpiration() == 0 || (initialAccessModel.getTimestamp() + initialAccessModel.getExpiration()) > Time.currentTime()) { + return; + } + } + } + + event.error(Errors.NOT_ALLOWED); + throw new ForbiddenException(); + } + + public void requireView(ClientModel client) { + if (!isAuthenticated()) { + event.error(Errors.NOT_ALLOWED); + throw new UnauthorizedException(); + } + + if (client == null) { + event.error(Errors.NOT_ALLOWED); + throw new ForbiddenException(); + } + + if (isBearerToken()) { + if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.VIEW_CLIENTS)) { + return; + } + } else if (isRegistrationAccessToken()) { + if (client.getRegistrationToken() != null && client.getRegistrationToken().equals(jwt.getId())) { + return; + } + } + + event.error(Errors.NOT_ALLOWED); + throw new ForbiddenException(); + } + + public void requireUpdate(ClientModel client) { + if (!isAuthenticated()) { + event.error(Errors.NOT_ALLOWED); + throw new UnauthorizedException(); + } + + if (client == null) { + event.error(Errors.NOT_ALLOWED); + throw new ForbiddenException(); + } + + if (isBearerToken()) { + if (hasRole(AdminRoles.MANAGE_CLIENTS)) { + return; + } + } else if (isRegistrationAccessToken()) { + if (client.getRegistrationToken() != null && client.getRegistrationToken().equals(jwt.getId())) { + return; + } + } + + event.error(Errors.NOT_ALLOWED); + throw new ForbiddenException(); + } + + public ClientInitialAccessModel getInitialAccessModel() { + return initialAccessModel; + } + + private boolean hasRole(String... role) { + try { + Map otherClaims = jwt.getOtherClaims(); + if (otherClaims != null) { + Map>> resourceAccess = (Map>>) jwt.getOtherClaims().get("resource_access"); + if (resourceAccess == null) { + return false; + } + + Map> realmManagement = resourceAccess.get(Constants.REALM_MANAGEMENT_CLIENT_ID); + if (realmManagement == null) { + return false; + } + + List resources = realmManagement.get("roles"); + if (resources == null) { + return false; + } + + for (String r : role) { + if (resources.contains(r)) { + return true; + } + } + } + return false; + } catch (Throwable t) { + return false; + } + } + +} diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationProvider.java index 0c6bc4e1b7..98bd6f904f 100644 --- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationProvider.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationProvider.java @@ -1,7 +1,6 @@ package org.keycloak.services.clientregistration; import org.keycloak.events.EventBuilder; -import org.keycloak.models.RealmModel; import org.keycloak.provider.Provider; /** @@ -9,7 +8,7 @@ import org.keycloak.provider.Provider; */ public interface ClientRegistrationProvider extends Provider { - void setAuth(ClientRegAuth auth); + void setAuth(ClientRegistrationAuth auth); void setEvent(EventBuilder event); 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 2aed3f1938..0581388a1c 100644 --- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationService.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationService.java @@ -35,7 +35,7 @@ public class ClientRegistrationService { } provider.setEvent(event); - provider.setAuth(new ClientRegAuth(session, event)); + provider.setAuth(new ClientRegistrationAuth(session, event)); return provider; } diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java new file mode 100644 index 0000000000..7cd334236e --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java @@ -0,0 +1,99 @@ +package org.keycloak.services.clientregistration; + +import org.keycloak.common.util.Time; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.models.ClientInitialAccessModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.representations.JsonWebToken; +import org.keycloak.services.ForbiddenException; +import org.keycloak.services.Urls; +import org.keycloak.util.TokenUtil; + +import javax.ws.rs.core.UriInfo; +import java.io.IOException; + +/** + * @author Stian Thorgersen + */ +public class ClientRegistrationTokenUtils { + + public static final String TYPE_INITIAL_ACCESS_TOKEN = "InitialAccessToken"; + public static final String TYPE_REGISTRATION_ACCESS_TOKEN = "RegistrationAccessToken"; + + public static String updateRegistrationAccessToken(KeycloakSession session, ClientModel client) { + return updateRegistrationAccessToken(session.getContext().getRealm(), session.getContext().getUri(), client); + } + + public static String updateRegistrationAccessToken(RealmModel realm, UriInfo uri, ClientModel client) { + String id = KeycloakModelUtils.generateId(); + client.setRegistrationToken(id); + String token = createToken(realm, uri, id, TYPE_REGISTRATION_ACCESS_TOKEN, 0); + return token; + } + + public static String createInitialAccessToken(RealmModel realm, UriInfo uri, ClientInitialAccessModel model) { + return createToken(realm, uri, model.getId(), TYPE_INITIAL_ACCESS_TOKEN, model.getTimestamp() + model.getExpiration()); + } + + public static JsonWebToken parseToken(RealmModel realm, UriInfo uri, String token) { + JWSInput input; + try { + input = new JWSInput(token); + } catch (Exception e) { + throw new ForbiddenException(e); + } + + if (!RSAProvider.verify(input, realm.getPublicKey())) { + throw new ForbiddenException("Invalid signature"); + } + + JsonWebToken jwt; + try { + jwt = input.readJsonContent(JsonWebToken.class); + } catch (IOException e) { + throw new ForbiddenException(e); + } + + if (!getIssuer(realm, uri).equals(jwt.getIssuer())) { + throw new ForbiddenException("Issuer doesn't match"); + } + + if (!jwt.isActive()) { + throw new ForbiddenException("Expired token"); + } + + if (!(TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType()) || + TYPE_INITIAL_ACCESS_TOKEN.equals(jwt.getType()) || + TYPE_REGISTRATION_ACCESS_TOKEN.equals(jwt.getType()))) { + throw new ForbiddenException("Invalid token type"); + } + + return jwt; + } + + private static String createToken(RealmModel realm, UriInfo uri, String id, String type, int expiration) { + JsonWebToken jwt = new JsonWebToken(); + + String issuer = getIssuer(realm, uri); + + jwt.type(type); + jwt.id(id); + jwt.issuedAt(Time.currentTime()); + jwt.expiration(expiration); + jwt.issuer(issuer); + jwt.audience(issuer); + + String token = new JWSBuilder().jsonContent(jwt).rsa256(realm.getPrivateKey()); + return token; + } + + private static String getIssuer(RealmModel realm, UriInfo uri) { + return Urls.realmIssuer(uri.getBaseUri(), realm.getName()); + } + +} 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 0fad9c2f07..38d71f227c 100644 --- a/services/src/main/java/org/keycloak/services/clientregistration/DefaultClientRegistrationProvider.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/DefaultClientRegistrationProvider.java @@ -2,10 +2,10 @@ 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.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.representations.idm.ClientRepresentation; @@ -23,7 +23,7 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv private KeycloakSession session; private EventBuilder event; - private ClientRegAuth auth; + private ClientRegistrationAuth auth; public DefaultClientRegistrationProvider(KeycloakSession session) { this.session = session; @@ -39,11 +39,19 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv try { ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true); - KeycloakModelUtils.generateRegistrationAccessToken(clientModel); - 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) { @@ -60,12 +68,15 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv ClientModel client = session.getContext().getRealm().getClientByClientId(clientId); auth.requireView(client); + ClientRepresentation rep = ModelToRepresentation.toRepresentation(client); + if (auth.isRegistrationAccessToken()) { - KeycloakModelUtils.generateRegistrationAccessToken(client); + String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client); + rep.setRegistrationAccessToken(registrationAccessToken); } event.client(client.getClientId()).success(); - return Response.ok(ModelToRepresentation.toRepresentation(client)).build(); + return Response.ok(rep).build(); } @PUT @@ -78,13 +89,13 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv auth.requireUpdate(client); RepresentationToModel.updateClient(rep, client); + rep = ModelToRepresentation.toRepresentation(client); if (auth.isRegistrationAccessToken()) { - KeycloakModelUtils.generateRegistrationAccessToken(client); + String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client); + rep.setRegistrationAccessToken(registrationAccessToken); } - rep = ModelToRepresentation.toRepresentation(client); - event.client(client.getClientId()).success(); return Response.ok(rep).build(); } @@ -106,7 +117,7 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv } @Override - public void setAuth(ClientRegAuth auth) { + public void setAuth(ClientRegistrationAuth auth) { this.auth = auth; } 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 b27ddb0383..961f28de78 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,28 +1,11 @@ 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.events.EventType; -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.protocol.oidc.OIDCClientDescriptionConverter; -import org.keycloak.protocol.oidc.representations.OIDCClientRepresentation; -import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.services.ErrorResponse; -import org.keycloak.services.clientregistration.ClientRegAuth; +import org.keycloak.services.clientregistration.ClientRegistrationAuth; import org.keycloak.services.clientregistration.ClientRegistrationProvider; -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 */ @@ -32,7 +15,7 @@ public class OIDCClientRegistrationProvider implements ClientRegistrationProvide private KeycloakSession session; private EventBuilder event; - private ClientRegAuth auth; + private ClientRegistrationAuth auth; public OIDCClientRegistrationProvider(KeycloakSession session) { this.session = session; @@ -55,7 +38,7 @@ public class OIDCClientRegistrationProvider implements ClientRegistrationProvide // // String registrationAccessToken = TokenGenerator.createRegistrationAccessToken(); // -// clientModel.setRegistrationSecret(registrationAccessToken); +// clientModel.setRegistrationToken(registrationAccessToken); // // URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build(); // @@ -87,7 +70,7 @@ public class OIDCClientRegistrationProvider implements ClientRegistrationProvide } @Override - public void setAuth(ClientRegAuth auth) { + public void setAuth(ClientRegistrationAuth auth) { this.auth = auth; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java new file mode 100644 index 0000000000..7ac203636e --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java @@ -0,0 +1,102 @@ +package org.keycloak.services.resources.admin; + +import org.keycloak.events.admin.OperationType; +import org.keycloak.models.ClientInitialAccessModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; +import org.keycloak.representations.idm.ClientInitialAccessPresentation; +import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils; + +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.*; +import javax.ws.rs.core.*; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public class ClientInitialAccessResource { + + private final RealmAuth auth; + private final RealmModel realm; + private final AdminEventBuilder adminEvent; + + @Context + protected KeycloakSession session; + + @Context + protected UriInfo uriInfo; + + public ClientInitialAccessResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) { + this.auth = auth; + this.realm = realm; + this.adminEvent = adminEvent; + + auth.init(RealmAuth.Resource.CLIENT); + } + + /** + * Create a new initial access token. + * + * @param config + * @return + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public ClientInitialAccessPresentation create(ClientInitialAccessCreatePresentation config, @Context final HttpServletResponse response) { + auth.requireManage(); + + int expiration = config.getExpiration() != null ? config.getExpiration() : 0; + int count = config.getCount() != null ? config.getCount() : 1; + + ClientInitialAccessModel clientInitialAccessModel = session.sessions().createClientInitialAccessModel(realm, expiration, count); + + adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, clientInitialAccessModel.getId()).representation(config).success(); + + if (session.getTransaction().isActive()) { + session.getTransaction().commit(); + } + + ClientInitialAccessPresentation rep = wrap(clientInitialAccessModel); + + String token = ClientRegistrationTokenUtils.createInitialAccessToken(realm, uriInfo, clientInitialAccessModel); + rep.setToken(token); + + response.setStatus(Response.Status.CREATED.getStatusCode()); + response.setHeader(HttpHeaders.LOCATION, uriInfo.getAbsolutePathBuilder().path(clientInitialAccessModel.getId()).build().toString()); + + return rep; + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List list() { + List models = session.sessions().listClientInitialAccess(realm); + List reps = new LinkedList<>(); + for (ClientInitialAccessModel m : models) { + ClientInitialAccessPresentation r = wrap(m); + reps.add(r); + } + return reps; + } + + @DELETE + @Path("{id}") + public void delete(final @PathParam("id") String id) { + session.sessions().removeClientInitialAccessModel(realm, id); + } + + private ClientInitialAccessPresentation wrap(ClientInitialAccessModel model) { + ClientInitialAccessPresentation rep = new ClientInitialAccessPresentation(); + rep.setId(model.getId()); + rep.setTimestamp(model.getTimestamp()); + rep.setExpiration(model.getExpiration()); + rep.setCount(model.getCount()); + rep.setRemainingCount(model.getRemainingCount()); + return rep; + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index f729c7fa34..03b06360e8 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -22,6 +22,7 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; +import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils; import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.ResourceAdminManager; @@ -228,8 +229,11 @@ public class ClientResource { public ClientRepresentation regenerateRegistrationAccessToken() { auth.requireManage(); - KeycloakModelUtils.generateRegistrationAccessToken(client); + String token = ClientRegistrationTokenUtils.updateRegistrationAccessToken(realm, uriInfo, client); + ClientRepresentation rep = ModelToRepresentation.toRepresentation(client); + rep.setRegistrationAccessToken(token); + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).representation(rep).success(); return rep; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 6ef9e1e120..36af035e3d 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -140,6 +140,18 @@ public class RealmAdminResource { return clientsResource; } + /** + * Base path for managing client initial access tokens + * + * @return + */ + @Path("clients-initial-access") + public ClientInitialAccessResource getClientInitialAccess() { + ClientInitialAccessResource resource = new ClientInitialAccessResource(realm, auth, adminEvent); + ResteasyProviderFactory.getInstance().injectProperties(resource); + return resource; + } + /** * base path for managing realm-level roles of this realm * diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientRegistrationTest.java index 8b8dfad45c..8535d463f9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientRegistrationTest.java @@ -2,6 +2,7 @@ package org.keycloak.testsuite.client; import org.junit.After; import org.junit.Before; +import org.keycloak.client.registration.Auth; import org.keycloak.client.registration.ClientRegistration; import org.keycloak.client.registration.ClientRegistrationException; import org.keycloak.models.AdminRoles; @@ -13,7 +14,6 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import javax.ws.rs.NotFoundException; -import javax.ws.rs.core.Response; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -76,13 +76,11 @@ public abstract class AbstractClientRegistrationTest extends AbstractKeycloakTes testRealms.add(rep); } - public ClientRepresentation createClient(ClientRepresentation client) { - Response response = adminClient.realm(REALM_NAME).clients().create(client); - String id = response.getLocation().toString(); - id = id.substring(id.lastIndexOf('/') + 1); - client.setId(id); - response.close(); - return client; + public ClientRepresentation createClient(ClientRepresentation client) throws ClientRegistrationException { + authManageClients(); + ClientRepresentation response = reg.create(client); + reg.auth(null); + return response; } public ClientRepresentation getClient(String clientId) { @@ -93,4 +91,20 @@ public abstract class AbstractClientRegistrationTest extends AbstractKeycloakTes } } + void authCreateClients() { + reg.auth(Auth.token(getToken("create-clients", "password"))); + } + + void authManageClients() { + reg.auth(Auth.token(getToken("manage-clients", "password"))); + } + + void authNoAccess() { + reg.auth(Auth.token(getToken("no-access", "password"))); + } + + private String getToken(String username, String password) { + return oauthClient.getToken(REALM_NAME, "security-admin-console", null, username, password).getToken(); + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java index 8b84b10af9..3390988e91 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java @@ -196,20 +196,4 @@ public class ClientRegistrationTest extends AbstractClientRegistrationTest { } } - private void authCreateClients() { - reg.auth(Auth.token(getToken("create-clients", "password"))); - } - - private void authManageClients() { - reg.auth(Auth.token(getToken("manage-clients", "password"))); - } - - private void authNoAccess() { - reg.auth(Auth.token(getToken("no-access", "password"))); - } - - private String getToken(String username, String password) { - return oauthClient.getToken(REALM_NAME, "security-admin-console", null, username, password).getToken(); - } - } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/InitialAccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/InitialAccessTokenTest.java new file mode 100644 index 0000000000..0ef75dd4cf --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/InitialAccessTokenTest.java @@ -0,0 +1,101 @@ +package org.keycloak.testsuite.client; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.admin.client.resource.ClientInitialAccessResource; +import org.keycloak.client.registration.Auth; +import org.keycloak.client.registration.ClientRegistrationException; +import org.keycloak.client.registration.HttpErrorException; +import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; +import org.keycloak.representations.idm.ClientInitialAccessPresentation; +import org.keycloak.representations.idm.ClientRepresentation; + +/** + * @author Stian Thorgersen + */ +public class InitialAccessTokenTest extends AbstractClientRegistrationTest { + + private ClientInitialAccessResource resource; + + @Before + public void before() throws Exception { + super.before(); + + resource = adminClient.realm(REALM_NAME).clientInitialAccess(); + } + + @Test + public void create() throws ClientRegistrationException { + ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation()); + + reg.auth(Auth.token(response)); + + ClientRepresentation rep = new ClientRepresentation(); + + ClientRepresentation created = reg.create(rep); + Assert.assertNotNull(created); + + try { + reg.create(rep); + } catch (ClientRegistrationException e) { + Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode()); + } + } + + @Test + public void createMultiple() throws ClientRegistrationException { + ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation(0, 2)); + + reg.auth(Auth.token(response)); + + ClientRepresentation rep = new ClientRepresentation(); + + ClientRepresentation created = reg.create(rep); + Assert.assertNotNull(created); + + created = reg.create(rep); + Assert.assertNotNull(created); + + try { + reg.create(rep); + } catch (ClientRegistrationException e) { + Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode()); + } + } + + @Test + public void createExpired() throws ClientRegistrationException, InterruptedException { + ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation(1, 1)); + + reg.auth(Auth.token(response)); + + ClientRepresentation rep = new ClientRepresentation(); + + Thread.sleep(2); + + try { + reg.create(rep); + } catch (ClientRegistrationException e) { + Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode()); + } + } + + @Test + public void createDeleted() throws ClientRegistrationException, InterruptedException { + ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation()); + + reg.auth(Auth.token(response)); + + resource.delete(response.getId()); + + ClientRepresentation rep = new ClientRepresentation(); + + try { + reg.create(rep); + } catch (ClientRegistrationException e) { + Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode()); + } + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java index be880bf293..cad28ab875 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java @@ -7,8 +7,6 @@ import org.keycloak.client.registration.ClientRegistrationException; import org.keycloak.client.registration.HttpErrorException; import org.keycloak.representations.idm.ClientRepresentation; -import javax.ws.rs.core.Response; - import static org.junit.Assert.*; /** @@ -22,13 +20,13 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest public void before() throws Exception { super.before(); - client = new ClientRepresentation(); - client.setEnabled(true); - client.setClientId("RegistrationAccessTokenTest"); - client.setSecret("RegistrationAccessTokenTestClientSecret"); - client.setRegistrationAccessToken("RegistrationAccessTokenTestRegistrationAccessToken"); - client.setRootUrl("http://root"); - client = createClient(client); + ClientRepresentation c = new ClientRepresentation(); + c.setEnabled(true); + c.setClientId("RegistrationAccessTokenTest"); + c.setSecret("RegistrationAccessTokenTestClientSecret"); + c.setRootUrl("http://root"); + + client = createClient(c); reg.auth(Auth.token(client.getRegistrationAccessToken())); } @@ -36,7 +34,7 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest private ClientRepresentation assertRead(String id, String registrationAccess, boolean expectSuccess) throws ClientRegistrationException { if (expectSuccess) { reg.auth(Auth.token(registrationAccess)); - ClientRepresentation rep = reg.get(client.getClientId()); + ClientRepresentation rep = reg.get(id); assertNotNull(rep); return rep; } else { @@ -76,6 +74,7 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest @Test public void updateClientWithRegistrationToken() throws ClientRegistrationException { client.setRootUrl("http://newroot"); + ClientRepresentation rep = reg.update(client); assertEquals("http://newroot", getClient(client.getId()).getRootUrl()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientInitialAccessTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientInitialAccessTest.java new file mode 100644 index 0000000000..79ef1ac679 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientInitialAccessTest.java @@ -0,0 +1,58 @@ +package org.keycloak.testsuite.admin; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.admin.client.resource.ClientInitialAccessResource; +import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; +import org.keycloak.representations.idm.ClientInitialAccessPresentation; + +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public class ClientInitialAccessTest extends AbstractClientTest { + + @Test + public void create() { + ClientInitialAccessResource resource = keycloak.realm(REALM_NAME).clientInitialAccess(); + + ClientInitialAccessPresentation access = resource.create(new ClientInitialAccessCreatePresentation(1000, 2)); + Assert.assertEquals(new Integer(2), access.getCount()); + Assert.assertEquals(new Integer(2), access.getRemainingCount()); + Assert.assertEquals(new Integer(1000), access.getExpiration()); + Assert.assertNotNull(access.getTimestamp()); + Assert.assertNotNull(access.getToken()); + + ClientInitialAccessPresentation access2 = resource.create(new ClientInitialAccessCreatePresentation()); + + List list = resource.list(); + Assert.assertEquals(2, list.size()); + + for (ClientInitialAccessPresentation r : list) { + if (r.getId().equals(access.getId())) { + Assert.assertEquals(new Integer(2), r.getCount()); + Assert.assertEquals(new Integer(2), r.getRemainingCount()); + Assert.assertEquals(new Integer(1000), r.getExpiration()); + Assert.assertNotNull(r.getTimestamp()); + Assert.assertNull(r.getToken()); + } else if(r.getId().equals(access2.getId())) { + Assert.assertEquals(new Integer(1), r.getCount()); + Assert.assertEquals(new Integer(1), r.getRemainingCount()); + Assert.assertEquals(new Integer(0), r.getExpiration()); + Assert.assertNotNull(r.getTimestamp()); + Assert.assertNull(r.getToken()); + } else { + Assert.fail("Unexpected id"); + } + } + + resource.delete(access.getId()); + resource.delete(access2.getId()); + + Assert.assertTrue(resource.list().isEmpty()); + } + +} From e8cb3a416df5759f576484ab35ff417c102dbf27 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 18 Nov 2015 10:32:17 +0100 Subject: [PATCH 2/4] KEYCLOAK-2085 Added initial access token support to admin console --- .../messages/admin-messages_en.properties | 13 +++ .../theme/base/admin/resources/js/app.js | 21 ++++ .../admin/resources/js/controllers/realm.js | 59 ++++++++++ .../theme/base/admin/resources/js/loaders.js | 7 ++ .../theme/base/admin/resources/js/services.js | 20 ++-- .../client-initial-access-create.html | 63 +++++++++++ .../partials/client-initial-access.html | 52 +++++++++ .../resources/templates/kc-tabs-realm.html | 1 + .../resources/admin/AdminConsole.java | 11 +- .../resources/admin/AdminMessagesLoader.java | 107 ------------------ 10 files changed, 231 insertions(+), 123 deletions(-) create mode 100755 forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access-create.html create mode 100755 forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access.html delete mode 100644 services/src/main/java/org/keycloak/services/resources/admin/AdminMessagesLoader.java diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index a9e74f7020..455cccef23 100644 --- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -109,6 +109,7 @@ realm-tab-email=Email realm-tab-themes=Themes realm-tab-cache=Cache realm-tab-tokens=Tokens +realm-tab-client-initial-access=Initial Access Tokens realm-tab-security-defenses=Security Defenses realm-tab-general=General add-realm=Add Realm @@ -470,3 +471,15 @@ identity-provider-mappers=Identity Provider Mappers create-identity-provider-mapper=Create Identity Provider Mapper add-identity-provider-mapper=Add Identity Provider Mapper client.description.tooltip=Specifies description of the client. For example 'My Client for TimeSheets'. Supports keys for localized values as well. For example\: ${my_client_description} + +expires=Expires +expiration=Expiration +count=Count +remainingCount=Remaining count +created=Created +back=Back +initial-access-tokens=Initial Access Tokens +add-initial-access-tokens=Add Initial Access Token +initial-access-token=Initial Access Token +initial-access.copyPaste.tooltip=Copy/paste the initial access token before navigating away from this page as it's not posible to retrieve later +continue=Continue \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index fe74ff27a0..9972eb11e7 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -176,6 +176,27 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'RealmTokenDetailCtrl' }) + .when('/realms/:realm/client-initial-access', { + templateUrl : resourceUrl + '/partials/client-initial-access.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + clientInitialAccess : function(ClientInitialAccessLoader) { + return ClientInitialAccessLoader(); + } + }, + controller : 'ClientInitialAccessCtrl' + }) + .when('/realms/:realm/client-initial-access/create', { + templateUrl : resourceUrl + '/partials/client-initial-access-create.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + } + }, + controller : 'ClientInitialAccessCreateCtrl' + }) .when('/realms/:realm/keys-settings', { templateUrl : resourceUrl + '/partials/realm-keys.html', resolve : { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index 6cd5beaaf1..c98f141a9c 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -1986,6 +1986,65 @@ module.controller('AuthenticationConfigCreateCtrl', function($scope, realm, flow }); +module.controller('ClientInitialAccessCtrl', function($scope, realm, clientInitialAccess, ClientInitialAccess, Dialog, Notifications, $route) { + $scope.realm = realm; + $scope.clientInitialAccess = clientInitialAccess; + + $scope.remove = function(id) { + Dialog.confirmDelete(id, 'initial access token', function() { + ClientInitialAccess.remove({ realm: realm.realm, id: id }, function() { + Notifications.success("The initial access token was deleted."); + $route.reload(); + }); + }); + } +}); + +module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, ClientInitialAccess, TimeUnit, Dialog, $location) { + $scope.expirationUnit = 'Days'; + $scope.expiration = TimeUnit.toUnit(0, $scope.expirationUnit); + $scope.count = 1; + $scope.realm = realm; + + $scope.$watch('expirationUnit', function(to, from) { + $scope.expiration = TimeUnit.convert($scope.expiration, from, to); + }); + + $scope.save = function() { + var expiration = TimeUnit.toSeconds($scope.expiration, $scope.expirationUnit); + ClientInitialAccess.save({ + realm: realm.realm + }, { expiration: expiration, count: $scope.count}, function (data) { + console.debug(data); + $scope.id = data.id; + $scope.token = data.token; + }); + }; + + $scope.cancel = function() { + $location.url('/realms/' + realm.realm + '/client-initial-access'); + }; + + $scope.done = function() { + var btns = { + ok: { + label: 'Continue', + cssClass: 'btn btn-primary' + }, + cancel: { + label: 'Cancel', + cssClass: 'btn btn-default' + } + } + + var title = 'Copy Initial Access Token'; + var message = 'Please copy and paste the initial access token before confirming as it can\'t be retrieved later'; + Dialog.open(title, message, btns, function() { + $location.url('/realms/' + realm.realm + '/client-initial-access'); + }); + }; +}); + diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js index bcd98b1486..61b608fe8e 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js @@ -475,6 +475,13 @@ module.factory('GroupLoader', function(Loader, Group, $route, $q) { }); }); +module.factory('ClientInitialAccessLoader', function(Loader, ClientInitialAccess, $route) { + return Loader.query(ClientInitialAccess, function() { + return { + realm: $route.current.params.realm + } + }); +}); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index 34fdc9d744..15e77a4357 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -102,6 +102,10 @@ module.service('Dialog', function($modal) { openDialog(title, message, btns, '/templates/kc-modal-message.html').then(success, cancel); } + dialog.open = function(title, message, btns, success, cancel) { + openDialog(title, message, btns, '/templates/kc-modal.html').then(success, cancel); + } + return dialog }); @@ -284,6 +288,13 @@ module.service('ServerInfo', function($resource, $q, $http) { } }); +module.factory('ClientInitialAccess', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/clients-initial-access/:id', { + realm : '@realm', + id : '@id' + }); +}); + module.factory('ClientProtocolMapper', function($resource) { return $resource(authUrl + '/admin/realms/:realm/clients/:client/protocol-mappers/models/:id', { @@ -1548,11 +1559,4 @@ module.factory('UserGroupMapping', function($resource) { method : 'PUT' } }); -}); - - - - - - - +}); \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access-create.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access-create.html new file mode 100755 index 0000000000..ef549396ab --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access-create.html @@ -0,0 +1,63 @@ +
+ + + +

{{:: 'add-client' | translate}}

+ +
+ +
+ + +
+ + +
+ {{:: 'expiration.tooltip' | translate}} +
+ +
+ +
+ +
+ {{:: 'count.tooltip' | translate}} +
+ +
+
+ + +
+
+
+ +
+
+ + +
+ +
+ + {{:: 'initial-access.copyPaste.tooltip' | translate}} +
+ +
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access.html new file mode 100755 index 0000000000..7b7c90e72d --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access.html @@ -0,0 +1,52 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+ +
+
+
+ + +
+
{{:: 'id' | translate}}{{:: 'created' | translate}}{{:: 'expires' | translate}}{{:: 'count' | translate}}{{:: 'remainingCount' | translate}}{{:: 'actions' | translate}}
{{ia.id}}{{(ia.timestamp * 1000)|date:'shortDate'}} {{(ia.timestamp * 1000)|date:'mediumTime'}}{{((ia.timestamp + ia.expiration) * 1000)|date:'shortDate'}} {{((ia.timestamp + ia.expiration) * 1000)|date:'mediumTime'}}{{ia.count}}{{ia.remainingCount}} + +
{{:: 'no-results' | translate}}{{:: 'no-clients-available' | translate}}
+
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html index 76f01eafb5..5d14503589 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html @@ -13,6 +13,7 @@
  • {{:: 'realm-tab-themes' | translate}}
  • {{:: 'realm-tab-cache' | translate}}
  • {{:: 'realm-tab-tokens' | translate}}
  • +
  • {{:: 'realm-tab-client-initial-access' | translate}}
  • {{:: 'realm-tab-security-defenses' | translate}}
  • \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java index 871bf05d6f..5c22200a41 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java @@ -40,12 +40,7 @@ import javax.ws.rs.ext.Providers; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; +import java.util.*; import javax.ws.rs.QueryParam; /** @@ -318,10 +313,10 @@ public class AdminConsole { } try { - Properties msgs = AdminMessagesLoader.getMessages(getTheme(), lang); + Properties msgs = getTheme().getMessages("admin-messages", Locale.forLanguageTag(lang)); if (msgs.isEmpty()) { logger.warn("Message bundle not found for language code '" + lang + "'"); - msgs = AdminMessagesLoader.getMessages(getTheme(), "en"); // fall back to en + msgs = getTheme().getMessages("admin-messages", Locale.ENGLISH); } if (msgs.isEmpty()) logger.fatal("Message bundle not found for language code 'en'"); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminMessagesLoader.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminMessagesLoader.java deleted file mode 100644 index cde38bcb73..0000000000 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminMessagesLoader.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @author tags. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package org.keycloak.services.resources.admin; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; -import org.jboss.logging.Logger; -import org.keycloak.freemarker.Theme; - -/** - * Simple loader and cache for message bundles consumed by angular-translate. - * - * Note that these bundles are converted to JSON before being shipped to the UI. - * Also, the content should be formatted such that it can be interpolated by - * angular-translate. This is somewhat different from an ordinary Java bundle. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. - */ -public class AdminMessagesLoader { - protected static final Logger logger = Logger.getLogger(AdminConsole.class); - - // theme locale bundle - protected static final Map> allMessages = new HashMap>(); - - static Properties getMessages(Theme theme, String strLocale) throws IOException { - String themeName = theme.getName(); - Map bundlesForTheme = allMessages.get(themeName); - if (bundlesForTheme == null) { - bundlesForTheme = new HashMap(); - allMessages.put(themeName, bundlesForTheme); - } - - return findMessagesForTheme(theme, strLocale, bundlesForTheme); - } - - - private static Properties findMessagesForTheme(Theme theme, - String strLocale, - Map bundlesForTheme) throws IOException { - Properties messages = bundlesForTheme.get(strLocale); - if (messages != null) return messages; // use cached bundle - - // load bundle from theme - Locale locale = Locale.forLanguageTag(strLocale); - messages = theme.getMessages("admin-messages", locale); - - String themeName = theme.getName(); - if (messages == null) throw new NullPointerException(themeName + ": Unable to find admin-messages bundle for locale=" + strLocale); - - if (!bundlesForTheme.isEmpty()) { - // use first bundle as the standard - String standardLocale = bundlesForTheme.keySet().iterator().next(); - Properties standardBundle = bundlesForTheme.get(standardLocale); - validateMessages(themeName, standardBundle, standardLocale, messages, strLocale); - } - - bundlesForTheme.put(strLocale, messages); - return messages; - } - - private static void validateMessages(String themeName, Properties standardBundle, String standardLocale, Properties messages, String strLocale) { - if (standardBundle.keySet().containsAll(messages.keySet()) && - (messages.keySet().containsAll(standardBundle.keySet()))) { - return; // it all checks out - } - - // otherwise, find the offending keys - int warnCount = 0; - for (Object key : standardBundle.keySet()) { - if (!messages.containsKey(key)) { - logger.error(themeName + " theme: Key '" + key + "' not found in admin-messages bundle for locale=" + strLocale + - ". However, this key exists in previously loaded bundle for locale=" + standardLocale); - warnCount++; - } - - if (warnCount > 4) return; // There could be lots of these. Don't fill up the log. - } - - for (Object key : messages.keySet()) { - if (!standardBundle.containsKey(key)) { - logger.error(themeName + " theme: Key '" + key + "' was found in admin-messages bundle for locale=" + strLocale + - ". However, this key does not exist in previously loaded bundle for locale=" + standardLocale); - warnCount++; - } - - if (warnCount > 4) return; // There could be lots of these. Don't fill up the log. - } - } -} From 1ff5d6eb069c6d4dd66a19966c8772a5fc07d837 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 18 Nov 2015 13:24:24 +0100 Subject: [PATCH 3/4] KEYCLOAK-2085 Internationalization in client initial access controller --- .../base/admin/messages/admin-messages_en.properties | 4 +++- .../theme/base/admin/resources/js/controllers/realm.js | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 455cccef23..09192ae702 100644 --- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -482,4 +482,6 @@ initial-access-tokens=Initial Access Tokens add-initial-access-tokens=Add Initial Access Token initial-access-token=Initial Access Token initial-access.copyPaste.tooltip=Copy/paste the initial access token before navigating away from this page as it's not posible to retrieve later -continue=Continue \ No newline at end of file +continue=Continue +initial-access-token.confirm.title=Copy Initial Access Token +initial-access-token.confirm.text=Please copy and paste the initial access token before confirming as it can't be retrieved later diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index c98f141a9c..a837f4f45f 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -2000,7 +2000,7 @@ module.controller('ClientInitialAccessCtrl', function($scope, realm, clientIniti } }); -module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, ClientInitialAccess, TimeUnit, Dialog, $location) { +module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, ClientInitialAccess, TimeUnit, Dialog, $location, $translate) { $scope.expirationUnit = 'Days'; $scope.expiration = TimeUnit.toUnit(0, $scope.expirationUnit); $scope.count = 1; @@ -2028,17 +2028,17 @@ module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, Clien $scope.done = function() { var btns = { ok: { - label: 'Continue', + label: $translate.instant('continue'), cssClass: 'btn btn-primary' }, cancel: { - label: 'Cancel', + label: $translate.instant('cancel'), cssClass: 'btn btn-default' } } - var title = 'Copy Initial Access Token'; - var message = 'Please copy and paste the initial access token before confirming as it can\'t be retrieved later'; + var title = $translate.instant('initial-access-token.confirm.title'); + var message = $translate.instant('initial-access-token.confirm.text'); Dialog.open(title, message, btns, function() { $location.url('/realms/' + realm.realm + '/client-initial-access'); }); From 41c9289f148c8f5b9f82a822f5c7559e2eceb616 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 18 Nov 2015 15:09:32 +0100 Subject: [PATCH 4/4] KEYCLOAK-1749 Moved clien registration api --- .../api}/pom.xml | 11 ++---- .../keycloak/client/registration/Auth.java | 0 .../registration/ClientRegistration.java | 0 .../ClientRegistrationException.java | 0 .../ClientRepresentationMixIn.java | 0 .../registration/HttpErrorException.java | 0 .../client/registration/HttpUtil.java | 0 client-registration/cli/pom.xml | 34 +++++++++++++++++++ .../cli/ClientRegistrationCLI.java | 27 +++++++++++++++ client-registration/pom.xml | 19 +++++++++++ pom.xml | 13 ++++++- .../client/AdapterInstallationConfigTest.java | 3 +- .../integration-arquillian/tests/pom.xml | 2 +- 13 files changed, 97 insertions(+), 12 deletions(-) rename {client-api => client-registration/api}/pom.xml (71%) rename {client-api => client-registration/api}/src/main/java/org/keycloak/client/registration/Auth.java (100%) rename {client-api => client-registration/api}/src/main/java/org/keycloak/client/registration/ClientRegistration.java (100%) rename {client-api => client-registration/api}/src/main/java/org/keycloak/client/registration/ClientRegistrationException.java (100%) rename {client-api => client-registration/api}/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java (100%) rename {client-api => client-registration/api}/src/main/java/org/keycloak/client/registration/HttpErrorException.java (100%) rename {client-api => client-registration/api}/src/main/java/org/keycloak/client/registration/HttpUtil.java (100%) create mode 100755 client-registration/cli/pom.xml create mode 100644 client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java create mode 100755 client-registration/pom.xml diff --git a/client-api/pom.xml b/client-registration/api/pom.xml similarity index 71% rename from client-api/pom.xml rename to client-registration/api/pom.xml index e1c1b5c71a..30c911b14d 100755 --- a/client-api/pom.xml +++ b/client-registration/api/pom.xml @@ -2,14 +2,14 @@ - keycloak-parent + keycloak-client-registration-parent org.keycloak 1.7.0.Final-SNAPSHOT 4.0.0 - keycloak-client-api - Keycloak Client API + keycloak-client-registration-api + Keycloak Client Registration API @@ -21,11 +21,6 @@ org.apache.httpcomponents httpclient - - junit - junit - test - diff --git a/client-api/src/main/java/org/keycloak/client/registration/Auth.java b/client-registration/api/src/main/java/org/keycloak/client/registration/Auth.java similarity index 100% rename from client-api/src/main/java/org/keycloak/client/registration/Auth.java rename to client-registration/api/src/main/java/org/keycloak/client/registration/Auth.java diff --git a/client-api/src/main/java/org/keycloak/client/registration/ClientRegistration.java b/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistration.java similarity index 100% rename from client-api/src/main/java/org/keycloak/client/registration/ClientRegistration.java rename to client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistration.java diff --git a/client-api/src/main/java/org/keycloak/client/registration/ClientRegistrationException.java b/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistrationException.java similarity index 100% rename from client-api/src/main/java/org/keycloak/client/registration/ClientRegistrationException.java rename to client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistrationException.java diff --git a/client-api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java b/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java similarity index 100% rename from client-api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java rename to client-registration/api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java diff --git a/client-api/src/main/java/org/keycloak/client/registration/HttpErrorException.java b/client-registration/api/src/main/java/org/keycloak/client/registration/HttpErrorException.java similarity index 100% rename from client-api/src/main/java/org/keycloak/client/registration/HttpErrorException.java rename to client-registration/api/src/main/java/org/keycloak/client/registration/HttpErrorException.java diff --git a/client-api/src/main/java/org/keycloak/client/registration/HttpUtil.java b/client-registration/api/src/main/java/org/keycloak/client/registration/HttpUtil.java similarity index 100% rename from client-api/src/main/java/org/keycloak/client/registration/HttpUtil.java rename to client-registration/api/src/main/java/org/keycloak/client/registration/HttpUtil.java diff --git a/client-registration/cli/pom.xml b/client-registration/cli/pom.xml new file mode 100755 index 0000000000..13b8a8f91e --- /dev/null +++ b/client-registration/cli/pom.xml @@ -0,0 +1,34 @@ + + + + keycloak-client-registration-parent + org.keycloak + 1.7.0.Final-SNAPSHOT + + 4.0.0 + + keycloak-client-registration-cli + Keycloak Client Registration CLI + + + + + org.keycloak + keycloak-core + + + org.keycloak + keycloak-client-registration-api + + + org.apache.httpcomponents + httpclient + + + org.jboss.aesh + aesh + + + + 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 new file mode 100644 index 0000000000..53c0b88445 --- /dev/null +++ b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java @@ -0,0 +1,27 @@ +package org.keycloak.client.registration.cli; + +import org.jboss.aesh.console.AeshConsole; +import org.jboss.aesh.console.AeshConsoleBuilder; +import org.jboss.aesh.console.Prompt; +import org.jboss.aesh.console.settings.Settings; +import org.jboss.aesh.console.settings.SettingsBuilder; + +/** + * @author Stian Thorgersen + */ +public class ClientRegistrationCLI { + + public static void main(String[] args) { + + Settings settings = new SettingsBuilder().logging(true).create(); + AeshConsole aeshConsole = new AeshConsoleBuilder().settings(settings) + .prompt(new Prompt("[aesh@rules]$ ")) +// .command() + .create(); + + aeshConsole.start(); + } + + +} + diff --git a/client-registration/pom.xml b/client-registration/pom.xml new file mode 100755 index 0000000000..214122831b --- /dev/null +++ b/client-registration/pom.xml @@ -0,0 +1,19 @@ + + + keycloak-parent + org.keycloak + 1.7.0.Final-SNAPSHOT + + + Keycloak Client Registration Parent + + 4.0.0 + keycloak-client-registration-parent + pom + + + api + cli + + diff --git a/pom.xml b/pom.xml index d545efd5f9..c137e5904b 100755 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,7 @@ 1.2.17 1.3.1b 1.5.1 + 0.66 1.4 7.5.Final @@ -135,7 +136,7 @@ common core - client-api + client-registration connections dependencies events @@ -580,6 +581,11 @@ pax-web-runtime ${pax.web.version} + + org.jboss.aesh + aesh + ${aesh.version} + @@ -622,6 +628,11 @@ keycloak-connections-http-client ${project.version} + + org.keycloak + keycloak-client-registration-api + ${project.version} + org.keycloak keycloak-connections-mongo-update diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java index bf98364dbd..4e0712ee31 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java @@ -9,8 +9,6 @@ import org.keycloak.common.enums.SslRequired; import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.representations.idm.ClientRepresentation; -import javax.ws.rs.core.Response; - import static org.junit.Assert.*; /** @@ -37,6 +35,7 @@ public class AdapterInstallationConfigTest extends AbstractClientRegistrationTes client.setRegistrationAccessToken("RegistrationAccessTokenTestRegistrationAccessToken"); client.setRootUrl("http://root"); client = createClient(client); + client.setSecret("RegistrationAccessTokenTestClientSecret"); client2 = new ClientRepresentation(); client2.setEnabled(true); diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index 074a08a1de..13a2059f82 100644 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -230,7 +230,7 @@ org.keycloak - keycloak-client-api + keycloak-client-registration-api org.keycloak