/*
 * Copyright 2014 Red Hat, Inc.
 *
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  and Apache License v2.0 which accompanies this distribution.
 *
 *  The Eclipse Public License is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 *
 *  The Apache License v2.0 is available at
 *  http://www.opensource.org/licenses/apache2.0.php
 *
 *  You may elect to redistribute this code under either of these licenses.
 */

package io.vertx.ext.web.handler.impl;

import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.ext.auth.AuthProvider;
import io.vertx.ext.auth.User;
import io.vertx.ext.web.Cookie;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.Session;
import io.vertx.ext.web.handler.SessionHandler;
import io.vertx.ext.web.sstore.SessionStore;

Author:Tim Fox
/** * @author <a href="http://tfox.org">Tim Fox</a> */
public class SessionHandlerImpl implements SessionHandler { private static final String SESSION_USER_HOLDER_KEY = "__vertx.userHolder"; private static final Logger log = LoggerFactory.getLogger(SessionHandlerImpl.class); private final SessionStore sessionStore; private String sessionCookieName; private String sessionCookiePath; private long sessionTimeout; private boolean nagHttps; private boolean sessionCookieSecure; private boolean sessionCookieHttpOnly; private int minLength; private AuthProvider authProvider; public SessionHandlerImpl(String sessionCookieName, String sessionCookiePath, long sessionTimeout, boolean nagHttps, boolean sessionCookieSecure, boolean sessionCookieHttpOnly, int minLength, SessionStore sessionStore) { this.sessionCookieName = sessionCookieName; this.sessionCookiePath = sessionCookiePath; this.sessionTimeout = sessionTimeout; this.nagHttps = nagHttps; this.sessionStore = sessionStore; this.sessionCookieSecure = sessionCookieSecure; this.sessionCookieHttpOnly = sessionCookieHttpOnly; this.minLength = minLength; } @Override public SessionHandler setSessionTimeout(long timeout) { this.sessionTimeout = timeout; return this; } @Override public SessionHandler setNagHttps(boolean nag) { this.nagHttps = nag; return this; } @Override public SessionHandler setCookieSecureFlag(boolean secure) { this.sessionCookieSecure = secure; return this; } @Override public SessionHandler setCookieHttpOnlyFlag(boolean httpOnly) { this.sessionCookieHttpOnly = httpOnly; return this; } @Override public SessionHandler setSessionCookieName(String sessionCookieName) { this.sessionCookieName = sessionCookieName; return this; } @Override public SessionHandler setSessionCookiePath(String sessionCookiePath) { this.sessionCookiePath = sessionCookiePath; return this; } @Override public SessionHandler setMinLength(int minLength) { this.minLength = minLength; return this; } @Override public SessionHandler setAuthProvider(AuthProvider authProvider) { this.authProvider = authProvider; return this; } @Override public void handle(RoutingContext context) { if (nagHttps && log.isDebugEnabled()) { String uri = context.request().absoluteURI(); if (!uri.startsWith("https:")) { log.debug( "Using session cookies without https could make you susceptible to session hijacking: " + uri); } } // Look for existing session cookie Cookie cookie = context.getCookie(sessionCookieName); if (cookie != null) { // Look up session String sessionID = cookie.getValue(); if (sessionID != null && sessionID.length() > minLength) { // we passed the OWASP min length requirements getSession(context.vertx(), sessionID, res -> { if (res.succeeded()) { Session session = res.result(); if (session != null) { context.setSession(session); // attempt to load the user from the session if auth provider is known if (authProvider != null) { UserHolder holder = session.get(SESSION_USER_HOLDER_KEY); if (holder != null) { User user = null; RoutingContext prevContext = holder.context; if (prevContext != null) { user = prevContext.user(); } else if (holder.user != null) { user = holder.user; user.setAuthProvider(authProvider); holder.context = context; holder.user = null; } holder.context = context; if (user != null) { context.setUser(user); } } addStoreSessionHandler(context, holder == null); } else { // never store user as there's no provider for auth addStoreSessionHandler(context, false); } } else { // Cannot find session - either it timed out, or was explicitly destroyed at the // server side on a // previous request. // OWASP clearly states that we shouldn't recreate the session as it allows // session fixation. // create a new anonymous session. createNewSession(context); } } else { context.fail(res.cause()); } context.next(); }); return; } } // requirements were not met, so a anonymous session is created. createNewSession(context); context.next(); } private void getSession(Vertx vertx, String sessionID, Handler<AsyncResult<Session>> resultHandler) { doGetSession(vertx, System.currentTimeMillis(), sessionID, resultHandler); } private void doGetSession(Vertx vertx, long startTime, String sessionID, Handler<AsyncResult<Session>> resultHandler) { sessionStore.get(sessionID, res -> { if (res.succeeded()) { if (res.result() == null) { // Can't find it so retry. This is necessary for clustered sessions as it can // take sometime for the session // to propagate across the cluster so if the next request for the session comes // in quickly at a different // node there is a possibility it isn't available yet. long retryTimeout = sessionStore.retryTimeout(); if (retryTimeout > 0 && System.currentTimeMillis() - startTime < retryTimeout) { vertx.setTimer(5, v -> doGetSession(vertx, startTime, sessionID, resultHandler)); return; } } } resultHandler.handle(res); }); } private void addStoreSessionHandler(RoutingContext context, boolean storeUser) { context.addHeadersEndHandler(v -> { Session session = context.session(); if (!session.isDestroyed()) { final int currentStatusCode = context.response().getStatusCode(); // Store the session (only and only if there was no error) if (currentStatusCode >= 200 && currentStatusCode < 400) { // store the current user into the session if (storeUser) { // during the request the user might have been removed if (context.user() != null) { session.put(SESSION_USER_HOLDER_KEY, new UserHolder(context)); } } session.setAccessed(); if (session.isRegenerated()) { // this means that a session id has been changed, usually it means a session // upgrade // (e.g.: anonymous to authenticated) or that the security requirements have // changed // see: // https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Life_Cycle // the session cookie needs to be updated to the new id final Cookie cookie = context.getCookie(sessionCookieName); // restore defaults cookie.setValue(session.value()).setPath("/").setSecure(sessionCookieSecure) .setHttpOnly(sessionCookieHttpOnly); // we must invalidate the old id sessionStore.delete(session.oldId(), delete -> { if (delete.failed()) { log.error("Failed to delete previous session", delete.cause()); } else { // we must wait for the result of the previous call in order to save the new one sessionStore.put(session, res -> { if (res.failed()) { log.error("Failed to store session", res.cause()); } }); } }); } else { sessionStore.put(session, res -> { if (res.failed()) { log.error("Failed to store session", res.cause()); } }); } } else { // don't send a cookie if status is not 2xx or 3xx // remove it from the set (do not invalidate) context.removeCookie(sessionCookieName, false); } } else { // invalidate the cookie as the session has been destroyed context.removeCookie(sessionCookieName); // delete from the storage sessionStore.delete(session.id(), res -> { if (res.failed()) { log.error("Failed to delete session", res.cause()); } }); } }); } private void createNewSession(RoutingContext context) { Session session = sessionStore.createSession(sessionTimeout, minLength); context.setSession(session); Cookie cookie = Cookie.cookie(sessionCookieName, session.value()); cookie.setPath(sessionCookiePath); cookie.setSecure(sessionCookieSecure); cookie.setHttpOnly(sessionCookieHttpOnly); // Don't set max age - it's a session cookie context.addCookie(cookie); // only store the user if there's a auth provider addStoreSessionHandler(context, authProvider != null); } }