001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nelson Silva <nelson.silva@inevo.pt>
016 */
017package org.nuxeo.ecm.platform.auth.saml.slo;
018
019import org.joda.time.DateTime;
020import org.nuxeo.ecm.platform.auth.saml.AbstractSAMLProfile;
021import org.nuxeo.ecm.platform.auth.saml.SAMLConfiguration;
022import org.nuxeo.ecm.platform.auth.saml.SAMLCredential;
023import org.opensaml.common.SAMLException;
024import org.opensaml.common.SAMLObject;
025import org.opensaml.common.SAMLVersion;
026import org.opensaml.common.binding.SAMLMessageContext;
027import org.opensaml.saml2.core.*;
028import org.opensaml.saml2.metadata.SingleLogoutService;
029import org.opensaml.xml.encryption.DecryptionException;
030import org.opensaml.xml.validation.ValidationException;
031
032/**
033 * WebSLO (Single Log Out) profile implementation.
034 *
035 * @since 6.0
036 */
037public class SLOProfileImpl extends AbstractSAMLProfile implements SLOProfile {
038
039    public SLOProfileImpl(SingleLogoutService slo) {
040        super(slo);
041    }
042
043    @Override
044    public String getProfileIdentifier() {
045        return PROFILE_URI;
046    }
047
048    public LogoutRequest buildLogoutRequest(SAMLMessageContext context, SAMLCredential credential) throws SAMLException {
049
050        LogoutRequest request = build(LogoutRequest.DEFAULT_ELEMENT_NAME);
051        request.setID(newUUID());
052        request.setVersion(SAMLVersion.VERSION_20);
053        request.setIssueInstant(new DateTime());
054        request.setDestination(getEndpoint().getLocation());
055
056        Issuer issuer = build(Issuer.DEFAULT_ELEMENT_NAME);
057        issuer.setValue(SAMLConfiguration.getEntityId());
058        request.setIssuer(issuer);
059
060        // Add session indexes
061        if (credential.getSessionIndexes() == null || credential.getSessionIndexes().isEmpty()) {
062            throw new SAMLException("No session indexes found");
063        }
064        for (String sessionIndex : credential.getSessionIndexes()) {
065            SessionIndex index = build(SessionIndex.DEFAULT_ELEMENT_NAME);
066            index.setSessionIndex(sessionIndex);
067            request.getSessionIndexes().add(index);
068        }
069
070        request.setNameID(credential.getNameID());
071
072        return request;
073
074    }
075
076    public boolean processLogoutRequest(SAMLMessageContext context, SAMLCredential credential) throws SAMLException {
077
078        SAMLObject message = context.getInboundSAMLMessage();
079
080        // Verify type
081        if (message == null || !(message instanceof LogoutRequest)) {
082            throw new SAMLException("Message is not of a LogoutRequest object type");
083        }
084
085        LogoutRequest request = (LogoutRequest) message;
086
087        // Validate signature of the response if present
088        if (request.getSignature() != null) {
089            log.debug("Verifying message signature");
090            try {
091                validateSignature(request.getSignature(), context.getPeerEntityId());
092            } catch (ValidationException e) {
093                log.error("Error validating signature", e);
094            } catch (org.opensaml.xml.security.SecurityException e) {
095                e.printStackTrace();
096            }
097            context.setInboundSAMLMessageAuthenticated(true);
098        }
099
100        // TODO - Validate destination
101
102        // Validate issuer
103        if (request.getIssuer() != null) {
104            log.debug("Verifying issuer of the message");
105            Issuer issuer = request.getIssuer();
106            validateIssuer(issuer, context);
107        }
108
109        // TODO - Validate issue time
110
111        // Get and validate the NameID
112        NameID nameID;
113        if (getDecrypter() != null && request.getEncryptedID() != null) {
114            try {
115                nameID = (NameID) getDecrypter().decrypt(request.getEncryptedID());
116            } catch (DecryptionException e) {
117                throw new SAMLException("Failed to decrypt NameID", e);
118            }
119        } else {
120            nameID = request.getNameID();
121        }
122
123        if (nameID == null) {
124            throw new SAMLException("The requested NameID is invalid");
125        }
126
127        // If no index is specified do logout
128        if (request.getSessionIndexes() == null || request.getSessionIndexes().isEmpty()) {
129            return true;
130        }
131
132        // Else check if this is on of our session indexes
133        for (SessionIndex sessionIndex : request.getSessionIndexes()) {
134            if (credential.getSessionIndexes().contains(sessionIndex.getSessionIndex())) {
135                return true;
136            }
137        }
138
139        return false;
140    }
141
142    public void processLogoutResponse(SAMLMessageContext context) throws SAMLException {
143
144        SAMLObject message = context.getInboundSAMLMessage();
145
146        if (!(message instanceof LogoutResponse)) {
147            throw new SAMLException("Message is not of a LogoutResponse object type");
148        }
149        LogoutResponse response = (LogoutResponse) message;
150
151        // Validate signature of the response if present
152        if (response.getSignature() != null) {
153            log.debug("Verifying message signature");
154            try {
155                validateSignature(response.getSignature(), context.getPeerEntityId());
156            } catch (ValidationException e) {
157                log.error("Error validating signature", e);
158            } catch (org.opensaml.xml.security.SecurityException e) {
159                e.printStackTrace();
160            }
161            context.setInboundSAMLMessageAuthenticated(true);
162        }
163
164        // TODO - Validate destination
165
166        // Validate issuer
167        if (response.getIssuer() != null) {
168            log.debug("Verifying issuer of the message");
169            Issuer issuer = response.getIssuer();
170            validateIssuer(issuer, context);
171        }
172
173        // TODO - Validate issue time
174
175        // Verify status
176        String statusCode = response.getStatus().getStatusCode().getValue();
177        if (!statusCode.equals(StatusCode.SUCCESS_URI) && !statusCode.equals(StatusCode.PARTIAL_LOGOUT_URI)) {
178            log.warn("Invalid status code " + statusCode + ": " + response.getStatus().getStatusMessage());
179        }
180    }
181}