keycloak-aplcache
Changes
client-registration/api/pom.xml 11(+3 -8)
client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistration.java 13(+10 -3)
client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistrationException.java 0(+0 -0)
client-registration/api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java 13(+13 -0)
client-registration/api/src/main/java/org/keycloak/client/registration/HttpErrorException.java 0(+0 -0)
client-registration/cli/pom.xml 34(+34 -0)
client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java 27(+27 -0)
client-registration/pom.xml 19(+19 -0)
core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessCreatePresentation.java 36(+36 -0)
forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties 15(+15 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js 59(+59 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access.html 52(+52 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access-create.html 63(+63 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html 1(+1 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientInitialAccessResource.java 31(+31 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java 5(+4 -1)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java 10(+5 -5)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java 8(+4 -4)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientInitialAccessAdapter.java 69(+69 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/ClientInitialAccessAdapter.java 55(+55 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/entities/ClientInitialAccessEntity.java 68(+68 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java 65(+51 -14)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java 8(+4 -4)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientInitialAccessEntity.java 48(+48 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java 90(+69 -21)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientInitialAccessMapper.java 80(+80 -0)
pom.xml 13(+12 -1)
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java 22(+3 -19)
services/src/main/java/org/keycloak/services/clientregistration/AdapterInstallationClientRegistrationProvider.java 5(+2 -3)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java 116(+82 -34)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationProvider.java 3(+1 -2)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationService.java 2(+1 -1)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java 99(+99 -0)
services/src/main/java/org/keycloak/services/clientregistration/DefaultClientRegistrationProvider.java 31(+21 -10)
services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java 25(+4 -21)
services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java 102(+102 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientInitialAccessTest.java 58(+58 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientRegistrationTest.java 30(+22 -8)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java 3(+1 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java 16(+0 -16)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/InitialAccessTokenTest.java 101(+101 -0)
Details
diff --git a/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java b/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java
new file mode 100644
index 0000000..ba382f6
--- /dev/null
+++ b/client-registration/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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+abstract class ClientRepresentationMixIn {
+
+ @JsonIgnore
+ String registrationAccessToken;
+
+}
client-registration/cli/pom.xml 34(+34 -0)
diff --git a/client-registration/cli/pom.xml b/client-registration/cli/pom.xml
new file mode 100755
index 0000000..13b8a8f
--- /dev/null
+++ b/client-registration/cli/pom.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-client-registration-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.7.0.Final-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-client-registration-cli</artifactId>
+ <name>Keycloak Client Registration CLI</name>
+ <description/>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-client-registration-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.aesh</groupId>
+ <artifactId>aesh</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>
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 0000000..53c0b88
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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();
+ }
+
+
+}
+
client-registration/pom.xml 19(+19 -0)
diff --git a/client-registration/pom.xml b/client-registration/pom.xml
new file mode 100755
index 0000000..2141228
--- /dev/null
+++ b/client-registration/pom.xml
@@ -0,0 +1,19 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.7.0.Final-SNAPSHOT</version>
+ </parent>
+
+ <name>Keycloak Client Registration Parent</name>
+ <description/>
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>keycloak-client-registration-parent</artifactId>
+ <packaging>pom</packaging>
+
+ <modules>
+ <module>api</module>
+ <module>cli</module>
+ </modules>
+</project>
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 aed99fc..c53eb4b 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 @@
<addForeignKeyConstraint baseColumnNames="ROLE_ID" baseTableName="GROUP_ROLE_MAPPING" constraintName="FK_GROUP_ROLE_ROLE" referencedColumnNames="ID" referencedTableName="KEYCLOAK_ROLE"/>
<addColumn tableName="CLIENT">
- <column name="REGISTRATION_SECRET" type="VARCHAR(255)"/>
+ <column name="REGISTRATION_TOKEN" type="VARCHAR(255)"/>
</addColumn>
</changeSet>
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 0000000..4c18b3d
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessCreatePresentation.java
@@ -0,0 +1,36 @@
+package org.keycloak.representations.idm;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..d8021ad
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessPresentation.java
@@ -0,0 +1,67 @@
+package org.keycloak.representations.idm;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0d103a6..10b6c20 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/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 a9e74f7..09192ae 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,17 @@ 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
+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/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index fe74ff2..9972eb1 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 6cd5bea..a837f4f 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, $translate) {
+ $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: $translate.instant('continue'),
+ cssClass: 'btn btn-primary'
+ },
+ cancel: {
+ label: $translate.instant('cancel'),
+ cssClass: 'btn btn-default'
+ }
+ }
+
+ 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');
+ });
+ };
+});
+
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 bcd98b1..61b608f 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 34fdc9d..15e77a4 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.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access.html
new file mode 100755
index 0000000..7b7c90e
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access.html
@@ -0,0 +1,52 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <kc-tabs-realm></kc-tabs-realm>
+
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="6">
+ <div class="form-inline">
+ <div class="form-group">
+ <div class="input-group">
+ <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search.id" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
+ <div class="input-group-addon">
+ <i class="fa fa-search" type="submit"></i>
+ </div>
+ </div>
+ </div>
+
+ <div class="pull-right" data-ng-show="access.manageRealm">
+ <a id="createClient" class="btn btn-default" href="#/realms/{{realm.realm}}/client-initial-access/create">{{:: 'create' | translate}}</a>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr data-ng-hide="clients.length == 0">
+ <th>{{:: 'id' | translate}}</th>
+ <th>{{:: 'created' | translate}}</th>
+ <th>{{:: 'expires' | translate}}</th>
+ <th>{{:: 'count' | translate}}</th>
+ <th>{{:: 'remainingCount' | translate}}</th>
+ <th>{{:: 'actions' | translate}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="ia in clientInitialAccess | filter:search | orderBy:'timestamp'">
+ <td>{{ia.id}}</td>
+ <td>{{(ia.timestamp * 1000)|date:'shortDate'}} {{(ia.timestamp * 1000)|date:'mediumTime'}}</td>
+ <td><span data-ng-show="ia.expiration > 0">{{((ia.timestamp + ia.expiration) * 1000)|date:'shortDate'}} {{((ia.timestamp + ia.expiration) * 1000)|date:'mediumTime'}}</span></td>
+ <td>{{ia.count}}</td>
+ <td>{{ia.remainingCount}}</td>
+ <td class="kc-action-cell">
+ <button class="btn btn-default btn-block btn-sm" data-ng-click="remove(ia.id)">{{:: 'delete' | translate}}</button>
+ </td>
+ </tr>
+ <tr data-ng-show="(clients | filter:search).length == 0">
+ <td class="text-muted" colspan="3" data-ng-show="search.clientId">{{:: 'no-results' | translate}}</td>
+ <td class="text-muted" colspan="3" data-ng-hide="search.clientId">{{:: 'no-clients-available' | translate}}</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<kc-menu></kc-menu>
\ 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 0000000..ef54939
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access-create.html
@@ -0,0 +1,63 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/client-initial-access">{{:: 'initial-access-tokens' | translate}}</a></li>
+ <li>{{:: 'add-initial-access-tokens' | translate}}</li>
+ </ol>
+
+ <h1 data-ng-show="create">{{:: 'add-client' | translate}}</h1>
+
+ <form class="form-horizontal" name="createForm" novalidate kc-read-only="!access.manageRealm" data-ng-hide="token">
+
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="expiration">{{:: 'expiration' | translate}}</label>
+
+ <div class="col-md-6 time-selector">
+ <input class="form-control" type="number" required min="0" max="31536000" data-ng-model="expiration" id="expiration"
+ name="expiration"/>
+ <select class="form-control" name="expirationUnit" data-ng-model="expirationUnit">
+ <option data-ng-selected="!expirationUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
+ </select>
+ </div>
+ <kc-tooltip>{{:: 'expiration.tooltip' | translate}}</kc-tooltip>
+ </div>
+
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="count">{{:: 'count' | translate}} </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="count" name="count" required min="1" max="100" data-ng-model="count">
+ </div>
+ <kc-tooltip>{{:: 'count.tooltip' | translate}}</kc-tooltip>
+ </div>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
+ <button kc-save>{{:: 'save' | translate}}</button>
+ <button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
+ </div>
+ </div>
+ </form>
+
+ <form name="displayForm" data-ng-show="token">
+ <div class="form-group">
+ <label for="initialAccessToken">{{:: 'initial-access-token' | translate}}</label>
+
+ <div>
+ <textarea type="text" id="initialAccessToken" name="initialAccessToken" class="form-control" rows="6" kc-select-action="click">{{token}}</textarea>
+ </div>
+
+ <kc-tooltip>{{:: 'initial-access.copyPaste.tooltip' | translate}}</kc-tooltip>
+ </div>
+
+ <div class="form-group">
+ <div>
+ <button class="btn btn-default" data-ng-click="done()">{{:: 'Back' | translate}}</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ 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 76f01ea..5d14503 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 @@
<li ng-class="{active: path[2] == 'theme-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/theme-settings">{{:: 'realm-tab-themes' | translate}}</a></li>
<li ng-class="{active: path[2] == 'cache-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/cache-settings">{{:: 'realm-tab-cache' | translate}}</a></li>
<li ng-class="{active: path[2] == 'token-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/token-settings">{{:: 'realm-tab-tokens' | translate}}</a></li>
+ <li ng-class="{active: path[2] == 'client-initial-access'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/client-initial-access">{{:: 'realm-tab-client-initial-access' | translate}}</a></li>
<li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">{{:: 'realm-tab-security-defenses' | translate}}</a></li>
</ul>
</div>
\ No newline at end of file
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 0000000..8875a4c
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface ClientInitialAccessResource {
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ ClientInitialAccessPresentation create(ClientInitialAccessCreatePresentation rep);
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ List<ClientInitialAccessPresentation> 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 48e29bb..8ac199c 100755
--- 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
@@ -56,5 +56,8 @@ public interface RealmResource {
@Path("client-session-stats")
@GET
List<Map<String, String>> 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 0000000..7447319
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/ClientInitialAccessModel.java
@@ -0,0 +1,22 @@
+package org.keycloak.models;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 c421aea..051493e 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/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
index f15614f..d04699f 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/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
index 1a59f4f..0c1df9c 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<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
+
void close();
}
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 43f2b93..4126daa 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
@@ -50,8 +50,6 @@ import java.util.UUID;
*/
public final class KeycloakModelUtils {
- private static final int RANDOM_PASSWORD_BYTES = 32;
-
private KeycloakModelUtils() {
}
@@ -189,16 +187,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 457d13c..b1d71ec 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
@@ -418,7 +418,6 @@ public class ModelToRepresentation {
rep.setNotBefore(clientModel.getNotBefore());
rep.setNodeReRegistrationTimeout(clientModel.getNodeReRegistrationTimeout());
rep.setClientAuthenticatorType(clientModel.getClientAuthenticatorType());
- rep.setRegistrationAccessToken(clientModel.getRegistrationSecret());
Set<String> 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 baa2b13..bb9dbc8 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
@@ -797,8 +797,6 @@ public class RepresentationToModel {
KeycloakModelUtils.generateSecret(client);
}
- client.setRegistrationSecret(resourceRep.getRegistrationAccessToken());
-
if (resourceRep.getAttributes() != null) {
for (Map.Entry<String, String> entry : resourceRep.getAttributes().entrySet()) {
client.setAttribute(entry.getKey(), entry.getValue());
@@ -875,7 +873,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 7a87373..e03452a 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 1c04b9d..7c7b97d 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<String, String> attributes = new HashMap<String, String>();
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 5ea0b11..a7dc0f4 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 881b129..6218e26 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 cbacd09..8eb562f 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<MongoClientEntity> 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 0000000..7d75335
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientInitialAccessAdapter implements ClientInitialAccessModel {
+
+ private final KeycloakSession session;
+ private final InfinispanUserSessionProvider provider;
+ private final Cache<String, SessionEntity> cache;
+ private final RealmModel realm;
+ private final ClientInitialAccessEntity entity;
+
+ public ClientInitialAccessAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> 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/compat/ClientInitialAccessAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/ClientInitialAccessAdapter.java
new file mode 100644
index 0000000..3398eff
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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/entities/ClientInitialAccessEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/entities/ClientInitialAccessEntity.java
new file mode 100644
index 0000000..ed0aeac
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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/compat/MemUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java
index f45edf1..db20ef8 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<String, UserSessionEntity> offlineUserSessions;
private final ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions;
+ private ConcurrentHashMap<String, ClientInitialAccessEntity> clientInitialAccess;
public MemUserSessionProvider(KeycloakSession session, ConcurrentHashMap<String, UserSessionEntity> userSessions, ConcurrentHashMap<String, String> userSessionsByBrokerSessionId,
ConcurrentHashMap<String, Set<String>> userSessionsByBrokerUserId, ConcurrentHashMap<String, ClientSessionEntity> clientSessions,
ConcurrentHashMap<UsernameLoginFailureKey, UsernameLoginFailureEntity> loginFailures,
- ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions, ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions) {
+ ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions, ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions, ConcurrentHashMap<String, ClientInitialAccessEntity> 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<ClientInitialAccessEntity> iaitr = clientInitialAccess.values().iterator();
+ while (iaitr.hasNext()) {
+ ClientInitialAccessEntity e = iaitr.next();
+ if (e.getRealmId().equals(realm.getId()) && (e.getExpires() < Time.currentTime())) {
+ iaitr.remove();
+ }
+ }
}
@Override
@@ -575,6 +575,43 @@ public class MemUserSessionProvider implements UserSessionProvider {
}
@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<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
+ List<ClientInitialAccessModel> 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 187a33f..451fcb0 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<String, UserSessionEntity> offlineUserSessions = new ConcurrentHashMap<String, UserSessionEntity>();
private ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions = new ConcurrentHashMap<String, ClientSessionEntity>();
+ private ConcurrentHashMap<String, ClientInitialAccessEntity> 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/entities/ClientInitialAccessEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientInitialAccessEntity.java
new file mode 100644
index 0000000..05daf1e
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 9ba1587..c819259 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<ClientInitialAccessModel> wrapClientInitialAccess(RealmModel realm, Collection<ClientInitialAccessEntity> entities) {
+ List<ClientInitialAccessModel> models = new LinkedList<>();
+ for (ClientInitialAccessEntity e : entities) {
+ models.add(wrap(realm, e));
+ }
+ return models;
+ }
+
ClientSessionAdapter wrap(RealmModel realm, ClientSessionEntity entity, boolean offline) {
Cache<String, SessionEntity> cache = getCache(offline);
return entity != null ? new ClientSessionAdapter(session, this, cache, realm, entity, offline) : null;
}
+ ClientInitialAccessAdapter wrap(RealmModel realm, ClientInitialAccessEntity entity) {
+ Cache<String, SessionEntity> 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<String, SessionEntity> 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<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
+ Map<String, ClientInitialAccessEntity> 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/mapreduce/ClientInitialAccessMapper.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientInitialAccessMapper.java
new file mode 100644
index 0000000..87c9b3c
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientInitialAccessMapper implements Mapper<String, SessionEntity, String, Object>, 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;
+ }
+ }
+
+}
pom.xml 13(+12 -1)
diff --git a/pom.xml b/pom.xml
index d545efd..c137e59 100755
--- a/pom.xml
+++ b/pom.xml
@@ -76,6 +76,7 @@
<log4j.version>1.2.17</log4j.version>
<greenmail.version>1.3.1b</greenmail.version>
<xmlsec.version>1.5.1</xmlsec.version>
+ <aesh.version>0.66</aesh.version>
<enforcer.plugin.version>1.4</enforcer.plugin.version>
<jboss.as.plugin.version>7.5.Final</jboss.as.plugin.version>
@@ -135,7 +136,7 @@
<modules>
<module>common</module>
<module>core</module>
- <module>client-api</module>
+ <module>client-registration</module>
<module>connections</module>
<module>dependencies</module>
<module>events</module>
@@ -580,6 +581,11 @@
<artifactId>pax-web-runtime</artifactId>
<version>${pax.web.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.jboss.aesh</groupId>
+ <artifactId>aesh</artifactId>
+ <version>${aesh.version}</version>
+ </dependency>
<!-- keycloak -->
<dependency>
@@ -624,6 +630,11 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
+ <artifactId>keycloak-client-registration-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-mongo-update</artifactId>
<version>${project.version}</version>
</dependency>
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 298623d..81c2df4 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -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 feffc5f..c667b5e 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/ClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationProvider.java
index 0c6bc4e..98bd6f9 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 2aed3f1..0581388 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 0000000..7cd3342
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0fad9c2..38d71f2 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 b27ddb0..961f28d 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -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/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
index 871bf05..5c22200 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/ClientInitialAccessResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java
new file mode 100644
index 0000000..7ac2036
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<ClientInitialAccessPresentation> list() {
+ List<ClientInitialAccessModel> models = session.sessions().listClientInitialAccess(realm);
+ List<ClientInitialAccessPresentation> 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 f729c7f..03b0636 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 fb4d4a6..9845031 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
@@ -144,6 +144,18 @@ public class RealmAdminResource {
}
/**
+ * 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
*
* @return
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 0000000..79ef1ac
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<ClientInitialAccessPresentation> 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());
+ }
+
+}
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 8b8dfad..8535d46 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/AdapterInstallationConfigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java
index bf98364..4e0712e 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/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 8b84b10..3390988 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 0000000..0ef75dd
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 be880bf..cad28ab 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-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 074a08a..13a2059 100644
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -230,7 +230,7 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-client-api</artifactId>
+ <artifactId>keycloak-client-registration-api</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>