/*
* 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.servlet.oauth;
import com.novell.ldapchai.exception.ChaiUnavailableException;
import password.pwm.AppProperty;
import password.pwm.PwmApplication;
import password.pwm.PwmConstants;
import password.pwm.bean.UserIdentity;
import password.pwm.config.Configuration;
import password.pwm.config.profile.ForgottenPasswordProfile;
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.PwmRequest;
import password.pwm.http.PwmSession;
import password.pwm.http.PwmURL;
import password.pwm.http.servlet.AbstractPwmServlet;
import password.pwm.http.servlet.PwmServletDefinition;
import password.pwm.http.servlet.forgottenpw.ForgottenPasswordServlet;
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.util.java.JavaHelper;
import password.pwm.util.java.JsonUtil;
import password.pwm.util.logging.PwmLogger;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@WebServlet(
name="OAuthConsumerServlet",
urlPatterns = {
PwmConstants.URL_PREFIX_PUBLIC + "/oauth"
}
)
public class OAuthConsumerServlet extends AbstractPwmServlet {
private static final PwmLogger LOGGER = PwmLogger.forClass(OAuthConsumerServlet.class);
@Override
protected ProcessAction readProcessAction(final PwmRequest request)
throws PwmUnrecoverableException
{
return null;
}
@Override
protected void processAction(final PwmRequest pwmRequest)
throws ServletException, IOException, ChaiUnavailableException, PwmUnrecoverableException
{
final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
final Configuration config = pwmRequest.getConfig();
final PwmSession pwmSession = pwmRequest.getPwmSession();
final boolean userIsAuthenticated = pwmSession.isAuthenticated();
final Optional<OAuthRequestState> oAuthRequestState = OAuthMachine.readOAuthRequestState(pwmRequest);
final OAuthUseCase oAuthUseCaseCase = oAuthRequestState.isPresent()
? oAuthRequestState.get().getoAuthState().getUseCase()
: OAuthUseCase.Authentication;
LOGGER.trace(pwmRequest, "processing oauth return request, useCase=" + oAuthUseCaseCase + ", incoming oAuthRequestState="
+ (oAuthRequestState.isPresent() ? JsonUtil.serialize(oAuthRequestState.get()) : "none")
);
// make sure it's okay to be processing this request.
switch (oAuthUseCaseCase) {
case Authentication: {
if (!userIsAuthenticated && !pwmSession.getSessionStateBean().isOauthInProgress()) {
if (oAuthRequestState.isPresent()) {
final String nextUrl = oAuthRequestState.get().getoAuthState().getNextUrl();
LOGGER.debug(pwmSession, "received unrecognized oauth response, ignoring authcode and redirecting to embedded next url: " + nextUrl);
pwmRequest.sendRedirect(nextUrl);
return;
}
final String errorMsg = "oauth consumer reached, but oauth authentication has not yet been initiated.";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR,errorMsg);
pwmRequest.respondWithError(errorInformation);
LOGGER.error(pwmSession, errorMsg);
return;
}
}
break;
default:
// for non-auth requests its okay to continue
break;
}
// check if there is an "error" on the request sent from the oauth server., if there is then halt.
{
final String oauthRequestError = pwmRequest.readParameterAsString("error");
if (oauthRequestError != null && !oauthRequestError.isEmpty()) {
final String errorMsg = "incoming request from remote oauth server is indicating an error: " + oauthRequestError;
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR, errorMsg, "Remote Error: " + oauthRequestError, null);
LOGGER.error(pwmSession, errorMsg);
pwmRequest.respondWithError(errorInformation);
return;
}
}
// check if user is already authenticated - shouldn't be in nominal usage.
if (userIsAuthenticated) {
switch (oAuthUseCaseCase) {
case Authentication:
LOGGER.debug(pwmSession, "oauth consumer reached, but user is already authenticated; will proceed and verify authcode matches current user identity.");
break;
case ForgottenPassword:
final String errorMsg = "oauth consumer reached via " + OAuthUseCase.ForgottenPassword + ", but user is already authenticated";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR,errorMsg);
pwmRequest.respondWithError(errorInformation);
LOGGER.error(pwmSession,errorMsg);
return;
default:
JavaHelper.unhandledSwitchStatement(oAuthUseCaseCase);
}
}
// mark the inprogress flag to false, if we get this far and fail user needs to start over.
pwmSession.getSessionStateBean().setOauthInProgress(false);
if (!oAuthRequestState.isPresent()) {
final String errorMsg = "state parameter is missing from oauth request";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR,errorMsg);
LOGGER.error(pwmSession,errorMsg);
pwmRequest.respondWithError(errorInformation);
return;
}
final OAuthState oauthState = oAuthRequestState.get().getoAuthState();
final OAuthSettings oAuthSettings = makeOAuthSettings(pwmRequest, oauthState);
final OAuthMachine oAuthMachine = new OAuthMachine(oAuthSettings);
// make sure request was initiated in users current session
if (!oAuthRequestState.get().isSessionMatch()) {
try{
switch (oAuthUseCaseCase) {
case Authentication:
LOGGER.debug(pwmSession, "oauth consumer reached but response is not for a request issued during the current session, will redirect back to oauth server for verification update");
final String nextURL = oauthState.getNextUrl();
oAuthMachine.redirectUserToOAuthServer(pwmRequest, nextURL, null, null);
return;
case ForgottenPassword:
LOGGER.debug(pwmSession, "oauth consumer reached but response is not for a request issued during the current session, will redirect back to forgotten password servlet");
pwmRequest.sendRedirect(PwmServletDefinition.ForgottenPassword);
return;
default:
JavaHelper.unhandledSwitchStatement(oAuthUseCaseCase);
}
} catch (PwmUnrecoverableException e) {
final String errorMsg = "unexpected error redirecting user to oauth page: " + e.toString();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR, errorMsg);
setLastError(pwmRequest, errorInformation);
LOGGER.error(errorInformation.toDebugStr());
}
}
final String requestCodeStr = pwmRequest.readParameterAsString(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_CODE));
LOGGER.trace(pwmSession,"received code from oauth server: " + requestCodeStr);
final OAuthResolveResults resolveResults;
try {
resolveResults = oAuthMachine.makeOAuthResolveRequest(pwmRequest, requestCodeStr);
} catch (IOException | PwmException e) {
final ErrorInformation errorInformation;
final String errorMsg = "unexpected error communicating with oauth server: " + e.toString();
if (e instanceof PwmException) {
errorInformation = new ErrorInformation(((PwmException) e).getError(),errorMsg);
} else {
errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg);
}
setLastError(pwmRequest, errorInformation);
LOGGER.error(errorInformation.toDebugStr());
return;
}
if (resolveResults == null || resolveResults.getAccessToken() == null || resolveResults.getAccessToken().isEmpty()) {
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR,"browser redirect from oauth server did not include an access token");
LOGGER.error(pwmRequest, errorInformation);
pwmRequest.respondWithError(errorInformation);
return;
}
if (resolveResults.getExpiresSeconds() > 0) {
if (resolveResults.getRefreshToken() == null || resolveResults.getRefreshToken().isEmpty()) {
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR,"oauth server gave expiration for access token, but did not provide a refresh token");
LOGGER.error(pwmRequest, errorInformation);
pwmRequest.respondWithError(errorInformation);
return;
}
}
final String oauthSuppliedUsername;
{
final String getAttributeResponseBodyStr = oAuthMachine.makeOAuthGetAttributeRequest(pwmRequest, resolveResults.getAccessToken());
final Map<String, String> getAttributeResultValues = JsonUtil.deserializeStringMap(getAttributeResponseBodyStr);
oauthSuppliedUsername = getAttributeResultValues.get(oAuthSettings.getDnAttributeName());
if (oauthSuppliedUsername == null || oauthSuppliedUsername.isEmpty()) {
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR,"OAuth server did not respond with an username attribute value");
LOGGER.error(pwmRequest, errorInformation);
pwmRequest.respondWithError(errorInformation);
return;
}
}
LOGGER.debug(pwmSession, "received user login id value from OAuth server: " + oauthSuppliedUsername);
if (oAuthUseCaseCase == OAuthUseCase.ForgottenPassword) {
redirecToForgottenPasswordServlet(pwmRequest, oauthSuppliedUsername);
return;
}
if (userIsAuthenticated) {
try {
final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
final UserIdentity resolvedIdentity = userSearchEngine.resolveUsername(
oauthSuppliedUsername,
null,
null,
pwmSession.getLabel()
);
if (resolvedIdentity != null && resolvedIdentity.canonicalEquals(pwmSession.getUserInfoBean().getUserIdentity(),pwmApplication)) {
LOGGER.debug(pwmSession, "verified incoming oauth code for already authenticated session does resolve to same as logged in user");
} else {
final String errorMsg = "incoming oauth code for already authenticated session does not resolve to same as logged in user ";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR,errorMsg);
LOGGER.error(pwmSession,errorMsg);
pwmRequest.respondWithError(errorInformation);
pwmSession.unauthenticateUser(pwmRequest);
return;
}
} catch (PwmOperationalException e) {
final String errorMsg = "error while examining incoming oauth code for already authenticated session: " + e.getMessage();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR,errorMsg);
LOGGER.error(pwmSession,errorMsg);
pwmRequest.respondWithError(errorInformation);
return;
}
}
try {
if (!userIsAuthenticated) {
final SessionAuthenticator sessionAuthenticator = new SessionAuthenticator(pwmApplication, pwmSession, PwmAuthenticationSource.OAUTH);
sessionAuthenticator.authUserWithUnknownPassword(oauthSuppliedUsername, AuthenticationType.AUTH_WITHOUT_PASSWORD);
}
// recycle the session to prevent session fixation attack.
pwmRequest.getPwmSession().getSessionStateBean().setSessionIdRecycleNeeded(true);
// forward to nextUrl
final String nextUrl = oauthState.getNextUrl();
LOGGER.debug(pwmSession, "oauth authentication completed, redirecting to originally requested URL: " + nextUrl);
pwmRequest.sendRedirect(nextUrl);
} catch (PwmException e) {
LOGGER.error(pwmSession, "error during OAuth authentication attempt: " + e.getMessage());
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR, e.getMessage());
pwmRequest.respondWithError(errorInformation);
return;
}
LOGGER.trace(pwmSession, "OAuth login sequence successfully completed");
}
private static OAuthSettings makeOAuthSettings(final PwmRequest pwmRequest, final OAuthState oAuthState) throws IOException, ServletException, PwmUnrecoverableException {
final OAuthUseCase oAuthUseCase = oAuthState.getUseCase();
switch (oAuthUseCase) {
case Authentication:
return OAuthSettings.forSSOAuthentication(pwmRequest.getConfig());
case ForgottenPassword:
final String profileId = oAuthState.getForgottenProfileId();
final ForgottenPasswordProfile profile = pwmRequest.getConfig().getForgottenPasswordProfiles().get(profileId);
return OAuthSettings.forForgottenPassword(profile);
default:
JavaHelper.unhandledSwitchStatement(oAuthUseCase);
}
final String errorMsg = "unable to calculate oauth settings for incoming request state";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR,errorMsg);
LOGGER.error(pwmRequest,errorMsg);
throw new PwmUnrecoverableException(errorInformation);
}
private void redirecToForgottenPasswordServlet(final PwmRequest pwmRequest, final String oauthSuppliedUsername) throws IOException, PwmUnrecoverableException {
final OAuthForgottenPasswordResults results = new OAuthForgottenPasswordResults(true, oauthSuppliedUsername);
final String encryptedResults = pwmRequest.getPwmApplication().getSecureService().encryptObjectToString(results);
final Map<String,String> httpParams = new HashMap<>();
httpParams.put(PwmConstants.PARAM_RECOVERY_OAUTH_RESULT, encryptedResults);
httpParams.put(PwmConstants.PARAM_ACTION_REQUEST, ForgottenPasswordServlet.ForgottenPasswordAction.oauthReturn.toString());
final String nextUrl = pwmRequest.getContextPath() + PwmServletDefinition.ForgottenPassword.servletUrl();
final String redirectUrl = PwmURL.appendAndEncodeUrlParameters(nextUrl,httpParams);
LOGGER.debug(pwmRequest, "forgotten password oauth sequence complete, redirecting to forgotten password with result data: " + JsonUtil.serialize(results));
pwmRequest.sendRedirect(redirectUrl);
}
}