diff --git a/pom.xml b/pom.xml
index d693ad2..ef7e1a0 100755
--- a/pom.xml
+++ b/pom.xml
@@ -14,9 +14,9 @@
swiss-backend
Swiss Backend
- 25
- 25
- 25
+ 21
+ 21
+ 21
@@ -50,10 +50,6 @@
h2
runtime
-
- org.springframework.boot
- spring-boot-starter-security
-
org.springframework.boot
spring-boot-starter-web
@@ -155,8 +151,8 @@
maven-compiler-plugin
3.14.1
- 25
- 25
+ 21
+ 21
org.projectlombok
diff --git a/src/main/java/nl/connectedit/swiss/auth/AuthController.java b/src/main/java/nl/connectedit/swiss/auth/AuthController.java
index 8e21159..3b2b88c 100644
--- a/src/main/java/nl/connectedit/swiss/auth/AuthController.java
+++ b/src/main/java/nl/connectedit/swiss/auth/AuthController.java
@@ -1,36 +1,48 @@
package nl.connectedit.swiss.auth;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
-import org.springframework.web.bind.annotation.CrossOrigin;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
import java.time.Instant;
import java.util.stream.Collectors;
@RestController
-@CrossOrigin
@RequestMapping("/api/auth")
public class AuthController {
private final JwtEncoder encoder;
+ @Value("${jwt.cookie.secure:true}")
+ private boolean secureCookie;
+
+ @Value("${jwt.cookie.samesite:false")
+ private String sameSite;
+
+ @Value("${jwt.cookie.domain:}")
+ private String cookieDomain;
+
public AuthController(JwtEncoder encoder) {
this.encoder = encoder;
}
@PostMapping
- public String auth(Authentication authentication) {
+ public void auth(Authentication authentication, HttpServletResponse response) {
Instant now = Instant.now();
long expiry = 36000L;
+
String scope = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
+
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
@@ -38,7 +50,67 @@ public class AuthController {
.subject(authentication.getName())
.claim("scope", scope)
.build();
- return this.encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
+
+ String token = this.encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
+
+ // Create HttpOnly cookie
+ Cookie cookie = new Cookie("AUTH-TOKEN", token);
+ cookie.setHttpOnly(true);
+ cookie.setSecure(secureCookie); // true for production (HTTPS), false for local dev
+ cookie.setPath("/");
+ cookie.setMaxAge((int) expiry); // 10 hours in seconds
+
+// String sameSite = "None"; // required for cross-origin
+ boolean secure = secureCookie; // true in production, false for local dev
+
+ response.setHeader("Set-Cookie", String.format(
+ "%s=%s; Path=%s; Max-Age=%d; HttpOnly; %s SameSite=%s%s",
+ cookie.getName(),
+ cookie.getValue(),
+ cookie.getPath(),
+ cookie.getMaxAge(),
+ secure ? "Secure;" : "",
+ sameSite,
+ cookieDomain.isEmpty() ? "" : "; Domain=" + cookieDomain
+ ));
+
+ response.setStatus(HttpServletResponse.SC_OK);
}
-}
+ @PostMapping("/logout")
+ public void logout(HttpServletResponse response) {
+ // Clear the cookie by setting Max-Age to 0
+ Cookie cookie = new Cookie("AUTH-TOKEN", "");
+ cookie.setHttpOnly(true);
+ cookie.setSecure(secureCookie);
+ cookie.setPath("/");
+ cookie.setMaxAge(0);
+
+ response.setHeader("Set-Cookie", String.format(
+ "%s=%s; Path=%s; Max-Age=%d; HttpOnly; %s SameSite=Lax",
+ cookie.getName(),
+ cookie.getValue(),
+ cookie.getPath(),
+ cookie.getMaxAge(),
+ secureCookie ? "Secure;" : ""
+ ));
+
+ response.setStatus(HttpServletResponse.SC_OK);
+ }
+
+ @GetMapping("/me")
+ public ResponseEntity> me(Authentication authentication) {
+ if (authentication == null) {
+ return ResponseEntity.status(HttpServletResponse.SC_UNAUTHORIZED).build();
+ }
+
+ return ResponseEntity.ok(
+ Map.of(
+ "username", authentication.getName(),
+ "roles", authentication.getAuthorities().stream()
+ .map(GrantedAuthority::getAuthority)
+ .toList()
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/nl/connectedit/swiss/config/CookieJwtAuthenticationFilter.java b/src/main/java/nl/connectedit/swiss/config/CookieJwtAuthenticationFilter.java
new file mode 100644
index 0000000..50a205e
--- /dev/null
+++ b/src/main/java/nl/connectedit/swiss/config/CookieJwtAuthenticationFilter.java
@@ -0,0 +1,58 @@
+package nl.connectedit.swiss.config;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+public class CookieJwtAuthenticationFilter extends OncePerRequestFilter {
+
+ private final JwtDecoder jwtDecoder;
+ private final JwtAuthenticationConverter jwtAuthenticationConverter;
+
+ public CookieJwtAuthenticationFilter(JwtDecoder jwtDecoder,
+ JwtAuthenticationConverter jwtAuthenticationConverter) {
+ this.jwtDecoder = jwtDecoder;
+ this.jwtAuthenticationConverter = jwtAuthenticationConverter;
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+
+ // Extract token from cookies only
+ String token = null;
+ Cookie[] cookies = request.getCookies();
+
+ if (cookies != null) {
+ for (Cookie cookie : cookies) {
+ if ("AUTH-TOKEN".equals(cookie.getName())) {
+ token = cookie.getValue();
+ break;
+ }
+ }
+ }
+
+ if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+ try {
+ Jwt jwt = jwtDecoder.decode(token);
+ var authentication = jwtAuthenticationConverter.convert(jwt);
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ } catch (Exception e) {
+ // Invalid token, continue without authentication
+ logger.debug("JWT validation failed: " + e.getMessage());
+ }
+ }
+
+ filterChain.doFilter(request, response);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/nl/connectedit/swiss/config/SecurityConfig.java b/src/main/java/nl/connectedit/swiss/config/SecurityConfig.java
index 1ebb3c7..eada35f 100644
--- a/src/main/java/nl/connectedit/swiss/config/SecurityConfig.java
+++ b/src/main/java/nl/connectedit/swiss/config/SecurityConfig.java
@@ -6,8 +6,6 @@ import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
-import jakarta.servlet.DispatcherType;
-import nl.connectedit.swiss.auth.Roles;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -15,8 +13,6 @@ import org.springframework.http.HttpMethod;
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.config.annotation.web.configurers.AbstractHttpConfigurer;
-import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -30,6 +26,7 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAut
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
+import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
@@ -40,6 +37,8 @@ import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
+import nl.connectedit.swiss.auth.Roles;
+
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@@ -57,16 +56,20 @@ public class SecurityConfig {
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
- .csrf(csrf -> csrf.ignoringRequestMatchers("/api/auth"))
+ .csrf(csrf -> csrf.ignoringRequestMatchers("/api/auth", "/api/auth/logout", "/api/auth/me"))
.authorizeHttpRequests(authorize -> authorize
- // Allow OPTIONS requests for CORS preflight
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
- // Allow POST to /api/auth without JWT (Basic Auth will still be required)
.requestMatchers(HttpMethod.POST, "/api/auth").permitAll()
- // All other requests require authentication
+ .requestMatchers(HttpMethod.POST, "/api/auth/logout").permitAll()
+ .requestMatchers(HttpMethod.GET, "/api/auth/me").authenticated()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
+ // Add cookie-based JWT filter before the Bearer token filter
+ .addFilterBefore(
+ new CookieJwtAuthenticationFilter(jwtDecoder(), jwtAuthenticationConverter()),
+ BearerTokenAuthenticationFilter.class
+ )
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptions -> exceptions
@@ -80,11 +83,9 @@ public class SecurityConfig {
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
- // Allow your Angular app's origin(s)
configuration.setAllowedOrigins(Arrays.asList(
"http://localhost:4200",
"http://localhost:8080",
- // Add your production URL here when deploying
"https://badminton-toernooi.nl",
"https://test.badminton-toernooi.nl"
));
@@ -99,6 +100,7 @@ public class SecurityConfig {
"Accept"
));
+ // CRITICAL: Must be true for cookies to work cross-origin
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
@@ -137,12 +139,10 @@ public class SecurityConfig {
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
- // Remove the SCOPE_ prefix
grantedAuthoritiesConverter.setAuthorityPrefix("");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
-
-}
+}
\ No newline at end of file
diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml
index 147d732..dc79da1 100755
--- a/src/main/resources/application-local.yaml
+++ b/src/main/resources/application-local.yaml
@@ -1,6 +1,8 @@
+
spring:
application:
name: swiss
+
# autoconfigure:
# exclude:
# - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
@@ -37,3 +39,10 @@ security: true
# descriptor:
# sql:
# BasicBinder: TRACE
+server:
+ port: 8080
+
+jwt:
+ cookie:
+ secure: false
+ samesite: Lax
\ No newline at end of file
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 83d09de..f5b8940 100755
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -37,3 +37,6 @@ jwt:
key: classpath:certs/private.pem
public:
key: classpath:certs/public.pem
+ cookie:
+ secure: true
+ samesite: None
\ No newline at end of file