/*
* 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;
}
}