keycloak-aplcache

Changes

Details

diff --git a/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistration.java b/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistration.java
index 3be763f..2b1a991 100644
--- a/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistration.java
+++ b/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistration.java
@@ -25,12 +25,12 @@ public class ClientRegistration {
 
     private HttpUtil httpUtil;
 
-    public ClientRegistration(String authServerUrl, String realm) {
-        httpUtil = new HttpUtil(HttpClients.createDefault(), HttpUtil.getUrl(authServerUrl, "realms", realm, "clients"));
+    public static ClientRegistrationBuilder create() {
+        return new ClientRegistrationBuilder();
     }
 
-    public ClientRegistration(String authServerUrl, String realm, HttpClient httpClient) {
-        httpUtil = new HttpUtil(httpClient, HttpUtil.getUrl(authServerUrl, "realms", realm, "clients"));
+    ClientRegistration(HttpUtil httpUtil) {
+        this.httpUtil = httpUtil;
     }
 
     public void close() throws ClientRegistrationException {
@@ -92,4 +92,41 @@ public class ClientRegistration {
         }
     }
 
+    public static class ClientRegistrationBuilder {
+
+        private String url;
+        private HttpClient httpClient;
+
+        ClientRegistrationBuilder() {
+        }
+
+        public ClientRegistrationBuilder url(String realmUrl) {
+            url = realmUrl;
+            return this;
+        }
+
+        public ClientRegistrationBuilder url(String authUrl, String realm) {
+            url = HttpUtil.getUrl(authUrl, "realms", realm, "clients");
+            return this;
+        }
+
+        public ClientRegistrationBuilder httpClient(HttpClient httpClient) {
+            this.httpClient = httpClient;
+            return this;
+        }
+
+        public ClientRegistration build() {
+            if (url == null) {
+                throw new IllegalStateException("url not configured");
+            }
+
+            if (httpClient == null) {
+                httpClient = HttpClients.createDefault();
+            }
+
+            return new ClientRegistration(new HttpUtil(httpClient, url));
+        }
+
+    }
+
 }
diff --git a/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java
index 53c0b88..f0b0857 100644
--- a/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java
+++ b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java
@@ -1,27 +1,68 @@
 package org.keycloak.client.registration.cli;
 
+import org.jboss.aesh.cl.parser.CommandLineParserException;
 import org.jboss.aesh.console.AeshConsole;
 import org.jboss.aesh.console.AeshConsoleBuilder;
 import org.jboss.aesh.console.Prompt;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandNotFoundException;
+import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder;
 import org.jboss.aesh.console.settings.Settings;
 import org.jboss.aesh.console.settings.SettingsBuilder;
+import org.jboss.aesh.terminal.Color;
+import org.jboss.aesh.terminal.TerminalColor;
+import org.jboss.aesh.terminal.TerminalString;
+import org.keycloak.client.registration.Auth;
+import org.keycloak.client.registration.ClientRegistration;
+import org.keycloak.client.registration.cli.commands.CreateCommand;
+import org.keycloak.client.registration.cli.commands.ExitCommand;
+import org.keycloak.client.registration.cli.commands.SetupCommand;
+
+import java.util.LinkedList;
+import java.util.List;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
 public class ClientRegistrationCLI {
 
-    public static void main(String[] args) {
+    private static ClientRegistration reg;
+
+    public static void main(String[] args) throws CommandLineParserException, CommandNotFoundException {
+        reg = ClientRegistration.create().url("http://localhost:8080/auth/realms/master").build();
+        reg.auth(Auth.token("..."));
+
+        Context context = new Context();
+
+        List<Command> commands = new LinkedList<>();
+        commands.add(new SetupCommand(context));
+        commands.add(new CreateCommand(context));
+        commands.add(new ExitCommand(context));
 
-        Settings settings = new SettingsBuilder().logging(true).create();
-        AeshConsole aeshConsole = new AeshConsoleBuilder().settings(settings)
-                .prompt(new Prompt("[aesh@rules]$ "))
-//                .command()
+        SettingsBuilder builder = new SettingsBuilder().logging(true);
+        builder.enableMan(true).readInputrc(false);
+
+        Settings settings = builder.create();
+
+        AeshCommandRegistryBuilder commandRegistryBuilder = new AeshCommandRegistryBuilder();
+        for (Command c : commands) {
+            commandRegistryBuilder.command(c);
+        }
+
+        AeshConsole aeshConsole = new AeshConsoleBuilder()
+                .commandRegistry(commandRegistryBuilder.create())
+                .settings(settings)
+                .prompt(new Prompt(new TerminalString("[clientreg]$ ",
+                        new TerminalColor(Color.GREEN, Color.DEFAULT, Color.Intensity.BRIGHT))))
                 .create();
 
         aeshConsole.start();
+/*
+        if (args.length > 0) {
+            CommandContainer command = registry.getCommand(args[0], null);
+            ParserGenerator.parseAndPopulate(command, args[0], Arrays.copyOfRange(args, 1, args.length));
+        }*/
     }
 
-
 }
 
diff --git a/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCommand.java b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCommand.java
new file mode 100644
index 0000000..280534b
--- /dev/null
+++ b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCommand.java
@@ -0,0 +1,64 @@
+package org.keycloak.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.Arguments;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.jboss.aesh.io.Resource;
+import org.keycloak.client.registration.ClientRegistrationException;
+import org.keycloak.client.registration.cli.Context;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@CommandDefinition(name="create", description = "[OPTIONS] FILE")
+public class CreateCommand implements Command {
+
+    @Option(shortName = 'h', hasValue = false, description = "display this help and exit")
+    private boolean help;
+
+    @Arguments(description = "files or directories thats listed")
+    private List<Resource> arguments;
+
+    private Context context;
+
+    public CreateCommand(Context context) {
+        this.context = context;
+    }
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
+        System.out.println(help);
+
+
+        if(help) {
+            commandInvocation.getShell().out().println(commandInvocation.getHelpInfo("create"));
+        }
+        else {
+
+            if(arguments != null) {
+                for(Resource f : arguments) {
+                    System.out.println(f.getAbsolutePath());
+                    ClientRepresentation rep = JsonSerialization.readValue(f.read(), ClientRepresentation.class);
+                    try {
+                        context.getReg().create(rep);
+                    } catch (ClientRegistrationException e) {
+                        e.printStackTrace();
+                    }
+                }
+
+            }
+        }
+//            reg.create();
+
+        return CommandResult.SUCCESS;
+    }
+
+}
diff --git a/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/ExitCommand.java b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/ExitCommand.java
new file mode 100644
index 0000000..507881b
--- /dev/null
+++ b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/ExitCommand.java
@@ -0,0 +1,29 @@
+package org.keycloak.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.Context;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@CommandDefinition(name="exit", description = "Exit the program")
+public class ExitCommand implements Command {
+
+    private Context context;
+
+    public ExitCommand(Context context) {
+        this.context = context;
+    }
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
+        commandInvocation.stop();
+        return CommandResult.SUCCESS;
+    }
+
+}
diff --git a/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/SetupCommand.java b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/SetupCommand.java
new file mode 100644
index 0000000..26579ab
--- /dev/null
+++ b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/SetupCommand.java
@@ -0,0 +1,48 @@
+package org.keycloak.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.jboss.aesh.io.Resource;
+import org.keycloak.client.registration.ClientRegistrationException;
+import org.keycloak.client.registration.cli.Context;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@CommandDefinition(name="setup", description = "")
+public class SetupCommand implements Command {
+
+    @Option(shortName = 'h', hasValue = false, description = "display this help and exit")
+    private boolean help;
+
+    private Context context;
+
+    public SetupCommand(Context context) {
+        this.context = context;
+    }
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
+        System.out.println(help);
+
+        if(help) {
+            commandInvocation.getShell().out().println(commandInvocation.getHelpInfo("create"));
+        }
+
+        return CommandResult.SUCCESS;
+    }
+
+
+    private String promptForUsername(CommandInvocation invocation) throws InterruptedException {
+        invocation.print("username: ");
+        return invocation.getInputLine();
+    }
+
+}
diff --git a/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/Context.java b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/Context.java
new file mode 100644
index 0000000..49d8fb9
--- /dev/null
+++ b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/Context.java
@@ -0,0 +1,37 @@
+package org.keycloak.client.registration.cli;
+
+import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.map.SerializationConfig;
+import org.codehaus.jackson.map.annotate.JsonSerialize;
+import org.keycloak.client.registration.ClientRegistration;
+import org.keycloak.util.SystemPropertiesJsonParserFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class Context {
+
+    private static final ObjectMapper mapper = new ObjectMapper(new SystemPropertiesJsonParserFactory());
+    static {
+        mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
+        mapper.enable(SerializationConfig.Feature.INDENT_OUTPUT);
+    }
+
+    private ClientRegistration reg;
+
+    public ClientRegistration getReg() {
+        return reg;
+    }
+
+    public void setReg(ClientRegistration reg) {
+        this.reg = reg;
+    }
+
+    public static <T> T readJson(InputStream bytes, Class<T> type) throws IOException {
+        return mapper.readValue(bytes, type);
+    }
+
+}
diff --git a/client-registration/pom.xml b/client-registration/pom.xml
index 2141228..9d1ea9f 100755
--- a/client-registration/pom.xml
+++ b/client-registration/pom.xml
@@ -14,6 +14,6 @@
 
     <modules>
         <module>api</module>
