Merge pull request #2034 from patriot1burke/master

saml client installation formats
This commit is contained in:
Bill Burke 2016-01-14 19:26:07 -05:00
commit af554a95eb
12 changed files with 433 additions and 50 deletions

View file

@ -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<SAMLIdentityP
.build().toString();
String descriptor =
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" 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" +
" <SingleLogoutService Binding=\"" + authnBinding + "\" Location=\"" + endpoint + "\"/>\n" +
" <AssertionConsumerService\n" +
" Binding=\"" + authnBinding + "\" Location=\"" + endpoint + "\"\n" +
" index=\"1\" isDefault=\"true\" />\n";
if (getConfig().isWantAuthnRequestsSigned()) {
descriptor +=
" <KeyDescriptor use=\"signing\">\n" +
" <dsig:KeyInfo xmlns:dsig=\"http://www.w3.org/2000/09/xmldsig#\">\n" +
" <dsig:X509Data>\n" +
" <dsig:X509Certificate>\n" + realm.getCertificatePem() + "\n" +
" </dsig:X509Certificate>\n" +
" </dsig:X509Data>\n" +
" </dsig:KeyInfo>\n" +
" </KeyDescriptor>\n";
}
descriptor +=
" </SPSSODescriptor>\n" +
"</EntityDescriptor>\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();
}

View file

@ -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);

View file

@ -26,7 +26,7 @@
<div class="form-group" ng-show="installation">
<div class="col-sm-12">
<a class="btn btn-primary btn-lg" data-ng-click="download()" type="submit" ng-show="installation">{{:: 'download' | translate}}</a>
<textarea class="form-control" rows="20" kc-select-action="click">{{installation}}</textarea>
<textarea class="form-control" rows="20" kc-select-action="click" data-ng-hide="configFormat.downloadOnly">{{installation}}</textarea>
</div>
</div>
</fieldset>

View file

@ -0,0 +1,35 @@
package org.keycloak.saml;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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 =
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"" + entityId + "\">\n" +
" <SPSSODescriptor AuthnRequestsSigned=\"" + wantAuthnRequestsSigned + "\"\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>" + nameIDPolicyFormat + "\n" +
" </NameIDFormat>\n" +
" <SingleLogoutService Binding=\"" + binding + "\" Location=\"" + logoutEndpoint + "\"/>\n" +
" <AssertionConsumerService\n" +
" Binding=\"" + binding + "\" Location=\"" + assertionEndpoint + "\"\n" +
" index=\"1\" isDefault=\"true\" />\n";
if (wantAuthnRequestsSigned) {
descriptor +=
" <KeyDescriptor use=\"signing\">\n" +
" <dsig:KeyInfo xmlns:dsig=\"http://www.w3.org/2000/09/xmldsig#\">\n" +
" <dsig:X509Data>\n" +
" <dsig:X509Certificate>\n" + certificatePem + "\n" +
" </dsig:X509Certificate>\n" +
" </dsig:X509Data>\n" +
" </dsig:KeyInfo>\n" +
" </KeyDescriptor>\n";
}
descriptor +=
" </SPSSODescriptor>\n" +
"</EntityDescriptor>\n";
return descriptor;
}
}

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -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);

View file

@ -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;

View file

@ -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

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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";
}
}

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<EntityDescriptor entityID=\"" + idpEntityId + "\"\n" +
" xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n" +
" <IDPSSODescriptor WantAuthnRequestsSigned=\"" + Boolean.toString(samlClient.requiresClientSignature()) + "\"\n" +
" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n";
if (samlClient.forceNameIDFormat() && samlClient.getNameIDFormat() != null) {
idp += " " + samlClient.getNameIDFormat();
} else {
idp += " <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>\n" +
" <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n" +
" <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>\n" +
" <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>\n";
}
String bindUrl = RealmsResource.protocolUrl(UriBuilder.fromUri(serverBaseUri)).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString();
idp += "\n" +
" <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n" +
" Location=\"" + bindUrl + "\" />\n" +
" <SingleLogoutService\n" +
" Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n" +
" Location=\"" + bindUrl + "\" />\n" +
" <KeyDescriptor use=\"signing\">\n" +
" <dsig:KeyInfo xmlns:dsig=\"http://www.w3.org/2000/09/xmldsig#\">\n" +
" <dsig:X509Data>\n" +
" <dsig:X509Certificate>\n" +
" " + realm.getCertificatePem() + "\n" +
" </dsig:X509Certificate>\n" +
" </dsig:X509Data>\n" +
" </dsig:KeyInfo>\n" +
" </KeyDescriptor>\n" +
" </IDPSSODescriptor>\n" +
"</EntityDescriptor>\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";
}
}

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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";
}
}

View file

@ -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

View file

@ -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) {