Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ebrains.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
tags:
- "*"

permissions:
contents: read

jobs:
to_ebrains:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/publish_images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
release:
types: [published]

permissions:
contents: read

env:
HARBOR_REGISTRY: docker-registry.ebrains.eu
DOCKER_BUILDKIT: 1
Expand Down
27 changes: 16 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#######################################################
# Build the spring boot maven project
#######################################################
FROM maven:3.9.16-amazoncorretto-21 AS mvn-build-env
# Digest is the multi-arch index for this tag (registry, 2026-11-09).
FROM maven:3.9.16-amazoncorretto-21@sha256:efdf660ec74911594e2a7f642e177ffba21b741c8e848223bf41a6d0fc367692 AS mvn-build-env
LABEL maintainer="Thanasis Karampatsis <tkarabatsis@athenarc.gr>"

ENV CODE_PATH="/opt/code"
Expand All @@ -19,7 +20,8 @@ RUN mvn -B -ntp clean package
#######################################################
# Setup the running container
#######################################################
FROM amazoncorretto:21-alpine3.24
# Digest is the multi-arch index for this tag (registry, 2026-11-09).
FROM amazoncorretto:21-alpine3.24@sha256:e37320b8ccdb61ed4272edf1f3c9836ca4ea660b37f048dbb6858901d5322145

#######################################################
# Setting up timezone
Expand All @@ -37,11 +39,14 @@ ENV FRAMEWORK_LOG_LEVEL="INFO"

WORKDIR /opt

# libcrypto3/libssl3 are refreshed because the amazoncorretto base image ships
# openssl 3.5.7-r0, which carries CVE-2026-14456, CVE-2026-63073 and
# CVE-2026-75803 (CVSS >= 8.0, blocks the container SCA gate). Alpine 3.24
# fixes them in openssl 3.5.8-r0.
RUN apk add --no-cache --upgrade libcrypto3 libssl3 curl
# Pin Alpine 3.24 versions. openssl 3.5.9-r0 is past 3.5.8-r0, which fixed
# CVE-2026-14456, CVE-2026-63073 and CVE-2026-75803. zlib 1.4-r0 fixes
# CVE-2026-4738 (the base image still ships 1.3.2-r1).
RUN apk add --no-cache \
libcrypto3=3.5.9-r0 \
libssl3=3.5.9-r0 \
curl=8.23.0-r0 \
zlib=1.4-r0

#######################################################
# Prepare the spring boot application files
Expand All @@ -50,11 +55,11 @@ COPY --from=mvn-build-env /opt/code/target/platform-backend.jar /usr/share/jars/

VOLUME /opt/platform/api

RUN addgroup -S appgroup && adduser -S appuser -G appgroup \
RUN addgroup -g 10001 appgroup && adduser -D -H -u 10001 -G appgroup appuser \
&& mkdir -p /opt/platform/api \
&& chown -R appuser:appgroup /opt/platform/api /usr/share/jars
&& chown -R 10001:10001 /opt/platform/api /usr/share/jars

USER appuser
USER 10001
ENTRYPOINT ["java", "--add-opens", "java.base/java.io=ALL-UNNAMED", "-Daeron.term.buffer.length", "-jar", "/usr/share/jars/platform-backend.jar"]
EXPOSE 8080
HEALTHCHECK --start-period=60s CMD curl --fail --silent --show-error http://localhost:8080/services/actuator/health | grep -q '"status":"UP"'
HEALTHCHECK --start-period=60s CMD ["sh", "-c", "set -o pipefail; curl --fail --silent --show-error http://localhost:8080/services/actuator/health | grep -q '\"status\":\"UP\"'"]
1 change: 1 addition & 0 deletions docs/context/session-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Not limited. One account may hold several portal sessions, laptop sign-ins and a
- DPoP for the laptop client: with `KEYCLOAK_CLI_REQUIRE_DPOP` on, the laptop client's (`mip-cli-<federation>`) tokens must be DPoP-bound (RFC 9449) and arrive with the `DPoP` scheme, and Spring verifies the DPoP proof (signature, `htu`/`htm`, `ath` — the SHA-256 of the access token, `iat`, replay and the token's `cnf.jkt` against the proof's key). Platform-client tokens (portal, notebooks) keep working over `Bearer`; remove the flag once every federation has switched. The proof's `htu` is the request URL, so nginx must pass `X-Forwarded-Proto`/`X-Forwarded-Host` for the backend location.
- Turn-on order: while the flag is off, a DPoP-bound token sent with `Bearer` is accepted without a proof (the binding is not enforced). With the flag on before clients send DPoP, every laptop user fails. Turn on Keycloak DPoP for the laptop client, this flag, and a DPoP-capable mip client release in one change window.
- The proof replay check keeps recent `jti` values in memory, per backend replica: with more than one replica, a proof can be replayed on another replica within the short `iat` window (it stays bound to one method and URL). Acceptable for now; use a shared cache if it matters later.
- JupyterHub login cookies expired on portal sign-out are marked Secure when the request is HTTPS (`COOKIE_SECURE`, default true, including `X-Forwarded-Proto`). Set `COOKIE_SECURE=false` for a portal served over plain HTTP, so those cookies can still be cleared.