-        <module>cli</module>
+        <!--<module>cli</module>-->
     </modules>
 </project>
diff --git a/docbook/auth-server-docs/reference/en/en-US/master.xml b/docbook/auth-server-docs/reference/en/en-US/master.xml
index 2af744f..95ff5ee 100755
--- a/docbook/auth-server-docs/reference/en/en-US/master.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/master.xml
@@ -48,6 +48,7 @@
                 <!ENTITY Recaptcha SYSTEM "modules/recaptcha.xml">
                 <!ENTITY AuthSPI SYSTEM "modules/auth-spi.xml">
                 <!ENTITY FilterAdapter SYSTEM "modules/servlet-filter-adapter.xml">
+                <!ENTITY ClientRegistration SYSTEM "modules/client-registration.xml">
                 ]>
 
 <book>
@@ -116,6 +117,7 @@ This one is short
         &MultiTenancy;
         &JAAS;
     </chapter>
+    &ClientRegistration;
 
     &IdentityBroker;
     &Themes;
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/admin-permissions.xml b/docbook/auth-server-docs/reference/en/en-US/modules/admin-permissions.xml
index 833d390..678c6d1 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/admin-permissions.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/admin-permissions.xml
@@ -64,6 +64,9 @@
                     <literal>manage-applications</literal> - Create, modify and delete applications in the realm
                 </listitem>
                 <listitem>
