/*
* 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.identity.federation.bindings.wildfly.sp;
import io.undertow.security.api.SecurityContext;
import io.undertow.security.idm.Account;
import io.undertow.security.idm.IdentityManager;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.form.FormParserFactory;
import io.undertow.servlet.handlers.ServletRequestContext;
import io.undertow.servlet.handlers.security.ServletFormAuthenticationMechanism;
import org.jboss.security.audit.AuditLevel;
import org.picketlink.common.ErrorCodes;
import org.picketlink.common.PicketLinkLogger;
import org.picketlink.common.PicketLinkLoggerFactory;
import org.picketlink.common.constants.GeneralConstants;
import org.picketlink.common.constants.JBossSAMLConstants;
import org.picketlink.common.exceptions.ConfigurationException;
import org.picketlink.common.exceptions.ParsingException;
import org.picketlink.common.exceptions.ProcessingException;
import org.picketlink.common.exceptions.fed.AssertionExpiredException;
import org.picketlink.common.util.DocumentUtil;
import org.picketlink.common.util.StringUtil;
import org.picketlink.common.util.SystemPropertiesUtil;
import org.picketlink.config.federation.AuthPropertyType;
import org.picketlink.config.federation.KeyProviderType;
import org.picketlink.config.federation.PicketLinkType;
import org.picketlink.config.federation.SPType;
import org.picketlink.config.federation.handler.Handlers;
import org.picketlink.identity.federation.api.saml.v2.metadata.MetaDataExtractor;
import org.picketlink.identity.federation.bindings.wildfly.ServiceProviderSAMLContext;
import org.picketlink.identity.federation.core.SerializablePrincipal;
import org.picketlink.identity.federation.core.audit.PicketLinkAuditEvent;
import org.picketlink.identity.federation.core.audit.PicketLinkAuditEventType;
import org.picketlink.identity.federation.core.audit.PicketLinkAuditHelper;
import org.picketlink.identity.federation.core.interfaces.TrustKeyManager;
import org.picketlink.identity.federation.core.parsers.saml.SAMLParser;
import org.picketlink.identity.federation.core.saml.v2.factories.SAML2HandlerChainFactory;
import org.picketlink.identity.federation.core.saml.v2.impl.DefaultSAML2HandlerChainConfig;
import org.picketlink.identity.federation.core.saml.v2.interfaces.SAML2Handler;
import org.picketlink.identity.federation.core.saml.v2.interfaces.SAML2HandlerChain;
import org.picketlink.identity.federation.core.saml.v2.interfaces.SAML2HandlerChainConfig;
import org.picketlink.identity.federation.core.saml.v2.interfaces.SAML2HandlerResponse;
import org.picketlink.identity.federation.core.saml.v2.util.AssertionUtil;
import org.picketlink.identity.federation.core.saml.v2.util.HandlerUtil;
import org.picketlink.identity.federation.core.saml.workflow.ServiceProviderSAMLWorkflow;
import org.picketlink.identity.federation.core.util.CoreConfigUtil;
import org.picketlink.identity.federation.core.util.XMLSignatureUtil;
import org.picketlink.identity.federation.saml.v1.assertion.SAML11AssertionType;
import org.picketlink.identity.federation.saml.v1.assertion.SAML11AuthenticationStatementType;
import org.picketlink.identity.federation.saml.v1.assertion.SAML11StatementAbstractType;
import org.picketlink.identity.federation.saml.v1.assertion.SAML11SubjectType;
import org.picketlink.identity.federation.saml.v1.protocol.SAML11ResponseType;
import org.picketlink.identity.federation.saml.v2.metadata.EndpointType;
import org.picketlink.identity.federation.saml.v2.metadata.EntitiesDescriptorType;
import org.picketlink.identity.federation.saml.v2.metadata.EntityDescriptorType;
import org.picketlink.identity.federation.saml.v2.metadata.IDPSSODescriptorType;
import org.picketlink.identity.federation.saml.v2.metadata.KeyDescriptorType;
import org.picketlink.identity.federation.web.config.AbstractSAMLConfigurationProvider;
import org.picketlink.identity.federation.web.core.HTTPContext;
import org.picketlink.identity.federation.web.core.SessionManager;
import org.picketlink.identity.federation.web.process.ServiceProviderBaseProcessor;
import org.picketlink.identity.federation.web.process.ServiceProviderSAMLRequestProcessor;
import org.picketlink.identity.federation.web.process.ServiceProviderSAMLResponseProcessor;
import org.picketlink.identity.federation.web.util.ConfigurationUtil;
import org.picketlink.identity.federation.web.util.PostBindingUtil;
import org.picketlink.identity.federation.web.util.RedirectBindingUtil;
import org.picketlink.identity.federation.web.util.SAMLConfigurationProvider;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.wildfly.extension.undertow.security.AccountImpl;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionListener;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static org.picketlink.common.constants.GeneralConstants.CONFIG_FILE_LOCATION;
import static org.picketlink.common.util.StringUtil.isNotNull;
import static org.picketlink.common.util.StringUtil.isNullOrEmpty;
/**
* PicketLink SP Authentication Mechanism that falls back to FORM
*
* @author Anil Saldhana
* @since November 04, 2013
*/
@WebListener
public class SPFormAuthenticationMechanism extends ServletFormAuthenticationMechanism {
private static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger();
public static final String INITIAL_LOCATION_STORED = "org.picketlink.federation.saml.initial_location";
protected transient String samlHandlerChainClass = null;
protected final ServletContext servletContext;
protected Map<String, Object> chainConfigOptions = new HashMap<String, Object>();
/**
* The user can inject a fully qualified name of a {@link org.picketlink.identity.federation.web.util.SAMLConfigurationProvider}
*/
protected SAMLConfigurationProvider configProvider;
/**
* If the service provider is configured with an IDP metadata file, then this certificate can be picked up from the metadata
*/
protected transient X509Certificate idpCertificate = null;
protected int timerInterval = -1;
protected Timer timer = null;
public static final String EMPTY_PASSWORD = "EMPTY_STR";
protected boolean enableAudit = false;
public static final String FORM_ACCOUNT_NOTE = "picketlink.form.account";
public static final String FORM_REQUEST_NOTE = "picketlink.REQUEST";
protected transient SAML2HandlerChain chain = null;
protected SPType spConfiguration = null;
protected PicketLinkType picketLinkConfiguration = null;
protected String serviceURL = null;
protected String identityURL = null;
protected String issuerID = null;
protected String configFile;
// Whether the authenticator has to to save and restore request
protected boolean saveRestoreRequest = true;
/**
* A Lock for Handler operations in the chain
*/
protected Lock chainLock = new ReentrantLock();
protected String canonicalizationMethod = CanonicalizationMethod.EXCLUSIVE_WITH_COMMENTS;
protected PicketLinkAuditHelper auditHelper;
protected TrustKeyManager keyManager;
private IDPSSODescriptorType idpMetadata;
public SPFormAuthenticationMechanism(FormParserFactory parserFactory, String name, String loginPage, String errorPage, ServletContext servletContext, SAMLConfigurationProvider configProvider, PicketLinkAuditHelper auditHelper) {
super(parserFactory, name, loginPage, errorPage);
this.servletContext = servletContext;
this.configProvider = configProvider;
this.auditHelper = auditHelper;
startPicketLink();
}
@Override
public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) {
ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpServletRequest request = (HttpServletRequest) servletRequestContext.getServletRequest();
HttpServletResponse response = (HttpServletResponse) servletRequestContext.getServletResponse();
String samlRequest = request.getParameter(GeneralConstants.SAML_REQUEST_KEY);
HttpSession session = request.getSession(true);
try {
// General User Request
if (!isNotNull(samlRequest) && !response.isCommitted()) {
session.setAttribute(INITIAL_LOCATION_STORED, true);
storeInitialLocation(exchange);
return generalUserRequest(exchange, securityContext);
}
} catch (Exception e) {
throw new RuntimeException("Could not send authn request to identity provider.", e);
}
if (response.isCommitted()) {
return new ChallengeResult(true);
}
return new ChallengeResult(false);
}
@Override
public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) {
// TODO: Deal with character encoding
// request.setCharacterEncoding(xyz)
ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
ServletContext servletContext = servletRequestContext.getCurrentServletContext();
HttpServletRequest request = (HttpServletRequest) servletRequestContext.getServletRequest();
HttpServletResponse response = (HttpServletResponse) servletRequestContext.getServletResponse();
HttpSession session = request.getSession(true);
// check if this call is resulting from the redirect after successful authentication.
// if so, make the authentication successful and continue the original request
//if (saveRestoreRequest && matchRequest(request)) {
if (saveRestoreRequest) {
Account savedAccount = (Account) session.getAttribute(FORM_ACCOUNT_NOTE);
if(savedAccount != null){
register(securityContext, savedAccount);
}
}
ServiceProviderSAMLWorkflow serviceProviderSAMLWorkflow = new ServiceProviderSAMLWorkflow();
// Eagerly look for Local LogOut
boolean localLogout = serviceProviderSAMLWorkflow.isLocalLogoutRequest(request);
if (localLogout) {
try {
serviceProviderSAMLWorkflow.sendToLogoutPage(request, response, session, servletContext, this.spConfiguration.getLogOutPage());
} catch (ServletException e) {
logger.samlLogoutError(e);
throw new RuntimeException(e);
} catch (IOException e1) {
logger.samlLogoutError(e1);
throw new RuntimeException(e1);
}
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
String samlRequest = request.getParameter(GeneralConstants.SAML_REQUEST_KEY);
String samlResponse = request.getParameter(GeneralConstants.SAML_RESPONSE_KEY);
Principal principal = request.getUserPrincipal();
try {
// If we have already authenticated the user and there is no request from IDP or logout from user
if (principal != null
&& !(serviceProviderSAMLWorkflow.isLocalLogoutRequest(request) || isGlobalLogout(request) || isNotNull(samlRequest) || isNotNull(samlResponse)))
return AuthenticationMechanismOutcome.AUTHENTICATED;
// Handle a SAML Response from IDP
if (isNotNull(samlResponse)) {
return handleSAMLResponse(exchange, securityContext);
}
// Handle SAML Requests from IDP
if (isNotNull(samlRequest)) {
return handleSAMLRequest(exchange,securityContext);
}// end if
// local authentication
return super.authenticate(exchange, securityContext);
} catch (IOException e) {
if (StringUtil.isNotNull(spConfiguration.getErrorPage())) {
try {
request.getRequestDispatcher(spConfiguration.getErrorPage()).forward(request, response);
} catch (ServletException e1) {
logger.samlErrorPageForwardError(spConfiguration.getErrorPage(), e1);
}catch (IOException e1) {
logger.samlErrorPageForwardError(spConfiguration.getErrorPage(), e1);
}
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
} else {
throw new RuntimeException(e);
}
}
}
private AuthenticationMechanismOutcome handleSAMLResponse(HttpServerExchange exchange, SecurityContext securityContext) throws IOException {
ServletRequestContext request = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpServletRequest httpServletRequest = (HttpServletRequest) request.getServletRequest();
HttpServletResponse response = (HttpServletResponse) request.getServletResponse();
String samlVersion = getSAMLVersion(httpServletRequest);
if (!JBossSAMLConstants.VERSION_2_0.get().equals(samlVersion)) {
return handleSAML11UnsolicitedResponse(httpServletRequest, response, securityContext);
}
return handleSAML2Response(exchange, securityContext);
}
/**
* Handle the user invocation for the first time
*
* @param httpServerExchange
* @param securityContext
* @return
* @throws IOException
*/
private ChallengeResult generalUserRequest(HttpServerExchange httpServerExchange, SecurityContext securityContext) throws IOException{
ServiceProviderSAMLWorkflow serviceProviderSAMLWorkflow = new ServiceProviderSAMLWorkflow();
serviceProviderSAMLWorkflow.setRedirectionHandler(new UndertowRedirectionHandler(httpServerExchange));
final ServletRequestContext servletRequestContext = httpServerExchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
ServletContext servletContext = servletRequestContext.getCurrentServletContext();
HttpServletRequest request = (HttpServletRequest) servletRequestContext.getServletRequest();
HttpServletResponse response = (HttpServletResponse) servletRequestContext.getServletResponse();
HttpSession session = request.getSession(true);
boolean willSendRequest = false;
HTTPContext httpContext = new HTTPContext(request, response, servletContext);
Set<SAML2Handler> handlers = chain.handlers();
boolean postBinding = spConfiguration.getBindingType().equals("POST");
// Neither saml request nor response from IDP
// So this is a user request
SAML2HandlerResponse saml2HandlerResponse = null;
try {
ServiceProviderBaseProcessor baseProcessor = new ServiceProviderBaseProcessor(postBinding, serviceURL,
this.picketLinkConfiguration, this.idpMetadata);
if (issuerID != null)
baseProcessor.setIssuer(issuerID);
baseProcessor.setIdentityURL(identityURL);
baseProcessor.setAuditHelper(auditHelper);
saml2HandlerResponse = baseProcessor.process(httpContext, handlers, chainLock);
} catch (ProcessingException pe) {
logger.samlSPHandleRequestError(pe);
throw new RuntimeException(pe);
} catch (ParsingException pe) {
logger.samlSPHandleRequestError(pe);
throw new RuntimeException(pe);
} catch (ConfigurationException pe) {
logger.samlSPHandleRequestError(pe);
throw new RuntimeException(pe);
}
willSendRequest = saml2HandlerResponse.getSendRequest();
Document samlResponseDocument = saml2HandlerResponse.getResultingDocument();
String relayState = saml2HandlerResponse.getRelayState();
String destination = saml2HandlerResponse.getDestination();
String destinationQueryStringWithSignature = saml2HandlerResponse.getDestinationQueryStringWithSignature();
if (destination != null && samlResponseDocument != null) {
try {
if (saveRestoreRequest) {
storeInitialLocation(httpServerExchange);
}
if (enableAudit) {
PicketLinkAuditEvent auditEvent = new PicketLinkAuditEvent(AuditLevel.INFO);
auditEvent.setType(PicketLinkAuditEventType.REQUEST_TO_IDP);
auditEvent.setWhoIsAuditing(servletContext.getContextPath());
auditHelper.audit(auditEvent);
}
serviceProviderSAMLWorkflow.sendRequestToIDP(destination, samlResponseDocument, relayState, response, willSendRequest,
destinationQueryStringWithSignature, isHttpPostBinding());
return new ChallengeResult(true);
} catch (Exception e) {
logger.samlSPHandleRequestError(e);
throw logger.samlSPProcessingExceptionError(e);
}
}
return super.sendChallenge(httpServerExchange, securityContext);
}
protected boolean matchRequest(HttpServletRequest request) {
return false; // assume this is a fresh request
}
protected void register(final SecurityContext securityContext, Account account) {
securityContext.authenticationComplete(account, "FORM", false);
}
/**
* Fall back on local authentication at the service provider side
*
* @param httpServerExchange
* @param securityContext
* @return
* @throws IOException
*/
protected AuthenticationMechanismOutcome localAuthentication(HttpServerExchange httpServerExchange, SecurityContext securityContext) throws IOException {
final ServletRequestContext servletRequestContext = httpServerExchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
ServletContext servletContext = servletRequestContext.getCurrentServletContext();
HttpServletRequest request = (HttpServletRequest) servletRequestContext.getServletRequest();
HttpServletResponse response = (HttpServletResponse) servletRequestContext.getServletResponse();
if (request.getUserPrincipal() == null) {
logger.samlSPFallingBackToLocalFormAuthentication();// fallback
try {
return super.authenticate(httpServerExchange,securityContext);
} catch (NoSuchMethodError e) {
/*// Use Reflection
try {
Method method = super.getClass().getMethod("authenticate",
new Class[] { HttpServletRequest.class, HttpServletResponse.class, LoginConfig.class });
return (Boolean) method.invoke(this, new Object[] { request.getRequest(), response.getResponse(),
loginConfig });
} catch (Exception ex) {
throw logger.unableLocalAuthentication(ex);
}*/
}
} else{
return AuthenticationMechanismOutcome.AUTHENTICATED;
}
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
/**
* Handle the IDP Request
*
* @param httpServerExchange
* @param securityContext
* @return
* @throws IOException
*/
private AuthenticationMechanismOutcome handleSAMLRequest(HttpServerExchange httpServerExchange, SecurityContext securityContext) throws IOException {
final ServletRequestContext servletRequestContext = httpServerExchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
ServletContext servletContext = servletRequestContext.getCurrentServletContext();
HttpServletRequest request = (HttpServletRequest) servletRequestContext.getServletRequest();
HttpServletResponse response = (HttpServletResponse) servletRequestContext.getServletResponse();
String samlRequest = request.getParameter(GeneralConstants.SAML_REQUEST_KEY);
HTTPContext httpContext = new HTTPContext(request, response, servletContext);
Set<SAML2Handler> handlers = chain.handlers();
try {
ServiceProviderSAMLRequestProcessor requestProcessor = new ServiceProviderSAMLRequestProcessor(
request.getMethod().equals("POST"), this.serviceURL, this.picketLinkConfiguration, this.idpMetadata);
requestProcessor.setTrustKeyManager(keyManager);
boolean result = requestProcessor.process(samlRequest, httpContext, handlers, chainLock);
if (enableAudit) {
PicketLinkAuditEvent auditEvent = new PicketLinkAuditEvent(AuditLevel.INFO);
auditEvent.setType(PicketLinkAuditEventType.REQUEST_FROM_IDP);
auditEvent.setWhoIsAuditing(servletContext.getContextPath());
auditHelper.audit(auditEvent);
}
// If response is already commited, we need to stop with processing of HTTP request
if (response.isCommitted())
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
if (result)
return AuthenticationMechanismOutcome.AUTHENTICATED;
} catch (Exception e) {
logger.samlSPHandleRequestError(e);
throw logger.samlSPProcessingExceptionError(e);
}
return localAuthentication(httpServerExchange,securityContext);
}
/**
* Handle IDP Response
*
* @param httpServerExchange
* @param securityContext
* @return
* @throws IOException
*/
private AuthenticationMechanismOutcome handleSAML2Response(HttpServerExchange httpServerExchange, SecurityContext securityContext) throws IOException {
ServiceProviderSAMLWorkflow serviceProviderSAMLWorkflow = new ServiceProviderSAMLWorkflow();
final ServletRequestContext servletRequestContext = httpServerExchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
ServletContext servletContext = servletRequestContext.getCurrentServletContext();
HttpServletRequest request = (HttpServletRequest) servletRequestContext.getServletRequest();
HttpServletResponse response = (HttpServletResponse) servletRequestContext.getServletResponse();
HttpSession session = request.getSession(true);
String samlResponse = request.getParameter(GeneralConstants.SAML_RESPONSE_KEY);
boolean willSendRequest = false;
HTTPContext httpContext = new HTTPContext(request, response, servletContext);
Set<SAML2Handler> handlers = chain.handlers();
Principal principal = request.getUserPrincipal();
if (! serviceProviderSAMLWorkflow.validate(request)) {
throw new IOException(ErrorCodes.VALIDATION_CHECK_FAILED);
}
// deal with SAML response from IDP
try {
ServiceProviderSAMLResponseProcessor responseProcessor = new ServiceProviderSAMLResponseProcessor(request.getMethod().equals("POST"), serviceURL, this.picketLinkConfiguration, this.idpMetadata);
if(auditHelper != null){
responseProcessor.setAuditHelper(auditHelper);
}
responseProcessor.setTrustKeyManager(keyManager);
SAML2HandlerResponse saml2HandlerResponse = responseProcessor.process(samlResponse, httpContext, handlers,
chainLock);
Document samlResponseDocument = saml2HandlerResponse.getResultingDocument();
String relayState = saml2HandlerResponse.getRelayState();
String destination = saml2HandlerResponse.getDestination();
willSendRequest = saml2HandlerResponse.getSendRequest();
String destinationQueryStringWithSignature = saml2HandlerResponse.getDestinationQueryStringWithSignature();
if (destination != null && samlResponseDocument != null) {
serviceProviderSAMLWorkflow.sendRequestToIDP(destination, samlResponseDocument,
relayState, response,
willSendRequest, destinationQueryStringWithSignature,
spConfiguration.getBindingType().equalsIgnoreCase("POST"));
} else {
// See if the session has been invalidated
boolean sessionValidity = sessionIsValid(session);
if (!sessionValidity) {
serviceProviderSAMLWorkflow.sendToLogoutPage(request, response, session, servletContext, this.spConfiguration.getLogOutPage());
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
// We got a response with the principal
final List<String> roles = saml2HandlerResponse.getRoles();
if (principal == null)
principal = (Principal) session.getAttribute(GeneralConstants.PRINCIPAL_ID);
String username = principal.getName();
String password = EMPTY_PASSWORD;
if (logger.isTraceEnabled()) {
logger.trace("Roles determined for username=" + username + "=" + Arrays.toString(roles.toArray()));
}
ServiceProviderSAMLContext.push(username, roles);
//TODO: figure out getting the principal via authentication
IdentityManager identityManager = securityContext.getIdentityManager();
final Principal userPrincipal = principal;
Account account = new AccountImpl(userPrincipal, new HashSet<String>(roles), password);
account = identityManager.verify(account);
//Register the principal with the request
register(securityContext, account);
if (enableAudit) {
PicketLinkAuditEvent auditEvent = new PicketLinkAuditEvent(AuditLevel.INFO);
auditEvent.setType(PicketLinkAuditEventType.RESPONSE_FROM_IDP);
auditEvent.setSubjectName(username);
auditEvent.setWhoIsAuditing(servletContext.getContextPath());
auditHelper.audit(auditEvent);
}
// Redirect the user to the originally requested URL
if (saveRestoreRequest) {
// Store the authenticated principal in the session.
session.setAttribute(FORM_ACCOUNT_NOTE, account);
if (session.getAttribute(INITIAL_LOCATION_STORED) != null) {
// Redirect to the original URL. Note that this will trigger the
// authenticator again, but on resubmission we will look in the
// session notes to retrieve the authenticated principal and
// prevent reauthentication
handleRedirectBack(httpServerExchange);
httpServerExchange.endExchange();
}
}
return AuthenticationMechanismOutcome.AUTHENTICATED;
}
} catch (ProcessingException pe) {
Throwable t = pe.getCause();
if (t != null && t instanceof AssertionExpiredException) {
logger.error("Assertion has expired. Asking IDP for reissue");
if (enableAudit) {
PicketLinkAuditEvent auditEvent = new PicketLinkAuditEvent(AuditLevel.INFO);
auditEvent.setType(PicketLinkAuditEventType.EXPIRED_ASSERTION);
auditEvent.setAssertionID(((AssertionExpiredException) t).getId());
auditHelper.audit(auditEvent);
}
// Just issue a fresh request back to IDP
generalUserRequest(httpServerExchange,securityContext);
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
logger.samlSPHandleRequestError(pe);
throw logger.samlSPProcessingExceptionError(pe);
} catch (Exception e) {
logger.samlSPHandleRequestError(e);
throw logger.samlSPProcessingExceptionError(e);
} finally {
ServiceProviderSAMLContext.clear();
}
return localAuthentication(httpServerExchange,securityContext);
}
/**
* <p>
* Indicates if the SP is configure with HTTP POST Binding.
* </p>
*
* @return
*/
protected boolean isHttpPostBinding() {
return spConfiguration.getBindingType().equalsIgnoreCase("POST");
}
protected boolean sessionIsValid(HttpSession session){
try{
long sessionTime = session.getCreationTime();
} catch(IllegalStateException ise){
return false;
}
return true;
}
protected String savedRequestURL(HttpSession session){
StringBuilder builder = new StringBuilder();
HttpServletRequest request = (HttpServletRequest) session.getAttribute(FORM_REQUEST_NOTE);
if(request != null){
builder.append(request.getRequestURI());
if(request.getQueryString() != null){
builder.append("?").append(request.getQueryString());
}
}
return builder.toString();
}
protected void startPicketLink() {
SystemPropertiesUtil.ensure();
Handlers handlers = null;
//Introduce a timer to reload configuration if desired
if(timerInterval > 0 ){
if(timer == null){
timer = new Timer();
}
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
processConfiguration();
initKeyProvider(servletContext);
}
}, timerInterval, timerInterval);
}
// Get the chain from config
if (StringUtil.isNullOrEmpty(samlHandlerChainClass)) {
chain = SAML2HandlerChainFactory.createChain();
} else {
try {
chain = SAML2HandlerChainFactory.createChain(this.samlHandlerChainClass);
} catch (ProcessingException e1) {
throw new RuntimeException(e1);
}
}
this.processConfiguration();
try {
if (picketLinkConfiguration != null) {
handlers = picketLinkConfiguration.getHandlers();
} else {
// Get the handlers
String handlerConfigFileName = GeneralConstants.HANDLER_CONFIG_FILE_LOCATION;
handlers = ConfigurationUtil.getHandlers(servletContext.getResourceAsStream(handlerConfigFileName));
}
chain.addAll(HandlerUtil.getHandlers(handlers));
this.initKeyProvider(servletContext);
this.populateChainConfig();
this.initializeHandlerChain();
} catch (Exception e) {
throw new RuntimeException(e);
}
if (this.picketLinkConfiguration == null) {
this.picketLinkConfiguration = new PicketLinkType();
this.picketLinkConfiguration.setIdpOrSP(spConfiguration);
this.picketLinkConfiguration.setHandlers(handlers);
}
new SessionManager(servletContext, new SessionManager.InitializationCallback() {
@Override
public void registerSessionListener(Class<? extends HttpSessionListener> listener) {
servletContext.addListener(listener);
}
});
}
/**
* <p>
* Initialize the KeyProvider configurations. This configurations are to be used during signing and validation of SAML
* assertions.
* </p>
*
* @param context
*/
protected void initKeyProvider(ServletContext context){
if (!doSupportSignature()) {
return;
}
KeyProviderType keyProvider = this.spConfiguration.getKeyProvider();
if (keyProvider == null && doSupportSignature())
throw new RuntimeException(ErrorCodes.NULL_VALUE + "KeyProvider is null for context=" + context.getContextPath());
try {
String keyManagerClassName = keyProvider.getClassName();
if (keyManagerClassName == null)
throw new RuntimeException(ErrorCodes.NULL_VALUE + "KeyManager class name");
Class<?> clazz = SecurityActions.loadClass(getClass(), keyManagerClassName);
if (clazz == null)
throw new ClassNotFoundException(ErrorCodes.CLASS_NOT_LOADED + keyManagerClassName);
this.keyManager = (TrustKeyManager) clazz.newInstance();
List<AuthPropertyType> authProperties = CoreConfigUtil.getKeyProviderProperties(keyProvider);
keyManager.setAuthProperties(authProperties);
keyManager.setValidatingAlias(keyProvider.getValidatingAlias());
String identityURL = this.spConfiguration.getIdentityURL();
//Special case when you need X509Data in SignedInfo
if(authProperties != null){
for(AuthPropertyType authPropertyType: authProperties){
String key = authPropertyType.getKey();
if(GeneralConstants.X509CERTIFICATE.equals(key)){
//we need X509Certificate in SignedInfo. The value is the alias name
keyManager.addAdditionalOption(GeneralConstants.X509CERTIFICATE, authPropertyType.getValue());
break;
}
}
}
keyManager.addAdditionalOption(ServiceProviderBaseProcessor.IDP_KEY, new URL(identityURL).getHost());
} catch (Exception e) {
logger.trustKeyManagerCreationError(e);
throw new RuntimeException(e.getLocalizedMessage());
}
logger.trace("Key Provider=" + keyProvider.getClassName());
}
/**
* <p>
* Indicates if digital signatures/validation of SAML assertions are enabled. Subclasses that supports signature should
* override this method.
* </p>
*
* @return
*/
protected boolean doSupportSignature() {
if (spConfiguration != null) {
return spConfiguration.isSupportsSignature();
}
return false;
}
protected void processConfiguration() {
InputStream is = null;
if (isNullOrEmpty(this.configFile)) {
this.configFile = CONFIG_FILE_LOCATION;
is = servletContext.getResourceAsStream(this.configFile);
} else {
try {
is = new FileInputStream(this.configFile);
} catch (FileNotFoundException e) {
throw logger.samlIDPConfigurationError(e);
}
}
try {
// Work on the IDP Configuration
if (configProvider != null) {
try {
if (is == null) {
// Try the older version
is = servletContext.getResourceAsStream(GeneralConstants.DEPRECATED_CONFIG_FILE_LOCATION);
// Additionally parse the deprecated config file
if (is != null && configProvider instanceof AbstractSAMLConfigurationProvider) {
((AbstractSAMLConfigurationProvider) configProvider).setConfigFile(is);
}
} else {
// Additionally parse the consolidated config file
if (is != null && configProvider instanceof AbstractSAMLConfigurationProvider) {
((AbstractSAMLConfigurationProvider) configProvider).setConsolidatedConfigFile(is);
}
}
picketLinkConfiguration = configProvider.getPicketLinkConfiguration();
spConfiguration = configProvider.getSPConfiguration();
} catch (ProcessingException e) {
throw logger.samlSPConfigurationError(e);
} catch (ParsingException e) {
throw logger.samlSPConfigurationError(e);
}
} else {
if (is != null) {
try {
picketLinkConfiguration = ConfigurationUtil.getConfiguration(is);
spConfiguration = (SPType) picketLinkConfiguration.getIdpOrSP();
} catch (ParsingException e) {
logger.trace(e);
throw logger.samlSPConfigurationError(e);
}
} else {
is = servletContext.getResourceAsStream(GeneralConstants.DEPRECATED_CONFIG_FILE_LOCATION);
if (is == null)
throw logger.configurationFileMissing(configFile);
spConfiguration = ConfigurationUtil.getSPConfiguration(is);
}
}
//Close the InputStream as we no longer need it
if(is != null){
try {
is.close();
} catch (IOException e) {
//ignore
}
}
if (this.picketLinkConfiguration != null) {
enableAudit = picketLinkConfiguration.isEnableAudit();
//See if we have the system property enabled
if(!enableAudit){
String sysProp = SecurityActions.getSystemProperty(GeneralConstants.AUDIT_ENABLE, "NULL");
if(!"NULL".equals(sysProp)){
enableAudit = Boolean.parseBoolean(sysProp);
}
}
if (enableAudit) {
if (auditHelper == null) {
String securityDomainName = PicketLinkAuditHelper.getSecurityDomainName(servletContext);
auditHelper = new PicketLinkAuditHelper(securityDomainName);
}
}
}
processIdPMetadata(spConfiguration);
this.identityURL = spConfiguration.getIdentityURL();
this.serviceURL = spConfiguration.getServiceURL();
this.canonicalizationMethod = spConfiguration.getCanonicalizationMethod();
logger.samlSPSettingCanonicalizationMethod(canonicalizationMethod);
XMLSignatureUtil.setCanonicalizationMethodType(canonicalizationMethod);
logger.trace("Identity Provider URL=" + this.identityURL);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void processIdPMetadata(SPType spConfiguration) {
IDPSSODescriptorType idpssoDescriptorType = null;
if (isNotNull(spConfiguration.getIdpMetadataFile())) {
idpssoDescriptorType = getIdpMetadataFromFile(spConfiguration);
} else {
idpssoDescriptorType = getIdpMetadataFromProvider(spConfiguration);
}
if (idpssoDescriptorType != null) {
List<EndpointType> endpoints = idpssoDescriptorType.getSingleSignOnService();
for (EndpointType endpoint : endpoints) {
String endpointBinding = endpoint.getBinding().toString();
if (endpointBinding.contains("HTTP-POST")) {
endpointBinding = "POST";
} else if (endpointBinding.contains("HTTP-Redirect")) {
endpointBinding = "REDIRECT";
}
if (spConfiguration.getBindingType().equals(endpointBinding)) {
spConfiguration.setIdentityURL(endpoint.getLocation().toString());
break;
}
}
List<KeyDescriptorType> keyDescriptors = idpssoDescriptorType.getKeyDescriptor();
if (keyDescriptors.size() > 0) {
this.idpCertificate = MetaDataExtractor.getCertificate(keyDescriptors.get(0));
}
this.idpMetadata = idpssoDescriptorType;
}
}
private IDPSSODescriptorType getIdpMetadataFromProvider(SPType spConfiguration) {
List<EntityDescriptorType> entityDescriptors = CoreConfigUtil.getMetadataConfiguration(spConfiguration,
this.servletContext);
if (entityDescriptors != null) {
for (EntityDescriptorType entityDescriptorType : entityDescriptors) {
IDPSSODescriptorType idpssoDescriptorType = handleMetadata(entityDescriptorType);
if (idpssoDescriptorType != null) {
return idpssoDescriptorType;
}
}
}
return null;
}
protected IDPSSODescriptorType getIdpMetadataFromFile(SPType configuration) {
ServletContext servletContext = this.servletContext;
InputStream is = servletContext.getResourceAsStream(configuration.getIdpMetadataFile());
if (is == null) {
return null;
}
Object metadata = null;
try {
Document samlDocument = DocumentUtil.getDocument(is);
SAMLParser parser = new SAMLParser();
metadata = parser.parse(DocumentUtil.getNodeAsStream(samlDocument));
} catch (Exception e) {
throw new RuntimeException(e);
}
IDPSSODescriptorType idpSSO = null;
if (metadata instanceof EntitiesDescriptorType) {
EntitiesDescriptorType entities = (EntitiesDescriptorType) metadata;
idpSSO = handleMetadata(entities);
} else {
idpSSO = handleMetadata((EntityDescriptorType) metadata);
}
if (idpSSO == null) {
logger.samlSPUnableToGetIDPDescriptorFromMetadata();
return idpSSO;
}
return idpSSO;
}
protected IDPSSODescriptorType handleMetadata(EntitiesDescriptorType entities) {
IDPSSODescriptorType idpSSO = null;
List<Object> entityDescs = entities.getEntityDescriptor();
for (Object entityDescriptor : entityDescs) {
if (entityDescriptor instanceof EntitiesDescriptorType) {
idpSSO = getIDPSSODescriptor(entities);
} else {
idpSSO = handleMetadata((EntityDescriptorType) entityDescriptor);
}
if (idpSSO != null) {
break;
}
}
return idpSSO;
}
protected IDPSSODescriptorType getIDPSSODescriptor(EntitiesDescriptorType entities) {
List<Object> entityDescs = entities.getEntityDescriptor();
for (Object entityDescriptor : entityDescs) {
if (entityDescriptor instanceof EntitiesDescriptorType) {
return getIDPSSODescriptor((EntitiesDescriptorType) entityDescriptor);
}
return CoreConfigUtil.getIDPDescriptor((EntityDescriptorType) entityDescriptor);
}
return null;
}
protected IDPSSODescriptorType handleMetadata(EntityDescriptorType entityDescriptor) {
return CoreConfigUtil.getIDPDescriptor(entityDescriptor);
}
protected void initializeHandlerChain() throws ConfigurationException, ProcessingException {
populateChainConfig();
SAML2HandlerChainConfig handlerChainConfig = new DefaultSAML2HandlerChainConfig(chainConfigOptions);
Set<SAML2Handler> samlHandlers = chain.handlers();
for (SAML2Handler handler : samlHandlers) {
handler.initChainConfig(handlerChainConfig);
}
}
protected void populateChainConfig() throws ConfigurationException, ProcessingException {
chainConfigOptions.put(GeneralConstants.CONFIGURATION, spConfiguration);
chainConfigOptions.put(GeneralConstants.ROLE_VALIDATOR_IGNORE, "false"); // No validator as tomcat realm does validn
if (doSupportSignature()) {
chainConfigOptions.put(GeneralConstants.KEYPAIR, keyManager.getSigningKeyPair());
//If there is a need for X509Data in signedinfo
String certificateAlias = (String)keyManager.getAdditionalOption(GeneralConstants.X509CERTIFICATE);
if(certificateAlias != null){
chainConfigOptions.put(GeneralConstants.X509CERTIFICATE, keyManager.getCertificate(certificateAlias));
}
}
}
private boolean isGlobalLogout(HttpServletRequest request) {
String gloStr = request.getParameter(GeneralConstants.GLOBAL_LOGOUT);
return isNotNull(gloStr) && "true".equalsIgnoreCase(gloStr);
}
private String getSAMLVersion(HttpServletRequest request) {
String samlResponse = request.getParameter(GeneralConstants.SAML_RESPONSE_KEY);
String version;
try {
Document samlDocument = toSAMLResponseDocument(samlResponse, "POST".equalsIgnoreCase(request.getMethod()));
Element element = samlDocument.getDocumentElement();
// let's try SAML 2.0 Version attribute first
version = element.getAttribute("Version");
if (isNullOrEmpty(version)) {
// fallback to SAML 1.1 Minor and Major attributes
String minorVersion = element.getAttribute("MinorVersion");
String majorVersion = element.getAttribute("MajorVersion");
version = minorVersion + "." + majorVersion;
}
} catch (Exception e) {
throw new RuntimeException("Could not extract version from SAML Response.", e);
}
return version;
}
private Document toSAMLResponseDocument(String samlResponse, boolean isPostBinding) throws ParsingException {
InputStream dataStream = null;
if (isPostBinding) {
// deal with SAML response from IDP
dataStream = PostBindingUtil.base64DecodeAsStream(samlResponse);
} else {
// deal with SAML response from IDP
dataStream = RedirectBindingUtil.base64DeflateDecode(samlResponse);
}
try {
return DocumentUtil.getDocument(dataStream);
} catch (Exception e) {
logger.samlResponseFromIDPParsingFailed();
throw new ParsingException("", e);
}
}
public AuthenticationMechanismOutcome handleSAML11UnsolicitedResponse(HttpServletRequest request, HttpServletResponse response, SecurityContext securityContext) {
String samlResponse = request.getParameter(GeneralConstants.SAML_RESPONSE_KEY);
Principal principal = request.getUserPrincipal();
// See if we got a response from IDP
if (isNotNull(samlResponse)) {
try {
InputStream base64DecodedResponse = null;
if ("GET".equalsIgnoreCase(request.getMethod())) {
base64DecodedResponse = RedirectBindingUtil.base64DeflateDecode(samlResponse);
} else {
base64DecodedResponse = PostBindingUtil.base64DecodeAsStream(samlResponse);
}
SAMLParser parser = new SAMLParser();
SAML11ResponseType saml11Response = (SAML11ResponseType) parser.parse(base64DecodedResponse);
List<SAML11AssertionType> assertions = saml11Response.get();
if (assertions.size() > 1) {
logger.trace("More than one assertion from IDP. Considering the first one.");
}
List<String> roles = new ArrayList<String>();
SAML11AssertionType assertion = assertions.get(0);
if (assertion != null) {
// Get the subject
List<SAML11StatementAbstractType> statements = assertion.getStatements();
for (SAML11StatementAbstractType statement : statements) {
if (statement instanceof SAML11AuthenticationStatementType) {
SAML11AuthenticationStatementType subStat = (SAML11AuthenticationStatementType) statement;
SAML11SubjectType subject = subStat.getSubject();
principal = new SerializablePrincipal(subject.getChoice().getNameID().getValue());
}
}
roles = AssertionUtil.getRoles(assertion, null);
}
String username = principal.getName();
String password = EMPTY_PASSWORD;
if (logger.isTraceEnabled()) {
logger.trace("Roles determined for username=" + username + "=" + Arrays.toString(roles.toArray()));
}
ServiceProviderSAMLContext.push(username, roles);
//TODO: figure out getting the principal via authentication
IdentityManager identityManager = securityContext.getIdentityManager();
final Principal userPrincipal = principal;
Account account = new AccountImpl(userPrincipal, new HashSet<String>(roles), password);
account = identityManager.verify(account);
//Register the principal with the request
register(securityContext, account);
if (enableAudit) {
PicketLinkAuditEvent auditEvent = new PicketLinkAuditEvent(AuditLevel.INFO);
auditEvent.setType(PicketLinkAuditEventType.RESPONSE_FROM_IDP);
auditEvent.setSubjectName(username);
auditEvent.setWhoIsAuditing(servletContext.getContextPath());
auditHelper.audit(auditEvent);
}
return AuthenticationMechanismOutcome.AUTHENTICATED;
} catch (Exception e) {
logger.samlSPHandleRequestError(e);
}
}
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
}