saml broker import/export, and module fixes
This commit is contained in:
parent
13b22d6644
commit
ce2c4188fb
10 changed files with 129 additions and 29 deletions
|
@ -301,20 +301,20 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
|||
authnBinding = JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get();
|
||||
}
|
||||
|
||||
String assertionConsumerService = uriInfo.getBaseUriBuilder().path("realms").path(realm.getName()).path("broker").path(getConfig().getProviderId()).build().toString();
|
||||
String assertionConsumerService = uriInfo.getBaseUriBuilder().path("realms").path(realm.getName()).path("broker").path(getConfig().getId()).build().toString();
|
||||
|
||||
|
||||
|
||||
String descriptor =
|
||||
"<EntityDescriptor entityID=\"" + getEntityId(uriInfo, realm) + "\n" +
|
||||
" <SPSSODescriptor AuthnRequestsSigned=\"" + getConfig().isWantAuthnRequestsSigned() + "\n" +
|
||||
"<EntityDescriptor entityID=\"" + getEntityId(uriInfo, realm) + "\">\n" +
|
||||
" <SPSSODescriptor AuthnRequestsSigned=\"" + getConfig().isWantAuthnRequestsSigned() + "\"\n" +
|
||||
" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol http://schemas.xmlsoap.org/ws/2003/07/secext\">\n" +
|
||||
" <NameIDFormat>" + getConfig().getNameIDPolicyFormat() + "\n" +
|
||||
" </NameIDFormat>\n" +
|
||||
// todo single logout service description
|
||||
// " <SingleLogoutService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"http://localhost:8081/sales-metadata/\"/>\n" +
|
||||
" <AssertionConsumerService\n" +
|
||||
" Binding=\"" + authnBinding + "\" Location=\"" + assertionConsumerService + "\n" +
|
||||
" Binding=\"" + authnBinding + "\" Location=\"" + assertionConsumerService + "\"\n" +
|
||||
" index=\"1\" isDefault=\"true\" />\n";
|
||||
if (getConfig().isWantAuthnRequestsSigned()) {
|
||||
descriptor +=
|
||||
|
|
22
broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java
Normal file → Executable file
22
broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java
Normal file → Executable file
|
@ -19,9 +19,12 @@ package org.keycloak.broker.saml;
|
|||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.picketlink.common.constants.JBossSAMLConstants;
|
||||
import org.picketlink.common.constants.JBossSAMLURIConstants;
|
||||
import org.picketlink.common.exceptions.ParsingException;
|
||||
import org.picketlink.common.util.DocumentUtil;
|
||||
import org.picketlink.identity.federation.core.parsers.saml.SAMLParser;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.EndpointType;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.EntitiesDescriptorType;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.EntityDescriptorType;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.IDPSSODescriptorType;
|
||||
|
@ -53,7 +56,7 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
|
|||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> parseConfig(InputStream inputStream) {
|
||||
public Map<String, String> parseConfig(InputStream inputStream) {
|
||||
try {
|
||||
Object parsedObject = new SAMLParser().parse(inputStream);
|
||||
EntityDescriptorType entityType;
|
||||
|
@ -76,11 +79,22 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
|
|||
|
||||
if (idpDescriptor != null) {
|
||||
SAMLIdentityProviderConfig samlIdentityProviderConfig = new SAMLIdentityProviderConfig();
|
||||
|
||||
samlIdentityProviderConfig.setSingleSignOnServiceUrl(idpDescriptor.getSingleSignOnService().get(0).getLocation().toString());
|
||||
String singleSignOnServiceUrl = null;
|
||||
boolean postBinding = false;
|
||||
for (EndpointType endpoint : idpDescriptor.getSingleSignOnService()) {
|
||||
if (endpoint.getBinding().toString().equals(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get())) {
|
||||
singleSignOnServiceUrl = endpoint.getLocation().toString();
|
||||
postBinding = true;
|
||||
break;
|
||||
} else if (endpoint.getBinding().toString().equals(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get())){
|
||||
singleSignOnServiceUrl = endpoint.getLocation().toString();
|
||||
}
|
||||
}
|
||||
samlIdentityProviderConfig.setSingleSignOnServiceUrl(singleSignOnServiceUrl);
|
||||
samlIdentityProviderConfig.setWantAuthnRequestsSigned(idpDescriptor.isWantAuthnRequestsSigned());
|
||||
samlIdentityProviderConfig.setValidateSignature(idpDescriptor.isWantAuthnRequestsSigned());
|
||||
samlIdentityProviderConfig.setPostBindingResponse(true);
|
||||
samlIdentityProviderConfig.setPostBindingResponse(postBinding);
|
||||
samlIdentityProviderConfig.setPostBindingAuthnRequest(postBinding);
|
||||
|
||||
List<KeyDescriptorType> keyDescriptor = idpDescriptor.getKeyDescriptor();
|
||||
String defaultCertificate = null;
|
||||
|
|
|
@ -5,9 +5,14 @@
|
|||
<!-- Insert resources here -->
|
||||
</resources>
|
||||
<dependencies>
|
||||
<module name="javax.api" />
|
||||
<module name="org.keycloak.keycloak-core"/>
|
||||
<module name="org.keycloak.keycloak-model-api"/>
|
||||
<module name="org.keycloak.keycloak-broker-core"/>
|
||||
<module name="org.keycloak.keycloak-saml-protocol"/>
|
||||
<module name="org.picketlink.common"/>
|
||||
<module name="org.picketlink.federation"/>
|
||||
<module name="javax.ws.rs.api"/>
|
||||
<module name="org.jboss.resteasy.resteasy-jaxrs"/>
|
||||
</dependencies>
|
||||
</module>
|
||||
|
|
|
@ -638,7 +638,7 @@ module.controller('RealmDefaultRolesCtrl', function ($scope, Realm, realm, appli
|
|||
|
||||
});
|
||||
|
||||
module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload, realm, instance, providerFactory, IdentityProvider, serverInfo, $location, Notifications) {
|
||||
module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload, $http, realm, instance, providerFactory, IdentityProvider, serverInfo, $location, Notifications) {
|
||||
console.log('RealmIdentityProviderCtrl');
|
||||
|
||||
$scope.realm = angular.copy(realm);
|
||||
|
@ -678,6 +678,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
|
|||
|
||||
$scope.files = [];
|
||||
$scope.importFile = false;
|
||||
$scope.importUrl = false;
|
||||
|
||||
$scope.onFileSelect = function($files) {
|
||||
$scope.importFile = true;
|
||||
|
@ -685,6 +686,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
|
|||
};
|
||||
|
||||
$scope.clearFileSelect = function() {
|
||||
$scope.importUrl = false;
|
||||
$scope.importFile = false;
|
||||
$scope.files = null;
|
||||
}
|
||||
|
@ -694,7 +696,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
|
|||
for (var i = 0; i < $scope.files.length; i++) {
|
||||
var $file = $scope.files[i];
|
||||
$scope.upload = $upload.upload({
|
||||
url: authUrl + '/admin/realms/' + realm.realm + '/identity-provider/',
|
||||
url: authUrl + '/admin/realms/' + realm.realm + '/identity-provider/import',
|
||||
// method: POST or PUT,
|
||||
// headers: {'headerKey': 'headerValue'}, withCredential: true,
|
||||
data: $scope.identityProvider,
|
||||
|
@ -714,6 +716,24 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
|
|||
}
|
||||
};
|
||||
|
||||
$scope.importFrom = function() {
|
||||
$scope.identityProvider.fromUrl = $scope.fromUrl;
|
||||
$http.post(authUrl + '/admin/realms/' + realm.realm + '/identity-provider/import', $scope.identityProvider)
|
||||
.success(function(data, status, headers) {
|
||||
$location.url("/realms/" + realm.realm + "/identity-provider-settings");
|
||||
Notifications.success("The " + $scope.identityProvider.name + " provider has been created.");
|
||||
}).error(function() {
|
||||
Notifications.error("The provider can not be imported. Please verify the url.");
|
||||
});
|
||||
};
|
||||
$scope.$watch('fromUrl', function(newVal, oldVal){
|
||||
if ($scope.fromUrl && $scope.fromUrl.length > 0) {
|
||||
$scope.importUrl = true;
|
||||
} else{
|
||||
$scope.importUrl = false;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('configuredProviders', function(configuredProviders) {
|
||||
if (configuredProviders) {
|
||||
$scope.configuredProviders = angular.copy(configuredProviders);
|
||||
|
|
|
@ -1102,7 +1102,7 @@ module.factory('PasswordPolicy', function() {
|
|||
});
|
||||
|
||||
module.factory('IdentityProvider', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/identity-provider/:id', {
|
||||
return $resource(authUrl + '/admin/realms/:realm/identity-provider/instances/:id', {
|
||||
realm : '@realm'
|
||||
}, {
|
||||
create : {
|
||||
|
@ -1118,7 +1118,7 @@ module.factory('IdentityProvider', function($resource) {
|
|||
});
|
||||
|
||||
module.factory('IdentityProviderExport', function($resource) {
|
||||
var url = authUrl + '/admin/realms/:realm/identity-provider/:id/export';
|
||||
var url = authUrl + '/admin/realms/:realm/identity-provider/instances/:id/export';
|
||||
return {
|
||||
url : function(parameters)
|
||||
{
|
||||
|
|
|
@ -25,8 +25,15 @@
|
|||
</div>
|
||||
<span tooltip-placement="right" tooltip="The friendly name for this identity provider." class="fa fa-info-circle"></span>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="newIdentityProvider">
|
||||
<label class="col-sm-2 control-label">Import IdP SAML Metadata </label>
|
||||
<div class="form-group" data-ng-show="newIdentityProvider && !importFile">
|
||||
<label class="col-sm-2 control-label" for="fromUrl">Import From Url</label>
|
||||
<div class="col-sm-4">
|
||||
<input class="form-control" id="fromUrl" type="text" ng-model="fromUrl">
|
||||
</div>
|
||||
<span tooltip-placement="right" tooltip="Import metadata from a remote IDP SAML entity descriptor." class="fa fa-info-circle"></span>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="newIdentityProvider && !importUrl">
|
||||
<label class="col-sm-2 control-label">Import From File</label>
|
||||
<div class="col-sm-4">
|
||||
<div class="controls kc-button-input-file" data-ng-show="!files || files.length == 0">
|
||||
<a href="#" class="btn btn-default"><span class="kc-icon-upload">Icon: Upload</span>Choose a File...</a>
|
||||
|
@ -37,14 +44,14 @@
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group clearfix" data-ng-show="!importFile">
|
||||
<div class="form-group clearfix" data-ng-show="!importFile && !importUrl">
|
||||
<label class="col-sm-2 control-label" for="singleSignOnServiceUrl">Single Sign-On Service Url<span class="required">*</span></label>
|
||||
<div class="col-sm-4">
|
||||
<input class="form-control" id="singleSignOnServiceUrl" type="text" ng-model="identityProvider.config.singleSignOnServiceUrl" required>
|
||||
</div>
|
||||
<span tooltip-placement="right" tooltip="The Url that must be used to send authentication requests(SAML AuthnRequest)." class="fa fa-info-circle"></span>
|
||||
</div>
|
||||
<div class="form-group clearfix" data-ng-show="!importFile">
|
||||
<div class="form-group clearfix" data-ng-show="!importFile && !importUrl">
|
||||
<label class="col-sm-2 control-label" for="nameIDPolicyFormat">NameID Policy Format</label>
|
||||
<div class="col-sm-4">
|
||||
<select id="nameIDPolicyFormat" ng-model="identityProvider.config.nameIDPolicyFormat">
|
||||
|
@ -60,42 +67,42 @@
|
|||
</div>
|
||||
<span tooltip-placement="right" tooltip="Specifies the URI reference corresponding to a name identifier format. Defaults to urn:oasis:names:tc:SAML:2.0:nameid-format:persistent." class="fa fa-info-circle"></span>
|
||||
</div>
|
||||
<div class="form-group clearfix" data-ng-show="!importFile">
|
||||
<div class="form-group clearfix" data-ng-show="!importFile && !importUrl">
|
||||
<label class="col-sm-2 control-label" for="signingCertificate">Validating X509 Certificate</label>
|
||||
<div class="col-sm-4">
|
||||
<textarea class="form-control" id="signingCertificate" ng-model="identityProvider.config.signingCertificate"/>
|
||||
</div>
|
||||
<span tooltip-placement="right" tooltip="The certificate in PEM format that must be used to check for signatures." class="fa fa-info-circle"></span>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="!importFile">
|
||||
<div class="form-group" data-ng-show="!importFile && !importUrl">
|
||||
<label class="col-sm-2 control-label" for="wantAuthnRequestsSigned">Want AuthnRequests Signed</label>
|
||||
<div class="col-sm-4">
|
||||
<input ng-model="identityProvider.config.wantAuthnRequestsSigned" id="wantAuthnRequestsSigned" onoffswitch />
|
||||
</div>
|
||||
<span tooltip-placement="right" tooltip=" Indicates whether the identity provider expects signed a AuthnRequest." class="fa fa-info-circle"></span>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="!importFile">
|
||||
<div class="form-group" data-ng-show="!importFile && !importUrl">
|
||||
<label class="col-sm-2 control-label" for="forceAuthn">Force Authentication</label>
|
||||
<div class="col-sm-4">
|
||||
<input ng-model="identityProvider.config.forceAuthn" id="forceAuthn" onoffswitch />
|
||||
</div>
|
||||
<span tooltip-placement="right" tooltip=" Indicates whether the identity provider must authenticate the presenter directly rather than rely on a previous security context." class="fa fa-info-circle"></span>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="!importFile">
|
||||
<div class="form-group" data-ng-show="!importFile && !importUrl">
|
||||
<label class="col-sm-2 control-label" for="validateSignature">Validate Signature</label>
|
||||
<div class="col-sm-4">
|
||||
<input ng-model="identityProvider.config.validateSignature" id="validateSignature" onoffswitch />
|
||||
</div>
|
||||
<span tooltip-placement="right" tooltip="Enable/disable signature validation of SAML responses." class="fa fa-info-circle"></span>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="!importFile">
|
||||
<div class="form-group" data-ng-show="!importFile && !importUrl">
|
||||
<label class="col-sm-2 control-label" for="postBindingResponse">HTTP-POST Binding Response</label>
|
||||
<div class="col-sm-4">
|
||||
<input ng-model="identityProvider.config.postBindingResponse" id="postBindingResponse" onoffswitch />
|
||||
</div>
|
||||
<span tooltip-placement="right" tooltip="Indicates whether the identity provider must respond to the AuthnRequest using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used." class="fa fa-info-circle"></span>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="!importFile">
|
||||
<div class="form-group" data-ng-show="!importFile && !importUrl">
|
||||
<label class="col-sm-2 control-label" for="postBindingAuthnRequest">HTTP-POST Binding for AuthnRequest</label>
|
||||
<div class="col-sm-4">
|
||||
<input ng-model="identityProvider.config.postBindingAuthnRequest" id="postBindingAuthnRequest" onoffswitch />
|
||||
|
@ -134,9 +141,10 @@
|
|||
|
||||
<div class="pull-right form-actions">
|
||||
<a class="btn btn-lg btn-primary" href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.id}}/export" data-ng-show="!importFile && !newIdentityProvider">Export</a>
|
||||
<button kc-save data-ng-show="!importFile">Save</button>
|
||||
<button type="submit" data-ng-click="clearFileSelect()" data-ng-show="importFile" class="btn btn-lg btn-default">Cancel</button>
|
||||
<button type="submit" data-ng-click="uploadFile()" data-ng-show="importFile" class="btn btn-lg btn-primary">Import from SAML Metadata</button>
|
||||
<button kc-save data-ng-show="!importFile && !importUrl">Save</button>
|
||||
<button type="submit" data-ng-click="clearFileSelect()" data-ng-show="importFile || importUrl" class="btn btn-lg btn-default">Cancel</button>
|
||||
<button type="submit" data-ng-click="uploadFile()" data-ng-show="importFile" class="btn btn-lg btn-primary">Import</button>
|
||||
<button type="submit" data-ng-click="importFrom()" data-ng-show="importUrl" class="btn btn-lg btn-primary">Import</button>
|
||||
<button kc-delete data-ng-click="remove()" data-ng-show="!newIdentityProvider">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -6,8 +6,13 @@ 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.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
/**
|
||||
* @author pedroigor
|
||||
|
@ -24,4 +29,9 @@ public interface IdentityProviderResource {
|
|||
|
||||
@DELETE
|
||||
void remove();
|
||||
|
||||
@GET
|
||||
@Path("export")
|
||||
public Response export(@QueryParam("format") String format);
|
||||
|
||||
}
|
|
@ -16,7 +16,7 @@ import java.util.List;
|
|||
*/
|
||||
public interface IdentityProvidersResource {
|
||||
|
||||
@Path("{id}")
|
||||
@Path("instances/{id}")
|
||||
IdentityProviderResource get(@PathParam("id") String id);
|
||||
|
||||
@GET
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.keycloak.services.resources.admin;
|
||||
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor;
|
||||
import org.jboss.resteasy.plugins.providers.multipart.InputPart;
|
||||
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
|
||||
import org.jboss.resteasy.spi.NotFoundException;
|
||||
|
@ -17,6 +18,7 @@ import org.keycloak.models.utils.ModelToRepresentation;
|
|||
import org.keycloak.models.utils.RepresentationToModel;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.services.managers.ResourceAdminManager;
|
||||
import org.keycloak.services.resources.flows.Flows;
|
||||
import org.keycloak.social.SocialIdentityProvider;
|
||||
|
||||
|
@ -102,8 +104,9 @@ public class IdentityProvidersResource {
|
|||
}
|
||||
|
||||
@POST
|
||||
@Path("import")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Response createWithFile(@Context UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
|
||||
public Response importFrom(@Context UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
|
||||
this.auth.requireManage();
|
||||
Map<String, List<InputPart>> formDataMap = input.getFormDataMap();
|
||||
|
||||
|
@ -135,7 +138,47 @@ public class IdentityProvidersResource {
|
|||
return create(uriInfo, representation);
|
||||
}
|
||||
|
||||
@Path("{id}")
|
||||
@POST
|
||||
@Path("import")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response importFrom(@Context UriInfo uriInfo, Map<String, Object> data) throws IOException {
|
||||
this.auth.requireManage();
|
||||
|
||||
String id = data.get("id").toString();
|
||||
String name = data.get("name").toString();
|
||||
String providerId = data.get("providerId").toString();
|
||||
String enabled = data.get("enabled").toString();
|
||||
String updateProfileFirstLogin = data.get("updateProfileFirstLogin").toString();
|
||||
String storeToken = "false";
|
||||
|
||||
if (data.containsKey("storeToken")) {
|
||||
storeToken = data.get("storeToken").toString();
|
||||
}
|
||||
|
||||
String from = data.get("fromUrl").toString();
|
||||
ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor();
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = executor.createRequest(from).getTarget(InputStream.class);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
IdentityProviderFactory providerFactory = getProviderFactorytById(providerId);
|
||||
Map config = providerFactory.parseConfig(inputStream);
|
||||
IdentityProviderRepresentation representation = new IdentityProviderRepresentation();
|
||||
|
||||
representation.setId(id);
|
||||
representation.setName(name);
|
||||
representation.setProviderId(providerId);
|
||||
representation.setEnabled(Boolean.valueOf(enabled));
|
||||
representation.setUpdateProfileFirstLogin(Boolean.valueOf(updateProfileFirstLogin));
|
||||
representation.setStoreToken(Boolean.valueOf(storeToken));
|
||||
representation.setConfig(config);
|
||||
|
||||
return create(uriInfo, representation);
|
||||
}
|
||||
|
||||
@Path("instances/{id}")
|
||||
public IdentityProviderResource getIdentityProvider(@PathParam("id") String providerId) {
|
||||
this.auth.requireView();
|
||||
IdentityProviderModel identityProviderModel = null;
|
||||
|
|
|
@ -157,7 +157,7 @@ public class AccountTest {
|
|||
});
|
||||
}
|
||||
|
||||
//@Test @Ignore
|
||||
@Test @Ignore
|
||||
public void runit() throws Exception {
|
||||
Thread.sleep(10000000);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue