/*
* Password Management Servlets (PWM)
* http://www.pwm-project.org
*
* Copyright (c) 2006-2009 Novell, Inc.
* Copyright (c) 2009-2017 The PWM Project
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package password.pwm.http.filter;
import com.novell.ldapchai.exception.ChaiUnavailableException;
import password.pwm.AppProperty;
import password.pwm.PwmApplication;
import password.pwm.PwmApplicationMode;
import password.pwm.PwmConstants;
import password.pwm.PwmHttpFilterAuthenticationProvider;
import password.pwm.bean.LoginInfoBean;
import password.pwm.bean.UserIdentity;
import password.pwm.bean.UserInfoBean;
import password.pwm.config.PwmSetting;
import password.pwm.error.ErrorInformation;
import password.pwm.error.PwmError;
import password.pwm.error.PwmException;
import password.pwm.error.PwmOperationalException;
import password.pwm.error.PwmUnrecoverableException;
import password.pwm.http.HttpHeader;
import password.pwm.http.ProcessStatus;
import password.pwm.http.PwmHttpResponseWrapper;
import password.pwm.http.PwmRequest;
import password.pwm.http.PwmSession;
import password.pwm.http.PwmURL;
import password.pwm.http.bean.ChangePasswordBean;
import password.pwm.http.servlet.LoginServlet;
import password.pwm.http.servlet.PwmServletDefinition;
import password.pwm.http.servlet.oauth.OAuthMachine;
import password.pwm.http.servlet.oauth.OAuthSettings;
import password.pwm.i18n.Display;
import password.pwm.ldap.PasswordChangeProgressChecker;
import password.pwm.ldap.search.UserSearchEngine;
import password.pwm.ldap.auth.AuthenticationType;
import password.pwm.ldap.auth.PwmAuthenticationSource;
import password.pwm.ldap.auth.SessionAuthenticator;
import password.pwm.svc.stats.Statistic;
import password.pwm.svc.stats.StatisticsManager;
import password.pwm.util.BasicAuthInfo;
import password.pwm.util.LocaleHelper;
import password.pwm.util.logging.PwmLogger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.Serializable;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* Authentication servlet filter. This filter wraps all servlet requests and requests direct to *.jsp
* URLs and provides user authentication services. Users must provide valid credentials to login. This
* filter checks for a Basic Authorization header in the request and will attempt to use that to validate
* the user, if not, then the user will be passed to a form based login page (LoginServlet;
*
* @author Jason D. Rivard
*/
public class AuthenticationFilter extends AbstractPwmFilter {
// ------------------------------ FIELDS ------------------------------
private static final PwmLogger LOGGER = PwmLogger.getLogger(AuthenticationFilter.class.getName());
public void processFilter(
final PwmApplicationMode mode,
final PwmRequest pwmRequest,
final PwmFilterChain chain
)
throws IOException, ServletException
{
final PwmURL pwmURL = pwmRequest.getURL();
if (pwmURL.isPublicUrl() && !pwmURL.isLoginServlet()) {
chain.doFilter();
return;
}
try {
final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
final PwmSession pwmSession = pwmRequest.getPwmSession();
if (pwmApplication.getApplicationMode() == PwmApplicationMode.NEW) {
if (pwmRequest.getURL().isConfigGuideURL()) {
chain.doFilter();
return;
}
}
if (pwmApplication.getApplicationMode() == PwmApplicationMode.CONFIGURATION) {
if (pwmRequest.getURL().isConfigManagerURL()) {
chain.doFilter();
return;
}
}
//user is already authenticated
if (pwmSession.isAuthenticated()) {
this.processAuthenticatedSession(pwmRequest, chain);
} else {
this.processUnAuthenticatedSession(pwmRequest, chain);
}
} catch (PwmUnrecoverableException e) {
LOGGER.error(e.toString());
throw new ServletException(e.toString());
}
}
@Override
boolean isInterested(final PwmApplicationMode mode, final PwmURL pwmURL) {
return !pwmURL.isResourceURL();
}
private void processAuthenticatedSession(
final PwmRequest pwmRequest,
final PwmFilterChain chain
)
throws IOException, ServletException, PwmUnrecoverableException
{
final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
final PwmSession pwmSession = pwmRequest.getPwmSession();
// read the basic auth info out of the header (if it exists);
if (pwmRequest.getConfig().readSettingAsBoolean(PwmSetting.BASIC_AUTH_ENABLED)) {
final BasicAuthInfo basicAuthInfo = BasicAuthInfo.parseAuthHeader(pwmApplication, pwmRequest);
final BasicAuthInfo originalBasicAuthInfo = pwmSession.getLoginInfoBean().getBasicAuth();
//check to make sure basic auth info is same as currently known user in session.
if (basicAuthInfo != null && originalBasicAuthInfo != null && !(originalBasicAuthInfo.equals(basicAuthInfo))) {
// if we read here then user is using basic auth, and header has changed since last request
// this means something is screwy, so log out the session
// read the current user info for logging
final UserInfoBean uiBean = pwmSession.getUserInfoBean();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_BAD_SESSION, "basic auth header user '" + basicAuthInfo.getUsername() + "' does not match currently logged in user '" + uiBean.getUserIdentity() + "', session will be logged out");
LOGGER.info(pwmRequest, errorInformation);
// log out their user
pwmSession.unauthenticateUser(pwmRequest);
// send en error to user.
pwmRequest.respondWithError(errorInformation, true);
return;
}
}
// check status of oauth expiration
if (pwmSession.getLoginInfoBean().getOauthExp() != null) {
final OAuthSettings oauthSettings = OAuthSettings.forSSOAuthentication(pwmRequest.getConfig());
final OAuthMachine oAuthMachine = new OAuthMachine(oauthSettings);
if (oAuthMachine.checkOAuthExpiration(pwmRequest)) {
pwmRequest.respondWithError(new ErrorInformation(PwmError.ERROR_OAUTH_ERROR,"oauth access token has expired"));
return;
}
}
handleAuthenticationCookie(pwmRequest);
if (forceRequiredRedirects(pwmRequest) == ProcessStatus.Halt) {
return;
}
// user session is authed, and session and auth header match, so forward request on.
chain.doFilter();
}
private static void handleAuthenticationCookie(final PwmRequest pwmRequest) {
if (!pwmRequest.isAuthenticated() || pwmRequest.getPwmSession().getLoginInfoBean().getType() != AuthenticationType.AUTHENTICATED) {
return;
}
if (pwmRequest.getPwmSession().getLoginInfoBean().isLoginFlag(LoginInfoBean.LoginFlag.authRecordSet)) {
return;
}
pwmRequest.getPwmSession().getLoginInfoBean().setFlag(LoginInfoBean.LoginFlag.authRecordSet);
final String cookieName = pwmRequest.getConfig().readAppProperty(AppProperty.HTTP_COOKIE_AUTHRECORD_NAME);
if (cookieName == null || cookieName.isEmpty()) {
LOGGER.debug(pwmRequest, "skipping auth record cookie set, cookie name parameter is blank" );
return;
}
final int cookieAgeSeconds = Integer.parseInt(pwmRequest.getConfig().readAppProperty(AppProperty.HTTP_COOKIE_AUTHRECORD_AGE));
if (cookieAgeSeconds < 1) {
LOGGER.debug(pwmRequest, "skipping auth record cookie set, cookie age parameter is less than 1" );
return;
}
final Instant authTime = pwmRequest.getPwmSession().getLoginInfoBean().getAuthTime();
final String userGuid = pwmRequest.getPwmSession().getUserInfoBean().getUserGuid();
final AuthRecord authRecord = new AuthRecord(authTime, userGuid);
try {
pwmRequest.getPwmResponse().writeEncryptedCookie(cookieName, authRecord, cookieAgeSeconds, PwmHttpResponseWrapper.CookiePath.Application);
LOGGER.debug(pwmRequest,"wrote auth record cookie to user browser for use during forgotten password");
} catch (PwmUnrecoverableException e) {
LOGGER.error(pwmRequest, "error while setting authentication record cookie: " + e.getMessage());
}
}
public static class AuthRecord implements Serializable {
private Instant date;
private String guid;
public AuthRecord(final Instant date, final String guid) {
this.date = date;
this.guid = guid;
}
public Instant getDate() {
return date;
}
public String getGuid() {
return guid;
}
}
private void processUnAuthenticatedSession(
final PwmRequest pwmRequest,
final PwmFilterChain chain
)
throws IOException, ServletException, PwmUnrecoverableException
{
final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
final PwmSession pwmSession = pwmRequest.getPwmSession();
final HttpServletRequest req = pwmRequest.getHttpServletRequest();
final boolean bypassSso = pwmRequest.getPwmSession().getLoginInfoBean().isLoginFlag(LoginInfoBean.LoginFlag.noSso);
if (!bypassSso && pwmRequest.getPwmApplication().getApplicationMode() == PwmApplicationMode.RUNNING) {
final ProcessStatus authenticationProcessStatus = attemptAuthenticationMethods(pwmRequest);
if (authenticationProcessStatus == ProcessStatus.Halt) {
return;
}
}
final String originalRequestedUrl = pwmRequest.getURLwithQueryString();
if (pwmRequest.isAuthenticated()) {
// redirect back to self so request starts over as authenticated.
LOGGER.trace(pwmRequest, "inline authentication occurred during this request, redirecting to current url to restart request");
pwmRequest.getPwmResponse().sendRedirect(originalRequestedUrl);
return;
}
// handle if authenticated during filter process.
if (pwmSession.isAuthenticated()) {
pwmSession.getSessionStateBean().setSessionIdRecycleNeeded(true);
LOGGER.debug(pwmSession,"session authenticated during request, issuing redirect to originally requested url: " + originalRequestedUrl);
pwmRequest.sendRedirect(originalRequestedUrl);
return;
}
if (pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.BASIC_AUTH_FORCE)) {
final String displayMessage = LocaleHelper.getLocalizedMessage(Display.Title_Application, pwmRequest);
pwmRequest.getPwmResponse().setHeader(HttpHeader.WWW_Authenticate,"Basic realm=\"" + displayMessage + "\"");
pwmRequest.getPwmResponse().setStatus(401);
return;
}
if (pwmRequest.getURL().isLoginServlet()) {
chain.doFilter();
return;
}
//user is not authenticated so forward to LoginPage.
LOGGER.trace(pwmSession.getLabel(), "user requested resource requiring authentication (" + req.getRequestURI() + "), but is not authenticated; redirecting to LoginServlet");
LoginServlet.redirectToLoginServlet(pwmRequest);
}
public static ProcessStatus attemptAuthenticationMethods(final PwmRequest pwmRequest) throws IOException, ServletException {
final Set<AuthenticationMethod> authenticationMethods = new HashSet<>(Arrays.asList(AuthenticationMethod.values()));
{
final String casUrl = pwmRequest.getConfig().readSettingAsString(PwmSetting.CAS_CLEAR_PASS_URL);
if (casUrl == null || casUrl.trim().isEmpty()) {
authenticationMethods.remove(AuthenticationMethod.CAS);
}
}
for (final AuthenticationMethod authenticationMethod : authenticationMethods) {
if (!pwmRequest.isAuthenticated()) {
try {
final Class<? extends PwmHttpFilterAuthenticationProvider> clazz = authenticationMethod.getImplementationClass();
final PwmHttpFilterAuthenticationProvider filterAuthenticationProvider = clazz.newInstance();
filterAuthenticationProvider.attemptAuthentication(pwmRequest);
if (pwmRequest.isAuthenticated()) {
LOGGER.trace(pwmRequest, "authentication provided by method " + clazz.getName());
}
if (filterAuthenticationProvider.hasRedirectedResponse()) {
LOGGER.trace(pwmRequest, "authentication provider " + clazz.getName() + " has issued a redirect, halting authentication process");
return ProcessStatus.Halt;
}
} catch (Exception e) {
final ErrorInformation errorInformation;
if (e instanceof PwmException) {
final String erorrMessage = "error during " + authenticationMethod + " authentication attempt: " + e.getMessage();
errorInformation = new ErrorInformation(((PwmException) e).getError(), erorrMessage);
} else {
errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, e.getMessage());
}
LOGGER.error(pwmRequest, errorInformation);
pwmRequest.respondWithError(errorInformation);
return ProcessStatus.Halt;
}
}
}
return ProcessStatus.Continue;
}
public static ProcessStatus forceRequiredRedirects(
final PwmRequest pwmRequest
)
throws PwmUnrecoverableException, IOException
{
final PwmSession pwmSession = pwmRequest.getPwmSession();
final PwmURL pwmURL = pwmRequest.getURL();
final UserInfoBean uiBean = pwmSession.getUserInfoBean();
if (pwmURL.isResourceURL() || pwmURL.isConfigManagerURL() || pwmURL.isLogoutURL() || pwmURL.isLoginServlet()) {
return ProcessStatus.Continue;
}
if (pwmRequest.getPwmApplication().getApplicationMode() != PwmApplicationMode.RUNNING) {
return ProcessStatus.Continue;
}
// high priority pw change
if (pwmRequest.getPwmSession().getLoginInfoBean().getType() == AuthenticationType.AUTH_FROM_PUBLIC_MODULE) {
if (!pwmURL.isChangePasswordURL()) {
LOGGER.debug(pwmRequest, "user is authenticated via forgotten password mechanism, redirecting to change password servlet");
pwmRequest.sendRedirect(
pwmRequest.getContextPath()
+ PwmConstants.URL_PREFIX_PUBLIC
+ "/"
+ PwmServletDefinition.PrivateChangePassword.servletUrlName());
return ProcessStatus.Halt;
} else {
return ProcessStatus.Continue;
}
}
// if change password in progress and req is for ChangePassword servlet, then allow request as is
if (pwmURL.isChangePasswordURL()) {
final ChangePasswordBean cpb = pwmRequest.getPwmApplication().getSessionStateService().getBean(pwmRequest, ChangePasswordBean.class);
final PasswordChangeProgressChecker.ProgressTracker progressTracker = cpb.getChangeProgressTracker();
if (progressTracker != null && progressTracker.getBeginTime() != null) {
return ProcessStatus.Continue;
}
}
if (uiBean.isRequiresResponseConfig()) {
if (!pwmURL.isSetupResponsesURL()) {
LOGGER.debug(pwmRequest, "user is required to setup responses, redirecting to setup responses servlet");
pwmRequest.sendRedirect(PwmServletDefinition.SetupResponses);
return ProcessStatus.Halt;
} else {
return ProcessStatus.Continue;
}
}
if (uiBean.isRequiresOtpConfig() && !pwmSession.getLoginInfoBean().isLoginFlag(LoginInfoBean.LoginFlag.skipOtp)) {
if (!pwmURL.isSetupOtpSecretURL()) {
LOGGER.debug(pwmRequest, "user is required to setup OTP configuration, redirecting to OTP setup page");
pwmRequest.sendRedirect(PwmServletDefinition.SetupOtp);
return ProcessStatus.Halt;
} else {
return ProcessStatus.Continue;
}
}
if (uiBean.isRequiresUpdateProfile()) {
if (!pwmURL.isProfileUpdateURL()) {
LOGGER.debug(pwmRequest, "user is required to update profile, redirecting to profile update servlet");
pwmRequest.sendRedirect(PwmServletDefinition.UpdateProfile);
return ProcessStatus.Halt;
} else {
return ProcessStatus.Continue;
}
}
if (uiBean.isRequiresNewPassword() && !pwmSession.getLoginInfoBean().isLoginFlag(LoginInfoBean.LoginFlag.skipNewPw)) {
if (!pwmURL.isChangePasswordURL()) {
LOGGER.debug(pwmRequest, "user password requires changing, redirecting to change password servlet");
pwmRequest.sendRedirect(PwmServletDefinition.PrivateChangePassword);
return ProcessStatus.Halt;
} else {
return ProcessStatus.Continue;
}
}
return ProcessStatus.Continue;
}
enum AuthenticationMethod {
BASIC_AUTH(BasicFilterAuthenticationProvider.class.getName()),
SSO_AUTH_HEADER(SSOHeaderFilterAuthenticationProvider.class.getName()),
CAS("password.pwm.util.CASFilterAuthenticationProvider"),
OAUTH(OAuthFilterAuthenticationProvider.class.getName())
;
private final String className;
AuthenticationMethod(final String className) {
this.className = className;
}
public Class<? extends PwmHttpFilterAuthenticationProvider> getImplementationClass() throws PwmUnrecoverableException {
try {
return (Class<? extends PwmHttpFilterAuthenticationProvider>) Class.forName(className);
} catch (ClassNotFoundException | ClassCastException e) {
final String errorMsg = "error loading authentication method: " + this.getImplementationClass() + ", error: " + e.getMessage();
LOGGER.error(errorMsg,e);
throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN,errorMsg));
}
}
}
public static class BasicFilterAuthenticationProvider implements PwmHttpFilterAuthenticationProvider {
@Override
public void attemptAuthentication(
final PwmRequest pwmRequest
)
throws PwmUnrecoverableException
{
if (!pwmRequest.getConfig().readSettingAsBoolean(PwmSetting.BASIC_AUTH_ENABLED)) {
return;
}
if (pwmRequest.isAuthenticated()) {
return;
}
final BasicAuthInfo basicAuthInfo = BasicAuthInfo.parseAuthHeader(pwmRequest.getPwmApplication(), pwmRequest);
if (basicAuthInfo == null) {
return;
}
try {
final PwmSession pwmSession = pwmRequest.getPwmSession();
final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
//user isn't already authenticated and has an auth header, so try to auth them.
LOGGER.debug(pwmSession, "attempting to authenticate user using basic auth header (username=" + basicAuthInfo.getUsername() + ")");
final SessionAuthenticator sessionAuthenticator = new SessionAuthenticator(
pwmApplication,
pwmSession,
PwmAuthenticationSource.BASIC_AUTH
);
final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
final UserIdentity userIdentity = userSearchEngine.resolveUsername(basicAuthInfo.getUsername(), null, null, pwmSession.getLabel());
sessionAuthenticator.authenticateUser(userIdentity, basicAuthInfo.getPassword());
pwmSession.getLoginInfoBean().setBasicAuth(basicAuthInfo);
} catch (ChaiUnavailableException e) {
StatisticsManager.incrementStat(pwmRequest, Statistic.LDAP_UNAVAILABLE_COUNT);
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_DIRECTORY_UNAVAILABLE, e.getMessage());
throw new PwmUnrecoverableException(errorInformation);
} catch (PwmException e) {
throw new PwmUnrecoverableException(e.getError());
}
}
@Override
public boolean hasRedirectedResponse() {
return false;
}
}
static class SSOHeaderFilterAuthenticationProvider implements PwmHttpFilterAuthenticationProvider {
@Override
public void attemptAuthentication(
final PwmRequest pwmRequest
)
throws PwmUnrecoverableException
{
{
final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
final PwmSession pwmSession = pwmRequest.getPwmSession();
final String headerName = pwmApplication.getConfig().readSettingAsString(PwmSetting.SSO_AUTH_HEADER_NAME);
if (headerName == null || headerName.length() < 1) {
return;
}
final String headerValue = pwmRequest.readHeaderValueAsString(headerName);
if (headerValue == null || headerValue.length() < 1) {
return;
}
LOGGER.debug(pwmRequest, "SSO Authentication header present in request, will search for user value of '" + headerValue + "'");
final SessionAuthenticator sessionAuthenticator = new SessionAuthenticator(
pwmApplication,
pwmSession,
PwmAuthenticationSource.SSO_HEADER
);
try {
sessionAuthenticator.authUserWithUnknownPassword(headerValue, AuthenticationType.AUTH_WITHOUT_PASSWORD);
} catch (PwmOperationalException e) {
throw new PwmUnrecoverableException(e.getErrorInformation());
}
}
}
@Override
public boolean hasRedirectedResponse() {
return false;
}
}
static class OAuthFilterAuthenticationProvider implements PwmHttpFilterAuthenticationProvider {
private boolean redirected = false;
public void attemptAuthentication(
final PwmRequest pwmRequest
)
throws PwmUnrecoverableException, IOException
{
final OAuthSettings oauthSettings = OAuthSettings.forSSOAuthentication(pwmRequest.getConfig());
if (!oauthSettings.oAuthIsConfigured()) {
return;
}
final String originalURL = pwmRequest.getURLwithQueryString();
final OAuthMachine oAuthMachine = new OAuthMachine(oauthSettings);
oAuthMachine.redirectUserToOAuthServer(pwmRequest, originalURL, null,null);
redirected = true;
}
@Override
public boolean hasRedirectedResponse() {
return redirected;
}
}
}