Merge pull request #1650 from stianst/client-reg2

KEYCLOAK-1868 Import clients through admin console
This commit is contained in:
Stian Thorgersen 2015-09-29 14:12:59 +02:00
commit 3e3d92c2e4
54 changed files with 934 additions and 413 deletions

View file

@ -8,6 +8,10 @@
</column>
</addColumn>
<addColumn tableName="CLIENT">
<column name="ROOT_URL" type="VARCHAR(255)"/>
</addColumn>
<createTable tableName="OFFLINE_USER_SESSION">
<column name="USER_ID" type="VARCHAR(36)">
<constraints nullable="false"/>

View file

@ -11,6 +11,7 @@ public class ClientRepresentation {
protected String id;
protected String clientId;
protected String name;
protected String rootUrl;
protected String adminUrl;
protected String baseUrl;
protected Boolean surrogateAuthRequired;
@ -74,6 +75,14 @@ public class ClientRepresentation {
this.surrogateAuthRequired = surrogateAuthRequired;
}
public String getRootUrl() {
return rootUrl;
}
public void setRootUrl(String rootUrl) {
this.rootUrl = rootUrl;
}
public String getAdminUrl() {
return adminUrl;
}

View file

@ -564,8 +564,6 @@ module.controller('ClientRoleDetailCtrl', function($scope, realm, client, role,
module.controller('ClientImportCtrl', function($scope, $location, $upload, realm, serverInfo, Notifications) {
$scope.realm = realm;
$scope.configFormats = serverInfo.clientImporters;
$scope.configFormat = null;
$scope.files = [];
@ -614,7 +612,6 @@ module.controller('ClientImportCtrl', function($scope, $location, $upload, realm
module.controller('ClientListCtrl', function($scope, realm, clients, Client, serverInfo, $route, Dialog, Notifications) {
$scope.realm = realm;
$scope.clients = clients;
$scope.importButton = serverInfo.clientImporters.length > 0;
$scope.removeClient = function(client) {
Dialog.confirmDelete(client.clientId, 'client', function() {
@ -670,7 +667,7 @@ module.controller('ClientInstallationCtrl', function($scope, realm, client, Clie
}
});
module.controller('ClientDetailCtrl', function($scope, realm, client, $route, serverInfo, Client, $location, Dialog, Notifications) {
module.controller('ClientDetailCtrl', function($scope, realm, client, $route, serverInfo, Client, ClientDescriptionConverter, $location, $modal, Dialog, Notifications) {
$scope.accessTypes = [
"confidential",
"public",
@ -709,40 +706,45 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, $route, se
$scope.samlEncrypt = false;
$scope.samlForcePostBinding = false;
$scope.samlForceNameIdFormat = false;
if (!$scope.create) {
if (!client.attributes) {
client.attributes = {};
function updateProperties() {
if (!$scope.client.attributes) {
$scope.client.attributes = {};
}
$scope.client= angular.copy(client);
$scope.accessType = $scope.accessTypes[0];
if (client.bearerOnly) {
if ($scope.client.bearerOnly) {
$scope.accessType = $scope.accessTypes[2];
} else if (client.publicClient) {
} else if ($scope.client.publicClient) {
$scope.accessType = $scope.accessTypes[1];
}
if (client.protocol) {
$scope.protocol = $scope.protocols[$scope.protocols.indexOf(client.protocol)];
if ($scope.client.protocol) {
$scope.protocol = $scope.protocols[$scope.protocols.indexOf($scope.client.protocol)];
} else {
$scope.protocol = $scope.protocols[0];
}
if (client.attributes['saml.signature.algorithm'] == 'RSA_SHA1') {
if ($scope.client.attributes['saml.signature.algorithm'] == 'RSA_SHA1') {
$scope.signatureAlgorithm = $scope.signatureAlgorithms[0];
} else if (client.attributes['saml.signature.algorithm'] == 'RSA_SHA256') {
} else if ($scope.client.attributes['saml.signature.algorithm'] == 'RSA_SHA256') {
$scope.signatureAlgorithm = $scope.signatureAlgorithms[1];
} else if (client.attributes['saml.signature.algorithm'] == 'RSA_SHA512') {
} else if ($scope.client.attributes['saml.signature.algorithm'] == 'RSA_SHA512') {
$scope.signatureAlgorithm = $scope.signatureAlgorithms[2];
} else if (client.attributes['saml.signature.algorithm'] == 'DSA_SHA1') {
} else if ($scope.client.attributes['saml.signature.algorithm'] == 'DSA_SHA1') {
$scope.signatureAlgorithm = $scope.signatureAlgorithms[3];
}
if (client.attributes['saml_name_id_format'] == 'unspecified') {
if ($scope.client.attributes['saml_name_id_format'] == 'unspecified') {
$scope.nameIdFormat = $scope.nameIdFormats[0];
} else if (client.attributes['saml_name_id_format'] == 'email') {
} else if ($scope.client.attributes['saml_name_id_format'] == 'email') {
$scope.nameIdFormat = $scope.nameIdFormats[1];
} else if (client.attributes['saml_name_id_format'] == 'transient') {
} else if ($scope.client.attributes['saml_name_id_format'] == 'transient') {
$scope.nameIdFormat = $scope.nameIdFormats[2];
} else if (client.attributes['saml_name_id_format'] == 'persistent') {
} else if ($scope.client.attributes['saml_name_id_format'] == 'persistent') {
$scope.nameIdFormat = $scope.nameIdFormats[3];
}
}
if (!$scope.create) {
$scope.client = angular.copy(client);
updateProperties();
} else {
$scope.client = { enabled: true, attributes: {}};
$scope.client.attributes['saml_signature_canonicalization_method'] = $scope.canonicalization[0].value;
@ -813,6 +815,29 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, $route, se
}
}
$scope.importFile = function(fileContent){
console.debug(fileContent);
ClientDescriptionConverter.save({
realm: realm.realm
}, fileContent, function (data) {
$scope.client = data;
updateProperties();
$scope.importing = true;
});
};
$scope.viewImportDetails = function() {
$modal.open({
templateUrl: resourceUrl + '/partials/modal/view-object.html',
controller: 'JsonModalCtrl',
resolve: {
object: function () {
return $scope.client;
}
}
})
};
$scope.switchChange = function() {
$scope.changed = true;
}

View file

@ -927,6 +927,13 @@ module.factory('Client', function($resource) {
});
});
module.factory('ClientDescriptionConverter', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/client-description-converter', {
realm : '@realm'
});
});
module.factory('ClientInstallation', function($resource) {
var url = authUrl + '/admin/realms/:realm/clients/:client/installation/json';
return {

View file

@ -10,6 +10,20 @@
<form class="form-horizontal" name="clientForm" novalidate kc-read-only="!access.manageClients">
<fieldset class="border-top">
<div class="form-group" data-ng-show="create">
<label for="name" class="col-sm-2 control-label">Import</label>
<div class="col-md-6" data-ng-hide="importing">
<label for="import-file" class="btn btn-default">Select file <i class="pficon pficon-import"></i></label>
<input id="import-file" type="file" class="hidden" kc-on-read-file="importFile($fileContent)">
</div>
<div class="col-md-6" data-ng-show="importing">
<button class="btn btn-default" data-ng-click="viewImportDetails()">View details</button>
<button class="btn btn-default" data-ng-click="reset()">Clear import</button>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="clientId">Client ID <span class="required" data-ng-show="create">*</span></label>
<div class="col-sm-6">
@ -174,6 +188,14 @@
<kc-tooltip>The name ID format to use for the subject.</kc-tooltip>
</div>
<div class="form-group" data-ng-show="!client.bearerOnly">
<label class="col-md-2 control-label" for="rootUrl">Root URL</label>
<div class="col-sm-6">
<input class="form-control" type="text" name="rootUrl" id="rootUrl" data-ng-model="client.rootUrl">
</div>
<kc-tooltip>Root URL appended to relative URLs</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-hide="client.bearerOnly || client.directGrantsOnly">
<label class="col-md-2 control-label" for="newRedirectUri"><span class="required" data-ng-show="protocol != 'saml'">*</span> Valid Redirect URIs</label>
@ -252,7 +274,7 @@
</div>
</div>
<kc-tooltip>Allowed CORS origins.</kc-tooltip>
<kc-tooltip>Allowed CORS origins. To permit all origins of Valid Redirect URIs add '+'. To permit all origins add '*'.</kc-tooltip>
</div>
</fieldset>
<fieldset data-ng-show="protocol == 'saml'">

View file

@ -37,7 +37,7 @@
<td><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></td>
<td>{{client.enabled}}</td>
<td ng-class="{'text-muted': !client.baseUrl}">
<a href="{{client.baseUrl}}" target="_blank" data-ng-show="client.baseUrl">{{client.baseUrl}}</a>
<a href="{{client.rootUrl}}{{client.baseUrl}}" target="_blank" data-ng-show="client.baseUrl">{{client.rootUrl}}{{client.baseUrl}}</a>
<span data-ng-hide="client.baseUrl">Not defined</span>
</td>
<td class="kc-action-cell">

View file

@ -1,8 +1,9 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>Add Realm</h1>
<form class="form-horizontal" name="realmForm" novalidate>
<fieldset>
<legend><span class="text">Create Realm</span></legend>
<div class="form-group">
<label for="name" class="col-sm-2 control-label">Import</label>

View file

@ -241,7 +241,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
if (client != null) {
attributes.put("client", new ClientBean(client));
attributes.put("client", new ClientBean(client, baseUri));
}
attributes.put("login", new LoginBean(formData));
@ -322,7 +322,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
logger.warn("Failed to load properties", e);
}
if (client != null) {
attributes.put("client", new ClientBean(client));
attributes.put("client", new ClientBean(client, baseUri));
}
Properties messagesBundle;

View file

@ -1,6 +1,9 @@
package org.keycloak.login.freemarker.model;
import org.keycloak.models.ClientModel;
import org.keycloak.services.util.ResolveRelative;
import java.net.URI;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -10,8 +13,11 @@ public class ClientBean {
protected ClientModel client;
public ClientBean(ClientModel client) {
private URI requestUri;
public ClientBean(ClientModel client, URI requestUri) {
this.client = client;
this.requestUri = requestUri;
}
public String getClientId() {
@ -23,7 +29,7 @@ public class ClientBean {
}
public String getBaseUrl() {
return client.getBaseUrl();
return ResolveRelative.resolveRelativeUri(requestUri, client.getRootUrl(), client.getBaseUrl());
}
}

View file

@ -1,13 +1,9 @@
package org.keycloak.admin.client.resource;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.List;
import java.util.Map;
@ -28,6 +24,12 @@ public interface RealmResource {
@Path("clients")
ClientsResource clients();
@Path("client-description-converter")
@POST
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN })
@Produces(MediaType.APPLICATION_JSON)
ClientRepresentation convertClientDescription(String description);
@Path("users")
UsersResource users();

View file

@ -56,6 +56,10 @@ public interface ClientModel extends RoleContainerModel {
void setManagementUrl(String url);
String getRootUrl();
void setRootUrl(String url);
String getBaseUrl();
void setBaseUrl(String url);

View file

@ -24,6 +24,7 @@ public class ClientEntity extends AbstractIdentifiableEntity {
private boolean surrogateAuthRequired;
private String managementUrl;
private String rootUrl;
private String baseUrl;
private boolean bearerOnly;
private boolean consentRequired;
@ -196,6 +197,14 @@ public class ClientEntity extends AbstractIdentifiableEntity {
this.managementUrl = managementUrl;
}
public String getRootUrl() {
return rootUrl;
}
public void setRootUrl(String rootUrl) {
this.rootUrl = rootUrl;
}
public String getBaseUrl() {
return baseUrl;
}

View file

@ -306,6 +306,7 @@ public class ModelToRepresentation {
rep.setServiceAccountsEnabled(clientModel.isServiceAccountsEnabled());
rep.setDirectGrantsOnly(clientModel.isDirectGrantsOnly());
rep.setSurrogateAuthRequired(clientModel.isSurrogateAuthRequired());
rep.setRootUrl(clientModel.getRootUrl());
rep.setBaseUrl(clientModel.getBaseUrl());
rep.setNotBefore(clientModel.getNotBefore());
rep.setNodeReRegistrationTimeout(clientModel.getNodeReRegistrationTimeout());

View file

@ -692,6 +692,7 @@ public class RepresentationToModel {
client.setManagementUrl(resourceRep.getAdminUrl());
if (resourceRep.isSurrogateAuthRequired() != null)
client.setSurrogateAuthRequired(resourceRep.isSurrogateAuthRequired());
if (resourceRep.getRootUrl() != null) client.setRootUrl(resourceRep.getRootUrl());
if (resourceRep.getBaseUrl() != null) client.setBaseUrl(resourceRep.getBaseUrl());
if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly());
if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired());
@ -796,6 +797,7 @@ public class RepresentationToModel {
if (rep.isPublicClient() != null) resource.setPublicClient(rep.isPublicClient());
if (rep.isFullScopeAllowed() != null) resource.setFullScopeAllowed(rep.isFullScopeAllowed());
if (rep.isFrontchannelLogout() != null) resource.setFrontchannelLogout(rep.isFrontchannelLogout());
if (rep.getRootUrl() != null) resource.setRootUrl(rep.getRootUrl());
if (rep.getAdminUrl() != null) resource.setManagementUrl(rep.getAdminUrl());
if (rep.getBaseUrl() != null) resource.setBaseUrl(rep.getBaseUrl());
if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired());

View file

@ -421,6 +421,16 @@ public class ClientAdapter implements ClientModel {
entity.setManagementUrl(url);
}
@Override
public void setRootUrl(String url) {
entity.setRootUrl(url);
}
@Override
public String getRootUrl() {
return entity.getRootUrl();
}
@Override
public void setBaseUrl(String url) {
entity.setBaseUrl(url);

View file

@ -345,6 +345,18 @@ public class ClientAdapter implements ClientModel {
updated.setManagementUrl(url);
}
@Override
public String getRootUrl() {
if (updated != null) return updated.getRootUrl();
return cached.getRootUrl();
}
@Override
public void setRootUrl(String url) {
getDelegateForUpdate();
updated.setRootUrl(url);
}
@Override
public String getBaseUrl() {
if (updated != null) return updated.getBaseUrl();

View file

@ -42,6 +42,7 @@ public class CachedClient implements Serializable {
private Set<ProtocolMapperModel> protocolMappers = new HashSet<ProtocolMapperModel>();
private boolean surrogateAuthRequired;
private String managementUrl;
private String rootUrl;
private String baseUrl;
private List<String> defaultRoles = new LinkedList<String>();
private boolean bearerOnly;
@ -76,6 +77,7 @@ public class CachedClient implements Serializable {
}
surrogateAuthRequired = model.isSurrogateAuthRequired();
managementUrl = model.getManagementUrl();
rootUrl = model.getRootUrl();
baseUrl = model.getBaseUrl();
defaultRoles.addAll(model.getDefaultRoles());
bearerOnly = model.isBearerOnly();
@ -169,6 +171,10 @@ public class CachedClient implements Serializable {
return managementUrl;
}
public String getRootUrl() {
return rootUrl;
}
public String getBaseUrl() {
return baseUrl;
}

View file

@ -441,6 +441,16 @@ public class ClientAdapter implements ClientModel {
entity.setManagementUrl(url);
}
@Override
public String getRootUrl() {
return entity.getRootUrl();
}
@Override
public void setRootUrl(String url) {
entity.setRootUrl(url);
}
@Override
public String getBaseUrl() {
return entity.getBaseUrl();

View file

@ -82,6 +82,9 @@ public class ClientEntity {
@Column(name="SURROGATE_AUTH_REQUIRED")
private boolean surrogateAuthRequired;
@Column(name="ROOT_URL")
private String rootUrl;
@Column(name="BASE_URL")
private String baseUrl;
@ -260,6 +263,14 @@ public class ClientEntity {
this.surrogateAuthRequired = surrogateAuthRequired;
}
public String getRootUrl() {
return rootUrl;
}
public void setRootUrl(String rootUrl) {
this.rootUrl = rootUrl;
}
public String getBaseUrl() {
return baseUrl;
}

View file

@ -439,6 +439,17 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
updateMongoEntity();
}
@Override
public void setRootUrl(String url) {
getMongoEntity().setRootUrl(url);
updateMongoEntity();
}
@Override
public String getRootUrl() {
return getMongoEntity().getRootUrl();
}
@Override
public void setBaseUrl(String url) {
getMongoEntity().setBaseUrl(url);

View file

@ -0,0 +1,167 @@
package org.keycloak.protocol.saml;
import org.keycloak.Config;
import org.keycloak.dom.saml.v2.metadata.*;
import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.exportimport.ClientDescriptionConverterFactory;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.saml.v2.util.SAMLMetadataUtil;
import org.keycloak.saml.processing.core.util.CoreConfigUtil;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class EntityDescriptorDescriptionConverter implements ClientDescriptionConverter, ClientDescriptionConverterFactory {
@Override
public boolean isSupported(String description) {
description = description.trim();
return (description.startsWith("<") && description.endsWith(">") && description.contains("EntityDescriptor"));
}
@Override
public ClientRepresentation convertToInternal(String description) {
return loadEntityDescriptors(new ByteArrayInputStream(description.getBytes()));
}
private static ClientRepresentation loadEntityDescriptors(InputStream is) {
Object metadata;
try {
metadata = new SAMLParser().parse(is);
} catch (ParsingException e) {
throw new RuntimeException(e);
}
EntitiesDescriptorType entities;
if (EntitiesDescriptorType.class.isInstance(metadata)) {
entities = (EntitiesDescriptorType) metadata;
} else {
entities = new EntitiesDescriptorType();
entities.addEntityDescriptor(metadata);
}
if (entities.getEntityDescriptor().size() != 1) {
throw new RuntimeException("Expected one entity descriptor");
}
EntityDescriptorType entity = (EntityDescriptorType) entities.getEntityDescriptor().get(0);
String entityId = entity.getEntityID();
ClientRepresentation app = new ClientRepresentation();
app.setClientId(entityId);
Map<String, String> attributes = new HashMap<>();
app.setAttributes(attributes);
List<String> redirectUris = new LinkedList<>();
app.setRedirectUris(redirectUris);
app.setFullScopeAllowed(true);
app.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
attributes.put(SamlProtocol.SAML_SERVER_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); // default to true
attributes.put(SamlProtocol.SAML_SIGNATURE_ALGORITHM, SignatureAlgorithm.RSA_SHA256.toString());
attributes.put(SamlProtocol.SAML_AUTHNSTATEMENT, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
SPSSODescriptorType spDescriptorType = CoreConfigUtil.getSPDescriptor(entity);
if (spDescriptorType.isWantAssertionsSigned()) {
attributes.put(SamlProtocol.SAML_ASSERTION_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
}
String logoutPost = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
if (logoutPost != null) attributes.put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, logoutPost);
String logoutRedirect = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
if (logoutPost != null) attributes.put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, logoutRedirect);
String assertionConsumerServicePostBinding = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
if (assertionConsumerServicePostBinding != null) {
attributes.put(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, assertionConsumerServicePostBinding);
redirectUris.add(assertionConsumerServicePostBinding);
}
String assertionConsumerServiceRedirectBinding = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
if (assertionConsumerServiceRedirectBinding != null) {
attributes.put(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, assertionConsumerServiceRedirectBinding);
redirectUris.add(assertionConsumerServiceRedirectBinding);
}
for (KeyDescriptorType keyDescriptor : spDescriptorType.getKeyDescriptor()) {
X509Certificate cert = null;
try {
cert = SAMLMetadataUtil.getCertificate(keyDescriptor);
} catch (ConfigurationException e) {
throw new RuntimeException(e);
} catch (ProcessingException e) {
throw new RuntimeException(e);
}
String certPem = KeycloakModelUtils.getPemFromCertificate(cert);
if (keyDescriptor.getUse() == KeyTypes.SIGNING) {
attributes.put(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
attributes.put(SamlProtocol.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, certPem);
} else if (keyDescriptor.getUse() == KeyTypes.ENCRYPTION) {
attributes.put(SamlProtocol.SAML_ENCRYPT, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
attributes.put(SamlProtocol.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, certPem);
}
}
return app;
}
private static String getLogoutLocation(SPSSODescriptorType idp, String bindingURI) {
String logoutResponseLocation = null;
List<EndpointType> endpoints = idp.getSingleLogoutService();
for (EndpointType endpoint : endpoints) {
if (endpoint.getBinding().toString().equals(bindingURI)) {
if (endpoint.getLocation() != null) {
logoutResponseLocation = endpoint.getLocation().toString();
} else {
logoutResponseLocation = null;
}
break;
}
}
return logoutResponseLocation;
}
@Override
public ClientDescriptionConverter create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "saml2-entity-descriptor";
}
}

View file

@ -1,21 +0,0 @@
package org.keycloak.protocol.saml;
import org.keycloak.exportimport.ClientImporter;
import org.keycloak.models.RealmModel;
import org.keycloak.services.resources.admin.RealmAuth;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class EntityDescriptorImporter implements ClientImporter {
@Override
public Object createJaxrsService(RealmModel realm, RealmAuth auth) {
return new EntityDescriptorImporterService(realm, auth);
}
@Override
public void close() {
}
}

View file

@ -1,42 +0,0 @@
package org.keycloak.protocol.saml;
import org.keycloak.Config;
import org.keycloak.exportimport.ClientImporter;
import org.keycloak.exportimport.ClientImporterFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class EntityDescriptorImporterFactory implements ClientImporterFactory {
@Override
public String getDisplayName() {
return "SAML 2.0 Entity Descriptor";
}
@Override
public ClientImporter create(KeycloakSession session) {
return new EntityDescriptorImporter();
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "saml2-entity-descriptor";
}
}

View file

@ -1,150 +0,0 @@
package org.keycloak.protocol.saml;
import org.jboss.resteasy.plugins.providers.multipart.InputPart;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.services.resources.admin.RealmAuth;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.saml.v2.util.SAMLMetadataUtil;
import org.keycloak.saml.processing.core.util.CoreConfigUtil;
import org.keycloak.dom.saml.v2.metadata.EndpointType;
import org.keycloak.dom.saml.v2.metadata.EntitiesDescriptorType;
import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType;
import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType;
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class EntityDescriptorImporterService {
protected RealmModel realm;
protected RealmAuth auth;
public EntityDescriptorImporterService(RealmModel realm, RealmAuth auth) {
this.realm = realm;
this.auth = auth;
}
@POST
@Path("upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public void updateEntityDescriptor(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
auth.requireManage();
Map<String, List<InputPart>> uploadForm = input.getFormDataMap();
List<InputPart> inputParts = uploadForm.get("file");
InputStream is = inputParts.get(0).getBody(InputStream.class, null);
loadEntityDescriptors(is, realm);
}
public static void loadEntityDescriptors(InputStream is, RealmModel realm) {
Object metadata = null;
try {
metadata = new SAMLParser().parse(is);
} catch (ParsingException e) {
throw new RuntimeException(e);
}
EntitiesDescriptorType entities;
if (EntitiesDescriptorType.class.isInstance(metadata)) {
entities = (EntitiesDescriptorType) metadata;
} else {
entities = new EntitiesDescriptorType();
entities.addEntityDescriptor(metadata);
}
for (Object o : entities.getEntityDescriptor()) {
EntityDescriptorType entity = (EntityDescriptorType)o;
String entityId = entity.getEntityID();
ClientModel app = realm.addClient(entityId);
app.setFullScopeAllowed(true);
app.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
app.setAttribute(SamlProtocol.SAML_SERVER_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); // default to true
app.setAttribute(SamlProtocol.SAML_SIGNATURE_ALGORITHM, SignatureAlgorithm.RSA_SHA256.toString());
app.setAttribute(SamlProtocol.SAML_AUTHNSTATEMENT, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
SPSSODescriptorType spDescriptorType = CoreConfigUtil.getSPDescriptor(entity);
if (spDescriptorType.isWantAssertionsSigned()) {
app.setAttribute(SamlProtocol.SAML_ASSERTION_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
}
String logoutPost = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
if (logoutPost != null) app.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, logoutPost);
String logoutRedirect = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
if (logoutPost != null) app.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, logoutRedirect);
String assertionConsumerServicePostBinding = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
if (assertionConsumerServicePostBinding != null) {
app.setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, assertionConsumerServicePostBinding);
app.addRedirectUri(assertionConsumerServicePostBinding);
}
String assertionConsumerServiceRedirectBinding = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
if (assertionConsumerServiceRedirectBinding != null) {
app.setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, assertionConsumerServiceRedirectBinding);
app.addRedirectUri(assertionConsumerServiceRedirectBinding);
}
for (KeyDescriptorType keyDescriptor : spDescriptorType.getKeyDescriptor()) {
X509Certificate cert = null;
try {
cert = SAMLMetadataUtil.getCertificate(keyDescriptor);
} catch (ConfigurationException e) {
throw new RuntimeException(e);
} catch (ProcessingException e) {
throw new RuntimeException(e);
}
String certPem = KeycloakModelUtils.getPemFromCertificate(cert);
if (keyDescriptor.getUse() == KeyTypes.SIGNING) {
app.setAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
app.setAttribute(SamlProtocol.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, certPem);
} else if (keyDescriptor.getUse() == KeyTypes.ENCRYPTION) {
app.setAttribute(SamlProtocol.SAML_ENCRYPT, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
app.setAttribute(SamlProtocol.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, certPem);
}
}
}
}
public static String getLogoutLocation(SPSSODescriptorType idp, String bindingURI) {
String logoutResponseLocation = null;
List<EndpointType> endpoints = idp.getSingleLogoutService();
for (EndpointType endpoint : endpoints) {
if (endpoint.getBinding().toString().equals(bindingURI)) {
if (endpoint.getLocation() != null) {
logoutResponseLocation = endpoint.getLocation().toString();
} else {
logoutResponseLocation = null;
}
break;
}
}
return logoutResponseLocation;
}
}

View file

@ -485,7 +485,7 @@ public class SamlProtocol implements LoginProtocol {
}
if (logoutServiceUrl == null && client instanceof ClientModel) logoutServiceUrl = ((ClientModel)client).getManagementUrl();
if (logoutServiceUrl == null || logoutServiceUrl.trim().equals("")) return null;
return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), logoutServiceUrl);
return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), client.getRootUrl(), logoutServiceUrl);
}

View file

@ -0,0 +1 @@
org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter

View file

@ -1 +0,0 @@
org.keycloak.protocol.saml.EntityDescriptorImporterFactory

View file

@ -1,8 +1,7 @@
package org.keycloak.exportimport;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;
import org.keycloak.services.resources.admin.RealmAuth;
import org.keycloak.representations.idm.ClientRepresentation;
/**
* Provider plugin interface for importing clients from an arbitrary configuration format
@ -10,6 +9,8 @@ import org.keycloak.services.resources.admin.RealmAuth;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface ClientImporter extends Provider {
public Object createJaxrsService(RealmModel realm, RealmAuth auth);
public interface ClientDescriptionConverter extends Provider {
ClientRepresentation convertToInternal(String description);
}

View file

@ -8,6 +8,8 @@ import org.keycloak.provider.ProviderFactory;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface ClientImporterFactory extends ProviderFactory<ClientImporter> {
public String getDisplayName();
public interface ClientDescriptionConverterFactory extends ProviderFactory<ClientDescriptionConverter> {
boolean isSupported(String description);
}

View file

@ -7,7 +7,7 @@ import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientImportSpi implements Spi {
public class ClientDescriptionConverterSpi implements Spi {
@Override
public boolean isInternal() {
@ -16,16 +16,17 @@ public class ClientImportSpi implements Spi {
@Override
public String getName() {
return "client-import";
return "client-description-converter";
}
@Override
public Class<? extends Provider> getProviderClass() {
return ClientImporter.class;
return ClientDescriptionConverter.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return ClientImporterFactory.class;
return ClientDescriptionConverterFactory.class;
}
}

View file

@ -0,0 +1,58 @@
package org.keycloak.exportimport;
import org.keycloak.Config;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class KeycloakClientDescriptionConverter implements ClientDescriptionConverterFactory, ClientDescriptionConverter {
public static final String ID = "keycloak";
@Override
public boolean isSupported(String description) {
description = description.trim();
return (description.startsWith("{") && description.endsWith("}") && description.contains("\"clientId\""));
}
@Override
public ClientRepresentation convertToInternal(String description) {
try {
return JsonSerialization.readValue(description, ClientRepresentation.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public ClientDescriptionConverter create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return ID;
}
}

View file

@ -0,0 +1,65 @@
package org.keycloak.protocol.oidc;
import org.keycloak.Config;
import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.exportimport.ClientDescriptionConverterFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.representations.OIDCClientRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class OIDCClientDescriptionConverter implements ClientDescriptionConverter, ClientDescriptionConverterFactory {
@Override
public boolean isSupported(String description) {
description = description.trim();
return (description.startsWith("{") && description.endsWith("}") && description.contains("\"redirect_uris\""));
}
@Override
public ClientRepresentation convertToInternal(String description) {
try {
OIDCClientRepresentation oidcRep = JsonSerialization.readValue(description, OIDCClientRepresentation.class);
ClientRepresentation client = new ClientRepresentation();
client.setClientId(KeycloakModelUtils.generateId());
client.setName(oidcRep.getClientName());
client.setRedirectUris(oidcRep.getRedirectUris());
client.setBaseUrl(oidcRep.getClientUri());
return client;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public ClientDescriptionConverter create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "openid-connect";
}
}

View file

@ -24,6 +24,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper;
import org.keycloak.protocol.oidc.utils.WebOriginsUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
@ -217,7 +218,7 @@ public class TokenManager {
}
public AccessToken createClientAccessToken(KeycloakSession session, Set<RoleModel> requestedRoles, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession, ClientSessionModel clientSession) {
AccessToken token = initToken(realm, client, user, userSession, clientSession);
AccessToken token = initToken(realm, client, user, userSession, clientSession, session.getContext().getUri());
for (RoleModel role : requestedRoles) {
addComposites(token, role);
}
@ -380,7 +381,7 @@ public class TokenManager {
}
protected AccessToken initToken(RealmModel realm, ClientModel client, UserModel user, UserSessionModel session, ClientSessionModel clientSession) {
protected AccessToken initToken(RealmModel realm, ClientModel client, UserModel user, UserSessionModel session, ClientSessionModel clientSession, UriInfo uriInfo) {
AccessToken token = new AccessToken();
if (clientSession != null) token.clientSession(clientSession.getId());
token.id(KeycloakModelUtils.generateId());
@ -398,7 +399,7 @@ public class TokenManager {
}
Set<String> allowedOrigins = client.getWebOrigins();
if (allowedOrigins != null) {
token.setAllowedOrigins(allowedOrigins);
token.setAllowedOrigins(WebOriginsUtils.resolveValidWebOrigins(uriInfo, client));
}
return token;
}

View file

@ -58,7 +58,7 @@ public class LoginStatusIframeEndpoint {
}
}
for (String r : RedirectUtils.resolveValidRedirects(uriInfo, client.getRedirectUris())) {
for (String r : RedirectUtils.resolveValidRedirects(uriInfo, client.getRootUrl(), client.getRedirectUris())) {
int i = r.indexOf('/', 8);
if (i != -1) {
r = r.substring(0, i);

View file

@ -0,0 +1,45 @@
package org.keycloak.protocol.oidc.representations;
import org.codehaus.jackson.annotate.JsonProperty;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class OIDCClientRepresentation {
@JsonProperty("redirect_uris")
private List<String> redirectUris;
@JsonProperty("client_name")
private String clientName;
@JsonProperty("client_uri")
private String clientUri;
public List<String> getRedirectUris() {
return redirectUris;
}
public void setRedirectUris(List<String> redirectUris) {
this.redirectUris = redirectUris;
}
public String getClientName() {
return clientName;
}
public void setClientName(String clientName) {
this.clientName = clientName;
}
public String getClientUri() {
return clientUri;
}
public void setClientUri(String clientUri) {
this.clientUri = clientUri;
}
}

View file

@ -19,22 +19,22 @@ public class RedirectUtils {
private static final Logger logger = Logger.getLogger(RedirectUtils.class);
public static String verifyRealmRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm) {
Set<String> validRedirects = getValidateRedirectUris(realm);
return verifyRedirectUri(uriInfo, redirectUri, realm, validRedirects);
Set<String> validRedirects = getValidateRedirectUris(uriInfo, realm);
return verifyRedirectUri(uriInfo, null, redirectUri, realm, validRedirects);
}
public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client) {
Set<String> validRedirects = client.getRedirectUris();
return verifyRedirectUri(uriInfo, redirectUri, realm, validRedirects);
return verifyRedirectUri(uriInfo, client.getRootUrl(), redirectUri, realm, validRedirects);
}
public static Set<String> resolveValidRedirects(UriInfo uriInfo, Set<String> validRedirects) {
public static Set<String> resolveValidRedirects(UriInfo uriInfo, String rootUrl, Set<String> validRedirects) {
// If the valid redirect URI is relative (no scheme, host, port) then use the request's scheme, host, and port
Set<String> resolveValidRedirects = new HashSet<String>();
for (String validRedirect : validRedirects) {
resolveValidRedirects.add(validRedirect); // add even relative urls.
if (validRedirect.startsWith("/")) {
validRedirect = relativeToAbsoluteURI(uriInfo, validRedirect);
validRedirect = relativeToAbsoluteURI(uriInfo, rootUrl, validRedirect);
logger.debugv("replacing relative valid redirect with: {0}", validRedirect);
resolveValidRedirects.add(validRedirect);
}
@ -42,17 +42,15 @@ public class RedirectUtils {
return resolveValidRedirects;
}
private static Set<String> getValidateRedirectUris(RealmModel realm) {
Set<String> redirects = new HashSet<String>();
private static Set<String> getValidateRedirectUris(UriInfo uriInfo, RealmModel realm) {
Set<String> redirects = new HashSet<>();
for (ClientModel client : realm.getClients()) {
for (String redirect : client.getRedirectUris()) {
redirects.add(redirect);
}
redirects.addAll(resolveValidRedirects(uriInfo, client.getRootUrl(), client.getRedirectUris()));
}
return redirects;
}
private static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, Set<String> validRedirects) {
private static String verifyRedirectUri(UriInfo uriInfo, String rootUrl, String redirectUri, RealmModel realm, Set<String> validRedirects) {
if (redirectUri == null) {
if (validRedirects.size() != 1) return null;
String validRedirect = validRedirects.iterator().next();
@ -66,7 +64,7 @@ public class RedirectUtils {
redirectUri = null;
} else {
String r = redirectUri.indexOf('?') != -1 ? redirectUri.substring(0, redirectUri.indexOf('?')) : redirectUri;
Set<String> resolveValidRedirects = resolveValidRedirects(uriInfo, validRedirects);
Set<String> resolveValidRedirects = resolveValidRedirects(uriInfo, rootUrl, validRedirects);
boolean valid = matchesRedirects(resolveValidRedirects, r);
@ -86,7 +84,7 @@ public class RedirectUtils {
valid = matchesRedirects(resolveValidRedirects, r);
}
if (valid && redirectUri.startsWith("/")) {
redirectUri = relativeToAbsoluteURI(uriInfo, redirectUri);
redirectUri = relativeToAbsoluteURI(uriInfo, rootUrl, redirectUri);
}
redirectUri = valid ? redirectUri : null;
}
@ -98,13 +96,16 @@ public class RedirectUtils {
}
}
private static String relativeToAbsoluteURI(UriInfo uriInfo, String relative) {
private static String relativeToAbsoluteURI(UriInfo uriInfo, String rootUrl, String relative) {
if (rootUrl == null) {
URI baseUri = uriInfo.getBaseUri();
String uri = baseUri.getScheme() + "://" + baseUri.getHost();
if (baseUri.getPort() != -1) {
uri += ":" + baseUri.getPort();
}
relative = uri + relative;
rootUrl = uri;
}
relative = rootUrl + relative;
return relative;
}

View file

@ -0,0 +1,30 @@
package org.keycloak.protocol.oidc.utils;
import org.keycloak.models.ClientModel;
import org.keycloak.util.UriUtils;
import javax.ws.rs.core.UriInfo;
import java.util.Set;
/**
* Created by st on 22.09.15.
*/
public class WebOriginsUtils {
public static final String INCLUDE_REDIRECTS = "+";
public static Set<String> resolveValidWebOrigins(UriInfo uriInfo, ClientModel client) {
Set<String> webOrigins = client.getWebOrigins();
if (webOrigins != null && webOrigins.contains("+")) {
webOrigins.remove(INCLUDE_REDIRECTS);
client.getRedirectUris();
for (String redirectUri : RedirectUtils.resolveValidRedirects(uriInfo, client.getRootUrl(), client.getRedirectUris())) {
if (redirectUri.startsWith("http://") || redirectUri.startsWith("https://")) {
webOrigins.add(UriUtils.getOrigin(redirectUri));
}
}
}
return webOrigins;
}
}

View file

@ -46,8 +46,8 @@ public class ResourceAdminManager {
this.session = session;
}
public static String resolveUri(URI requestUri, String uri) {
String absoluteURI = ResolveRelative.resolveRelativeUri(requestUri, uri);
public static String resolveUri(URI requestUri, String rootUrl, String uri) {
String absoluteURI = ResolveRelative.resolveRelativeUri(requestUri, rootUrl, uri);
return StringPropertyReplacer.replaceProperties(absoluteURI);
}
@ -58,8 +58,7 @@ public class ResourceAdminManager {
return null;
}
// this is to support relative admin urls when keycloak and clients are deployed on the same machine
String absoluteURI = ResolveRelative.resolveRelativeUri(requestUri, mgmtUrl);
String absoluteURI = ResolveRelative.resolveRelativeUri(requestUri, client.getRootUrl(), mgmtUrl);
// this is for resolving URI like "http://${jboss.host.name}:8080/..." in order to send request to same machine and avoid request to LB in cluster environment
return StringPropertyReplacer.replaceProperties(absoluteURI);

View file

@ -779,7 +779,7 @@ public class AccountService extends AbstractSecuredLocalService {
if (referrerUri != null) {
referrerUri = RedirectUtils.verifyRedirectUri(uriInfo, referrerUri, realm, referrerClient);
} else {
referrerUri = ResolveRelative.resolveRelativeUri(uriInfo.getRequestUri(), referrerClient.getBaseUrl());
referrerUri = ResolveRelative.resolveRelativeUri(uriInfo.getRequestUri(), client.getRootUrl(), referrerClient.getBaseUrl());
}
if (referrerUri != null) {

View file

@ -0,0 +1,100 @@
package org.keycloak.services.resources;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.events.EventBuilder;
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.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ErrorResponse;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientRegistrationService {
protected static final Logger logger = Logger.getLogger(ClientRegistrationService.class);
private RealmModel realm;
private EventBuilder event;
@Context
private KeycloakSession session;
public ClientRegistrationService(RealmModel realm, EventBuilder event) {
this.realm = realm;
this.event = event;
}
@POST
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN })
public Response create(String description, @QueryParam("format") String format) {
if (format == null) {
format = KeycloakClientDescriptionConverter.ID;
}
ClientDescriptionConverter converter = session.getProvider(ClientDescriptionConverter.class, format);
if (converter == null) {
throw new BadRequestException("Invalid format");
}
ClientRepresentation rep = converter.convertToInternal(description);
try {
ClientModel clientModel = RepresentationToModel.createClient(session, realm, rep, true);
rep = ModelToRepresentation.toRepresentation(clientModel);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
return Response.created(uri).entity(rep).build();
} catch (ModelDuplicateException e) {
return ErrorResponse.exists("Client " + rep.getClientId() + " already exists");
}
}
@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();
if (client == null) {
throw new NotFoundException("Client not found");
}
return ModelToRepresentation.toRepresentation(client);
}
@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");
}
RepresentationToModel.updateClient(rep, client);
}
@DELETE
@Path("{clientId}")
public void delete(@PathParam("clientId") String clientId) {
ClientModel client = realm.getClientByClientId(clientId);
if (client == null) {
throw new NotFoundException("Client not found");
}
realm.removeClient(client.getId());
}
}

View file

@ -112,6 +112,15 @@ 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}/clients-managements")
public ClientsManagementService getClientsManagementService(final @PathParam("realm") String name) {
RealmModel realm = init(name);

View file

@ -13,7 +13,8 @@ import org.keycloak.events.EventType;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.events.admin.AdminEventQuery;
import org.keycloak.events.admin.OperationType;
import org.keycloak.exportimport.ClientImporter;
import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.exportimport.ClientDescriptionConverterFactory;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
@ -25,7 +26,9 @@ import org.keycloak.models.cache.CacheUserProvider;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.adapters.action.GlobalRequestResult;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
@ -99,10 +102,18 @@ public class RealmAdminResource {
*
* @return
*/
@Path("client-importers/{formatId}")
public Object getClientImporter(@PathParam("formatId") String formatId) {
ClientImporter importer = session.getProvider(ClientImporter.class, formatId);
return importer.createJaxrsService(realm, auth);
@Path("client-description-converter")
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN })
@POST
@Produces(MediaType.APPLICATION_JSON)
public ClientRepresentation convertClientDescription(String description) {
for (ProviderFactory<ClientDescriptionConverter> factory : session.getKeycloakSessionFactory().getProviderFactories(ClientDescriptionConverter.class)) {
if (((ClientDescriptionConverterFactory) factory).isSupported(description)) {
return factory.create(session).convertToInternal(description);
}
}
throw new BadRequestException("Unsupported format");
}
/**

View file

@ -4,8 +4,6 @@ import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.IdentityProviderFactory;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.OperationType;
import org.keycloak.exportimport.ClientImporter;
import org.keycloak.exportimport.ClientImporterFactory;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.models.KeycloakSession;
@ -51,7 +49,6 @@ public class ServerInfoAdminResource {
setSocialProviders(info);
setIdentityProviders(info);
setThemes(info);
setClientImporters(info);
setProviders(info);
setProtocolMapperTypes(info);
setBuiltinProtocolMappers(info);
@ -144,7 +141,7 @@ public class ServerInfoAdminResource {
ProtocolMapper mapper = (ProtocolMapper)p;
List<ProtocolMapperTypeRepresentation> types = info.getProtocolMapperTypes().get(mapper.getProtocol());
if (types == null) {
types = new LinkedList<ProtocolMapperTypeRepresentation>();
types = new LinkedList<>();
info.getProtocolMapperTypes().put(mapper.getProtocol(), types);
}
ProtocolMapperTypeRepresentation rep = new ProtocolMapperTypeRepresentation();
@ -179,17 +176,6 @@ public class ServerInfoAdminResource {
}
}
private void setClientImporters(ServerInfoRepresentation info) {
info.setClientImporters(new LinkedList<Map<String, String>>());
for (ProviderFactory p : session.getKeycloakSessionFactory().getProviderFactories(ClientImporter.class)) {
ClientImporterFactory factory = (ClientImporterFactory)p;
Map<String, String> data = new HashMap<String, String>();
data.put("id", factory.getId());
data.put("name", factory.getDisplayName());
info.getClientImporters().add(data);
}
}
private static Map<String, List<String>> createEnumsMap(Class... enums) {
Map<String, List<String>> m = new HashMap<>();
for (Class e : enums) {

View file

@ -8,8 +8,11 @@ import java.net.URI;
* @version $Revision: 1 $
*/
public class ResolveRelative {
public static String resolveRelativeUri(URI requestUri, String url) {
public static String resolveRelativeUri(URI requestUri, String rootUrl, String url) {
if (url == null || !url.startsWith("/")) return url;
if (rootUrl != null) {
return rootUrl + url;
} else {
UriBuilder builder = UriBuilder.fromPath(url).host(requestUri.getHost());
builder.scheme(requestUri.getScheme());
if (requestUri.getPort() != -1) {
@ -18,3 +21,4 @@ public class ResolveRelative {
return builder.build().toString();
}
}
}

View file

@ -0,0 +1,2 @@
org.keycloak.exportimport.KeycloakClientDescriptionConverter
org.keycloak.protocol.oidc.OIDCClientDescriptionConverter

View file

@ -1,6 +1,6 @@
org.keycloak.protocol.LoginProtocolSpi
org.keycloak.protocol.ProtocolMapperSpi
org.keycloak.exportimport.ClientImportSpi
org.keycloak.exportimport.ClientDescriptionConverterSpi
org.keycloak.wellknown.WellKnownSpi
org.keycloak.messages.MessagesSpi
org.keycloak.authentication.AuthenticatorSpi

View file

@ -177,6 +177,7 @@ public class AdminAPITest {
if (appRep.isBearerOnly() != null) Assert.assertEquals(appRep.isBearerOnly(), storedApp.isBearerOnly());
if (appRep.isPublicClient() != null) Assert.assertEquals(appRep.isPublicClient(), storedApp.isPublicClient());
if (appRep.isFullScopeAllowed() != null) Assert.assertEquals(appRep.isFullScopeAllowed(), storedApp.isFullScopeAllowed());
if (appRep.getRootUrl() != null) Assert.assertEquals(appRep.getRootUrl(), storedApp.getRootUrl());
if (appRep.getAdminUrl() != null) Assert.assertEquals(appRep.getAdminUrl(), storedApp.getAdminUrl());
if (appRep.getBaseUrl() != null) Assert.assertEquals(appRep.getBaseUrl(), storedApp.getBaseUrl());
if (appRep.isSurrogateAuthRequired() != null) Assert.assertEquals(appRep.isSurrogateAuthRequired(), storedApp.isSurrogateAuthRequired());

View file

@ -1,13 +1,18 @@
package org.keycloak.testsuite.admin;
import org.apache.commons.io.IOUtils;
import org.junit.Test;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.NotFoundException;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@ -132,4 +137,35 @@ public class RealmTest extends AbstractClientTest {
}
}
@Test
public void convertKeycloakClientDescription() throws IOException {
ClientRepresentation description = new ClientRepresentation();
description.setClientId("client-id");
description.setRedirectUris(Collections.singletonList("http://localhost"));
ClientRepresentation converted = realm.convertClientDescription(JsonSerialization.writeValueAsString(description));
assertEquals("client-id", converted.getClientId());
assertEquals("http://localhost", converted.getRedirectUris().get(0));
}
@Test
public void convertOIDCClientDescription() throws IOException {
String description = IOUtils.toString(getClass().getResourceAsStream("/client-descriptions/client-oidc.json"));
ClientRepresentation converted = realm.convertClientDescription(description);
assertEquals(36, converted.getClientId().length());
assertEquals(1, converted.getRedirectUris().size());
assertEquals("http://localhost", converted.getRedirectUris().get(0));
}
@Test
public void convertSAMLClientDescription() throws IOException {
String description = IOUtils.toString(getClass().getResourceAsStream("/client-descriptions/saml-entity-descriptor.xml"));
ClientRepresentation converted = realm.convertClientDescription(description);
assertEquals("loadbalancer-9.siroe.com", converted.getClientId());
assertEquals(1, converted.getRedirectUris().size());
assertEquals("https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp", converted.getRedirectUris().get(0));
}
}

View file

@ -30,7 +30,7 @@ public class SamlAdapterTest {
initializeSamlSecuredWar("/keycloak-saml/bad-client-signed-post", "/bad-client-sales-post-sig", "bad-client-post-sig.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/bad-realm-signed-post", "/bad-realm-sales-post-sig", "bad-realm-post-sig.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/encrypted-post", "/sales-post-enc", "post-enc.war", classLoader);
SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth", this);
SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
server.getServer().deploy(createDeploymentInfo("employee.war", "/employee", SamlSPFacade.class));

View file

@ -1,5 +1,6 @@
package org.keycloak.testsuite.keycloaksaml;
import org.apache.commons.io.IOUtils;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput;
import org.junit.Assert;
import org.junit.ClassRule;
@ -8,6 +9,8 @@ import org.junit.Test;
import org.junit.rules.ExternalResource;
import org.keycloak.Config;
import org.keycloak.adapters.saml.SamlPrincipal;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
@ -24,6 +27,7 @@ import org.keycloak.protocol.saml.mappers.HardcodedRole;
import org.keycloak.protocol.saml.mappers.RoleListMapper;
import org.keycloak.protocol.saml.mappers.RoleNameMapper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
import org.keycloak.services.managers.RealmManager;
@ -34,6 +38,7 @@ import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.keycloak.util.JsonSerialization;
import org.openqa.selenium.WebDriver;
import javax.ws.rs.client.Client;
@ -51,6 +56,8 @@ import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;
import static org.junit.Assert.assertEquals;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@ -106,9 +113,9 @@ public class SamlAdapterTestStrategy extends ExternalResource {
public void testPostSimpleLoginLogout() {
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/");
Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post/");
System.out.println(driver.getPageSource());
Assert.assertTrue(driver.getPageSource().contains("bburke"));
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post?GLO=true");
@ -117,9 +124,9 @@ public class SamlAdapterTestStrategy extends ExternalResource {
public void testPostSimpleUnauthorized(CheckAuthError error) {
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/");
Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
loginPage.login("unauthorized", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post/");
System.out.println(driver.getPageSource());
error.check(driver);
}
@ -127,7 +134,7 @@ public class SamlAdapterTestStrategy extends ExternalResource {
public void testPostSimpleLoginLogoutIdpInitiated() {
driver.navigate().to(AUTH_SERVER_URL + "/realms/demo/protocol/saml/clients/sales-post");
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post/");
System.out.println(driver.getPageSource());
Assert.assertTrue(driver.getPageSource().contains("bburke"));
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post?GLO=true");
@ -136,9 +143,9 @@ public class SamlAdapterTestStrategy extends ExternalResource {
public void testPostSignedLoginLogout() {
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-sig/");
Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig/");
Assert.assertTrue(driver.getPageSource().contains("bburke"));
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-sig?GLO=true");
checkLoggedOut(APP_SERVER_BASE_URL + "/sales-post-sig/");
@ -146,9 +153,9 @@ public class SamlAdapterTestStrategy extends ExternalResource {
}
public void testPostSignedLoginLogoutTransientNameID() {
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-sig-transient/");
Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig-transient/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig-transient/");
System.out.println(driver.getPageSource());
Assert.assertFalse(driver.getPageSource().contains("bburke"));
Assert.assertTrue(driver.getPageSource().contains("principal=G-"));
@ -158,9 +165,9 @@ public class SamlAdapterTestStrategy extends ExternalResource {
}
public void testPostSignedLoginLogoutPersistentNameID() {
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-sig-persistent/");
Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig-persistent/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig-persistent/");
System.out.println(driver.getPageSource());
Assert.assertFalse(driver.getPageSource().contains("bburke"));
Assert.assertTrue(driver.getPageSource().contains("principal=G-"));
@ -170,9 +177,9 @@ public class SamlAdapterTestStrategy extends ExternalResource {
}
public void testPostSignedLoginLogoutEmailNameID() {
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-sig-email/");
Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig-email/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig-email/");
System.out.println(driver.getPageSource());
Assert.assertTrue(driver.getPageSource().contains("principal=bburke@redhat.com"));
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-sig-email?GLO=true");
@ -188,8 +195,8 @@ public class SamlAdapterTestStrategy extends ExternalResource {
Assert.assertTrue(driver.getCurrentUrl().startsWith(AUTH_SERVER_URL + "/realms/demo/protocol/saml"));
System.out.println(driver.getCurrentUrl());
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee/");
Assert.assertEquals(SamlSPFacade.sentRelayState, SamlSPFacade.RELAY_STATE);
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee/");
assertEquals(SamlSPFacade.sentRelayState, SamlSPFacade.RELAY_STATE);
Assert.assertNotNull(SamlSPFacade.samlResponse);
}
@ -206,13 +213,13 @@ public class SamlAdapterTestStrategy extends ExternalResource {
requiredRoles.add("user");
SendUsernameServlet.checkRoles = requiredRoles;
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee2/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee2/");
SendUsernameServlet.checkRoles = null;
SamlPrincipal principal = (SamlPrincipal) SendUsernameServlet.sentPrincipal;
Assert.assertNotNull(principal);
Assert.assertEquals("bburke@redhat.com", principal.getAttribute(X500SAMLProfileConstants.EMAIL.get()));
Assert.assertEquals("bburke@redhat.com", principal.getFriendlyAttribute("email"));
Assert.assertEquals("617", principal.getAttribute("phone"));
assertEquals("bburke@redhat.com", principal.getAttribute(X500SAMLProfileConstants.EMAIL.get()));
assertEquals("bburke@redhat.com", principal.getFriendlyAttribute("email"));
assertEquals("617", principal.getAttribute("phone"));
Assert.assertNull(principal.getFriendlyAttribute("phone"));
driver.navigate().to(APP_SERVER_BASE_URL + "/employee2/?GLO=true");
checkLoggedOut(APP_SERVER_BASE_URL + "/employee2/");
@ -252,11 +259,11 @@ public class SamlAdapterTestStrategy extends ExternalResource {
requiredRoles.add("pee-on");
SendUsernameServlet.checkRoles = requiredRoles;
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee2/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee2/");
SendUsernameServlet.checkRoles = null;
SamlPrincipal principal = (SamlPrincipal) SendUsernameServlet.sentPrincipal;
Assert.assertNotNull(principal);
Assert.assertEquals("hard", principal.getAttribute("hardcoded-attribute"));
assertEquals("hard", principal.getAttribute("hardcoded-attribute"));
}
@ -266,7 +273,7 @@ public class SamlAdapterTestStrategy extends ExternalResource {
driver.navigate().to(APP_SERVER_BASE_URL + "/employee-sig/");
Assert.assertTrue(driver.getCurrentUrl().startsWith(AUTH_SERVER_URL + "/realms/demo/protocol/saml"));
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig/");
Assert.assertTrue(driver.getPageSource().contains("bburke"));
driver.navigate().to(APP_SERVER_BASE_URL + "/employee-sig?GLO=true");
checkLoggedOut(APP_SERVER_BASE_URL + "/employee-sig/");
@ -277,7 +284,7 @@ public class SamlAdapterTestStrategy extends ExternalResource {
driver.navigate().to(APP_SERVER_BASE_URL + "/employee-sig-front/");
Assert.assertTrue(driver.getCurrentUrl().startsWith(AUTH_SERVER_URL + "/realms/demo/protocol/saml"));
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig-front/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig-front/");
Assert.assertTrue(driver.getPageSource().contains("bburke"));
driver.navigate().to(APP_SERVER_BASE_URL + "/employee-sig-front?GLO=true");
checkLoggedOut(APP_SERVER_BASE_URL + "/employee-sig-front/");
@ -291,19 +298,19 @@ public class SamlAdapterTestStrategy extends ExternalResource {
Assert.assertTrue(driver.getCurrentUrl().startsWith(AUTH_SERVER_URL + "/realms/demo/protocol/saml"));
System.out.println("login to form");
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig/");
Assert.assertTrue(driver.getPageSource().contains("bburke"));
// visit 2nd app
System.out.println("visit 2nd app ");
driver.navigate().to(APP_SERVER_BASE_URL + "/employee-sig-front/");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig-front/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig-front/");
Assert.assertTrue(driver.getPageSource().contains("bburke"));
// visit 3rd app
System.out.println("visit 3rd app ");
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-sig/");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig/");
Assert.assertTrue(driver.getPageSource().contains("bburke"));
// logout of first app
@ -320,9 +327,9 @@ public class SamlAdapterTestStrategy extends ExternalResource {
public void testPostEncryptedLoginLogout() {
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-enc/");
Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-enc/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-enc/");
Assert.assertTrue(driver.getPageSource().contains("bburke"));
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-enc?GLO=true");
checkLoggedOut(APP_SERVER_BASE_URL + "/sales-post-enc/");
@ -330,8 +337,8 @@ public class SamlAdapterTestStrategy extends ExternalResource {
}
public void testPostBadClientSignature() {
driver.navigate().to(APP_SERVER_BASE_URL + "/bad-client-sales-post-sig/");
Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
Assert.assertEquals(driver.getTitle(), "We're sorry...");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
assertEquals(driver.getTitle(), "We're sorry...");
}
public static interface CheckAuthError {
@ -340,39 +347,19 @@ public class SamlAdapterTestStrategy extends ExternalResource {
public void testPostBadRealmSignature(CheckAuthError error) {
driver.navigate().to(APP_SERVER_BASE_URL + "/bad-realm-sales-post-sig/");
Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/bad-realm-sales-post-sig/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/bad-realm-sales-post-sig/");
System.out.println(driver.getPageSource());
error.check(driver);
}
private static String createToken(String AUTH_SERVER_URL, AbstractKeycloakRule keycloakRule) {
KeycloakSession session = keycloakRule.startSession();
try {
RealmManager manager = new RealmManager(session);
RealmModel adminRealm = manager.getRealm(Config.getAdminRealm());
ClientModel adminConsole = adminRealm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
TokenManager tm = new TokenManager();
UserModel admin = session.users().getUserByUsername("admin", adminRealm);
ClientSessionModel clientSession = session.sessions().createClientSession(adminRealm, adminConsole);
clientSession.setNote(OIDCLoginProtocol.ISSUER, AUTH_SERVER_URL + "/realms/master");
UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false, null, null);
AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, true, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
return tm.encodeToken(adminRealm, token);
} finally {
keycloakRule.stopSession(session, true);
}
}
public void testMetadataPostSignedLoginLogout() throws Exception {
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-metadata/");
Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-metadata/");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-metadata/");
String pageSource = driver.getPageSource();
Assert.assertTrue(pageSource.contains("bburke"));
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-metadata?GLO=true");
@ -380,30 +367,21 @@ public class SamlAdapterTestStrategy extends ExternalResource {
}
public static void uploadSP(String AUTH_SERVER_URL, AbstractKeycloakRule keycloakRule) {
String token = createToken(AUTH_SERVER_URL, keycloakRule);
final String authHeader = "Bearer " + token;
ClientRequestFilter authFilter = new ClientRequestFilter() {
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
}
};
Client client = ClientBuilder.newBuilder().register(authFilter).build();
UriBuilder authBase = UriBuilder.fromUri(AUTH_SERVER_URL + "");
WebTarget adminRealms = client.target(AdminRoot.realmsUrl(authBase));
public static void uploadSP(String AUTH_SERVER_URL) {
try {
Keycloak keycloak = Keycloak.getInstance(AUTH_SERVER_URL, "master", "admin", "admin", Constants.ADMIN_CONSOLE_CLIENT_ID, null);
RealmResource admin = keycloak.realm("demo");
admin.toRepresentation();
MultipartFormDataOutput formData = new MultipartFormDataOutput();
InputStream is = SamlAdapterTestStrategy.class.getResourceAsStream("/keycloak-saml/sp-metadata.xml");
Assert.assertNotNull(is);
formData.addFormData("file", is, MediaType.APPLICATION_XML_TYPE);
ClientRepresentation clientRep = admin.convertClientDescription(IOUtils.toString(SamlAdapterTestStrategy.class.getResourceAsStream("/keycloak-saml/sp-metadata.xml")));
Response response = admin.clients().create(clientRep);
WebTarget upload = adminRealms.path("demo/client-importers/saml2-entity-descriptor/upload");
System.out.println(upload.getUri());
Response response = upload.request().post(Entity.entity(formData, MediaType.MULTIPART_FORM_DATA));
Assert.assertEquals(204, response.getStatus());
response.close();
client.close();
assertEquals(201, response.getStatus());
keycloak.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,11 +1,14 @@
package org.keycloak.testsuite.saml;
import org.apache.commons.io.IOUtils;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.Config;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
@ -22,9 +25,11 @@ import org.keycloak.protocol.saml.mappers.HardcodedRole;
import org.keycloak.protocol.saml.mappers.RoleListMapper;
import org.keycloak.protocol.saml.mappers.RoleNameMapper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.admin.AdminRoot;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
@ -56,6 +61,8 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import static org.junit.Assert.assertEquals;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@ -480,30 +487,21 @@ public class SamlBindingTest {
}
public static void uploadSP() {
String token = createToken();
final String authHeader = "Bearer " + token;
ClientRequestFilter authFilter = new ClientRequestFilter() {
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
try {
Keycloak keycloak = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", Constants.ADMIN_CONSOLE_CLIENT_ID, null);
RealmResource admin = keycloak.realm("demo");
admin.toRepresentation();
ClientRepresentation clientRep = admin.convertClientDescription(IOUtils.toString(SamlBindingTest.class.getResourceAsStream("/saml/sp-metadata.xml")));
Response response = admin.clients().create(clientRep);
assertEquals(201, response.getStatus());
keycloak.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
};
Client client = ClientBuilder.newBuilder().register(authFilter).build();
UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth");
WebTarget adminRealms = client.target(AdminRoot.realmsUrl(authBase));
MultipartFormDataOutput formData = new MultipartFormDataOutput();
InputStream is = SamlBindingTest.class.getResourceAsStream("/saml/sp-metadata.xml");
Assert.assertNotNull(is);
formData.addFormData("file", is, MediaType.APPLICATION_XML_TYPE);
WebTarget upload = adminRealms.path("demo/client-importers/saml2-entity-descriptor/upload");
System.out.println(upload.getUri());
Response response = upload.request().post(Entity.entity(formData, MediaType.MULTIPART_FORM_DATA));
Assert.assertEquals(204, response.getStatus());
response.close();
client.close();
}

View file

@ -0,0 +1,6 @@
{
"client_name": "Name",
"redirect_uris": [
"http://localhost"
]
}

View file

@ -0,0 +1,82 @@
<EntityDescriptor
xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
entityID="loadbalancer-9.siroe.com">
<SPSSODescriptor
AuthnRequestsSigned="false"
WantAssertionsSigned="false"
protocolSupportEnumeration=
"urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>
MIICYDCCAgqgAwIBAgICBoowDQYJKoZIhvcNAQEEBQAwgZIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
EwpDYWxpZm9ybmlhMRQwEgYDVQQHEwtTYW50YSBDbGFyYTEeMBwGA1UEChMVU3VuIE1pY3Jvc3lz
dGVtcyBJbmMuMRowGAYDVQQLExFJZGVudGl0eSBTZXJ2aWNlczEcMBoGA1UEAxMTQ2VydGlmaWNh
dGUgTWFuYWdlcjAeFw0wNjExMDIxOTExMzRaFw0xMDA3MjkxOTExMzRaMDcxEjAQBgNVBAoTCXNp
cm9lLmNvbTEhMB8GA1UEAxMYbG9hZGJhbGFuY2VyLTkuc2lyb2UuY29tMIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQCjOwa5qoaUuVnknqf5pdgAJSEoWlvx/jnUYbkSDpXLzraEiy2UhvwpoBgB
EeTSUaPPBvboCItchakPI6Z/aFdH3Wmjuij9XD8r1C+q//7sUO0IGn0ORycddHhoo0aSdnnxGf9V
tREaqKm9dJ7Yn7kQHjo2eryMgYxtr/Z5Il5F+wIDAQABo2AwXjARBglghkgBhvhCAQEEBAMCBkAw
DgYDVR0PAQH/BAQDAgTwMB8GA1UdIwQYMBaAFDugITflTCfsWyNLTXDl7cMDUKuuMBgGA1UdEQQR
MA+BDW1hbGxhQHN1bi5jb20wDQYJKoZIhvcNAQEEBQADQQB/6DOB6sRqCZu2OenM9eQR0gube85e
nTTxU4a7x1naFxzYXK1iQ1vMARKMjDb19QEJIEJKZlDK4uS7yMlf1nFS
</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<KeyDescriptor use="encryption">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>
MIICTDCCAfagAwIBAgICBo8wDQYJKoZIhvcNAQEEBQAwgZIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
EwpDYWxpZm9ybmlhMRQwEgYDVQQHEwtTYW50YSBDbGFyYTEeMBwGA1UEChMVU3VuIE1pY3Jvc3lz
dGVtcyBJbmMuMRowGAYDVQQLExFJZGVudGl0eSBTZXJ2aWNlczEcMBoGA1UEAxMTQ2VydGlmaWNh
dGUgTWFuYWdlcjAeFw0wNjExMDcyMzU2MTdaFw0xMDA4MDMyMzU2MTdaMCMxITAfBgNVBAMTGGxv
YWRiYWxhbmNlci05LnNpcm9lLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAw574iRU6
HsSO4LXW/OGTXyfsbGv6XRVOoy3v+J1pZ51KKejcDjDJXNkKGn3/356AwIaqbcymWd59T0zSqYfR
Hn+45uyjYxRBmVJseLpVnOXLub9jsjULfGx0yjH4w+KsZSZCXatoCHbj/RJtkzuZY6V9to/hkH3S
InQB4a3UAgMCAwEAAaNgMF4wEQYJYIZIAYb4QgEBBAQDAgZAMA4GA1UdDwEB/wQEAwIE8DAfBgNV
HSMEGDAWgBQ7oCE35Uwn7FsjS01w5e3DA1CrrjAYBgNVHREEETAPgQ1tYWxsYUBzdW4uY29tMA0G
CSqGSIb3DQEBBAUAA0EAMlbfBg/ff0Xkv4DOR5LEqmfTZKqgdlD81cXynfzlF7XfnOqI6hPIA90I
x5Ql0ejivIJAYcMGUyA+/YwJg2FGoA==
</X509Certificate>
</X509Data>
</KeyInfo>
<EncryptionMethod Algorithm=
"https://www.w3.org/2001/04/xmlenc#aes128-cbc">
<KeySize xmlns="https://www.w3.org/2001/04/xmlenc#">128</KeySize>
</EncryptionMethod>
</KeyDescriptor>
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp"
ResponseLocation="https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp"/>
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="https://LoadBalancer-9.siroe.com:3443/federation/SPSloSoap/metaAlias/sp"/>
<ManageNameIDService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://LoadBalancer-9.siroe.com:3443/federation/SPMniRedirect/metaAlias/sp"
ResponseLocation="https://LoadBalancer-9.siroe.com:3443/federation/SPMniRedirect/metaAlias/sp"/>
<ManageNameIDService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="https://LoadBalancer-9.siroe.com:3443/federation/SPMniSoap/metaAlias/sp"
ResponseLocation="https://LoadBalancer-9.siroe.com:3443/federation/SPMniSoap/metaAlias/sp"/>
<NameIDFormat>
urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
</NameIDFormat>
<NameIDFormat>
urn:oasis:names:tc:SAML:2.0:nameid-format:transient
</NameIDFormat>
<AssertionConsumerService
isDefault="true"
index="0"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp"/>
<AssertionConsumerService
index="1"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp"/>
</SPSSODescriptor>
</EntityDescriptor>

View file

@ -30,7 +30,6 @@ import org.junit.Test;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.adapter.AdapterTestStrategy;
import org.keycloak.testsuite.keycloaksaml.SamlAdapterTestStrategy;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import org.openqa.selenium.WebDriver;
@ -73,7 +72,7 @@ public class TomcatSamlTest {
tomcat.deploySaml("/bad-client-sales-post-sig", "bad-client-signed-post");
tomcat.deploySaml("/bad-realm-sales-post-sig", "bad-realm-signed-post");
tomcat.deploySaml("/sales-post-enc", "encrypted-post");
SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth", keycloakRule);
SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
tomcat.start();