diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java index 0014470637..12b0428ca1 100755 --- a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java @@ -38,6 +38,7 @@ import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder; import org.keycloak.saml.SAML2AuthnRequestBuilder; import org.keycloak.saml.SAML2LogoutRequestBuilder; import org.keycloak.saml.SAML2NameIDPolicyBuilder; +import org.keycloak.saml.SPMetadataDescriptor; import org.keycloak.saml.SignatureAlgorithm; import org.keycloak.saml.common.constants.GeneralConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; @@ -227,31 +228,11 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider\n" + - " \n" + - " " + getConfig().getNameIDPolicyFormat() + "\n" + - " \n" + - " \n" + - " \n"; - if (getConfig().isWantAuthnRequestsSigned()) { - descriptor += - " \n" + - " \n" + - " \n" + - " \n" + realm.getCertificatePem() + "\n" + - " \n" + - " \n" + - " \n" + - " \n"; - } - descriptor += - " \n" + - "\n"; + boolean wantAuthnRequestsSigned = getConfig().isWantAuthnRequestsSigned(); + String entityId = getEntityId(uriInfo, realm); + String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat(); + String certificatePem = realm.getCertificatePem(); + String descriptor = SPMetadataDescriptor.getSPDescriptor(authnBinding, endpoint, endpoint, wantAuthnRequestsSigned, entityId, nameIDPolicyFormat, certificatePem); return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build(); } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index b0853cb242..aac6a55101 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -700,14 +700,28 @@ module.controller('ClientInstallationCtrl', function($scope, realm, client, serv $scope.changeFormat = function() { var url = ClientInstallation.url({ realm: $routeParams.realm, client: $routeParams.client, provider: $scope.configFormat.id }); - $http.get(url).success(function(data) { - var installation = data; - if ($scope.configFormat.mediaType == 'application/json') { - installation = angular.fromJson(data); - installation = angular.toJson(installation, true); - } - $scope.installation = installation; - }) + if ($scope.configFormat.mediaType == 'application/zip') { + $http({ + url: url, + method: 'GET', + responseType: 'arraybuffer', + cache: false + }).success(function(data) { + var installation = data; + $scope.installation = installation; + } + ); + } else { + $http.get(url).success(function (data) { + var installation = data; + if ($scope.configFormat.mediaType == 'application/json') { + installation = angular.fromJson(data); + installation = angular.toJson(installation, true); + } + $scope.installation = installation; + }); + } + }; $scope.download = function() { saveAs(new Blob([$scope.installation], { type: $scope.configFormat.mediaType }), $scope.configFormat.filename); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html index 1530ba87c6..d03fdd267d 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html @@ -26,7 +26,7 @@
{{:: 'download' | translate}} - +
diff --git a/saml/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java b/saml/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java new file mode 100755 index 0000000000..78ff2f30a4 --- /dev/null +++ b/saml/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java @@ -0,0 +1,35 @@ +package org.keycloak.saml; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SPMetadataDescriptor { + public static String getSPDescriptor(String binding, String assertionEndpoint, String logoutEndpoint, boolean wantAuthnRequestsSigned, String entityId, String nameIDPolicyFormat, String certificatePem) { + String descriptor = + "\n" + + " \n" + + " " + nameIDPolicyFormat + "\n" + + " \n" + + " \n" + + " \n"; + if (wantAuthnRequestsSigned) { + descriptor += + " \n" + + " \n" + + " \n" + + " \n" + certificatePem + "\n" + + " \n" + + " \n" + + " \n" + + " \n"; + } + descriptor += + " \n" + + "\n"; + return descriptor; + } +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java index 3ac9892d2e..3ddd8831f5 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlClient.java @@ -3,6 +3,8 @@ package org.keycloak.protocol.saml; import org.keycloak.models.ClientConfigResolver; import org.keycloak.models.ClientModel; import org.keycloak.saml.SignatureAlgorithm; +import org.keycloak.saml.common.constants.GeneralConstants; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; /** * @author Bill Burke @@ -37,7 +39,24 @@ public class SamlClient extends ClientConfigResolver { } public String getNameIDFormat() { - return resolveAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE); + String nameIdFormat = null; + + String configuredNameIdFormat = resolveAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE); + if (configuredNameIdFormat != null) { + if (configuredNameIdFormat.equals("email")) { + nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get(); + } else if (configuredNameIdFormat.equals("persistent")) { + nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get(); + } else if (configuredNameIdFormat.equals("transient")) { + nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get(); + } else if (configuredNameIdFormat.equals("username")) { + nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get(); + } else { + nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get(); + } + } + return nameIdFormat; + } public void setNameIDFormat(String format) { client.setAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, format); 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 c76c853d74..d6af4d8b33 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 @@ -251,18 +251,8 @@ public class SamlProtocol implements LoginProtocol { boolean forceFormat = samlClient.forceNameIDFormat(); String configuredNameIdFormat = samlClient.getNameIDFormat(); if ((nameIdFormat == null || forceFormat) && configuredNameIdFormat != null) { - if (configuredNameIdFormat.equals("email")) { - nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get(); - } else if (configuredNameIdFormat.equals("persistent")) { - nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get(); - } else if (configuredNameIdFormat.equals("transient")) { - nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get(); - } else if (configuredNameIdFormat.equals("username")) { - nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get(); - } else { - nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get(); - } - } + nameIdFormat = configuredNameIdFormat; + } if (nameIdFormat == null) return SAML_DEFAULT_NAMEID_FORMAT; return nameIdFormat; 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 bd6e84693f..1f9073577e 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 @@ -1,5 +1,6 @@ package org.keycloak.protocol.saml; +import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.security.PublicKey; @@ -14,6 +15,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.ResteasyProviderFactory; @@ -477,7 +479,12 @@ public class SamlService extends AuthorizationEndpointBase { @Path("descriptor") @Produces(MediaType.APPLICATION_XML) public String getDescriptor() throws Exception { - InputStream is = getClass().getResourceAsStream("/idp-metadata-template.xml"); + return getIDPMetadataDescriptor(uriInfo, realm); + + } + + public static String getIDPMetadataDescriptor(UriInfo uriInfo, RealmModel realm) throws IOException { + InputStream is = SamlService.class.getResourceAsStream("/idp-metadata-template.xml"); String template = StreamUtil.readString(is); template = template.replace("${idp.entityID}", RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString()); template = template.replace("${idp.sso.HTTP-POST}", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString()); @@ -485,7 +492,6 @@ public class SamlService extends AuthorizationEndpointBase { template = template.replace("${idp.sls.HTTP-POST}", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString()); template = template.replace("${idp.signing.certificate}", realm.getCertificatePem()); return template; - } @GET diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/installation/ModAuthMellonClientInstallation.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/installation/ModAuthMellonClientInstallation.java new file mode 100755 index 0000000000..c7bd844ba8 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/installation/ModAuthMellonClientInstallation.java @@ -0,0 +1,117 @@ +package org.keycloak.protocol.saml.installation; + +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.protocol.ClientInstallationProvider; +import org.keycloak.protocol.saml.SamlClient; +import org.keycloak.protocol.saml.SamlProtocol; + +import javax.ws.rs.core.Response; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ModAuthMellonClientInstallation implements ClientInstallationProvider { + @Override + public Response generateInstallation(KeycloakSession session, RealmModel realm, ClientModel client, URI serverBaseUri) { + SamlClient samlClient = new SamlClient(client); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(baos); + String idpDescriptor = SamlIDPDescriptorClientInstallation.getIDPDescriptorForClient(realm, client, serverBaseUri); + String spDescriptor = SamlSPDescriptorClientInstallation.getSPDescriptorForClient(client); + String clientDirName = client.getClientId() + .replace('/', '_') + .replace(' ', '_'); + try { + zip.putNextEntry(new ZipEntry(clientDirName + "/idp-metadata.xml")); + zip.write(idpDescriptor.getBytes()); + zip.closeEntry(); + zip.putNextEntry(new ZipEntry(clientDirName + "/sp-metadata.xml")); + zip.write(spDescriptor.getBytes()); + zip.closeEntry(); + if (samlClient.requiresClientSignature()) { + if (samlClient.getClientSigningPrivateKey() != null) { + zip.putNextEntry(new ZipEntry(clientDirName + "/client-private-key.pem")); + zip.write(samlClient.getClientSigningPrivateKey().getBytes()); + zip.closeEntry(); + } + if (samlClient.getClientSigningCertificate() != null) { + zip.putNextEntry(new ZipEntry(clientDirName + "/client-cert.pem")); + zip.write(samlClient.getClientSigningCertificate().getBytes()); + zip.closeEntry(); + } + } + zip.close(); + baos.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + + return Response.ok(baos.toByteArray(), getMediaType()).build(); + } + + @Override + public String getProtocol() { + return SamlProtocol.LOGIN_PROTOCOL; + } + + @Override + public String getDisplayType() { + return "Mod Auth Mellon files"; + } + + @Override + public String getHelpText() { + return "This is a zip file. It contains a SAML SP descriptor, SAML IDP descriptor, private key pem, and certificate pem that you will use to configure mod_auth_mellon for Apache. You'll use these files when crafting the main Apache configuration file. See mod_auth_mellon website for more details."; + } + + @Override + public String getFilename() { + return "keycloak-mod-auth-mellon-sp-config.zip"; + } + + @Override + public String getMediaType() { + return "application/zip"; + } + + @Override + public boolean isDownloadOnly() { + return true; + } + + @Override + public void close() { + + } + + @Override + public ClientInstallationProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public String getId() { + return "mod-auth-mellon"; + } +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java new file mode 100755 index 0000000000..0caee9b221 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java @@ -0,0 +1,120 @@ +package org.keycloak.protocol.saml.installation; + +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.protocol.ClientInstallationProvider; +import org.keycloak.protocol.saml.SamlClient; +import org.keycloak.protocol.saml.SamlProtocol; +import org.keycloak.services.resources.RealmsResource; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import java.net.URI; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SamlIDPDescriptorClientInstallation implements ClientInstallationProvider { + public static String getIDPDescriptorForClient(RealmModel realm, ClientModel client, URI serverBaseUri) { + SamlClient samlClient = new SamlClient(client); + String idpEntityId = RealmsResource.realmBaseUrl(UriBuilder.fromUri(serverBaseUri)).build(realm.getName()).toString(); + String idp = "\n" + + "\n" + + " \n"; + if (samlClient.forceNameIDFormat() && samlClient.getNameIDFormat() != null) { + idp += " " + samlClient.getNameIDFormat(); + } else { + idp += " urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\n" + + " urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n" + + " urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n" + + " urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n"; + } + String bindUrl = RealmsResource.protocolUrl(UriBuilder.fromUri(serverBaseUri)).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString(); + idp += "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " " + realm.getCertificatePem() + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + return idp; + } + + @Override + public Response generateInstallation(KeycloakSession session, RealmModel realm, ClientModel client, URI serverBaseUri) { + String descriptor = getIDPDescriptorForClient(realm, client, serverBaseUri); + return Response.ok(descriptor, MediaType.TEXT_PLAIN_TYPE).build(); + } + + @Override + public String getProtocol() { + return SamlProtocol.LOGIN_PROTOCOL; + } + + @Override + public String getDisplayType() { + return "SAML Metadata IDPSSODescriptor"; + } + + @Override + public String getHelpText() { + return "SAML Metadata IDSSODescriptor tailored for the client. This is special because not every client may require things like digital signatures"; + } + + @Override + public String getFilename() { + return "client-tailored-saml-idp-metadata.xml"; + } + + public String getMediaType() { + return MediaType.APPLICATION_XML; + } + + @Override + public boolean isDownloadOnly() { + return false; + } + + @Override + public void close() { + + } + + @Override + public ClientInstallationProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public String getId() { + return "saml-idp-descriptor"; + } +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java new file mode 100755 index 0000000000..0165e3096a --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java @@ -0,0 +1,93 @@ +package org.keycloak.protocol.saml.installation; + +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.protocol.ClientInstallationProvider; +import org.keycloak.protocol.saml.SamlClient; +import org.keycloak.protocol.saml.SamlProtocol; +import org.keycloak.saml.SPMetadataDescriptor; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.net.URI; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SamlSPDescriptorClientInstallation implements ClientInstallationProvider { + public static String getSPDescriptorForClient(ClientModel client) { + SamlClient samlClient = new SamlClient(client); + String assertionUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE); + if (assertionUrl == null) assertionUrl = client.getManagementUrl(); + String logoutUrl = client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE); + if (logoutUrl == null) logoutUrl = client.getManagementUrl(); + String nameIdFormat = samlClient.getNameIDFormat(); + if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT; + return SPMetadataDescriptor.getSPDescriptor(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get(), assertionUrl, logoutUrl, samlClient.requiresClientSignature(), client.getClientId(), nameIdFormat, samlClient.getClientSigningCertificate()); + } + + @Override + public Response generateInstallation(KeycloakSession session, RealmModel realm, ClientModel client, URI serverBaseUri) { + String descriptor = getSPDescriptorForClient(client); + return Response.ok(descriptor, MediaType.TEXT_PLAIN_TYPE).build(); + } + + @Override + public String getProtocol() { + return SamlProtocol.LOGIN_PROTOCOL; + } + + @Override + public String getDisplayType() { + return "SAML Metadata SPSSODescriptor"; + } + + @Override + public String getHelpText() { + return "SAML SP Metadata EntityDescriptor or rather SPSSODescriptor. This is an XML file."; + } + + @Override + public String getFilename() { + return "saml-sp-metadata.xml"; + } + + public String getMediaType() { + return MediaType.APPLICATION_XML; + } + + @Override + public boolean isDownloadOnly() { + return false; + } + + @Override + public void close() { + + } + + @Override + public ClientInstallationProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public String getId() { + return "saml-sp-descriptor"; + } +} diff --git a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider index f8e9df55e2..f1d1ef2150 100755 --- a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider +++ b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider @@ -1 +1,4 @@ org.keycloak.protocol.saml.installation.KeycloakSamlClientInstallation +org.keycloak.protocol.saml.installation.SamlSPDescriptorClientInstallation +org.keycloak.protocol.saml.installation.SamlIDPDescriptorClientInstallation +org.keycloak.protocol.saml.installation.ModAuthMellonClientInstallation diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index e0ea2003e7..7e13bf97de 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -46,7 +46,12 @@ public class RealmsResource { protected BruteForceProtector protector; public static UriBuilder realmBaseUrl(UriInfo uriInfo) { - return uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getRealmResource"); + UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder(); + return realmBaseUrl(baseUriBuilder); + } + + public static UriBuilder realmBaseUrl(UriBuilder baseUriBuilder) { + return baseUriBuilder.path(RealmsResource.class).path(RealmsResource.class, "getRealmResource"); } public static UriBuilder accountUrl(UriBuilder base) {