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