/*
* 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 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.PwmConstants;
import password.pwm.bean.LoginInfoBean;
import password.pwm.bean.UserIdentity;
import password.pwm.config.Configuration;
import password.pwm.config.PwmSetting;
import password.pwm.error.ErrorInformation;
import password.pwm.error.PwmError;
import password.pwm.error.PwmException;
import password.pwm.error.PwmUnrecoverableException;
import password.pwm.http.HttpHeader;
import password.pwm.http.PwmRequest;
import password.pwm.http.PwmURL;
import password.pwm.http.client.PwmHttpClient;
import password.pwm.http.client.PwmHttpClientConfiguration;
import password.pwm.http.servlet.PwmServletDefinition;
import password.pwm.util.BasicAuthInfo;
import password.pwm.util.java.JavaHelper;
import password.pwm.util.java.JsonUtil;
import password.pwm.util.java.StringUtil;
import password.pwm.util.java.TimeDuration;
import password.pwm.util.logging.PwmLogger;
import password.pwm.util.macro.MacroMachine;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class OAuthMachine {
private static final PwmLogger LOGGER = PwmLogger.forClass(OAuthMachine.class);
private final OAuthSettings settings;
public OAuthMachine(final OAuthSettings settings) {
this.settings = settings;
}
public static Optional<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.getSessionID().equals(pwmRequest.getPwmSession().getSessionStateBean().getSessionVerificationKey());
LOGGER.trace(pwmRequest, "read state while parsing oauth consumer request with match=" + sessionMatch + ", " + JsonUtil.serialize(oAuthState));
return Optional.of(new OAuthRequestState(oAuthState, sessionMatch));
}
}
return Optional.empty();
}
public void redirectUserToOAuthServer(
final PwmRequest pwmRequest,
final String nextUrl,
final UserIdentity userIdentity,
final String forgottenPasswordProfile
)
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 String state = makeStateStringForRequest(pwmRequest, nextUrl, forgottenPasswordProfile);
final String redirectUri = 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);
if (userIdentity != null) {
final String parametersValue = figureUsernameGrantParam(pwmRequest, userIdentity);
if (!StringUtil.isEmpty(parametersValue)) {
urlParams.put("parameters", parametersValue);
}
}
final String redirectUrl = PwmURL.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();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg);
throw new PwmUnrecoverableException(errorInformation);
}
}
OAuthResolveResults makeOAuthResolveRequest(
final PwmRequest pwmRequest,
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 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 code resolver response from server at " + requestUrl + ", error: " + e.getMessage());
}
return oAuthResolveResults;
}
private OAuthResolveResults makeOAuthRefreshRequest(
final PwmRequest pwmRequest,
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 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;
}
String makeOAuthGetAttributeRequest(
final PwmRequest pwmRequest,
final String accessToken
)
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);
return restResults.getResponseBody();
}
private static RestResults makeHttpRequest(
final PwmRequest pwmRequest,
final String debugText,
final OAuthSettings settings,
final String requestUrl,
final Map<String,String> requestParams
)
throws IOException, PwmUnrecoverableException
{
final Instant startTime = Instant.now();
final String requestBody = PwmURL.appendAndEncodeUrlParameters("", requestParams);
LOGGER.trace(pwmRequest, "beginning " + debugText + " request to " + requestUrl + ", body: \n" + requestBody);
final HttpPost httpPost = new HttpPost(requestUrl);
httpPost.setHeader(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 = settings.getCertificates();
final HttpResponse httpResponse;
final String bodyResponse;
try {
if (certs == null || certs.length == 0) {
httpResponse = PwmHttpClient.getHttpClient(pwmRequest.getConfig()).execute(httpPost);
} else {
httpResponse = PwmHttpClient.getHttpClient(pwmRequest.getConfig(), new PwmHttpClientConfiguration.Builder().setCertificate(certs).create()).execute(httpPost);
}
bodyResponse = EntityUtils.toString(httpResponse.getEntity());
} catch (PwmException | IOException e) {
final String errorMsg;
if (e instanceof PwmException) {
errorMsg = "error during " + debugText + " http request to oauth server, remote error: " + ((PwmException) e).getErrorInformation().toDebugStr();
} else {
errorMsg = "io error during " + debugText + " http request to oauth server: " + e.getMessage();
}
throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_OAUTH_ERROR, errorMsg));
}
final StringBuilder debugOutput = new StringBuilder();
debugOutput.append(debugText).append(" ").append(
TimeDuration.fromCurrent(startTime).asCompactString()).append(", status: ").append(
httpResponse.getStatusLine()).append("\n");
for (final 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());
if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
throw new PwmUnrecoverableException(new ErrorInformation(
PwmError.ERROR_OAUTH_ERROR,
"unexpected HTTP status code (" + httpResponse.getStatusLine().getStatusCode() + ") during " + debugText + " request to " + requestUrl
));
}
return new RestResults(httpResponse, bodyResponse);
}
public static String figureOauthSelfEndPointUrl(final PwmRequest pwmRequest) {
final String debugSource;
final String redirect_uri;
{
final String returnUrlOverride = pwmRequest.getConfig().readAppProperty(AppProperty.OAUTH_RETURN_URL_OVERRIDE);
final String siteURL = pwmRequest.getConfig().readSettingAsString(PwmSetting.PWM_SITE_URL);
if (returnUrlOverride != null && !returnUrlOverride.trim().isEmpty()) {
debugSource = "AppProperty(\"" + AppProperty.OAUTH_RETURN_URL_OVERRIDE.getKey() + "\")";
redirect_uri = returnUrlOverride
+ PwmServletDefinition.OAuthConsumer.servletUrl();
} else if (siteURL != null && !siteURL.trim().isEmpty()) {
debugSource = "SiteURL Setting";
redirect_uri = siteURL
+ PwmServletDefinition.OAuthConsumer.servletUrl();
} else {
debugSource = "Input Request URL";
final String inputURI = pwmRequest.getHttpServletRequest().getRequestURL().toString();
try {
final URI requestUri = new URI(inputURI);
final int port = requestUri.getPort();
redirect_uri = requestUri.getScheme() + "://" + requestUri.getHost()
+ (port > 0 && port != 80 && port != 443 ? ":" + requestUri.getPort() : "")
+ pwmRequest.getContextPath()
+ PwmServletDefinition.OAuthConsumer.servletUrl();
} catch (URISyntaxException e) {
throw new IllegalStateException("unable to parse inbound request uri while generating oauth redirect: " + e.getMessage());
}
}
}
LOGGER.trace("calculated oauth self end point URI as '" + redirect_uri + "' using method " + debugSource);
return redirect_uri;
}
static class RestResults {
final HttpResponse httpResponse;
final String responseBody;
RestResults(
final HttpResponse httpResponse,
final String responseBody
)
{
this.httpResponse = httpResponse;
this.responseBody = responseBody;
}
public HttpResponse getHttpResponse()
{
return httpResponse;
}
public String getResponseBody()
{
return responseBody;
}
}
public 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 Instant expirationDate = loginInfoBean.getOauthExp();
if (expirationDate == null || Instant.now().isBefore(expirationDate)) {
//not expired
return false;
}
LOGGER.trace(pwmRequest, "oauth access token has expired, attempting to refresh");
try {
final OAuthResolveResults resolveResults = makeOAuthRefreshRequest(pwmRequest,
loginInfoBean.getOauthRefToken());
if (resolveResults != null) {
if (resolveResults.getExpiresSeconds() > 0) {
final Instant accessTokenExpirationDate = Instant.ofEpochMilli(System.currentTimeMillis() + 1000 * resolveResults.getExpiresSeconds());
LOGGER.trace(pwmRequest, "noted oauth access token expiration at timestamp " + JavaHelper.toIsoDate(accessTokenExpirationDate));
loginInfoBean.setOauthExp(accessTokenExpirationDate);
loginInfoBean.setOauthRefToken(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 String makeStateStringForRequest(
final PwmRequest pwmRequest,
final String nextUrl,
final String forgottenPasswordProfileID
)
throws PwmUnrecoverableException
{
final OAuthUseCase oAuthUseCase = settings.getUse();
final String sessionId = pwmRequest.getPwmSession().getSessionStateBean().getSessionVerificationKey();
final OAuthState oAuthState;
switch (oAuthUseCase) {
case Authentication:
oAuthState = OAuthState.newSSOAuthenticationState(sessionId, nextUrl);
break;
case ForgottenPassword:
oAuthState = OAuthState.newForgottenPasswordState(sessionId, forgottenPasswordProfileID);
break;
default:
throw new IllegalStateException("unexpected oAuthUseCase: " + oAuthUseCase);
}
LOGGER.trace(pwmRequest, "issuing oauth state id="
+ oAuthState.getStateID() + " with the next destination URL set to " + oAuthState.getNextUrl());
final String jsonValue = JsonUtil.serialize(oAuthState);
return pwmRequest.getPwmApplication().getSecureService().encryptToString(jsonValue);
}
private String figureUsernameGrantParam(
final PwmRequest pwmRequest,
final UserIdentity userIdentity
)
throws IOException, PwmUnrecoverableException
{
if (userIdentity == null) {
return null;
}
final String macroText = settings.getUsernameSendValue();
if (StringUtil.isEmpty(macroText)) {
return null;
}
final MacroMachine macroMachine = MacroMachine.forUser(pwmRequest, userIdentity);
final String username = macroMachine.expandMacros(macroText);
LOGGER.debug(pwmRequest, "calculated username value for user as: " + username);
final String grantUrl = settings.getLoginURL();
final String signUrl = grantUrl.replace("/grant","/sign");
final Map<String, String> requestPayload;
{
final Map<String, String> dataPayload = new HashMap<>();
dataPayload.put("username", username);
final List<Map<String,String>> listWrapper = new ArrayList<>();
listWrapper.add(dataPayload);
requestPayload = new HashMap<>();
requestPayload.put("data", JsonUtil.serializeCollection(listWrapper));
}
LOGGER.debug(pwmRequest, "preparing to send username to OAuth /sign endpoint for future injection to /grant redirect");
final RestResults restResults = makeHttpRequest(pwmRequest, "OAuth pre-inject username signing service",settings, signUrl, requestPayload);
final String resultBody = restResults.getResponseBody();
final Map<String,String> resultBodyMap = JsonUtil.deserializeStringMap(resultBody);
final String data = resultBodyMap.get("data");
LOGGER.debug(pwmRequest, "oauth /sign endpoint returned signed username data: " + data);
return data;
}
}