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            try {
093                validateSignature(request.getSignature(), context.getPeerEntityId());
094            } catch (ValidationException e) {
095                log.error("Error validating signature", e);
096            } catch (org.opensaml.xml.security.SecurityException e) {
097                e.printStackTrace();
098            }
099            context.setInboundSAMLMessageAuthenticated(true);
100        }
101
102        // TODO - Validate destination
103
104        // Validate issuer
105        if (request.getIssuer() != null) {
106            log.debug("Verifying issuer of the message");
107            Issuer issuer = request.getIssuer();
108            validateIssuer(issuer, context);
109        }
110
111        // TODO - Validate issue time
112
113        // Get and validate the NameID
114        NameID nameID;
115        if (getDecrypter() != null && request.getEncryptedID() != null) {
116            try {
117                nameID = (NameID) getDecrypter().decrypt(request.getEncryptedID());
118            } catch (DecryptionException e) {
119                throw new SAMLException("Failed to decrypt NameID", e);
120            }
121        } else {
122            nameID = request.getNameID();
123        }
124
125        if (nameID == null) {
126            throw new SAMLException("The requested NameID is invalid");
127        }
128
129        // If no index is specified do logout
130        if (request.getSessionIndexes() == null || request.getSessionIndexes().isEmpty()) {
131            return true;
132        }
133
134        // Else check if this is on of our session indexes
135        for (SessionIndex sessionIndex : request.getSessionIndexes()) {
136            if (credential.getSessionIndexes().contains(sessionIndex.getSessionIndex())) {
137                return true;
138            }
139        }
140
141        return false;
142    }
143
144    public void processLogoutResponse(SAMLMessageContext context) throws SAMLException {
145
146        SAMLObject message = context.getInboundSAMLMessage();
147
148        if (!(message instanceof LogoutResponse)) {
149            throw new SAMLException("Message is not of a LogoutResponse object type");
150        }
151        LogoutResponse response = (LogoutResponse) message;
152
153        // Validate signature of the response if present
154        if (response.getSignature() != null) {
155            log.debug("Verifying message signature");
156            try {
157                validateSignature(response.getSignature(), context.getPeerEntityId());
158            } catch (ValidationException e) {
159                log.error("Error validating signature", e);
160            } catch (org.opensaml.xml.security.SecurityException e) {
161                e.printStackTrace();
162            }
163            context.setInboundSAMLMessageAuthenticated(true);
164        }
165
166        // TODO - Validate destination
167
168        // Validate issuer
169        if (response.getIssuer() != null) {
170            log.debug("Verifying issuer of the message");
171            Issuer issuer = response.getIssuer();
172            validateIssuer(issuer, context);
173        }
174
175        // TODO - Validate issue time
176
177        // Verify status
178        String statusCode = response.getStatus().getStatusCode().getValue();
179        if (!statusCode.equals(StatusCode.SUCCESS_URI) && !statusCode.equals(StatusCode.PARTIAL_LOGOUT_URI)) {
180            log.warn("Invalid status code " + statusCode + ": " + response.getStatus().getStatusMessage());
181        }
182    }
183}