This web app is a demo for my WebAuthn server library (fxamacker/webauthn). It supports WebAuthn registration and authentication. It implements proposed REST API for FIDO2 servers.
WebAuthn (Web Authentication) is a W3C web standard for authenticating users to web-based apps and services. It's a core component of FIDO2, the successor of FIDO U2F legacy protocol.
- fxamacker/webauthn to parse and validate registration and authentication requests.
 - Bootstrap and jQuery for web interface.
 - gorilla/mux for routing and gorilla/sessions for session management.
 - Redis for session storage.
 - PostgreSQL for data persistence.
 
This demo is not for production use because it's designed to be a demo.
- Go 1.12 (or newer)
 - Tested on x86_64 but it should work on other little-endian systems supported by Go.
 
go get github.com/fxamacker/webauthn-demo
$ CERTS_DIR=[folder containing cert.pem and key.pem] docker-compose up
WebAuthn demo runs at https://localhost:8443 on your Docker host.
- Edit config.json to change WebAuthn server settings as needed.
 - Edit .env as needed:
- CERTS_DIR: folder containing cert.pem and key.pem.
 - DB_NAME: database name (default: webauthn).
 - DB_PASSWORD: database password (default: dockerpwd).
 - DB_USER: database user (default: docker).
 - DB_DATA_DIR: database storage folder.
 - CACHE_DATA_DIR: cache storage folder.
 - SESSION_KEY: base64 encoded session encryption key.
 
 - Run WebAuthn demo:
 
$ docker-compose up
WebAuthn demo runs at https://localhost:8443 on your Docker host.
Registration process consists of two steps: create credential creation options and register credentials. See signup.html, webauthn.register.js, and registration_handlers.go.
Create credential creation options:
Server handles /attestation/options request by returning credential creation options (PublicKeyCredentialCreationOptions) to client.  Client then uses those options with navigator.credentials.create() to create new credentials.
// Simplified `/attestation/options` handler from registration_handlers.go
func (s *server) handleAttestationOptions(w http.ResponseWriter, r *http.Request) {
    // Get user from datastore by username.
    u, _ := s.dataStore.getUser(r.Context(), optionsRequest.Username)
    
    // Create PublicKeyCredentialCreationOptions using webauthn library.
    creationOptions, _ := webauthn.NewAttestationOptions(s.webAuthnConfig, &webauthn.User{ID: u.UserID, Name: u.UserName, DisplayName: u.DisplayName, CredentialIDs: u.CredentialIDs})
    // Save creationOptions and user info in session to verify new credential later.
    session.Values[WebAuthnCreationOptions] = creationOptions
    session.Values[UserSession] = &userSession{User: u}
    // Write creationOptions to response.
}
Register credentials:
Server verifies and registers new credentials received via /attestation/result.
// Simplified `/attestation/result` handler from registration_handlers.go
func (s *server) handleAttestationResult(w http.ResponseWriter, r *http.Request) {
    // Get saved creationOptions and user info from session.
    // Parse and verify credential in request body.
    credentialAttestation, _ := webauthn.ParseAttestation(r.Body)
    expected := &webauthn.AttestationExpectedData{
	Origin:           s.rpOrigin,
	RPID:             savedCreationOptions.RP.ID,
	CredentialAlgs:   credentialAlgs,
	Challenge:        base64.RawURLEncoding.EncodeToString(savedCreationOptions.Challenge),
	UserVerification: savedCreationOptions.AuthenticatorSelection.UserVerification,
    }    
    _, _, err = webauthn.VerifyAttestation(credentialAttestation, expected)
   // Save user credential in datastore.
   c := &credential{
	CredentialID: credentialAttestation.RawID,
	UserID:       uSession.User.UserID,
	Counter:      credentialAttestation.AuthnData.Counter,
	CoseKey:      credentialAttestation.AuthnData.Credential.Raw,
   }    
   err = s.dataStore.addUserCredential(r.Context(), uSession.User, c)
   // Write "ok" response. 
}
Authentication process requires two steps: create credential request options and verify credentials. See signin.html, webauthn.authn.js, and authentication_handlers.go.
Create credential request options:
Server handles /assertion/options request by returning credential request options (PublicKeyCredentialRequestOptions) to client.  Client then uses those options with navigator.credentials.get() to get existing credentials.
// Simplified `/assertion/options` handler from authentication_handlers.go
func (s *server) handleAssertionOptions(w http.ResponseWriter, r *http.Request) {
    // Get user from datastore by username.
    u, _ := s.dataStore.getUser(r.Context(), optionsRequest.Username)
    
    // Create PublicKeyCredentialRequestOptions using webauthn library.
    requestOptions, _ := webauthn.NewAssertionOptions(s.webAuthnConfig, &webauthn.User{ID: u.UserID, Name: u.UserName, DisplayName: u.DisplayName, CredentialIDs: u.CredentialIDs})
    // Save requestOptions and user info in session to verify credential later.
    session.Values[WebAuthnRequestOptions] = requestOptions
    session.Values[UserSession] = &userSession{User: u}
    // Write requestOptions to response.
}
Verify credentials:
Server verifies credentials received via /asssertion/result.
// Simplified `/assertion/result` handler from authentication_handlers.go
func (s *server) handleAssertionResult(w http.ResponseWriter, r *http.Request) {
    // Get saved requestOptions and user info.
    // Parse credential in request body.
    credentialAssertion, _ := webauthn.ParseAssertion(r.Body)
    // Get credential from datastore by received credential ID.
    c, _ := s.dataStore.getCredential(r.Context(), uSession.User.UserID, credentialAssertion.RawID)
    // Verify credential.
    expected := &webauthn.AssertionExpectedData{
	Origin:            s.rpOrigin,
	RPID:              savedRequestOptions.RPID,
	Challenge:         base64.RawURLEncoding.EncodeToString(savedRequestOptions.Challenge),
	UserVerification:  savedRequestOptions.UserVerification,
	UserID:            uSession.User.UserID,
	UserCredentialIDs: userCredentialIDs,
	PrevCounter:       c.Counter,
	Credential:        credKey,
    }
    err = webauthn.VerifyAssertion(credentialAssertion, expected)
    // Update authenticator counter in datastore.
    c.Counter = credentialAssertion.AuthnData.Counter
    err = s.dataStore.updateCredential(r.Context(), c)
    // Write "ok" response. 
}
Security fixes are provided for the latest released version.
To report security vulnerabilities, please email faye.github@gmail.com and allow time for the problem to be resolved before reporting it to the public.
Copyright (c) 2019-present Faye Amacker
fxamacker/webauthn-demo is licensed under the Apache License, Version 2.0. See LICENSE for the full license text.