+                    <literal>create-clients</literal> - Create clients in the realm
+                </listitem>
+                <listitem>
                     <literal>manage-clients</literal> - Create, modify and delete clients in the realm
                 </listitem>
                 <listitem>
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml b/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml
new file mode 100755
index 0000000..0070cc0
--- /dev/null
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml
@@ -0,0 +1,202 @@
+<chapter id="client-registration">
+    <title>Client Registration</title>
+
+    <para>
+        In order for an application or service to utilize Keycloak it has to register a client in Keycloak. An
+        admin can do this through the admin console (or admin REST endpoints), but clients can also register themselves
+        through Keycloak's client registration service.
+    </para>
+
+    <para>
+        The Client Registration Service provides built-in support for Keycloak Client Representations, OpenID Connect
+        Client Meta Data and SAML Entity Descriptors. It's also possible to plugin custom client registration providers
+        if required. The Client Registration Service endpoint is <literal>&lt;KEYCLOAK URL&gt;/clients/&lt;provider&gt;</literal>.
+    </para>
+    <para>
+        The built-in supported <literal>providers</literal> are:
+        <itemizedlist>
+            <listitem><literal>default</literal> Keycloak Representations</listitem>
+            <listitem><literal>install</literal> Keycloak Adapter Configuration</listitem>
+            <!--<listitem><literal>openid-connect</literal> OpenID Connect Dynamic Client Registration</listitem>-->
+            <!--<listitem><literal>saml-ed</literal> SAML Entity Descriptors</listitem>-->
+        </itemizedlist>
+        The following sections will describe how to use the different providers.
+    </para>
+
+    <section>
+        <title>Authentication</title>
+        <para>
+            To invoke the Client Registration Services you need a token. The token can be a standard bearer token, a
+            initial access token or a registration access token.
+        </para>
+
+        <section>
+            <title>Bearer Token</title>
+            <para>
+                The bearertoken can be issued on behalf of a user or a Service Account. The following permissions are required
+                to invoke the endpoints (see <link linkend='admin-permissions'>Admin Permissions</link> for more details):
+                <itemizedlist>
+                    <listitem>
+                        <literal>create-client</literal> or <literal>manage-client</literal> - To create clients
+                    </listitem>
+                    <listitem>
+                        <literal>view-client</literal> or <literal>manage-client</literal> - To view clients
+                    </listitem>
+                    <listitem>
+                        <literal>manage-client</literal> - To update or delete clients
+                    </listitem>
+                </itemizedlist>
+                If you are using a regular bearer token to create clients we recommend using a token from on behalf of a
+                Service Account with only the <literal>create-client</literal> role. See the
+                <link linkend="service-accounts">Service Account</link> section for more details.
+            </para>
+        </section>
+
+        <section>
+            <title>Initial Access Token</title>
+            <para>
+                The best approach to create new clients is by using initial access tokens. An initial access token can
+                only be used to create clients and has a configurable expiration as well as a configurable limit on
+                how many clients can be created.
+            </para>
+            <para>
+                An initial access token can be created through the admin console. To create a new initial access token
+                first select the realm in the admin console, then click on <literal>Realm Settings</literal> in the menu
+                on the left, followed by <literal>Initial Access Tokens</literal> in the tabs displayed in the page.
+            </para>
+            <para>
+                You will now be able to see any existing initial access tokens. If you have access you can delete tokens
+                that are no longer required. You can only retrieve the value of the token when you are creating it. To
+                create a new token click on <literal>Create</literal>. You can now optionally add how long the token
+                should be valid, also how many clients can be created using the token. After you click on <literal>Save</literal>
+                the token value is displayed. It is important that you copy/paste this token now as you won't be able
+                to retrieve it later. If you forget to copy/paste it, then delete the token and create another one.
+                The token value is used as a standard bearer token when invoking the Client Registration Services, by
+                adding it to the Authorization header in the request. For example:
+<programlisting><![CDATA[
+Authorization: bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJmMjJmNzQyYy04ZjNlLTQ2M....
+]]></programlisting>
+            </para>
+        </section>
+
+        <section>
+            <title>Registration Access Token</title>
+            <para>
+                When you create a client through the Client Registration Service the response will include a registration
+                access token. The registration access token provides access to retrieve the client configuration later, but
+                also to update or delete the client. The registration access token is included with the request in the
+                same way as a bearer token or initial access token. Registration access tokens are only valid once
+                when it's used the response will include a new token.
+            </para>
+            <para>
+                If a client was created outside of the Client Registration Service it won't have a registration access
+                token associated with it. You can create one through the admin console. This can also be useful if
+                you loose the token for a particular client. To create a new token find the client in the admin console
+                and click on <literal>Credentials</literal>. Then click on <literal>Generate registration access token</literal>.
+            </para>
+        </section>
+    </section>
+
+    <section>
+        <title>Keycloak Representations</title>
+        <para>
+            The <literal>default</literal> client registration provider can be used to create, retrieve, update and delete a client. It uses
+            Keycloaks Client Representation format which provides support for configuring clients exactly as they can
+            be configured through the admin console, including for example configuring protocol mappers.
+        </para>
+        <para>
+            To create a client create a Client Representation (JSON) then do a HTTP POST to:
+            <literal>&lt;KEYCLOAK URL&gt;/clients/&lt;provider&gt;/default</literal>. It will return a Client Representation
+            that also includes the registration access token. You should save the registration access token somewhere
+            if you want to retrieve the config, update or delete the client later.
+        </para>
+        <para>
+            To retrieve the Client Representation then do a HTTP GET to:
+            <literal>&lt;KEYCLOAK URL&gt;/clients/&lt;provider&gt;/default/&lt;client id&gt;</literal>. It will also
+            return a new registration access token.
+        </para>
+        <para>
+            To update the Client Representation then do a HTTP PUT to with the updated Client Representation to:
+            <literal>&lt;KEYCLOAK URL&gt;/clients/&lt;provider&gt;/default/&lt;client id&gt;</literal>. It will also
+            return a new registration access token.
+        </para>
+        <para>
+            To delete the Client Representation then do a HTTP DELETE to:
+            <literal>&lt;KEYCLOAK URL&gt;/clients/&lt;provider&gt;/default/&lt;client id&gt;</literal>
+        </para>
+    </section>
+
+    <section>
+        <title>Keycloak Adapter Configuration</title>
+        <para>
+            The <default>installation</default> client registration provider can be used to retrieve the adapter configuration
+            for a client. In addition to token authentication you can also authenticate with client credentials using
+            HTTP basic authentication. To do this include the following header in the request:
+<programlisting><![CDATA[
+Authorization: basic BASE64(client-id + ':' + client-secret)
+]]></programlisting>
+        </para>
+        <para>
+            To retrieve the Adapter Configuration then do a HTTP GET to:
+            <literal>&lt;KEYCLOAK URL&gt;/clients/&lt;provider&gt;/installation/&lt;client id&gt;</literal>
+        </para>
+        <para>
+            No authentication is required for public clients. This means that for the JavaScript adapter you can
+            load the client configuration directly from Keycloak using the above URL.
+        </para>
+    </section>
+
+    <!--
+    <section>
+        <title>OpenID Connect Dynamic Client Registration</title>
+        <para>
+            TODO
+        </para>
+    </section>
+    -->
+
+    <!--
+    <section>
+        <title>SAML Entity Descriptors</title>
+        <para>
+            TODO
+        </para>
+    </section>
+    -->
+
+    <section>
+        <title>Client Registration Java API</title>
+        <para>
+            The Client Registration Java API makes it easy to use the Client Registration Service using Java. To use
+            include the dependency <literal>org.keycloak:keycloak-client-registration-api:&gt;VERSION&lt;</literal> from
+            Maven.
+        </para>
+        <para>
+            For full instructions on using the Client Registration refer to the JavaDocs. Below is an example of creating
+            a client:
+<programlisting><![CDATA[
+String initialAccessToken = "eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJmMjJmNzQyYy04ZjNlLTQ2M....";
+
+ClientRepresentation client = new ClientRepresentation();
+client.setClientId(CLIENT_ID);
+
+ClientRegistration reg = ClientRegistration.create().url("http://keycloak/auth/realms/myrealm").build();
+reg.auth(initialAccessToken);
+
+client = reg.create(client);
+
+String registrationAccessToken = client.getRegistrationAccessToken();
+]]></programlisting>
+        </para>
+    </section>
+
+    <!--
+    <section>
+        <title>Client Registration CLI</title>
+        <para>
+            TODO
+        </para>
+    </section>
+    -->
+
+</chapter>
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 c819259..d89f47b 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
@@ -339,7 +339,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
         // Remove expired client initial access
         map = new MapReduceTask(sessionCache)
