Je souhaite ajouter une authentification multifacteur avec des jetons logiciels TOTP à une application Angular & Spring, tout en gardant tout aussi proche que possible des valeurs par défaut de Spring Boot Démarreur de sécurité .
La validation des jetons se produit localement (avec la bibliothèque aerogear-otp-Java), sans fournisseur d'API tiers.
La configuration de jetons pour un utilisateur fonctionne, mais leur validation en utilisant Spring Security Authentication Manager/Providers ne fonctionne pas.
L'API a un point de terminaison /auth/token
À partir duquel le frontend peut obtenir un jeton JWT en fournissant un nom d'utilisateur et un mot de passe. La réponse inclut également un état d'authentification, qui peut être AUTHENTIFIÉ ou PRE_AUTHENTICATED_MFA_REQUIRED.
Si l'utilisateur a besoin de l'authentification multifacteur, le jeton est émis avec une seule autorité accordée de PRE_AUTHENTICATED_MFA_REQUIRED
Et un délai d'expiration de 5 minutes. Cela permet à l'utilisateur d'accéder au point de terminaison /auth/mfa-token
Où il peut fournir le code TOTP à partir de son application Authenticator et obtenir le jeton entièrement authentifié pour accéder au site.
J'ai créé mon MfaAuthenticationProvider
personnalisé qui implémente AuthenticationProvider
:
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// validate the OTP code
}
@Override
public boolean supports(Class<?> authentication) {
return OneTimePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
Et un OneTimePasswordAuthenticationToken
qui étend AbstractAuthenticationToken
pour contenir le nom d'utilisateur (tiré du JWT signé) et le code OTP.
J'ai mon WebSecurityConfigurerAdapter
personnalisé, où j'ajoute mon AuthenticationProvider
personnalisé via http.authenticationProvider()
. Selon le JavaDoc, cela semble être le bon endroit:
Permet d'ajouter un AuthenticationProvider supplémentaire à utiliser
Les parties pertinentes de mon SecurityConfig
ressemblent à ceci.
@Configuration
@EnableWebSecurity
@EnableJpaAuditing(auditorAwareRef = "appSecurityAuditorAware")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final TokenProvider tokenProvider;
public SecurityConfig(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authenticationProvider(new MfaAuthenticationProvider());
http.authorizeRequests()
// Public endpoints, HTML, Assets, Error Pages and Login
.antMatchers("/", "favicon.ico", "/asset/**", "/pages/**", "/api/auth/token").permitAll()
// MFA auth endpoint
.antMatchers("/api/auth/mfa-token").hasAuthority(ROLE_PRE_AUTH_MFA_REQUIRED)
// much more config
Le AuthController
a le AuthenticationManagerBuilder
injecté et le rassemble.
@RestController
@RequestMapping(AUTH)
public class AuthController {
private final TokenProvider tokenProvider;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
public AuthController(TokenProvider tokenProvider, AuthenticationManagerBuilder authenticationManagerBuilder) {
this.tokenProvider = tokenProvider;
this.authenticationManagerBuilder = authenticationManagerBuilder;
}
@PostMapping("/mfa-token")
public ResponseEntity<Token> mfaToken(@Valid @RequestBody OneTimePassword oneTimePassword) {
var username = SecurityUtils.getCurrentUserLogin().orElse("");
var authenticationToken = new OneTimePasswordAuthenticationToken(username, oneTimePassword.getCode());
var authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
// rest of class
Cependant, publier contre /auth/mfa-token
Conduit à cette erreur:
"error": "Forbidden",
"message": "Access Denied",
"trace": "org.springframework.security.authentication.ProviderNotFoundException: No AuthenticationProvider found for de.....OneTimePasswordAuthenticationToken
Pourquoi Spring Security ne récupère-t-il pas mon fournisseur d'authentification? Le débogage du contrôleur me montre que DaoAuthenticationProvider
est le seul fournisseur d'authentification dans AuthenticationProviderManager
.
Si j'expose mon MfaAuthenticationProvider
en tant que bean, c'est le fournisseur uniquement qui est enregistré, donc j'obtiens le contraire:
No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken.
Alors, comment puis-je obtenir les deux?
Quelle est la méthode recommandée pour intégrer un AuthenticationProvider
supplémentaire dans un système configuré Spring Boot Security Starter , de sorte que j'obtienne les deux, le DaoAuthenticationProvider
et mon propre MfaAuthenticationProvider
personnalisé? Je veux conserver les valeurs par défaut de Spring Boot Scurity Starter et avoir mon propre fournisseur en plus.
Je sais que l'algorithme OTP ne protège pas par lui-même contre les attaques de relecture pendant la tranche de temps pendant laquelle le code est valide; La RFC 6238 le précise
Le vérificateur NE DOIT PAS accepter la deuxième tentative du protocole OTP après que la validation réussie a été émise pour le premier OTP, ce qui garantit une utilisation unique d'un OTP.
Je me demandais s'il existe un moyen recommandé de mettre en œuvre la protection. Étant donné que les jetons OTP sont basés sur le temps, je pense à stocker la dernière connexion réussie sur le modèle de l'utilisateur et à m'assurer qu'il n'y a qu'une seule connexion réussie par tranche de 30 secondes. Cela signifie bien sûr une synchronisation sur le modèle utilisateur. Des meilleures approches?
Je vous remercie.
-
PS: puisqu'il s'agit d'une question de sécurité je cherche une réponse tirée de sources crédibles et/ou officielles. Je vous remercie.
Pour répondre à ma propre question, voici comment je l'ai mis en œuvre, après de nouvelles recherches.
J'ai un fournisseur comme pojo qui implémente AuthenticationProvider
. Ce n'est délibérément pas un Bean/Component. Sinon, Spring l'enregistrerait comme le seul fournisseur.
public class MfaAuthenticationProvider implements AuthenticationProvider {
private final AccountService accountService;
@Override
public Authentication authenticate(Authentication authentication) {
// here be code
}
Dans mon SecurityConfig, je laisse Spring autowire le AuthenticationManagerBuilder
et injecter manuellement mon MfaAuthenticationProvider
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final AuthenticationManagerBuilder authenticationManagerBuilder;
@Override
protected void configure(HttpSecurity http) throws Exception {
// other code
authenticationManagerBuilder.authenticationProvider(getMfaAuthenticationProvider());
// more code
}
// package private for testing purposes.
MfaAuthenticationProvider getMfaAuthenticationProvider() {
return new MfaAuthenticationProvider(accountService);
}
Après l'authentification standard, si l'utilisateur a activé MFA, il est pré-authentifié avec une autorité accordée de PRE_AUTHENTICATED_MFA_REQUIRED. Cela leur permet d'accéder à un seul point de terminaison, /auth/mfa-token
. Ce point de terminaison prend le nom d'utilisateur du JWT valide et du TOTP fourni et l'envoie à la méthode authenticate()
du authenticationManagerBuilder, qui choisit le MfaAuthenticationProvider
car il peut gérer OneTimePasswordAuthenticationToken
.
var authenticationToken = new OneTimePasswordAuthenticationToken(usernameFromJwt, providedOtp);
var authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);