All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tomtom.speedtools.rest.security.SessionManager Maven / Gradle / Ivy

Go to download

Consists of utilities to create reliable, scalable RestEasy REST APIs using the Akka framework to process incoming requests.

There is a newer version: 3.4.4
Show newest version
/*
 * Copyright (C) 2012-2017. TomTom International BV (http://tomtom.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.tomtom.speedtools.rest.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.ws.rs.core.SecurityContext;
import java.io.Serializable;
import java.security.Principal;

/**
 * The session manager is the central place to start and terminate sessions, and create a {@link SecurityContext} for
 * the ongoing session. It encapsulates how information is stored in a session, and how sessions are marked as
 * authenticated.
 *
 * None of the methods provided by this class are thread-safe in the sense that no other threads can interfere
 * with the session during method calls of another thread. This is due to the asynchronous nature of HTTP, where, even
 * if the methods here were thread-safe, a session that was created could e.g. be torn down immediately again by a
 * different thread while the HTTP request of the first thread is still being processed by the application.
 * However, the session manager guarantees that any data set in the session is set atomically, so that the
 * session can never contain mixed data from invocations by multiple concurrent calls. For more information, see the
 * JavaDoc of each method in question.
 */
public class SessionManager {
    private static final Logger LOG = LoggerFactory.getLogger(SessionManager.class);

    /**
     * Authentication schemes used by this class in {@code startWebSession} and {@code startAppSession}.
     * Any other strings may be used as well, for example, by derived classes of {@code SessionManager}.
     */
    public static final String AUTHENTICATION_SCHEME_USERNAME = "USERNAME";
    public static final String AUTHENTICATION_SCHEME_APPTOKEN = "APPTOKEN";

    /**
     * The name of the key that is used to store our session data in the {@link HttpSession}. The name must be
     * name-spaced to prevent collisions with e.g. container attributes or attributes set by other frameworks.
     */
    @Nonnull
    private static final String SESSION_DATA_KEY = "com.tomtom.speedtools.rest.security.SESSIONDATA";

    /**
     * The current servlet request.
     */
    @Nonnull
    private final HttpServletRequest httpServletRequest;

    /**
     * Constructs a new {@code SessionManager} that is initialised with the current request context. Note that
     * the constructor must be called on the same thread that called the current JAX-RS resource, otherwise
     * request-contextual data cannot be retrieved. This constructor will throw an error in case it is not called on the
     * correct thread.
     *
     * @throws RuntimeException In case this constructor is not accessed on the thread that is used to call the JAX-RS
     *                          resource method.
     */
    public SessionManager() {
        this(ContextHelper.getContextData(HttpServletRequest.class));
    }

    /**
     * Package-private constructor for use by unit tests.
     *
     * @param httpServletRequest The current request.
     */
    SessionManager(@Nonnull final HttpServletRequest httpServletRequest) {
        assert httpServletRequest != null;

        this.httpServletRequest = httpServletRequest;
    }

    /**
     * Starts a new web session for the given {@code principal} in the current {@code httpServletRequest}. A potentially
     * existing session is terminated first to prevent session fixation attacks.
     *
     * This method is not thread-safe in the sense that a session actually exists when this method returns,
     * because a racing request may have torn down the session that is being created here while this method is
     * executing, or the data set in this call is actually the data stored in the session at the point this method
     * returns. This cannot be prevented even with locking, because HTTP is inherently asynchronous, and even if this
     * method would lock, as soon as the method returns a racing request could then tear down the session or modify the
     * session data. However, this method guarantees that all data that is being set as part of the
     * authenticated session is set atomically, i.e. multiple racing calls will not cause the session to contain mixed
     * data of those different calls, one call will always win. Also, data set during this call is stored in the session
     * before this call returns, but as described above, may have already been overwritten by a racing thread at the
     * point this call has returned.
     *
     * @param principal The {@link Principal} for which to create a session.
     * @return The session ID of the created session.
     */
    @Nonnull
    public String startWebSession(@Nonnull final Principal principal) {
        assert principal != null;

        return startSession(principal, AUTHENTICATION_SCHEME_USERNAME);
    }

