/*
* 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;
import java.io.ByteArrayInputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeMap;
import java.util.Vector;
import org.apache.xml.security.exceptions.Base64DecodingException;
import org.apache.xml.security.utils.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Basic implementation of SAML POST browser profile.
* This is a deprecated and limited interface that will
* not be maintained and may be removed in a future version.
* Please use the SAMLBrowserProfile factory and default
* provider instead of this class.
*
* @author Scott Cantor
* @created April 1, 2002
* @deprecated
*/
public class SAMLPOSTProfile
{
private static Logger log = LoggerFactory.getLogger(SAMLPOSTProfile.class);
private static TreeMap replayExpMap = new TreeMap();
private static HashSet replayCache = new HashSet();
/**
* Locates an assertion containing a "bearer" AuthenticationStatement in
* the response and validates the enclosing assertion with respect to the
* POST profile
*
* @param r The response to the accepting site
* @param audiences The set of audience values to test any conditions
* against
* @return An SSO assertion
*
* @throws SAMLException Thrown if a valid SSO assertion cannot be found
*/
public static SAMLAssertion getSSOAssertion(SAMLResponse r, Collection audiences)
throws SAMLException
{
int acount = 0;
boolean bExpired = false;
Iterator assertions = r.getAssertions();
assertion_loop :
while (assertions.hasNext())
{
acount++;
bExpired = false;
SAMLAssertion a=(SAMLAssertion)assertions.next();
// A SSO assertion must be bounded front and back.
Date notBefore = a.getNotBefore();
Date notOnOrAfter = a.getNotOnOrAfter();
if (notBefore == null || notOnOrAfter == null)
continue;
if (notBefore.getTime() - 300000 > System.currentTimeMillis())
{
bExpired = true;
continue;
}
if (notOnOrAfter.getTime() + 300000 <= System.currentTimeMillis())
{
bExpired = true;
continue;
}
// Check conditions. The only type we know about is an audience restriction.
Iterator conditions = a.getConditions();
while (conditions.hasNext())
{
SAMLCondition c=(SAMLCondition)conditions.next();
if (!(c instanceof SAMLAudienceRestrictionCondition) ||
!((SAMLAudienceRestrictionCondition)c).eval(audiences))
continue assertion_loop;
}
// Look for an authentication statement.
Iterator statements = a.getStatements();
while (statements.hasNext())
{
SAMLStatement s=(SAMLStatement)statements.next();
if (!(s instanceof SAMLAuthenticationStatement))
continue;
SAMLSubject subject=((SAMLAuthenticationStatement)s).getSubject();
Iterator methods=subject.getConfirmationMethods();
while (methods.hasNext())
if (((String)methods.next()).equals(SAMLSubject.CONF_BEARER))
return a;
}
}
if (bExpired == true && acount == 1)
throw new ExpiredAssertionException(SAMLException.RESPONDER,"SAMLPOSTProfile.getSSOAssertion() unable to find a SSO assertion with valid time condition");
throw new FatalProfileException(SAMLException.RESPONDER,"SAMLPOSTProfile.getSSOAssertion() unable to find a valid SSO assertion");
}
/**
* Locates a "bearer" AuthenticationStatement in the assertion and
* validates the statement with respect to the POST profile
*
* @param a The SSO assertion sent to the accepting site
* @return A "bearer" authentication statement
*
* @throws SAMLException Thrown if a SSO statement cannot be found
*/
public static SAMLAuthenticationStatement getSSOStatement(SAMLAssertion a)
throws SAMLException
{
// Look for an authentication statement.
Iterator statements = a.getStatements();
while (statements.hasNext())
{
SAMLStatement s=(SAMLStatement)statements.next();
if (!(s instanceof SAMLAuthenticationStatement))
continue;
SAMLSubject subject=((SAMLAuthenticationStatement)s).getSubject();
Iterator methods=subject.getConfirmationMethods();
while (methods.hasNext())
if (((String)methods.next()).equals(SAMLSubject.CONF_BEARER))
return (SAMLAuthenticationStatement)s;
}
throw new FatalProfileException(SAMLException.RESPONDER,"SAMLPOSTProfile.getSSOStatement() unable to find a valid SSO statement");
}
/**
* Searches the replay cache for the specified assertion and inserts a
* newly seen assertion into the cache<P>
*
* Also performs garbage collection of the cache by deleting expired
* entries.
*
* @param a The assertion to look up and possibly add
* @return true iff the assertion has not been seen before
*/
public static synchronized boolean checkReplayCache(SAMLAssertion a)
{
// Garbage collect any expired entries.
Set trash = replayExpMap.headMap(new Date()).keySet();
for (Iterator i = trash.iterator(); i.hasNext(); replayCache.remove(replayExpMap.get(i.next())))
;
trash.clear();
// If it's already been seen, bail.
if (!replayCache.add(a.getId()))
return false;
// Not a multi-map, so if there's duplicate timestamp, increment by a millisecond.
Date expires = new Date(a.getNotOnOrAfter().getTime() + 300000);
while (replayExpMap.containsKey(expires))
expires.setTime(expires.getTime() + 1);
// Add the pair to the expiration map.
replayExpMap.put(expires, a.getId());
return true;
}
/**
* Parse a Base-64 encoded buffer back into a SAML response and optionally test its
* validity against the POST profile<P>
*
* The signature over the response is not verified or examined, nor is the
* identity of the signer. The replay cache is also not checked.
*
* @param buf A Base-64 encoded buffer containing a SAML
* response
* @param receiver The URL of the intended consumer of the
* response
* @param ttlSeconds Seconds allowed to lapse from the issuance of
* the response
* @param process Process the response or just decode and parse it?
* @return SAML response sent by origin site
* @exception SAMLException Thrown if the response is invalid
*/
public static SAMLResponse accept(byte[] buf, String receiver, int ttlSeconds, boolean process)
throws SAMLException
{
try
{
SAMLResponse r = new SAMLResponse(new ByteArrayInputStream(Base64.decode(buf)));
if (process)
process(r, receiver, ttlSeconds);
return r;
}
catch (Base64DecodingException e)
{
throw new InvalidAssertionException(SAMLException.REQUESTER, "SAMLPOSTProfile.accept() unable to decode base64 response");
}
}
/**
* Test the validity of a response against the POST profile<P>
*
* The signature over the response is not verified or examined, nor is the
* identity of the signer. The replay cache is also not checked.
*
* @param r The response to process
* @param receiver The URL of the intended consumer of the
* response
* @param ttlSeconds Seconds allowed to lapse from the issuance of
* the response
* @return SAML response sent by origin site
* @exception SAMLException Thrown if the response is invalid
*/
public static void process(SAMLResponse r, String receiver, int ttlSeconds)
throws SAMLException
{
if (receiver == null || receiver.length() == 0 || !receiver.equals(r.getRecipient()))
throw new InvalidAssertionException(SAMLException.REQUESTER, "SAMLPOSTProfile.accept() detected recipient mismatch: " + r.getRecipient());
if (r.getIssueInstant().getTime() + (1000 * ttlSeconds) + 300000 < System.currentTimeMillis())
throw new ExpiredAssertionException(SAMLException.RESPONDER, "SAMLPOSTProfile.accept() detected expired response");
}
/**
* Used by authenticating site to generate a SAML response conforming to
* the POST profile<P>
*
* The response MUST be signed by the caller before sending to relying
* site.<P>
*
* Implementations that need to embed additional statements or more complex
* conditions can override or ignore this class.
*
* @param recipient URL of intended consumer
* @param issuer Issuer of assertion
* @param audiences URIs identifying intended relying
* parties/communities (optional)
* @param name Name of subject
* @param nameQualifier Federates or qualifies subject name (optional)
* @param format URI describing name semantics and format
* (optional)
* @param subjectIP Client address of subject (optional)
* @param authMethod URI of authentication method being asserted
* @param authInstant Date and time of authentication being asserted
* @param bindings Set of SAML authorities the relying party
* may contact (optional)
* @return SAML response to send to accepting site
* @exception SAMLException Base class of exceptions that may be thrown
* during processing
* @deprecated Callers should prefer the overloaded method
* that accepts <code>SAMLNameIdentifier</code> objects
*/
public static SAMLResponse prepare(
String recipient,
String issuer,
Collection audiences,
String name,
String nameQualifier,
String format,
String subjectIP,
String authMethod,
Date authInstant,
Collection bindings)
throws SAMLException {
return prepare(
recipient,
issuer,
audiences,
new SAMLNameIdentifier(name, nameQualifier, format),
subjectIP,
authMethod,
authInstant,
bindings);
}
/**
* Used by authenticating site to generate a SAML response conforming to
* the POST profile<P>
*
* The response MUST be signed by the caller before sending to relying
* site.<P>
*
* Implementations that need to embed additional statements or more complex
* conditions can override or ignore this class.
*
* @param recipient URL of intended consumer
* @param issuer Issuer of assertion
* @param audiences URIs identifying intended relying
* parties/communities (optional)
* @param nameId Name Identifier representing the subject
* @param subjectIP Client address of subject (optional)
* @param authMethod URI of authentication method being asserted
* @param authInstant Date and time of authentication being asserted
* @param bindings Set of SAML authorities the relying party
* may contact (optional)
* @return SAML response to send to accepting site
* @exception SAMLException Base class of exceptions that may be thrown
* during processing
*/
public static SAMLResponse prepare(String recipient,
String issuer,
Collection audiences,
SAMLNameIdentifier nameId,
String subjectIP,
String authMethod,
Date authInstant,
Collection bindings)
throws SAMLException
{
log.info("Creating SAML Response.");
if (recipient == null || recipient.length() == 0)
throw new SAMLException(SAMLException.RESPONDER, "SAMLPOSTProfile.prepare() requires recipient");
Vector conditions = new Vector(1);
if (audiences != null && audiences.size() > 0)
conditions.add(new SAMLAudienceRestrictionCondition(audiences));
String[] confirmationMethods = {SAMLSubject.CONF_BEARER};
SAMLSubject subject = new SAMLSubject(nameId, Arrays.asList(confirmationMethods), null, null);
SAMLStatement[] statements =
{new SAMLAuthenticationStatement(subject, authMethod, authInstant, subjectIP, null, bindings)};
SAMLAssertion[] assertions = {
new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(System.currentTimeMillis() + 300000),
conditions, null, Arrays.asList(statements))
};
return new SAMLResponse(null, recipient, Arrays.asList(assertions), null);
}
}