Skip to content

12178 session cookie CSRF protections#12188

Draft
ErykKul wants to merge 3 commits intodevelopfrom
12178_CSRF_session_cookie_CSRF_protections
Draft

12178 session cookie CSRF protections#12188
ErykKul wants to merge 3 commits intodevelopfrom
12178_CSRF_session_cookie_CSRF_protections

Conversation

@ErykKul
Copy link
Collaborator

@ErykKul ErykKul commented Feb 27, 2026

What this PR does / why we need it:
This PR adds an opt-in hardening layer for API session-cookie auth (JSESSIONID) so JSF/SAML-style deployments can use session auth with stronger CSRF protections, without breaking existing token-based API clients.

Implemented changes:

  • Adds a new feature flag: dataverse.feature.api-session-auth-hardening (FeatureFlags.API_SESSION_AUTH_HARDENING).
  • Adds request auth-mechanism tagging in API auth flow (apiKey, workflowKey, signedUrl, bearerToken, sessionCookie, none) via CompoundAuthMechanism.
  • Updates SessionCookieAuthMechanism to return only authenticated session users (guest session no longer tagged as session-cookie auth).
  • Adds per-session API CSRF token lifecycle in DataverseSession (getOrCreateApiCsrfToken, matchesApiCsrfToken, clearApiCsrfToken).
  • Adds GET /api/users/:csrf-token (auth required) for session-cookie clients to fetch CSRF token.
  • Adds hardening enforcement in AuthFilter (only when hardening flag is enabled and auth mechanism is sessionCookie):
    • Requires Origin/Referer + X-Dataverse-CSRF-Token for state-changing methods (POST/PUT/PATCH/DELETE).
    • Applies same checks to the two known mutating GET endpoints:
      • GET /api/datasets/{id}/uploadurls
      • GET /api/datasets/{id}/cleanStorage
    • Adds /api/access/* guardrails:
      • keeps safe methods (GET/HEAD/OPTIONS) allowed
      • keeps POST /api/access/datafiles allowed with same-origin validation
      • blocks other mutating /api/access/* for session-cookie auth
  • Keeps behavior unchanged when hardening flag is off.

Which issue(s) this PR closes:

Special notes for your reviewer:

  • Scope is intentionally hardening-only and feature-flagged; default behavior is unchanged.
  • JSF compatibility is preserved for download paths (GET /api/access/* and same-origin POST /api/access/datafiles).
  • Path normalization in AuthFilter handles optional api/ prefix and trailing slashes for robust matching.
  • @since for the new flag is set to Dataverse 6.9 to match current branch revision.

Suggestions on how to test this:

  1. Run focused tests:
    • AuthFilterTest
    • DataverseSessionTest
    • SessionCookieAuthMechanismTest
    • CompoundAuthMechanismTest
  2. Manual verification with flags:
    • Enable dataverse.feature.api-session-auth=true
    • Enable dataverse.feature.api-session-auth-hardening=true
  3. Login through JSF (any supported IdP path: builtin/Shibboleth/OIDC/etc.), then:
    • GET /api/users/:csrf-token with session cookie returns token.
    • State-changing API call with correct Origin + X-Dataverse-CSRF-Token succeeds.
    • Same call missing/invalid Origin or CSRF token returns 403.
    • GET /api/datasets/{id}/uploadurls and GET /api/datasets/{id}/cleanStorage require Origin + CSRF token under hardening.
    • POST /api/access/datafiles is allowed for same-origin and blocked for cross-origin.
    • Mutating /api/access/* endpoints other than batch download POST are blocked for session-cookie auth.

Does this PR introduce a user interface change? If mockups are available, please link/include them here:
No direct UI change.

Is there a release notes update needed for this change?:
Yes, included:

  • doc/release-notes/12178-session-cookie-api-hardening.md

Additional documentation:

  • Installation/config docs updated:
    • doc/sphinx-guides/source/installation/config.rst
  • Native API docs updated:
    • doc/sphinx-guides/source/api/native-api.rst

Security model note: session-cookie hardening vs bearer token

This PR does not claim session-cookie auth is identical to bearer auth, but with hardening enabled it reaches a comparable security posture for first-party browser calls.

  • Session-cookie auth (JSESSIONID) is normally vulnerable to CSRF because cookies are sent automatically by browsers.
  • This PR adds explicit anti-CSRF controls for session-cookie requests:
    • strict Origin/Referer validation
    • X-Dataverse-CSRF-Token validation for state-changing requests
    • guardrails on /api/access/* plus mutating GET guardrails
  • Result: cross-site forged requests are blocked under the hardening flag.

Comparison to bearer:

  • Bearer is not auto-sent by the browser, so CSRF exposure is lower by default.
  • Session cookies with HttpOnly can reduce JS-level token exposure compared to bearer tokens stored in browser-accessible storage.
  • Both mechanisms remain vulnerable to credential theft (session hijack or token theft), so HTTPS, secure cookie/token handling, and XSS prevention are still required.

Additional documentation:

- Added CSRF token management to DataverseSession for session-cookie authentication.
- Introduced new feature flag `dataverse.feature.api-session-auth-hardening` to enable CSRF protections.
- Enhanced AuthFilter to validate CSRF tokens and request origins for state-changing API calls.
- Updated CompoundAuthMechanism to tag authentication mechanisms in requests.
- Created new endpoint `GET /api/users/:csrf-token` for clients to retrieve CSRF tokens.
- Added tests for new functionality and updated existing tests to cover new behavior.
- Documented changes in release notes for clarity on new security features.
@github-actions github-actions bot added the Type: Feature a feature request label Feb 27, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an opt-in “API session-cookie auth hardening” track to support JSESSIONID-based API usage with CSRF protections and endpoint guardrails, without changing default behavior when the flag is off.

Changes:

  • Adds auth-mechanism tagging to the API auth flow (apiKey, workflowKey, signedUrl, bearerToken, sessionCookie, none) and ensures guest sessions are not treated as session-cookie auth.
  • Implements session-scoped API CSRF token lifecycle in DataverseSession and introduces GET /api/users/:csrf-token to retrieve it (auth required).
  • Enforces hardening rules in AuthFilter (Origin/Referer + X-Dataverse-CSRF-Token for mutating requests, plus /api/access/* and mutating-GET guardrails) and adds focused tests/docs.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/test/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanismTest.java Adds coverage ensuring guest sessions don’t authenticate via session-cookie mechanism.
src/test/java/edu/harvard/iq/dataverse/api/auth/CompoundAuthMechanismTest.java Updates tests to assert auth-mechanism tagging behavior.
src/test/java/edu/harvard/iq/dataverse/api/auth/AuthFilterTest.java New tests for session-cookie hardening behavior (Origin/CSRF/access guardrails).
src/test/java/edu/harvard/iq/dataverse/DataverseSessionTest.java New tests for CSRF token creation/reuse/clear lifecycle.
src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java Adds API_SESSION_AUTH_HARDENING feature flag and updates related documentation.
src/main/java/edu/harvard/iq/dataverse/api/auth/SessionCookieAuthMechanism.java Ensures only authenticated (non-guest) session users are treated as session-cookie auth.
src/main/java/edu/harvard/iq/dataverse/api/auth/CompoundAuthMechanism.java Sets request auth-mechanism tag on ContainerRequestContext.
src/main/java/edu/harvard/iq/dataverse/api/auth/AuthFilter.java Implements session-cookie CSRF/origin enforcement + /api/access/* and mutating-GET guardrails behind the hardening flag.
src/main/java/edu/harvard/iq/dataverse/api/Users.java Adds GET /api/users/:csrf-token endpoint for session-cookie clients (hardening required).
src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java Adds constants for auth-mechanism tagging and CSRF header name.
src/main/java/edu/harvard/iq/dataverse/DataverseSession.java Adds per-session API CSRF token storage and clears it on user changes.
doc/sphinx-guides/source/installation/config.rst Documents new hardening flag and deployment guidance for cookie attributes.
doc/sphinx-guides/source/api/native-api.rst Documents CSRF token endpoint and usage for session-cookie clients.
doc/release-notes/12178-session-cookie-api-hardening.md Adds release note entry for the new hardening track.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +131 to +142
public String getOrCreateApiCsrfToken() {
if (apiCsrfToken == null || apiCsrfToken.isBlank()) {
apiCsrfToken = UUID.randomUUID().toString();
}
return apiCsrfToken;
}

public boolean matchesApiCsrfToken(String token) {
return token != null && apiCsrfToken != null && apiCsrfToken.equals(token);
}

public void clearApiCsrfToken() {
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getOrCreateApiCsrfToken() lazily initializes apiCsrfToken without any synchronization. Because DataverseSession is session-scoped, concurrent requests on the same session can race and generate/overwrite different tokens, causing intermittent CSRF validation failures for legitimate clients. Consider making token initialization atomic (e.g., synchronize initialization, use an AtomicReference, or generate once in a @PostConstruct), so a token can’t be replaced mid-session by a parallel request.

Suggested change
public String getOrCreateApiCsrfToken() {
if (apiCsrfToken == null || apiCsrfToken.isBlank()) {
apiCsrfToken = UUID.randomUUID().toString();
}
return apiCsrfToken;
}
public boolean matchesApiCsrfToken(String token) {
return token != null && apiCsrfToken != null && apiCsrfToken.equals(token);
}
public void clearApiCsrfToken() {
public synchronized String getOrCreateApiCsrfToken() {
if (apiCsrfToken == null || apiCsrfToken.isBlank()) {
apiCsrfToken = UUID.randomUUID().toString();
}
return apiCsrfToken;
}
public synchronized boolean matchesApiCsrfToken(String token) {
return token != null && apiCsrfToken != null && apiCsrfToken.equals(token);
}
public synchronized void clearApiCsrfToken() {

Copilot uses AI. Check for mistakes.
@github-actions

This comment has been minimized.

1 similar comment
@github-actions
Copy link

📦 Pushed preview images as

ghcr.io/gdcc/dataverse:12178-CSRF-session-cookie-CSRF-protections
ghcr.io/gdcc/configbaker:12178-CSRF-session-cookie-CSRF-protections

🚢 See on GHCR. Use by referencing with full name as printed above, mind the registry name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Feature a feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Session-Cookie API Hardening with CSRF Protections and Mutating-GET Guardrails

2 participants