/*
* JBoss, Home of Professional Open Source Copyright 2009, Red Hat Middleware
* LLC, and individual contributors by the @authors tag. See the copyright.txt
* in the distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this software; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
* site: http://www.fsf.org.
*/
package org.picketlink.identity.federation.core.wstrust.auth;
import java.io.IOException;
import java.security.Principal;
import java.security.acl.Group;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import javax.xml.datatype.XMLGregorianCalendar;
import org.jboss.security.SecurityConstants;
import org.jboss.security.SecurityContext;
import org.jboss.security.SimpleGroup;
import org.jboss.security.SimplePrincipal;
import org.jboss.security.identity.Role;
import org.jboss.security.identity.RoleGroup;
import org.jboss.security.mapping.MappingContext;
import org.jboss.security.mapping.MappingManager;
import org.jboss.security.mapping.MappingType;
import org.picketlink.identity.federation.PicketLinkLogger;
import org.picketlink.identity.federation.PicketLinkLoggerFactory;
import org.picketlink.identity.federation.core.constants.AttributeConstants;
import org.picketlink.identity.federation.core.constants.PicketLinkFederationConstants;
import org.picketlink.identity.federation.core.exceptions.ParsingException;
import org.picketlink.identity.federation.core.factories.JBossAuthCacheInvalidationFactory;
import org.picketlink.identity.federation.core.factories.JBossAuthCacheInvalidationFactory.TimeCacheExpiry;
import org.picketlink.identity.federation.core.saml.v2.util.AssertionUtil;
import org.picketlink.identity.federation.core.util.StringUtil;
import org.picketlink.identity.federation.core.wstrust.STSClient;
import org.picketlink.identity.federation.core.wstrust.STSClientConfig;
import org.picketlink.identity.federation.core.wstrust.STSClientConfig.Builder;
import org.picketlink.identity.federation.core.wstrust.STSClientFactory;
import org.picketlink.identity.federation.core.wstrust.SamlCredential;
import org.picketlink.identity.federation.core.wstrust.WSTrustException;
import org.picketlink.identity.federation.core.wstrust.plugins.saml.SAMLUtil;
import org.picketlink.identity.federation.saml.v2.assertion.AssertionType;
import org.w3c.dom.Element;
/**
* Abstract JAAS LoginModule for JBoss STS (Security Token Service). </p>
*
* Subclasses are required to implement {@link #invokeSTS(STSClient)()} to perform their specific actions.
*
* <h3>Configuration</h3> Concrete implementations specify from where the username and credentials should be read from. <lu> <li>
* Callback handler, {@link NameCallback} and {@link PasswordCallback}.</li> <li>From the login modules options configuration.</li>
* <li>From the login modules earlier in the login modules stack.</li> </lu>
*
* <h3>Configuration example</h3> 1. Callbackhandler configuration:
*
* <pre>
* {@code
* <application-policy name="saml-issue-token">
* <authentication>
* <login-module code="org.picketlink.identity.federation.core.wstrust.auth.STSIssuingLoginModule" flag="required">
* <module-option name="configFile">/sts-client.properties</module-option>
* </login-module>
* </authentication>
* </application-policy>
* }
* </pre>
*
* 2. Login module options configuration:
*
* <pre>
* {@code
* <application-policy name="saml-issue-token">
* <authentication>
* <login-module code="org.picketlink.identity.federation.core.wstrust.auth.STSIssuingLoginModule" flag="required">
* <module-option name="configFile">/sts-client.properties</module-option>
* <module-option name="useOptionsCredentials">true</module-option>
* </login-module>
* </authentication>
* </application-policy>
* }
* </pre>
*
* 3. Password stacking configuration:
*
* <pre>
* {@code
* <application-policy name="saml-issue-token">
* <authentication>
* <login-module code="org.picketlink.identity.federation.core.wstrust.auth.STSIssuingLoginModule" flag="required">
* <module-option name="configFile">/sts-client.properties</module-option>
* <module-option name="password-stacking">useFirstPass</module-option>
* </login-module>
* </authentication>
* </application-policy>
* }
* </pre>
*
* <h3>Password stacking</h3> Password stacking can be configured which means that a Login module configured with
* 'password-stacking' set to 'true' will set the username and password in the shared state map. Login modules that come after
* can set 'password-stacking' to 'useFirstPass' which means that that login module will use the username and password from the
* shared map.
* <p/>
* </pre> 4. Mapping Provider configuration:
*
* <pre>
* {@code
* <application-policy name="saml-issue-token">
* <authentication>
* <login-module code="org.picketlink.identity.federation.core.wstrust.auth.STSIssuingLoginModule" flag="required">
* <module-option name="configFile">/sts-client.properties</module-option>
* <module-option name="password-stacking">useFirstPass</module-option>
* </login-module>
* <mapping>
* <mapping-module code="org.picketlink.identity.federation.bindings.jboss.auth.mapping.STSPrincipalMappingProvider" type="principal"/>
* <mapping-module code="org.picketlink.identity.federation.bindings.jboss.auth.mapping.STSGroupMappingProvider" type="role"/>
* </mapping>
* </authentication>
* </application-policy>
* }
* </pre>
*
* <h3>Mapping Providers</h3>
* Principal and Role mapping providers may be configured on subclasses of this login module and be leveraged to populate the
* JAAS Subject with appropriate user id and roles. The token is made available to the mapping providers so that identity
* information may be extracted.
* <p/>
*
* Subclasses can define more configuration options by overriding initialize. Also note that subclasses are not forced to put
* configuration options in a file. They can all be set as options just like the 'configFile' is specified above.
*
* <h3>Additional Configuration</h3>
* <p>
* roleKey: By default, the saml attributes with key "Role" are assumed to represent user roles. You can configure a comma
* separated list of string values to represent the attribute names for user roles.
* </p>
*
* <p>
* cache.invalidation: set it to true if you require invalidation of JBoss Auth Cache at SAML Principal expiration.
* </p>
* <p>
* jboss.security.security_domain: name of the security domain where this login module is configured. This is only required if
* the cache.invalidation option is configured.
* </p>
*
* <p>
* inject.callerprincipal: set it to true if you want to add a group principal called "CallerPrincipal" with the roles from the
* assertion, into the subject
* </p>
*
* @author <a href="mailto:dbevenius@jboss.com">Daniel Bevenius</a>
* @author Anil.Saldhana@redhat.com
*/
public abstract class AbstractSTSLoginModule implements LoginModule {
protected static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger();
/**
* Key used in share state map when LMs are stacked.
*/
public static final String SHARED_TOKEN = "org.picketlink.identity.federation.core.wstrust.lm.stsToken";
/**
* Options configuration name;
*/
public static final String OPTIONS_CREDENTIALS = "useOptionsCredentials";
/**
* Options configuration name;
*/
public static final String OPTIONS_PW_STACKING = "password-stacking";
/**
* This is the required option that should identify the configuration file for WSTrustClient.
*/
public static final String STS_CONFIG_FILE = "configFile";
/**
* Attribute names indicating the user roles
*/
public static final String ROLE_KEY = "roleKey";
/**
* Key to specify the end point address
*/
public static final String ENDPOINT_ADDRESS = "endpointAddress";
/**
* Key to specify the port name
*/
public static final String PORT_NAME = "portName";
/**
* Key to specify the service name
*/
public static final String SERVICE_NAME = "serviceName";
/**
* Key to specify the username
*/
public static final String USERNAME_KEY = "username";
/**
* Key to specify the password
*/
public static final String PASSWORD_KEY = "password";
/**
* Key to specify whether this batch issue request
*/
public static final String IS_BATCH = "isBatch";
/**
* Paramater name.
*/
public static final String MAX_CLIENTS_IN_POOL = "maxClientsInPool";
/**
* Paramater name.
*/
public static final String INITIAL_NUMBER_OF_CLIENTS = "initialNumberOfClients";
/**
* The subject to be populated.
*/
protected Subject subject;
/**
* Callback handler used to gather information from the caller.
*/
protected CallbackHandler callbackHandler;
/**
* WS-Trust SAML Assertion element.
*/
protected Element samlToken;
/**
* The outcome of the authentication process.
*/
protected boolean success;
/**
* The options map passed into this login modules initalize method.
*/
protected Map<String, ?> options;
/**
* The shared state map passed into this login modules initalize method.
*/
@SuppressWarnings("rawtypes")
protected Map sharedState;
/**
* Indicates whether password stacking option was configured.
*/
protected boolean passwordStacking;
/**
* Indicates whether the password-stacking options was specifed as 'useFirstPass'.
*/
protected boolean useFirstPass;
/**
* Indicates whether the 'useOptionsCredentials' was configured.
*/
protected boolean useOptionsCredentials;
/**
* Name of the saml attribute representing roles. Can be csv
*/
protected String roleKey = AttributeConstants.ROLE_IDENTIFIER_ASSERTION;
protected boolean enableCacheInvalidation = false;
/**
* Should a separate Group Principal called "CallerPrincipal" be injected into subject with the roles from the assertion?
*/
protected boolean injectCallerPrincipalGroup = false;
protected String securityDomain = null;
/**
* Value to indicate whether the RST is a batch request
*/
protected boolean isBatch = false;
/**
* Maximal number of clients in the STS Client Pool.
*/
protected int maxClientsInPool = 0;
/**
* Number of clients initialized for in case pool is out of free clients.
*/
protected int initialNumberOfClients = 0;
/**
* Initialized this login module. Simple stores the passed in fields and also validates the options.
*
* @param subject The subject to authenticate/populate.
* @param callbackHandler The callbackhandler that will gather information required by this login module.
* @param sharedState State that is shared with other login modules. Used when modules are chained/stacked.
* @param options The options that were specified for this login module.
*/
public void initialize(final Subject subject, final CallbackHandler callbackHandler, final Map<String, ?> sharedState,
final Map<String, ?> options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
this.options = options;
this.sharedState = sharedState;
final String pwStackingOption = (String) options.get(OPTIONS_PW_STACKING);
passwordStacking = pwStackingOption != null;
if (passwordStacking)
useFirstPass = "useFirstPass".equals(pwStackingOption);
final Boolean useOptionsCreds = Boolean.valueOf((String) options.get(OPTIONS_CREDENTIALS));
if (useOptionsCreds != null)
useOptionsCredentials = useOptionsCreds.booleanValue();
final String roleKeyStr = (String) options.get(ROLE_KEY);
if (roleKeyStr != null && roleKeyStr.length() > 0)
roleKey = roleKeyStr;
String cacheInvalidation = (String) options.get("cache.invalidation");
if (cacheInvalidation != null && !cacheInvalidation.isEmpty()) {
enableCacheInvalidation = Boolean.parseBoolean(cacheInvalidation);
securityDomain = (String) options.get(SecurityConstants.SECURITY_DOMAIN_OPTION);
if (securityDomain == null || securityDomain.isEmpty())
throw logger.optionNotSet(SecurityConstants.SECURITY_DOMAIN_OPTION);
}
String callerPrincipalGroup = (String) options.get("inject.callerprincipal");
if (callerPrincipalGroup != null && !callerPrincipalGroup.isEmpty()) {
this.injectCallerPrincipalGroup = Boolean.parseBoolean(callerPrincipalGroup);
}
String batchIssueString = (String) options.get(IS_BATCH);
if (StringUtil.isNotNull(batchIssueString)) {
this.isBatch = Boolean.parseBoolean(batchIssueString);
}
String maxClientsString = (String) options.get(MAX_CLIENTS_IN_POOL);
if (StringUtil.isNotNull(maxClientsString)) {
try {
this.maxClientsInPool = Integer.parseInt(maxClientsString);
} catch (Exception e) {
logger.cannotParseParameterValue(MAX_CLIENTS_IN_POOL, e);
}
}
String initialNumberOfClientsString = (String) options.get(INITIAL_NUMBER_OF_CLIENTS);
if (StringUtil.isNotNull(initialNumberOfClientsString)) {
try {
this.initialNumberOfClients = Integer.parseInt(initialNumberOfClientsString);
} catch (Exception e) {
logger.cannotParseParameterValue(INITIAL_NUMBER_OF_CLIENTS, e);
}
}
}
/**
* Subclasses must implement the login to perform their specific tasks.
*
* The login module should call {@link #setSamlToken(Element)} with the saml token element that should be added to the
* public credentials in {@link #commit()}.
*
* @return true If the login was successful otherwise false.
* @throws LoginException If an error occurs while trying to perform the authentication.
*/
public boolean login() throws LoginException {
try {
final Builder builder = createBuilder();
if (useOptionsCredentials) {
useCredentialsFromOptions(builder, options);
} else if (isUseFirstPass()) {
useCredentialsFromSharedState(builder);
} else {
useCredentialsFromCallback(builder);
}
if (passwordStacking)
setPasswordStackingCredentials(builder);
final STSClient stsClient = createWSTrustClient(builder.build());
final Element token = invokeSTS(stsClient);
if (token == null) {
// Throw an exception as returing false only says that this login module should be ignored.
throw logger.authCouldNotIssueSAMLToken();
}
setSuccess(true);
setSamlToken(token);
setSharedToken(token);
return true;
} catch (WSTrustException e) {
throw logger.authLoginError(e);
}
}
public abstract Element invokeSTS(final STSClient stsclient) throws WSTrustException, LoginException;
/**
* Commit will package the samlToken set by the login method in a new {@link SamlCredential}. This new SamlCredential will
* be put into the Subject public credentials set.
*/
public boolean commit() throws LoginException {
if (success) {
final SamlCredential samlCredential = new SamlCredential(samlToken);
final boolean added = subject.getPublicCredentials().add(samlCredential);
populateSubject();
if (added)
logger.trace("Added Credential " + samlCredential);
return true;
} else {
return false;
}
}
/**
* Called if the overall authentication failed (phase 2).
*/
public boolean abort() throws LoginException {
success = false;
clearState();
return true;
}
public boolean logout() throws LoginException {
clearState();
return true;
}
/**
* Subclasses can override and create a preconfigured builder
*
* @return
*/
protected Builder createBuilder() {
if (options.containsKey(STS_CONFIG_FILE)) {
return new STSClientConfig.Builder(getRequiredOption(getOptions(), STS_CONFIG_FILE));
} else {
Builder builder = new Builder();
builder.endpointAddress((String) options.get(ENDPOINT_ADDRESS));
builder.portName((String) options.get(PORT_NAME)).serviceName((String) options.get(SERVICE_NAME));
builder.username((String) options.get(USERNAME_KEY)).password((String) options.get(PASSWORD_KEY));
builder.setBatch(isBatch);
String passwordString = (String) options.get(PASSWORD_KEY);
if (passwordString != null && passwordString.startsWith(PicketLinkFederationConstants.PASS_MASK_PREFIX)) {
// password is masked
String salt = (String) options.get(PicketLinkFederationConstants.SALT);
if (StringUtil.isNullOrEmpty(salt))
throw logger.optionNotSet("Salt");
String iCount = (String) options.get(PicketLinkFederationConstants.ITERATION_COUNT);
if (StringUtil.isNullOrEmpty(iCount))
throw logger.optionNotSet("Iteration Count");
int iterationCount = Integer.parseInt(iCount);
try {
builder.password(StringUtil.decode(passwordString, salt, iterationCount));
} catch (Exception e) {
throw logger.unableToDecodePasswordError("Unable to decode password:" + passwordString);
}
}
return builder;
}
}
protected void useCredentialsFromCallback(final Builder builder) throws LoginException {
final NameCallback nameCallback = new NameCallback("user:");
final PasswordCallback passwordCallback = new PasswordCallback("password:", true);
try {
getCallbackHandler().handle(new Callback[] { nameCallback, passwordCallback });
String userNameStr = nameCallback.getName();
if (StringUtil.isNotNull(userNameStr)) {
builder.username(userNameStr);
} else {
logger.trace("UserName from callback is null");
}
char[] passChars = passwordCallback.getPassword();
if (passChars != null) {
builder.password(new String(passChars));
} else {
logger.trace("Password from callback is null");
}
} catch (final IOException e) {
throw logger.authLoginError(e);
} catch (final UnsupportedCallbackException e) {
throw logger.authLoginError(e);
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void setPasswordStackingCredentials(final Builder builder) {
final Map sharedState = this.sharedState;
sharedState.put("javax.security.auth.login.name", builder.getUsername());
sharedState.put("javax.security.auth.login.password", builder.getPassword());
}
protected void useCredentialsFromSharedState(final Builder builder) {
builder.username(getSharedUsername()).password(new String(getSharedPassword()));
}
/**
* This method allows subclassed to retreive configuration options map and set on the builder.
*
* @param builder
* @param options
*/
protected void useCredentialsFromOptions(Builder builder, Map<String, ?> options2) {
// NoOp.
}
/**
* This method gives users a chance to override how the {@link STSClientConfig} is created. For example some users might
* perfer to not use a file containing the configuration properties, which is the default, but instead have the
* configuration options in the login modules configuration directly.
*
* @param options The options passed to the initialize method.
* @return {@link STSClientConfig} The configuration for STSClient.
*/
protected STSClientConfig getConfiguration(final Map<String, ?> options) {
final String configFile = getRequiredOption(options, STS_CONFIG_FILE);
return new STSClientConfig.Builder(configFile).build();
}
protected STSClient createWSTrustClient(final STSClientConfig config) {
try {
return STSClientFactory.getInstance(maxClientsInPool).create(initialNumberOfClients, config);
} catch (final Exception e) {
throw logger.authCouldNotCreateWSTrustClient(e);
}
}
protected String getRequiredOption(final Map<String, ?> options, final String optionName) {
final String option = (String) options.get(optionName);
if (option == null)
throw logger.optionNotSet(optionName);
return option;
}
protected boolean isSuccess() {
return success;
}
protected void setSuccess(boolean success) {
this.success = success;
}
protected Subject getSubject() {
return subject;
}
protected CallbackHandler getCallbackHandler() {
return callbackHandler;
}
protected void setSamlToken(final Element samlToken) {
this.samlToken = samlToken;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected void setSharedToken(final Object token) {
if (sharedState == null)
return;
/*
* This is hidious but must be done since the signature of initialize in LoginModule is: public void initialize(final
* Subject subject, final CallbackHandler callbackHandler, final Map<String, ?> sharedState, final Map<String, ?>
* options) Notice how sharedState is defined. This means that it will not be possible to put anything into that map
* without bypassing generics.
*/
// Cast the shartState to a raw map
final Map state = sharedState;
// Put the Token into the shared state map
state.put(SHARED_TOKEN, token);
}
/**
* Gets Security Token from the share state map if one was made available by a previous LM in the stack.
*
* @return Object A security token if one was stored in the shared state map. Or null if one does not exist.
*/
protected Object getSharedToken() {
if (sharedState == null)
return null;
return sharedState.get(SHARED_TOKEN);
}
/**
* Gets the options provided to this LM in it's {@link #initialize(Subject, CallbackHandler, Map, Map)}.
*
* @return Map<String, ?> The options map.
*/
protected Map<String, ?> getOptions() {
return options;
}
protected String getSharedUsername() {
if (sharedState == null)
return null;
Object sharedName = sharedState.get("javax.security.auth.login.name");
if (sharedName == null) {
return null;
} else if (sharedName instanceof String) {
return (String)sharedName;
}
else if (sharedName instanceof Principal) {
return ((Principal)sharedName).getName();
}
// TODO: change to proper message
throw new RuntimeException("sharedState javax.security.auth.login.name is supposed to contain String or Principal, but contains " + sharedName.getClass().getName());
}
protected char[] getSharedPassword() {
if (sharedState == null)
return null;
final Object object = sharedState.get("javax.security.auth.login.password");
if (object instanceof char[])
return (char[]) object;
else if (object instanceof String)
return ((String) object).toCharArray();
return null;
}
protected boolean isUseFirstPass() {
return useFirstPass;
}
protected boolean isUsePasswordStacking() {
return passwordStacking;
}
protected boolean isUseOptionsConfig() {
return useOptionsCredentials;
}
private void clearState() {
removeAllSamlCredentials(subject);
samlToken = null;
}
public static void removeAllSamlCredentials(final Subject subject) {
final Set<SamlCredential> samlCredentials = subject.getPublicCredentials(SamlCredential.class);
if (!samlCredentials.isEmpty()) {
subject.getPublicCredentials().removeAll(samlCredentials);
}
}
@SuppressWarnings("deprecation")
protected void populateSubject() {
MappingManager mappingManager = getMappingManager();
if (mappingManager == null) {
return;
}
MappingContext<Principal> principalMappingContext = null;
MappingContext<RoleGroup> roleMappingContext = null;
try {
principalMappingContext = mappingManager.getMappingContext(MappingType.PRINCIPAL.toString());
} catch (NoSuchMethodError nse) {
principalMappingContext = mappingManager.getMappingContext(Principal.class);
}
try {
roleMappingContext = mappingManager.getMappingContext(MappingType.ROLE.toString());
} catch (NoSuchMethodError nse) {
roleMappingContext = mappingManager.getMappingContext(RoleGroup.class);
}
Map<String, Object> contextMap = new HashMap<String, Object>();
contextMap.put(SHARED_TOKEN, this.samlToken);
AssertionType assertion = null;
try {
assertion = SAMLUtil.fromElement(samlToken);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (principalMappingContext != null) {
principalMappingContext.performMapping(contextMap, null);
Principal principal = principalMappingContext.getMappingResult().getMappedObject();
subject.getPrincipals().add(principal);
// If the user has configured cache invalidation of subject based on saml token expiry
if (enableCacheInvalidation) {
TimeCacheExpiry cacheExpiry = JBossAuthCacheInvalidationFactory.getCacheExpiry();
XMLGregorianCalendar expiry = AssertionUtil.getExpiration(assertion);
if (expiry != null) {
cacheExpiry.register(securityDomain, expiry.toGregorianCalendar().getTime(), principal);
} else {
logger.samlAssertionWithoutExpiration(assertion.getID());
}
}
}
if (roleMappingContext != null) {
roleMappingContext.performMapping(contextMap, null);
RoleGroup group = roleMappingContext.getMappingResult().getMappedObject();
SimpleGroup rolePrincipal = new SimpleGroup(group.getRoleName());
for (Role role : group.getRoles()) {
rolePrincipal.addMember(new SimplePrincipal(role.getRoleName()));
}
subject.getPrincipals().add(rolePrincipal);
} else {
List<String> roleKeys = new ArrayList<String>();
roleKeys.addAll(StringUtil.tokenize(roleKey));
List<String> roles = AssertionUtil.getRoles(assertion, roleKeys);
if (roles.size() > 0) {
SimpleGroup group = new SimpleGroup(SecurityConstants.ROLES_IDENTIFIER);
for (String role : roles) {
group.addMember(new SimplePrincipal(role));
}
subject.getPrincipals().add(group);
}
}
if (injectCallerPrincipalGroup) {
Group callerPrincipal = new SimpleGroup("CallerPrincipal");
List<String> roles = AssertionUtil.getRoles(assertion, null);
for (String role : roles) {
callerPrincipal.addMember(new SimplePrincipal(role));
}
subject.getPrincipals().add(callerPrincipal);
}
}
protected MappingManager getMappingManager() {
SecurityContext securityContext = SecurityActions.getSecurityContext();
if (securityContext == null) {
return null;
} else {
return securityContext.getMappingManager();
}
}
}