Return-Path: X-Original-To: apmail-cxf-commits-archive@www.apache.org Delivered-To: apmail-cxf-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 79CA010EF6 for ; Thu, 27 Nov 2014 14:32:20 +0000 (UTC) Received: (qmail 55283 invoked by uid 500); 27 Nov 2014 14:32:20 -0000 Delivered-To: apmail-cxf-commits-archive@cxf.apache.org Received: (qmail 55222 invoked by uid 500); 27 Nov 2014 14:32:20 -0000 Mailing-List: contact commits-help@cxf.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@cxf.apache.org Delivered-To: mailing list commits@cxf.apache.org Received: (qmail 55212 invoked by uid 99); 27 Nov 2014 14:32:20 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 27 Nov 2014 14:32:20 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id D2C919497DC; Thu, 27 Nov 2014 14:32:19 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: coheigea@apache.org To: commits@cxf.apache.org Message-Id: <0838297f81e9467381a1f18e209635c4@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: cxf-fediz git commit: [FEDIZ-73] - Added initial SAML SSO handler for the IdP Date: Thu, 27 Nov 2014 14:32:19 +0000 (UTC) Repository: cxf-fediz Updated Branches: refs/heads/master 0630d4e09 -> 31ffb897e [FEDIZ-73] - Added initial SAML SSO handler for the IdP Project: http://git-wip-us.apache.org/repos/asf/cxf-fediz/repo Commit: http://git-wip-us.apache.org/repos/asf/cxf-fediz/commit/31ffb897 Tree: http://git-wip-us.apache.org/repos/asf/cxf-fediz/tree/31ffb897 Diff: http://git-wip-us.apache.org/repos/asf/cxf-fediz/diff/31ffb897 Branch: refs/heads/master Commit: 31ffb897e5355eb5988305467175bdac9bb16278 Parents: 0630d4e Author: Colm O hEigeartaigh Authored: Thu Nov 27 14:31:53 2014 +0000 Committer: Colm O hEigeartaigh Committed: Thu Nov 27 14:31:53 2014 +0000 ---------------------------------------------------------------------- .../TrustedIdpSAMLProtocolHandler.java | 417 +++++++++++++++++++ 1 file changed, 417 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cxf-fediz/blob/31ffb897/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/protocols/TrustedIdpSAMLProtocolHandler.java ---------------------------------------------------------------------- diff --git a/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/protocols/TrustedIdpSAMLProtocolHandler.java b/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/protocols/TrustedIdpSAMLProtocolHandler.java new file mode 100644 index 0000000..e0fe66c --- /dev/null +++ b/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/protocols/TrustedIdpSAMLProtocolHandler.java @@ -0,0 +1,417 @@ +/** + * 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.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Date; +import java.util.UUID; +import java.util.zip.DataFormatException; + +import javax.servlet.http.HttpServletRequest; +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.service.idp.domain.Idp; +import org.apache.cxf.fediz.service.idp.domain.TrustedIdp; +import org.apache.cxf.fediz.service.idp.spi.TrustedIdpProtocolHandler; +import org.apache.cxf.fediz.service.idp.util.WebUtils; +import org.apache.cxf.helpers.DOMUtils; +import org.apache.cxf.jaxrs.utils.ExceptionUtils; +import org.apache.cxf.jaxrs.utils.HttpUtils; +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.SSOConstants; +import org.apache.cxf.staxutils.StaxUtils; +import org.apache.cxf.ws.security.tokenstore.SecurityToken; +import org.apache.wss4j.common.ext.WSSecurityException; +import org.apache.wss4j.common.saml.OpenSAMLUtil; +import org.apache.wss4j.common.saml.SamlAssertionWrapper; +import org.apache.wss4j.common.util.DOM2Writer; +import org.apache.xml.security.stax.impl.util.IDGenerator; +import org.opensaml.saml2.core.AuthnRequest; +import org.opensaml.xml.XMLObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.webflow.execution.RequestContext; + +@Component +public class TrustedIdpSAMLProtocolHandler implements TrustedIdpProtocolHandler { + + 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 AuthnRequestBuilder authnRequestBuilder = new DefaultAuthnRequestBuilder(); + private long stateTimeToLive = SSOConstants.DEFAULT_STATE_TIME; + + static { + OpenSAMLUtil.initSamlEngine(); + } + + @Override + public boolean canHandleRequest(HttpServletRequest request) { + // TODO Auto-generated method stub + return false; + } + + @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() + ); + // if (isSignRequest()) { + // authnRequest.setDestination(idpServiceAddress); + //} + Element authnRequestElement = OpenSAMLUtil.toDom(authnRequest, doc); + String authnRequestEncoded = encodeAuthnRequest(authnRequestElement); + + String relayState = URLEncoder.encode(UUID.randomUUID().toString(), "UTF-8"); + + String urlEncodedRequest = URLEncoder.encode(authnRequestEncoded, "UTF-8"); + + UriBuilder ub = UriBuilder.fromUri(trustedIdp.getUrl()); + + ub.queryParam(SSOConstants.SAML_REQUEST, urlEncodedRequest); + ub.queryParam(SSOConstants.RELAY_STATE, relayState); + //if (isSignRequest()) { + // signRequest(urlEncodedRequest, info.getRelayState(), ub); + //} + // TODO String contextCookie = createCookie(SSOConstants.RELAY_STATE, + // relayState, + // idp.getIdpUrl().getPath(), + // null); + + /*context.abortWith(Response.seeOther(ub.build()) + .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store") + .header("Pragma", "no-cache") + .header(HttpHeaders.SET_COOKIE, contextCookie) + .build());*/ + + 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"); + } + } + + + protected String encodeAuthnRequest(Element authnRequest) throws IOException { + String requestMessage = DOM2Writer.nodeToString(authnRequest); + + DeflateEncoderDecoder encoder = new DeflateEncoderDecoder(); + byte[] deflatedBytes = encoder.deflateToken(requestMessage.getBytes("UTF-8")); + + return Base64Utility.encode(deflatedBytes); + } + + protected String createCookie(String name, + String value, + String path, + String domain) { + + String contextCookie = name + "=" + value; + // Setting a specific path restricts the browsers + // to return a cookie only to the web applications + // listening on that specific context path + if (path != null) { + contextCookie += ";Path=" + path; + } + + // Setting a specific domain further restricts the browsers + // to return a cookie only to the web applications + // listening on the specific context path within a particular domain + if (domain != null) { + contextCookie += ";Domain=" + domain; + } + + // Keep the cookie across the browser restarts until it actually expires. + // Note that the Expires property has been deprecated but apparently is + // supported better than 'max-age' property by different browsers + // (Firefox, IE, etc) + Date expiresDate = new Date(System.currentTimeMillis() + stateTimeToLive); + String cookieExpires = HttpUtils.getHttpDateFormat().format(expiresDate); + contextCookie += ";Expires=" + cookieExpires; + //TODO: Consider adding an 'HttpOnly' attribute + + return contextCookie; + } + + + @Override + public SecurityToken mapSignInResponse(RequestContext context, Idp idp, TrustedIdp trustedIdp) { + + try { + + String relayState = (String) WebUtils.getAttributeFromFlowScope(context, + SSOConstants.RELAY_STATE); + // TODO Validate RelayState + System.out.println("RS: " + relayState); + + String encodedSAMLResponse = (String) WebUtils.getAttributeFromFlowScope(context, + SSOConstants.SAML_RESPONSE); + org.opensaml.saml2.core.Response samlResponse = + readSAMLResponse(false, encodedSAMLResponse); + + // Validate the Response + /* + * TODOvalidateSamlResponseProtocol(samlResponse); + SSOValidatorResponse validatorResponse = + validateSamlSSOResponse(false, samlResponse, requestState); + + String assertion = validatorResponse.getAssertion(); + SamlAssertionWrapper wrapper = new SamlAssertionWrapper(assertion); + */ + SamlAssertionWrapper wrapper = + new SamlAssertionWrapper(samlResponse.getAssertions().get(0)); + + // 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); + // new SecurityToken(id, new Date(), validatorResponse.getSessionNotOnOrAfter()); + // TODO new Date() above incorrect + + idpToken.setToken(wrapper.toDOM(DOMUtils.newDocument())); + // LOG.info("[IDP_TOKEN={}] for user '{}' created from [RP_TOKEN={}] issued by home realm [{}/{}]", + // id, wfResp.getUsername(), wfResp.getUniqueTokenId(), whr, wfResp.getIssuer()); + //.debug("Created date={}", wfResp.getTokenCreated()); + //LOG.debug("Expired date={}", wfResp.getTokenExpires()); + //if (LOG.isDebugEnabled()) { + // LOG.debug("Validated 'wresult' : " + // + System.getProperty("line.separator") + wresult); + //} + return idpToken; + } catch (IllegalStateException ex) { + throw ex; + } catch (Exception ex) { + LOG.warn("Unexpected exception occured", ex); + throw new IllegalStateException("Unexpected exception occured: " + ex.getMessage()); + } + } + + private org.opensaml.saml2.core.Response readSAMLResponse( + boolean postBinding, String samlResponse + ) { + if (StringUtils.isEmpty(samlResponse)) { + throw ExceptionUtils.toBadRequestException(null, null); + } + + String samlResponseDecoded = samlResponse; + /* + // URL Decoding only applies for the re-direct binding + if (!postBinding) { + try { + samlResponseDecoded = URLDecoder.decode(samlResponse, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw ExceptionUtils.toBadRequestException(null, null); + } + } + */ + InputStream tokenStream = null; + // (isSupportBase64Encoding()) { TODO + try { + byte[] deflatedToken = Base64Utility.decode(samlResponseDecoded); + tokenStream = !postBinding //&& isSupportDeflateEncoding() + ? new DeflateEncoderDecoder().inflateToken(deflatedToken) + : new ByteArrayInputStream(deflatedToken); + } catch (Base64Exception ex) { + throw ExceptionUtils.toBadRequestException(ex, null); + } catch (DataFormatException ex) { + throw ExceptionUtils.toBadRequestException(ex, null); + } + /*} else { TODO + try { + tokenStream = new ByteArrayInputStream(samlResponseDecoded.getBytes("UTF-8")); + } catch (UnsupportedEncodingException ex) { + throw ExceptionUtils.toBadRequestException(ex, null); + } + }*/ + + Document responseDoc = null; + try { + responseDoc = StaxUtils.read(new InputStreamReader(tokenStream, "UTF-8")); + } catch (Exception ex) { + throw new WebApplicationException(400); + } + + LOG.debug("Received response: " + DOM2Writer.nodeToString(responseDoc.getDocumentElement())); + + XMLObject responseObject = null; + try { + responseObject = OpenSAMLUtil.fromDom(responseDoc.getDocumentElement()); + } catch (WSSecurityException ex) { + throw ExceptionUtils.toBadRequestException(ex, null); + } + if (!(responseObject instanceof org.opensaml.saml2.core.Response)) { + throw ExceptionUtils.toBadRequestException(null, null); + } + return (org.opensaml.saml2.core.Response)responseObject; + } + + /** + * Validate the received SAML Response as per the protocol + protected void validateSamlResponseProtocol( + org.opensaml.saml2.core.Response samlResponse + ) { + try { + SAMLProtocolResponseValidator protocolValidator = new SAMLProtocolResponseValidator(); + protocolValidator.setKeyInfoMustBeAvailable(true); // TODO + protocolValidator.validateSamlResponse(samlResponse, getSignatureCrypto(), getCallbackHandler()); + } catch (WSSecurityException ex) { + LOG.debug(ex.getMessage(), ex); + throw ExceptionUtils.toBadRequestException(null, null); + } + } + */ + /** + * Validate the received SAML Response as per the Web SSO profile + protected SSOValidatorResponse validateSamlSSOResponse( + boolean postBinding, + org.opensaml.saml2.core.Response samlResponse + ) { + try { + SAMLSSOResponseValidator ssoResponseValidator = new SAMLSSOResponseValidator(); + ssoResponseValidator.setAssertionConsumerURL( + messageContext.getUriInfo().getAbsolutePath().toString()); + + ssoResponseValidator.setClientAddress( + messageContext.getHttpServletRequest().getRemoteAddr()); + + ssoResponseValidator.setIssuerIDP(requestState.getIdpServiceAddress()); + ssoResponseValidator.setRequestId(requestState.getSamlRequestId()); + ssoResponseValidator.setSpIdentifier(requestState.getIssuerId()); + ssoResponseValidator.setEnforceAssertionsSigned(true); // TODO + ssoResponseValidator.setEnforceKnownIssuer(enforceKnownIssuer); + // ssoResponseValidator.setReplayCache(getReplayCache()); + + return ssoResponseValidator.validateSamlResponse(samlResponse, postBinding); + } catch (WSSecurityException ex) { + LOG.debug(ex.getMessage(), ex); + throw ExceptionUtils.toBadRequestException(ex, null); + } + } + */ + +/* + private FedizContext getFedizContext(Idp idpConfig, + TrustedIdp trustedIdpConfig) throws ProcessingException { + + ContextConfig config = new ContextConfig(); + + config.setName("whatever"); + + // Configure certificate store + String certificate = trustedIdpConfig.getCertificate(); + boolean isCertificateLocation = !certificate.startsWith("-----BEGIN CERTIFICATE"); + if (isCertificateLocation) { + CertificateStores certStores = new CertificateStores(); + TrustManagersType tm0 = new TrustManagersType(); + KeyStoreType ks0 = new KeyStoreType(); + ks0.setType("PEM"); + // ks0.setType("JKS"); + // ks0.setPassword("changeit"); + ks0.setFile(trustedIdpConfig.getCertificate()); + tm0.setKeyStore(ks0); + certStores.getTrustManager().add(tm0); + config.setCertificateStores(certStores); + } + + // Configure trusted IDP + TrustedIssuers trustedIssuers = new TrustedIssuers(); + TrustedIssuerType ti0 = new TrustedIssuerType(); + ti0.setCertificateValidation(ValidationType.PEER_TRUST); + ti0.setName(trustedIdpConfig.getName()); + // ti0.setSubject(".*CN=www.sts.com.*"); + trustedIssuers.getIssuer().add(ti0); + config.setTrustedIssuers(trustedIssuers); + + FederationProtocolType protocol = new FederationProtocolType(); + config.setProtocol(protocol); + + AudienceUris audienceUris = new AudienceUris(); + audienceUris.getAudienceItem().add(idpConfig.getRealm()); + config.setAudienceUris(audienceUris); + + FedizContext fedContext = new FedizContext(config); + if (!isCertificateLocation) { + CertificateStore cs = null; + + X509Certificate cert; + try { + cert = parseCertificate(trustedIdpConfig.getCertificate()); + } catch (Exception ex) { + LOG.error("Failed to parse trusted certificate", ex); + throw new ProcessingException("Failed to parse trusted certificate"); + } + cs = new CertificateStore(Collections.singletonList(cert).toArray(new X509Certificate[0])); + + TrustManager tm = new TrustManager(cs); + fedContext.getCertificateStores().add(tm); + } + + fedContext.init(); + return fedContext; + } + + private X509Certificate parseCertificate(String certificate) + throws CertificateException, Base64DecodingException { + + //before decoding we need to get rod off the prefix and suffix + byte [] decoded = Base64.decode(certificate.replaceAll("-----BEGIN CERTIFICATE-----", ""). + replaceAll("-----END CERTIFICATE-----", "")); + + return (X509Certificate)CertificateFactory.getInstance("X.509"). + generateCertificate(new ByteArrayInputStream(decoded)); + } +*/ + +}