-
Notifications
You must be signed in to change notification settings - Fork 43
Expand file tree
/
Copy pathMfaConfiguration.java
More file actions
160 lines (143 loc) · 6.39 KB
/
MfaConfiguration.java
File metadata and controls
160 lines (143 loc) · 6.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package com.digitalsanctuary.spring.user.security;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.security.authorization.AllRequiredFactorsAuthorizationManager;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
import org.springframework.security.authorization.RequiredFactor;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* Configuration that registers {@link MfaConfigProperties} and provides MFA-related beans.
* <p>
* This configuration is always active because {@code WebSecurityConfig} requires {@link MfaConfigProperties} regardless
* of whether MFA is enabled. The {@code DefaultAuthorizationManagerFactory} bean is only created when MFA is enabled.
* </p>
* <p>
* When enabled, the {@code DefaultAuthorizationManagerFactory} is configured with an
* {@link AllRequiredFactorsAuthorizationManager} that makes {@code .authenticated()} in
* {@code authorizeHttpRequests} additionally require all configured factor authorities. Spring Security 7's built-in
* infrastructure handles enforcement and session management automatically.
* </p>
*
* @see MfaConfigProperties
* @see WebSecurityConfig
*/
@Slf4j
@Configuration
@EnableConfigurationProperties(MfaConfigProperties.class)
@RequiredArgsConstructor
public class MfaConfiguration {
/**
* Mapping from user-facing factor names to Spring Security {@link FactorGrantedAuthority} authority strings.
*/
private static final Map<String, String> FACTOR_AUTHORITY_MAP = Map.of(
"PASSWORD", FactorGrantedAuthority.PASSWORD_AUTHORITY,
"WEBAUTHN", FactorGrantedAuthority.WEBAUTHN_AUTHORITY);
private final MfaConfigProperties mfaConfigProperties;
private final WebAuthnConfigProperties webAuthnConfigProperties;
/**
* Creates a {@link DefaultAuthorizationManagerFactory} with an additional authorization requirement for all
* configured MFA factors. This makes {@code .authenticated()} in {@code authorizeHttpRequests} require all
* configured factors to be satisfied.
*
* @return the authorization manager factory configured with required factor authorities
*/
@Bean
@ConditionalOnProperty(name = "user.mfa.enabled", havingValue = "true", matchIfMissing = false)
public DefaultAuthorizationManagerFactory<Object> mfaAuthorizationManagerFactory() {
AllRequiredFactorsAuthorizationManager.Builder<Object> factorsBuilder =
AllRequiredFactorsAuthorizationManager.builder();
// Unknown/blank factors are silently skipped here; validateMfaConfiguration() will
// throw before startup completes if the configuration is invalid.
for (String factor : mfaConfigProperties.getFactors()) {
if (factor == null || factor.isBlank()) {
continue;
}
String authority = FACTOR_AUTHORITY_MAP.get(factor.toUpperCase());
if (authority != null) {
factorsBuilder.requireFactor(RequiredFactor.withAuthority(authority).build());
}
}
AuthorizationManager<Object> factorsManager = factorsBuilder.build();
DefaultAuthorizationManagerFactory<Object> factory = new DefaultAuthorizationManagerFactory<>();
factory.setAdditionalAuthorization(factorsManager);
log.info("MFA enabled with required factors: {}", mfaConfigProperties.getFactors());
return factory;
}
/**
* Validates MFA configuration on application startup. Performs validation only when MFA is enabled; returns
* immediately otherwise.
*
* @param event the context refreshed event
*/
@EventListener(ContextRefreshedEvent.class)
public void validateMfaConfiguration(ContextRefreshedEvent event) {
if (!mfaConfigProperties.isEnabled()) {
return;
}
List<String> factors = mfaConfigProperties.getFactors();
if (factors == null || factors.isEmpty()) {
throw new IllegalStateException(
"MFA is enabled (user.mfa.enabled=true) but no factors are configured. "
+ "Set user.mfa.factors to a comma-separated list of factors (e.g., PASSWORD,WEBAUTHN).");
}
for (String factor : factors) {
if (factor == null || factor.isBlank()) {
throw new IllegalStateException(
"MFA factors list contains a null or blank entry. Check user.mfa.factors configuration.");
}
if (!FACTOR_AUTHORITY_MAP.containsKey(factor.toUpperCase())) {
throw new IllegalStateException(
"Unknown MFA factor: '" + factor + "'. Supported factors: " + FACTOR_AUTHORITY_MAP.keySet());
}
}
if (factors.stream().anyMatch(f -> "WEBAUTHN".equalsIgnoreCase(f)) && !webAuthnConfigProperties.isEnabled()) {
throw new IllegalStateException(
"MFA factor WEBAUTHN is configured but WebAuthn is disabled (user.webauthn.enabled=false). "
+ "Enable WebAuthn or remove WEBAUTHN from user.mfa.factors.");
}
if (factors.stream().anyMatch(f -> "PASSWORD".equalsIgnoreCase(f))) {
log.warn("MFA factor PASSWORD is configured. Users with passwordless (passkey-only) accounts "
+ "will not be able to satisfy the PASSWORD factor. Consider your account types carefully.");
}
}
/**
* Resolves the configured factor names to Spring Security authority strings.
* <p>
* Package-private for testing via {@link MfaConfigurationTest}.
* </p>
*
* @return list of Spring Security authority strings
*/
List<String> resolveFactorAuthorities() {
List<String> authorities = new ArrayList<>();
for (String factor : mfaConfigProperties.getFactors()) {
String authority = FACTOR_AUTHORITY_MAP.get(factor.toUpperCase());
if (authority != null) {
authorities.add(authority);
}
}
return authorities;
}
/**
* Maps a user-facing factor name to a Spring Security authority string.
*
* @param factorName the factor name (e.g., "PASSWORD", "WEBAUTHN")
* @return the corresponding Spring Security authority string, or null if unknown
*/
public static String mapFactorToAuthority(String factorName) {
if (factorName == null || factorName.isBlank()) {
return null;
}
return FACTOR_AUTHORITY_MAP.get(factorName.toUpperCase());
}
}