KEYCLOAK-1749 Add documentation and fixed clean-up of expired initial access tokens
This commit is contained in:
parent
41c9289f14
commit
7e8c80c0df
15 changed files with 502 additions and 38 deletions
|
@ -25,12 +25,12 @@ public class ClientRegistration {
|
||||||
|
|
||||||
private HttpUtil httpUtil;
|
private HttpUtil httpUtil;
|
||||||
|
|
||||||
public ClientRegistration(String authServerUrl, String realm) {
|
public static ClientRegistrationBuilder create() {
|
||||||
httpUtil = new HttpUtil(HttpClients.createDefault(), HttpUtil.getUrl(authServerUrl, "realms", realm, "clients"));
|
return new ClientRegistrationBuilder();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClientRegistration(String authServerUrl, String realm, HttpClient httpClient) {
|
ClientRegistration(HttpUtil httpUtil) {
|
||||||
httpUtil = new HttpUtil(httpClient, HttpUtil.getUrl(authServerUrl, "realms", realm, "clients"));
|
this.httpUtil = httpUtil;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() throws ClientRegistrationException {
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,68 @@
|
||||||
package org.keycloak.client.registration.cli;
|
package org.keycloak.client.registration.cli;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.parser.CommandLineParserException;
|
||||||
import org.jboss.aesh.console.AeshConsole;
|
import org.jboss.aesh.console.AeshConsole;
|
||||||
import org.jboss.aesh.console.AeshConsoleBuilder;
|
import org.jboss.aesh.console.AeshConsoleBuilder;
|
||||||
import org.jboss.aesh.console.Prompt;
|
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.Settings;
|
||||||
import org.jboss.aesh.console.settings.SettingsBuilder;
|
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>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class ClientRegistrationCLI {
|
public class ClientRegistrationCLI {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
private static ClientRegistration reg;
|
||||||
|
|
||||||
Settings settings = new SettingsBuilder().logging(true).create();
|
public static void main(String[] args) throws CommandLineParserException, CommandNotFoundException {
|
||||||
AeshConsole aeshConsole = new AeshConsoleBuilder().settings(settings)
|
reg = ClientRegistration.create().url("http://localhost:8080/auth/realms/master").build();
|
||||||
.prompt(new Prompt("[aesh@rules]$ "))
|
reg.auth(Auth.token("..."));
|
||||||
// .command()
|
|
||||||
|
Context context = new Context();
|
||||||
|
|
||||||
|
List<Command> 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();
|
.create();
|
||||||
|
|
||||||
aeshConsole.start();
|
aeshConsole.start();
|
||||||
|
/*
|
||||||
|
if (args.length > 0) {
|
||||||
|
CommandContainer command = registry.getCommand(args[0], null);
|
||||||
|
ParserGenerator.parseAndPopulate(command, args[0], Arrays.copyOfRange(args, 1, args.length));
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,6 +14,6 @@
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>api</module>
|
<module>api</module>
|
||||||
<module>cli</module>
|
<!--<module>cli</module>-->
|
||||||
</modules>
|
</modules>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
<!ENTITY Recaptcha SYSTEM "modules/recaptcha.xml">
|
<!ENTITY Recaptcha SYSTEM "modules/recaptcha.xml">
|
||||||
<!ENTITY AuthSPI SYSTEM "modules/auth-spi.xml">
|
<!ENTITY AuthSPI SYSTEM "modules/auth-spi.xml">
|
||||||
<!ENTITY FilterAdapter SYSTEM "modules/servlet-filter-adapter.xml">
|
<!ENTITY FilterAdapter SYSTEM "modules/servlet-filter-adapter.xml">
|
||||||
|
<!ENTITY ClientRegistration SYSTEM "modules/client-registration.xml">
|
||||||
]>
|
]>
|
||||||
|
|
||||||
<book>
|
<book>
|
||||||
|
@ -116,6 +117,7 @@ This one is short
|
||||||
&MultiTenancy;
|
&MultiTenancy;
|
||||||
&JAAS;
|
&JAAS;
|
||||||
</chapter>
|
</chapter>
|
||||||
|
&ClientRegistration;
|
||||||
|
|
||||||
&IdentityBroker;
|
&IdentityBroker;
|
||||||
&Themes;
|
&Themes;
|
||||||
|
|
|
@ -63,6 +63,9 @@
|
||||||
<listitem>
|
<listitem>
|
||||||
<literal>manage-applications</literal> - Create, modify and delete applications in the realm
|
<literal>manage-applications</literal> - Create, modify and delete applications in the realm
|
||||||
</listitem>
|
</listitem>
|
||||||
|
<listitem>
|
||||||
|
<literal>create-clients</literal> - Create clients in the realm
|
||||||
|
</listitem>
|
||||||
<listitem>
|
<listitem>
|
||||||
<literal>manage-clients</literal> - Create, modify and delete clients in the realm
|
<literal>manage-clients</literal> - Create, modify and delete clients in the realm
|
||||||
</listitem>
|
</listitem>
|
||||||
|
|
202
docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml
Executable file
202
docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml
Executable file
|
@ -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><KEYCLOAK URL>/clients/<provider></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><KEYCLOAK URL>/clients/<provider>/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><KEYCLOAK URL>/clients/<provider>/default/<client id></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><KEYCLOAK URL>/clients/<provider>/default/<client id></literal>. It will also
|
||||||
|
return a new registration access token.
|
||||||
|
</para>
|
||||||
|
<para>
|
||||||
|
To delete the Client Representation then do a HTTP DELETE to:
|
||||||
|
<literal><KEYCLOAK URL>/clients/<provider>/default/<client id></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><KEYCLOAK URL>/clients/<provider>/installation/<client id></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:>VERSION<</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>
|
|
@ -339,7 +339,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
|
|
||||||
// Remove expired client initial access
|
// Remove expired client initial access
|
||||||
map = new MapReduceTask(sessionCache)
|
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())
|
.reducedWith(new FirstResultReducer())
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,7 @@ public class ClientInitialAccessMapper implements Mapper<String, SessionEntity,
|
||||||
|
|
||||||
private EmitValue emit = EmitValue.ENTITY;
|
private EmitValue emit = EmitValue.ENTITY;
|
||||||
|
|
||||||
private Integer time;
|
private Integer expired;
|
||||||
private Integer remainingCount;
|
|
||||||
|
|
||||||
public static ClientInitialAccessMapper create(String realm) {
|
public static ClientInitialAccessMapper create(String realm) {
|
||||||
return new ClientInitialAccessMapper(realm);
|
return new ClientInitialAccessMapper(realm);
|
||||||
|
@ -36,14 +35,8 @@ public class ClientInitialAccessMapper implements Mapper<String, SessionEntity,
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClientInitialAccessMapper time(int time) {
|
public ClientInitialAccessMapper expired(int time) {
|
||||||
this.time = time;
|
this.expired = time;
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public ClientInitialAccessMapper remainingCount(int remainingCount) {
|
|
||||||
this.remainingCount = remainingCount;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,21 +52,27 @@ public class ClientInitialAccessMapper implements Mapper<String, SessionEntity,
|
||||||
|
|
||||||
ClientInitialAccessEntity entity = (ClientInitialAccessEntity) e;
|
ClientInitialAccessEntity entity = (ClientInitialAccessEntity) e;
|
||||||
|
|
||||||
if (time != null && entity.getExpiration() > 0 && (entity.getTimestamp() + entity.getExpiration()) < time) {
|
boolean include = false;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remainingCount != null && entity.getRemainingCount() == remainingCount) {
|
if (include) {
|
||||||
return;
|
switch (emit) {
|
||||||
}
|
case KEY:
|
||||||
|
collector.emit(key, key);
|
||||||
switch (emit) {
|
break;
|
||||||
case KEY:
|
case ENTITY:
|
||||||
collector.emit(key, key);
|
collector.emit(key, entity);
|
||||||
break;
|
break;
|
||||||
case ENTITY:
|
}
|
||||||
collector.emit(key, entity);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ public class ClientRegistrationTokenUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String createInitialAccessToken(RealmModel realm, UriInfo uri, ClientInitialAccessModel model) {
|
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) {
|
public static JsonWebToken parseToken(RealmModel realm, UriInfo uri, String token) {
|
||||||
|
|
|
@ -29,7 +29,7 @@ public abstract class AbstractClientRegistrationTest extends AbstractKeycloakTes
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void before() throws Exception {
|
public void before() throws Exception {
|
||||||
reg = new ClientRegistration(testContext.getAuthServerContextRoot() + "/auth", "test");
|
reg = ClientRegistration.create().url(testContext.getAuthServerContextRoot() + "/auth", "test").build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
|
|
@ -26,13 +26,15 @@ public class InitialAccessTokenTest extends AbstractClientRegistrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void create() throws ClientRegistrationException {
|
public void create() throws ClientRegistrationException, InterruptedException {
|
||||||
ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation());
|
ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation());
|
||||||
|
|
||||||
reg.auth(Auth.token(response));
|
reg.auth(Auth.token(response));
|
||||||
|
|
||||||
ClientRepresentation rep = new ClientRepresentation();
|
ClientRepresentation rep = new ClientRepresentation();
|
||||||
|
|
||||||
|
Thread.sleep(2);
|
||||||
|
|
||||||
ClientRepresentation created = reg.create(rep);
|
ClientRepresentation created = reg.create(rep);
|
||||||
Assert.assertNotNull(created);
|
Assert.assertNotNull(created);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue