Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion spring-security-lab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
## Sources 🔗

* [Spring Security, demystified by Daniel Garnier Moiroux 2022](https://youtu.be/iJ2muJniikY)
* [Spring Security Architecture Principles by Daniel Garnier-Moiroux 2024](https://youtu.be/HyoLl3VcRFY?si=DmpYBQCvsztM7Ubi)
* [Spring Security Architecture Principles by Daniel Garnier-Moiroux 2024](https://youtu.be/HyoLl3VcRFY?si=DmpYBQCvsztM7Ubi)
* [How to Secure your REST APIs with Spring Security & JSON Web Tokens (JWTs)](https://www.danvega.dev/blog/spring-security-jwt)
7 changes: 6 additions & 1 deletion spring-security-lab/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Contains Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
</dependencies>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package pl.mperor.lab.spring;

import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class AppConfiguration {

@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(
User.builder()
.username("user")
.password("{noop}password")
.authorities("READ","ROLE_USER")
.build()
);
}

@Bean
public ApplicationListener<AuthenticationSuccessEvent> successListener() {
return event -> System.out.printf(
"🎉 SUCCESS [%s] 🆔⇒%s 🔐⇒%s%n",
event.getAuthentication().getClass().getSimpleName(),
event.getAuthentication().getName(),
event.getAuthentication().getAuthorities()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package pl.mperor.lab.spring.api;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class ApiController {

@GetMapping("/hello")
String hello() {
return "Hello World 🌐";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package pl.mperor.lab.spring.api;

import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

private final TokenService tokenService;

public AuthController(TokenService tokenService) {
this.tokenService = tokenService;
}

@PostMapping("/token")
public String token(Authentication authentication) {
return tokenService.generateToken(authentication);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package pl.mperor.lab.spring.api;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.stream.Collectors;

@Service
public class TokenService {

private final JwtEncoder encoder;

public TokenService(JwtEncoder encoder) {
this.encoder = encoder;
}

public String generateToken(Authentication authentication) {
Instant now = Instant.now();
String scope = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.filter(authority -> !authority.startsWith("ROLE"))
.collect(Collectors.joining(" "));
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(1, ChronoUnit.HOURS))
.subject(authentication.getName())
.claim("scope", scope)
.build();
var encoderParameters = JwtEncoderParameters.from(JwsHeader.with(MacAlgorithm.HS512).build(), claims);
return this.encoder.encode(encoderParameters).getTokenValue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package pl.mperor.lab.spring.api.config;

import com.nimbusds.jose.jwk.source.ImmutableSecret;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
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.web.SecurityFilterChain;

import javax.crypto.spec.SecretKeySpec;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class ApiSecurityConfig {

private String jwtKey;

ApiSecurityConfig(@Value("${jwt.key}") String jwtKey) {
this.jwtKey = jwtKey;
}

@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/token").hasRole("USER")
.anyRequest().hasAuthority("SCOPE_READ")
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(configurer -> configurer.jwt(withDefaults()))
.httpBasic(withDefaults())
.build();
}

@Bean
public JwtEncoder jwtEncoder() {
return new NimbusJwtEncoder(new ImmutableSecret<>(jwtKey.getBytes()));
}

@Bean
public JwtDecoder jwtDecoder() {
byte[] bytes = jwtKey.getBytes();
SecretKeySpec originalKey = new SecretKeySpec(bytes, 0, bytes.length, "HmacSHA256");
return NimbusJwtDecoder.withSecretKey(originalKey).macAlgorithm(MacAlgorithm.HS512).build();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package pl.mperor.lab.spring;
package pl.mperor.lab.spring.greetings;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package pl.mperor.lab.spring;
package pl.mperor.lab.spring.greetings;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -21,5 +21,4 @@ public String publicPage() {
public String privatePage() {
return "Private page 🔑(Secret room)! " + service.greetUser();
}

}
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
package pl.mperor.lab.spring.config;
package pl.mperor.lab.spring.greetings.config;

import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
public class GreetingsSecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain greetingsSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/**")
.authorizeHttpRequests(authorizeHttp -> {
authorizeHttp.requestMatchers("/").permitAll();
authorizeHttp.requestMatchers("/error").permitAll();
Expand All @@ -34,24 +30,4 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.authenticationProvider(new HackerAuthenticationProvider())
.build();
}

@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(
User.builder()
.username("user")
.password("{noop}password")
.authorities("ROLE user")
.build()
);
}

@Bean
public ApplicationListener<AuthenticationSuccessEvent> successListener() {
return event -> System.out.printf(
"🎉 SUCCESS [%s] %s%n",
event.getAuthentication().getClass().getSimpleName(),
event.getAuthentication().getName()
);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package pl.mperor.lab.spring.config;
package pl.mperor.lab.spring.greetings.config;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
Expand All @@ -15,7 +15,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
return UsernamePasswordAuthenticationToken.authenticated(
"hacker",
null, // 😎 hacker don't need credentials
AuthorityUtils.createAuthorityList("ROLE_admin")
AuthorityUtils.createAuthorityList("ROLE_ADMIN")
);
}
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package pl.mperor.lab.spring.config;
package pl.mperor.lab.spring.greetings.config;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
Expand All @@ -25,7 +25,7 @@ public static RobotAuthentication unauthenticated(String password) {
}

public static RobotAuthentication authenticated() {
return new RobotAuthentication(AuthorityUtils.createAuthorityList("ROLE_robot"), null);
return new RobotAuthentication(AuthorityUtils.createAuthorityList("ROLE_ROBOT"), null);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package pl.mperor.lab.spring.config;
package pl.mperor.lab.spring.greetings.config;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package pl.mperor.lab.spring.config;
package pl.mperor.lab.spring.greetings.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand Down Expand Up @@ -60,6 +60,4 @@ protected void doFilterInternal(

// 2. Do the Rest™️
}


}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package pl.mperor.lab.spring.config;
package pl.mperor.lab.spring.greetings.config;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand Down
5 changes: 4 additions & 1 deletion spring-security-lab/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@

# Useful classes during DEBUG session:
# - FilterChainProxy
# - DefaultSecurityFilterChain
# - DefaultSecurityFilterChain

jwt:
key: 404a91cf551282839e3fa9afff8bc366add5c7bf6d983a1c03557ad7c0c050ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package pl.mperor.lab.spring.api;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class ApiControllerTest {

@Autowired
MockMvc mvc;

@Test
void shouldReturnUnauthorizedWithNoJwt() throws Exception {
this.mvc.perform(get("/api/hello"))
.andExpect(status().isUnauthorized());
}

@Test
void shouldReturnUnauthorizedWithInvalidJwt() throws Exception {
this.mvc.perform(get("/api/hello").header(HttpHeaders.AUTHORIZATION, "Bearer FAKEINCORRECTTOKEN"))
.andExpect(status().isUnauthorized());
}

@Test
void shouldReturnWelcomeMessageWithValidJwt() throws Exception {
var token = this.mvc.perform(post("/api/auth/token")
.with(httpBasic("user", "password")))
.andReturn()
.getResponse()
.getContentAsString();

assertThat(token).isNotEmpty();

MvcResult response = this.mvc.perform(get("/api/hello").header(HttpHeaders.AUTHORIZATION, "Bearer " + token))
.andExpect(status().isOk())
.andReturn();

assertEquals("Hello World 🌐", response.getResponse().getContentAsString());
}
}
Loading
Loading