SecurityConfig.java
package org.fresnel.backend.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
/**
* Stateless HTTP-Basic security configuration for the Fresnel backend.
*
* <p>Public, read-only endpoints (validate, preview, info, GET design persistence
* by id) remain accessible to unauthenticated callers so the SPA stays usable
* without login. All mutating endpoints (POST design save/persist, POST job
* submissions, hologram submit, DELETE) require an authenticated principal.
*
* <p>Two users are seeded from configuration ({@code fresnel.security.user.*} and
* {@code fresnel.security.admin.*}) — these are placeholder credentials suitable
* for local development; for any non-throwaway environment override the
* passwords via environment variables (see README).
*
* <p>Sessions are stateless (no {@code JSESSIONID}). CSRF is disabled for the
* {@code /api/**} surface because authentication is conveyed by the {@code
* Authorization} header on every request, not by a cookie that an attacker could
* piggy-back via a forged form.
*/
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// CSRF protection is disabled only for the stateless REST surface.
// Authentication is conveyed by the Authorization header on every
// request (HTTP Basic), not by a session cookie that an attacker
// could ride via a forged form, so the standard CSRF threat model
// does not apply. Any non-/api/** routes (e.g. the static SPA shell
// served from /, /index.html, /assets/**) keep the default CSRF
// protection enabled.
.csrf(csrf -> csrf.ignoringRequestMatchers("/api/**"))
.cors(cors -> {})
.authorizeHttpRequests(auth -> auth
// Public read-only endpoints.
.requestMatchers(HttpMethod.GET,
"/api/designs/persist/**",
"/api/designs/preview*",
"/api/designs/*/info",
"/api/jobs/*",
"/api/jobs/*/events",
"/api/jobs/*/result.png",
"/error", "/", "/index.html",
"/assets/**", "/static/**", "/favicon.ico").permitAll()
.requestMatchers(HttpMethod.POST,
"/api/designs/validate",
"/api/designs/preview.png",
"/api/designs/load",
"/api/designs/*/info",
"/api/designs/*/preview.png").permitAll()
// Mutating endpoints require an authenticated user.
.requestMatchers(HttpMethod.POST,
"/api/designs/save",
"/api/designs/persist",
"/api/designs/export*",
"/api/designs/*/export*",
"/api/jobs/**",
"/api/holograms/**").authenticated()
.requestMatchers(HttpMethod.DELETE, "/api/**").authenticated()
.anyRequest().permitAll())
.httpBasic(basic -> {});
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public InMemoryUserDetailsManager userDetailsService(
PasswordEncoder encoder,
@Value("${fresnel.security.user.username:user}") String userName,
@Value("${fresnel.security.user.password:user}") String userPassword,
@Value("${fresnel.security.admin.username:admin}") String adminName,
@Value("${fresnel.security.admin.password:admin}") String adminPassword) {
UserDetails user = User.withUsername(userName)
.password(encoder.encode(userPassword))
.roles("USER")
.build();
UserDetails admin = User.withUsername(adminName)
.password(encoder.encode(adminPassword))
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}