/* * Copyright 2001-2005 Internet2 * * Licensed 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 gov.nih.nci.cagrid.opensaml.provider; import gov.nih.nci.cagrid.opensaml.ExpiredAssertionException; import gov.nih.nci.cagrid.opensaml.FatalProfileException; import gov.nih.nci.cagrid.opensaml.ReplayCache; import gov.nih.nci.cagrid.opensaml.ReplayedAssertionException; import gov.nih.nci.cagrid.opensaml.SAMLAssertion; import gov.nih.nci.cagrid.opensaml.SAMLAuthenticationStatement; import gov.nih.nci.cagrid.opensaml.SAMLBrowserProfile; import gov.nih.nci.cagrid.opensaml.SAMLConfig; import gov.nih.nci.cagrid.opensaml.SAMLException; import gov.nih.nci.cagrid.opensaml.SAMLRequest; import gov.nih.nci.cagrid.opensaml.SAMLResponse; import gov.nih.nci.cagrid.opensaml.SAMLStatement; import gov.nih.nci.cagrid.opensaml.SAMLSubject; import gov.nih.nci.cagrid.opensaml.UnsupportedProfileException; import gov.nih.nci.cagrid.opensaml.XML; import gov.nih.nci.cagrid.opensaml.artifact.Artifact; import gov.nih.nci.cagrid.opensaml.artifact.ArtifactParseException; import gov.nih.nci.cagrid.opensaml.artifact.ArtifactParserException; import gov.nih.nci.cagrid.opensaml.artifact.SAMLArtifact; import java.io.ByteArrayInputStream; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import javax.servlet.http.HttpServletRequest; import org.apache.commons.codec.binary.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Element; /** * Default implementation of the SAML 1.x browser profiles * * @author Scott Cantor * @created February 3, 2005 */ public class BrowserProfileProvider implements SAMLBrowserProfile { private static Logger log = LoggerFactory.getLogger(BrowserProfileProvider.class); private static int skew = 1000 * SAMLConfig.instance().getIntProperty("gov.nih.nci.cagrid.opensaml.clock-skew"); public BrowserProfileProvider(Element e) { } /** * @see gov.nih.gov.nih.nci.cagrid.opensaml.SAMLBrowserProfile#receive(javax.servlet.http.HttpServletRequest) */ public BrowserProfileRequest receive(HttpServletRequest requestContext) throws UnsupportedProfileException { BrowserProfileRequest bpr = new BrowserProfileRequest(); bpr.SAMLResponse = requestContext.getParameter("SAMLResponse"); if (bpr.SAMLResponse == null) { bpr.SAMLArt = requestContext.getParameterValues("SAMLart"); if (bpr.SAMLArt == null || bpr.SAMLArt.length == 0) throw new UnsupportedProfileException("no SAMLResponse or SAMLart parameters supplied in HTTP request"); } bpr.TARGET = requestContext.getParameter("TARGET"); return bpr; } /** * @see gov.nih.gov.nih.nci.cagrid.opensaml.SAMLBrowserProfile#receive(java.lang.StringBuffer, gov.nih.gov.nih.nci.cagrid.opensaml.SAMLBrowserProfile.BrowserProfileRequest, java.lang.String, gov.nih.gov.nih.nci.cagrid.opensaml.ReplayCache, gov.nih.gov.nih.nci.cagrid.opensaml.SAMLBrowserProfile.ArtifactMapper, int) */ public BrowserProfileResponse receive( StringBuffer issuer, BrowserProfileRequest requestContext, String recipient, ReplayCache replayCache, ArtifactMapper artifactMapper, int minorVersion ) throws SAMLException { long now = System.currentTimeMillis(); // Java handles the parameter parsing, so we just check the results. SAMLResponse response = null; SAMLAssertion assertion = null; SAMLAuthenticationStatement authnStatement = null; boolean wasPost = true; if (requestContext.SAMLResponse != null) { response = new SAMLResponse(new ByteArrayInputStream(Base64.decodeBase64(requestContext.SAMLResponse.getBytes())),minorVersion); if (log.isDebugEnabled()) log.debug("decoded SAML response:\n" + response.toString()); try { // Check security bits in the outer wrapper (Recipient and IssueInstant). if (XML.isEmpty(recipient) || !XML.safeCompare(recipient,response.getRecipient())) throw new FatalProfileException("detected recipient mismatch in POST profile response"); if (response.getIssueInstant().getTime() < now-(2*skew)) throw new ExpiredAssertionException("detected expired POST profile response"); // We don't verify the signature, but at least check for one. if (!response.isSigned()) throw new FatalProfileException("detected unsigned POST profile response"); } catch (SAMLException e) { if (issuer != null) { Iterator assertions=response.getAssertions(); if (assertions.hasNext()) issuer.append(((SAMLAssertion)assertions.next()).getIssuer()); } throw e; } } else { // Check for artifacts. if (requestContext.SAMLArt == null || requestContext.SAMLArt.length == 0) throw new FatalProfileException("no SAMLResponse or SAMLart parameters supplied"); if (artifactMapper == null) { throw new FatalProfileException("support of artifact profile requires ArtifactMapper interface object"); } // Import the artifacts. Artifact[] artifacts = new Artifact[requestContext.SAMLArt.length]; for (int index = 0; index < requestContext.SAMLArt.length; index++) { try { log.debug("processing encoded artifact (" + requestContext.SAMLArt[index] + ")"); // If a replay cache was provided, check for replay. if (replayCache != null) { String key = "A_" + requestContext.SAMLArt[index]; if (!replayCache.check(key,new Date(System.currentTimeMillis() + 2*skew))) throw new ReplayedAssertionException("rejecting replayed artifact (" + requestContext.SAMLArt[index] + ")"); } else log.warn("replay cache was not provided, this is a potential security risk!"); artifacts[index] = SAMLArtifact.getTypeCode(requestContext.SAMLArt[index]).getParser().parse(requestContext.SAMLArt[index]); } catch (ArtifactParseException e) { log.error("invalid artifact (" + requestContext.SAMLArt[index] + ")"); throw new FatalProfileException("unable to parse artifact"); } catch (ArtifactParserException e) { log.error("unrecognized artifact type (" + requestContext.SAMLArt[index] + ")"); throw new FatalProfileException("unable to build parser for received artifact, unknown type"); } } // That's actually the hard part. The rest of the work is mostly done by the caller. // An exception might get tossed here, of course. SAMLRequest request = new SAMLRequest(Arrays.asList(artifacts)); request.setMinorVersion(minorVersion); response = artifactMapper.resolve(request); wasPost = false; } // At this point, we have a seemingly valid response, either via POST or from an artifact callback. // This is messy. We have to basically guess as to where the authentication statement is, by finding // one with an appropriate subject confirmation method. We go for the first match inside a valid assertion. try { boolean bExpired = false; for (Iterator assertions=response.getAssertions(); assertion == null && assertions.hasNext();) { bExpired=false; SAMLAssertion a=(SAMLAssertion)assertions.next(); // The assertion must be bounded front and back. Date notBefore=a.getNotBefore(); Date notOnOrAfter=a.getNotOnOrAfter(); if (notBefore == null || notOnOrAfter == null) { log.debug("skipping assertion without time conditions..."); continue; } if (now + skew < notBefore.getTime()) { bExpired=true; log.debug("skipping assertion that's not yet valid..."); continue; } if (notOnOrAfter.getTime() <= now - skew) { bExpired=true; log.debug("skipping expired assertion..."); continue; } // Look for an authentication statement. for (Iterator statements=a.getStatements(); authnStatement == null && statements.hasNext();) { SAMLStatement s=(SAMLStatement)statements.next(); if (!(s instanceof SAMLAuthenticationStatement)) continue; SAMLAuthenticationStatement as=(SAMLAuthenticationStatement)s; SAMLSubject subject=as.getSubject(); for (Iterator methods=subject.getConfirmationMethods(); methods.hasNext();) { String m=(String)methods.next(); if ((wasPost && m.equals(SAMLSubject.CONF_BEARER)) || m.equals(SAMLSubject.CONF_ARTIFACT) || m.equals(SAMLSubject.CONF_ARTIFACT01)) { authnStatement=as; assertion=a; break; } } } } if (authnStatement == null) { if (bExpired == true && response.getAssertions().hasNext()) throw new ExpiredAssertionException("unable to accept assertion because of clock skew"); throw new FatalProfileException("unable to locate a valid authentication statement"); } else if (wasPost) { // Check for assertion replay. With artifact, the back-channel acts as a replay guard. if (replayCache != null) { String key="P_" + assertion.getId(); if (!replayCache.check(key,assertion.getNotOnOrAfter())) throw new ReplayedAssertionException("rejecting replayed assertion ID (" + assertion.getId() + ")"); } else log.warn("replay cache was not provided, this is a serious security risk!"); } } catch (SAMLException e) { if (issuer != null) { Iterator assertions=response.getAssertions(); if (assertions.hasNext()) issuer.append(((SAMLAssertion)assertions.next()).getIssuer()); } throw e; } // Copy over profile data. BrowserProfileResponse profileResponse = new BrowserProfileResponse(); profileResponse.response = response; profileResponse.assertion = assertion; profileResponse.authnStatement = authnStatement; // Extract TARGET parameter, if any. Might be required in SAML, but this is more forgiving. profileResponse.TARGET=requestContext.TARGET; return profileResponse; } }