wake-up-neo.com

Multi-Faktor-Authentifizierung mit Spring Boot 2 und Spring Security 5

Ich möchte einer Angular & Spring-Anwendung) eine Multi-Faktor-Authentifizierung mit TOTP-Soft-Token hinzufügen und dabei alles so nah wie möglich an den Standardeinstellungen von Spring Boot halten Sicherheitsstarter .

Die Token-Validierung erfolgt lokal (mit der Aerogear-Otp-Java-Bibliothek), ohne API-Anbieter eines Drittanbieters.

Das Einrichten von Token für einen Benutzer funktioniert, die Überprüfung durch Nutzung von Spring Security Authentication Manager/Providers jedoch nicht.

TL; DR

  • Wie kann ein zusätzlicher AuthenticationProvider offiziell in ein Spring Boot Security Starter konfiguriertes System integriert werden?
  • Welche Möglichkeiten werden empfohlen, um Wiederholungsangriffe zu verhindern?

Lange Version

Die API verfügt über einen Endpunkt /auth/token, Von dem das Frontend ein JWT-Token erhalten kann, indem Benutzername und Kennwort angegeben werden. Die Antwort enthält auch einen Authentifizierungsstatus, der entweder AUTHENTIFIZIERT oder PRE_AUTHENTICATED_MFA_REQUIRED sein kann.

Wenn der Benutzer MFA benötigt, wird dem Token eine einzige erteilte Berechtigung von PRE_AUTHENTICATED_MFA_REQUIRED Und eine Ablaufzeit von 5 Minuten ausgestellt. Auf diese Weise kann der Benutzer auf den Endpunkt /auth/mfa-token Zugreifen, auf dem er den TOTP-Code aus seiner Authenticator-App bereitstellen und das vollständig authentifizierte Token für den Zugriff auf die Site erhalten kann.

Anbieter und Token

Ich habe mein benutzerdefiniertes MfaAuthenticationProvider erstellt, das AuthenticationProvider implementiert:

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // validate the OTP code
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return OneTimePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

Und ein OneTimePasswordAuthenticationToken, das AbstractAuthenticationToken erweitert, um den Benutzernamen (aus dem signierten JWT) und den OTP-Code zu enthalten.

Konfig

Ich habe mein benutzerdefiniertes WebSecurityConfigurerAdapter, wo ich mein benutzerdefiniertes AuthenticationProvider über http.authenticationProvider() hinzufüge. Nach JavaDoc scheint dies der richtige Ort zu sein:

Ermöglicht das Hinzufügen eines zusätzlichen AuthenticationProviders

Die relevanten Teile meines SecurityConfig sehen so aus.

    @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

Regler

Das AuthController hat das AuthenticationManagerBuilder injiziert und zieht alles zusammen.

@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

Das Posten gegen /auth/mfa-token Führt jedoch zu folgendem Fehler:

"error": "Forbidden",
"message": "Access Denied",
"trace": "org.springframework.security.authentication.ProviderNotFoundException: No AuthenticationProvider found for de.....OneTimePasswordAuthenticationToken

Warum holt Spring Security meinen Authentifizierungsanbieter nicht ab? Das Debuggen des Controllers zeigt mir, dass DaoAuthenticationProvider der einzige Authentifizierungsanbieter in AuthenticationProviderManager ist.

Wenn ich mein MfaAuthenticationProvider als Bean verfügbar mache, ist nur der Anbieter registriert, sodass ich das Gegenteil erhalte:

No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken. 

Also, wie bekomme ich beides?

Meine Frage

Was ist der empfohlene Weg, um ein zusätzliches AuthenticationProvider in ein Spring Boot Security Starter konfiguriertes System zu integrieren, damit ich beide bekomme, das DaoAuthenticationProvider und mein eigenes benutzerdefiniertes MfaAuthenticationProvider? Ich möchte die Standardeinstellungen von Spring Boot Scurity Starter beibehalten und zusätzlich einen eigenen Provider haben.

Verhinderung von Wiederholungsangriffen

Ich weiß, dass der OTP-Algorithmus selbst nicht innerhalb der Zeitscheibe, in der der Code gültig ist, vor Wiederholungsangriffen schützt. RFC 6238 macht dies deutlich

Der Prüfer darf den zweiten Versuch des OTP NICHT akzeptieren, nachdem die erfolgreiche Validierung für das erste OTP durchgeführt wurde, wodurch die einmalige Verwendung eines OTP sichergestellt wird.

Ich habe mich gefragt, ob es einen empfohlenen Weg gibt, Schutz zu implementieren. Da die OTP-Token zeitbasiert sind, denke ich daran, die letzte erfolgreiche Anmeldung im Benutzermodell zu speichern und sicherzustellen, dass es nur eine erfolgreiche Anmeldung pro 30 Sekunden Zeitscheibe gibt. Dies bedeutet natürlich eine Synchronisation auf dem Benutzermodell. Irgendwelche besseren Ansätze?

Danke dir.

- -

PS: Da es sich um eine Sicherheitsfrage handelt, suche ich nach einer Antwort aus glaubwürdigen und/oder offiziellen Quellen. Danke dir.

11
phisch

Um meine eigene Frage zu beantworten, habe ich sie nach weiteren Recherchen so umgesetzt.

Ich habe einen Anbieter als Pojo, der AuthenticationProvider implementiert. Es ist absichtlich keine Bohne/Komponente. Andernfalls würde Spring es als einzigen Anbieter registrieren.

public class MfaAuthenticationProvider implements AuthenticationProvider {
    private final AccountService accountService;

    @Override
    public Authentication authenticate(Authentication authentication) {
        // here be code 
        }

In meiner SecurityConfig lasse ich Spring das AuthenticationManagerBuilder automatisch verdrahten und injiziere mein MfaAuthenticationProvider manuell

@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);
}

Wenn der Benutzer nach der Standardauthentifizierung MFA aktiviert hat, wird er mit der erteilten Berechtigung PRE_AUTHENTICATED_MFA_REQUIRED vorauthentifiziert. Dadurch können sie auf einen einzelnen Endpunkt zugreifen, /auth/mfa-token. Dieser Endpunkt nimmt den Benutzernamen vom gültigen JWT und dem bereitgestellten TOTP und sendet ihn an die Methode authenticate() des authenticationManagerBuilder, die das MfaAuthenticationProvider auswählt, da es OneTimePasswordAuthenticationToken verarbeiten kann.

    var authenticationToken = new OneTimePasswordAuthenticationToken(usernameFromJwt, providedOtp);
    var authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
0
phisch