/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.syncope.core.logic;
import com.fasterxml.uuid.Generators;
import com.fasterxml.uuid.impl.RandomBasedGenerator;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer;
import org.apache.cxf.rs.security.jose.jws.JwsSignatureVerifier;
import org.apache.syncope.common.lib.AbstractBaseBean;
import org.apache.syncope.common.lib.SyncopeClientException;
import org.apache.syncope.common.lib.to.AttrTO;
import org.apache.syncope.common.lib.to.SAML2RequestTO;
import org.apache.syncope.common.lib.to.SAML2LoginResponseTO;
import org.apache.syncope.common.lib.to.MappingItemTO;
import org.apache.syncope.common.lib.to.SAML2ReceivedResponseTO;
import org.apache.syncope.common.lib.types.AnyTypeKind;
import org.apache.syncope.common.lib.types.ClientExceptionType;
import org.apache.syncope.common.lib.types.SAML2BindingType;
import org.apache.syncope.common.lib.types.StandardEntitlement;
import org.apache.syncope.core.logic.saml2.SAML2ReaderWriter;
import org.apache.syncope.core.logic.saml2.SAML2IdPCache;
import org.apache.syncope.core.logic.saml2.SAML2IdPEntity;
import org.apache.syncope.core.persistence.api.attrvalue.validation.ParsingValidationException;
import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO;
import org.apache.syncope.core.persistence.api.dao.NotFoundException;
import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
import org.apache.syncope.core.persistence.api.dao.SAML2IdPDAO;
import org.apache.syncope.core.persistence.api.dao.UserDAO;
import org.apache.syncope.core.persistence.api.entity.EntityFactory;
import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
import org.apache.syncope.core.persistence.api.entity.PlainSchema;
import org.apache.syncope.core.persistence.api.entity.SAML2IdP;
import org.apache.syncope.core.persistence.api.entity.user.UPlainAttrValue;
import org.apache.syncope.core.persistence.api.entity.user.User;
import org.apache.syncope.core.provisioning.api.IntAttrName;
import org.apache.syncope.core.provisioning.api.data.AccessTokenDataBinder;
import org.apache.syncope.core.provisioning.api.data.MappingItemTransformer;
import org.apache.syncope.core.provisioning.api.utils.EntityUtils;
import org.apache.syncope.core.provisioning.java.IntAttrNameParser;
import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
import org.joda.time.DateTime;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.schema.XSString;
import org.opensaml.saml.common.SAMLVersion;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
import org.opensaml.saml.saml2.core.AuthnContext;
import org.opensaml.saml.saml2.core.AuthnContextClassRef;
import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.AuthnStatement;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.NameIDPolicy;
import org.opensaml.saml.saml2.core.NameIDType;
import org.opensaml.saml.saml2.core.RequestedAuthnContext;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.SessionIndex;
import org.opensaml.saml.saml2.core.StatusCode;
import org.opensaml.saml.saml2.core.impl.AuthnContextClassRefBuilder;
import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder;
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
import org.opensaml.saml.saml2.core.impl.LogoutRequestBuilder;
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
import org.opensaml.saml.saml2.core.impl.NameIDPolicyBuilder;
import org.opensaml.saml.saml2.core.impl.RequestedAuthnContextBuilder;
import org.opensaml.saml.saml2.core.impl.SessionIndexBuilder;
import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml.saml2.metadata.NameIDFormat;
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
import org.opensaml.saml.saml2.metadata.impl.AssertionConsumerServiceBuilder;
import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorBuilder;
import org.opensaml.saml.saml2.metadata.impl.KeyDescriptorBuilder;
import org.opensaml.saml.saml2.metadata.impl.NameIDFormatBuilder;
import org.opensaml.saml.saml2.metadata.impl.SPSSODescriptorBuilder;
import org.opensaml.saml.saml2.metadata.impl.SingleLogoutServiceBuilder;
import org.opensaml.xmlsec.keyinfo.KeyInfoGenerator;
import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.stereotype.Component;
@Component
public class SAML2SPLogic extends AbstractSAML2Logic<AbstractBaseBean> {
private static final Integer JWT_RELAY_STATE_DURATION = 5;
private static final String JWT_CLAIM_IDP_DEFLATE = "IDP_DEFLATE";
private static final String JWT_CLAIM_IDP_ENTITYID = "IDP_ENTITYID";
private static final String JWT_CLAIM_NAMEID_FORMAT = "NAMEID_FORMAT";
private static final String JWT_CLAIM_NAMEID_VALUE = "NAMEID_VALUE";
private static final String JWT_CLAIM_SESSIONINDEX = "SESSIONINDEX";
private static final RandomBasedGenerator UUID_GENERATOR = Generators.randomBasedGenerator();
@Autowired
private JwsSignatureVerifier jwsSignatureCerifier;
@Autowired
private AccessTokenDataBinder accessTokenDataBinder;
@Autowired
private SAML2IdPCache cache;
@Autowired
private UserDAO userDAO;
@Autowired
private SAML2IdPDAO saml2IdPDAO;
@Autowired
private PlainSchemaDAO plainSchemaDAO;
@Autowired
private AccessTokenDAO accessTokenDAO;
@Autowired
private IntAttrNameParser intAttrNameParser;
@Autowired
private EntityFactory entityFactory;
@Autowired
private SAML2ReaderWriter saml2rw;
@PreAuthorize("hasRole('" + StandardEntitlement.ANONYMOUS + "')")
public void getMetadata(final String spEntityID, final String urlContext, final OutputStream os) {
check();
try {
EntityDescriptor spEntityDescriptor = new EntityDescriptorBuilder().buildObject();
spEntityDescriptor.setEntityID(spEntityID);
SPSSODescriptor spSSODescriptor = new SPSSODescriptorBuilder().buildObject();
spSSODescriptor.setWantAssertionsSigned(true);
spSSODescriptor.setAuthnRequestsSigned(true);
spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory();
keyInfoGeneratorFactory.setEmitEntityCertificate(true);
KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance();
keyInfoGenerator.generate(loader.getCredential());
KeyDescriptor keyDescriptor = new KeyDescriptorBuilder().buildObject();
keyDescriptor.setKeyInfo(keyInfoGenerator.generate(loader.getCredential()));
spSSODescriptor.getKeyDescriptors().add(keyDescriptor);
NameIDFormat nameIDFormat = new NameIDFormatBuilder().buildObject();
nameIDFormat.setFormat(NameIDType.PERSISTENT);
spSSODescriptor.getNameIDFormats().add(nameIDFormat);
nameIDFormat = new NameIDFormatBuilder().buildObject();
nameIDFormat.setFormat(NameIDType.TRANSIENT);
spSSODescriptor.getNameIDFormats().add(nameIDFormat);
for (SAML2BindingType bindingType : SAML2BindingType.values()) {
AssertionConsumerService assertionConsumerService = new AssertionConsumerServiceBuilder().buildObject();
assertionConsumerService.setIndex(bindingType.ordinal());
assertionConsumerService.setBinding(bindingType.getUri());
assertionConsumerService.setLocation(spEntityID + urlContext + "/assertion-consumer");
spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService);
spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor);
SingleLogoutService singleLogoutService = new SingleLogoutServiceBuilder().buildObject();
singleLogoutService.setBinding(bindingType.getUri());
singleLogoutService.setLocation(spEntityID + urlContext + "/logout");
singleLogoutService.setResponseLocation(spEntityID + urlContext + "/logout");
spSSODescriptor.getSingleLogoutServices().add(singleLogoutService);
}
spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor);
saml2rw.write(new OutputStreamWriter(os), spEntityDescriptor, true);
} catch (Exception e) {
LOG.error("While getting SP metadata", e);
SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
sce.getElements().add(e.getMessage());
throw sce;
}
}
private SAML2IdPEntity getIdP(final String entityID) {
SAML2IdPEntity idp = null;
SAML2IdP saml2IdP = saml2IdPDAO.findByEntityID(entityID);
if (saml2IdP != null) {
try {
idp = cache.put(saml2IdP);
} catch (Exception e) {
LOG.error("Could not build SAML 2.0 IdP with key ", entityID, e);
SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
sce.getElements().add(e.getMessage());
throw sce;
}
}
if (idp == null) {
throw new NotFoundException("SAML 2.0 IdP '" + entityID + "'");
}
return idp;
}
@PreAuthorize("hasRole('" + StandardEntitlement.ANONYMOUS + "')")
public SAML2RequestTO createLoginRequest(final String spEntityID, final String idpEntityID) {
check();
// 1. look for IdP
SAML2IdPEntity idp = StringUtils.isBlank(idpEntityID) ? cache.getFirst() : cache.get(idpEntityID);
if (idp == null) {
if (StringUtils.isBlank(idpEntityID)) {
List<SAML2IdP> all = saml2IdPDAO.findAll();
if (!all.isEmpty()) {
idp = getIdP(all.get(0).getKey());
}
} else {
idp = getIdP(idpEntityID);
}
}
if (idp == null) {
throw new NotFoundException(StringUtils.isBlank(idpEntityID)
? "Any SAML 2.0 IdP"
: "SAML 2.0 IdP '" + idpEntityID + "'");
}
if (idp.getSSOLocation(idp.getBindingType()) == null) {
throw new IllegalArgumentException("No SingleSignOnService available for " + idp.getId());
}
// 2. create AuthnRequest
Issuer issuer = new IssuerBuilder().buildObject();
issuer.setValue(spEntityID);
NameIDPolicy nameIDPolicy = new NameIDPolicyBuilder().buildObject();
if (idp.supportsNameIDFormat(NameIDType.TRANSIENT)) {
nameIDPolicy.setFormat(NameIDType.TRANSIENT);
} else if (idp.supportsNameIDFormat(NameIDType.PERSISTENT)) {
nameIDPolicy.setFormat(NameIDType.PERSISTENT);
} else {
throw new IllegalArgumentException("Could not find supported NameIDFormat for IdP " + idpEntityID);
}
nameIDPolicy.setAllowCreate(true);
nameIDPolicy.setSPNameQualifier(spEntityID);
AuthnContextClassRef authnContextClassRef = new AuthnContextClassRefBuilder().buildObject();
authnContextClassRef.setAuthnContextClassRef(AuthnContext.PPT_AUTHN_CTX);
RequestedAuthnContext requestedAuthnContext = new RequestedAuthnContextBuilder().buildObject();
requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef);
AuthnRequest authnRequest = new AuthnRequestBuilder().buildObject();
authnRequest.setID("_" + UUID_GENERATOR.generate().toString());
authnRequest.setForceAuthn(false);
authnRequest.setIsPassive(false);
authnRequest.setVersion(SAMLVersion.VERSION_20);
authnRequest.setProtocolBinding(idp.getBindingType().getUri());
authnRequest.setIssueInstant(new DateTime());
authnRequest.setIssuer(issuer);
authnRequest.setNameIDPolicy(nameIDPolicy);
authnRequest.setRequestedAuthnContext(requestedAuthnContext);
authnRequest.setDestination(idp.getSSOLocation(idp.getBindingType()).getLocation());
SAML2RequestTO requestTO = new SAML2RequestTO();
requestTO.setIdpServiceAddress(authnRequest.getDestination());
requestTO.setBindingType(idp.getBindingType());
try {
// 3. generate relay state as JWT
Map<String, Object> claims = new HashMap<>();
claims.put(JWT_CLAIM_IDP_DEFLATE, idp.isUseDeflateEncoding());
Triple<String, String, Date> relayState =
accessTokenDataBinder.generateJWT(authnRequest.getID(), JWT_RELAY_STATE_DURATION, claims);
// 4. sign and encode AuthnRequest
switch (idp.getBindingType()) {
case REDIRECT:
requestTO.setRelayState(URLEncoder.encode(relayState.getMiddle(), StandardCharsets.UTF_8.name()));
requestTO.setContent(URLEncoder.encode(
saml2rw.encode(authnRequest, true), StandardCharsets.UTF_8.name()));
requestTO.setSignAlg(URLEncoder.encode(saml2rw.getSigAlgo(), StandardCharsets.UTF_8.name()));
requestTO.setSignature(URLEncoder.encode(
saml2rw.sign(requestTO.getContent(), requestTO.getRelayState()),
StandardCharsets.UTF_8.name()));
break;
case POST:
default:
requestTO.setRelayState(relayState.getMiddle());
saml2rw.sign(authnRequest);
requestTO.setContent(saml2rw.encode(authnRequest, idp.isUseDeflateEncoding()));
}
} catch (Exception e) {
LOG.error("While generating AuthnRequest", e);
SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
sce.getElements().add(e.getMessage());
throw sce;
}
return requestTO;
}
private List<String> findMatchingUser(final String keyValue, final MappingItemTO connObjectKeyItem) {
List<String> result = new ArrayList<>();
String transformed = keyValue;
for (MappingItemTransformer transformer : MappingUtils.getMappingItemTransformers(connObjectKeyItem)) {
List<Object> output = transformer.beforePull(
null,
null,
Collections.<Object>singletonList(transformed));
if (output != null && !output.isEmpty()) {
transformed = output.get(0).toString();
}
}
IntAttrName intAttrName = intAttrNameParser.parse(connObjectKeyItem.getIntAttrName(), AnyTypeKind.USER);
if (intAttrName.getField() != null) {
switch (intAttrName.getField()) {
case "key":
User byKey = userDAO.find(transformed);
if (byKey != null) {
result.add(byKey.getKey());
}
break;
case "username":
User byUsername = userDAO.findByUsername(transformed);
if (byUsername != null) {
result.add(byUsername.getKey());
}
break;
default:
}
} else if (intAttrName.getSchemaType() != null) {
switch (intAttrName.getSchemaType()) {
case PLAIN:
PlainAttrValue value = entityFactory.newEntity(UPlainAttrValue.class);
PlainSchema schema = plainSchemaDAO.find(intAttrName.getSchemaName());
if (schema == null) {
value.setStringValue(transformed);
} else {
try {
value.parseValue(schema, transformed);
} catch (ParsingValidationException e) {
LOG.error("While parsing provided key value {}", transformed, e);
value.setStringValue(transformed);
}
}
CollectionUtils.collect(userDAO.findByAttrValue(intAttrName.getSchemaName(), value),
EntityUtils.keyTransformer(), result);
break;
case DERIVED:
CollectionUtils.collect(userDAO.findByDerAttrValue(intAttrName.getSchemaName(), transformed),
EntityUtils.keyTransformer(), result);
break;
default:
}
}
return result;
}
@PreAuthorize("hasRole('" + StandardEntitlement.ANONYMOUS + "')")
public SAML2LoginResponseTO validateLoginResponse(final SAML2ReceivedResponseTO response) {
check();
// 1. first checks for the provided relay state
JwsJwtCompactConsumer relayState = new JwsJwtCompactConsumer(response.getRelayState());
if (!relayState.verifySignatureWith(jwsSignatureCerifier)) {
throw new IllegalArgumentException("Invalid signature found in Relay State");
}
Boolean useDeflateEncoding = Boolean.valueOf(
relayState.getJwtClaims().getClaim(JWT_CLAIM_IDP_DEFLATE).toString());
// 2. parse the provided SAML response
Response samlResponse;
try {
XMLObject responseObject = saml2rw.read(useDeflateEncoding, response.getSamlResponse());
if (!(responseObject instanceof Response)) {
throw new IllegalArgumentException("Expected " + Response.class.getName()
+ ", got " + responseObject.getClass().getName());
}
samlResponse = (Response) responseObject;
} catch (Exception e) {
LOG.error("While parsing AuthnResponse", e);
SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
sce.getElements().add(e.getMessage());
throw sce;
}
// 3. further checks:
// 3a. the SAML Reponse's InResponseTo
if (!relayState.getJwtClaims().getSubject().equals(samlResponse.getInResponseTo())) {
throw new IllegalArgumentException("Unmatching request ID: " + samlResponse.getInResponseTo());
}
// 3b. the SAML Response status
if (!StatusCode.SUCCESS.equals(samlResponse.getStatus().getStatusCode().getValue())) {
throw new BadCredentialsException("The SAML IdP replied with "
+ samlResponse.getStatus().getStatusCode().getValue());
}
// 4. validate the SAML response and, if needed, decrypt the provided assertion(s)
SAML2IdPEntity idp = getIdP(samlResponse.getIssuer().getValue());
if (idp.getConnObjectKeyItem() == null) {
throw new IllegalArgumentException("No mapping provided for SAML 2.0 IdP '" + idp.getId() + "'");
}
try {
saml2rw.validate(samlResponse, idp.getTrustStore());
} catch (Exception e) {
LOG.error("While validating AuthnResponse", e);
SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
sce.getElements().add(e.getMessage());
throw sce;
}
// 5. prepare the result: find matching user (if any) and return the received attributes
SAML2LoginResponseTO responseTO = new SAML2LoginResponseTO();
responseTO.setIdp(idp.getId());
responseTO.setSloSupported(idp.getSLOLocation(idp.getBindingType()) != null);
NameID nameID = null;
String keyValue = null;
for (Assertion assertion : samlResponse.getAssertions()) {
nameID = assertion.getSubject().getNameID();
if (StringUtils.isNotBlank(nameID.getValue())
&& idp.getConnObjectKeyItem().getExtAttrName().equals("NameID")) {
keyValue = nameID.getValue();
}
if (assertion.getConditions().getNotOnOrAfter() != null) {
responseTO.setNotOnOrAfter(assertion.getConditions().getNotOnOrAfter().toDate());
}
for (AuthnStatement authnStmt : assertion.getAuthnStatements()) {
responseTO.setSessionIndex(authnStmt.getSessionIndex());
responseTO.setAuthInstant(authnStmt.getAuthnInstant().toDate());
if (authnStmt.getSessionNotOnOrAfter() != null) {
responseTO.setNotOnOrAfter(authnStmt.getSessionNotOnOrAfter().toDate());
}
}
for (AttributeStatement attrStmt : assertion.getAttributeStatements()) {
for (Attribute attr : attrStmt.getAttributes()) {
if (!attr.getAttributeValues().isEmpty()) {
String attrName = attr.getFriendlyName() == null ? attr.getName() : attr.getFriendlyName();
if (attrName.equals(idp.getConnObjectKeyItem().getExtAttrName())
&& attr.getAttributeValues().get(0) instanceof XSString) {
keyValue = ((XSString) attr.getAttributeValues().get(0)).getValue();
}
AttrTO attrTO = new AttrTO();
attrTO.setSchema(attrName);
for (XMLObject value : attr.getAttributeValues()) {
if (value.getDOM() != null) {
attrTO.getValues().add(value.getDOM().getTextContent());
}
}
responseTO.getAttrs().add(attrTO);
}
}
}
}
if (nameID == null) {
throw new IllegalArgumentException("NameID not found");
}
List<String> matchingUsers = keyValue == null
? Collections.<String>emptyList()
: findMatchingUser(keyValue, idp.getConnObjectKeyItem());
LOG.debug("Found {} matching users for NameID {}", matchingUsers.size(), nameID.getValue());
if (matchingUsers.isEmpty()) {
throw new NotFoundException("User matching the provided NameID value " + nameID.getValue());
} else if (matchingUsers.size() > 1) {
throw new IllegalArgumentException("Several users match the provided NameID value " + nameID.getValue());
}
responseTO.setUsername(userDAO.find(matchingUsers.get(0)).getUsername());
responseTO.setNameID(nameID.getValue());
// 6. generate JWT for further access
Map<String, Object> claims = new HashMap<>();
claims.put(JWT_CLAIM_IDP_ENTITYID, idp.getId());
claims.put(JWT_CLAIM_NAMEID_FORMAT, nameID.getFormat());
claims.put(JWT_CLAIM_NAMEID_VALUE, nameID.getValue());
claims.put(JWT_CLAIM_SESSIONINDEX, responseTO.getSessionIndex());
responseTO.setAccessToken(accessTokenDataBinder.create(responseTO.getUsername(), claims, true));
return responseTO;
}
@PreAuthorize("isAuthenticated() and not(hasRole('" + StandardEntitlement.ANONYMOUS + "'))")
public SAML2RequestTO createLogoutRequest(final String accessToken, final String spEntityID) {
check();
// 1. fetch the current JWT used for Syncope authentication
JwsJwtCompactConsumer consumer = new JwsJwtCompactConsumer(accessToken);
if (!consumer.verifySignatureWith(jwsSignatureCerifier)) {
throw new IllegalArgumentException("Invalid signature found in Access Token");
}
// 2. look for IdP
String idpEntityID = (String) consumer.getJwtClaims().getClaim(JWT_CLAIM_IDP_ENTITYID);
if (idpEntityID == null) {
throw new NotFoundException("No SAML 2.0 IdP information found in the access token");
}
SAML2IdPEntity idp = cache.get(idpEntityID);
if (idp == null) {
throw new NotFoundException("SAML 2.0 IdP '" + idpEntityID + "'");
}
if (idp.getSLOLocation(idp.getBindingType()) == null) {
throw new IllegalArgumentException("No SingleLogoutService available for " + idp.getId());
}
// 3. create LogoutRequest
LogoutRequest logoutRequest = new LogoutRequestBuilder().buildObject();
logoutRequest.setID("_" + UUID_GENERATOR.generate().toString());
logoutRequest.setDestination(idp.getSLOLocation(idp.getBindingType()).getLocation());
DateTime now = new DateTime();
logoutRequest.setIssueInstant(now);
logoutRequest.setNotOnOrAfter(now.plusMinutes(5));
Issuer issuer = new IssuerBuilder().buildObject();
issuer.setValue(spEntityID);
logoutRequest.setIssuer(issuer);
NameID nameID = new NameIDBuilder().buildObject();
nameID.setFormat((String) consumer.getJwtClaims().getClaim(JWT_CLAIM_NAMEID_FORMAT));
nameID.setValue((String) consumer.getJwtClaims().getClaim(JWT_CLAIM_NAMEID_VALUE));
logoutRequest.setNameID(nameID);
SessionIndex sessionIndex = new SessionIndexBuilder().buildObject();
sessionIndex.setSessionIndex((String) consumer.getJwtClaims().getClaim(JWT_CLAIM_SESSIONINDEX));
logoutRequest.getSessionIndexes().add(sessionIndex);
SAML2RequestTO requestTO = new SAML2RequestTO();
requestTO.setIdpServiceAddress(logoutRequest.getDestination());
requestTO.setBindingType(idp.getBindingType());
try {
// 3. generate relay state as JWT
Map<String, Object> claims = new HashMap<>();
claims.put(JWT_CLAIM_IDP_DEFLATE,
idp.getBindingType() == SAML2BindingType.REDIRECT ? true : idp.isUseDeflateEncoding());
Triple<String, String, Date> relayState =
accessTokenDataBinder.generateJWT(logoutRequest.getID(), JWT_RELAY_STATE_DURATION, claims);
requestTO.setRelayState(relayState.getMiddle());
// 4. sign and encode AuthnRequest
switch (idp.getBindingType()) {
case REDIRECT:
requestTO.setContent(saml2rw.encode(logoutRequest, true));
requestTO.setSignAlg(saml2rw.getSigAlgo());
requestTO.setSignature(saml2rw.sign(requestTO.getContent(), requestTO.getRelayState()));
break;
case POST:
default:
saml2rw.sign(logoutRequest);
requestTO.setContent(saml2rw.encode(logoutRequest, idp.isUseDeflateEncoding()));
}
} catch (Exception e) {
LOG.error("While generating LogoutRequest", e);
SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
sce.getElements().add(e.getMessage());
throw sce;
}
return requestTO;
}
@PreAuthorize("isAuthenticated() and not(hasRole('" + StandardEntitlement.ANONYMOUS + "'))")
public void validateLogoutResponse(final String accessToken, final SAML2ReceivedResponseTO response) {
check();
// 1. fetch the current JWT used for Syncope authentication
JwsJwtCompactConsumer consumer = new JwsJwtCompactConsumer(accessToken);
if (!consumer.verifySignatureWith(jwsSignatureCerifier)) {
throw new IllegalArgumentException("Invalid signature found in Access Token");
}
// 2. extract raw SAML response and relay state
JwsJwtCompactConsumer relayState = null;
Boolean useDeflateEncoding = false;
if (StringUtils.isNotBlank(response.getRelayState())) {
// first checks for the provided relay state, if available
relayState = new JwsJwtCompactConsumer(response.getRelayState());
if (!relayState.verifySignatureWith(jwsSignatureCerifier)) {
throw new IllegalArgumentException("Invalid signature found in Relay State");
}
useDeflateEncoding = Boolean.valueOf(
relayState.getJwtClaims().getClaim(JWT_CLAIM_IDP_DEFLATE).toString());
}
// 3. parse the provided SAML response
LogoutResponse logoutResponse;
try {
XMLObject responseObject = saml2rw.read(useDeflateEncoding, response.getSamlResponse());
if (!(responseObject instanceof LogoutResponse)) {
throw new IllegalArgumentException("Expected " + LogoutResponse.class.getName()
+ ", got " + responseObject.getClass().getName());
}
logoutResponse = (LogoutResponse) responseObject;
} catch (Exception e) {
LOG.error("While parsing LogoutResponse", e);
SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
sce.getElements().add(e.getMessage());
throw sce;
}
// 4. if relay state was available, check the SAML Reponse's InResponseTo
if (relayState != null && !relayState.getJwtClaims().getSubject().equals(logoutResponse.getInResponseTo())) {
throw new IllegalArgumentException("Unmatching request ID: " + logoutResponse.getInResponseTo());
}
// 5. finally check for the logout status
if (StatusCode.SUCCESS.equals(logoutResponse.getStatus().getStatusCode().getValue())) {
accessTokenDAO.delete(consumer.getJwtClaims().getTokenId());
} else {
SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
if (logoutResponse.getStatus().getStatusMessage() == null) {
sce.getElements().add(logoutResponse.getStatus().getStatusCode().getValue());
} else {
sce.getElements().add(logoutResponse.getStatus().getStatusMessage().getMessage());
}
throw sce;
}
}
@Override
protected AbstractBaseBean resolveReference(
final Method method, final Object... args) throws UnresolvedReferenceException {
throw new UnresolvedReferenceException();
}
}