Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Burke 2015-10-08 16:19:56 -04:00
commit d39aee0a72
29 changed files with 995 additions and 56 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; package org.keycloak.representations.idm;
import java.util.ArrayList; import org.codehaus.jackson.annotate.JsonIgnore;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $

View file

@ -209,6 +209,7 @@ new Keycloak({ url: 'http://localhost/auth', realm: 'myrealm', clientId: 'myApp'
<listitem>redirectUri - specifies the uri to redirect to after login</listitem> <listitem>redirectUri - specifies the uri to redirect to after login</listitem>
<listitem>prompt - can be set to 'none' to check if the user is logged in already (if not logged in, a login form is not displayed)</listitem> <listitem>prompt - can be set to 'none' to check if the user is logged in already (if not logged in, a login form is not displayed)</listitem>
<listitem>loginHint - used to pre-fill the username/email field on the login form</listitem> <listitem>loginHint - used to pre-fill the username/email field on the login form</listitem>
<listitem>action - if value is 'register' then user is redirected to registration page, otherwise to login page</listitem>
</itemizedlist> </itemizedlist>
</para> </para>
</simplesect> </simplesect>
@ -246,6 +247,20 @@ new Keycloak({ url: 'http://localhost/auth', realm: 'myrealm', clientId: 'myApp'
</para> </para>
</simplesect> </simplesect>
<simplesect>
<title>register(options)</title>
<para>Redirects to registration form. It's a shortcut for doing login with option action = 'register'</para>
<para>Options are same as login method but 'action' is overwritten to 'register'</para>
</simplesect>
<simplesect>
<title>createRegisterUrl(options)</title>
<para>Returns the url to registration page. It's a shortcut for doing createRegisterUrl with option action = 'register'</para>
<para>Options are same as createLoginUrl method but 'action' is overwritten to 'register'</para>
</simplesect>
<simplesect> <simplesect>
<title>accountManagement()</title> <title>accountManagement()</title>

View file

@ -69,7 +69,16 @@ public enum EventType {
IMPERSONATE(true), IMPERSONATE(true),
CUSTOM_REQUIRED_ACTION(true), CUSTOM_REQUIRED_ACTION(true),
CUSTOM_REQUIRED_ACTION_ERROR(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; private boolean saveByDefault;

View file

@ -12,6 +12,6 @@ Open the Keycloak admin console, click on Add Realm, click on 'Choose a JSON fil
Deploy the JS Console to Keycloak by running: Deploy the JS Console to Keycloak by running:
mvn install jboss-as:deploy mvn install wildfly:deploy
Open the console at http://localhost:8080/js-console and login with username: 'user', and password: 'password'. Open the console at http://localhost:8080/js-console and login with username: 'user', and password: 'password'.

View file

@ -7,6 +7,7 @@
<div> <div>
<button onclick="keycloak.login()">Login</button> <button onclick="keycloak.login()">Login</button>
<button onclick="keycloak.logout()">Logout</button> <button onclick="keycloak.logout()">Logout</button>
<button onclick="keycloak.register()">Register</button>
<button onclick="refreshToken(9999)">Refresh Token</button> <button onclick="refreshToken(9999)">Refresh Token</button>
<button onclick="refreshToken(30)">Refresh Token (if <30s validity)</button> <button onclick="refreshToken(30)">Refresh Token (if <30s validity)</button>
<button onclick="loadProfile()">Get Profile</button> <button onclick="loadProfile()">Get Profile</button>
@ -18,6 +19,7 @@
<button onclick="output(keycloak)">Show Details</button> <button onclick="output(keycloak)">Show Details</button>
<button onclick="output(keycloak.createLoginUrl())">Show Login URL</button> <button onclick="output(keycloak.createLoginUrl())">Show Login URL</button>
<button onclick="output(keycloak.createLogoutUrl())">Show Logout URL</button> <button onclick="output(keycloak.createLogoutUrl())">Show Logout URL</button>
<button onclick="output(keycloak.createRegisterUrl())">Show Register URL</button>
</div> </div>
<h2>Result</h2> <h2>Result</h2>

View file

@ -7,6 +7,7 @@ onText=AN
offText=AUS offText=AUS
client=de Client client=de Client
clear=de Clear clear=de Clear
selectOne=de Select One...
# Realm settings # Realm settings
realm-detail.enabled.tooltip=de Users and clients can only access a realm if it's enabled realm-detail.enabled.tooltip=de Users and clients can only access a realm if it's enabled
@ -114,3 +115,15 @@ not-before.tooltip=de Revoke any tokens issued before this date.
set-to-now=de Set To Now set-to-now=de Set To Now
push=de Push push=de Push
push.tooltip=de For every client that has an admin URL, notify them of the new revocation policy. push.tooltip=de For every client that has an admin URL, notify them of the new revocation policy.
#Protocol Mapper
usermodel.prop.label=de Property
usermodel.prop.tooltip=de Name of the property method in the UserModel interface. For example, a value of 'email' would reference the UserModel.getEmail() method.
usermodel.attr.label=de User Attribute
usermodel.attr.tooltip=de Name of stored user attribute which is the name of an attribute within the UserModel.attribute map.
userSession.modelNote.label=de User Session Note
userSession.modelNote.tooltip=de Name of stored user session note within the UserSessionModel.note map.
multivalued.label=de Multivalued
multivalued.tooltip=de Indicates if attribute supports multiple values. If true, then the list of all values of this attribute will be set as claim. If false, then just first value will be set as claim
selectRole.label=de Select Role
selectRole.tooltip=de Enter role in the textbox to the left, or click this button to browse and select the role you want

View file

@ -7,6 +7,7 @@ onText=ON
offText=OFF offText=OFF
client=Client client=Client
clear=Clear clear=Clear
selectOne=Select One...
# Realm settings # Realm settings
realm-detail.enabled.tooltip=Users and clients can only access a realm if it's enabled realm-detail.enabled.tooltip=Users and clients can only access a realm if it's enabled
@ -114,3 +115,15 @@ not-before.tooltip=Revoke any tokens issued before this date.
set-to-now=Set To Now set-to-now=Set To Now
push=Push push=Push
push.tooltip=For every client that has an admin URL, notify them of the new revocation policy. push.tooltip=For every client that has an admin URL, notify them of the new revocation policy.
#Protocol Mapper
usermodel.prop.label=Property
usermodel.prop.tooltip=Name of the property method in the UserModel interface. For example, a value of 'email' would reference the UserModel.getEmail() method.
usermodel.attr.label=User Attribute
usermodel.attr.tooltip=Name of stored user attribute which is the name of an attribute within the UserModel.attribute map.
userSession.modelNote.label=User Session Note
userSession.modelNote.tooltip=Name of stored user session note within the UserSessionModel.note map.
multivalued.label=Multivalued
multivalued.tooltip=Indicates if attribute supports multiple values. If true, then the list of all values of this attribute will be set as claim. If false, then just first value will be set as claim
selectRole.label=Select Role
selectRole.tooltip=Enter role in the textbox to the left, or click this button to browse and select the role you want

View file

@ -1,16 +1,16 @@
<div> <div>
<div data-ng-repeat="option in properties" class="form-group" data-ng-controller="ProviderConfigCtrl"> <div data-ng-repeat="option in properties" class="form-group" data-ng-controller="ProviderConfigCtrl">
<label class="col-md-2 control-label">{{option.label}}</label> <label class="col-md-2 control-label">{{:: option.label | translate}}</label>
<div class="col-sm-6" data-ng-hide="option.type == 'boolean' || option.type == 'List' || option.type == 'Role' || option.type == 'ClientList'"> <div class="col-sm-6" data-ng-hide="option.type == 'boolean' || option.type == 'List' || option.type == 'Role' || option.type == 'ClientList'">
<input class="form-control" type="text" data-ng-model="config[ option.name ]" > <input class="form-control" type="text" data-ng-model="config[ option.name ]" >
</div> </div>
<div class="col-sm-6" data-ng-show="option.type == 'boolean'"> <div class="col-sm-6" data-ng-show="option.type == 'boolean'">
<input ng-model="config[ option.name ]" value="'true'" name="option.name" id="option.name" onoffswitchstring /> <input ng-model="config[ option.name ]" value="'true'" name="option.name" id="option.name" onoffswitchstring on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div> </div>
<div class="col-sm-6" data-ng-show="option.type == 'List'"> <div class="col-sm-6" data-ng-show="option.type == 'List'">
<select ng-model="config[ option.name ]" ng-options="data for data in option.defaultValue"> <select ng-model="config[ option.name ]" ng-options="data for data in option.defaultValue">
<option value="" selected> Select one... </option> <option value="" selected> {{:: 'selectOne' | translate}} </option>
</select> </select>
</div> </div>
<div class="col-sm-6" data-ng-show="option.type == 'Role'"> <div class="col-sm-6" data-ng-show="option.type == 'Role'">
@ -19,16 +19,16 @@
<input class="form-control" type="text" data-ng-model="config[ option.name ]" > <input class="form-control" type="text" data-ng-model="config[ option.name ]" >
</div> </div>
<div class="col-sm-2"> <div class="col-sm-2">
<button type="submit" data-ng-click="openRoleSelector(option.name, config)" class="btn btn-default" tooltip-placement="top" tooltip-trigger="mouseover mouseout" tooltip="Enter role in the textbox to the left, or click this button to browse and select the role you want">Select Role</button> <button type="submit" data-ng-click="openRoleSelector(option.name, config)" class="btn btn-default" tooltip-placement="top" tooltip-trigger="mouseover mouseout" tooltip="{{:: 'selectRole.tooltip' | translate}}">{{:: 'selectRole.label' | translate}}</button>
</div> </div>
</div> </div>
</div> </div>
<div class="col-sm-4" data-ng-show="option.type == 'ClientList'"> <div class="col-sm-4" data-ng-show="option.type == 'ClientList'">
<select ng-model="config[ option.name ]" ng-options="client.clientId as client.clientId for client in clients"> <select ng-model="config[ option.name ]" ng-options="client.clientId as client.clientId for client in clients">
<option value="" selected> Select one... </option> <option value="" selected> {{:: 'selectOne' | translate}} </option>
</select> </select>
</div> </div>
<kc-tooltip>{{option.helpText}}</kc-tooltip> <kc-tooltip>{{:: option.helpText | translate}}</kc-tooltip>
</div> </div>
</div> </div>

View file

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

View file

@ -183,6 +183,18 @@
return url; return url;
} }
kc.register = function (options) {
return adapter.register(options);
}
kc.createRegisterUrl = function(options) {
if (!options) {
options = {};
}
options.action = 'register';
return kc.createLoginUrl(options);
}
kc.createAccountUrl = function(options) { kc.createAccountUrl = function(options) {
var url = getRealmUrl() var url = getRealmUrl()
+ '/account' + '/account'
@ -760,6 +772,11 @@
return createPromise().promise; return createPromise().promise;
}, },
register: function(options) {
window.location.href = kc.createRegisterUrl(options);
return createPromise().promise;
},
accountManagement : function() { accountManagement : function() {
window.location.href = kc.createAccountUrl(); window.location.href = kc.createAccountUrl();
return createPromise().promise; return createPromise().promise;
@ -858,6 +875,16 @@
return promise.promise; return promise.promise;
}, },
register : function() {
var registerUrl = kc.createRegisterUrl();
var ref = window.open(registerUrl, '_blank', 'location=no');
ref.addEventListener('loadstart', function(event) {
if (event.url.indexOf('http://localhost') == 0) {
ref.close();
}
});
},
accountManagement : function() { accountManagement : function() {
var accountUrl = kc.createAccountUrl(); var accountUrl = kc.createAccountUrl();
var ref = window.open(accountUrl, '_blank', 'location=no'); var ref = window.open(accountUrl, '_blank', 'location=no');

View file

@ -70,6 +70,15 @@ public class MigrateTo1_6_0 {
if ((adminConsoleClient != null) && !localeMapperAdded(adminConsoleClient)) { if ((adminConsoleClient != null) && !localeMapperAdded(adminConsoleClient)) {
adminConsoleClient.addProtocolMapper(localeMapper); 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 REALM_ADMIN = "realm-admin";
public static String CREATE_REALM = "create-realm"; 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_REALM = "view-realm";
public static String VIEW_USERS = "view-users"; 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_CLIENTS = "manage-clients";
public static String MANAGE_EVENTS = "manage-events"; 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>common</module>
<module>core</module> <module>core</module>
<module>core-jaxrs</module> <module>core-jaxrs</module>
<module>client-api</module>
<module>connections</module> <module>connections</module>
<module>dependencies</module> <module>dependencies</module>
<module>events</module> <module>events</module>
@ -650,6 +651,11 @@
<artifactId>keycloak-core</artifactId> <artifactId>keycloak-core</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-core-jaxrs</artifactId> <artifactId>keycloak-core-jaxrs</artifactId>

View file

@ -7,6 +7,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
@ -44,7 +45,11 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
String clientSecret = null; String clientSecret = null;
String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); 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) { if (authorizationHeader != null) {
String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader); String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
@ -54,7 +59,7 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
} else { } 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 // 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(); Response challengeResponse = Response.status(Response.Status.UNAUTHORIZED).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + context.getRealm().getName() + "\"").build();
context.challenge(challengeResponse); context.challenge(challengeResponse);
return; 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); client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
clientSecret = formData.getFirst("client_secret"); clientSecret = formData.getFirst("client_secret");
} }

View file

@ -17,14 +17,14 @@ public class ProtocolMapperUtils {
public static final String USER_ATTRIBUTE = "user.attribute"; public static final String USER_ATTRIBUTE = "user.attribute";
public static final String USER_SESSION_NOTE = "user.session.note"; public static final String USER_SESSION_NOTE = "user.session.note";
public static final String MULTIVALUED = "multivalued"; public static final String MULTIVALUED = "multivalued";
public static final String USER_MODEL_PROPERTY_LABEL = "User Property"; public static final String USER_MODEL_PROPERTY_LABEL = "usermodel.prop.label";
public static final String USER_MODEL_PROPERTY_HELP_TEXT = "Name of the property method in the UserModel interface. For example, a value of 'email' would reference the UserModel.getEmail() method."; public static final String USER_MODEL_PROPERTY_HELP_TEXT = "usermodel.prop.tooltip";
public static final String USER_MODEL_ATTRIBUTE_LABEL = "User Attribute"; public static final String USER_MODEL_ATTRIBUTE_LABEL = "usermodel.attr.label";
public static final String USER_MODEL_ATTRIBUTE_HELP_TEXT = "Name of stored user attribute which is the name of an attribute within the UserModel.attribute map."; public static final String USER_MODEL_ATTRIBUTE_HELP_TEXT = "usermodel.attr.tooltip";
public static final String USER_SESSION_MODEL_NOTE_LABEL = "User Session Note"; public static final String USER_SESSION_MODEL_NOTE_LABEL = "userSession.modelNote.label";
public static final String USER_SESSION_MODEL_NOTE_HELP_TEXT = "Name of stored user session note within the UserSessionModel.note map."; public static final String USER_SESSION_MODEL_NOTE_HELP_TEXT = "userSession.modelNote.tooltip";
public static final String MULTIVALUED_LABEL = "Multivalued"; public static final String MULTIVALUED_LABEL = "multivalued.label";
public static final String MULTIVALUED_HELP_TEXT = "Indicates if attribute supports multiple values. If true, then the list of all values of this attribute will be set as claim. If false, then just first value will be set as claim"; public static final String MULTIVALUED_HELP_TEXT = "multivalued.tooltip";
public static String getUserModelValue(UserModel user, String propertyName) { public static String getUserModelValue(UserModel user, String propertyName) {

View file

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

View file

@ -3,21 +3,27 @@ package org.keycloak.services.resources;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException; 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.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.exportimport.ClientDescriptionConverter; import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.exportimport.KeycloakClientDescriptionConverter; import org.keycloak.exportimport.KeycloakClientDescriptionConverter;
import org.keycloak.models.ClientModel; import org.keycloak.models.*;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ErrorResponse; 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.*;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.net.URI; import java.net.URI;
@ -36,6 +42,8 @@ public class ClientRegistrationService {
@Context @Context
private KeycloakSession session; private KeycloakSession session;
private AppAuthManager authManager = new AppAuthManager();
public ClientRegistrationService(RealmModel realm, EventBuilder event) { public ClientRegistrationService(RealmModel realm, EventBuilder event) {
this.realm = realm; this.realm = realm;
this.event = event; this.event = event;
@ -44,6 +52,10 @@ public class ClientRegistrationService {
@POST @POST
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN }) @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN })
public Response create(String description, @QueryParam("format") String format) { public Response create(String description, @QueryParam("format") String format) {
event.event(EventType.CLIENT_REGISTER);
authenticate(true, null);
if (format == null) { if (format == null) {
format = KeycloakClientDescriptionConverter.ID; format = KeycloakClientDescriptionConverter.ID;
} }
@ -58,6 +70,10 @@ public class ClientRegistrationService {
ClientModel clientModel = RepresentationToModel.createClient(session, realm, rep, true); ClientModel clientModel = RepresentationToModel.createClient(session, realm, rep, true);
rep = ModelToRepresentation.toRepresentation(clientModel); rep = ModelToRepresentation.toRepresentation(clientModel);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build(); 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(); return Response.created(uri).entity(rep).build();
} catch (ModelDuplicateException e) { } catch (ModelDuplicateException e) {
return ErrorResponse.exists("Client " + rep.getClientId() + " already exists"); return ErrorResponse.exists("Client " + rep.getClientId() + " already exists");
@ -67,34 +83,79 @@ public class ClientRegistrationService {
@GET @GET
@Path("{clientId}") @Path("{clientId}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public ClientRepresentation get(@PathParam("clientId") String clientId) { public Response get(@PathParam("clientId") String clientId) {
AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event, realm); event.event(EventType.CLIENT_INFO);
ClientModel client = clientAuth.getClient();
ClientModel client = authenticate(false, clientId);
if (client == null) { 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 @PUT
@Path("{clientId}") @Path("{clientId}")
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public void update(@PathParam("clientId") String clientId, ClientRepresentation rep) { public Response update(@PathParam("clientId") String clientId, ClientRepresentation rep) {
ClientModel client = realm.getClientByClientId(clientId); event.event(EventType.CLIENT_UPDATE).client(clientId);
if (client == null) {
throw new NotFoundException("Client not found"); ClientModel client = authenticate(false, clientId);
}
RepresentationToModel.updateClient(rep, client); RepresentationToModel.updateClient(rep, client);
logger.infov("Updated client {0}", rep.getClientId());
event.success();
return Response.status(Response.Status.OK).build();
} }
@DELETE @DELETE
@Path("{clientId}") @Path("{clientId}")
public void delete(@PathParam("clientId") String clientId) { public Response delete(@PathParam("clientId") String clientId) {
ClientModel client = realm.getClientByClientId(clientId); event.event(EventType.CLIENT_DELETE).client(clientId);
if (client == null) {
throw new NotFoundException("Client not found"); 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; return service;
} }
// @Path("{realm}/client-registration") @Path("{realm}/client-registration")
// public ClientRegistrationService getClientsService(final @PathParam("realm") String name) { public ClientRegistrationService getClientsService(final @PathParam("realm") String name) {
// RealmModel realm = init(name); RealmModel realm = init(name);
// EventBuilder event = new EventBuilder(realm, session, clientConnection); EventBuilder event = new EventBuilder(realm, session, clientConnection);
// ClientRegistrationService service = new ClientRegistrationService(realm, event); ClientRegistrationService service = new ClientRegistrationService(realm, event);
// ResteasyProviderFactory.getInstance().injectProperties(service); ResteasyProviderFactory.getInstance().injectProperties(service);
// return service; return service;
// } }
@Path("{realm}/clients-managements") @Path("{realm}/clients-managements")
public ClientsManagementService getClientsManagementService(final @PathParam("realm") String name) { 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.models.Constants;
import org.keycloak.testsuite.arquillian.annotation.AdapterLibsLocationProperty; import org.keycloak.testsuite.arquillian.annotation.AdapterLibsLocationProperty;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; 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.ADMIN;
import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER; import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
@ -55,6 +57,10 @@ public class ContainersTestEnricher {
@ClassScoped @ClassScoped
private InstanceProducer<Keycloak> adminClient; private InstanceProducer<Keycloak> adminClient;
@Inject
@ClassScoped
private InstanceProducer<OAuthClient> oauthClient;
private ContainerController controller; private ContainerController controller;
private final boolean migrationTests = System.getProperty("migration", "false").equals("true"); private final boolean migrationTests = System.getProperty("migration", "false").equals("true");
@ -92,6 +98,7 @@ public class ContainersTestEnricher {
initializeTestContext(testClass); initializeTestContext(testClass);
initializeAdminClient(); initializeAdminClient();
initializeOAuthClient();
} }
private void initializeTestContext(Class testClass) { private void initializeTestContext(Class testClass) {
@ -116,6 +123,10 @@ public class ContainersTestEnricher {
MASTER, ADMIN, ADMIN, Constants.ADMIN_CONSOLE_CLIENT_ID)); MASTER, ADMIN, ADMIN, Constants.ADMIN_CONSOLE_CLIENT_ID));
} }
private void initializeOAuthClient() {
oauthClient.set(new OAuthClient(getAuthServerContextRootFromSystemProperty() + "/auth"));
}
/** /**
* *
* @param testClass * @param testClass

View file

@ -1,8 +1,6 @@
package org.keycloak.testsuite.arquillian; package org.keycloak.testsuite.arquillian;
import org.keycloak.testsuite.arquillian.provider.URLProvider; import org.keycloak.testsuite.arquillian.provider.*;
import org.keycloak.testsuite.arquillian.provider.SuiteContextProvider;
import org.keycloak.testsuite.arquillian.provider.TestContextProvider;
import org.jboss.arquillian.container.spi.client.container.DeployableContainer; 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.impl.enricher.resource.URLResourceProvider;
import org.jboss.arquillian.container.test.spi.client.deployment.ApplicationArchiveProcessor; 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.enricher.resource.ResourceProvider;
import org.jboss.arquillian.test.spi.execution.TestExecutionDecider; import org.jboss.arquillian.test.spi.execution.TestExecutionDecider;
import org.keycloak.testsuite.arquillian.jira.JiraTestExecutionDecider; import org.keycloak.testsuite.arquillian.jira.JiraTestExecutionDecider;
import org.keycloak.testsuite.arquillian.provider.AdminClientProvider;
import org.keycloak.testsuite.arquillian.undertow.CustomUndertowContainer; import org.keycloak.testsuite.arquillian.undertow.CustomUndertowContainer;
/** /**
@ -27,7 +24,8 @@ public class KeycloakArquillianExtension implements LoadableExtension {
builder builder
.service(ResourceProvider.class, SuiteContextProvider.class) .service(ResourceProvider.class, SuiteContextProvider.class)
.service(ResourceProvider.class, TestContextProvider.class) .service(ResourceProvider.class, TestContextProvider.class)
.service(ResourceProvider.class, AdminClientProvider.class); .service(ResourceProvider.class, AdminClientProvider.class)
.service(ResourceProvider.class, OAuthClientProvider.class);
builder builder
.service(DeploymentScenarioGenerator.class, DeploymentTargetModifier.class) .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 org.keycloak.representations.idm.UserRepresentation;
import static org.keycloak.testsuite.admin.Users.setPasswordFor; import static org.keycloak.testsuite.admin.Users.setPasswordFor;
import org.keycloak.testsuite.arquillian.SuiteContext; import org.keycloak.testsuite.arquillian.SuiteContext;
import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
import org.keycloak.testsuite.auth.page.AuthServer; import org.keycloak.testsuite.auth.page.AuthServer;
import org.keycloak.testsuite.auth.page.AuthServerContextRoot; import org.keycloak.testsuite.auth.page.AuthServerContextRoot;
@ -51,6 +52,9 @@ public abstract class AbstractKeycloakTest {
@ArquillianResource @ArquillianResource
protected Keycloak adminClient; protected Keycloak adminClient;
@ArquillianResource
protected OAuthClient oauthClient;
protected List<RealmRepresentation> testRealmReps; protected List<RealmRepresentation> testRealmReps;
@Drone @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> <groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId> <artifactId>keycloak-admin-client</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-api</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId> <artifactId>keycloak-services</artifactId>