Skip to content

Latest commit

 

History

History
404 lines (296 loc) · 16.7 KB

README.md

File metadata and controls

404 lines (296 loc) · 16.7 KB

WebAuthn Library

GoDoc Go Report Card

This library is meant to handle Web Authentication for Go apps that wish to implement a passwordless solution for users. This library conforms as much as possible to the guidelines and implementation procedures outlined by the document.

Fork

This library is a hard fork of github.com/duo-labs/webauthn and is the natural successor to that library.

See the migration guide for more information about how to migrate and the differences between the libraries.

It is distributed under the same 3-Clause BSD license as the original fork, with the only amendment being the additional 3-Clause BSD license attributing license rights to this repository.

Go Version Support Policy

This library; unless otherwise explicitly expressed; will officially support versions of go which are currently supported by the go maintainers (usually 3 minor versions) with a brief transition time (usually 1 patch release of go, for example if go 1.21.0 is released, we will likely still support go 1.17 until go 1.21.1 is released). These specific rules apply at the time of a published release.

This library in our opinion handles a critical element of security in a dependent project and we aim to avoid backwards compatibility at the cost of security wherever possible. We also consider this especially important in a language like go where their backwards compatibility when upgrading the compile tools is usually flawless.

This policy means that users who wish to build this with older versions of go may find there are features being used which are not available in that version. The current intentionally supported versions of go are as follows:

  • go 1.22
  • go 1.21
  • go 1.20:
    • Go 1.20 support has been removed due to the new toolchain directive and lack of support in Go 1.20. This directive is unfortunately being used in dependent libraries and we'd opt for ensuring we can easily obtain potential fixes to CVE's rather than backwards compatibility. A such we have lifted the version requirement and implemented the toolchain directive in our module to reflect the intended toolchain.

Status

This library is still version 0, as per Semantic Versioning 2.0 rules there may be breaking changes without warning. While we strive to avoid such changes and strive to notify users they may be unavoidable.

Quickstart

go get github.com/go-webauthn/webauthn and initialize it in your application with basic configuration values.

Make sure your user model is able to handle the interface functions laid out in webauthn/types.go. This means also supporting the storage and retrieval of the credential and authenticator structs in webauthn/credential.go and webauthn/authenticator.go, respectively.

Examples

The following examples show some basic use cases of the library. For consistency sake the following variables are used to denote specific things:

  • Variable webAuthn: the webauthn.WebAuthn instance you initialize elsewhere in your code
  • Variable datastore: the pseudocode backend service that stores your webauthn session data and users such as PostgreSQL
  • Variable session: the webauthn.SessionData object
  • Variable user: your webauthn.User implementation

We try to avoid using specific external libraries (excluding stdlib) where possible, and you'll need to adapt these examples with this in mind.

Initialize the request handler

package example

import (
	"fmt"

	"github.com/go-webauthn/webauthn/webauthn"
)

var (
	webAuthn *webauthn.WebAuthn
	err error
)

// Your initialization function
func main() {
	wconfig := &webauthn.Config{
		RPDisplayName: "Go Webauthn", // Display Name for your site
		RPID: "go-webauthn.local", // Generally the FQDN for your site
		RPOrigins: []string{"https://login.go-webauthn.local"}, // The origin URLs allowed for WebAuthn requests
	}
	
	if webAuthn, err = webauthn.New(wconfig); err != nil {
		fmt.Println(err)
	}
}

Registering an account

package example

func BeginRegistration(w http.ResponseWriter, r *http.Request) {
	user := datastore.GetUser() // Find or create the new user  
	options, session, err := webAuthn.BeginRegistration(user)
	// handle errors if present
	// store the sessionData values 
	JSONResponse(w, options, http.StatusOK) // return the options generated
	// options.publicKey contain our registration options
}

func FinishRegistration(w http.ResponseWriter, r *http.Request) {
	user := datastore.GetUser() // Get the user
	
	// Get the session data stored from the function above
	session := datastore.GetSession()
		
	credential, err := webAuthn.FinishRegistration(user, session, r)
	if err != nil {
		// Handle Error and return.

		return
	}
	
	// If creation was successful, store the credential object
	// Pseudocode to add the user credential.
	user.AddCredential(credential)
	datastore.SaveUser(user)

	JSONResponse(w, "Registration Success", http.StatusOK) // Handle next steps
}