    /**
     * Starts a new app session for the given {@code Principal} in the current {@code httpServletRequest}. A potentially
     * existing session is terminated first to prevent session fixation attacks.
     *
     * This method is not thread-safe in the sense that a session actually exists when this method returns,
     * because a racing request may have torn down the session that is being created here while this method is
     * executing, or the data set in this call is actually the data stored in the session at the point this method
     * returns. This cannot be prevented even with locking, because HTTP is inherently asynchronous, and even if this
     * method would lock, as soon as the method returns a racing request could then tear down the session or modify the
     * session data. However, this method guarantees that all data that is being set as part of the
     * authenticated session is set atomically, i.e. multiple racing calls will not cause the session to contain mixed
     * data of those different calls, one call will always win. Also, data set during this call is stored in the session
     * before this call returns, but as described above, may have already been overwritten by a racing thread at the
     * point this call has returned.
     *
     * @param principal The {@link Principal} for which to create a session.
     * @return The session ID of the created session.
     */
    @Nonnull
    public String startAppSession(@Nonnull final Principal principal) {
        assert principal != null;

        return startSession(principal, AUTHENTICATION_SCHEME_APPTOKEN);
    }

    /**
     * Terminates the current session. If there is no current session, the method does nothing.
     *
     * This method is not thread-safe, as a racing request may be setting up a new session for the current
     * request while this method is terminating the current session.
     */
    public void terminateSession() {
        // Check whether there actually is an ongoing session.
        @Nullable final HttpSession httpSession = getCurrentSession();
        if (httpSession == null) {
            return;
        }

        clearSessionData(httpSession);

        try {
            httpSession.invalidate();
        } catch (final IllegalStateException e) {

            // To be expected, since multiple racing HTTP requests may terminate the same session.
            LOG.info("terminateSession: session has already been terminated", e);
        }
    }

    /**
     * Returns the session ID of the current session, even if there is no user authenticated in the current
     * session. Returns {@code null} if there is no current session. Note however that the session may have
     * already been terminated by the time this method returns.
     *
     * @return The session ID of the current session, or {@code null} if there is no ongoing session.
     */
    @Nullable
    public String getCurrentSessionId() {

        // Get the current session. Return {@code null} if there is no ongoing session.
        @Nullable final HttpSession httpSession = getCurrentSession();
        if (httpSession == null) {
            return null;
        }

        return getSessionId(httpSession);
    }

    /**
     * Creates the {@link SecurityContext} for the current session. It does so by inspecting the {@link HttpSession} for
     * the presence of a username. If and only if a user is authenticated in the session, a {@link SecurityContext}
     * representing that user is created and returned. If no user is authenticated in the session, {@code null} will be
     * returned.
     *
     * Note: must not be used to access a {@link SecurityContext}, only to create the initial
     * {@code SecurityContext}! The way to access a {@code SecurityContext} is by either getting it injected via the
     * {@link javax.ws.rs.core.Context @Context} annotation, or by retrieving it from the RestEasy context stack.
     *
     * This method is not thread-safe in the sense that an authenticated session will actually exist even if
     * this method returns a {@link SecurityContext}, because a racing request may terminate the session while this
     * method here is executing. However, this method guarantees that the data in the returned {@link
     * SecurityContext} does not contain data from racing threads, but will always contain the atomic set of data that
     * was set on the authenticated session.
     *
     * @param httpServletRequest The current request.
     * @return If a user is authenticated in this session, then a {@link SecurityContext} is returned. Returns {@code
     * null} otherwise.
     */
    @Nullable
    public static SecurityContext createSecurityContextForSession(@Nonnull final HttpServletRequest httpServletRequest) {
        assert httpServletRequest != null;

        // Get the current session. {@link HttpServletRequest#getSession(boolean) getSession(false)} returns
        // {@code null} if there is no ongoing session.
        @Nullable final HttpSession httpSession = httpServletRequest.getSession(false);
        if (httpSession == null) {
            return null;
        }

        // Get the session data. The simple fact that the session contains our session data is sufficient proof that
        // the session is authenticated. Return no {@link SecurityContext} if no session data (i.e. current session is
        // not authenticated).
        @Nullable final SessionData sessionData = getSessionData(httpSession);
        if (sessionData == null) {
            return null;
        }

        final SecurityContext securityContext =
                new SecurityContextImpl(new PrincipalImpl(sessionData.getUserId()), sessionData.getAuthenticationScheme());

        return securityContext;
    }

