/*
* oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text.
*
* Copyright (c) 2014, Gluu
*/
package org.xdi.oxauth.authorize.ws.rs;
import com.google.common.collect.Sets;
import org.apache.commons.lang.StringUtils;
import org.codehaus.jettison.json.JSONException;
import org.gluu.jsf2.service.FacesService;
import org.gluu.site.ldap.persistence.exception.EntryPersistenceException;
import org.slf4j.Logger;
import org.xdi.model.AuthenticationScriptUsageType;
import org.xdi.model.custom.script.conf.CustomScriptConfiguration;
import org.xdi.model.security.Identity;
import org.xdi.oxauth.auth.Authenticator;
import org.xdi.oxauth.i18n.LanguageBean;
import org.xdi.oxauth.model.auth.AuthenticationMode;
import org.xdi.oxauth.model.authorize.AuthorizeErrorResponseType;
import org.xdi.oxauth.model.authorize.AuthorizeParamsValidator;
import org.xdi.oxauth.model.authorize.AuthorizeRequestParam;
import org.xdi.oxauth.model.common.Prompt;
import org.xdi.oxauth.model.common.SessionIdState;
import org.xdi.oxauth.model.common.SessionState;
import org.xdi.oxauth.model.common.User;
import org.xdi.oxauth.model.config.Constants;
import org.xdi.oxauth.model.configuration.AppConfiguration;
import org.xdi.oxauth.model.error.ErrorResponseFactory;
import org.xdi.oxauth.model.exception.AcrChangedException;
import org.xdi.oxauth.model.jwt.JwtClaimName;
import org.xdi.oxauth.model.ldap.ClientAuthorizations;
import org.xdi.oxauth.model.registration.Client;
import org.xdi.oxauth.model.util.LocaleUtil;
import org.xdi.oxauth.model.util.Util;
import org.xdi.oxauth.service.*;
import org.xdi.oxauth.service.external.ExternalAuthenticationService;
import org.xdi.service.net.NetworkService;
import org.xdi.util.StringHelper;
import javax.enterprise.context.RequestScoped;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
/**
* @author Javier Rojas Blum
* @author Yuriy Movchan
* @version January 20, 2017
*/
@RequestScoped
@Named
public class AuthorizeAction {
@Inject
private Logger log;
@Inject
private Authenticator authenticator;
@Inject
private ClientService clientService;
@Inject
private ScopeService scopeService;
@Inject
private ErrorResponseFactory errorResponseFactory;
@Inject
private SessionStateService sessionStateService;
@Inject
private UserService userService;
@Inject
private RedirectionUriService redirectionUriService;
@Inject
private AuthenticationService authenticationService;
@Inject
private ClientAuthorizationsService clientAuthorizationsService;
@Inject
private ExternalAuthenticationService externalAuthenticationService;
@Inject
private AuthenticationMode defaultAuthenticationMode;
@Inject
private LanguageBean languageBean;
@Inject
private NetworkService networkService;
@Inject
private Identity identity;
@Inject
private AppConfiguration appConfiguration;
@Inject
private FacesService facesService;
@Inject
private FacesContext facesContext;
@Inject
private ExternalContext externalContext;
// OAuth 2.0 request parameters
private String scope;
private String responseType;
private String clientId;
private String redirectUri;
private String state;
// OpenID Connect request parameters
private String responseMode;
private String nonce;
private String display;
private String prompt;
private Integer maxAge;
private String uiLocales;
private String idTokenHint;
private String loginHint;
private String acrValues;
private String amrValues;
private String request;
private String requestUri;
private String codeChallenge;
private String codeChallengeMethod;
// custom oxAuth parameters
private String sessionState;
public void checkUiLocales() {
List<String> uiLocalesList = null;
if (StringUtils.isNotBlank(uiLocales)) {
uiLocalesList = Util.splittedStringAsList(uiLocales, " ");
List<Locale> supportedLocales = new ArrayList<Locale>();
for (Iterator<Locale> it = facesContext.getApplication().getSupportedLocales(); it.hasNext(); ) {
supportedLocales.add(it.next());
}
Locale matchingLocale = LocaleUtil.localeMatch(uiLocalesList, supportedLocales);
if (matchingLocale != null)
languageBean.setLocaleCode(matchingLocale.getLanguage());
}
}
public void checkPermissionGranted() {
if ((clientId == null) || clientId.isEmpty()) {
log.error("Permission denied. client_id should be not empty.");
permissionDenied();
return;
}
Client client = null;
try {
client = clientService.getClient(clientId);
} catch (EntryPersistenceException ex) {
log.error("Permission denied. Failed to find client by inum '{}' in LDAP.", clientId, ex);
permissionDenied();
return;
}
if (client == null) {
log.error("Permission denied. Failed to find client_id '{}' in LDAP.", clientId);
permissionDenied();
return;
}
SessionState session = getSession();
List<Prompt> prompts = Prompt.fromString(prompt, " ");
try {
session = sessionStateService.assertAuthenticatedSessionCorrespondsToNewRequest(session, acrValues);
} catch (AcrChangedException e) {
log.debug("There is already existing session which has another acr then {}, session: {}", acrValues, session.getId());
if (prompts.contains(Prompt.LOGIN)) {
session = handleAcrChange(session, prompts);
} else {
log.error("Please provide prompt=login to force login with new ACR or otherwise perform logout and re-authenticate.");
permissionDenied();
return;
}
}
if (session == null || StringUtils.isBlank(session.getUserDn()) || SessionIdState.AUTHENTICATED != session.getState()) {
Map<String, String> parameterMap = externalContext.getRequestParameterMap();
Map<String, String> requestParameterMap = authenticationService.getAllowedParameters(parameterMap);
String redirectTo = "/login.xhtml";
boolean useExternalAuthenticator = externalAuthenticationService.isEnabled(AuthenticationScriptUsageType.INTERACTIVE);
if (useExternalAuthenticator) {
List<String> acrValuesList = acrValuesList();
if (acrValuesList.isEmpty()) {
if (StringHelper.isNotEmpty(defaultAuthenticationMode.getName())) {
acrValuesList = Arrays.asList(defaultAuthenticationMode.getName());
} else {
CustomScriptConfiguration defaultExternalAuthenticator = externalAuthenticationService.getDefaultExternalAuthenticator(AuthenticationScriptUsageType.INTERACTIVE);
if (defaultExternalAuthenticator != null) {
acrValuesList = Arrays.asList(defaultExternalAuthenticator.getName());
}
}
}
CustomScriptConfiguration customScriptConfiguration = externalAuthenticationService.determineCustomScriptConfiguration(AuthenticationScriptUsageType.INTERACTIVE, acrValuesList);
if (customScriptConfiguration == null) {
log.error("Failed to get CustomScriptConfiguration. auth_step: {}, acr_values: {}", 1, this.acrValues);
permissionDenied();
return;
}
String acr = customScriptConfiguration.getName();
requestParameterMap.put(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, acr);
requestParameterMap.put("auth_step", Integer.toString(1));
String tmpRedirectTo = externalAuthenticationService.executeExternalGetPageForStep(customScriptConfiguration, 1);
if (StringHelper.isNotEmpty(tmpRedirectTo)) {
log.trace("Redirect to person authentication login page: {}", tmpRedirectTo);
redirectTo = tmpRedirectTo;
}
}
// Store Remote IP
String remoteIp = networkService.getRemoteIp();
requestParameterMap.put(Constants.REMOTE_IP, remoteIp);
// Create unauthenticated session
SessionState unauthenticatedSession = sessionStateService.generateUnauthenticatedSessionState(null, new Date(), SessionIdState.UNAUTHENTICATED, requestParameterMap, false);
unauthenticatedSession.setSessionAttributes(requestParameterMap);
unauthenticatedSession.addPermission(clientId, false);
boolean persisted = sessionStateService.persistSessionState(unauthenticatedSession, !prompts.contains(Prompt.NONE)); // always persist is prompt is not none
if (persisted && log.isTraceEnabled()) {
log.trace("Session '{}' persisted to LDAP", unauthenticatedSession.getId());
}
this.sessionState = unauthenticatedSession.getId();
sessionStateService.createSessionStateCookie(this.sessionState);
Map<String, Object> loginParameters = new HashMap<String, Object>();
if (requestParameterMap.containsKey(AuthorizeRequestParam.LOGIN_HINT)) {
loginParameters.put(AuthorizeRequestParam.LOGIN_HINT,
requestParameterMap.get(AuthorizeRequestParam.LOGIN_HINT));
}
facesService.redirect(redirectTo, loginParameters);
return;
}
if (StringUtils.isBlank(redirectionUriService.validateRedirectionUri(clientId, redirectUri))) {
permissionDenied();
}
final User user = userService.getUserByDn(session.getUserDn());
log.trace("checkPermissionGranted, user = " + user);
if (AuthorizeParamsValidator.noNonePrompt(prompts)) {
if (appConfiguration.getTrustedClientEnabled()) { // if trusted client = true, then skip authorization page and grant access directly
if (client.getTrustedClient() && !prompts.contains(Prompt.CONSENT)) {
permissionGranted(session);
return;
}
}
if (client.getPersistClientAuthorizations()) {
ClientAuthorizations clientAuthorizations = clientAuthorizationsService.findClientAuthorizations(user.getAttribute("inum"), client.getClientId());
if (clientAuthorizations != null && clientAuthorizations.getScopes() != null &&
Arrays.asList(clientAuthorizations.getScopes()).containsAll(
org.xdi.oxauth.model.util.StringUtils.spaceSeparatedToList(scope))) {
permissionGranted(session);
return;
}
}
} else {
invalidRequest();
}
return;
}
private SessionState handleAcrChange(SessionState session, List<Prompt> prompts) {
if (session != null && prompts.contains(Prompt.LOGIN)) { // change session state only if prompt=none
if (session.getState() == SessionIdState.AUTHENTICATED) {
session.getSessionAttributes().put("prompt", prompt);
session.setState(SessionIdState.UNAUTHENTICATED);
// Update Remote IP
String remoteIp = networkService.getRemoteIp();
session.getSessionAttributes().put(Constants.REMOTE_IP, remoteIp);
sessionStateService.updateSessionState(session);
sessionStateService.reinitLogin(session, false);
}
}
return session;
}
/**
* By definition we expects space separated acr values as it is defined in spec. But we also try maybe some client
* sent it to us as json array. So we try both.
*
* @return acr value list
*/
private List<String> acrValuesList() {
List<String> acrs;
try {
acrs = Util.jsonArrayStringAsList(this.acrValues);
} catch (JSONException ex) {
acrs = Util.splittedStringAsList(acrValues, " ");
}
return acrs;
}
private SessionState getSession() {
if (StringUtils.isBlank(sessionState)) {
sessionState = sessionStateService.getSessionStateFromCookie();
if (StringUtils.isBlank(this.sessionState)) {
return null;
}
}
if (!identity.isLoggedIn()) {
authenticator.authenticateBySessionState(sessionState);
}
SessionState ldapSessionState = sessionStateService.getSessionState(sessionState);
if (ldapSessionState == null) {
identity.logout();
}
return ldapSessionState;
}
public List<org.xdi.oxauth.model.common.Scope> getScopes() {
List<org.xdi.oxauth.model.common.Scope> scopes = new ArrayList<org.xdi.oxauth.model.common.Scope>();
if (scope != null && !scope.isEmpty()) {
String[] scopesName = scope.split(" ");
for (String scopeName : scopesName) {
org.xdi.oxauth.model.common.Scope s = scopeService.getScopeByDisplayName(scopeName);
if (s != null && s.getDescription() != null) {
scopes.add(s);
}
}
}
return scopes;
}
/**
* Returns the scope of the access request.
*
* @return The scope of the access request.
*/
public String getScope() {
return scope;
}
/**
* Sets the scope of the access request.
*
* @param scope The scope of the access request.
*/
public void setScope(String scope) {
this.scope = scope;
}
/**
* Returns the response type: <code>code</code> for requesting an authorization code (authorization code grant) or
* <strong>token</strong> for requesting an access token (implicit grant).
*
* @return The response type.
*/
public String getResponseType() {
return responseType;
}
/**
* Sets the response type.
*
* @param responseType The response type.
*/
public void setResponseType(String responseType) {
this.responseType = responseType;
}
/**
* Returns the client identifier.
*
* @return The client identifier.
*/
public String getClientId() {
return clientId;
}
/**
* Sets the client identifier.
*
* @param clientId The client identifier.
*/
public void setClientId(String clientId) {
this.clientId = clientId;
}
/**
* Returns the redirection URI.
*
* @return The redirection URI.
*/
public String getRedirectUri() {
return redirectUri;
}
/**
* Sets the redirection URI.
*
* @param redirectUri The redirection URI.
*/
public void setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
}
/**
* Returns an opaque value used by the client to maintain state between the request and callback. The authorization
* server includes this value when redirecting the user-agent back to the client. The parameter should be used for
* preventing cross-site request forgery.
*
* @return The state between the request and callback.
*/
public String getState() {
return state;
}
/**
* Sets the state between the request and callback.
*
* @param state The state between the request and callback.
*/
public void setState(String state) {
this.state = state;
}
/**
* Returns the mechanism to be used for returning parameters from the Authorization Endpoint.
*
* @return The response mode.
*/
public String getResponseMode() {
return responseMode;
}
/**
* Sets the mechanism to be used for returning parameters from the Authorization Endpoint.
*
* @param responseMode The response mode.
*/
public void setResponseMode(String responseMode) {
this.responseMode = responseMode;
}
/**
* Return a string value used to associate a user agent session with an ID Token, and to mitigate replay attacks.
*
* @return The nonce value.
*/
public String getNonce() {
return nonce;
}
/**
* Sets a string value used to associate a user agent session with an ID Token, and to mitigate replay attacks.
*
* @param nonce The nonce value.
*/
public void setNonce(String nonce) {
this.nonce = nonce;
}
/**
* Returns an ASCII string value that specifies how the Authorization Server displays the authentication page
* to the End-User.
*
* @return The display value.
*/
public String getDisplay() {
return display;
}
/**
* Sets an ASCII string value that specifies how the Authorization Server displays the authentication page
* to the End-User.
*
* @param display The display value
*/
public void setDisplay(String display) {
this.display = display;
}
/**
* Returns a space delimited list of ASCII strings that can contain the values
* login, consent, select_account, and none.
*
* @return A list of prompt options.
*/
public String getPrompt() {
return prompt;
}
/**
* Sets a space delimited list of ASCII strings that can contain the values
* login, consent, select_account, and none.
*
* @param prompt A list of prompt options.
*/
public void setPrompt(String prompt) {
this.prompt = prompt;
}
public Integer getMaxAge() {
return maxAge;
}
public void setMaxAge(Integer maxAge) {
this.maxAge = maxAge;
}
public String getUiLocales() {
return uiLocales;
}
public void setUiLocales(String uiLocales) {
this.uiLocales = uiLocales;
}
public String getIdTokenHint() {
return idTokenHint;
}
public void setIdTokenHint(String idTokenHint) {
this.idTokenHint = idTokenHint;
}
public String getLoginHint() {
return loginHint;
}
public void setLoginHint(String loginHint) {
this.loginHint = loginHint;
}
public String getAcrValues() {
return acrValues;
}
public void setAcrValues(String acrValues) {
this.acrValues = acrValues;
}
public String getAmrValues() {
return amrValues;
}
public void setAmrValues(String amrValues) {
this.amrValues = amrValues;
}
/**
* Returns a JWT encoded OpenID Request Object.
*
* @return A JWT encoded OpenID Request Object.
*/
public String getRequest() {
return request;
}
/**
* Sets a JWT encoded OpenID Request Object.
*
* @param request A JWT encoded OpenID Request Object.
*/
public void setRequest(String request) {
this.request = request;
}
/**
* Returns an URL that points to an OpenID Request Object.
*
* @return An URL that points to an OpenID Request Object.
*/
public String getRequestUri() {
return requestUri;
}
/**
* Sets an URL that points to an OpenID Request Object.
*
* @param requestUri An URL that points to an OpenID Request Object.
*/
public void setRequestUri(String requestUri) {
this.requestUri = requestUri;
}
public String getSessionState() {
return sessionState;
}
public void setSessionState(String p_sessionState) {
sessionState = p_sessionState;
}
public void permissionGranted() {
final SessionState session = getSession();
permissionGranted(session);
}
public void permissionGranted(SessionState session) {
try {
final User user = userService.getUserByDn(session.getUserDn());
if (user == null) {
log.error("Permission denied. Failed to find session user: userDn = " + session.getUserDn() + ".");
permissionDenied();
return;
}
if (clientId == null) {
clientId = session.getSessionAttributes().get(AuthorizeRequestParam.CLIENT_ID);
}
final Client client = clientService.getClient(clientId);
if (scope == null) {
scope = session.getSessionAttributes().get(AuthorizeRequestParam.SCOPE);
}
// oxAuth #441 Pre-Authorization + Persist Authorizations... don't write anything
// If a client has pre-authorization=true, there is no point to create the entry under
// ou=clientAuthorizations it will negatively impact performance, grow the size of the
// ldap database, and serve no purpose.
if (client.getPersistClientAuthorizations() && !client.getTrustedClient()) {
final Set<String> scopes = Sets.newHashSet(org.xdi.oxauth.model.util.StringUtils.spaceSeparatedToList(scope));
clientAuthorizationsService.add(user.getAttribute("inum"), client.getClientId(), scopes);
}
session.addPermission(clientId, true);
sessionStateService.updateSessionState(session);
// OXAUTH-297 - set session_state cookie
sessionStateService.createSessionStateCookie(sessionState);
Map<String, String> sessionAttribute = authenticationService.getAllowedParameters(session.getSessionAttributes());
if (sessionAttribute.containsKey(AuthorizeRequestParam.PROMPT)) {
List<Prompt> prompts = Prompt.fromString(sessionAttribute.get(AuthorizeRequestParam.PROMPT), " ");
prompts.remove(Prompt.CONSENT);
sessionAttribute.put(AuthorizeRequestParam.PROMPT, org.xdi.oxauth.model.util.StringUtils.implodeEnum(prompts, " "));
}
final String parametersAsString = authenticationService.parametersAsString(sessionAttribute);
final String uri = "seam/resource/restv1/oxauth/authorize?" + parametersAsString;
log.trace("permissionGranted, redirectTo: {}", uri);
facesService.redirectToExternalURL(uri);
} catch (UnsupportedEncodingException e) {
log.trace(e.getMessage(), e);
}
}
public void permissionDenied() {
log.trace("permissionDenied");
final SessionState session = getSession();
StringBuilder sb = new StringBuilder();
if (redirectUri == null) {
redirectUri = session.getSessionAttributes().get(AuthorizeRequestParam.REDIRECT_URI);
}
if (state == null) {
state = session.getSessionAttributes().get(AuthorizeRequestParam.STATE);
}
sb.append(redirectUri);
if (redirectUri != null && redirectUri.contains("?")) {
sb.append("&");
} else {
sb.append("?");
}
sb.append(errorResponseFactory.getErrorAsQueryString(AuthorizeErrorResponseType.ACCESS_DENIED,
getState()));
facesService.redirectToExternalURL(sb.toString());
}
public void invalidRequest() {
log.trace("invalidRequest");
StringBuilder sb = new StringBuilder();
sb.append(redirectUri);
if (redirectUri != null && redirectUri.contains("?")) {
sb.append("&");
} else {
sb.append("?");
}
sb.append(errorResponseFactory.getErrorAsQueryString(AuthorizeErrorResponseType.INVALID_REQUEST,
getState()));
facesService.redirectToExternalURL(sb.toString());
}
public void consentRequired() {
StringBuilder sb = new StringBuilder();
sb.append(redirectUri);
if (redirectUri != null && redirectUri.contains("?")) {
sb.append("&");
} else {
sb.append("?");
}
sb.append(errorResponseFactory.getErrorAsQueryString(AuthorizeErrorResponseType.CONSENT_REQUIRED, getState()));
facesService.redirectToExternalURL(sb.toString());
}
public String getCodeChallenge() {
return codeChallenge;
}
public void setCodeChallenge(String codeChallenge) {
this.codeChallenge = codeChallenge;
}
public String getCodeChallengeMethod() {
return codeChallengeMethod;
}
public void setCodeChallengeMethod(String codeChallengeMethod) {
this.codeChallengeMethod = codeChallengeMethod;
}
public String encodeParameters(String url, Map<String, Object> parameters) {
if (parameters.isEmpty()) return url;
StringBuilder builder = new StringBuilder(url);
for (Map.Entry<String, Object> param : parameters.entrySet()) {
String parameterName = param.getKey();
if (!containsParameter(url, parameterName)) {
Object parameterValue = param.getValue();
if (parameterValue instanceof Iterable) {
for (Object value : (Iterable) parameterValue) {
builder.append('&')
.append(parameterName)
.append('=');
if (value != null) {
builder.append(encode(value));
}
}
} else {
builder.append('&')
.append(parameterName)
.append('=');
if (parameterValue != null) {
builder.append(encode(parameterValue));
}
}
}
}
if (url.indexOf('?') < 0) {
builder.setCharAt(url.length(), '?');
}
return builder.toString();
}
private boolean containsParameter(String url, String parameterName) {
return url.indexOf('?' + parameterName + '=') > 0 ||
url.indexOf('&' + parameterName + '=') > 0;
}
private String encode(Object value) {
try {
return URLEncoder.encode(String.valueOf(value), "UTF-8");
} catch (UnsupportedEncodingException iee) {
throw new RuntimeException(iee);
}
}
}