-                .mappedWith(ClientInitialAccessMapper.create(realm.getId()).time(Time.currentTime()).remainingCount(0).emitKey())
+                .mappedWith(ClientInitialAccessMapper.create(realm.getId()).expired(Time.currentTime()).emitKey())
                 .reducedWith(new FirstResultReducer())
                 .execute();
 
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
index 87c9b3c..98dab2f 100644
--- 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
@@ -24,8 +24,7 @@ public class ClientInitialAccessMapper implements Mapper<String, SessionEntity, 
 
     private EmitValue emit = EmitValue.ENTITY;
 
-    private Integer time;
-    private Integer remainingCount;
+    private Integer expired;
 
     public static ClientInitialAccessMapper create(String realm) {
         return new ClientInitialAccessMapper(realm);
@@ -36,14 +35,8 @@ public class ClientInitialAccessMapper implements Mapper<String, SessionEntity, 
         return this;
     }
 
-    public ClientInitialAccessMapper time(int time) {
-        this.time = time;
-        return this;
-    }
-
-
-    public ClientInitialAccessMapper remainingCount(int remainingCount) {
-        this.remainingCount = remainingCount;
+    public ClientInitialAccessMapper expired(int time) {
+        this.expired = time;
         return this;
     }
 
@@ -59,21 +52,27 @@ public class ClientInitialAccessMapper implements Mapper<String, SessionEntity, 
 
         ClientInitialAccessEntity entity = (ClientInitialAccessEntity) e;
 
-        if (time != null && entity.getExpiration() > 0 && (entity.getTimestamp() + entity.getExpiration()) < time) {
-            return;
-        }
+        boolean include = false;
 
-        if (remainingCount != null && entity.getRemainingCount() == remainingCount) {
-            return;
+        if (expired != null) {
+            if (entity.getRemainingCount() <= 0) {
+                include = true;
+            } else if (entity.getExpiration() > 0 && (entity.getTimestamp() + entity.getExpiration()) < expired) {
+                include = true;
+            }
+        } else {
+            include = true;
         }
 
-        switch (emit) {
-            case KEY:
-                collector.emit(key, key);
-                break;
-            case ENTITY:
-                collector.emit(key, entity);
-                break;
+        if (include) {
+            switch (emit) {
+                case KEY:
+                    collector.emit(key, key);
+                    break;
+                case ENTITY:
+                    collector.emit(key, entity);
+                    break;
+            }
         }
     }
 
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
index 7cd3342..241bcff 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
@@ -37,7 +37,7 @@ public class ClientRegistrationTokenUtils {
     }
 
     public static String createInitialAccessToken(RealmModel realm, UriInfo uri, ClientInitialAccessModel model) {
-        return createToken(realm, uri, model.getId(), TYPE_INITIAL_ACCESS_TOKEN, model.getTimestamp() + model.getExpiration());
+        return createToken(realm, uri, model.getId(), TYPE_INITIAL_ACCESS_TOKEN, model.getExpiration() > 0 ? model.getTimestamp() + model.getExpiration() : 0);
     }
 
     public static JsonWebToken parseToken(RealmModel realm, UriInfo uri, String token) {
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 8535d46..6278653 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
@@ -29,7 +29,7 @@ public abstract class AbstractClientRegistrationTest extends AbstractKeycloakTes
 
     @Before
     public void before() throws Exception {
-        reg = new ClientRegistration(testContext.getAuthServerContextRoot() + "/auth", "test");
+        reg = ClientRegistration.create().url(testContext.getAuthServerContextRoot() + "/auth", "test").build();
     }
 
     @After
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
index 0ef75dd..6698b5e 100644
--- 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
@@ -26,13 +26,15 @@ public class InitialAccessTokenTest extends AbstractClientRegistrationTest {
     }
 
     @Test
-    public void create() throws ClientRegistrationException {
+    public void create() throws ClientRegistrationException, InterruptedException {
         ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation());
 
         reg.auth(Auth.token(response));
 
         ClientRepresentation rep = new ClientRepresentation();
 
+        Thread.sleep(2);
+
         ClientRepresentation created = reg.create(rep);
         Assert.assertNotNull(created);