/*
* 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.service;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.ejb.Stateless;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.gluu.site.ldap.persistence.exception.EmptyEntryPersistenceException;
import org.gluu.site.ldap.persistence.exception.EntryPersistenceException;
import org.slf4j.Logger;
import org.xdi.oxauth.audit.ApplicationAuditLogger;
import org.xdi.oxauth.model.audit.Action;
import org.xdi.oxauth.model.audit.OAuth2AuditLog;
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.config.StaticConfiguration;
import org.xdi.oxauth.model.config.WebKeysConfiguration;
import org.xdi.oxauth.model.configuration.AppConfiguration;
import org.xdi.oxauth.model.crypto.signature.SignatureAlgorithm;
import org.xdi.oxauth.model.exception.AcrChangedException;
import org.xdi.oxauth.model.jwt.Jwt;
import org.xdi.oxauth.model.jwt.JwtClaimName;
import org.xdi.oxauth.model.jwt.JwtSubClaimObject;
import org.xdi.oxauth.model.token.JwtSigner;
import org.xdi.oxauth.model.util.Util;
import org.xdi.oxauth.service.external.ExternalAuthenticationService;
import org.xdi.oxauth.util.ServerUtil;
import org.xdi.service.CacheService;
import org.xdi.util.StringHelper;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
/**
* @author Yuriy Zabrovarnyy
* @author Yuriy Movchan
* @author Javier Rojas Blum
* @version December 29, 2016
*/
@Stateless
@Named
public class SessionStateService {
public static final String SESSION_STATE_COOKIE_NAME = "session_state";
public static final String SESSION_CUSTOM_STATE = "session_custom_state";
@Inject
private Logger log;
@Inject
private ExternalAuthenticationService externalAuthenticationService;
@Inject
private ApplicationAuditLogger applicationAuditLogger;
@Inject
private AppConfiguration appConfiguration;
@Inject
private StaticConfiguration staticConfiguration;
@Inject
private WebKeysConfiguration webKeysConfiguration;
@Inject
private FacesContext facesContext;
@Inject
private ExternalContext externalContext;
@Inject
private CacheService cacheService;
public String getAcr(SessionState session) {
if (session == null || session.getSessionAttributes() == null) {
return null;
}
String acr = session.getSessionAttributes().get(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE);
if (StringUtils.isBlank(acr)) {
acr = session.getSessionAttributes().get("acr_values");
}
return acr;
}
// #34 - update session attributes with each request
// 1) redirect_uri change -> update session
// 2) acr change -> throw acr change exception
// 3) client_id change -> do nothing
// https://github.com/GluuFederation/oxAuth/issues/34
public SessionState assertAuthenticatedSessionCorrespondsToNewRequest(SessionState session, String acrValuesStr) throws AcrChangedException {
if (session != null && !session.getSessionAttributes().isEmpty() && session.getState() == SessionIdState.AUTHENTICATED) {
final Map<String, String> sessionAttributes = session.getSessionAttributes();
String sessionAcr = getAcr(session);
if (StringUtils.isBlank(sessionAcr)) {
log.error("Failed to fetch acr from session, attributes: " + sessionAttributes);
return session;
}
boolean isAcrChanged = acrValuesStr != null && !acrValuesStr.equals(sessionAcr);
if (isAcrChanged) {
Map<String, Integer> acrToLevel = externalAuthenticationService.acrToLevelMapping();
Integer sessionAcrLevel = acrToLevel.get(sessionAcr);
Integer currentAcrLevel = acrToLevel.get(acrValuesStr);
log.info("Acr is changed. Session acr: " + sessionAcr + "(level: " + sessionAcrLevel + "), " +
"current acr: " + acrValuesStr + "(level: " + currentAcrLevel + ")");
if (sessionAcrLevel < currentAcrLevel) {
throw new AcrChangedException();
} else { // https://github.com/GluuFederation/oxAuth/issues/291
return session; // we don't want to reinit login because we have stronger acr (avoid overriding)
}
}
reinitLogin(session, false);
}
return session;
}
public void reinitLogin(SessionState session, boolean force) {
final Map<String, String> sessionAttributes = session.getSessionAttributes();
final Map<String, String> currentSessionAttributes = getCurrentSessionAttributes(sessionAttributes);
if (force || !currentSessionAttributes.equals(sessionAttributes)) {
sessionAttributes.putAll(currentSessionAttributes);
// Reinit login
sessionAttributes.put("c", "1");
for (Iterator<Entry<String, String>> it = currentSessionAttributes.entrySet().iterator(); it.hasNext(); ) {
Entry<String, String> currentSessionAttributesEntry = it.next();
String name = currentSessionAttributesEntry.getKey();
if (name.startsWith("auth_step_passed_")) {
it.remove();
}
}
session.setSessionAttributes(currentSessionAttributes);
boolean updateResult = updateSessionState(session, true, true, true);
if (!updateResult) {
log.debug("Failed to update session entry: '{}'", session.getId());
}
}
}
public void resetToStep(SessionState session, int resetToStep) {
final Map<String, String> sessionAttributes = session.getSessionAttributes();
int currentStep = 1;
if (sessionAttributes.containsKey("auth_step")) {
currentStep = StringHelper.toInteger(sessionAttributes.get("auth_step"), currentStep);
}
for (int i = resetToStep; i <= currentStep; i++) {
String key = String.format("auth_step_passed_%d", i);
sessionAttributes.remove(key);
}
sessionAttributes.put("auth_step", String.valueOf(resetToStep));
boolean updateResult = updateSessionState(session, true, true, true);
if (!updateResult) {
log.debug("Failed to update session entry: '{}'", session.getId());
}
}
private Map<String, String> getCurrentSessionAttributes(Map<String, String> sessionAttributes) {
// Update from request
if (facesContext != null) {
// Clone before replacing new attributes
final Map<String, String> currentSessionAttributes = new HashMap<String, String>(sessionAttributes);
Map<String, String> parameterMap = externalContext.getRequestParameterMap();
Map<String, String> newRequestParameterMap = AuthenticationService.getAllowedParameters(parameterMap);
for (Entry<String, String> newRequestParameterMapEntry : newRequestParameterMap.entrySet()) {
String name = newRequestParameterMapEntry.getKey();
if (!StringHelper.equalsIgnoreCase(name, "auth_step")) {
currentSessionAttributes.put(name, newRequestParameterMapEntry.getValue());
}
}
return currentSessionAttributes;
} else {
return sessionAttributes;
}
}
public String getSessionStateFromCookie(HttpServletRequest request) {
try {
final Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(SESSION_STATE_COOKIE_NAME) /*&& cookie.getSecure()*/) {
log.trace("Found session_state cookie: '{}'", cookie.getValue());
return cookie.getValue();
}
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return "";
}
public String getSessionStateFromCookie() {
try {
if (facesContext == null) {
return null;
}
final HttpServletRequest request = (HttpServletRequest) externalContext.getRequest();
return getSessionStateFromCookie(request);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
public void createSessionStateCookie(String sessionState, HttpServletResponse httpResponse) {
// Create the special cookie header with secure flag but not HttpOnly because the session_state
// needs to be read from the OP iframe using JavaScript
String header = SESSION_STATE_COOKIE_NAME + "=" + sessionState;
header += "; Path=/";
header += "; Secure";
if (appConfiguration.getSessionStateHttpOnly()) {
header += "; HttpOnly";
}
httpResponse.addHeader("Set-Cookie", header);
}
public void createSessionStateCookie(String sessionState) {
try {
final Object response = externalContext.getResponse();
if (response instanceof HttpServletResponse) {
final HttpServletResponse httpResponse = (HttpServletResponse) response;
createSessionStateCookie(sessionState, httpResponse);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
public void removeSessionStateCookie() {
try {
if (facesContext != null && externalContext != null) {
final Object response = externalContext.getResponse();
if (response instanceof HttpServletResponse) {
removeSessionStateCookie((HttpServletResponse) response);
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
public void removeSessionStateCookie(HttpServletResponse httpResponse) {
final Cookie cookie = new Cookie(SESSION_STATE_COOKIE_NAME, null); // Not necessary, but saves bandwidth.
cookie.setPath("/");
cookie.setMaxAge(0); // Don't set to -1 or it will become a session cookie!
httpResponse.addCookie(cookie);
}
public SessionState getSessionState() {
String sessionState = getSessionStateFromCookie();
if (StringHelper.isNotEmpty(sessionState)) {
return getSessionState(sessionState);
}
return null;
}
public Map<String, String> getSessionAttributes(SessionState sessionState) {
if (sessionState != null) {
return sessionState.getSessionAttributes();
}
return null;
}
public SessionState generateAuthenticatedSessionState(String userDn) {
return generateAuthenticatedSessionState(userDn, "");
}
public SessionState generateAuthenticatedSessionState(String userDn, String prompt) {
Map<String, String> sessionIdAttributes = new HashMap<String, String>();
sessionIdAttributes.put("prompt", prompt);
return generateSessionState(userDn, new Date(), SessionIdState.AUTHENTICATED, sessionIdAttributes, true);
}
public SessionState generateAuthenticatedSessionState(String userDn, Map<String, String> sessionIdAttributes) {
return generateSessionState(userDn, new Date(), SessionIdState.AUTHENTICATED, sessionIdAttributes, true);
}
public SessionState generateUnauthenticatedSessionState(String userDn, Date authenticationDate, SessionIdState state, Map<String, String> sessionIdAttributes, boolean persist) {
return generateSessionState(userDn, authenticationDate, state, sessionIdAttributes, persist);
}
private SessionState generateSessionState(String userDn, Date authenticationDate, SessionIdState state, Map<String, String> sessionIdAttributes, boolean persist) {
final String uuid = UUID.randomUUID().toString();
final String dn = dn(uuid);
if (StringUtils.isBlank(dn)) {
return null;
}
if (SessionIdState.AUTHENTICATED == state) {
if (StringUtils.isBlank(userDn)) {
return null;
}
}
final SessionState sessionState = new SessionState();
sessionState.setId(uuid);
sessionState.setDn(dn);
sessionState.setUserDn(userDn);
Boolean sessionAsJwt = appConfiguration.getSessionAsJwt();
sessionState.setIsJwt(sessionAsJwt != null && sessionAsJwt);
if (authenticationDate != null) {
sessionState.setAuthenticationTime(authenticationDate);
}
if (state != null) {
sessionState.setState(state);
}
sessionState.setSessionAttributes(sessionIdAttributes);
sessionState.setLastUsedAt(new Date());
if (sessionState.getIsJwt()) {
sessionState.setJwt(generateJwt(sessionState, userDn).asString());
}
boolean persisted = false;
if (persist) {
persisted = persistSessionState(sessionState);
}
auditLogging(sessionState);
log.trace("Generated new session, id = '{}', state = '{}', asJwt = '{}', persisted = '{}'", sessionState.getId(), sessionState.getState(), sessionState.getIsJwt(), persisted);
return sessionState;
}
private Jwt generateJwt(SessionState sessionState, String audience) {
try {
JwtSigner jwtSigner = new JwtSigner(appConfiguration, webKeysConfiguration, SignatureAlgorithm.RS512, audience);
Jwt jwt = jwtSigner.newJwt();
// claims
jwt.getClaims().setClaim("id", sessionState.getId());
jwt.getClaims().setClaim("authentication_time", sessionState.getAuthenticationTime());
jwt.getClaims().setClaim("user_dn", sessionState.getUserDn());
jwt.getClaims().setClaim("state", sessionState.getState() != null ?
sessionState.getState().getValue() : "");
jwt.getClaims().setClaim("session_attributes", JwtSubClaimObject.fromMap(sessionState.getSessionAttributes()));
jwt.getClaims().setClaim("last_used_at", sessionState.getLastUsedAt());
jwt.getClaims().setClaim("permission_granted", sessionState.getPermissionGranted());
jwt.getClaims().setClaim("permission_granted_map", JwtSubClaimObject.fromBooleanMap(sessionState.getPermissionGrantedMap().getPermissionGranted()));
jwt.getClaims().setClaim("involved_clients_map", JwtSubClaimObject.fromBooleanMap(sessionState.getInvolvedClients().getPermissionGranted()));
// sign
return jwtSigner.sign();
} catch (Exception e) {
log.error("Failed to sign session jwt! " + e.getMessage(), e);
throw new RuntimeException(e);
}
}
public SessionState setSessionStateAuthenticated(SessionState sessionState, String p_userDn) {
sessionState.setUserDn(p_userDn);
sessionState.setAuthenticationTime(new Date());
sessionState.setState(SessionIdState.AUTHENTICATED);
boolean persisted = updateSessionState(sessionState, true, true, true);
auditLogging(sessionState);
log.trace("Authenticated session, id = '{}', state = '{}', persisted = '{}'", sessionState.getId(), sessionState.getState(), persisted);
return sessionState;
}
public boolean persistSessionState(final SessionState sessionState) {
return persistSessionState(sessionState, false);
}
public boolean persistSessionState(final SessionState sessionState, boolean forcePersistence) {
List<Prompt> prompts = getPromptsFromSessionState(sessionState);
try {
final int unusedLifetime = appConfiguration.getSessionIdUnusedLifetime();
if ((unusedLifetime > 0 && isPersisted(prompts)) || forcePersistence) {
sessionState.setLastUsedAt(new Date());
sessionState.setPersisted(true);
log.trace("sessionStateAttributes: " + sessionState.getPermissionGrantedMap());
putInCache(sessionState);
return true;
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return false;
}
public boolean updateSessionState(final SessionState sessionState) {
return updateSessionState(sessionState, true);
}
public boolean updateSessionState(final SessionState sessionState, boolean updateLastUsedAt) {
return updateSessionState(sessionState, updateLastUsedAt, false, true);
}
public boolean updateSessionState(final SessionState sessionState, boolean updateLastUsedAt, boolean forceUpdate, boolean modified) {
List<Prompt> prompts = getPromptsFromSessionState(sessionState);
try {
final int unusedLifetime = appConfiguration.getSessionIdUnusedLifetime();
if ((unusedLifetime > 0 && isPersisted(prompts)) || forceUpdate) {
boolean update = modified;
if (updateLastUsedAt) {
Date lastUsedAt = new Date();
if (sessionState.getLastUsedAt() != null) {
long diff = lastUsedAt.getTime() - sessionState.getLastUsedAt().getTime();
if (diff > 500) { // update only if diff is more than 500ms
update = true;
sessionState.setLastUsedAt(lastUsedAt);
}
} else {
update = true;
sessionState.setLastUsedAt(lastUsedAt);
}
}
if (!sessionState.isPersisted()) {
update = true;
sessionState.setPersisted(true);
}
if (update) {
try {
mergeWithRetry(sessionState, 3);
} catch (EmptyEntryPersistenceException ex) {
log.warn("Faield to update session entry '{}': '{}'", sessionState.getId(), ex.getMessage());
}
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
return true;
}
private void putInCache(SessionState sessionState) {
int expirationInSeconds = sessionState.getState() == SessionIdState.UNAUTHENTICATED ?
appConfiguration.getSessionIdUnauthenticatedUnusedLifetime() :
appConfiguration.getSessionIdUnusedLifetime();
cacheService.put(Integer.toString(expirationInSeconds), sessionState.getId(), sessionState); // first parameter is expiration instead of region for memcached
}
private SessionState getFromCache(String sessionId) {
return (SessionState) cacheService.get(null, sessionId);
}
private SessionState mergeWithRetry(final SessionState sessionState, int maxAttempts) {
EntryPersistenceException lastException = null;
for (int i = 1; i <= maxAttempts; i++) {
try {
putInCache(sessionState);
return sessionState;
} catch (EntryPersistenceException ex) {
lastException = ex;
if (ex.getCause() instanceof LDAPException) {
LDAPException parentEx = ((LDAPException) ex.getCause());
log.debug("LDAP exception resultCode: '{}'", parentEx.getResultCode().intValue());
if ((parentEx.getResultCode().intValue() == ResultCode.NO_SUCH_ATTRIBUTE_INT_VALUE) ||
(parentEx.getResultCode().intValue() == ResultCode.ATTRIBUTE_OR_VALUE_EXISTS_INT_VALUE)) {
log.warn("Session entry update attempt '{}' was unsuccessfull", i);
continue;
}
}
throw ex;
}
}
log.error("Session entry update attempt was unsuccessfull after '{}' attempts", maxAttempts);
throw lastException;
}
public void updateSessionStateIfNeeded(SessionState sessionState, boolean modified) {
updateSessionState(sessionState, true, false, modified);
}
private boolean isPersisted(List<Prompt> prompts) {
if (prompts != null && prompts.contains(Prompt.NONE)) {
final Boolean persistOnPromptNone = appConfiguration.getSessionIdPersistOnPromptNone();
return persistOnPromptNone != null && persistOnPromptNone;
}
return true;
}
private String dn(String p_id) {
final String baseDn = getBaseDn();
final StringBuilder sb = new StringBuilder();
if (Util.allNotBlank(p_id, getBaseDn())) {
sb.append("oxAuthSessionId=").append(p_id).append(",").append(baseDn);
}
return sb.toString();
}
public SessionState getSessionById(String sessionId) {
return getFromCache(sessionId);
}
public SessionState getSessionState(String sessionState) {
if (StringHelper.isEmpty(sessionState)) {
return null;
}
try {
final SessionState entity = getSessionById(sessionState);
log.trace("Try to get session by id: {} ...", sessionState);
if (entity != null) {
log.trace("Session dn: {}", entity.getDn());
if (isSessionValid(entity)) {
return entity;
}
}
} catch (Exception ex) {
log.trace(ex.getMessage(), ex);
}
log.trace("Failed to get session by id: {}", sessionState);
return null;
}
private String getBaseDn() {
return staticConfiguration.getBaseDn().getSessionId();
}
public boolean remove(SessionState sessionState) {
try {
cacheService.remove(null, sessionState.getId());
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
return true;
}
public void remove(List<SessionState> list) {
for (SessionState id : list) {
try {
remove(id);
} catch (Exception e) {
log.error("Failed to remove entry", e);
}
}
}
public boolean isSessionValid(SessionState sessionState) {
if (sessionState == null) {
return false;
}
final long sessionInterval = TimeUnit.SECONDS.toMillis(appConfiguration.getSessionIdUnusedLifetime());
final long sessionUnauthenticatedInterval = TimeUnit.SECONDS.toMillis(appConfiguration.getSessionIdUnauthenticatedUnusedLifetime());
final long timeSinceLastAccess = System.currentTimeMillis() - sessionState.getLastUsedAt().getTime();
if (timeSinceLastAccess > sessionInterval && appConfiguration.getSessionIdUnusedLifetime() != -1) {
return false;
}
if (sessionState.getState() == SessionIdState.UNAUTHENTICATED && timeSinceLastAccess > sessionUnauthenticatedInterval && appConfiguration.getSessionIdUnauthenticatedUnusedLifetime() != -1) {
return false;
}
return true;
}
private List<Prompt> getPromptsFromSessionState(final SessionState sessionState) {
String promptParam = sessionState.getSessionAttributes().get("prompt");
return Prompt.fromString(promptParam, " ");
}
public boolean isSessionStateAuthenticated() {
SessionState sessionState = getSessionState();
if (sessionState == null) {
return false;
}
SessionIdState sessionIdState = sessionState.getState();
if (SessionIdState.AUTHENTICATED.equals(sessionIdState)) {
return true;
}
return false;
}
public boolean isNotSessionStateAuthenticated() {
return !isSessionStateAuthenticated();
}
private void auditLogging(SessionState sessionState) {
HttpServletRequest httpServletRequest = ServerUtil.getRequestOrNull();
if (httpServletRequest != null) {
Action action;
switch (sessionState.getState()) {
case AUTHENTICATED:
action = Action.SESSION_AUTHENTICATED;
break;
case UNAUTHENTICATED:
action = Action.SESSION_UNAUTHENTICATED;
break;
default:
action = Action.SESSION_UNAUTHENTICATED;
}
OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(httpServletRequest), action);
oAuth2AuditLog.setSuccess(true);
applicationAuditLogger.sendMessage(oAuth2AuditLog);
}
}
}