password.pwm.http.servlet.OAuthConsumerServlet.java Source code

Java tutorial

Introduction

Here is the source code for password.pwm.http.servlet.OAuthConsumerServlet.java

Source

/*
 * Password Management Servlets (PWM)
 * http://code.google.com/p/pwm/
 *
 * Copyright (c) 2006-2009 Novell, Inc.
 * Copyright (c) 2009-2015 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;

import com.novell.ldapchai.exception.ChaiUnavailableException;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.util.EntityUtils;
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.PwmSetting;
import password.pwm.error.*;
import password.pwm.http.PwmRequest;
import password.pwm.http.PwmSession;
import password.pwm.http.ServletHelper;
import password.pwm.http.bean.LoginInfoBean;
import password.pwm.http.client.PwmHttpClient;
import password.pwm.http.client.PwmHttpClientConfiguration;
import password.pwm.ldap.UserSearchEngine;
import password.pwm.ldap.auth.AuthenticationType;
import password.pwm.ldap.auth.PwmAuthenticationSource;
import password.pwm.ldap.auth.SessionAuthenticator;
import password.pwm.util.BasicAuthInfo;
import password.pwm.util.JsonUtil;
import password.pwm.util.PasswordData;
import password.pwm.util.TimeDuration;
import password.pwm.util.logging.PwmLogger;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@WebServlet(name = "OAuthConsumerServlet", urlPatterns = { PwmConstants.URL_PREFIX_PUBLIC + "/oauth" })
public class OAuthConsumerServlet extends AbstractPwmServlet {
    private static final PwmLogger LOGGER = PwmLogger.forClass(OAuthConsumerServlet.class);
    private static int oauthStateIdCounter = 0;

    private static class OAuthState implements Serializable {
        private final int stateID = oauthStateIdCounter++;
        private final Date issueTime = new Date();
        private String sessionID;
        private String nextUrl;
    }

    @Override
    protected ProcessAction readProcessAction(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 Settings settings = Settings.fromConfiguration(pwmApplication.getConfig());

        final boolean userIsAuthenticated = pwmSession.getSessionStateBean().isAuthenticated();
        final OAuthRequestState oAuthRequestState = readOAuthRequestState(pwmRequest);

        if (!userIsAuthenticated && !pwmSession.getSessionStateBean().isOauthInProgress()) {
            if (oAuthRequestState != null) {
                final String nextUrl = oAuthRequestState.oAuthState.nextUrl;
                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;
        }

        final String oauthRequestError = pwmRequest.readParameterAsString("error");
        if (oauthRequestError != null && !oauthRequestError.isEmpty()) {
            final String errorMsg = "error detected from oauth request parameter: " + oauthRequestError;
            final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR, errorMsg,
                    "Remote Error: " + oauthRequestError, null);
            LOGGER.error(pwmSession, errorMsg);
            pwmRequest.respondWithError(errorInformation);
            return;
        }

        if (userIsAuthenticated) {
            LOGGER.debug(pwmSession,
                    "oauth consumer reached, but user is already authenticated; will proceed and verify authcode matches current user identity.");
        }

        // mark the inprogress flag to false, if we get this far and fail user needs to start over.
        pwmSession.getSessionStateBean().setOauthInProgress(false);

        if (oAuthRequestState == null) {
            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;
        }

        if (!oAuthRequestState.sessionMatch) {
            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");

            try {
                final String nextURL = oAuthRequestState.oAuthState.nextUrl;
                redirectUserToOAuthServer(pwmRequest, nextURL);
                return;
            } catch (PwmUnrecoverableException e) {
                final String errorMsg = "unexpected error redirecting user to oauth page: " + e.toString();
                ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg);
                pwmRequest.setResponseError(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 = makeOAuthResolveRequest(pwmRequest, settings, 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);
            }
            pwmRequest.setResponseError(errorInformation);
            LOGGER.error(errorInformation.toDebugStr());
            return;
        }

        if (resolveResults == null || resolveResults.getAccessToken() == null
                || resolveResults.getAccessToken().isEmpty()) {
            final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR,
                    "OAuth server did not respond with 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 = makeOAuthGetAttributeRequest(pwmRequest,
                    resolveResults.getAccessToken(), settings);
            final Map<String, String> getAttributeResultValues = JsonUtil
                    .deserializeStringMap(getAttributeResponseBodyStr);
            oauthSuppliedUsername = getAttributeResultValues.get(settings.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 (userIsAuthenticated) {
            try {
                final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmRequest);
                final UserIdentity resolvedIdentity = userSearchEngine.resolveUsername(oauthSuppliedUsername, null,
                        null);
                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 = oAuthRequestState.oAuthState.nextUrl;
            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 OAuthResolveResults makeOAuthResolveRequest(final PwmRequest pwmRequest, final Settings settings,
            final String requestCode) throws IOException, PwmUnrecoverableException {
        final Configuration config = pwmRequest.getConfig();
        final String requestUrl = settings.getCodeResolveUrl();
        final String grant_type = config.readAppProperty(AppProperty.OAUTH_ID_ACCESS_GRANT_TYPE);
        final String redirect_uri = figureOauthSelfEndPointUrl(pwmRequest);
        final String clientID = settings.getClientID();

        final Map<String, String> requestParams = new HashMap<>();
        requestParams.put(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_CODE), requestCode);
        requestParams.put(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_GRANT_TYPE), grant_type);
        requestParams.put(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_REDIRECT_URI), redirect_uri);
        requestParams.put(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_CLIENT_ID), clientID);

        final RestResults restResults = makeHttpRequest(pwmRequest, "OAuth code resolver", settings, requestUrl,
                requestParams);
        final HttpResponse httpResponse = restResults.getHttpResponse();

        if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_OAUTH_ERROR,
                    "unexpected HTTP status code (" + httpResponse.getStatusLine().getStatusCode() + ")"));
        }

        final String resolveResponseBodyStr = restResults.getResponseBody();

        final Map<String, String> resolveResultValues = JsonUtil.deserializeStringMap(resolveResponseBodyStr);
        final OAuthResolveResults oAuthResolveResults = new OAuthResolveResults();

        oAuthResolveResults.setAccessToken(
                resolveResultValues.get(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_ACCESS_TOKEN)));
        oAuthResolveResults.setRefreshToken(
                resolveResultValues.get(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_REFRESH_TOKEN)));
        oAuthResolveResults.setExpiresSeconds(0);
        try {
            oAuthResolveResults.setExpiresSeconds(Integer.parseInt(
                    resolveResultValues.get(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_EXPIRES))));
        } catch (Exception e) {
            LOGGER.warn(pwmRequest, "error parsing oauth expires value in resolve request: " + e.getMessage());
        }

        return oAuthResolveResults;
    }

    public static boolean checkOAuthExpiration(final PwmRequest pwmRequest) {
        if (!Boolean.parseBoolean(pwmRequest.getConfig().readAppProperty(AppProperty.OAUTH_ENABLE_TOKEN_REFRESH))) {
            return false;
        }

        final LoginInfoBean loginInfoBean = pwmRequest.getPwmSession().getLoginInfoBean();
        final Date expirationDate = loginInfoBean.getOauthExpiration();

        if (expirationDate == null || (new Date()).before(expirationDate)) {
            //not expired
            return false;
        }

        LOGGER.trace(pwmRequest, "oauth access token has expired, attempting to refresh");

        final Settings settings = Settings.fromConfiguration(pwmRequest.getConfig());
        try {
            OAuthResolveResults resolveResults = makeOAuthRefreshRequest(pwmRequest, settings,
                    loginInfoBean.getOauthRefreshToken());
            if (resolveResults != null) {
                if (resolveResults.getExpiresSeconds() > 0) {
                    final Date accessTokenExpirationDate = new Date(
                            System.currentTimeMillis() + 1000 * resolveResults.getExpiresSeconds());
                    LOGGER.trace(pwmRequest, "noted oauth access token expiration at timestamp "
                            + PwmConstants.DEFAULT_DATETIME_FORMAT.format(accessTokenExpirationDate));
                    loginInfoBean.setOauthExpiration(accessTokenExpirationDate);
                    loginInfoBean.setOauthRefreshToken(resolveResults.getRefreshToken());
                    return false;
                }
            }
        } catch (PwmUnrecoverableException | IOException e) {
            LOGGER.error(pwmRequest, "error while processing oauth token refresh:" + e.getMessage());
        }
        LOGGER.error(pwmRequest, "unable to refresh oauth token for user, unauthenticated session");
        pwmRequest.getPwmSession().unauthenticateUser(pwmRequest);
        return true;
    }

    private static OAuthResolveResults makeOAuthRefreshRequest(final PwmRequest pwmRequest, final Settings settings,
            final String refreshCode) throws IOException, PwmUnrecoverableException {
        final Configuration config = pwmRequest.getConfig();
        final String requestUrl = settings.getCodeResolveUrl();
        final String grant_type = config.readAppProperty(AppProperty.OAUTH_ID_REFRESH_GRANT_TYPE);

        final Map<String, String> requestParams = new HashMap<>();
        requestParams.put(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_REFRESH_TOKEN), refreshCode);
        requestParams.put(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_GRANT_TYPE), grant_type);

        final RestResults restResults = makeHttpRequest(pwmRequest, "OAuth refresh resolver", settings, requestUrl,
                requestParams);
        final HttpResponse httpResponse = restResults.getHttpResponse();

        if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_OAUTH_ERROR,
                    "unexpected HTTP status code (" + httpResponse.getStatusLine().getStatusCode() + ")"));
        }

        final String resolveResponseBodyStr = restResults.getResponseBody();

        final Map<String, String> resolveResultValues = JsonUtil.deserializeStringMap(resolveResponseBodyStr);
        final OAuthResolveResults oAuthResolveResults = new OAuthResolveResults();

        oAuthResolveResults.setAccessToken(
                resolveResultValues.get(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_ACCESS_TOKEN)));
        oAuthResolveResults.setRefreshToken(refreshCode);
        oAuthResolveResults.setExpiresSeconds(0);
        try {
            oAuthResolveResults.setExpiresSeconds(Integer.parseInt(
                    resolveResultValues.get(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_EXPIRES))));
        } catch (Exception e) {
            LOGGER.warn(pwmRequest, "error parsing oauth expires value in resolve request: " + e.getMessage());
        }

        return oAuthResolveResults;
    }

    private static String makeOAuthGetAttributeRequest(final PwmRequest pwmRequest, final String accessToken,
            final Settings settings) throws IOException, PwmUnrecoverableException {
        final Configuration config = pwmRequest.getConfig();
        final String requestUrl = settings.getAttributesUrl();
        final Map<String, String> requestParams = new HashMap<>();
        requestParams.put(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_ACCESS_TOKEN), accessToken);
        requestParams.put(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_ATTRIBUTES),
                settings.getDnAttributeName());

        final RestResults restResults = makeHttpRequest(pwmRequest, "OAuth getattribute", settings, requestUrl,
                requestParams);
        final HttpResponse httpResponse = restResults.getHttpResponse();

        if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_OAUTH_ERROR,
                    "unexpected HTTP status code (" + httpResponse.getStatusLine().getStatusCode() + ")"));
        }

        return restResults.getResponseBody();
    }

    private static RestResults makeHttpRequest(final PwmRequest pwmRequest, final String debugText,
            final Settings settings, final String requestUrl, final Map<String, String> requestParams)
            throws IOException, PwmUnrecoverableException {
        final Date startTime = new Date();
        final String requestBody = ServletHelper.appendAndEncodeUrlParameters("", requestParams);
        LOGGER.trace(pwmRequest,
                "beginning " + debugText + " request to " + requestUrl + ", body: \n" + requestBody);
        final HttpPost httpPost = new HttpPost(requestUrl);
        httpPost.setHeader(PwmConstants.HttpHeader.Authorization.getHttpName(),
                new BasicAuthInfo(settings.getClientID(), settings.getSecret()).toAuthHeader());
        final StringEntity bodyEntity = new StringEntity(requestBody);
        bodyEntity.setContentType(PwmConstants.ContentTypeValue.form.getHeaderValue());
        httpPost.setEntity(bodyEntity);

        final X509Certificate[] certs = pwmRequest.getConfig()
                .readSettingAsCertificate(PwmSetting.OAUTH_ID_CERTIFICATE);
        final HttpResponse httpResponse;
        if (certs == null || certs.length == 0) {
            httpResponse = PwmHttpClient.getHttpClient(pwmRequest.getConfig()).execute(httpPost);
        } else {
            httpResponse = PwmHttpClient
                    .getHttpClient(pwmRequest.getConfig(), new PwmHttpClientConfiguration(certs)).execute(httpPost);
        }
        final String bodyResponse = EntityUtils.toString(httpResponse.getEntity());

        final StringBuilder debugOutput = new StringBuilder();
        debugOutput.append(debugText).append(TimeDuration.fromCurrent(startTime).asCompactString())
                .append(", status: ").append(httpResponse.getStatusLine()).append("\n");
        for (Header responseHeader : httpResponse.getAllHeaders()) {
            debugOutput.append(" response header: ").append(responseHeader.getName()).append(": ")
                    .append(responseHeader.getValue()).append("\n");
        }

        debugOutput.append(" body:\n ").append(bodyResponse);
        LOGGER.trace(pwmRequest, debugOutput.toString());
        return new RestResults(httpResponse, bodyResponse);
    }

    public static String figureOauthSelfEndPointUrl(final PwmRequest pwmRequest) {
        final HttpServletRequest req = pwmRequest.getHttpServletRequest();
        final String redirect_uri;
        try {
            final URI requestUri = new URI(req.getRequestURL().toString());
            redirect_uri = requestUri.getScheme() + "://" + requestUri.getHost()
                    + (requestUri.getPort() > 0 ? ":" + requestUri.getPort() : "")
                    + PwmServletDefinition.OAuthConsumer.servletUrl();
        } catch (URISyntaxException e) {
            throw new IllegalStateException(
                    "unable to parse inbound request uri while generating oauth redirect: " + e.getMessage());
        }
        return redirect_uri;
    }

    public static class Settings implements Serializable {
        private String loginURL;
        private String codeResolveUrl;
        private String attributesUrl;
        private String clientID;
        private PasswordData secret;
        private String dnAttributeName;

        public String getLoginURL() {
            return loginURL;
        }

        public String getCodeResolveUrl() {
            return codeResolveUrl;
        }

        public String getAttributesUrl() {
            return attributesUrl;
        }

        public String getClientID() {
            return clientID;
        }

        public PasswordData getSecret() {
            return secret;
        }

        public String getDnAttributeName() {
            return dnAttributeName;
        }

        public boolean oAuthIsConfigured() {
            return (loginURL != null && !loginURL.isEmpty())
                    && (codeResolveUrl != null && !codeResolveUrl.isEmpty())
                    && (attributesUrl != null && !attributesUrl.isEmpty())
                    && (clientID != null && !clientID.isEmpty()) && (secret != null)
                    && (dnAttributeName != null && !dnAttributeName.isEmpty());
        }

        public static Settings fromConfiguration(final Configuration config) {
            final Settings settings = new Settings();
            settings.loginURL = config.readSettingAsString(PwmSetting.OAUTH_ID_LOGIN_URL);
            settings.codeResolveUrl = config.readSettingAsString(PwmSetting.OAUTH_ID_CODERESOLVE_URL);
            settings.attributesUrl = config.readSettingAsString(PwmSetting.OAUTH_ID_ATTRIBUTES_URL);
            settings.clientID = config.readSettingAsString(PwmSetting.OAUTH_ID_CLIENTNAME);
            settings.secret = config.readSettingAsPassword(PwmSetting.OAUTH_ID_SECRET);
            settings.dnAttributeName = config.readSettingAsString(PwmSetting.OAUTH_ID_DN_ATTRIBUTE_NAME);
            return settings;
        }
    }

    static class RestResults {
        final HttpResponse httpResponse;
        final String responseBody;

        RestResults(HttpResponse httpResponse, String responseBody) {
            this.httpResponse = httpResponse;
            this.responseBody = responseBody;
        }

        public HttpResponse getHttpResponse() {
            return httpResponse;
        }

        public String getResponseBody() {
            return responseBody;
        }
    }

    static class OAuthResolveResults implements Serializable {
        private String accessToken;
        private int expiresSeconds;
        private String refreshToken;

        public String getAccessToken() {
            return accessToken;
        }

        public void setAccessToken(String accessToken) {
            this.accessToken = accessToken;
        }

        public int getExpiresSeconds() {
            return expiresSeconds;
        }

        public void setExpiresSeconds(int expiresSeconds) {
            this.expiresSeconds = expiresSeconds;
        }

        public String getRefreshToken() {
            return refreshToken;
        }

        public void setRefreshToken(String refreshToken) {
            this.refreshToken = refreshToken;
        }
    }

    public static String makeStateStringForRequest(final PwmRequest pwmRequest, final String nextUrl)
            throws PwmUnrecoverableException {
        OAuthState oAuthState = new OAuthState();
        oAuthState.sessionID = pwmRequest.getPwmSession().getSessionStateBean().getSessionVerificationKey();
        oAuthState.nextUrl = nextUrl;

        LOGGER.trace(pwmRequest, "issuing oauth state id=" + oAuthState.stateID
                + " with the next destination URL set to " + oAuthState.nextUrl);

        final String jsonValue = JsonUtil.serialize(oAuthState);
        return pwmRequest.getPwmApplication().getSecureService().encryptToString(jsonValue);
    }

    public static class OAuthRequestState {
        private OAuthState oAuthState;
        private boolean sessionMatch;

        public OAuthRequestState(OAuthState oAuthState, boolean sessionMatch) {
            this.oAuthState = oAuthState;
            this.sessionMatch = sessionMatch;
        }
    }

    public static void redirectUserToOAuthServer(final PwmRequest pwmRequest, final String nextUrl)
            throws PwmUnrecoverableException, IOException {

        LOGGER.trace(pwmRequest,
                "preparing to redirect user to oauth authentication service, setting nextUrl to " + nextUrl);
        pwmRequest.getPwmSession().getSessionStateBean().setOauthInProgress(true);

        final Configuration config = pwmRequest.getConfig();
        final OAuthConsumerServlet.Settings settings = OAuthConsumerServlet.Settings.fromConfiguration(config);
        final String state = OAuthConsumerServlet.makeStateStringForRequest(pwmRequest, nextUrl);
        final String redirectUri = OAuthConsumerServlet.figureOauthSelfEndPointUrl(pwmRequest);
        final String code = config.readAppProperty(AppProperty.OAUTH_ID_REQUEST_TYPE);

        final Map<String, String> urlParams = new LinkedHashMap<>();
        urlParams.put(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_CLIENT_ID), settings.getClientID());
        urlParams.put(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_RESPONSE_TYPE), code);
        urlParams.put(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_STATE), state);
        urlParams.put(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_REDIRECT_URI), redirectUri);

        final String redirectUrl = ServletHelper.appendAndEncodeUrlParameters(settings.getLoginURL(), urlParams);

        try {
            pwmRequest.sendRedirect(redirectUrl);
            pwmRequest.getPwmSession().getSessionStateBean().setOauthInProgress(true);
            LOGGER.debug(pwmRequest, "redirecting user to oauth id server, url: " + redirectUrl);
        } catch (PwmUnrecoverableException e) {
            final String errorMsg = "unexpected error redirecting user to oauth page: " + e.toString();
            ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg);
            pwmRequest.setResponseError(errorInformation);
            LOGGER.error(errorInformation.toDebugStr());
        }

    }

    public static OAuthRequestState readOAuthRequestState(final PwmRequest pwmRequest)
            throws PwmUnrecoverableException {
        final String requestStateStr = pwmRequest
                .readParameterAsString(pwmRequest.getConfig().readAppProperty(AppProperty.HTTP_PARAM_OAUTH_STATE));
        if (requestStateStr != null) {
            final String stateJson = pwmRequest.getPwmApplication().getSecureService()
                    .decryptStringValue(requestStateStr);
            final OAuthState oAuthState = JsonUtil.deserialize(stateJson, OAuthState.class);
            if (oAuthState != null) {
                final boolean sessionMatch = oAuthState.sessionID
                        .equals(pwmRequest.getPwmSession().getSessionStateBean().getSessionVerificationKey());
                LOGGER.trace(pwmRequest,
                        "read state while parsing oauth consumer request id=" + oAuthState.stateID + ", issueTime="
                                + PwmConstants.DEFAULT_DATETIME_FORMAT.format(oAuthState.issueTime) + ", nextUrl="
                                + oAuthState.nextUrl + ", sessionIDmatch=" + sessionMatch);

                return new OAuthRequestState(oAuthState, sessionMatch);
            }
        }

        return null;
    }
}