    /**
     * Starts a new session of the given {@code authenticationScheme} for the given {@code principal} in the current
     * servlet request. A potentially existing session is terminated first to prevent session fixation attacks.
     *
     * This method is not thread-safe in the sense that a session actually exists when this method returns,
     * because a racing request may have torn down the session that is being created here while this method is
     * executing, or the data set in this call is actually the data stored in the session at the point this method
     * returns. This cannot be prevented even with locking, because HTTP is inherently asynchronous, and even if this
     * method would lock, as soon as the method returns a racing request could then tear down the session or modify the
     * session data. However, this method guarantees that all data that is being set as part of the
     * authenticated session is set atomically, i.e. multiple racing calls will not cause the session to contain mixed
     * data of those different calls, one call will always win. Also, data set during this call is stored in the session
     * before this call returns, but as described above, may have already been overwritten by a racing thread at the
     * point this call has returned.
     *
     * @param principal            The {@link Principal} for which to create a session.
     * @param authenticationScheme The authentication scheme of the session to create.
     * @return The session ID of the created session.
     */
    @Nonnull
    private String startSession(
            @Nonnull final Principal principal,
            @Nonnull final String authenticationScheme) {
        assert principal != null;
        assert authenticationScheme != null;

        @Nonnull final HttpSession httpSession = createNewSession();
        try {
            // Set authenticated user ID and authentication scheme on session. Throws an {@link IllegalStateException}
            // in case the session has been concurrently invalidated before the session data could be set.
            @Nonnull final String userId = setSessionData(httpSession, principal, authenticationScheme);
            @Nonnull final String sessionId = getSessionId(httpSession);
            LOG.debug("startSession: created new session for user with ID {}. Session ID is {}", userId, sessionId);

            return sessionId;
        } catch (final IllegalStateException e) {

            // To be expected, since a racing HTTP request may have terminated this session before we were able to set
            // the session data.
            LOG.info("startSession: session has already been terminated", e);

            // We carry on as if we were able to set the session data, because the method does not guarantee that there
            // actually is a valid authenticated session at the end of this method.
            return getSessionId(httpSession);
        }
    }

    /**
     * Returns the current session, even if there is no user authenticated in the current session. Returns
     * {@code null} if there is no current session.
     *
     * This method is not thread-safe in the sense that a concurrent request request may have invalidated the
     * returned session before this method could complete, but after it retrieved the session from the underlying
     * request. Also, a concurrent request may have created a new session after this method checked for session
     * existence but before it returned (i.e. this method would return {@code null} even though at the point it does so,
     * a session could exist).
     *
     * @return The currently ongoing session (authenticated or unauthenticated) or {@code null} if there is no current
     * session.
     */
    @Nullable
    private HttpSession getCurrentSession() {

        // Get the current session. {@link HttpServletRequest#getSession(boolean) getSession(false)} returns
        // {@code null} if there is no ongoing session.
        return httpServletRequest.getSession(false);
    }

    /**
     * Creates a new session, and terminates any existing session to prevent session fixation attacks (see https://www.owasp.org/index.php/Session_fixation).
     *
     * This method is not thread-safe in the sense that a concurrent request request may have invalidated the
     * returned session before this method could complete, but after it created the session.
     *
     * @return The newly created session.
     */
    @Nonnull
    private HttpSession createNewSession() {

        // Terminate any existing session first to prevent session fixation attacks.
        terminateSession();

        /**
         * {@link HttpServletRequest#getSession()} gets the current session or creates a new session if one does not
         * exist already. Since we've just terminated a potentially existing session, there shouldn't be a session, but
         * a concurrent thread may have created a new one already in between. That does not create a session fixation
         * attack vector, because the session ID will be different from the session ID that came in with current
         * request.
         */
        @Nonnull final HttpSession httpSession = httpServletRequest.getSession();
        assert httpSession != null; // Check explicitly since {@link HttpSession#getSession()} has no codified post-conditions.
        return httpSession;
    }

    /**
     * Returns the session ID of the given {@code httpSession}.
     *
     * @param httpSession The session for which to retrieve the session ID.
     * @return The session ID of the given {@code httpSession}.
     */
    @Nonnull
    private static String getSessionId(@Nonnull final HttpSession httpSession) {
        @Nonnull final String sessionId = httpSession.getId();
        assert sessionId != null; // Check explicitly since {@link HttpSession#getId()} has no codified post-conditions.
        return sessionId;
    }