Logging into an account

package example

func BeginLogin(w http.ResponseWriter, r *http.Request) {
	user := datastore.GetUser() // Find the user
	
	options, session, err := webAuthn.BeginLogin(user)
	if err != nil {
		// Handle Error and return.

		return
	}
	
	// store the session values
	datastore.SaveSession(session)
	
	JSONResponse(w, options, http.StatusOK) // return the options generated
	// options.publicKey contain our registration options
}

func FinishLogin(w http.ResponseWriter, r *http.Request) {
	user := datastore.GetUser() // Get the user 
	
	// Get the session data stored from the function above
	session := datastore.GetSession()
	
	credential, err := webAuthn.FinishLogin(user, session, r)
	if err != nil {
		// Handle Error and return.

		return
	}

	// Handle credential.Authenticator.CloneWarning

	// If login was successful, update the credential object
	// Pseudocode to update the user credential.
	user.UpdateCredential(credential)
	datastore.SaveUser(user)
	
	JSONResponse(w, "Login Success", http.StatusOK)
}

Modifying Credential Options

You can modify the default credential creation options for registration and login by providing optional structs to the BeginRegistration and BeginLogin functions.

Registration modifiers

You can modify the registration options in the following ways:

package example

import (
	"github.com/go-webauthn/webauthn/protocol"
	"github.com/go-webauthn/webauthn/webauthn"
)

var webAuthn webauthn.WebAuthn // init this in your init function

func beginRegistration() {
	// Updating the AuthenticatorSelection options. 
	// See the struct declarations for values
	authSelect := protocol.AuthenticatorSelection{
		AuthenticatorAttachment: protocol.AuthenticatorAttachment("platform"),
		RequireResidentKey: protocol.ResidentKeyNotRequired(),
		UserVerification: protocol.VerificationRequired,
	}

	// Updating the ConveyencePreference options. 
	// See the struct declarations for values
	conveyancePref := protocol.PreferNoAttestation

	user := datastore.GetUser() // Get the user  
	opts, session, err := webAuthn.BeginRegistration(user, webauthn.WithAuthenticatorSelection(authSelect), webauthn.WithConveyancePreference(conveyancePref))

	// Handle next steps
}

Login modifiers

You can modify the login options to allow only certain credentials:

package example

import (
	"github.com/go-webauthn/webauthn/protocol"
	"github.com/go-webauthn/webauthn/webauthn"
)

var webAuthn webauthn.WebAuthn // init this in your init function

func beginLogin() {
	// Updating the AuthenticatorSelection options. 
	// See the struct declarations for values
	allowList := make([]protocol.CredentialDescriptor, 1)
	allowList[0] = protocol.CredentialDescriptor{
		CredentialID: credentialToAllowID,
		Type: protocol.CredentialType("public-key"),
	}

	user := datastore.GetUser() // Get the user  

	opts, session, err := w.BeginLogin(user, webauthn.WithAllowedCredentials(allowList))

	// Handle next steps
}

Timeout Mechanics

The library by default does not enforce timeouts. However the default timeouts sent to the browser are taken from the specification. You can override both of these behaviours however.

package example

import (
	"fmt"
	"time"
	
	"github.com/go-webauthn/webauthn/protocol"
	"github.com/go-webauthn/webauthn/webauthn"
)

func main() {
	wconfig := &webauthn.Config{
		RPDisplayName: "Go Webauthn",                               // Display Name for your site
		RPID:          "go-webauthn.local",                         // Generally the FQDN for your site
		RPOrigins:     []string{"https://login.go-webauthn.local"}, // The origin URLs allowed for WebAuthn requests
		Timeouts: webauthn.TimeoutsConfig{
			Login: webauthn.TimeoutConfig{
				Enforce:    true, // Require the response from the client comes before the end of the timeout.
				Timeout:    time.Second * 60, // Standard timeout for login sessions.
				TimeoutUVD: time.Second * 60, // Timeout for login sessions which have user verification set to discouraged.
			},
			Registration: webauthn.TimeoutConfig{
				Enforce:    true, // Require the response from the client comes before the end of the timeout.
				Timeout:    time.Second * 60, // Standard timeout for registration sessions.
				TimeoutUVD: time.Second * 60, // Timeout for login sessions which have user verification set to discouraged.
			},
		},
	}
	
	webAuthn, err := webauthn.New(wconfig)
	if err != nil {
		fmt.Println(err)
	}
}

