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.SAMLCredential;
022import org.opensaml.common.SAMLException;
023import org.opensaml.common.SAMLObject;
024import org.opensaml.common.SAMLVersion;
025import org.opensaml.common.binding.SAMLMessageContext;
026import org.opensaml.saml2.core.*;
027import org.opensaml.saml2.metadata.SingleLogoutService;
028import org.opensaml.xml.encryption.DecryptionException;
029import org.opensaml.xml.validation.ValidationException;
030
031/**
032 * WebSLO (Single Log Out) profile implementation.
033 *
034 * @since 6.0
035 */
036public class SLOProfileImpl extends AbstractSAMLProfile implements SLOProfile {
037
038    public SLOProfileImpl(SingleLogoutService slo) {
039        super(slo);
040    }
041
042    @Override
043    public String getProfileIdentifier() {
044        return PROFILE_URI;
045    }
046
047    public LogoutRequest buildLogoutRequest(SAMLMessageContext context, SAMLCredential credential) throws SAMLException {
048
049        LogoutRequest request = build(LogoutRequest.DEFAULT_ELEMENT_NAME);
050        request.setID(newUUID());
051        // TODO(nfgs) Build issuer
052        // request.setIssuer(issuer);
053        request.setVersion(SAMLVersion.VERSION_20);
054        request.setIssueInstant(new DateTime());
055
056        request.setDestination(getEndpoint().getLocation());
057
058        // Add session indexes
059        if (credential.getSessionIndexes() == null || credential.getSessionIndexes().isEmpty()) {
060            throw new SAMLException("No session indexes found");
061        }
062        for (String sessionIndex : credential.getSessionIndexes()) {
063            SessionIndex index = build(SessionIndex.DEFAULT_ELEMENT_NAME);
064            index.setSessionIndex(sessionIndex);
065            request.getSessionIndexes().add(index);
066        }
067
068        request.setNameID(credential.getNameID());
069
070        return request;
071
072    }
073
074    public boolean processLogoutRequest(SAMLMessageContext context, SAMLCredential credential) throws SAMLException {
075
076        SAMLObject message = context.getInboundSAMLMessage();
077
078        // Verify type
079        if (message == null || !(message instanceof LogoutRequest)) {
080            throw new SAMLException("Message is not of a LogoutRequest object type");
081        }
082
083        LogoutRequest request = (LogoutRequest) message;
084
085        // Validate signature of the response if present
086        if (request.getSignature() != null) {
087            log.debug("Verifying message signature");
088            try {
089                validateSignature(request.getSignature(), context.getPeerEntityId());
090            } catch (ValidationException e) {
091                log.error("Error validating signature", e);
092            } catch (org.opensaml.xml.security.SecurityException e) {
093                e.printStackTrace();
094            }
095            context.setInboundSAMLMessageAuthenticated(true);
096        }
097
098        // TODO - Validate destination
099
100        // Validate issuer
101        if (request.getIssuer() != null) {
102            log.debug("Verifying issuer of the message");
103            Issuer issuer = request.getIssuer();
104            validateIssuer(issuer, context);
105        }
106
107        // TODO - Validate issue time
108
109        // Get and validate the NameID
110        NameID nameID;
111        if (getDecrypter() != null && request.getEncryptedID() != null) {
112            try {
113                nameID = (NameID) getDecrypter().decrypt(request.getEncryptedID());
114            } catch (DecryptionException e) {
115                throw new SAMLException("Failed to decrypt NameID", e);
116            }
117        } else {
118            nameID = request.getNameID();
119        }
120
121        if (nameID == null) {
122            throw new SAMLException("The requested NameID is invalid");
123        }
124
125        // If no index is specified do logout
126        if (request.getSessionIndexes() == null || request.getSessionIndexes().isEmpty()) {
127            return true;
128        }
129
130        // Else check if this is on of our session indexes
131        for (SessionIndex sessionIndex : request.getSessionIndexes()) {
132            if (credential.getSessionIndexes().contains(sessionIndex.getSessionIndex())) {
133                return true;
134            }
135        }
136
137        return false;
138    }
139
140    public void processLogoutResponse(SAMLMessageContext context) throws SAMLException {
141
142        SAMLObject message = context.getInboundSAMLMessage();
143
144        if (!(message instanceof LogoutResponse)) {
145            throw new SAMLException("Message is not of a LogoutResponse object type");
146        }
147        LogoutResponse response = (LogoutResponse) message;
148
149        // Validate signature of the response if present
150        if (response.getSignature() != null) {
151            log.debug("Verifying message signature");
152            try {
153                validateSignature(response.getSignature(), context.getPeerEntityId());
154            } catch (ValidationException e) {
155                log.error("Error validating signature", e);
156            } catch (org.opensaml.xml.security.SecurityException e) {
157                e.printStackTrace();
158            }
159            context.setInboundSAMLMessageAuthenticated(true);
160        }
161
162        // TODO - Validate destination
163
164        // Validate issuer
165        if (response.getIssuer() != null) {
166            log.debug("Verifying issuer of the message");
167            Issuer issuer = response.getIssuer();
168            validateIssuer(issuer, context);
169        }
170
171        // TODO - Validate issue time
172
173        // Verify status
174        String statusCode = response.getStatus().getStatusCode().getValue();
175        if (!statusCode.equals(StatusCode.SUCCESS_URI) && !statusCode.equals(StatusCode.PARTIAL_LOGOUT_URI)) {
176            log.warn("Invalid status code " + statusCode + ": " + response.getStatus().getStatusMessage());
177        }
178    }
179}