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
1 change: 1 addition & 0 deletions spring-security-modules/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<module>spring-security-core</module>
<module>spring-security-core-2</module>
<module>spring-security-core-3</module>
<module>spring-security-mfa</module>
<!-- <module>spring-security-ldap</module>-->
<!-- <module>spring-security-legacy-oidc</module> --> <!-- failing after upgrade to jdk21 --> <!-- Ths module wasn't able to upgrade because is using Spring Security oauth2 which reached end of life. JAVA-29292 and JAVA-37717-->
<module>spring-security-oauth2</module> <!-- This module wasn't updated to spring boot 3 because of spring-oauth2 not supporting Jakarta and reaching end of life. JAVA-29293 -->
Expand Down
68 changes: 68 additions & 0 deletions spring-security-modules/spring-security-mfa/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-mfa</artifactId>
<packaging>war</packaging>
<name>spring-security-mfa</name>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.3</version>
<relativePath/>
</parent>

<properties>
<java.version>17</java.version>
<groupId>com.baeldung</groupId>
<spring-boot.version>4.0.3</spring-boot.version>
<spring-security.version>7.0.0</spring-security.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>${spring-security.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.baeldung.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authorization.AuthorizationManagerFactory;
import org.springframework.security.authorization.AuthorizationManagerFactories;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.FactorGrantedAuthority;

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

@Configuration
@EnableWebSecurity
public class AdminMfaSecurityConfig {

@Bean
@Order(1)
SecurityFilterChain adminSecurityFilterChain(HttpSecurity http) throws Exception {

AuthorizationManagerFactory<Object> mfa =
AuthorizationManagerFactories
.multiFactor()
.requireFactors(
FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.X509_AUTHORITY
)
.build();

http
.securityMatcher("/admin/**")
.authorizeHttpRequests(auth ->
auth
.requestMatchers("/admin/**")
.access(mfa.hasRole("ADMIN"))
.anyRequest()
.authenticated()
)
.formLogin(withDefaults());

return http.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.baeldung.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.web.SecurityFilterChain;

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

@Configuration
@EnableWebSecurity
@EnableMultiFactorAuthentication(
authorities = {
FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.X509_AUTHORITY
}
)
public class GlobalMfaSecurityConfig {

@Bean
@Order(3)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {

http
.securityMatcher("/**")
.authorizeHttpRequests(auth ->
auth
.requestMatchers("/public").permitAll()
.anyRequest().authenticated()
)
.formLogin(withDefaults());

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.baeldung.config;
import java.time.Duration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authorization.AuthorizationManagerFactory;
import org.springframework.security.authorization.AuthorizationManagerFactories;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;

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

@Configuration
@EnableWebSecurity
public class TimeBasedMfaSecurityConfig {

@Bean
@Order(2)
SecurityFilterChain profileSecurityFilterChain(HttpSecurity http) throws Exception {

AuthorizationManagerFactory<Object> recentLogin =
AuthorizationManagerFactories
.multiFactor()
.requireFactor(
factor ->
factor
.passwordAuthority()
.validDuration(Duration.ofMinutes(5))
)
.build();

http
.securityMatcher("/profile", "/profile/**")
.authorizeHttpRequests(auth ->
auth
.requestMatchers("/profile", "/profile/**")
.access(recentLogin.authenticated())
.anyRequest()
.authenticated()
)
.formLogin(withDefaults());

return http.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.baeldung.controller;

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

@RestController
public class DemoController {

@GetMapping("/public")
public String publicEndpoint() {
return "public endpoint";
}

@GetMapping("/profile")
public String profileEndpoint() {
return "profile endpoint";
}

@GetMapping("/admin/dashboard")
public String adminDashboard() {
return "admin dashboard";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.baeldung.mfa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = "com.baeldung")
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.baeldung.security;

import java.util.function.Supplier;

import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.security.authorization.AllAuthoritiesAuthorizationManager;
import org.springframework.security.core.authority.FactorGrantedAuthority;

@Component
public class AdminMfaAuthorizationManager implements AuthorizationManager<Object> {

AuthorizationManager<Object> mfa =
AllAuthoritiesAuthorizationManager
.hasAllAuthorities(
FactorGrantedAuthority.OTT_AUTHORITY,
FactorGrantedAuthority.PASSWORD_AUTHORITY
);

@Override
public AuthorizationResult authorize(
Supplier<? extends Authentication> authentication,
Object context) {

Authentication auth = authentication.get();

if (auth != null && "admin".equals(auth.getName())) {
return mfa.authorize(authentication, context);
}
return new AuthorizationDecision(true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.baeldung;

import com.baeldung.mfa.Application;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.hamcrest.Matchers.containsString;

@SpringBootTest(classes = Application.class)
@AutoConfigureMockMvc
class AdminMfaSecurityTest {
@Autowired
MockMvc mockMvc;
@Test
void givenAdminWithoutMfa_whenAccessAdminEndpoint_thenForbidden() throws Exception {

mockMvc.perform(
get("/admin/dashboard")
.with(user("admin").roles("ADMIN"))
)
.andExpect(status().is3xxRedirection())
.andExpect(header().string("Location", containsString("/login")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.baeldung;


import com.baeldung.mfa.Application;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.hamcrest.Matchers.containsString;

@SpringBootTest(classes = Application.class)
@AutoConfigureMockMvc
class GlobalMfaSecurityTest {
@Autowired
MockMvc mockMvc;
@Test
void givenUserWithoutMfa_whenAccessProfile_thenForbidden() throws Exception {
mockMvc.perform(
get("/profile")
.with(user("user").roles("USER"))
)
.andExpect(status().is3xxRedirection())
.andExpect(header().string("Location", containsString("/login")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.baeldung;

import com.baeldung.mfa.Application;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(classes = Application.class)
class Mfa7ApplicationTests {

@Test
void contextLoads() {
}

}
Loading