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;
032
033/**
034 * WebSLO (Single Log Out) profile implementation.
035 *
036 * @since 6.0
037 */
038public class SLOProfileImpl extends AbstractSAMLProfile implements SLOProfile {
039
040    public SLOProfileImpl(SingleLogoutService slo) {
041        super(slo);
042    }
043
044    @Override
045    public String getProfileIdentifier() {
046        return PROFILE_URI;
047    }
048
049    @Override
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    @Override
079    public boolean processLogoutRequest(SAMLMessageContext context, SAMLCredential credential) throws SAMLException {
080
081        SAMLObject message = context.getInboundSAMLMessage();
082
083        // Verify type
084        if (message == null || !(message instanceof LogoutRequest)) {
085            throw new SAMLException("Message is not of a LogoutRequest object type");
086        }
087
088        LogoutRequest request = (LogoutRequest) message;
089
090        // Validate signature of the response if present
091        if (request.getSignature() != null) {
092            log.debug("Verifying message signature");
093            validateSignature(request.getSignature(), context.getPeerEntityId());
094            context.setInboundSAMLMessageAuthenticated(true);
095        }
096
097        // TODO - Validate destination
098
099        // Validate issuer
100        if (request.getIssuer() != null) {
101            log.debug("Verifying issuer of the message");
102            Issuer issuer = request.getIssuer();
103            validateIssuer(issuer, context);
104        }
105
106        // TODO - Validate issue time
107
108        // Get and validate the NameID
109        NameID nameID;
110        if (getDecrypter() != null && request.getEncryptedID() != null) {
111            try {
112                nameID = (NameID) getDecrypter().decrypt(request.getEncryptedID());
113            } catch (DecryptionException e) {
114                throw new SAMLException("Failed to decrypt NameID", e);
115            }
116        } else {
117            nameID = request.getNameID();
118        }
119
120        if (nameID == null) {
121            throw new SAMLException("The requested NameID is invalid");
122        }
123
124        // If no index is specified do logout
125        if (request.getSessionIndexes() == null || request.getSessionIndexes().isEmpty()) {
126            return true;
127        }
128
129        // Else check if this is on of our session indexes
130        for (SessionIndex sessionIndex : request.getSessionIndexes()) {
131            if (credential.getSessionIndexes().contains(sessionIndex.getSessionIndex())) {
132                return true;
133            }
134        }
135
136        return false;
137    }
138
139    @Override
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            validateSignature(response.getSignature(), context.getPeerEntityId());
153            context.setInboundSAMLMessageAuthenticated(true);
154        }
155
156        // TODO - Validate destination
157
158        // Validate issuer
159        if (response.getIssuer() != null) {
160            log.debug("Verifying issuer of the message");
161            Issuer issuer = response.getIssuer();
162            validateIssuer(issuer, context);
163        }
164
165        // TODO - Validate issue time
166
167        // Verify status
168        String statusCode = response.getStatus().getStatusCode().getValue();
169        if (!statusCode.equals(StatusCode.SUCCESS_URI) && !statusCode.equals(StatusCode.PARTIAL_LOGOUT_URI)) {
170            log.warn("Invalid status code " + statusCode + ": " + response.getStatus().getStatusMessage());
171        }
172    }
173}