WebAuthn und Spring Boot

  • von

Nicolai Mainiero, Softwarearchitekt bei sidion

WebAuthn und Spring Boot

Immer wieder kommt es zu Sicherheitslücken bei denen Passwörter entwendet und bekannt werden. Eine Seite, auf der man prüfen kann, ob die eigenen Passwörter schon mal in solch einer Sicherheitslücke aufgetaucht sind, ist „have i been pwned“ [4]. Dazu kommt, dass Passwörter oft mehrfach verwendet werden, so dass eine Sicherheitslücke auf einer unbedeutenden Seite dazu führen kann, dass auch das Passwort fürs Online-Banking bekannt wird. Physische Sicherheitsschlüssel schaffen hier Abhilfe, da für jede Webseite individuelle Zugangsdaten erzeugt werden, die nie diesen Sicherheitsschlüssel verlassen. Diese Funktion in eine Webseite einzubauen, ist durch vorgefertigte Bibliotheken fast genauso einfach wie eine traditionelle Benutzerregistrierung zu implementieren.

Das ist ein Solo Key. Ein quelloffener FIDO2 Sicherheitsschlüssel. FIDO2 ist ein Standard, um starte Authentifizierungslösungen im Web zu realisieren. Dabei kann nicht nur solch ein USB-Stick als externer Authentifikations-Faktor verwendet werden. Es kann sich dabei auch um ein Handy, ein Bluetooth Gerät oder ein Fingerabdrucksensor handeln. Das Protokoll ist so flexibel aufgebaut und definiert eine eigene API (Client to Authenticator Protocol) damit der Browser, aber auch Desktopanwendungen und Web Services, diese Geräte zur Authentifikation nutzen können.

Browser

Hier soll aber der Fokus auf die Verwendung der WebAuthn API im Browser liegen. Wenn sich ein neuer Benutzer erstmalig bei einer Webapplikation registriert wir ihm üblicherweise ein Formular präsentiert, in dem er mindestens einen Benutzernamen und ein Passwort eingeben muss, damit er sich später mit diesen Zugangsdaten authentifizieren kann. Der Server speichert diese Daten dann sicher ab und der Benutzer ist registriert. Der Ablauf mit WebAuthn ändert sich gar nicht so stark, allerdings lassen wir es nicht mehr zu, dass der Benutzer sein „Passwort“ selber wählt. Bei der Registrierung weisen wir den Browser an, mit Hilfe des Sicherheitsschlüssels ein neues Public-Key-Credential für die aktuelle Seite zu erzeugen.

const publicKeyCredentialCreationOptions = {
    challenge: Uint8Array.from(
        randomStringFromServer, c => c.charCodeAt(0)),
    rp: {
        name: "Awesome Security",
        id: "awesomesecurity.com",
    },
    user: {
        id: Uint8Array.from(
            "UZSL85T9AFC", c => c.charCodeAt(0)),
        name: "user@mail.provider",
        displayName: "User",
    },
    pubKeyCredParams: [{alg: -7, type: "public-key"}],
    authenticatorSelection: {
        authenticatorAttachment: "cross-platform",
    },
    timeout: 60000,
    attestation: "direct"
};

const credential = await navigator.credentials.create({
    publicKey: publicKeyCredentialCreationOptions
});

Um ein solches Credential zu erzeugen müssen einige Optionen ausgewählt werden. Die challenge soll ein zufälliger von Server erzeugter String sein, um Replay-Angriffe abwehren zu können. Die rp bzw. relying party ist die Stelle, die für die Registrierung und Authentifikation des Benutzers verantwortlich ist. Die id muss ein Subset der aktuellen Domain sein. Der user enthält Informationen über den Benuzter für den ein Credential erzeugt werden soll. Die id sollte dabei keine persönlichen Informationen enthalten, da diese auf dem Sicherheitsschlüssel gespeichert werden könnten. Mit den pubKeyCredParams kann angegeben werden, welche Algorithmen der Server verarbeiten kann. Die Werte für das alg Feld sind unter [3] dokumentiert. Mit dem optinalen Feld authenticatorSelection kann die Art des verwendeten Faktors weiter eingeschränkt werden. Mir cross-platform erlauben wir einen Sicherheitsschlüssel, mit platform erzwingen wir zum Beispiel Windows Hello oder Touch ID. Mit timeout können wir festlegen, wie lange der Browser auf eine Benutzerinteraktion zum Erzeugen des Credentials wartet und attestation lässt den Server anzeigen, wie relevant für ihn diese Daten sind. Der Wert none signalisiert, dass der Server an der Beglaubigung nicht interessiert ist, indirect signalisiert, dass anonymisierte Daten ausreichend sind und bei direct werden die Daten vom Sicherheitsschlüssel angefordert. Diese Daten können unter Umständen verwendet werden um den Benutzer zu tracken.

