diff --git a/pom.xml b/pom.xml
index 9fdc693..5e5cfe7 100755
--- a/pom.xml
+++ b/pom.xml
@@ -31,6 +31,10 @@
org.springframework.boot
spring-boot-starter-security
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
com.auth0
java-jwt
diff --git a/src/main/java/nl/connectedit/swiss/auth/AuthController.java b/src/main/java/nl/connectedit/swiss/auth/AuthController.java
new file mode 100644
index 0000000..8e21159
--- /dev/null
+++ b/src/main/java/nl/connectedit/swiss/auth/AuthController.java
@@ -0,0 +1,44 @@
+package nl.connectedit.swiss.auth;
+
+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 java.time.Instant;
+import java.util.stream.Collectors;
+
+@RestController
+@CrossOrigin
+@RequestMapping("/api/auth")
+public class AuthController {
+
+ private final JwtEncoder encoder;
+
+ public AuthController(JwtEncoder encoder) {
+ this.encoder = encoder;
+ }
+
+ @PostMapping
+ public String auth(Authentication authentication) {
+ 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)
+ .expiresAt(now.plusSeconds(expiry))
+ .subject(authentication.getName())
+ .claim("scope", scope)
+ .build();
+ return this.encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
+ }
+
+}
diff --git a/src/main/java/nl/connectedit/swiss/auth/Roles.java b/src/main/java/nl/connectedit/swiss/auth/Roles.java
new file mode 100644
index 0000000..112d941
--- /dev/null
+++ b/src/main/java/nl/connectedit/swiss/auth/Roles.java
@@ -0,0 +1,9 @@
+package nl.connectedit.swiss.auth;
+
+public final class Roles {
+
+ public static final String USER = "USER";
+
+ private Roles() {
+ }
+}
\ 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 846858c..1ebb3c7 100644
--- a/src/main/java/nl/connectedit/swiss/config/SecurityConfig.java
+++ b/src/main/java/nl/connectedit/swiss/config/SecurityConfig.java
@@ -1,26 +1,148 @@
package nl.connectedit.swiss.config;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+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;
+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;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtEncoder;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
+import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
+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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
-//@Configuration
-//@EnableWebSecurity
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Arrays;
+
+@Configuration
+@EnableWebSecurity
public class SecurityConfig {
-// @Bean
- public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ private final RSAPublicKey key;
+ private final RSAPrivateKey priv;
+
+ public SecurityConfig(@Value("${jwt.public.key}") RSAPublicKey key,
+ @Value("${jwt.private.key}") RSAPrivateKey priv) {
+ this.key = key;
+ this.priv = priv;
+ }
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
- .csrf(AbstractHttpConfigurer::disable)
- .authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
-// .authorizeHttpRequests(request -> {
-// request.dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll();
-// request.requestMatchers("/error").permitAll();
-// });
+ .cors(cors -> cors.configurationSource(corsConfigurationSource()))
+ .csrf(csrf -> csrf.ignoringRequestMatchers("/api/auth"))
+ .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
+ .anyRequest().authenticated()
+ )
+ .httpBasic(Customizer.withDefaults())
+ .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .exceptionHandling(exceptions -> exceptions
+ .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
+ .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
+ );
return http.build();
}
+
+ @Bean
+ 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"
+ ));
+
+ configuration.setAllowedMethods(Arrays.asList(
+ "GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"
+ ));
+
+ configuration.setAllowedHeaders(Arrays.asList(
+ "Authorization",
+ "Content-Type",
+ "Accept"
+ ));
+
+ configuration.setAllowCredentials(true);
+ configuration.setMaxAge(3600L);
+
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", configuration);
+ return source;
+ }
+
+ @Bean
+ UserDetailsService users() {
+ return new InMemoryUserDetailsManager(
+ User.withUsername("bcholten")
+ .password(passwordEncoder().encode("bcholten"))
+ .roles(Roles.USER)
+ .build()
+ );
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ JwtDecoder jwtDecoder() {
+ return NimbusJwtDecoder.withPublicKey(this.key).build();
+ }
+
+ @Bean
+ JwtEncoder jwtEncoder() {
+ JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build();
+ JWKSource jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
+ return new NimbusJwtEncoder(jwks);
+ }
+
+ @Bean
+ public JwtAuthenticationConverter jwtAuthenticationConverter() {
+ JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
+ // Remove the SCOPE_ prefix
+ grantedAuthoritiesConverter.setAuthorityPrefix("");
+
+ JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
+ jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
+ return jwtAuthenticationConverter;
+ }
+
}
diff --git a/src/main/java/nl/connectedit/swiss/controller/PlayerController.java b/src/main/java/nl/connectedit/swiss/controller/PlayerController.java
index 06ee06f..6f44cb1 100644
--- a/src/main/java/nl/connectedit/swiss/controller/PlayerController.java
+++ b/src/main/java/nl/connectedit/swiss/controller/PlayerController.java
@@ -1,6 +1,8 @@
package nl.connectedit.swiss.controller;
+import jakarta.annotation.security.RolesAllowed;
import lombok.RequiredArgsConstructor;
+import nl.connectedit.swiss.auth.Roles;
import nl.connectedit.swiss.domain.entity.Player;
import nl.connectedit.swiss.dto.PlayerDto;
import nl.connectedit.swiss.mapper.PlayerMapper;
@@ -12,6 +14,7 @@ import java.util.List;
@RestController
@CrossOrigin
+@RolesAllowed(Roles.USER)
@RequiredArgsConstructor
public class PlayerController {
diff --git a/src/main/java/nl/connectedit/swiss/controller/RegistrationController.java b/src/main/java/nl/connectedit/swiss/controller/RegistrationController.java
index 3c757e9..4d00778 100644
--- a/src/main/java/nl/connectedit/swiss/controller/RegistrationController.java
+++ b/src/main/java/nl/connectedit/swiss/controller/RegistrationController.java
@@ -1,6 +1,8 @@
package nl.connectedit.swiss.controller;
+import jakarta.annotation.security.RolesAllowed;
import lombok.RequiredArgsConstructor;
+import nl.connectedit.swiss.auth.Roles;
import nl.connectedit.swiss.domain.TournamentStatus;
import nl.connectedit.swiss.dto.TournamentRegistrationDto;
import nl.connectedit.swiss.mapper.TournamentPlayerRegistrationMapper;
@@ -20,6 +22,7 @@ import java.util.List;
@RestController
@CrossOrigin
+@RolesAllowed(Roles.USER)
@RequiredArgsConstructor
public class RegistrationController {
diff --git a/src/main/java/nl/connectedit/swiss/controller/TestController.java b/src/main/java/nl/connectedit/swiss/controller/TestController.java
index b253528..74cbfdf 100644
--- a/src/main/java/nl/connectedit/swiss/controller/TestController.java
+++ b/src/main/java/nl/connectedit/swiss/controller/TestController.java
@@ -3,10 +3,12 @@ package nl.connectedit.swiss.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
+import jakarta.annotation.security.RolesAllowed;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
+import nl.connectedit.swiss.auth.Roles;
import nl.connectedit.swiss.domain.*;
import nl.connectedit.swiss.domain.entity.Player;
import nl.connectedit.swiss.domain.entity.Registration;
@@ -39,6 +41,7 @@ import static nl.connectedit.swiss.domain.PlayerStrength.*;
@RestController
@CrossOrigin
+@RolesAllowed(Roles.USER)
@RequiredArgsConstructor
public class TestController {
diff --git a/src/main/java/nl/connectedit/swiss/controller/TournamentController.java b/src/main/java/nl/connectedit/swiss/controller/TournamentController.java
index 3453d7e..76bb435 100755
--- a/src/main/java/nl/connectedit/swiss/controller/TournamentController.java
+++ b/src/main/java/nl/connectedit/swiss/controller/TournamentController.java
@@ -1,7 +1,9 @@
package nl.connectedit.swiss.controller;
+import jakarta.annotation.security.RolesAllowed;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
+import nl.connectedit.swiss.auth.Roles;
import nl.connectedit.swiss.domain.TournamentStatus;
import nl.connectedit.swiss.domain.entity.Tournament;
import nl.connectedit.swiss.dto.ResultDto;
@@ -20,6 +22,7 @@ import java.util.Objects;
@RestController
@CrossOrigin
+@RolesAllowed(Roles.USER)
@RequiredArgsConstructor
public class TournamentController {
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 40dbcee..83d09de 100755
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -30,4 +30,10 @@ management:
probes:
enabled: true
-security: true
\ No newline at end of file
+security: true
+
+jwt:
+ private:
+ key: classpath:certs/private.pem
+ public:
+ key: classpath:certs/public.pem