Credential Record

The WebAuthn Level 3 specification describes the Credential Record which includes several required and optional elements that you should store for. See § 4 Terminology for details.

This section describes this element.

The fields listed in the specification have corresponding fields in the webauthn.Credential struct. See the below table for more information. We also include JSON mappings for those that wish to just store these values as JSON.

Specification Field Library Field JSON Field Notes
type N/A N/A This field is always publicKey for WebAuthn
id ID id
publicKey PublicKey publicKey
signCount Authenticator.SignCount authenticator.signCount
transports Transport transport
uvInitialized Flags.UserVerified flags.userVerified
backupEligible Flags.BackupEligible flags.backupEligible
backupState Flags.BackupState flags.backupState
attestationObject Attestation.Object attestation.object This field is a composite of the attestationObject and the relevant values to validate it
attestationClientDataJSON Attestation.ClientDataJSON attestation.clientDataJSON

Flags

It's important to note that the recommendations and requirements for flag storage have changed over the course of the evolution of the WebAuthn specification. We at the present time only make the flags classified like this available for easy storage however we also make the Protocol Value available. At such a time as these recommendations or requirements change we will adapt accordingly. The Protocol Value is a raw representation of the flags and as such is resistant to breaking changes whereas the other flags or lack thereof may not be.

Implementers are therefore encouraged to use func (CredentialFlags) ProtocolValue to retrieve the raw value and webauthn.NewCredentialFlags to restore it; and instead of using the individual flags to store the value store the Protocol Value, and only store the individual flags as a means to perform compliance related decisions.

Notable Changes

This contains some notable changes to the flags over the life of the library.

v0.11.0

In v0.11.0 we started validating the backup related flags to ensure that they were in a valid state as per the requirements in the spec. This introduced issues for some users as they had not been storing them and at least at one point the flag values were difficult to obtain.

This has lead to an effective breaking change and a state where some credentials cannot be validated. The resolution to this particular issue is to adapt current storage methods so that the values of the flags or each individual flag default to a null-like value and manually perform an update to the storage and struct when a credential with null-like values is observed.

The values can be obtained prior to validating the parsed response similar to the example below:

package example

import (
	"net/http"
	
	"github.com/go-webauthn/webauthn/protocol"
	"github.com/go-webauthn/webauthn/webauthn"
)

func FinishLogin(w http.ResponseWriter, r *http.Request) {
  // Abstract Business Logic: Get the WebAuthn User. 
  user := datastore.GetUser()

  // Abstract Business Logic: Get the WebAuthn Session Data. 
  session := datastore.GetSession()

  parsedResponse, err := protocol.ParseCredentialRequestResponse(r)
  if err != nil {
    // Handle Error and return.
    return
  }

  // Handle updating the appropriate credential using the flags value.
  flags := webauthn.NewCredentialFlags(parsedResponse.Response.AuthenticatorData.Flags)
}

Storage

It is also important to note that restoring the webauthn.Credential with the correct values will likely affect the validity of the webauthn.Credential, i.e. if some values are not restored the webauthn.Credential may fail validation in this scenario.

Verification

As long as the webauthn.Credential struct has exactly the same values when restored the Credential Verify function can be leveraged to verify the credential against the metadata.Provider. This can be either done during registration, on every login, or with a audit schedule.

In addition to using the Credential Verify function the webauthn.Config can contain a provider which will process all registrations automatically.

At this time no tooling exists to verify the credential automatically outside the registration flow. Implementation of this is considered domain logic and beyond the scope of what we provide documentation for; we just provide the necessary tooling to implement this yourself.

Acknowledgements

We graciously acknowledge the original authors of this library github.com/duo-labs/webauthn for their amazing implementation. Without their amazing work this library could not exist.