Das Public-Key-Credential wird nun an den Server gesendet, der es validieren muss. Dafür sieht die WebAuthn ein Verfahren mit 19 Punkte vor. Hier bietet es sich an, auf eine der zahlreich verfügbaren Implementierungen [1] zurückzugreifen.

Die Authentifizierung erfolgt dann, indem den Benutzer eine vom Server erzeugte zufällige Zeichenkette kryptographisch mit seinem Sicherheitsschlüssel signieren muss.

const publicKeyCredentialRequestOptions = {
    challenge: Uint8Array.from(
        randomStringFromServer, c => c.charCodeAt(0)),
    allowCredentials: [{
        id: Uint8Array.from(
            credentialId, c => c.charCodeAt(0)),
        type: 'public-key',
        transports: ['usb', 'ble', 'nfc'],
    }],
    timeout: 60000,
}

const assertion = await navigator.credentials.get({
    publicKey: publicKeyCredentialRequestOptions
});

Der Server erzeugt also eine challenge und wählt mit allowCredentials den hinterlegten Schlüssel aus. Bei den transports kann noch eingeschränkt werden, wie mit dem Sicherheitsschlüssel kommuniziert werden darf. In diesem Fall sind USB, Bluetooth und NFC zulässig. Die so erzeugte Signatur muss nun vom Server geprüft werden. Wenn sie dem erwarteten Ergebnis entspricht, hat sich der Benutzer erfolgreich authentifiziert.

Spring Boot

Die Integration in Spring Boot gestaltet sich dank einer Bibliothek für Spring Security problemlos. WebAuthn4J Spring Security [5] implementiert die serverseitige Konfiguration und Validierung, die für WebAuthn benötigt wird. Dafür greift es auf die Java Implementierung WebAuthn4J die auch in anderen Projekten wie zum Beispiel Keycloak oder Red Hat SSO zum Einsatz kommt.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.apply(new WebAuthnAuthenticationProviderConfigurer<>(webAuthnAuthenticatorService, webAuthnManager));
        builder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // WebAuthn Login
        http.apply(WebAuthnLoginConfigurer.webAuthnLogin())
                .defaultSuccessUrl("/", true)
                .rpId("example.com")
                .attestationOptionsEndpoint()
                    .rp()
                        .name("WebAuthn4J Spring Security Sample MPA")
                        .and()
                    .pubKeyCredParams(
                        new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, COSEAlgorithmIdentifier.ES256),
                        new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, COSEAlgorithmIdentifier.RS1)
                    )
                    .attestation(AttestationConveyancePreference.DIRECT)
                    .extensions()
                        .uvm(true)
                        .credProps(true);
    }
}

Damit Spring Security die korrekten Optionen für den Aufruf der JavaScrip API im Browser setzten kann, muss es zunächst konfiguriert werden. Dies geschieht wie für Spring Boot üblich mit Hilfe einer @Configuration. In diesem Fall wird der WebSecurityConfigurerAdapter erweitert, um WebAuthn zu ermöglichen. Dazu fügen wir dem AuthenticationManagerBuilder ein WebAuthnAuthenticationProviderConfigurer hinzu. Zusätzlich wir noch ein geeigneter UserDetailService und PasswordEncoder gesetzt. Jetzt kann die HttpSecurity konfiguriert werden, damit die korrekten Parameter an die JavaScript API weitergegeben werden. In rpId wird die id der relying party definiert. Über pubKeyCredParams kann gesteuert werden welche kryptographischen Algorithmen angeboten werden sollen. Diese landen dann im gleichnamigen Feld der publicKeyCredentialCreationOptions des Frontends.

Ein vollständiges Beispiel inklusive notwendigen HTML Formularen und JavaScript findet sich im samples Verzeichnis der WebAuthn4J Spring Security Bibliothek. Ebenso ist dort ein Beispiel zu finden, wie man WebAuthn bei einer Single Page Applikation (mit Angular) realisiert.

Fazit

Der Einsatz von Alternativen Authentifikationslösungen ist nicht so schwer wie es scheint. Die Details werden von Bibliotheken für alle gängigen Programmiersprachen hinter leicht verständlichen und einfach nutzbaren APIs versteckt, so dass sich der Entwickler nicht mit der Komplexität von Public-Key Kryptographie auseinandersetzten muss. Aus meiner Sicht sollte die Möglichkeit einen Sicherheitsschlüssel in einer Webanwendung zu hinterlegen Standard sein.

[1] https://webauthn.io/ - Testseite und Beispielimplementierungen der WebAuthn Spezifikation

[2] https://fidoalliance.org/fido2/ - FIDO2: WebAuthn & CTAP

[3] https://www.iana.org/assignments/cose/cose.xhtml#algorithms

[4] https://haveibeenpwned.com/

[5] https://github.com/webauthn4j/webauthn4j-spring-security

[6] https://github.com/webauthn4j/webauthn4j

Zurück