KEYCLOAK-1749 Client registration service and client java api

This commit is contained in:
Stian Thorgersen 2015-09-30 13:13:17 +02:00
parent 7fec1677ee
commit 366a1629e5
21 changed files with 910 additions and 41 deletions

31
client-api/pom.xml Executable file
View File

@ -0,0 +1,31 @@
<?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-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.6.0.Final-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-client-api</artifactId>
<name>Keycloak Client API</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,275 @@
package org.keycloak.client.registration;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.util.Base64;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.io.InputStream;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientRegistration {
private String clientRegistrationUrl;
private HttpClient httpClient;
private Auth auth;
public static ClientRegistrationBuilder create() {
return new ClientRegistrationBuilder();
}
private ClientRegistration() {
}
public ClientRepresentation create(ClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client);
InputStream resultStream = doPost(content);
return deserialize(resultStream, ClientRepresentation.class);
}
public ClientRepresentation get() throws ClientRegistrationException {
if (auth instanceof ClientIdSecretAuth) {
String clientId = ((ClientIdSecretAuth) auth).clientId;
return get(clientId);
} else {
throw new ClientRegistrationException("Requires client authentication");
}
}
public ClientRepresentation get(String clientId) throws ClientRegistrationException {
InputStream resultStream = doGet(clientId);
return resultStream != null ? deserialize(resultStream, ClientRepresentation.class) : null;
}
public void update(ClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client);
doPut(content, client.getClientId());
}
public void delete() throws ClientRegistrationException {
if (auth instanceof ClientIdSecretAuth) {
String clientId = ((ClientIdSecretAuth) auth).clientId;
delete(clientId);
} else {
throw new ClientRegistrationException("Requires client authentication");
}
}
public void delete(String clientId) throws ClientRegistrationException {
doDelete(clientId);
}
public void close() throws ClientRegistrationException {
if (httpClient instanceof CloseableHttpClient) {
try {
((CloseableHttpClient) httpClient).close();
} catch (IOException e) {
throw new ClientRegistrationException("Failed to close http client", e);
}
}
}
private InputStream doPost(String content) throws ClientRegistrationException {
try {
HttpPost request = new HttpPost(clientRegistrationUrl);
request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
request.setHeader(HttpHeaders.ACCEPT, "application/json");
request.setEntity(new StringEntity(content));
auth.addAuth(request);
HttpResponse response = httpClient.execute(request);
InputStream responseStream = null;
if (response.getEntity() != null) {
responseStream = response.getEntity().getContent();
}
if (response.getStatusLine().getStatusCode() == 201) {
return responseStream;
} else {
responseStream.close();
throw new HttpErrorException(response.getStatusLine());
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
}
}
private InputStream doGet(String endpoint) throws ClientRegistrationException {
try {
HttpGet request = new HttpGet(clientRegistrationUrl + "/" + endpoint);
request.setHeader(HttpHeaders.ACCEPT, "application/json");
auth.addAuth(request);
HttpResponse response = httpClient.execute(request);
InputStream responseStream = null;
if (response.getEntity() != null) {
responseStream = response.getEntity().getContent();
}
if (response.getStatusLine().getStatusCode() == 200) {
return responseStream;
} else if (response.getStatusLine().getStatusCode() == 404) {
responseStream.close();
return null;
} else {
responseStream.close();
throw new HttpErrorException(response.getStatusLine());
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
}
}
private void doPut(String content, String endpoint) throws ClientRegistrationException {
try {
HttpPut request = new HttpPut(clientRegistrationUrl + "/" + endpoint);
request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
request.setHeader(HttpHeaders.ACCEPT, "application/json");
request.setEntity(new StringEntity(content));
auth.addAuth(request);
HttpResponse response = httpClient.execute(request);
if (response.getEntity() != null) {
response.getEntity().getContent().close();
}
if (response.getStatusLine().getStatusCode() != 200) {
throw new HttpErrorException(response.getStatusLine());
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
}
}
private void doDelete(String endpoint) throws ClientRegistrationException {
try {
HttpDelete request = new HttpDelete(clientRegistrationUrl + "/" + endpoint);
auth.addAuth(request);
HttpResponse response = httpClient.execute(request);
if (response.getEntity() != null) {
response.getEntity().getContent().close();
}
if (response.getStatusLine().getStatusCode() != 200) {
throw new HttpErrorException(response.getStatusLine());
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
}
}
private String serialize(ClientRepresentation client) throws ClientRegistrationException {
try {
return JsonSerialization.writeValueAsString(client);
} catch (IOException e) {
throw new ClientRegistrationException("Failed to write json object", e);
}
}
private <T> T deserialize(InputStream inputStream, Class<T> clazz) throws ClientRegistrationException {
try {
return JsonSerialization.readValue(inputStream, clazz);
} catch (IOException e) {
throw new ClientRegistrationException("Failed to read json object", e);
}
}
public static class ClientRegistrationBuilder {
private String realm;
private String authServerUrl;
private Auth auth;
private HttpClient httpClient;
public ClientRegistrationBuilder realm(String realm) {
this.realm = realm;
return this;
}
public ClientRegistrationBuilder authServerUrl(String authServerUrl) {
this.authServerUrl = authServerUrl;
return this;
}
public ClientRegistrationBuilder auth(String token) {
this.auth = new TokenAuth(token);
return this;
}
public ClientRegistrationBuilder auth(String clientId, String clientSecret) {
this.auth = new ClientIdSecretAuth(clientId, clientSecret);
return this;
}
public ClientRegistrationBuilder httpClient(HttpClient httpClient) {
this.httpClient = httpClient;
return this;
}
public ClientRegistration build() {
ClientRegistration clientRegistration = new ClientRegistration();
clientRegistration.clientRegistrationUrl = authServerUrl + "/realms/" + realm + "/client-registration";
clientRegistration.httpClient = httpClient != null ? httpClient : HttpClients.createDefault();
clientRegistration.auth = auth;
return clientRegistration;
}
}
public interface Auth {
void addAuth(HttpRequest httpRequest);
}
public static class AuthorizationHeaderAuth implements Auth {
private String credentials;
public AuthorizationHeaderAuth(String credentials) {
this.credentials = credentials;
}
public void addAuth(HttpRequest httpRequest) {
httpRequest.setHeader(HttpHeaders.AUTHORIZATION, credentials);
}
}
public static class TokenAuth extends AuthorizationHeaderAuth {
public TokenAuth(String token) {
super("Bearer " + token);
}
}
public static class ClientIdSecretAuth extends AuthorizationHeaderAuth {
private String clientId;
public ClientIdSecretAuth(String clientId, String clientSecret) {
super("Basic " + Base64.encodeBytes((clientId + ":" + clientSecret).getBytes()));
this.clientId = clientId;
}
}
}

View File

@ -0,0 +1,16 @@
package org.keycloak.client.registration;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientRegistrationException extends Exception {
public ClientRegistrationException(String s, Throwable throwable) {
super(s, throwable);
}
public ClientRegistrationException(String s) {
super(s);
}
}

View File

@ -0,0 +1,22 @@
package org.keycloak.client.registration;
import org.apache.http.StatusLine;
import java.io.IOException;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class HttpErrorException extends IOException {
private StatusLine statusLine;
public HttpErrorException(StatusLine statusLine) {
this.statusLine = statusLine;
}
public StatusLine getStatusLine() {
return statusLine;
}
}

View File

@ -1,14 +1,12 @@
package org.keycloak.representations.idm;
import java.util.ArrayList;
import org.codehaus.jackson.annotate.JsonIgnore;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.codehaus.jackson.annotate.JsonIgnore;
import org.keycloak.util.MultivaluedHashMap;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $

View File

@ -69,7 +69,16 @@ public enum EventType {
IMPERSONATE(true),
CUSTOM_REQUIRED_ACTION(true),
CUSTOM_REQUIRED_ACTION_ERROR(true),
EXECUTE_ACTIONS(true);
EXECUTE_ACTIONS(true),
CLIENT_INFO(false),
CLIENT_INFO_ERROR(false),
CLIENT_REGISTER(true),
CLIENT_REGISTER_ERROR(true),
CLIENT_UPDATE(true),
CLIENT_UPDATE_ERROR(true),
CLIENT_DELETE(true),
CLIENT_DELETE_ERROR(true);
private boolean saveByDefault;

View File

@ -89,6 +89,7 @@ personalInfo=Personal Info:
role_admin=Admin
role_realm-admin=Realm Admin
role_create-realm=Create realm
role_create-client=Create client
role_view-realm=View realm
role_view-users=View users
role_view-applications=View applications

View File

@ -70,6 +70,15 @@ public class MigrateTo1_6_0 {
if ((adminConsoleClient != null) && !localeMapperAdded(adminConsoleClient)) {
adminConsoleClient.addProtocolMapper(localeMapper);
}
ClientModel client = realm.getMasterAdminClient();
if (client.getRole(AdminRoles.CREATE_CLIENT) == null) {
RoleModel role = client.addRole(AdminRoles.CREATE_CLIENT);
role.setDescription("${role_" + AdminRoles.CREATE_CLIENT + "}");
role.setScopeParamRequired(false);
realm.getRole(AdminRoles.ADMIN).addCompositeRole(role);
}
}
}

View File

@ -13,6 +13,7 @@ public class AdminRoles {
public static String REALM_ADMIN = "realm-admin";
public static String CREATE_REALM = "create-realm";
public static String CREATE_CLIENT = "create-client";
public static String VIEW_REALM = "view-realm";
public static String VIEW_USERS = "view-users";
@ -26,6 +27,6 @@ public class AdminRoles {
public static String MANAGE_CLIENTS = "manage-clients";
public static String MANAGE_EVENTS = "manage-events";
public static String[] ALL_REALM_ROLES = {VIEW_REALM, VIEW_USERS, VIEW_CLIENTS, VIEW_EVENTS, VIEW_IDENTITY_PROVIDERS, MANAGE_REALM, MANAGE_USERS, MANAGE_CLIENTS, MANAGE_EVENTS, MANAGE_IDENTITY_PROVIDERS};
public static String[] ALL_REALM_ROLES = {CREATE_CLIENT, VIEW_REALM, VIEW_USERS, VIEW_CLIENTS, VIEW_EVENTS, VIEW_IDENTITY_PROVIDERS, MANAGE_REALM, MANAGE_USERS, MANAGE_CLIENTS, MANAGE_EVENTS, MANAGE_IDENTITY_PROVIDERS};
}

View File

@ -137,6 +137,7 @@
<module>common</module>
<module>core</module>
<module>core-jaxrs</module>
<module>client-api</module>
<module>connections</module>
<module>dependencies</module>
<module>events</module>
@ -650,6 +651,11 @@
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core-jaxrs</artifactId>

View File

@ -7,6 +7,7 @@ import java.util.List;
import java.util.Map;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@ -44,7 +45,11 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
String clientSecret = null;
String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
MediaType mediaType = context.getHttpRequest().getHttpHeaders().getMediaType();
boolean hasFormData = mediaType != null && mediaType.isCompatible(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
MultivaluedMap<String, String> formData = hasFormData ? context.getHttpRequest().getDecodedFormParameters() : null;
if (authorizationHeader != null) {
String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
@ -54,7 +59,7 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
} else {
// Don't send 401 if client_id parameter was sent in request. For example IE may automatically send "Authorization: Negotiate" in XHR requests even for public clients
if (!formData.containsKey(OAuth2Constants.CLIENT_ID)) {
if (formData != null && !formData.containsKey(OAuth2Constants.CLIENT_ID)) {
Response challengeResponse = Response.status(Response.Status.UNAUTHORIZED).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + context.getRealm().getName() + "\"").build();
context.challenge(challengeResponse);
return;
@ -62,7 +67,7 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
}
}
if (client_id == null) {
if (formData != null && client_id == null) {
client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
clientSecret = formData.getFirst("client_secret");
}

View File

@ -3,6 +3,7 @@ package org.keycloak.services.managers;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.ClientConnection;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@ -39,6 +40,11 @@ public class AppAuthManager extends AuthenticationManager {
return tokenString;
}
public AuthResult authenticateBearerToken(KeycloakSession session, RealmModel realm) {
KeycloakContext ctx = session.getContext();
return authenticateBearerToken(session, realm, ctx.getUri(), ctx.getConnection(), ctx.getRequestHeaders());
}
public AuthResult authenticateBearerToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
String tokenString = extractAuthorizationHeaderToken(headers);
if (tokenString == null) return null;

View File

@ -3,21 +3,27 @@ package org.keycloak.services.resources;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.exportimport.KeycloakClientDescriptionConverter;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.*;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ForbiddenException;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
@ -36,6 +42,8 @@ public class ClientRegistrationService {
@Context
private KeycloakSession session;
private AppAuthManager authManager = new AppAuthManager();
public ClientRegistrationService(RealmModel realm, EventBuilder event) {
this.realm = realm;
this.event = event;
@ -44,6 +52,10 @@ public class ClientRegistrationService {
@POST
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN })
public Response create(String description, @QueryParam("format") String format) {
event.event(EventType.CLIENT_REGISTER);
authenticate(true, null);
if (format == null) {
format = KeycloakClientDescriptionConverter.ID;
}
@ -58,6 +70,10 @@ public class ClientRegistrationService {
ClientModel clientModel = RepresentationToModel.createClient(session, realm, rep, true);
rep = ModelToRepresentation.toRepresentation(clientModel);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
logger.infov("Created client {0}", rep.getClientId());
event.client(rep.getClientId()).success();
return Response.created(uri).entity(rep).build();
} catch (ModelDuplicateException e) {
return ErrorResponse.exists("Client " + rep.getClientId() + " already exists");
@ -67,34 +83,79 @@ public class ClientRegistrationService {
@GET
@Path("{clientId}")
@Produces(MediaType.APPLICATION_JSON)
public ClientRepresentation get(@PathParam("clientId") String clientId) {
AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event, realm);
ClientModel client = clientAuth.getClient();
public Response get(@PathParam("clientId") String clientId) {
event.event(EventType.CLIENT_INFO);
ClientModel client = authenticate(false, clientId);
if (client == null) {
throw new NotFoundException("Client not found");
return Response.status(Response.Status.NOT_FOUND).build();
}
return ModelToRepresentation.toRepresentation(client);
return Response.ok(ModelToRepresentation.toRepresentation(client)).build();
}
@PUT
@Path("{clientId}")
@Consumes(MediaType.APPLICATION_JSON)
public void update(@PathParam("clientId") String clientId, ClientRepresentation rep) {
ClientModel client = realm.getClientByClientId(clientId);
if (client == null) {
throw new NotFoundException("Client not found");
}
public Response update(@PathParam("clientId") String clientId, ClientRepresentation rep) {
event.event(EventType.CLIENT_UPDATE).client(clientId);
ClientModel client = authenticate(false, clientId);
RepresentationToModel.updateClient(rep, client);
logger.infov("Updated client {0}", rep.getClientId());
event.success();
return Response.status(Response.Status.OK).build();
}
@DELETE
@Path("{clientId}")
public void delete(@PathParam("clientId") String clientId) {
ClientModel client = realm.getClientByClientId(clientId);
if (client == null) {
throw new NotFoundException("Client not found");
public Response delete(@PathParam("clientId") String clientId) {
event.event(EventType.CLIENT_DELETE).client(clientId);
ClientModel client = authenticate(false, clientId);
if (realm.removeClient(client.getId())) {
event.success();
return Response.ok().build();
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}
realm.removeClient(client.getId());
}
private ClientModel authenticate(boolean create, String clientId) {
String authorizationHeader = session.getContext().getRequestHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
boolean bearer = authorizationHeader != null && authorizationHeader.split(" ")[0].equalsIgnoreCase("Bearer");
if (bearer) {
AuthenticationManager.AuthResult authResult = authManager.authenticateBearerToken(session, realm);
AccessToken.Access realmAccess = authResult.getToken().getResourceAccess(Constants.REALM_MANAGEMENT_CLIENT_ID);
if (realmAccess != null) {
if (realmAccess.isUserInRole(AdminRoles.MANAGE_CLIENTS)) {
return create ? null : realm.getClientByClientId(clientId);
}
if (create && realmAccess.isUserInRole(AdminRoles.CREATE_CLIENT)) {
return create ? null : realm.getClientByClientId(clientId);
}
}
} else if (!create) {
ClientModel client;
try {
AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event, realm);
client = clientAuth.getClient();
if (client != null && !client.isPublicClient() && client.getClientId().equals(clientId)) {
return client;
}
} catch (Throwable t) {
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
}

View File

@ -112,14 +112,14 @@ public class RealmsResource {
return service;
}
// @Path("{realm}/client-registration")
// public ClientRegistrationService getClientsService(final @PathParam("realm") String name) {
// RealmModel realm = init(name);
// EventBuilder event = new EventBuilder(realm, session, clientConnection);
// ClientRegistrationService service = new ClientRegistrationService(realm, event);
// ResteasyProviderFactory.getInstance().injectProperties(service);
// return service;
// }
@Path("{realm}/client-registration")
public ClientRegistrationService getClientsService(final @PathParam("realm") String name) {
RealmModel realm = init(name);
EventBuilder event = new EventBuilder(realm, session, clientConnection);
ClientRegistrationService service = new ClientRegistrationService(realm, event);
ResteasyProviderFactory.getInstance().injectProperties(service);
return service;
}
@Path("{realm}/clients-managements")
public ClientsManagementService getClientsManagementService(final @PathParam("realm") String name) {

View File

@ -20,6 +20,8 @@ import org.keycloak.admin.client.Keycloak;
import org.keycloak.models.Constants;
import org.keycloak.testsuite.arquillian.annotation.AdapterLibsLocationProperty;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
import org.keycloak.testsuite.util.OAuthClient;
import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN;
import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
@ -55,6 +57,10 @@ public class ContainersTestEnricher {
@ClassScoped
private InstanceProducer<Keycloak> adminClient;
@Inject
@ClassScoped
private InstanceProducer<OAuthClient> oauthClient;
private ContainerController controller;
private final boolean migrationTests = System.getProperty("migration", "false").equals("true");
@ -92,6 +98,7 @@ public class ContainersTestEnricher {
initializeTestContext(testClass);
initializeAdminClient();
initializeOAuthClient();
}
private void initializeTestContext(Class testClass) {
@ -116,6 +123,10 @@ public class ContainersTestEnricher {
MASTER, ADMIN, ADMIN, Constants.ADMIN_CONSOLE_CLIENT_ID));
}
private void initializeOAuthClient() {
oauthClient.set(new OAuthClient(getAuthServerContextRootFromSystemProperty() + "/auth"));
}
/**
*
* @param testClass

View File

@ -1,8 +1,6 @@
package org.keycloak.testsuite.arquillian;
import org.keycloak.testsuite.arquillian.provider.URLProvider;
import org.keycloak.testsuite.arquillian.provider.SuiteContextProvider;
import org.keycloak.testsuite.arquillian.provider.TestContextProvider;
import org.keycloak.testsuite.arquillian.provider.*;
import org.jboss.arquillian.container.spi.client.container.DeployableContainer;
import org.jboss.arquillian.container.test.impl.enricher.resource.URLResourceProvider;
import org.jboss.arquillian.container.test.spi.client.deployment.ApplicationArchiveProcessor;
@ -12,7 +10,6 @@ import org.jboss.arquillian.graphene.location.CustomizableURLResourceProvider;
import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider;
import org.jboss.arquillian.test.spi.execution.TestExecutionDecider;
import org.keycloak.testsuite.arquillian.jira.JiraTestExecutionDecider;
import org.keycloak.testsuite.arquillian.provider.AdminClientProvider;
import org.keycloak.testsuite.arquillian.undertow.CustomUndertowContainer;
/**
@ -27,7 +24,8 @@ public class KeycloakArquillianExtension implements LoadableExtension {
builder
.service(ResourceProvider.class, SuiteContextProvider.class)
.service(ResourceProvider.class, TestContextProvider.class)
.service(ResourceProvider.class, AdminClientProvider.class);
.service(ResourceProvider.class, AdminClientProvider.class)
.service(ResourceProvider.class, OAuthClientProvider.class);
builder
.service(DeploymentScenarioGenerator.class, DeploymentTargetModifier.class)

View File

@ -0,0 +1,29 @@
package org.keycloak.testsuite.arquillian.provider;
import org.jboss.arquillian.core.api.Instance;
import org.jboss.arquillian.core.api.annotation.Inject;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider;
import org.keycloak.testsuite.util.OAuthClient;
import java.lang.annotation.Annotation;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class OAuthClientProvider implements ResourceProvider {
@Inject
Instance<OAuthClient> oauthClient;
@Override
public boolean canProvide(Class<?> type) {
return OAuthClient.class.isAssignableFrom(type);
}
@Override
public Object lookup(ArquillianResource resource, Annotation... qualifiers) {
return oauthClient.get();
}
}

View File

@ -0,0 +1,77 @@
package org.keycloak.testsuite.util;
import org.apache.commons.io.IOUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.keycloak.OAuth2Constants;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class OAuthClient {
private String baseUrl;
public OAuthClient(String baseUrl) {
this.baseUrl = baseUrl;
}
public AccessTokenResponse getToken(String realm, String clientId, String clientSecret, String username, String password) {
CloseableHttpClient httpclient = HttpClients.createDefault();
try {
HttpPost post = new HttpPost(OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(baseUrl)).build(realm));
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
parameters.add(new BasicNameValuePair("username", username));
parameters.add(new BasicNameValuePair("password", password));
if (clientSecret != null) {
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
post.setHeader("Authorization", authorization);
} else {
parameters.add(new BasicNameValuePair("client_id", clientId));
}
UrlEncodedFormEntity formEntity;
try {
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
post.setEntity(formEntity);
CloseableHttpResponse response = httpclient.execute(post);
if (response.getStatusLine().getStatusCode() != 200) {
throw new RuntimeException("Failed to retrieve token: " + response.getStatusLine().toString() + " / " + IOUtils.toString(response.getEntity().getContent()));
}
return JsonSerialization.readValue(response.getEntity().getContent(), AccessTokenResponse.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
finally {
try {
httpclient.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@ -20,6 +20,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
import org.keycloak.testsuite.arquillian.SuiteContext;
import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.WebDriver;
import org.keycloak.testsuite.auth.page.AuthServer;
import org.keycloak.testsuite.auth.page.AuthServerContextRoot;
@ -51,6 +52,9 @@ public abstract class AbstractKeycloakTest {
@ArquillianResource
protected Keycloak adminClient;
@ArquillianResource
protected OAuthClient oauthClient;
protected List<RealmRepresentation> testRealmReps;
@Drone

View File

@ -0,0 +1,306 @@
package org.keycloak.testsuite.client;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.client.registration.ClientRegistration;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import static org.junit.Assert.*;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientRegistrationTest extends AbstractKeycloakTest {
private static final String REALM_NAME = "test";
private static final String CLIENT_ID = "test-client";
private static final String CLIENT_SECRET = "test-client-secret";
private ClientRegistration clientRegistrationAsAdmin;
private ClientRegistration clientRegistrationAsClient;
@Before
public void before() throws ClientRegistrationException {
clientRegistrationAsAdmin = clientBuilder().auth(getToken("manage-clients", "password")).build();
clientRegistrationAsClient = clientBuilder().auth(CLIENT_ID, CLIENT_SECRET).build();
}
@After
public void after() throws ClientRegistrationException {
clientRegistrationAsAdmin.close();
clientRegistrationAsClient.close();
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation rep = new RealmRepresentation();
rep.setEnabled(true);
rep.setRealm(REALM_NAME);
rep.setUsers(new LinkedList<UserRepresentation>());
LinkedList<CredentialRepresentation> credentials = new LinkedList<>();
CredentialRepresentation password = new CredentialRepresentation();
password.setType(CredentialRepresentation.PASSWORD);
password.setValue("password");
credentials.add(password);
UserRepresentation user = new UserRepresentation();
user.setEnabled(true);
user.setUsername("manage-clients");
user.setCredentials(credentials);
user.setClientRoles(Collections.singletonMap(Constants.REALM_MANAGEMENT_CLIENT_ID, Collections.singletonList(AdminRoles.MANAGE_CLIENTS)));
rep.getUsers().add(user);
UserRepresentation user2 = new UserRepresentation();
user2.setEnabled(true);
user2.setUsername("create-clients");
user2.setCredentials(credentials);
user2.setClientRoles(Collections.singletonMap(Constants.REALM_MANAGEMENT_CLIENT_ID, Collections.singletonList(AdminRoles.CREATE_CLIENT)));
rep.getUsers().add(user2);
UserRepresentation user3 = new UserRepresentation();
user3.setEnabled(true);
user3.setUsername("no-access");
user3.setCredentials(credentials);
rep.getUsers().add(user3);
testRealms.add(rep);
}
private void registerClient(ClientRegistration clientRegistration) throws ClientRegistrationException {
ClientRepresentation client = new ClientRepresentation();
client.setClientId(CLIENT_ID);
client.setSecret(CLIENT_SECRET);
ClientRepresentation createdClient = clientRegistration.create(client);
assertEquals(CLIENT_ID, createdClient.getClientId());
client = adminClient.realm(REALM_NAME).clients().get(createdClient.getId()).toRepresentation();
assertEquals(CLIENT_ID, client.getClientId());
AccessTokenResponse token2 = oauthClient.getToken(REALM_NAME, CLIENT_ID, CLIENT_SECRET, "manage-clients", "password");
assertNotNull(token2.getToken());
}
@Test
public void registerClientAsAdmin() throws ClientRegistrationException {
registerClient(clientRegistrationAsAdmin);
}
@Test
public void registerClientAsAdminWithCreateOnly() throws ClientRegistrationException {
ClientRegistration clientRegistration = clientBuilder().auth(getToken("create-clients", "password")).build();
try {
registerClient(clientRegistration);
} finally {
clientRegistration.close();
}
}
@Test
public void registerClientAsAdminWithNoAccess() throws ClientRegistrationException {
ClientRegistration clientRegistration = clientBuilder().auth(getToken("no-access", "password")).build();
try {
registerClient(clientRegistration);
fail("Expected 403");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
} finally {
clientRegistration.close();
}
}
@Test
public void getClientAsAdminWithCreateOnly() throws ClientRegistrationException {
registerClient(clientRegistrationAsAdmin);
ClientRegistration clientRegistration = clientBuilder().auth(getToken("create-clients", "password")).build();
try {
clientRegistration.get(CLIENT_ID);
fail("Expected 403");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
} finally {
clientRegistration.close();
}
}
@Test
public void wrongClient() throws ClientRegistrationException {
registerClient(clientRegistrationAsAdmin);
ClientRepresentation client = new ClientRepresentation();
client.setClientId("test-client-2");
client.setSecret("test-client-2-secret");
clientRegistrationAsAdmin.create(client);
ClientRegistration clientRegistration = clientBuilder().auth("test-client-2", "test-client-2-secret").build();
client = clientRegistration.get("test-client-2");
assertNotNull(client);
assertEquals("test-client-2", client.getClientId());
try {
try {
clientRegistration.get(CLIENT_ID);
fail("Expected 403");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
client = clientRegistrationAsAdmin.get(CLIENT_ID);
try {
clientRegistration.update(client);
fail("Expected 403");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
try {
clientRegistration.delete(CLIENT_ID);
fail("Expected 403");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}
finally {
clientRegistration.close();
}
}
@Test
public void getClientAsAdminWithNoAccess() throws ClientRegistrationException {
registerClient(clientRegistrationAsAdmin);
ClientRegistration clientRegistration = clientBuilder().auth(getToken("no-access", "password")).build();
try {
clientRegistration.get(CLIENT_ID);
fail("Expected 403");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
} finally {
clientRegistration.close();
}
}
private void updateClient(ClientRegistration clientRegistration) throws ClientRegistrationException {
ClientRepresentation client = clientRegistration.get(CLIENT_ID);
client.setRedirectUris(Collections.singletonList("http://localhost:8080/app"));
clientRegistration.update(client);
ClientRepresentation updatedClient = clientRegistration.get(CLIENT_ID);
assertEquals(1, updatedClient.getRedirectUris().size());
assertEquals("http://localhost:8080/app", updatedClient.getRedirectUris().get(0));
}
@Test
public void updateClientAsAdmin() throws ClientRegistrationException {
registerClient(clientRegistrationAsAdmin);
updateClient(clientRegistrationAsAdmin);
}
@Test
public void updateClientAsAdminWithCreateOnly() throws ClientRegistrationException {
ClientRegistration clientRegistration = clientBuilder().auth(getToken("create-clients", "password")).build();
try {
updateClient(clientRegistration);
fail("Expected 403");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
} finally {
clientRegistration.close();
}
}
@Test
public void updateClientAsAdminWithNoAccess() throws ClientRegistrationException {
ClientRegistration clientRegistration = clientBuilder().auth(getToken("no-access", "password")).build();
try {
updateClient(clientRegistration);
fail("Expected 403");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
} finally {
clientRegistration.close();
}
}
@Test
public void updateClientAsClient() throws ClientRegistrationException {
registerClient(clientRegistrationAsAdmin);
updateClient(clientRegistrationAsClient);
}
private void deleteClient(ClientRegistration clientRegistration) throws ClientRegistrationException {
clientRegistration.delete(CLIENT_ID);
// Can't authenticate as client after client is deleted
ClientRepresentation client = clientRegistrationAsAdmin.get(CLIENT_ID);
assertNull(client);
}
@Test
public void deleteClientAsAdmin() throws ClientRegistrationException {
registerClient(clientRegistrationAsAdmin);
deleteClient(clientRegistrationAsAdmin);
}
@Test
public void deleteClientAsAdminWithCreateOnly() throws ClientRegistrationException {
ClientRegistration clientRegistration = clientBuilder().auth(getToken("create-clients", "password")).build();
try {
deleteClient(clientRegistration);
fail("Expected 403");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
} finally {
clientRegistration.close();
}
}
@Test
public void deleteClientAsAdminWithNoAccess() throws ClientRegistrationException {
ClientRegistration clientRegistration = clientBuilder().auth(getToken("no-access", "password")).build();
try {
deleteClient(clientRegistration);
fail("Expected 403");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
} finally {
clientRegistration.close();
}
}
@Test
public void deleteClientAsClient() throws ClientRegistrationException {
registerClient(clientRegistrationAsAdmin);
deleteClient(clientRegistrationAsClient);
}
private ClientRegistration.ClientRegistrationBuilder clientBuilder() {
return ClientRegistration.create().realm("test").authServerUrl(testContext.getAuthServerContextRoot() + "/auth");
}
private String getToken(String username, String password) {
return oauthClient.getToken(REALM_NAME, "security-admin-console", null, username, password).getToken();
}
}

View File

@ -201,6 +201,10 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-api</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>