From 7e8c80c0df388fd58986317cca9c3e66706c6ff0 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Thu, 19 Nov 2015 16:06:35 +0100 Subject: [PATCH] KEYCLOAK-1749 Add documentation and fixed clean-up of expired initial access tokens --- .../registration/ClientRegistration.java | 45 +++- .../cli/ClientRegistrationCLI.java | 53 ++++- .../client/registration/cli/Context.java | 37 ++++ .../cli/commands/CreateCommand.java | 64 ++++++ .../cli/commands/ExitCommand.java | 29 +++ .../cli/commands/SetupCommand.java | 48 +++++ client-registration/pom.xml | 2 +- .../reference/en/en-US/master.xml | 2 + .../en/en-US/modules/admin-permissions.xml | 3 + .../en/en-US/modules/client-registration.xml | 202 ++++++++++++++++++ .../InfinispanUserSessionProvider.java | 2 +- .../mapreduce/ClientInitialAccessMapper.java | 45 ++-- .../ClientRegistrationTokenUtils.java | 2 +- .../AbstractClientRegistrationTest.java | 2 +- .../client/InitialAccessTokenTest.java | 4 +- 15 files changed, 502 insertions(+), 38 deletions(-) create mode 100644 client-registration/cli/src/main/java/org/keycloak/client/registration/cli/Context.java create mode 100644 client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCommand.java create mode 100644 client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/ExitCommand.java create mode 100644 client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/SetupCommand.java create mode 100755 docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml 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 3be763f9d9..2b1a991c6f 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 53c0b88445..f0b0857760 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 Stian Thorgersen */ public class ClientRegistrationCLI { - public static void main(String[] args) { + private static ClientRegistration reg; - Settings settings = new SettingsBuilder().logging(true).create(); - AeshConsole aeshConsole = new AeshConsoleBuilder().settings(settings) - .prompt(new Prompt("[aesh@rules]$ ")) -// .command() + 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 commands = new LinkedList<>(); + commands.add(new SetupCommand(context)); + commands.add(new CreateCommand(context)); + commands.add(new ExitCommand(context)); + + 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/Context.java b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/Context.java new file mode 100644 index 0000000000..49d8fb90ac --- /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 Stian Thorgersen + */ +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 readJson(InputStream bytes, Class type) throws IOException { + return mapper.readValue(bytes, type); + } + +} 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 0000000000..280534b1db --- /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 Stian Thorgersen + */ +@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 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 0000000000..507881bd25 --- /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 Stian Thorgersen + */ +@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 0000000000..26579abd4e --- /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 Stian Thorgersen + */ +@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/pom.xml b/client-registration/pom.xml index 214122831b..9d1ea9f5a9 100755 --- a/client-registration/pom.xml +++ b/client-registration/pom.xml @@ -14,6 +14,6 @@ api - cli + 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 2af744f8e9..95ff5eeaba 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 @@ + ]> @@ -116,6 +117,7 @@ This one is short &MultiTenancy; &JAAS; + &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 833d3901bf..678c6d13f7 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 @@ -63,6 +63,9 @@ manage-applications - Create, modify and delete applications in the realm + + create-clients - Create clients in the realm + manage-clients - Create, modify and delete clients in the realm 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 0000000000..0070cc004b --- /dev/null +++ b/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml @@ -0,0 +1,202 @@ + + Client Registration + + + 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. + + + + The Client Registration Service provides built-in support for Keycloak Client Representations, OpenID Connect + Client Meta Data and SAML Entity Descriptors. It's also possible to plugin custom client registration providers + if required. The Client Registration Service endpoint is <KEYCLOAK URL>/clients/<provider>. + + + The built-in supported providers are: + + default Keycloak Representations + install Keycloak Adapter Configuration + + + + The following sections will describe how to use the different providers. + + +
+ Authentication + + 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. + + +
+ Bearer Token + + The bearertoken can be issued on behalf of a user or a Service Account. The following permissions are required + to invoke the endpoints (see Admin Permissions for more details): + + + create-client or manage-client - To create clients + + + view-client or manage-client - To view clients + + + manage-client - To update or delete clients + + + 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 create-client role. See the + Service Account section for more details. + +
+ +
+ Initial Access Token + + 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. + + + 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 Realm Settings in the menu + on the left, followed by Initial Access Tokens in the tabs displayed in the page. + + + 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 Create. 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 Save + 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: + + +
+ +
+ Registration Access Token + + 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. + + + 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 Credentials. Then click on Generate registration access token. + +
+
+ +
+ Keycloak Representations + + The default 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. + + + To create a client create a Client Representation (JSON) then do a HTTP POST to: + <KEYCLOAK URL>/clients/<provider>/default. It will return a Client Representation + that also includes the registration access token. You should save the registration access token somewhere + if you want to retrieve the config, update or delete the client later. + + + To retrieve the Client Representation then do a HTTP GET to: + <KEYCLOAK URL>/clients/<provider>/default/<client id>. It will also + return a new registration access token. + + + To update the Client Representation then do a HTTP PUT to with the updated Client Representation to: + <KEYCLOAK URL>/clients/<provider>/default/<client id>. It will also + return a new registration access token. + + + To delete the Client Representation then do a HTTP DELETE to: + <KEYCLOAK URL>/clients/<provider>/default/<client id> + +
+ +
+ Keycloak Adapter Configuration + + The installation client registration provider can be used to retrieve the adapter configuration + for a client. In addition to token authentication you can also authenticate with client credentials using + HTTP basic authentication. To do this include the following header in the request: + + + + To retrieve the Adapter Configuration then do a HTTP GET to: + <KEYCLOAK URL>/clients/<provider>/installation/<client id> + + + 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. + +
+ + + + + +
+ Client Registration Java API + + The Client Registration Java API makes it easy to use the Client Registration Service using Java. To use + include the dependency org.keycloak:keycloak-client-registration-api:>VERSION< from + Maven. + + + For full instructions on using the Client Registration refer to the JavaDocs. Below is an example of creating + a client: + + +
+ + + +
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 c819259849..d89f47b023 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 87c9b3c24a..98dab2fbbd 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 0 && (entity.getTimestamp() + entity.getExpiration()) < time) { - return; + boolean include = false; + + if (expired != null) { + if (entity.getRemainingCount() <= 0) { + include = true; + } else if (entity.getExpiration() > 0 && (entity.getTimestamp() + entity.getExpiration()) < expired) { + include = true; + } + } else { + include = true; } - if (remainingCount != null && entity.getRemainingCount() == remainingCount) { - return; - } - - 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 7cd334236e..241bcff57e 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 8535d463f9..627865353f 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 0ef75dd4cf..6698b5e274 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);