    /**
     * Sets all data necessary to establish an authenticated session in the given {@code httpSession}. This method is
     * not thread-safe in the sense that one thread may set the data here, but another thread may have already
     * removed the data again before this method returns. However, this method ensures that all data to be set
     * in this call is either set or not (in case a racing thread terminates the session). The session will never
     * contain mixed data from different racing threads, one thread will always win.
     *
     * @param httpSession          The session to set the session data on.
     * @param principal            The {@link Principal} from which to take the user ID.
     * @param authenticationScheme The authentication scheme to set on the session.
     * @return The user ID that was set in the session.
     * @throws IllegalStateException Throws an {@link IllegalStateException} in case the session has been invalidated
     *                               before this method could set the session data.
     */
    @Nonnull
    private static String setSessionData(
            @Nonnull final HttpSession httpSession,
            @Nonnull final Principal principal,
            @Nonnull final String authenticationScheme) throws IllegalStateException {
        assert httpSession != null;
        assert principal != null;
        assert authenticationScheme != null;

        @Nonnull final String userId = principal.getName();
        final SessionData sessionData = new SessionData(userId, authenticationScheme);

        /**
         * Throws an {@link IllegalStateException} in case session has already been invalidated. Don't catch it to allow
         * the caller to deal with this situation, because unlike in the other methods, we can't decided locally what
         * the correct course of action is, since the goal of this method was to set the session data, and we may not be
         * able to.
         */
        httpSession.setAttribute(SESSION_DATA_KEY, sessionData);
        return userId;
    }

    /**
     * Gets all session data from the given {@code httpSession}. This method is not thread-safe in the sense
     * that one thread may get the data here, but another thread may have already removed the data from the session
     * before this method returns, or the current session may have been concurrently invalidated before this method can
     * read the session data. However, this method ensures that either all data is retrieved, or nothing (in
     * case a racing thread terminates the session). The returned session data will never contain mixed data from
     * different racing threads.
     *
     * @param httpSession The session to retrieve the session data from.
     * @return The session data, or {@code null} if not an authenticated session.
     */
    @Nullable
    private static SessionData getSessionData(@Nonnull final HttpSession httpSession) {
        assert httpSession != null;

        @Nullable SessionData sessionData;
        try {
            sessionData = (SessionData) httpSession.getAttribute(SESSION_DATA_KEY);
        } catch (final IllegalStateException e) {
            // To be expected, since a racing HTTP request may have terminated this session before we got here.
            LOG.info("getSessionData: session has already been terminated", e);
            sessionData = null;
        }

        return sessionData;
    }

    /**
     * Clears all session data that was previously set by the SessionManager on the given {@code httpSession} (but not
     * data set by other parties than the SessionManager). Does nothing if no session data is currently set or the
     * session has been concurrently terminated before this method could remove the session data.
     *
     * @param httpSession The {@link HttpSession} to clear.
     */
    private static void clearSessionData(@Nonnull final HttpSession httpSession) {
        assert httpSession != null;

        try {
            httpSession.removeAttribute(SESSION_DATA_KEY);
        } catch (final IllegalStateException e) {
            // To be expected, since a racing HTTP request may have terminated this session before we got here. We
            // carry on as if nothing had happened, because the goal was to remove the session data, and an invalidated
            // session will not hold the session data, hence we have achieved that goal.
            LOG.info("clearSessionData: session has already been terminated", e);
        }
    }

    /**
     * Immutable container to hold session data. The immutability ensures that either all or nothing is set in the
     * session, and racing requests cannot cause the session to contain a mixed set of data from different requests.
     *
     * This class is serializable, to enable containers to replicate session state between multiple nodes.
     *
     * Made package private for unit test.
     */
    static class SessionData implements Serializable {
        /**
         * Version of this class to support serialization. Must be increased iff a change is made to this class that
         * breaks serialisation compatibility with previous versions of this class (see http://docs.oracle.com/javase/6/docs/platform/serialization/spec/version.html#6678
         * for when exactly the version must be changed).
         */
        private static final long serialVersionUID = 1L;

        /**
         * The ID of the authenticated user in the {@link HttpSession}.
         *
         * @serial
         */
        @Nonnull
        private final String userId;

        /**
         * The authentication scheme of the authentication session.
         *
         * @serial
         */
        @Nonnull
        private final String authenticationScheme;

        /**
         * Constructs a session data object.
         *
         * Made package private for unit test.
         *
         * @param userId               The user ID of the user authenticated in this session.
         * @param authenticationScheme The authentication scheme of this session.
         */
        SessionData(
                @Nonnull final String userId,
                @Nonnull final String authenticationScheme) {
            assert userId != null;
            assert authenticationScheme != null;

            this.userId = userId;
            this.authenticationScheme = authenticationScheme;
        }

        /**
         * Returns the ID of the user authenticated in this session.
         *
         * Made package private for unit test.
         *
         * @return The ID of the authenticated user.
         */
        @Nonnull
        String getUserId() {
            return userId;
        }

        /**
         * Returns the authentication scheme of this session.
         *
         * Made package private for unit test.
         *
         * @return The authentication scheme.
         */
        @Nonnull
        String getAuthenticationScheme() {
            return authenticationScheme;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy