CsrfRequestMatcher.java

/*
 * Copyright (c) 2007-2026 MetaSolutions AB
 *
 * 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 org.entrystore.rest.springboot.security;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Component;
import org.springframework.web.util.WebUtils;

import java.util.Set;

/**
 * Matches requests that must carry a CSRF token. Three predicates compose:
 *   1. method is unsafe (POST/PUT/PATCH/DELETE)
 *   2. request carries the session cookie (auth_token by default) — Basic-auth and
 *      other token-based flows that don't establish a session are bypassed
 *   3. path is not on the explicit exemption list (login, signup, password reset,
 *      and the SAML ACS callback)
 *
 * <p>Mutating endpoints under {@code /management/**} (logging, solr reindex) are
 * intentionally NOT exempt: a cookie-authenticated user — including a regular user able
 * to POST {@code /management/solr} — is in scope for CSRF. The path-pattern protection
 * extends to any future mutating {@code /management/**} endpoint without needing matcher
 * changes. Read-only actuator/status calls remain exempt by virtue of being GETs (see
 * {@link #SAFE_METHODS}).
 * Ops scripts that drive these endpoints should authenticate via Basic auth (which sets
 * no session cookie and therefore skips CSRF) or forward {@code X-XSRF-TOKEN} like other
 * cookie-authenticated clients.
 */
@Component
public class CsrfRequestMatcher implements RequestMatcher {

	// Shared with CsrfCookieFilter — both filter and matcher agree on the same notion of "safe".
	public static final Set<String> SAFE_METHODS = Set.of("GET", "HEAD", "TRACE", "OPTIONS");

	private final String sessionCookieName;
	private final RequestMatcher exemptPaths;

	public CsrfRequestMatcher(@Value("${server.servlet.session.cookie.name:auth_token}") String sessionCookieName) {
		this.sessionCookieName = sessionCookieName;
		var pathMatcher = PathPatternRequestMatcher.withDefaults();
		this.exemptPaths = new OrRequestMatcher(
				pathMatcher.matcher(HttpMethod.POST, "/auth/cookie"),
				pathMatcher.matcher(HttpMethod.POST, "/auth/signup"),
				pathMatcher.matcher(HttpMethod.POST, "/auth/signup/confirm"),
				pathMatcher.matcher(HttpMethod.POST, "/auth/pwreset"),
				pathMatcher.matcher(HttpMethod.POST, "/auth/pwreset/confirm"),
				pathMatcher.matcher(HttpMethod.POST, "/login/saml2/sso/**"),
				// Also the 5.x assertion consumer service URL, still accepted.
				new SamlAcsRequestMatcher()
		);
	}

	@Override
	public boolean matches(HttpServletRequest request) {
		if (SAFE_METHODS.contains(request.getMethod())) {
			return false;
		}
		if (WebUtils.getCookie(request, sessionCookieName) == null) {
			return false;
		}
		return !exemptPaths.matches(request);
	}
}