blob: f42e40034f8249165fb13b49f74361fd9290f002 [file] [log] [blame]
/**
* 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.cxf.fediz.service.idp.protocols;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.zip.DataFormatException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.UriBuilder;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.apache.cxf.common.util.Base64Exception;
import org.apache.cxf.common.util.Base64Utility;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.fediz.core.util.CertsUtils;
import org.apache.cxf.fediz.core.util.DOMUtils;
import org.apache.cxf.fediz.service.idp.IdpConstants;
import org.apache.cxf.fediz.service.idp.domain.Idp;
import org.apache.cxf.fediz.service.idp.domain.TrustedIdp;
import org.apache.cxf.fediz.service.idp.util.WebUtils;
import org.apache.cxf.jaxrs.utils.ExceptionUtils;
import org.apache.cxf.rs.security.saml.DeflateEncoderDecoder;
import org.apache.cxf.rs.security.saml.sso.AuthnRequestBuilder;
import org.apache.cxf.rs.security.saml.sso.DefaultAuthnRequestBuilder;
import org.apache.cxf.rs.security.saml.sso.EHCacheTokenReplayCache;
import org.apache.cxf.rs.security.saml.sso.SAMLProtocolResponseValidator;
import org.apache.cxf.rs.security.saml.sso.SAMLSSOResponseValidator;
import org.apache.cxf.rs.security.saml.sso.SSOConstants;
import org.apache.cxf.rs.security.saml.sso.SSOValidatorResponse;
import org.apache.cxf.rs.security.saml.sso.TokenReplayCache;
import org.apache.cxf.staxutils.StaxUtils;
import org.apache.cxf.ws.security.tokenstore.SecurityToken;
import org.apache.wss4j.common.crypto.Crypto;
import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.common.saml.OpenSAMLUtil;
import org.apache.wss4j.common.util.DOM2Writer;
import org.apache.xml.security.stax.impl.util.IDGenerator;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.webflow.execution.RequestContext;
@Component
public class TrustedIdpSAMLProtocolHandler extends AbstractTrustedIdpProtocolHandler {
/**
* Whether to sign the request or not. The default is "true".
*/
public static final String SIGN_REQUEST = "sign.request";
/**
* Whether to require a KeyInfo or not when processing a (signed) Response. The default is "true".
*/
public static final String REQUIRE_KEYINFO = "require.keyinfo";
/**
* Whether the assertions contained in the Response must be signed or not (if the response itself
* is not signed). The default is "true".
*/
public static final String REQUIRE_SIGNED_ASSERTIONS = "require.signed.assertions";
/**
* Whether we have to "know" the issuer of the SAML Response or not. The default is "true".
*/
public static final String REQUIRE_KNOWN_ISSUER = "require.known.issuer";
/**
* Whether we BASE-64 decode the response or not. The default is "true".
*/
public static final String SUPPORT_BASE64_ENCODING = "support.base64.encoding";
/**
* Whether we support Deflate encoding or not. The default is "false".
*/
public static final String SUPPORT_DEFLATE_ENCODING = "support.deflate.encoding";
public static final String PROTOCOL = "urn:oasis:names:tc:SAML:2.0:profiles:SSO:browser";
private static final Logger LOG = LoggerFactory.getLogger(TrustedIdpSAMLProtocolHandler.class);
private static final String SAML_SSO_REQUEST_ID = "saml-sso-request-id";
private AuthnRequestBuilder authnRequestBuilder = new DefaultAuthnRequestBuilder();
private TokenReplayCache<String> replayCache;
static {
OpenSAMLUtil.initSamlEngine();
}
@Override
public String getProtocol() {
return PROTOCOL;
}
@Override
public URL mapSignInRequest(RequestContext context, Idp idp, TrustedIdp trustedIdp) {
try {
Document doc = DOMUtils.createDocument();
doc.appendChild(doc.createElement("root"));
// Create the AuthnRequest
AuthnRequest authnRequest =
authnRequestBuilder.createAuthnRequest(
null, idp.getRealm(), idp.getIdpUrl().toString()
);
boolean signRequest = isBooleanPropertyConfigured(trustedIdp, SIGN_REQUEST, true);
if (signRequest) {
authnRequest.setDestination(trustedIdp.getUrl());
}
Element authnRequestElement = OpenSAMLUtil.toDom(authnRequest, doc);
String authnRequestEncoded = encodeAuthnRequest(authnRequestElement);
String urlEncodedRequest = URLEncoder.encode(authnRequestEncoded, "UTF-8");
UriBuilder ub = UriBuilder.fromUri(trustedIdp.getUrl());
ub.queryParam(SSOConstants.SAML_REQUEST, urlEncodedRequest);
String wctx = context.getFlowScope().getString(IdpConstants.TRUSTED_IDP_CONTEXT);
ub.queryParam(SSOConstants.RELAY_STATE, wctx);
if (signRequest) {
signRequest(urlEncodedRequest, wctx, idp, ub);
}
// Store the Request ID
String authnRequestId = authnRequest.getID();
WebUtils.putAttributeInExternalContext(context, SAML_SSO_REQUEST_ID, authnRequestId);
HttpServletResponse response = WebUtils.getHttpServletResponse(context);
response.addHeader("Cache-Control", "no-cache, no-store");
response.addHeader("Pragma", "no-cache");
return ub.build().toURL();
} catch (MalformedURLException ex) {
LOG.error("Invalid Redirect URL for Trusted Idp", ex);
throw new IllegalStateException("Invalid Redirect URL for Trusted Idp");
} catch (UnsupportedEncodingException ex) {
LOG.error("Invalid Redirect URL for Trusted Idp", ex);
throw new IllegalStateException("Invalid Redirect URL for Trusted Idp");
} catch (Exception ex) {
LOG.error("Invalid Redirect URL for Trusted Idp", ex);
throw new IllegalStateException("Invalid Redirect URL for Trusted Idp");
}
}
@Override
public SecurityToken mapSignInResponse(RequestContext context, Idp idp, TrustedIdp trustedIdp) {
try {
String encodedSAMLResponse = (String) WebUtils.getAttributeFromFlowScope(context,
SSOConstants.SAML_RESPONSE);
// Read the response + convert to an OpenSAML Response Object
org.opensaml.saml.saml2.core.Response samlResponse =
readSAMLResponse(encodedSAMLResponse, trustedIdp);
Crypto crypto = CertsUtils.getCryptoFromCertificate(trustedIdp.getCertificate());
validateSamlResponseProtocol(samlResponse, crypto, trustedIdp);
// Validate the Response
SSOValidatorResponse validatorResponse =
validateSamlSSOResponse(samlResponse, idp, trustedIdp, context);
// Create new Security token with new id.
// Parameters for freshness computation are copied from original IDP_TOKEN
String id = IDGenerator.generateID("_");
SecurityToken idpToken =
new SecurityToken(id, validatorResponse.getCreated(), validatorResponse.getSessionNotOnOrAfter());
idpToken.setToken(validatorResponse.getAssertionElement());
String whr = (String) WebUtils.getAttributeFromFlowScope(context, IdpConstants.HOME_REALM);
LOG.info("[IDP_TOKEN={}] created from [RP_TOKEN={}] issued by home realm [{}]",
id, validatorResponse.getResponseId(), whr);
LOG.debug("Created date={}", validatorResponse.getCreated());
LOG.debug("Expired date={}", validatorResponse.getSessionNotOnOrAfter());
LOG.debug("Validated: {}{}", System.getProperty("line.separator"), validatorResponse.getAssertion());
return idpToken;
} catch (BadRequestException ex) {
throw ex;
} catch (Exception ex) {
LOG.warn("Unexpected exception occured", ex);
throw new IllegalStateException("Unexpected exception occured: " + ex.getMessage());
}
}
private String encodeAuthnRequest(Element authnRequest) throws IOException {
String requestMessage = DOM2Writer.nodeToString(authnRequest);
LOG.debug(requestMessage);
DeflateEncoderDecoder encoder = new DeflateEncoderDecoder();
byte[] deflatedBytes = encoder.deflateToken(requestMessage.getBytes(StandardCharsets.UTF_8));
return Base64Utility.encode(deflatedBytes);
}
/**
* Sign a request according to the redirect binding spec for Web SSO
*/
private void signRequest(
String authnRequest,
String relayState,
Idp config,
UriBuilder ub
) throws Exception {
Crypto crypto = CertsUtils.getCryptoFromCertificate(config.getCertificate());
if (crypto == null) {
LOG.error("No crypto instance of properties file configured for signature");
throw new IllegalStateException("Invalid IdP configuration");
}
String alias = crypto.getDefaultX509Identifier();
X509Certificate cert = CertsUtils.getX509CertificateFromCrypto(crypto, alias);
if (cert == null) {
LOG.error("No cert was found to sign the request using alias: " + alias);
throw new IllegalStateException("Invalid IdP configuration");
}
String sigAlgo = SSOConstants.RSA_SHA1;
String pubKeyAlgo = cert.getPublicKey().getAlgorithm();
String jceSigAlgo = "SHA1withRSA";
LOG.debug("automatic sig algo detection: " + pubKeyAlgo);
if ("DSA".equalsIgnoreCase(pubKeyAlgo)) {
sigAlgo = SSOConstants.DSA_SHA1;
jceSigAlgo = "SHA1withDSA";
}
LOG.debug("Using Signature algorithm " + sigAlgo);
ub.queryParam(SSOConstants.SIG_ALG, URLEncoder.encode(sigAlgo, "UTF-8"));
// Get the password
String password = config.getCertificatePassword();
// Get the private key
PrivateKey privateKey = crypto.getPrivateKey(alias, password);
// Sign the request
Signature signature = Signature.getInstance(jceSigAlgo);
signature.initSign(privateKey);
String requestToSign =
SSOConstants.SAML_REQUEST + "=" + authnRequest + "&"
+ SSOConstants.RELAY_STATE + "=" + relayState + "&"
+ SSOConstants.SIG_ALG + "=" + URLEncoder.encode(sigAlgo, "UTF-8");
signature.update(requestToSign.getBytes(StandardCharsets.UTF_8));
byte[] signBytes = signature.sign();
String encodedSignature = Base64.getEncoder().encodeToString(signBytes);
ub.queryParam(SSOConstants.SIGNATURE, URLEncoder.encode(encodedSignature, "UTF-8"));
}
private org.opensaml.saml.saml2.core.Response readSAMLResponse(String samlResponse, TrustedIdp trustedIdp) {
if (StringUtils.isEmpty(samlResponse)) {
throw ExceptionUtils.toBadRequestException(null, null);
}
String samlResponseDecoded = samlResponse;
final Reader reader;
if (isBooleanPropertyConfigured(trustedIdp, SUPPORT_BASE64_ENCODING, true)) {
try {
byte[] deflatedToken = Base64Utility.decode(samlResponseDecoded);
final InputStream tokenStream = isBooleanPropertyConfigured(trustedIdp, SUPPORT_DEFLATE_ENCODING, false)
? new DeflateEncoderDecoder().inflateToken(deflatedToken)
: new ByteArrayInputStream(deflatedToken);
reader = new InputStreamReader(tokenStream, StandardCharsets.UTF_8);
} catch (Base64Exception | DataFormatException ex) {
throw ExceptionUtils.toBadRequestException(ex, null);
}
} else {
reader = new StringReader(samlResponseDecoded);
}
final Document responseDoc;
try {
responseDoc = StaxUtils.read(reader);
} catch (Exception ex) {
throw new WebApplicationException(400);
}
LOG.debug("Received response: " + DOM2Writer.nodeToString(responseDoc.getDocumentElement()));
final XMLObject responseObject;
try {
responseObject = OpenSAMLUtil.fromDom(responseDoc.getDocumentElement());
} catch (WSSecurityException ex) {
throw ExceptionUtils.toBadRequestException(ex, null);
}
if (!(responseObject instanceof org.opensaml.saml.saml2.core.Response)) {
throw ExceptionUtils.toBadRequestException(null, null);
}
return (org.opensaml.saml.saml2.core.Response)responseObject;
}
/**
* Validate the received SAML Response as per the protocol
*/
private void validateSamlResponseProtocol(
org.opensaml.saml.saml2.core.Response samlResponse, Crypto crypto, TrustedIdp trustedIdp
) {
try {
SAMLProtocolResponseValidator protocolValidator = new SAMLProtocolResponseValidator();
protocolValidator.setKeyInfoMustBeAvailable(
isBooleanPropertyConfigured(trustedIdp, REQUIRE_KEYINFO, true));
protocolValidator.validateSamlResponse(samlResponse, crypto, null);
} catch (WSSecurityException ex) {
LOG.debug(ex.getMessage(), ex);
throw ExceptionUtils.toBadRequestException(null, null);
}
}
/**
* Validate the received SAML Response as per the Web SSO profile
*/
private SSOValidatorResponse validateSamlSSOResponse(
org.opensaml.saml.saml2.core.Response samlResponse,
Idp idp,
TrustedIdp trustedIdp,
RequestContext requestContext
) {
try {
SAMLSSOResponseValidator ssoResponseValidator = new SAMLSSOResponseValidator();
ssoResponseValidator.setAssertionConsumerURL(idp.getIdpUrl().toString());
HttpServletRequest servletRequest = WebUtils.getHttpServletRequest(requestContext);
ssoResponseValidator.setClientAddress(servletRequest.getRemoteAddr());
String issuer = trustedIdp.getIssuer();
if (issuer == null || issuer.isEmpty()) {
LOG.debug("Issuer name is not defined in trusted 3rd party configuration. "
+ "Using URL instead for issuer validation");
issuer = trustedIdp.getUrl();
}
LOG.debug("Using {} for issuer validation", issuer);
ssoResponseValidator.setIssuerIDP(issuer);
// Get the stored request ID
String requestId =
(String)WebUtils.getAttributeFromExternalContext(requestContext, SAML_SSO_REQUEST_ID);
ssoResponseValidator.setRequestId(requestId);
ssoResponseValidator.setSpIdentifier(idp.getRealm());
ssoResponseValidator.setEnforceAssertionsSigned(
isBooleanPropertyConfigured(trustedIdp, REQUIRE_SIGNED_ASSERTIONS, true));
ssoResponseValidator.setEnforceKnownIssuer(
isBooleanPropertyConfigured(trustedIdp, REQUIRE_KNOWN_ISSUER, true));
HttpServletRequest httpServletRequest = WebUtils.getHttpServletRequest(requestContext);
boolean post = "POST".equals(httpServletRequest.getMethod());
if (post) {
ssoResponseValidator.setReplayCache(getReplayCache());
}
return ssoResponseValidator.validateSamlResponse(samlResponse, post);
} catch (Exception ex) {
LOG.debug(ex.getMessage(), ex);
throw ExceptionUtils.toBadRequestException(ex, null);
}
}
public void setReplayCache(TokenReplayCache<String> replayCache) {
this.replayCache = replayCache;
}
public TokenReplayCache<String> getReplayCache() throws IllegalAccessException, ReflectiveOperationException {
if (replayCache == null) {
replayCache = new EHCacheTokenReplayCache();
}
return replayCache;
}
}