keycloak-aplcache

app importer

10/28/2014 1:54:58 PM

Changes

Details

diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js
index cbcb3a6..6ebea5f 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js
@@ -610,10 +610,26 @@ module.config([ '$routeProvider', function($routeProvider) {
                 },
                 applications : function(ApplicationListLoader) {
                     return ApplicationListLoader();
+                },
+                serverInfo : function(ServerInfoLoader) {
+                    return ServerInfoLoader();
                 }
+
             },
             controller : 'ApplicationListCtrl'
         })
+        .when('/import/application/:realm', {
+            templateUrl : 'partials/application-import.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                serverInfo : function(ServerInfoLoader) {
+                    return ServerInfoLoader();
+                }
+            },
+            controller : 'ApplicationImportCtrl'
+        })
 
         // OAUTH Client
 
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js
index 318f1bb..3d80c2b 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/applications.js
@@ -366,9 +366,62 @@ module.controller('ApplicationRoleDetailCtrl', function($scope, realm, applicati
 
 });
 
-module.controller('ApplicationListCtrl', function($scope, realm, applications, Application, $location) {
+module.controller('ApplicationImportCtrl', function($scope, $location, $upload, realm, serverInfo, Notifications) {
+
+    $scope.realm = realm;
+    $scope.configFormats = serverInfo.applicationImporters;
+    $scope.configFormat = null;
+
+    $scope.files = [];
+
+    $scope.onFileSelect = function($files) {
+        $scope.files = $files;
+    };
+
+    $scope.clearFileSelect = function() {
+        $scope.files = null;
+    }
+
+    $scope.uploadFile = function() {
+        //$files: an array of files selected, each file has name, size, and type.
+        for (var i = 0; i < $scope.files.length; i++) {
+            var $file = $scope.files[i];
+            $scope.upload = $upload.upload({
+                url: authUrl + '/admin/realms/' + realm.realm + '/application-importers/' + $scope.configFormat.id + '/upload',
+                // method: POST or PUT,
+                // headers: {'headerKey': 'headerValue'}, withCredential: true,
+                data: {myObj: ""},
+                file: $file
+                /* set file formData name for 'Content-Desposition' header. Default: 'file' */
+                //fileFormDataName: myFile,
+                /* customize how data is added to formData. See #40#issuecomment-28612000 for example */
+                //formDataAppender: function(formData, key, val){}
+            }).progress(function(evt) {
+                console.log('percent: ' + parseInt(100.0 * evt.loaded / evt.total));
+            }).success(function(data, status, headers) {
+                Notifications.success("Uploaded successfully.");
+                $location.url("/realms/" + realm.realm + "/applications");
+            })
+                .error(function() {
+                    Notifications.error("The file can not be uploaded. Please verify the file.");
+
+                });
+            //.then(success, error, progress);
+        }
+    };
+
+    $scope.$watch(function() {
+        return $location.path();
+    }, function() {
+        $scope.path = $location.path().substring(1).split("/");
+    });
+});
+
+
+module.controller('ApplicationListCtrl', function($scope, realm, applications, Application, serverInfo, $location) {
     $scope.realm = realm;
     $scope.applications = applications;
+    $scope.importButton = serverInfo.applicationImporters.length > 0;
     $scope.$watch(function() {
         return $location.path();
     }, function() {
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-import.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-import.html
new file mode 100755
index 0000000..723bf01
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-import.html
@@ -0,0 +1,42 @@
+<div class="bs-sidebar col-md-3 clearfix" data-ng-include data-src="'partials/realm-menu.html'"></div>
+<div id="content-area" class="col-md-9" role="main">
+    <ul class="nav nav-tabs nav-tabs-pf">
+        <li class="active"><a href="">Application Import</a></li>
+    </ul>
+    <h2></h2>
+    <div id="content">
+        <h2><span>{{application.name}}</span> Application Import <span tooltip-placement="right" tooltip="Helper utility for importing application definitions from various formats." class="fa fa-info-circle"></span></h2>
+        <form class="form-horizontal" name="realmForm" novalidate>
+            <fieldset class="border-top">
+                <div class="form-group input-select">
+                    <label class="col-sm-2 control-label" for="configFormats">Format Option</label>
+                    <div class="col-sm-4">
+                        <div class="input-group">
+                            <div class="select-kc">
+                                <select id="configFormats" name="configFormats" ng-model="configFormat" ng-options="format.name for format in configFormats">
+                                    <option value="" selected> Select a Format </option>
+                                </select>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="col-sm-2 control-label">Import 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>
+                            <input id="import-file" type="file" class="transparent" ng-file-select="onFileSelect($files)">
+                        </div>
+                        <span class="kc-uploaded-file" data-ng-show="files.length > 0">
+                            {{files[0].name}}
+                        </span>
+                    </div>
+                </div>
+                <div class="pull-right form-actions" data-ng-show="files.length > 0">
+                    <button type="submit" data-ng-click="clearFileSelect()" class="btn btn-lg btn-default">Cancel</button>
+                    <button type="submit" data-ng-click="uploadFile()" class="btn btn-lg btn-primary">Import</button>
+                </div>
+            </fieldset>
+        </form>
+    </div>
+</div>
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-list.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-list.html
index f6eec65..364b4c7 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-list.html
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/application-list.html
@@ -19,7 +19,8 @@
                         </button>
                     </div>
                     <div class="pull-right">
-                        <a class="btn btn-primary" href="#/create/application/{{realm.realm}}">Add Application</a>
+                        <a class="btn btn-primary" href="#/import/application/{{realm.realm}}" data-ng-show="importButton">Import</a>
+                        <a class="btn btn-primary" href="#/create/application/{{realm.realm}}">Create</a>
                     </div>
                 </th>
             </tr>
diff --git a/saml/saml-protocol/pom.xml b/saml/saml-protocol/pom.xml
index ea64438..7e9c389 100755
--- a/saml/saml-protocol/pom.xml
+++ b/saml/saml-protocol/pom.xml
@@ -19,6 +19,11 @@
     </properties>
     <dependencies>
         <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-multipart-provider</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>org.keycloak</groupId>
             <artifactId>keycloak-core</artifactId>
             <version>${project.version}</version>
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporter.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporter.java
new file mode 100755
index 0000000..32f0990
--- /dev/null
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporter.java
@@ -0,0 +1,21 @@
+package org.keycloak.protocol.saml;
+
+import org.keycloak.exportimport.ApplicationImporter;
+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 ApplicationImporter {
+    @Override
+    public Object createJaxrsService(RealmModel realm, RealmAuth auth) {
+        return new EntityDescriptorImporterService(realm, auth);
+    }
+
+    @Override
+    public void close() {
+
+    }
+}
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporterFactory.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporterFactory.java
new file mode 100755
index 0000000..43252b7
--- /dev/null
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporterFactory.java
@@ -0,0 +1,37 @@
+package org.keycloak.protocol.saml;
+
+import org.keycloak.Config;
+import org.keycloak.exportimport.ApplicationImporter;
+import org.keycloak.exportimport.ApplicationImporterFactory;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class EntityDescriptorImporterFactory implements ApplicationImporterFactory {
+    @Override
+    public String getDisplayName() {
+        return "SAML 2.0 Entity Descriptor";
+    }
+
+    @Override
+    public ApplicationImporter create(KeycloakSession session) {
+        return new EntityDescriptorImporter();
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public String getId() {
+        return "saml2-entity-descriptor";
+    }
+}
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporterService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporterService.java
new file mode 100755
index 0000000..833fa2d
--- /dev/null
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporterService.java
@@ -0,0 +1,144 @@
+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.ApplicationModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.services.resources.admin.RealmAuth;
+import org.picketlink.common.constants.JBossSAMLURIConstants;
+import org.picketlink.common.exceptions.ConfigurationException;
+import org.picketlink.common.exceptions.ParsingException;
+import org.picketlink.common.exceptions.ProcessingException;
+import org.picketlink.identity.federation.core.parsers.saml.SAMLParser;
+import org.picketlink.identity.federation.core.saml.v2.util.SAMLMetadataUtil;
+import org.picketlink.identity.federation.core.util.CoreConfigUtil;
+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.KeyDescriptorType;
+import org.picketlink.identity.federation.saml.v2.metadata.KeyTypes;
+import org.picketlink.identity.federation.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();
+            ApplicationModel app = realm.addApplication(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 adminUrl = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
+            if (adminUrl != null) app.setManagementUrl(adminUrl);
+
+            String urlPattern = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
+            if (urlPattern == null) {
+                urlPattern = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
+            }
+            if (urlPattern != null) {
+                app.addRedirectUri(urlPattern);
+            }
+
+            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;
+    }
+
+
+}
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
index 439bd17..e5d1571 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
@@ -17,8 +17,8 @@ import org.keycloak.protocol.LoginProtocol;
 import org.keycloak.services.managers.ClientSessionCode;
 import org.keycloak.services.managers.ResourceAdminManager;
 import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.services.resources.admin.ClientAttributeCertificateResource;
 import org.keycloak.services.resources.flows.Flows;
-import org.keycloak.util.PemUtils;
 import org.picketlink.common.constants.GeneralConstants;
 import org.picketlink.common.constants.JBossSAMLURIConstants;
 import org.picketlink.identity.federation.core.saml.v2.constants.X500SAMLProfileConstants;
@@ -35,6 +35,13 @@ import java.security.PublicKey;
  */
 public class SamlProtocol implements LoginProtocol {
     protected static final Logger logger = Logger.getLogger(SamlProtocol.class);
+
+
+    public static final String ATTRIBUTE_TRUE_VALUE = "true";
+    public static final String ATTRIBUTE_FALSE_VALUE = "false";
+    public static final String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE;
+    public static final String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + ClientAttributeCertificateResource.X509CERTIFICATE;
+    public static final String SAML_CLIENT_SIGNATURE_ATTRIBUTE = "saml.client.signature";
     public static final String LOGIN_PROTOCOL = "saml";
     public static final String SAML_BINDING = "saml_binding";
     public static final String SAML_POST_BINDING = "post";
@@ -46,7 +53,7 @@ public class SamlProtocol implements LoginProtocol {
     public static final String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm";
     public static final String SAML_ENCRYPT = "saml.encrypt";
     public static final String SAML_FORCE_POST_BINDING = "saml.force.post.binding";
-    public static final String REQUEST_ID = "REQUEST_ID";
+    public static final String SAML_REQUEST_ID = "SAML_REQUEST_ID";
 
     protected KeycloakSession session;
 
@@ -114,7 +121,7 @@ public class SamlProtocol implements LoginProtocol {
     public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) {
         ClientSessionModel clientSession = accessCode.getClientSession();
         ClientModel client = clientSession.getClient();
-        String requestID = clientSession.getNote(REQUEST_ID);
+        String requestID = clientSession.getNote(SAML_REQUEST_ID);
         String relayState = clientSession.getNote(GeneralConstants.RELAY_STATE);
         String redirectUri = clientSession.getRedirectUri();
         String responseIssuer = getResponseIssuer(realm);
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
index e18693a..7ddabe9 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
@@ -2,17 +2,13 @@ package org.keycloak.protocol.saml;
 
 import org.keycloak.VerificationException;
 import org.keycloak.models.ClientModel;
-import org.keycloak.services.resources.admin.ClientAttributeCertificateResource;
 import org.keycloak.util.PemUtils;
 import org.picketlink.common.exceptions.ProcessingException;
 import org.picketlink.identity.federation.api.saml.v2.sig.SAML2Signature;
 import org.w3c.dom.Document;
-import org.w3c.dom.Node;
 
-import java.security.KeyPair;
 import java.security.PublicKey;
 import java.security.cert.Certificate;
-import java.security.cert.X509Certificate;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -20,11 +16,8 @@ import java.security.cert.X509Certificate;
  */
 public class SamlProtocolUtils {
 
-    public static final String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE;
-    public static final String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + ClientAttributeCertificateResource.X509CERTIFICATE;
-
     public static void verifyDocumentSignature(ClientModel client, Document document) throws VerificationException {
-        if (!"true".equals(client.getAttribute("saml.client.signature"))) {
+        if (!"true".equals(client.getAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE))) {
             return;
         }
         SAML2Signature saml2Signature = new SAML2Signature();
@@ -39,11 +32,11 @@ public class SamlProtocolUtils {
     }
 
     public static PublicKey getSignatureValidationKey(ClientModel client) throws VerificationException {
-        return getPublicKey(client, SAML_SIGNING_CERTIFICATE_ATTRIBUTE);
+        return getPublicKey(client, SamlProtocol.SAML_SIGNING_CERTIFICATE_ATTRIBUTE);
     }
 
     public static PublicKey getEncryptionValidationKey(ClientModel client) throws VerificationException {
-        return getPublicKey(client, SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE);
+        return getPublicKey(client, SamlProtocol.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE);
     }
 
     public static PublicKey getPublicKey(ClientModel client, String attribute) throws VerificationException {
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
index 527ade9..8f51234 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -198,7 +198,7 @@ public class SamlService {
             clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE);
             clientSession.setNote(SamlProtocol.SAML_BINDING, getBindingType());
             clientSession.setNote(GeneralConstants.RELAY_STATE, relayState);
-            clientSession.setNote(SamlProtocol.REQUEST_ID, requestAbstractType.getID());
+            clientSession.setNote(SamlProtocol.SAML_REQUEST_ID, requestAbstractType.getID());
 
             Response response = authManager.checkNonFormAuthentication(session, clientSession, realm, uriInfo, request, clientConnection, headers, event);
             if (response != null) return response;
diff --git a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.exportimport.ApplicationImporterFactory b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.exportimport.ApplicationImporterFactory
new file mode 100755
index 0000000..0971c24
--- /dev/null
+++ b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.exportimport.ApplicationImporterFactory
@@ -0,0 +1 @@
+org.keycloak.protocol.saml.EntityDescriptorImporterFactory
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/exportimport/ApplicationImporter.java b/services/src/main/java/org/keycloak/exportimport/ApplicationImporter.java
new file mode 100755
index 0000000..f4c526e
--- /dev/null
+++ b/services/src/main/java/org/keycloak/exportimport/ApplicationImporter.java
@@ -0,0 +1,15 @@
+package org.keycloak.exportimport;
+
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.Provider;
+import org.keycloak.services.resources.admin.RealmAuth;
+
+/**
+ * Provider plugin interface for importing applications from an arbitrary configuration format
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ApplicationImporter extends Provider {
+    public Object createJaxrsService(RealmModel realm, RealmAuth auth);
+}
diff --git a/services/src/main/java/org/keycloak/exportimport/ApplicationImporterFactory.java b/services/src/main/java/org/keycloak/exportimport/ApplicationImporterFactory.java
new file mode 100755
index 0000000..b73e2e6
--- /dev/null
+++ b/services/src/main/java/org/keycloak/exportimport/ApplicationImporterFactory.java
@@ -0,0 +1,13 @@
+package org.keycloak.exportimport;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * Provider plugin interface for importing applications from an arbitrary configuration format
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ApplicationImporterFactory extends ProviderFactory<ApplicationImporter> {
+    public String getDisplayName();
+}
diff --git a/services/src/main/java/org/keycloak/exportimport/ApplicationImportSpi.java b/services/src/main/java/org/keycloak/exportimport/ApplicationImportSpi.java
new file mode 100755
index 0000000..4493b19
--- /dev/null
+++ b/services/src/main/java/org/keycloak/exportimport/ApplicationImportSpi.java
@@ -0,0 +1,26 @@
+package org.keycloak.exportimport;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ApplicationImportSpi implements Spi {
+
+    @Override
+    public String getName() {
+        return "application-import";
+    }
+
+    @Override
+    public Class<? extends Provider> getProviderClass() {
+        return ApplicationImporter.class;
+    }
+
+    @Override
+    public Class<? extends ProviderFactory> getProviderFactoryClass() {
+        return ApplicationImporterFactory.class;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index bc4167e..a8f3707 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -8,6 +8,7 @@ import org.keycloak.events.Event;
 import org.keycloak.events.EventQuery;
 import org.keycloak.events.EventStoreProvider;
 import org.keycloak.events.EventType;
+import org.keycloak.exportimport.ApplicationImporter;
 import org.keycloak.models.ApplicationModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelDuplicateException;
@@ -74,6 +75,17 @@ public class RealmAdminResource {
     }
 
     /**
+     * Base path for importing applications under this realm.
+     *
+     * @return
+     */
+    @Path("application-importers/{formatId}")
+    public Object getApplicationImporter(@PathParam("formatId") String formatId) {
+        ApplicationImporter importer = session.getProvider(ApplicationImporter.class, formatId);
+        return importer.createJaxrsService(realm, auth);
+    }
+
+    /**
      * Base path for managing applications under this realm.
      *
      * @return
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java
index 33e6b29..a0af968 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java
@@ -2,6 +2,8 @@ package org.keycloak.services.resources.admin;
 
 import org.keycloak.Version;
 import org.keycloak.events.EventListenerProvider;
+import org.keycloak.exportimport.ApplicationImporter;
+import org.keycloak.exportimport.ApplicationImporterFactory;
 import org.keycloak.freemarker.Theme;
 import org.keycloak.freemarker.ThemeProvider;
 import org.keycloak.models.KeycloakSession;
@@ -41,6 +43,7 @@ public class ServerInfoAdminResource {
         setThemes(info);
         setEventListeners(info);
         setProtocols(info);
+        setApplicationImporters(info);
         return info;
     }
 
@@ -82,7 +85,16 @@ public class ServerInfoAdminResource {
         Collections.sort(info.protocols);
     }
 
-
+    private void setApplicationImporters(ServerInfoRepresentation info) {
+        info.applicationImporters = new LinkedList<Map<String, String>>();
+        for (ProviderFactory p : session.getKeycloakSessionFactory().getProviderFactories(ApplicationImporter.class)) {
+            ApplicationImporterFactory factory = (ApplicationImporterFactory)p;
+            Map<String, String> data = new HashMap<String, String>();
+            data.put("id", factory.getId());
+            data.put("name", factory.getDisplayName());
+            info.applicationImporters.add(data);
+        }
+    }
 
     public static class ServerInfoRepresentation {
 
@@ -92,7 +104,7 @@ public class ServerInfoAdminResource {
 
         private List<String> socialProviders;
         private List<String> protocols;
-        private List<String> applicationImporters;
+        private List<Map<String, String>> applicationImporters;
 
 
         private List<String> eventListeners;
@@ -124,7 +136,7 @@ public class ServerInfoAdminResource {
             return protocols;
         }
 
-        public List<String> getApplicationImporters() {
+        public List<Map<String, String>> getApplicationImporters() {
             return applicationImporters;
         }
     }
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 62aa8e7..7cea20d 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -1 +1,2 @@
-org.keycloak.protocol.LoginProtocolSpi
\ No newline at end of file
+org.keycloak.protocol.LoginProtocolSpi
+org.keycloak.exportimport.ApplicationImportSpi
\ No newline at end of file
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
index 2700d0f..302fc91 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
@@ -1,15 +1,40 @@
 package org.keycloak.testsuite.saml;
 
+import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput;
 import org.junit.Assert;
 import org.junit.ClassRule;
 import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
+import org.keycloak.Config;
+import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.resources.admin.AdminRoot;
 import org.keycloak.testsuite.pages.LoginPage;
 import org.keycloak.testsuite.rule.WebResource;
 import org.keycloak.testsuite.rule.WebRule;
 import org.openqa.selenium.WebDriver;
 
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import java.io.IOException;
+import java.io.InputStream;
+
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
@@ -24,10 +49,12 @@ public class SamlBindingTest {
 
             initializeSamlSecuredWar("/saml/simple-post", "/sales-post",  "post.war", classLoader);
             initializeSamlSecuredWar("/saml/signed-post", "/sales-post-sig",  "post-sig.war", classLoader);
+            initializeSamlSecuredWar("/saml/signed-metadata", "/sales-metadata",  "post-metadata.war", classLoader);
             initializeSamlSecuredWar("/saml/signed-get", "/employee-sig",  "employee-sig.war", classLoader);
             initializeSamlSecuredWar("/saml/bad-client-signed-post", "/bad-client-sales-post-sig",  "bad-client-post-sig.war", classLoader);
             initializeSamlSecuredWar("/saml/bad-realm-signed-post", "/bad-realm-sales-post-sig",  "bad-realm-post-sig.war", classLoader);
             initializeSamlSecuredWar("/saml/encrypted-post", "/sales-post-enc",  "post-enc.war", classLoader);
+            uploadSP();
 
         }
 
@@ -113,5 +140,64 @@ public class SamlBindingTest {
         Assert.assertTrue(driver.getPageSource().contains("null"));
     }
 
+    private static String createToken() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmManager manager = new RealmManager(session);
+
+            RealmModel adminRealm = manager.getRealm(Config.getAdminRealm());
+            ApplicationModel adminConsole = adminRealm.getApplicationByName(Constants.ADMIN_CONSOLE_APPLICATION);
+            TokenManager tm = new TokenManager();
+            UserModel admin = session.users().getUserByUsername("admin", adminRealm);
+            UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false);
+            AccessToken token = tm.createClientAccessToken(tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession);
+            return tm.encodeToken(adminRealm, token);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+
+    @Test
+    public void testMetadataPostSignedLoginLogout() throws Exception {
+
+        driver.navigate().to("http://localhost:8081/sales-metadata/");
+        Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/auth/realms/demo/protocol/saml");
+        loginPage.login("bburke", "password");
+        Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/sales-metadata/");
+        String pageSource = driver.getPageSource();
+        Assert.assertTrue(pageSource.contains("bburke"));
+        driver.navigate().to("http://localhost:8081/sales-metadata?GLO=true");
+        Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/auth/realms/demo/protocol/saml");
+
+    }
+
+    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);
+            }
+        };
+        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/application-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();
+    }
+
 
 }
diff --git a/testsuite/integration/src/test/resources/saml/signed-metadata/WEB-INF/keystore.jks b/testsuite/integration/src/test/resources/saml/signed-metadata/WEB-INF/keystore.jks
new file mode 100755
index 0000000..144830b
Binary files /dev/null and b/testsuite/integration/src/test/resources/saml/signed-metadata/WEB-INF/keystore.jks differ
diff --git a/testsuite/integration/src/test/resources/saml/signed-metadata/WEB-INF/picketlink.xml b/testsuite/integration/src/test/resources/saml/signed-metadata/WEB-INF/picketlink.xml
new file mode 100755
index 0000000..17d37df
--- /dev/null
+++ b/testsuite/integration/src/test/resources/saml/signed-metadata/WEB-INF/picketlink.xml
@@ -0,0 +1,31 @@
+<PicketLink xmlns="urn:picketlink:identity-federation:config:2.1">
+	<PicketLinkSP xmlns="urn:picketlink:identity-federation:config:2.1"
+		ServerEnvironment="tomcat" BindingType="POST" SupportsSignatures="true">
+		<IdentityURL>${idp-sig.url::http://localhost:8081/auth/realms/demo/protocol/saml}
+		</IdentityURL>
+		<ServiceURL>${sales-post-sig.url::http://localhost:8081/sales-metadata/}
+		</ServiceURL>
+		<KeyProvider
+			ClassName="org.picketlink.identity.federation.core.impl.KeyStoreKeyManager">
+			<Auth Key="KeyStoreURL" Value="saml/signed-post/WEB-INF/keystore.jks" />
+			<Auth Key="KeyStorePass" Value="store123" />
+			<Auth Key="SigningKeyPass" Value="test123" />
+			<Auth Key="SigningKeyAlias" Value="http://localhost:8080/sales-post-sig/" />
+			<ValidatingAlias Key="localhost" Value="demo" />
+			<ValidatingAlias Key="127.0.0.1" Value="demo" />
+		</KeyProvider>
+
+	</PicketLinkSP>
+	<Handlers xmlns="urn:picketlink:identity-federation:handler:config:2.1">
+		<Handler
+			class="org.picketlink.identity.federation.web.handlers.saml2.SAML2LogOutHandler" />
+		<Handler
+			class="org.picketlink.identity.federation.web.handlers.saml2.SAML2AuthenticationHandler" />
+		<Handler
+			class="org.picketlink.identity.federation.web.handlers.saml2.RolesGenerationHandler" />
+		<Handler
+			class="org.picketlink.identity.federation.web.handlers.saml2.SAML2SignatureGenerationHandler" />
+		<Handler
+			class="org.picketlink.identity.federation.web.handlers.saml2.SAML2SignatureValidationHandler" />
+	</Handlers>
+</PicketLink>
diff --git a/testsuite/integration/src/test/resources/saml/sp-metadata.xml b/testsuite/integration/src/test/resources/saml/sp-metadata.xml
new file mode 100755
index 0000000..9b8b899
--- /dev/null
+++ b/testsuite/integration/src/test/resources/saml/sp-metadata.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntitiesDescriptor Name="urn:mace:shibboleth:testshib:two"
+                    xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+        >
+    <EntityDescriptor entityID="http://localhost:8081/sales-metadata/">
+        <SPSSODescriptor AuthnRequestsSigned="true"
+                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">
+            <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+            </NameIDFormat>
+            <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/sales-metadata/"/>
+            <AssertionConsumerService
+                    Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/sales-metadata/"
+                    index="1" isDefault="true" />
+            <KeyDescriptor use="signing">
+                <dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
+                    <dsig:X509Data>
+                        <dsig:X509Certificate>
+                            MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw==
+                        </dsig:X509Certificate>
+                    </dsig:X509Data>
+                </dsig:KeyInfo>
+            </KeyDescriptor>
+        </SPSSODescriptor>
+        <Organization>
+            <OrganizationName xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+                              xml:lang="en">JBoss</OrganizationName>
+            <OrganizationDisplayName xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+                                     xml:lang="en">JBoss by Red Hat</OrganizationDisplayName>
+            <OrganizationURL xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+                             xml:lang="en">http://localhost:8080/sales-metadata/</OrganizationURL>
+        </Organization>
+        <ContactPerson contactType="technical">
+            <GivenName>The</GivenName>
+            <SurName>Admin</SurName>
+            <EmailAddress>admin@mycompany.com</EmailAddress>
+        </ContactPerson>
+    </EntityDescriptor>
+</EntitiesDescriptor>
\ No newline at end of file