## Not available

Expand Down
20 changes: 17 additions & 3 deletions src/main/java/hbp/mip/configurations/SecurityConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import hbp.mip.user.ActiveUserService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -17,6 +18,7 @@
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
Expand Down Expand Up @@ -90,6 +92,10 @@ public class SecurityConfiguration {
@Value("${authentication.cli_require_dpop:false}")
private boolean cliRequireDpop;

// Secure on HTTPS. COOKIE_SECURE=false keeps clearing these cookies over plain HTTP.
@Value("${authentication.secure_cookies:true}")
private boolean secureCookies;

@Value("${server.port:8080}")
private int serverPort;

Expand Down Expand Up @@ -163,8 +169,7 @@ SecurityFilterChain clientSecurityFilterChain(HttpSecurity http,
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(
ClearSiteDataHeaderWriter.Directive.CACHE,
ClearSiteDataHeaderWriter.Directive.STORAGE)))
.addLogoutHandler(new CookieClearingLogoutHandler(
expiredHubCookie("jupyterhub-hub-login"), expiredHubCookie("jupyterhub-session-id")))
.addLogoutHandler(this::clearHubCookies)
.deleteCookies("JSESSIONID")
.logoutSuccessHandler(successHandler));

Expand Down Expand Up @@ -222,10 +227,19 @@ OidcSessionRegistry oidcSessionRegistry() {

// ponytail: assumes the hub's default base path; a setting if a deployment moves it. A notebook server's own
// cookie is not cleared, because its path holds the username: it ends at the hub's next token refresh.
private static Cookie expiredHubCookie(String name) {
private void clearHubCookies(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
new CookieClearingLogoutHandler(
expiredHubCookie("jupyterhub-hub-login", request.isSecure()),
expiredHubCookie("jupyterhub-session-id", request.isSecure()))
.logout(request, response, authentication);
}

private Cookie expiredHubCookie(String name, boolean requestSecure) {
Cookie cookie = new Cookie(name, null);
cookie.setPath("/notebook/hub/");
cookie.setMaxAge(0);
cookie.setSecure(secureCookies && requestSecure);
return cookie;
}

Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ authentication:
cli_require_dpop: ${KEYCLOAK_CLI_REQUIRE_DPOP:false}
# Portal sessions end at this age however active they are (8h or PT8H).
session_max_age: ${SESSION_MAX_AGE:8h}
# Secure attribute on the JupyterHub cookies cleared at sign-out. Default on: the
# attribute is set when the request is HTTPS (including X-Forwarded-Proto). Set
# COOKIE_SECURE=false so a portal served over plain HTTP can still clear them.
secure_cookies: ${COOKIE_SECURE:true}

### EXTERNAL SERVICES ###
services:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,9 @@ void logoutClearsTheSiteDataOverHttps() throws Exception {
mvc.perform(post("/services/logout").contextPath("/services").secure(true).session(session("alice")))
.andExpect(header().string("Clear-Site-Data", "\"cache\", \"storage\""))
.andExpect(cookie().maxAge("jupyterhub-hub-login", 0))
.andExpect(cookie().path("jupyterhub-hub-login", "/notebook/hub/"));
.andExpect(cookie().path("jupyterhub-hub-login", "/notebook/hub/"))
.andExpect(cookie().secure("jupyterhub-hub-login", true))
.andExpect(cookie().secure("jupyterhub-session-id", true));
}

@Test
Expand All @@ -316,7 +318,9 @@ void loopbackLogoutEndsTheSessionWithoutACsrfToken() throws Exception {
MockHttpSession session = session("alice");

mvc.perform(post("/services/logout").contextPath("/services").session(session))
.andExpect(status().is3xxRedirection());
.andExpect(status().is3xxRedirection())
.andExpect(cookie().secure("jupyterhub-hub-login", false))
.andExpect(cookie().secure("jupyterhub-session-id", false));

assertThat(session.isInvalid()).isTrue();
}
Expand Down
Loading