/*
* JBoss, Home of Professional Open Source
*
* Copyright 2013 Red Hat, Inc. and/or its affiliates.
*
* 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 org.picketlink.trust.jbossws.jaas;
import org.jboss.security.SimpleGroup;
import org.jboss.security.auth.spi.AbstractServerLoginModule;
import org.picketlink.common.exceptions.ConfigurationException;
import org.picketlink.common.util.Base64;
import org.picketlink.identity.federation.core.parsers.saml.SAMLAssertionParser;
import org.picketlink.identity.federation.core.saml.v2.util.XMLTimeUtil;
import org.picketlink.identity.federation.saml.v2.assertion.AssertionType;
import org.picketlink.identity.federation.saml.v2.assertion.AttributeStatementType;
import org.picketlink.identity.federation.saml.v2.assertion.AttributeStatementType.ASTChoiceType;
import org.picketlink.identity.federation.saml.v2.assertion.AudienceRestrictionType;
import org.picketlink.identity.federation.saml.v2.assertion.ConditionAbstractType;
import org.picketlink.identity.federation.saml.v2.assertion.ConditionsType;
import org.picketlink.identity.federation.saml.v2.assertion.NameIDType;
import org.picketlink.identity.federation.saml.v2.assertion.StatementAbstractType;
import org.picketlink.identity.federation.saml.v2.assertion.SubjectType;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginException;
import javax.security.jacc.PolicyContext;
import javax.servlet.http.HttpServletRequest;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.Principal;
import java.security.acl.Group;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* <p> A login module that consumes a SAML Assertion passed via the password piece of a Basic authentication request. In other
* words, the SAML Assertion should be passed as the password (with a username of "SAML-BEARER-TOKEN") in a BASIC auth style
* request. The Authorization HTTP header would look like a normal BASIC auth version (e.g. "Basic
* U0FNTC1CRUFSRVItVE9LRU46PHNhbWw6QXNz="), but the Base64 Decoded Credentials will look like: </p>
*
* <pre>
* SAML-BEARER-TOKEN:<saml:Assertion ...>...</saml:Assertion>
* </pre>
* <p> This class will validate the SAML Assertion and then consume it, making the JAAS principal the same as the SAML subject. JAAS
* role information is pulled from a multi-value SAML Attribute called "Role". </p>
*
* @author eric.wittmann@redhat.com
*/
public class SAMLBearerTokenLoginModule extends AbstractServerLoginModule {
public static final String AUTHORIZATION = "Authorization";
public static final String BASIC = "Basic";
public static final String SAML_BEARER_TOKEN = "SAML-BEARER-TOKEN:";
/** Configured in standalone.xml in the login module */
private Set<String> allowedIssuers = new HashSet<String>();
private Principal identity;
private Set<String> roles = new HashSet<String>();
/**
* Constructor.
*/
public SAMLBearerTokenLoginModule() {
}
/**
* @see org.jboss.security.auth.spi.AbstractServerLoginModule#initialize(javax.security.auth.Subject,
* javax.security.auth.callback.CallbackHandler, java.util.Map, java.util.Map)
*/
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
super.initialize(subject, callbackHandler, sharedState, options);
String val = (String) options.get("allowedIssuers");
if (val != null) {
String[] split = val.split(",");
for (String issuer : split) {
if (issuer != null && issuer.trim().length() > 0) {
allowedIssuers.add(issuer);
}
}
}
}
/**
* @see org.jboss.security.auth.spi.AbstractServerLoginModule#login()
*/
@Override
public boolean login() throws LoginException {
InputStream is = null;
try {
HttpServletRequest request = (HttpServletRequest) PolicyContext.getContext("javax.servlet.http.HttpServletRequest");
String authorization = request.getHeader(AUTHORIZATION);
if (authorization != null && authorization.startsWith(BASIC)) {
String b64Data = authorization.substring(6);
byte[] dataBytes = Base64.decode(b64Data);
String data = new String(dataBytes, "UTF-8");
if (data.startsWith(SAML_BEARER_TOKEN)) {
String assertionData = data.substring(18);
SAMLAssertionParser parser = new SAMLAssertionParser();
is = new ByteArrayInputStream(assertionData.getBytes("UTF-8"));
XMLEventReader xmlEventReader = XMLInputFactory.newInstance().createXMLEventReader(is);
Object parsed = parser.parse(xmlEventReader);
AssertionType assertion = (AssertionType) parsed;
validateAssertion(assertion, request);
consumeAssertion(assertion);
loginOk = true;
return true;
}
}
} catch (LoginException le) {
throw le;
} catch (Exception e) {
e.printStackTrace();
loginOk = false;
return false;
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
}
}
}
return super.login();
}
/**
* Validates that the assertion is acceptable based on configurable criteria.
*
* @param assertion
* @param request
*
* @throws LoginException
*/
private void validateAssertion(AssertionType assertion, HttpServletRequest request) throws LoginException {
// Possibly fail the assertion based on issuer.
String issuer = assertion.getIssuer().getValue();
if (!allowedIssuers.contains(issuer)) {
throw new LoginException("Dis-allowed SAML Assertion Issuer: " + issuer + " Allowed: " + allowedIssuers);
}
// Possibly fail the assertion based on audience restriction
String currentAudience = request.getContextPath();
Set<String> audienceRestrictions = getAudienceRestrictions(assertion);
if (!audienceRestrictions.contains(currentAudience)) {
throw new LoginException("SAML Assertion Audience Restrictions not valid for this context (" + currentAudience
+ ")");
}
// Possibly fail the assertion based on time.
try {
ConditionsType conditionsType = assertion.getConditions();
if (conditionsType != null) {
XMLGregorianCalendar now = XMLTimeUtil.getIssueInstant();
XMLGregorianCalendar notBefore = conditionsType.getNotBefore();
XMLGregorianCalendar notOnOrAfter = conditionsType.getNotOnOrAfter();
if (!XMLTimeUtil.isValid(now, notBefore, notOnOrAfter)) {
String msg = "SAML Assertion has expired: " + "Now=" + now.toXMLFormat() + " ::notBefore="
+ notBefore.toXMLFormat() + " ::notOnOrAfter=" + notOnOrAfter;
throw new LoginException(msg);
}
} else {
throw new LoginException("SAML Assertion not valid (no Conditions supplied).");
}
} catch (ConfigurationException e) {
// should never happen - see AssertionUtil.hasExpired code for why
throw new LoginException(e.getMessage());
}
}
/**
* Gets the audience restriction condition.
*
* @param assertion
*/
private Set<String> getAudienceRestrictions(AssertionType assertion) {
Set<String> rval = new HashSet<String>();
if (assertion == null || assertion.getConditions() == null || assertion.getConditions().getConditions() == null) {
return rval;
}
List<ConditionAbstractType> conditions = assertion.getConditions().getConditions();
for (ConditionAbstractType conditionAbstractType : conditions) {
if (conditionAbstractType instanceof AudienceRestrictionType) {
AudienceRestrictionType art = (AudienceRestrictionType) conditionAbstractType;
List<URI> audiences = art.getAudience();
for (URI uri : audiences) {
rval.add(uri.toString());
}
}
}
return rval;
}
/**
* Consumes the assertion, resulting in the extraction of the Subject as the JAAS principal and the Role Statements as the JAAS
* roles.
*
* @param assertion
*
* @throws Exception
*/
private void consumeAssertion(AssertionType assertion) throws Exception {
SubjectType samlSubjectType = assertion.getSubject();
String samlSubject = ((NameIDType) samlSubjectType.getSubType().getBaseID()).getValue();
identity = createIdentity(samlSubject);
Set<StatementAbstractType> statements = assertion.getStatements();
for (StatementAbstractType statement : statements) {
if (statement instanceof AttributeStatementType) {
AttributeStatementType attrStatement = (AttributeStatementType) statement;
List<ASTChoiceType> attributes = attrStatement.getAttributes();
for (ASTChoiceType astChoiceType : attributes) {
if (astChoiceType.getAttribute() != null && astChoiceType.getAttribute().getName().equals("Role")) {
List<Object> values = astChoiceType.getAttribute().getAttributeValue();
for (Object roleValue : values) {
if (roleValue != null) {
roles.add(roleValue.toString());
}
}
}
}
}
}
}
/**
* @see org.jboss.security.auth.spi.AbstractServerLoginModule#getIdentity()
*/
@Override
protected Principal getIdentity() {
return identity;
}
/**
* @see org.jboss.security.auth.spi.AbstractServerLoginModule#getRoleSets()
*/
@Override
protected Group[] getRoleSets() throws LoginException {
Group[] groups = new Group[1];
groups[0] = new SimpleGroup("Roles");
try {
for (String role : roles) {
groups[0].addMember(createIdentity(role));
}
} catch (Exception e) {
throw new LoginException("Failed to create group principal: " + e.getMessage());
}
return groups;
}
}