CookieFilter.java
/*
* The coLAB project
* Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
*
* Licensed under the MIT License
*/
package ch.colabproject.colab.api.security;
import ch.colabproject.colab.api.controller.RequestManager;
import ch.colabproject.colab.api.model.user.HttpSession;
import java.io.IOException;
import javax.annotation.Priority;
import javax.inject.Inject;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.ext.Provider;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Intercept all request to the API. Make sure COLAB_SESSION_ID exists
* <p>
* With a priority of 1, this {@link PreMatching @PreMatching} filter is the very first to be
* executed
*
* @author maxence
*/
@Provider
@Priority(1)
@PreMatching
public class CookieFilter implements ContainerRequestFilter, ContainerResponseFilter {
/** Default max-age to one week [s] */
private static final int COOKIE_MAX_AGE = 3600 * 24 * 7;
/**
* Logger
*/
private static final Logger logger = LoggerFactory.getLogger(CookieFilter.class);
/**
* Cluster-wide session cache.
*/
@Inject
private SessionManager sessionManager;
/**
* Request related logic
*/
@Inject
private RequestManager requestManager;
/**
* Name of the cookie
*/
private static final String COOKIE_NAME = "COLAB_SESSION_ID";
/**
* To parse cookie values
*/
private static class ParsedCookie {
/**
* Http Session id
*/
private Long id = null;
/**
* Http session secret
*/
private String secret = null;
/**
* Parse the cookie value
*
* @param value cookie value to parse
*/
public ParsedCookie(String value) {
logger.trace("Parse cookie value");
// The cookie: uid=1234:v=<SECRET>
String[] split = value.split(":");
if (split.length == 2) {
try {
if (split[0].length() >= 5 && split[1].length() >= 3) {
this.id = Long.parseLong(split[0].substring(4), 10);
this.secret = split[1].substring(2);
} else {
logger.error("Invalid cookie: struct not match");
}
} catch (NumberFormatException ex) {
logger.error("Invalid cookie: v=<NOT_A_NUBER>;...");
}
}
}
}
/**
* Intercept request and make sure a httpSession is bound to the request
*
* @param requestContext request context
*
* @throws IOException if an I/O exception occurs.
*/
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
Cookie cookie = requestContext.getCookies().get(COOKIE_NAME);
if (cookie != null) {
String cookieValue = cookie.getValue();
logger.trace("Request received with session id {}", cookieValue);
ParsedCookie parsedCookie = new ParsedCookie(cookieValue);
if (parsedCookie.id != null && parsedCookie.secret != null) {
HttpSession httpSession = sessionManager.getAndValidate(parsedCookie.id,
parsedCookie.secret);
if (httpSession != null) {
requestManager.setHttpSessionId(httpSession.getId());
return;
}
} else {
logger.debug("Invalid cookie: reject");
}
requestManager.setHttpSessionId(null);
}
}
/**
* Intercept response, save httpSession in sessions cache and make sure set-cookie header is set
*
* @param requestContext the request context
* @param responseContext the response context
*
* @throws IOException if an I/O exception occurs.
*/
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) throws IOException {
HttpSession session = requestManager.getHttpSession();
Cookie cookie = requestContext.getCookies().get(COOKIE_NAME);
if (session != null) {
String cookieValue = null;
if (cookie != null && !StringUtils.isEmpty(cookie.getValue())) {
cookieValue = cookie.getValue();
ParsedCookie parsedCookie = new ParsedCookie(cookieValue);
if (!parsedCookie.id.equals(session.getId())) {
logger.trace("New httpSession detected");
// session changed during the request => clear cookieValue to force to generate
// a full new cookie
cookieValue = null;
}
}
if (cookieValue == null) {
logger.trace("CookieValue not set: build from rawSecret");
// CookieValue not set -> build from
if (StringUtils.isBlank(session.getRawSessionSecret())) {
// at login, new httpSession is created (SessionManager.createHttpSession)
// the raw secret must be available here to be sent to client
logger.error("COOKIE VALUE IS NOT SET");
}
cookieValue = "uid=" + session.getId() + ":v=" + session.getRawSessionSecret();
}
NewCookie sessionCookie = new NewCookie(COOKIE_NAME, cookieValue,
"/", null, null, COOKIE_MAX_AGE, true, true);
// hack: SameSite not handled yet by jakarta rs library
// inject SameSite=Lax by hand
String theCookie = sessionCookie.toString() + ";SameSite=Lax";
logger.trace("Request completed with session id {}", session.getId());
responseContext.getHeaders().add(HttpHeaders.SET_COOKIE, theCookie);
} else {
// not session => clear cookie if exists
if (cookie != null) {
// Clear cookie by setting no value and max-age=0
NewCookie sessionCookie = new NewCookie(COOKIE_NAME, "",
"/", null, null, 0, true, true);
logger.trace("Request completed with session id {}", sessionCookie);
responseContext.getHeaders().add(HttpHeaders.SET_COOKIE, sessionCookie);
}
}
}
}