001/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *      Nelson Silva
018 */
019
020package org.nuxeo.ecm.platform.auth.saml;
021
022import org.apache.commons.logging.Log;
023import org.apache.commons.logging.LogFactory;
024import org.nuxeo.ecm.platform.auth.saml.binding.SAMLBinding;
025import org.nuxeo.ecm.platform.auth.saml.key.KeyManager;
026import org.nuxeo.runtime.api.Framework;
027import org.opensaml.common.SAMLObject;
028import org.opensaml.common.xml.SAMLConstants;
029import org.opensaml.saml2.core.NameIDType;
030import org.opensaml.saml2.metadata.AssertionConsumerService;
031import org.opensaml.saml2.metadata.EntityDescriptor;
032import org.opensaml.saml2.metadata.KeyDescriptor;
033import org.opensaml.saml2.metadata.NameIDFormat;
034import org.opensaml.saml2.metadata.SPSSODescriptor;
035import org.opensaml.saml2.metadata.SingleLogoutService;
036import org.opensaml.xml.Configuration;
037import org.opensaml.xml.security.SecurityHelper;
038import org.opensaml.xml.security.credential.Credential;
039import org.opensaml.xml.security.credential.UsageType;
040import org.opensaml.xml.security.keyinfo.KeyInfoGenerator;
041import org.opensaml.xml.signature.KeyInfo;
042
043import javax.xml.namespace.QName;
044import java.util.ArrayList;
045import java.util.Arrays;
046import java.util.Collection;
047import java.util.HashSet;
048import java.util.LinkedList;
049import java.util.List;
050import java.util.Set;
051
052/**
053 * @since 7.3
054 */
055public class SAMLConfiguration {
056
057    protected static final Log log = LogFactory.getLog(SAMLConfiguration.class);
058
059    public static final String ENTITY_ID = "nuxeo.saml2.entityId";
060
061    public static final String LOGIN_BINDINGS = "nuxeo.saml2.loginBindings";
062
063    public static final String AUTHN_REQUESTS_SIGNED = "nuxeo.saml2.authnRequestsSigned";
064
065    public static final String WANT_ASSERTIONS_SIGNED = "nuxeo.saml2.wantAssertionsSigned";
066
067    public static final String SKEW_TIME_MS= "nuxeo.saml2.skewTimeMs";
068
069    public static final int DEFAULT_SKEW_TIME_MS = 1000 * 60; // 1 minute;
070
071    public static final String BINDING_PREFIX = "urn:oasis:names:tc:SAML:2.0:bindings";
072
073    public static final String DEFAULT_LOGIN_BINDINGS = "HTTP-Redirect,HTTP-POST";
074
075    public static final Collection<String> nameID = Arrays.asList(NameIDType.EMAIL, NameIDType.TRANSIENT,
076        NameIDType.PERSISTENT, NameIDType.UNSPECIFIED, NameIDType.X509_SUBJECT);
077
078    private SAMLConfiguration() {
079
080    }
081
082    public static String getEntityId() {
083        return Framework.getProperty(ENTITY_ID, Framework.getProperty("nuxeo.url"));
084    }
085
086    public static List<String> getLoginBindings() {
087        Set<String> supportedBindings = new HashSet<>();
088        for (SAMLBinding binding : SAMLAuthenticationProvider.bindings) {
089            supportedBindings.add(binding.getBindingURI());
090        }
091        List<String> bindings = new ArrayList<>();
092        String[] suffixes = Framework.getProperty(LOGIN_BINDINGS, DEFAULT_LOGIN_BINDINGS).split(",");
093        for (String sufix : suffixes) {
094            String binding = BINDING_PREFIX + ":" + sufix;
095            if (supportedBindings.contains(binding)) {
096                bindings.add(binding);
097            } else {
098                log.warn("Unknown SAML binding " + binding);
099            }
100        }
101        return bindings;
102    }
103
104    public static boolean getAuthnRequestsSigned() {
105        return Boolean.parseBoolean(Framework.getProperty(AUTHN_REQUESTS_SIGNED));
106    }
107
108    public static boolean getWantAssertionsSigned() {
109        return Boolean.parseBoolean(Framework.getProperty(WANT_ASSERTIONS_SIGNED));
110    }
111
112    public static int getSkewTimeMillis() {
113        String skewTimeMs = Framework.getProperty(SKEW_TIME_MS);
114        return skewTimeMs != null ? Integer.parseInt(skewTimeMs) : DEFAULT_SKEW_TIME_MS;
115    }
116
117    /**
118     * Returns the {@link EntityDescriptor} for the Nuxeo Service Provider
119     */
120    public static EntityDescriptor getEntityDescriptor(String baseURL) {
121
122        // Entity Descriptor
123        EntityDescriptor descriptor = build(EntityDescriptor.DEFAULT_ELEMENT_NAME);
124        // descriptor.setID(id);
125        descriptor.setEntityID(getEntityId());
126
127        // SPSSO Descriptor
128        descriptor.getRoleDescriptors().add(getSPSSODescriptor(baseURL));
129
130        return descriptor;
131    }
132
133    /**
134     * Returns the {@link SPSSODescriptor} for the Nuxeo Service Provider
135     */
136    public static SPSSODescriptor getSPSSODescriptor(String baseURL) {
137        SPSSODescriptor spDescriptor = build(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
138        spDescriptor.setAuthnRequestsSigned(getAuthnRequestsSigned());
139        spDescriptor.setWantAssertionsSigned(getWantAssertionsSigned());
140        spDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
141
142        // Name ID
143        spDescriptor.getNameIDFormats().addAll(buildNameIDFormats(nameID));
144
145        // Generate key info
146        KeyManager keyManager = Framework.getService(KeyManager.class);
147        if (keyManager.getSigningCredential() != null) {
148            spDescriptor.getKeyDescriptors().add(
149                buildKeyDescriptor(UsageType.SIGNING,
150                    generateKeyInfoForCredential(keyManager.getSigningCredential())));
151        }
152        if (keyManager.getEncryptionCredential() != null) {
153            spDescriptor.getKeyDescriptors().add(
154                buildKeyDescriptor(UsageType.ENCRYPTION,
155                    generateKeyInfoForCredential(keyManager.getEncryptionCredential())));
156        }
157        if (keyManager.getTlsCredential() != null) {
158            spDescriptor.getKeyDescriptors().add(
159                buildKeyDescriptor(UsageType.UNSPECIFIED,
160                    generateKeyInfoForCredential(keyManager.getTlsCredential())));
161        }
162
163        // LOGIN
164        int index = 0;
165        for (String binding : getLoginBindings()) {
166            AssertionConsumerService consumer = build(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
167            consumer.setLocation(baseURL);
168            consumer.setBinding(binding);
169            consumer.setIsDefault(index == 0);
170            consumer.setIndex(index++);
171            spDescriptor.getAssertionConsumerServices().add(consumer);
172        }
173
174        // LOGOUT - SAML2_POST_BINDING_URI
175        SingleLogoutService logoutService = build(SingleLogoutService.DEFAULT_ELEMENT_NAME);
176        logoutService.setLocation(baseURL);
177        logoutService.setBinding(SAMLConstants.SAML2_POST_BINDING_URI);
178        spDescriptor.getSingleLogoutServices().add(logoutService);
179        return spDescriptor;
180    }
181
182    private static KeyDescriptor buildKeyDescriptor(UsageType type, KeyInfo key) {
183        KeyDescriptor descriptor = build(KeyDescriptor.DEFAULT_ELEMENT_NAME);
184        descriptor.setUse(type);
185        descriptor.setKeyInfo(key);
186        return descriptor;
187    }
188
189    private static Collection<NameIDFormat> buildNameIDFormats(Collection<String> nameIDs) {
190
191        Collection<NameIDFormat> formats = new LinkedList<>();
192
193        // Populate nameIDs
194        for (String nameIDValue : nameIDs) {
195            NameIDFormat nameID = build(NameIDFormat.DEFAULT_ELEMENT_NAME);
196            nameID.setFormat(nameIDValue);
197            formats.add(nameID);
198        }
199
200        return formats;
201    }
202
203    private static KeyInfo generateKeyInfoForCredential(Credential credential) {
204        try {
205            KeyInfoGenerator keyInfoGenerator = SecurityHelper.getKeyInfoGenerator(credential, null, null);
206            return keyInfoGenerator.generate(credential);
207        } catch (org.opensaml.xml.security.SecurityException e) {
208            log.error("Failed to  generate key info.");
209        }
210        return null;
211    }
212
213    private static <T extends SAMLObject> T build(QName qName) {
214        return (T) Configuration.getBuilderFactory().getBuilder(qName).buildObject(qName);
215    }
216
217}