/** * Copyright (c) Codice Foundation * <p> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p> * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package org.codice.ddf.security.idp.server; import static java.util.Objects.nonNull; import static org.apache.commons.lang.StringUtils.isEmpty; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.Encoded; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.xml.soap.SOAPElement; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPHeaderElement; import javax.xml.soap.SOAPPart; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import org.apache.commons.io.IOUtils; import org.apache.cxf.binding.soap.Soap11; import org.apache.cxf.binding.soap.SoapMessage; import org.apache.cxf.binding.soap.saaj.SAAJInInterceptor; import org.apache.cxf.helpers.DOMUtils; import org.apache.cxf.rs.security.saml.sso.SSOConstants; import org.apache.cxf.ws.security.tokenstore.SecurityToken; import org.apache.wss4j.common.crypto.CryptoType; import org.apache.wss4j.common.ext.WSSecurityException; import org.apache.wss4j.common.saml.OpenSAMLUtil; import org.apache.wss4j.common.saml.builder.SAML2Constants; import org.apache.wss4j.common.util.DOM2Writer; import org.boon.Boon; import org.codehaus.stax2.XMLInputFactory2; import org.codice.ddf.configuration.SystemBaseUrl; import org.codice.ddf.security.common.HttpUtils; import org.codice.ddf.security.common.jaxrs.RestSecurity; import org.codice.ddf.security.handler.api.BaseAuthenticationToken; import org.codice.ddf.security.handler.api.GuestAuthenticationToken; import org.codice.ddf.security.handler.api.HandlerResult; import org.codice.ddf.security.handler.api.PKIAuthenticationTokenFactory; import org.codice.ddf.security.handler.api.SAMLAuthenticationToken; import org.codice.ddf.security.handler.api.UPAuthenticationToken; import org.codice.ddf.security.handler.basic.BasicAuthenticationHandler; import org.codice.ddf.security.handler.pki.PKIHandler; import org.codice.ddf.security.idp.binding.api.Binding; import org.codice.ddf.security.idp.binding.api.ResponseCreator; import org.codice.ddf.security.idp.binding.api.impl.ResponseCreatorImpl; import org.codice.ddf.security.idp.binding.post.PostBinding; import org.codice.ddf.security.idp.binding.redirect.RedirectBinding; import org.codice.ddf.security.idp.binding.soap.SoapBinding; import org.codice.ddf.security.idp.binding.soap.SoapRequestDecoder; import org.codice.ddf.security.idp.cache.CookieCache; import org.codice.ddf.security.policy.context.ContextPolicy; import org.opensaml.core.config.ConfigurationService; import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.config.XMLObjectProviderRegistry; import org.opensaml.saml.common.SignableSAMLObject; import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.AuthnContextClassRef; import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.core.LogoutRequest; import org.opensaml.saml.saml2.core.LogoutResponse; import org.opensaml.saml.saml2.core.RequestedAuthnContext; import org.opensaml.saml.saml2.core.StatusCode; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.security.credential.UsageType; import org.opensaml.xmlsec.signature.SignableXMLObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import ddf.security.Subject; import ddf.security.assertion.SecurityAssertion; import ddf.security.assertion.impl.SecurityAssertionImpl; import ddf.security.encryption.EncryptionService; import ddf.security.liberty.paos.Request; import ddf.security.liberty.paos.impl.RequestBuilder; import ddf.security.liberty.paos.impl.RequestMarshaller; import ddf.security.liberty.paos.impl.RequestUnmarshaller; import ddf.security.liberty.paos.impl.ResponseBuilder; import ddf.security.liberty.paos.impl.ResponseMarshaller; import ddf.security.liberty.paos.impl.ResponseUnmarshaller; import ddf.security.samlp.LogoutMessage; import ddf.security.samlp.MetadataConfigurationParser; import ddf.security.samlp.SamlProtocol; import ddf.security.samlp.SimpleSign; import ddf.security.samlp.SystemCrypto; import ddf.security.samlp.ValidationException; import ddf.security.samlp.impl.EntityInformation; import ddf.security.samlp.impl.HtmlResponseTemplate; import ddf.security.samlp.impl.RelayStates; import ddf.security.samlp.impl.SamlValidator; import ddf.security.service.SecurityManager; import ddf.security.service.SecurityServiceException; import net.shibboleth.utilities.java.support.logic.ConstraintViolationException; @Path("/") public class IdpEndpoint implements Idp { public static final String SERVICES_IDP_PATH = SystemBaseUrl.getRootContext() + "/idp"; private static final Logger LOGGER = LoggerFactory.getLogger(IdpEndpoint.class); private static final String CERTIFICATES_ATTR = "javax.servlet.request.X509Certificate"; /** * Input factory */ private static volatile XMLInputFactory xmlInputFactory = null; static { XMLInputFactory xmlInputFactoryTmp = XMLInputFactory2.newInstance(); xmlInputFactoryTmp.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.FALSE); xmlInputFactoryTmp.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE); xmlInputFactoryTmp.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE); // This disables DTDs entirely for that factory xmlInputFactoryTmp.setProperty(XMLInputFactory.IS_COALESCING, Boolean.FALSE); xmlInputFactory = xmlInputFactoryTmp; } protected CookieCache cookieCache = new CookieCache(); private PKIAuthenticationTokenFactory tokenFactory; private SecurityManager securityManager; private Map<String, EntityInformation> serviceProviders = new ConcurrentHashMap<>(); private String indexHtml; private String submitForm; private String redirectPage; private String soapMessage; private Boolean strictSignature = true; private SystemCrypto systemCrypto; private LogoutMessage logoutMessage; private RelayStates<LogoutState> logoutStates; private boolean guestAccess = true; public static final ImmutableSet<UsageType> USAGE_TYPES = ImmutableSet.of(UsageType.UNSPECIFIED, UsageType.SIGNING); public IdpEndpoint(String signaturePropertiesPath, String encryptionPropertiesPath, EncryptionService encryptionService) { systemCrypto = new SystemCrypto(encryptionPropertiesPath, signaturePropertiesPath, encryptionService); } public void init() { try ( InputStream indexStream = IdpEndpoint.class.getResourceAsStream("/html/index.html"); InputStream submitFormStream = IdpEndpoint.class.getResourceAsStream( "/templates/submitForm.handlebars"); InputStream redirectPageStream = IdpEndpoint.class.getResourceAsStream( "/templates/redirect.handlebars"); InputStream soapMessageStream = IdpEndpoint.class.getResourceAsStream( "/templates/soap.handlebars"); ) { indexHtml = IOUtils.toString(indexStream); submitForm = IOUtils.toString(submitFormStream); redirectPage = IOUtils.toString(redirectPageStream); soapMessage = IOUtils.toString(soapMessageStream); } catch (Exception e) { LOGGER.info("Unable to load index page for IDP.", e); } OpenSAMLUtil.initSamlEngine(); XMLObjectProviderRegistry xmlObjectProviderRegistry = ConfigurationService.get( XMLObjectProviderRegistry.class); xmlObjectProviderRegistry.registerObjectProvider(Request.DEFAULT_ELEMENT_NAME, new RequestBuilder(), new RequestMarshaller(), new RequestUnmarshaller()); xmlObjectProviderRegistry.registerObjectProvider( ddf.security.liberty.paos.Response.DEFAULT_ELEMENT_NAME, new ResponseBuilder(), new ResponseMarshaller(), new ResponseUnmarshaller()); } private void parseServiceProviderMetadata(List<String> serviceProviderMetadata) { if (serviceProviderMetadata != null) { try { MetadataConfigurationParser metadataConfigurationParser = new MetadataConfigurationParser(serviceProviderMetadata, ed -> { EntityInformation entityInfo = new EntityInformation.Builder(ed, SUPPORTED_BINDINGS).build(); if (entityInfo != null) { serviceProviders.put(ed.getEntityID(), entityInfo); } }); serviceProviders.putAll(metadataConfigurationParser.getEntryDescriptions() .entrySet() .stream() .map(e -> Maps.immutableEntry(e.getKey(), new EntityInformation.Builder(e.getValue(), SUPPORTED_BINDINGS).build())) .filter(e -> nonNull(e.getValue())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); } catch (IOException e) { LOGGER.warn("Unable to parse SP metadata configuration. Check the configuration for SP metadata.", e); } } } @POST @Path("/login") @Consumes({"text/xml", "application/soap+xml"}) public Response doSoapLogin(InputStream body, @Context HttpServletRequest request) { if (!request.isSecure()) { throw new IllegalArgumentException("Authn Request must use TLS."); } SoapBinding soapBinding = new SoapBinding(systemCrypto, serviceProviders); try { String bodyStr = IOUtils.toString(body); AuthnRequest authnRequest = soapBinding.decoder() .decodeRequest(bodyStr); String relayState = ((SoapRequestDecoder) soapBinding.decoder()).decodeRelayState( bodyStr); soapBinding.validator() .validateRelayState(relayState); soapBinding.validator() .validateAuthnRequest(authnRequest, bodyStr, null, null, null, strictSignature); boolean hasCookie = hasValidCookie(request, authnRequest.isForceAuthn()); AuthObj authObj = determineAuthMethod(bodyStr, authnRequest); org.opensaml.saml.saml2.core.Response response = handleLogin(authnRequest, authObj.method, request, authObj, authnRequest.isPassive(), hasCookie); Response samlpResponse = soapBinding.creator() .getSamlpResponse(relayState, authnRequest, response, null, soapMessage); samlpResponse.getHeaders() .put("SOAPAction", Collections.singletonList( "http://www.oasis-open.org/committees/security")); return samlpResponse; } catch (IOException e) { LOGGER.debug("Unable to decode SOAP AuthN Request", e); } catch (SimpleSign.SignatureException e) { LOGGER.debug("Unable to validate signature.", e); } catch (ValidationException e) { LOGGER.debug("Unable to validate request.", e); } catch (SecurityServiceException e) { LOGGER.debug("Unable to authenticate user.", e); } catch (WSSecurityException | IllegalArgumentException e) { LOGGER.debug("Bad request.", e); } return null; } private AuthObj determineAuthMethod(String bodyStr, AuthnRequest authnRequest) { XMLStreamReader xmlStreamReader = null; try { xmlStreamReader = xmlInputFactory.createXMLStreamReader(new StringReader(bodyStr)); } catch (XMLStreamException e) { LOGGER.debug("Unable to parse SOAP message from client.", e); } SoapMessage soapMessage = new SoapMessage(Soap11.getInstance()); SAAJInInterceptor.SAAJPreInInterceptor preInInterceptor = new SAAJInInterceptor.SAAJPreInInterceptor(); soapMessage.setContent(XMLStreamReader.class, xmlStreamReader); preInInterceptor.handleMessage(soapMessage); SAAJInInterceptor inInterceptor = new SAAJInInterceptor(); inInterceptor.handleMessage(soapMessage); SOAPPart soapMessageContent = (SOAPPart) soapMessage.getContent(Node.class); AuthObj authObj = new AuthObj(); try { Iterator soapHeaderElements = soapMessageContent.getEnvelope() .getHeader() .examineAllHeaderElements(); while (soapHeaderElements.hasNext()) { SOAPHeaderElement soapHeaderElement = (SOAPHeaderElement) soapHeaderElements.next(); if (soapHeaderElement.getLocalName() .equals("Security")) { Iterator childElements = soapHeaderElement.getChildElements(); while (childElements.hasNext()) { Object nextElement = childElements.next(); if (nextElement instanceof SOAPElement) { SOAPElement element = (SOAPElement) nextElement; if (element.getLocalName() .equals("UsernameToken")) { Iterator usernameTokenElements = element.getChildElements(); Object next; while (usernameTokenElements.hasNext()) { if ((next = usernameTokenElements.next()) instanceof Element) { Element nextEl = (Element) next; if (nextEl.getLocalName() .equals("Username")) { authObj.username = nextEl.getTextContent(); } else if (nextEl.getLocalName() .equals("Password")) { authObj.password = nextEl.getTextContent(); } } } if (authObj.username != null && authObj.password != null) { authObj.method = USER_PASS; break; } } else if (element.getLocalName() .equals("Assertion") && element.getNamespaceURI() .equals("urn:oasis:names:tc:SAML:2.0:assertion")) { authObj.assertion = new SecurityToken(element.getAttribute("ID"), element, null, null); authObj.method = SAML; break; } } } } } } catch (SOAPException e) { LOGGER.debug("Unable to parse SOAP message.", e); } RequestedAuthnContext requestedAuthnContext = authnRequest.getRequestedAuthnContext(); boolean requestingPki = false; boolean requestingUp = false; if (requestedAuthnContext != null) { List<AuthnContextClassRef> authnContextClassRefs = requestedAuthnContext.getAuthnContextClassRefs(); for (AuthnContextClassRef authnContextClassRef : authnContextClassRefs) { String authnContextClassRefStr = authnContextClassRef.getAuthnContextClassRef(); if (SAML2Constants.AUTH_CONTEXT_CLASS_REF_X509.equals(authnContextClassRefStr) || SAML2Constants.AUTH_CONTEXT_CLASS_REF_SMARTCARD_PKI.equals( authnContextClassRefStr) || SAML2Constants.AUTH_CONTEXT_CLASS_REF_SOFTWARE_PKI.equals( authnContextClassRefStr) || SAML2Constants.AUTH_CONTEXT_CLASS_REF_SPKI.equals(authnContextClassRefStr) || SAML2Constants.AUTH_CONTEXT_CLASS_REF_TLS_CLIENT.equals( authnContextClassRefStr)) { requestingPki = true; } else if (SAML2Constants.AUTH_CONTEXT_CLASS_REF_PASSWORD.equals( authnContextClassRefStr) || SAML2Constants.AUTH_CONTEXT_CLASS_REF_PASSWORD_PROTECTED_TRANSPORT.equals( authnContextClassRefStr)) { requestingUp = true; } } } else { //The requested auth context isn't required so we don't know what they want... just set both to true requestingPki = true; requestingUp = true; } if (requestingUp && authObj.method != null && authObj.method.equals(USER_PASS)) { LOGGER.trace("Found UsernameToken and correct AuthnContextClassRef"); return authObj; } else if (requestingPki && authObj.method == null) { LOGGER.trace("Found no token, but client requested PKI AuthnContextClassRef"); authObj.method = PKI; return authObj; } else if (authObj.method == null) { LOGGER.debug( "No authentication tokens found for the current request and the client did not request PKI authentication"); } return authObj; } @POST @Path("/login") public Response showPostLogin(@FormParam(SAML_REQ) String samlRequest, @FormParam(RELAY_STATE) String relayState, @Context HttpServletRequest request) throws WSSecurityException { LOGGER.debug("Received POST IdP request."); return showLoginPage(samlRequest, relayState, null, null, request, new PostBinding(systemCrypto, serviceProviders), submitForm, SamlProtocol.POST_BINDING); } @GET @Path("/login") public Response showGetLogin(@QueryParam(SAML_REQ) String samlRequest, @Encoded @QueryParam(RELAY_STATE) String relayState, @QueryParam(SSOConstants.SIG_ALG) String signatureAlgorithm, @QueryParam(SSOConstants.SIGNATURE) String signature, @Context HttpServletRequest request) throws WSSecurityException { LOGGER.debug("Received GET IdP request."); return showLoginPage(samlRequest, relayState, signatureAlgorithm, signature, request, new RedirectBinding(systemCrypto, serviceProviders), redirectPage, SamlProtocol.REDIRECT_BINDING); } private Response showLoginPage(String samlRequest, String relayState, String signatureAlgorithm, String signature, HttpServletRequest request, Binding binding, String template, String originalBinding) throws WSSecurityException { String responseStr; AuthnRequest authnRequest = null; try { Map<String, Object> responseMap = new HashMap<>(); binding.validator() .validateRelayState(relayState); authnRequest = binding.decoder() .decodeRequest(samlRequest); authnRequest.getIssueInstant(); binding.validator() .validateAuthnRequest(authnRequest, samlRequest, relayState, signatureAlgorithm, signature, strictSignature); if (!request.isSecure()) { throw new IllegalArgumentException("Authn Request must use TLS."); } X509Certificate[] certs = (X509Certificate[]) request.getAttribute(CERTIFICATES_ATTR); boolean hasCerts = (certs != null && certs.length > 0); boolean hasCookie = hasValidCookie(request, authnRequest.isForceAuthn()); if ((authnRequest.isPassive() && hasCerts) || hasCookie) { LOGGER.debug("Received Passive & PKI AuthnRequest."); org.opensaml.saml.saml2.core.Response samlpResponse; try { samlpResponse = handleLogin(authnRequest, PKI, request, null, authnRequest.isPassive(), hasCookie); LOGGER.debug("Passive & PKI AuthnRequest logged in successfully."); } catch (SecurityServiceException e) { LOGGER.debug(e.getMessage(), e); return getErrorResponse(relayState, authnRequest, StatusCode.AUTHN_FAILED, binding); } catch (WSSecurityException e) { LOGGER.debug(e.getMessage(), e); return getErrorResponse(relayState, authnRequest, StatusCode.REQUEST_DENIED, binding); } catch (SimpleSign.SignatureException | ConstraintViolationException e) { LOGGER.debug(e.getMessage(), e); return getErrorResponse(relayState, authnRequest, StatusCode.REQUEST_UNSUPPORTED, binding); } LOGGER.debug("Returning Passive & PKI SAML Response."); NewCookie cookie = null; if (hasCookie) { cookieCache.addActiveSp(getCookie(request).getValue(), authnRequest.getIssuer() .getValue()); } else { cookie = createCookie(request, samlpResponse); if (cookie != null) { cookieCache.addActiveSp(cookie.getValue(), authnRequest.getIssuer() .getValue()); } } logAddedSp(authnRequest); return binding.creator() .getSamlpResponse(relayState, authnRequest, samlpResponse, cookie, template); } else { LOGGER.debug("Building the JSON map to embed in the index.html page for login."); Document doc = DOMUtils.createDocument(); doc.appendChild(doc.createElement("root")); String authn = DOM2Writer.nodeToString(OpenSAMLUtil.toDom(authnRequest, doc, false)); String encodedAuthn = RestSecurity.deflateAndBase64Encode(authn); responseMap.put(PKI, hasCerts); responseMap.put(GUEST, guestAccess); responseMap.put(SAML_REQ, encodedAuthn); responseMap.put(RELAY_STATE, relayState); String assertionConsumerServiceURL = ((ResponseCreatorImpl) binding.creator()).getAssertionConsumerServiceURL( authnRequest); responseMap.put(ACS_URL, assertionConsumerServiceURL); responseMap.put(SSOConstants.SIG_ALG, signatureAlgorithm); responseMap.put(SSOConstants.SIGNATURE, signature); responseMap.put(ORIGINAL_BINDING, originalBinding); } String json = Boon.toJson(responseMap); LOGGER.debug("Returning index.html page."); responseStr = indexHtml.replace(IDP_STATE_OBJ, json); return Response.ok(responseStr) .build(); } catch (IllegalArgumentException e) { LOGGER.debug(e.getMessage(), e); if (authnRequest != null) { try { return getErrorResponse(relayState, authnRequest, StatusCode.REQUEST_UNSUPPORTED, binding); } catch (IOException | SimpleSign.SignatureException e1) { LOGGER.debug(e1.getMessage(), e1); } } } catch (UnsupportedOperationException e) { LOGGER.debug(e.getMessage(), e); if (authnRequest != null) { try { return getErrorResponse(relayState, authnRequest, StatusCode.UNSUPPORTED_BINDING, binding); } catch (IOException | SimpleSign.SignatureException e1) { LOGGER.debug(e1.getMessage(), e1); } } } catch (SimpleSign.SignatureException e) { LOGGER.debug("Unable to validate AuthRequest Signature", e); } catch (IOException e) { LOGGER.debug("Unable to decode AuthRequest", e); } catch (ValidationException e) { LOGGER.debug("AuthnRequest schema validation failed.", e); } return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .build(); } void logAddedSp(AuthnRequest authnRequest) { LOGGER.debug("request id [{}] added activeSP list: {}", authnRequest.getID(), authnRequest.getIssuer() .getValue()); } private Response getErrorResponse(String relayState, AuthnRequest authnRequest, String statusCode, Binding binding) throws WSSecurityException, IOException, SimpleSign.SignatureException { LOGGER.debug("Creating SAML Response for error condition."); org.opensaml.saml.saml2.core.Response samlResponse = SamlProtocol.createResponse( SamlProtocol.createIssuer(SystemBaseUrl.constructUrl("/idp/login", true)), SamlProtocol.createStatus(statusCode), authnRequest.getID(), null); LOGGER.debug("Encoding error SAML Response for post or redirect."); String template = ""; if (binding instanceof PostBinding) { template = submitForm; } else if (binding instanceof RedirectBinding) { template = redirectPage; } else if (binding instanceof SoapBinding) { template = soapMessage; } return binding.creator() .getSamlpResponse(relayState, authnRequest, samlResponse, null, template); } @GET @Path("/login/sso") public Response processLogin(@QueryParam(SAML_REQ) String samlRequest, @QueryParam(RELAY_STATE) String relayState, @QueryParam(AUTH_METHOD) String authMethod, @QueryParam(SSOConstants.SIG_ALG) String signatureAlgorithm, @QueryParam(SSOConstants.SIGNATURE) String signature, @QueryParam(ORIGINAL_BINDING) String originalBinding, @Context HttpServletRequest request) { LOGGER.debug("Processing login request: [ authMethod {} ], [ sigAlg {} ], [ relayState {} ]", authMethod, signatureAlgorithm, relayState); try { Binding binding; String template; if (!request.isSecure()) { throw new IllegalArgumentException("Authn Request must use TLS."); } //the authn request is always encoded as if it came in via redirect when coming from the web app Binding redirectBinding = new RedirectBinding(systemCrypto, serviceProviders); AuthnRequest authnRequest = redirectBinding.decoder() .decodeRequest(samlRequest); String assertionConsumerServiceBinding = ResponseCreator.getAssertionConsumerServiceBinding(authnRequest, serviceProviders); if (HTTP_POST_BINDING.equals(originalBinding)) { binding = new PostBinding(systemCrypto, serviceProviders); template = submitForm; } else if (HTTP_REDIRECT_BINDING.equals(originalBinding)) { binding = redirectBinding; template = redirectPage; } else { throw new IdpException(new UnsupportedOperationException( "Must use HTTP POST or Redirect bindings.")); } binding.validator() .validateAuthnRequest(authnRequest, samlRequest, relayState, signatureAlgorithm, signature, strictSignature); if (HTTP_POST_BINDING.equals(assertionConsumerServiceBinding)) { if (!(binding instanceof PostBinding)) { binding = new PostBinding(systemCrypto, serviceProviders); } } else if (HTTP_REDIRECT_BINDING.equals(assertionConsumerServiceBinding)) { if (!(binding instanceof RedirectBinding)) { binding = new RedirectBinding(systemCrypto, serviceProviders); } } org.opensaml.saml.saml2.core.Response encodedSaml = handleLogin(authnRequest, authMethod, request, null, false, false); LOGGER.debug("Returning SAML Response for relayState: {}" + relayState); NewCookie newCookie = createCookie(request, encodedSaml); Response response = binding.creator() .getSamlpResponse(relayState, authnRequest, encodedSaml, newCookie, template); if (newCookie != null) { cookieCache.addActiveSp(newCookie.getValue(), authnRequest.getIssuer() .getValue()); logAddedSp(authnRequest); } return response; } catch (SecurityServiceException e) { LOGGER.info("Unable to retrieve subject for user.", e); return Response.status(Response.Status.UNAUTHORIZED) .build(); } catch (WSSecurityException e) { LOGGER.info("Unable to encode SAMLP response.", e); } catch (SimpleSign.SignatureException e) { LOGGER.info("Unable to sign SAML response.", e); } catch (IllegalArgumentException e) { LOGGER.info(e.getMessage(), e); return Response.status(Response.Status.BAD_REQUEST) .build(); } catch (ValidationException e) { LOGGER.info("AuthnRequest schema validation failed.", e); return Response.status(Response.Status.BAD_REQUEST) .build(); } catch (IOException e) { LOGGER.info("Unable to create SAML Response.", e); } catch (IdpException e) { LOGGER.info(e.getMessage(), e); return Response.status(Response.Status.BAD_REQUEST) .build(); } return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .build(); } protected org.opensaml.saml.saml2.core.Response handleLogin(AuthnRequest authnRequest, String authMethod, HttpServletRequest request, AuthObj authObj, boolean passive, boolean hasCookie) throws SecurityServiceException, WSSecurityException, SimpleSign.SignatureException, ConstraintViolationException { LOGGER.debug("Performing login for user. passive: {}, cookie: {}", passive, hasCookie); BaseAuthenticationToken token = null; request.setAttribute(ContextPolicy.ACTIVE_REALM, BaseAuthenticationToken.ALL_REALM); if (PKI.equals(authMethod)) { LOGGER.debug("Logging user in via PKI."); PKIHandler pkiHandler = new PKIHandler(); pkiHandler.setTokenFactory(tokenFactory); try { HandlerResult handlerResult = pkiHandler.getNormalizedToken(request, null, null, false); if (handlerResult.getStatus() .equals(HandlerResult.Status.COMPLETED)) { token = handlerResult.getToken(); } } catch (ServletException e) { LOGGER.info("Encountered an exception while checking for PKI auth info.", e); } } else if (USER_PASS.equals(authMethod)) { LOGGER.debug("Logging user in via BASIC auth."); if (authObj != null && authObj.username != null && authObj.password != null) { token = new UPAuthenticationToken(authObj.username, authObj.password, BaseAuthenticationToken.ALL_REALM); } else { BasicAuthenticationHandler basicAuthenticationHandler = new BasicAuthenticationHandler(); HandlerResult handlerResult = basicAuthenticationHandler.getNormalizedToken(request, null, null, false); if (handlerResult.getStatus() .equals(HandlerResult.Status.COMPLETED)) { token = handlerResult.getToken(); } } } else if (SAML.equals(authMethod)) { LOGGER.debug("Logging user in via SAML assertion."); token = new SAMLAuthenticationToken(null, authObj.assertion, BaseAuthenticationToken.ALL_REALM); } else if (GUEST.equals(authMethod) && guestAccess) { LOGGER.debug("Logging user in as Guest."); token = new GuestAuthenticationToken(BaseAuthenticationToken.ALL_REALM, request.getRemoteAddr()); } else { throw new IllegalArgumentException("Auth method is not supported."); } org.w3c.dom.Element samlToken = null; String statusCode; if (hasCookie) { samlToken = getSamlAssertion(request); statusCode = StatusCode.SUCCESS; } else { try { statusCode = StatusCode.AUTHN_FAILED; Subject subject = securityManager.getSubject(token); for (Object principal : subject.getPrincipals() .asList()) { if (principal instanceof SecurityAssertion) { SecurityToken securityToken = ((SecurityAssertion) principal).getSecurityToken(); samlToken = securityToken.getToken(); } } if (samlToken != null) { statusCode = StatusCode.SUCCESS; } } catch (SecurityServiceException e) { if (!passive) { throw e; } else { statusCode = StatusCode.AUTHN_FAILED; } } } LOGGER.debug("User log in successful."); return SamlProtocol.createResponse(SamlProtocol.createIssuer(SystemBaseUrl.constructUrl( "/idp/login", true)), SamlProtocol.createStatus(statusCode), authnRequest.getID(), samlToken); } private Cookie getCookie(HttpServletRequest request) { Map<String, Cookie> cookies = HttpUtils.getCookieMap(request); return cookies.get(COOKIE); } private Element getSamlAssertion(HttpServletRequest request) { Element samlToken = null; Cookie cookie = getCookie(request); if (cookie != null) { LOGGER.debug("Retrieving cookie {}:{} from cache.", cookie.getValue(), cookie.getName()); String key = cookie.getValue(); LOGGER.debug("Retrieving SAML Token from cookie."); samlToken = cookieCache.getSamlAssertion(key); } return samlToken; } private boolean hasValidCookie(HttpServletRequest request, boolean forceAuthn) { Cookie cookie = getCookie(request); if (cookie != null) { LOGGER.debug("Retrieving cookie {}:{} from cache.", cookie.getValue(), cookie.getName()); String key = cookie.getValue(); LOGGER.debug("Retrieving SAML Token from cookie."); Element samlToken = cookieCache.getSamlAssertion(key); if (samlToken != null) { String assertionId = samlToken.getAttribute("ID"); SecurityToken securityToken = new SecurityToken(assertionId, samlToken, null); SecurityAssertionImpl assertion = new SecurityAssertionImpl(securityToken); if (forceAuthn || !assertion.isPresentlyValid()) { cookieCache.removeSamlAssertion(key); return false; } return true; } } return false; } private LogoutState getLogoutState(HttpServletRequest request) { LogoutState logoutState = null; Cookie cookie = getCookie(request); if (cookie != null) { logoutState = logoutStates.decode(cookie.getValue(), false); } return logoutState; } private NewCookie createCookie(HttpServletRequest request, org.opensaml.saml.saml2.core.Response response) { LOGGER.debug("Creating cookie for user."); if (response.getAssertions() != null && response.getAssertions() .size() > 0) { Assertion assertion = response.getAssertions() .get(0); if (assertion != null) { UUID uuid = UUID.randomUUID(); cookieCache.cacheSamlAssertion(uuid.toString(), assertion.getDOM()); URL url; try { url = new URL(request.getRequestURL() .toString()); LOGGER.debug("Returning new cookie for user."); return new NewCookie(COOKIE, uuid.toString(), SERVICES_IDP_PATH, url.getHost(), NewCookie.DEFAULT_VERSION, null, -1, null, true, true); } catch (MalformedURLException e) { LOGGER.info("Unable to create session cookie. Client will need to log in again.", e); } } } return null; } @GET @Path("/login/metadata") @Produces("application/xml") public Response retrieveMetadata() throws WSSecurityException, CertificateEncodingException { List<String> nameIdFormats = new ArrayList<>(); nameIdFormats.add(SAML2Constants.NAMEID_FORMAT_PERSISTENT); nameIdFormats.add(SAML2Constants.NAMEID_FORMAT_UNSPECIFIED); nameIdFormats.add(SAML2Constants.NAMEID_FORMAT_X509_SUBJECT_NAME); CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS); cryptoType.setAlias(systemCrypto.getSignatureCrypto() .getDefaultX509Identifier()); X509Certificate[] certs = systemCrypto.getSignatureCrypto() .getX509Certificates(cryptoType); X509Certificate issuerCert = null; if (certs != null && certs.length > 0) { issuerCert = certs[0]; } cryptoType = new CryptoType(CryptoType.TYPE.ALIAS); cryptoType.setAlias(systemCrypto.getEncryptionCrypto() .getDefaultX509Identifier()); certs = systemCrypto.getEncryptionCrypto() .getX509Certificates(cryptoType); X509Certificate encryptionCert = null; if (certs != null && certs.length > 0) { encryptionCert = certs[0]; } EntityDescriptor entityDescriptor = SamlProtocol.createIdpMetadata(SystemBaseUrl.constructUrl("/idp/login", true), Base64.getEncoder() .encodeToString( issuerCert != null ? issuerCert.getEncoded() : new byte[0]), Base64.getEncoder() .encodeToString(encryptionCert != null ? encryptionCert.getEncoded() : new byte[0]), nameIdFormats, SystemBaseUrl.constructUrl("/idp/login", true), SystemBaseUrl.constructUrl("/idp/login", true), SystemBaseUrl.constructUrl("/idp/logout", true)); Document doc = DOMUtils.createDocument(); doc.appendChild(doc.createElement("root")); return Response.ok(DOM2Writer.nodeToString(OpenSAMLUtil.toDom(entityDescriptor, doc, false))) .build(); } /** * aka HTTP-Redirect * * @param samlRequest the base64 encoded saml request * @param samlResponse the base64 encoded saml response * @param relayState the UUID that references the logout state * @param signatureAlgorithm this signing algorithm * @param signature the signature of the url * @param request the http servlet request * @return Response redirecting to an service provider * @throws WSSecurityException * @throws IdpException */ @Override @GET @Path("/logout") public Response processRedirectLogout(@QueryParam(SAML_REQ) final String samlRequest, @QueryParam(SAML_RESPONSE) final String samlResponse, @QueryParam(RELAY_STATE) final String relayState, @QueryParam(SSOConstants.SIG_ALG) final String signatureAlgorithm, @QueryParam(SSOConstants.SIGNATURE) final String signature, @Context final HttpServletRequest request) throws WSSecurityException, IdpException { LogoutState logoutState = getLogoutState(request); Cookie cookie = getCookie(request); try { if (samlRequest != null) { LogoutRequest logoutRequest = logoutMessage.extractSamlLogoutRequest(RestSecurity.inflateBase64( samlRequest)); validateRedirect(relayState, signatureAlgorithm, signature, request, samlRequest, logoutRequest, logoutRequest.getIssuer() .getValue()); return handleLogoutRequest(cookie, logoutState, logoutRequest, SamlProtocol.Binding.HTTP_REDIRECT, relayState); } else if (samlResponse != null) { LogoutResponse logoutResponse = logoutMessage.extractSamlLogoutResponse(RestSecurity.inflateBase64( samlResponse)); String requestId = logoutState != null ? logoutState.getCurrentRequestId() : null; validateRedirect(relayState, signatureAlgorithm, signature, request, samlResponse, logoutResponse, logoutResponse.getIssuer() .getValue(), requestId); return handleLogoutResponse(cookie, logoutState, logoutResponse, SamlProtocol.Binding.HTTP_REDIRECT); } } catch (XMLStreamException e) { throw new IdpException("Unable to parse Saml Object.", e); } catch (ValidationException e) { throw new IdpException("Unable to validate Saml Object", e); } catch (IOException e) { throw new IdpException("Unable to deflate Saml Object", e); } throw new IdpException("Could not process logout"); } void validateRedirect(String relayState, String signatureAlgorithm, String signature, HttpServletRequest request, String samlString, SignableXMLObject logoutRequest, String issuer) throws ValidationException { validateRedirect(relayState, signatureAlgorithm, signature, request, samlString, logoutRequest, issuer, null); } void validateRedirect(String relayState, String signatureAlgorithm, String signature, HttpServletRequest request, String samlString, SignableXMLObject logoutRequest, String issuer, String requestId) throws ValidationException { if (strictSignature) { if (isEmpty(signature) || isEmpty(signatureAlgorithm) || isEmpty(issuer)) { throw new ValidationException("No signature present for AuthnRequest."); } SamlValidator.Builder validator = new SamlValidator.Builder(new SimpleSign(systemCrypto)).setRedirectParams( relayState, signature, signatureAlgorithm, samlString, serviceProviders.get(issuer) .getSigningCertificate()); if (requestId != null) { validator.setRequestId(requestId); } validator.buildAndValidate(request.getRequestURL() .toString(), SamlProtocol.Binding.HTTP_REDIRECT, logoutRequest); } } @Override @POST @Path("/logout") public Response processPostLogout(@FormParam(SAML_REQ) final String samlRequest, @FormParam(SAML_RESPONSE) final String samlResponse, @FormParam(RELAY_STATE) final String relayState, @Context final HttpServletRequest request) throws WSSecurityException, IdpException { LogoutState logoutState = getLogoutState(request); Cookie cookie = getCookie(request); try { if (samlRequest != null) { LogoutRequest logoutRequest = logoutMessage.extractSamlLogoutRequest(RestSecurity.inflateBase64( samlRequest)); validatePost(request, logoutRequest); return handleLogoutRequest(cookie, logoutState, logoutRequest, SamlProtocol.Binding.HTTP_POST, relayState); } else if (samlResponse != null) { LogoutResponse logoutResponse = logoutMessage.extractSamlLogoutResponse(RestSecurity.inflateBase64( samlResponse)); String requestId = logoutState != null ? logoutState.getCurrentRequestId() : null; validatePost(request, logoutResponse, requestId); return handleLogoutResponse(cookie, logoutState, logoutResponse, SamlProtocol.Binding.HTTP_POST); } } catch (IOException | XMLStreamException e) { throw new IdpException("Unable to inflate Saml Object", e); } catch (ValidationException e) { throw new IdpException("Unable to validate Saml Object", e); } throw new IdpException("Unable to process logout"); } void validatePost(HttpServletRequest request, SignableSAMLObject samlObject) throws ValidationException { validatePost(request, samlObject, null); } void validatePost(HttpServletRequest request, SignableSAMLObject samlObject, String requestId) throws ValidationException { if (strictSignature) { SamlValidator.Builder validator = new SamlValidator.Builder(new SimpleSign(systemCrypto)); if (requestId != null) { validator.setRequestId(requestId); } validator.buildAndValidate(request.getRequestURL() .toString(), SamlProtocol.Binding.HTTP_POST, samlObject); } } private Response handleLogoutResponse(Cookie cookie, LogoutState logoutState, LogoutResponse logoutObject, SamlProtocol.Binding incomingBinding) throws IdpException { if (logoutObject != null && logoutObject.getStatus() != null && logoutObject.getStatus() .getStatusCode() != null && !StatusCode.SUCCESS.equals(logoutObject.getStatus() .getStatusCode() .getValue())) { logoutState.setPartialLogout(true); } return continueLogout(logoutState, cookie, incomingBinding); } Response handleLogoutRequest(Cookie cookie, LogoutState logoutState, LogoutRequest logoutRequest, SamlProtocol.Binding incomingBinding, String relayState) throws IdpException { if (logoutState != null) { LOGGER.info("Received logout request and already have a logout state (in progress)"); return Response.ok("Logout already in progress") .build(); } logoutState = new LogoutState(getActiveSps(cookie.getValue())); logoutState.setOriginalIssuer(logoutRequest.getIssuer() .getValue()); logoutState.setNameId(logoutRequest.getNameID() .getValue()); logoutState.setOriginalRequestId(logoutRequest.getID()); logoutState.setInitialRelayState(relayState); logoutStates.encode(cookie.getValue(), logoutState); cookieCache.removeSamlAssertion(cookie.getValue()); return continueLogout(logoutState, cookie, incomingBinding); } private Response continueLogout(LogoutState logoutState, Cookie cookie, SamlProtocol.Binding incomingBinding) throws IdpException { if (logoutState == null) { throw new IdpException("Cannot continue a Logout that doesn't exist!"); } try { SignableSAMLObject logoutObject; String relay = ""; String entityId = ""; SamlProtocol.Type samlType; Optional<String> nextTarget = logoutState.getNextTarget(); if (nextTarget.isPresent()) { // Another target exists, log them out entityId = nextTarget.get(); if (logoutState.getOriginalIssuer() .equals(entityId)) { return continueLogout(logoutState, cookie, incomingBinding); } LogoutRequest logoutRequest = logoutMessage.buildLogoutRequest(logoutState.getNameId(), SystemBaseUrl.constructUrl("/idp/logout", true)); logoutState.setCurrentRequestId(logoutRequest.getID()); logoutObject = logoutRequest; samlType = SamlProtocol.Type.REQUEST; relay = ""; } else { // No more targets, respond to original issuer entityId = logoutState.getOriginalIssuer(); String status = logoutState.isPartialLogout() ? StatusCode.PARTIAL_LOGOUT : StatusCode.SUCCESS; logoutObject = logoutMessage.buildLogoutResponse(SystemBaseUrl.constructUrl( "/idp/logout", true), status, logoutState.getOriginalRequestId()); relay = logoutState.getInitialRelayState(); LogoutState decode = logoutStates.decode(cookie.getValue(), true); samlType = SamlProtocol.Type.RESPONSE; } LOGGER.debug("Responding to [{}] with a [{}] and relay state [{}]", entityId, samlType, relay); EntityInformation.ServiceInfo entityServiceInfo = serviceProviders.get(entityId) .getLogoutService(incomingBinding); if (entityServiceInfo == null) { LOGGER.info("Could not find entity service info for {}", entityId); return continueLogout(logoutState, cookie, incomingBinding); } switch (entityServiceInfo.getBinding()) { case HTTP_REDIRECT: return getSamlRedirectResponse(logoutObject, entityServiceInfo.getUrl(), relay, samlType); case HTTP_POST: return getSamlPostResponse(logoutObject, entityServiceInfo.getUrl(), relay, samlType); default: LOGGER.debug("No supported binding available for SP [{}].", entityId); logoutState.setPartialLogout(true); return continueLogout(logoutState, cookie, incomingBinding); } } catch (WSSecurityException | SimpleSign.SignatureException | IOException e) { LOGGER.debug("Error while processing logout", e); } throw new IdpException("Server error while processing logout"); } public Set<String> getActiveSps(String cacheId) { return cookieCache.getActiveSpSet(cacheId); } private Response getSamlRedirectResponse(XMLObject samlResponse, String targetUrl, String relayState, SamlProtocol.Type samlType) throws IOException, SimpleSign.SignatureException, WSSecurityException { LOGGER.debug("Signing SAML response for redirect."); Document doc = DOMUtils.createDocument(); doc.appendChild(doc.createElement("root")); String encodedResponse = URLEncoder.encode(RestSecurity.deflateAndBase64Encode(DOM2Writer.nodeToString( OpenSAMLUtil.toDom(samlResponse, doc, false))), "UTF-8"); String requestToSign = String.format("%s=%s&RelayState=%s", samlType.getKey(), encodedResponse, relayState); UriBuilder uriBuilder = UriBuilder.fromUri(targetUrl); uriBuilder.queryParam(samlType.getKey(), encodedResponse); uriBuilder.queryParam(SSOConstants.RELAY_STATE, relayState == null ? "" : relayState); new SimpleSign(systemCrypto).signUriString(requestToSign, uriBuilder); LOGGER.debug("Signing successful."); return Response.ok(HtmlResponseTemplate.getRedirectPage(uriBuilder.build() .toString())) .build(); } private Response getSamlPostResponse(SignableSAMLObject samlObject, String targetUrl, String relayState, SamlProtocol.Type samlType) throws SimpleSign.SignatureException, WSSecurityException { Document doc = DOMUtils.createDocument(); doc.appendChild(doc.createElement("root")); LOGGER.debug("Signing SAML POST Response."); new SimpleSign(systemCrypto).signSamlObject(samlObject); LOGGER.debug("Converting SAML Response to DOM"); String assertionResponse = DOM2Writer.nodeToString(OpenSAMLUtil.toDom(samlObject, doc)); String encodedSamlResponse = Base64.getEncoder() .encodeToString(assertionResponse.getBytes(StandardCharsets.UTF_8)); return Response.ok(HtmlResponseTemplate.getPostPage(targetUrl, samlType, encodedSamlResponse, relayState)) .build(); } public void setSecurityManager(SecurityManager securityManager) { this.securityManager = securityManager; } public void setTokenFactory(PKIAuthenticationTokenFactory tokenFactory) { this.tokenFactory = tokenFactory; } public void setSpMetadata(List<String> spMetadata) { parseServiceProviderMetadata(spMetadata); } public void setStrictSignature(Boolean strictSignature) { this.strictSignature = strictSignature; } public void setExpirationTime(int expirationTime) { this.cookieCache.setExpirationTime(expirationTime); } public void setLogoutMessage(LogoutMessage logoutMessage) { this.logoutMessage = logoutMessage; } public void setLogoutStates(RelayStates<LogoutState> logoutStates) { this.logoutStates = logoutStates; } public RelayStates<LogoutState> getLogoutStates() { return this.logoutStates; } public void setGuestAccess(boolean guestAccess) { this.guestAccess = guestAccess; } private static class AuthObj { String method; String username; String password; SecurityToken assertion; } }