mfa

package module
v0.0.0-...-59d1fe4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 11, 2025 License: MIT Imports: 28 Imported by: 0

README

A Serverless MFA API with support for WebAuthn

This project provides a semi-generic backend API for supporting WebAuthn credential registration and authentication. It is intended to be run in a manner as to be shared between multiple consuming applications. It uses an API key and secret to authenticate requests, and further uses that secret as the encryption key. Loss of the API secret would mean loss of all WebAuthn credentials stored.

This application can be run in two ways:

  1. As a standalone server using the builtin webserver available in the server/ folder
  2. As a AWS Lambda function using the lambda/ implementation. This implementation can also use AWS CDK to help automate build/deployment. It should also be noted that the lambda format depends on some resources already existing in AWS. There is a lambda/terraform/ folder with the Terraform configurations needed to provision them.

The API

Yes, as you'll see below this API makes heavy use of custom headers for things that seem like they could go into the request body. We chose to use headers though so that what is sent in the body can be handed off directly to the WebAuthn library and fit the structures it was expecting without causing any conflicts, etc.

Required Headers
  1. x-mfa-apikey - The API Key
  2. x-mfa-apisecret - The API Key Secret
  3. x-mfa-RPDisplayName - The Relying Party Display Name, ex: ACME Inc.
  4. x-mfa-RPID - The Relying Party ID, ex: domain.com (should only be the top level domain, no subdomain, protocol, or path)
  5. x-mfa-RPOrigin - The browser Origin for the request, ex: https://sub.domain.com (include appropriate subdomain and protocol, no path or port)
  6. x-mfa-UserUUID - The UUID for the user attempting to register or authenticate with WebAuthn. This has nothing to do with WebAuthn, but is the primary key for finding the right records in DynamoDB
  7. x-mfa-Username - The user's username of your service
  8. x-mfa-UserDisplayName - The user's display name
Optional headers
  1. x-mfa-Usericon -
  2. x-mfa-Rpicon -
Begin Registration

POST /webauthn/register

Finish Registration

PUT /webauthn/register

Begin Login

POST /webauthn/login

Finish Login

PUT /webauthn/login

Delete Webauthn "User"

DELETE /webauthn/user

Delete one of the user's Webauthn credentials

DELETE /webauthn/credential

Documentation

Index

Constants

View Source
const ApiKeyTablePK = "value"

ApiKeyTablePK is the primary key in the ApiKey DynamoDB table

View Source
const IDParam = "id"
View Source
const LegacyU2FCredID = "u2f"

LegacyU2FCredID is a special case credential ID for legacy U2F support. At most one credential for each user may have this in its ID field.

View Source
const UserContextKey = "user"

UserContextKey is the context key that points to the authenticated user

View Source
const WebAuthnTablePK = "uuid"

WebAuthnTablePK is the primary key in the WebAuthn DynamoDB table

Variables

This section is empty.

Functions

func SetConfig

func SetConfig(c EnvConfig)

Types

type ApiKey

type ApiKey struct {
	Key          string   `dynamodbav:"value" json:"value"`
	Secret       string   `dynamodbav:"-" json:"-"`
	HashedSecret string   `dynamodbav:"hashedApiSecret" json:"hashedApiSecret"`
	Email        string   `dynamodbav:"email" json:"email"`
	CreatedAt    int      `dynamodbav:"createdAt" json:"createdAt"`
	ActivatedAt  int      `dynamodbav:"activatedAt" json:"activatedAt"`
	Store        *Storage `dynamodbav:"-" json:"-"`
}

ApiKey holds API key data from DynamoDB

func NewApiKey

func NewApiKey(email string) (ApiKey, error)

NewApiKey creates a new key with a random value

func (*ApiKey) Activate

func (k *ApiKey) Activate() error

Activate an ApiKey. Creates a random string for the key secret and updates the Secret, HashedSecret, and ActivatedAt fields.

func (*ApiKey) DecryptData

func (k *ApiKey) DecryptData(ciphertext []byte) ([]byte, error)

DecryptData uses the Secret to AES decrypt an arbitrary data block. It does not decrypt the key itself.

func (*ApiKey) DecryptLegacy

func (k *ApiKey) DecryptLegacy(ciphertext string) (string, error)

DecryptLegacy uses the Secret to AES decrypt an arbitrary data block. This is intended only for legacy data such as U2F keys.

func (*ApiKey) EncryptData

func (k *ApiKey) EncryptData(plaintext []byte) ([]byte, error)

EncryptData uses the Secret to AES encrypt an arbitrary data block. It does not encrypt the key itself.

func (*ApiKey) EncryptLegacy

func (k *ApiKey) EncryptLegacy(plaintext string) (string, error)

EncryptLegacy uses the Secret to AES encrypt an arbitrary data block. This is intended only for legacy data such as U2F keys. The returned data is the Base64-encoded IV and the Base64-encoded cipher text separated by a colon.

func (*ApiKey) Hash

func (k *ApiKey) Hash() error

Hash generates a bcrypt hash from the Secret field and stores it in HashedSecret

func (*ApiKey) IsCorrect

func (k *ApiKey) IsCorrect(given string) error

IsCorrect returns true if and only if the key is active and the given string is a match for HashedSecret

func (*ApiKey) Load

func (k *ApiKey) Load() error

Load refreshes an ApiKey from the database record

func (*ApiKey) ReEncrypt

func (k *ApiKey) ReEncrypt(oldKey ApiKey, v *[]byte) error

ReEncrypt decrypts a data block with an old key, then encrypts the resulting plaintext with a new key

func (*ApiKey) ReEncryptLegacy

func (k *ApiKey) ReEncryptLegacy(oldKey ApiKey, v *string) error

ReEncryptLegacy decrypts a data block with an old key, then encrypts the resulting plaintext with a new key. This uses a legacy ciphertext format that is stored as Base64 strings.

func (*ApiKey) ReEncryptTOTPs

func (k *ApiKey) ReEncryptTOTPs(storage *Storage, oldKey ApiKey) (complete, incomplete int, err error)

ReEncryptTOTPs loads each TOTP record that was encrypted using the old key, re-encrypts it using the new key, and writes the updated data back to the database.

func (*ApiKey) ReEncryptWebAuthnUser

func (k *ApiKey) ReEncryptWebAuthnUser(storage *Storage, user WebauthnUser) error

ReEncryptWebAuthnUser re-encrypts a WebAuthnUser using the new key, and writes the updated data back to the database.

func (*ApiKey) ReEncryptWebAuthnUsers

func (k *ApiKey) ReEncryptWebAuthnUsers(storage *Storage, oldKey ApiKey) (complete, incomplete int, err error)

ReEncryptWebAuthnUsers loads each WebAuthn record that was encrypted using the old key, re-encrypts it using the new key, and writes the updated data back to the database.

func (*ApiKey) Save

func (k *ApiKey) Save() error

Save an ApiKey to the database

type App

type App struct {
	// contains filtered or unexported fields
}

func NewApp

func NewApp(cfg EnvConfig) *App

NewApp creates a new App containing configuration and service clients

func (*App) ActivateApiKey

func (a *App) ActivateApiKey(w http.ResponseWriter, r *http.Request)

ActivateApiKey is the handler for the POST /api-key/activate endpoint. It creates the key secret and updates the database record.

func (*App) BeginLogin

func (a *App) BeginLogin(w http.ResponseWriter, r *http.Request)

BeginLogin processes the first half of the Webauthn Authentication flow. It is the handler for the "POST /webauthn/login" endpoint, initiated by the client at the beginning of a login request.

func (*App) BeginRegistration

func (a *App) BeginRegistration(w http.ResponseWriter, r *http.Request)

BeginRegistration processes the first half of the Webauthn Registration flow. It is the handler for the "POST /webauthn/register" endpoint, initiated by the client when creation of a new passkey is requested.

func (*App) CreateApiKey

func (a *App) CreateApiKey(w http.ResponseWriter, r *http.Request)

CreateApiKey is the handler for the POST /api-key endpoint. It creates a new API Key and saves it to the database.

func (*App) DeleteCredential

func (a *App) DeleteCredential(w http.ResponseWriter, r *http.Request)

DeleteCredential is the handler for the "DELETE /webauthn/credential/{credID}" endpoint. It removes a single passkey identified by "credID", which is the key_handle_hash returned by the FinishRegistration endpoint, or "u2f" if it is a legacy U2F credential.

func (*App) DeleteUser

func (a *App) DeleteUser(w http.ResponseWriter, r *http.Request)

DeleteUser is the handler for the "DELETE /webauthn/user" endpoint. It removes a user and any stored passkeys owned by the user.

func (*App) FinishLogin

func (a *App) FinishLogin(w http.ResponseWriter, r *http.Request)

FinishLogin processes the second half of the Webauthn Authentication flow. It is the handler for the "PUT /webauthn/login" endpoint, initiated by the client with login data signed with the private key.

func (*App) FinishRegistration

func (a *App) FinishRegistration(w http.ResponseWriter, r *http.Request)

FinishRegistration processes the last half of the Webauthn Registration flow. It is the handler for the "PUT /webauthn/register" endpoint, initiated by the client with information encrypted by the new private key.

func (*App) GetConfig

func (a *App) GetConfig() EnvConfig

GetConfig returns the config data for the App

func (*App) GetDB

func (a *App) GetDB() *Storage

GetDB returns the database storage client for the App

func (*App) RotateApiKey

func (a *App) RotateApiKey(w http.ResponseWriter, r *http.Request)

RotateApiKey facilitates the rotation of API Keys. All data in webauthn and totp tables that is encrypted by the old key will be re-encrypted using the new key. If the process does not run to completion, this endpoint can be called any number of times to continue the process. A status of 200 does not indicate that all keys were encrypted using the new key. Check the response data to determine if the rotation process is complete.

type EnvConfig

type EnvConfig struct {
	ApiKeyTable   string `required:"true" split_words:"true"`
	TotpTable     string `required:"true" split_words:"true"`
	WebauthnTable string `required:"true" split_words:"true"`

	AwsEndpoint      string `default:"" split_words:"true"`
	AwsDefaultRegion string `default:"" split_words:"true"`

	AWSConfig aws.Config `json:"-"`
}

EnvConfig holds environment specific configurations and is populated on init

func (*EnvConfig) InitAWS

func (e *EnvConfig) InitAWS()

func (*EnvConfig) String

func (e *EnvConfig) String() string

type Storage

type Storage struct {
	// contains filtered or unexported fields
}

Storage provides wrapper methods for interacting with DynamoDB

func NewStorage

func NewStorage(config aws.Config) (*Storage, error)

NewStorage creates a new Storage service, which includes a new DynamoDB Client

func (*Storage) Delete

func (s *Storage) Delete(table, attrName, attrVal string) error

Delete deletes key.

func (*Storage) Load

func (s *Storage) Load(table, attrName, attrVal string, item interface{}) error

Load retrieves the value at key and unmarshals it into item.

func (*Storage) ScanApiKey

func (s *Storage) ScanApiKey(table, apiKey string, items any) error

ScanApiKey a table using apiKey-index

func (*Storage) Store

func (s *Storage) Store(table string, item interface{}) error

Store puts item at key.

type TOTP

type TOTP struct {
	UUID             string `dynamodbav:"uuid" json:"uuid"`
	ApiKey           string `dynamodbav:"apiKey" json:"apiKey"`
	EncryptedTotpKey string `dynamodbav:"encryptedTotpKey" json:"encryptedTotpKey"`
}

type User

type User interface{}

func AuthenticateRequest

func AuthenticateRequest(r *http.Request) (User, error)

AuthenticateRequest checks the provided API key against the keys stored in the database. If the key is active and valid, a Webauthn client and WebauthnUser are created and stored in the request context.

type WebauthnMeta

type WebauthnMeta struct {
	RPDisplayName   string `json:"RPDisplayName"` // Display Name for your site
	RPID            string `json:"RPID"`          // Generally the FQDN for your site
	RPOrigin        string `json:"RPOrigin"`      // The origin URL for WebAuthn requests
	RPIcon          string `json:"RPIcon"`        // Optional icon URL for your site
	UserUUID        string `json:"UserUUID"`
	Username        string `json:"Username"`
	UserDisplayName string `json:"UserDisplayName"`
	UserIcon        string `json:"UserIcon"`
}

WebauthnMeta holds metadata about the calling service for use in WebAuthn responses. Since this service/api is consumed by multiple sources this information cannot be stored in the envConfig

type WebauthnUser

type WebauthnUser struct {
	// Shared fields between U2F and WebAuthn
	ID          string   `dynamodbav:"uuid" json:"uuid"`
	ApiKeyValue string   `dynamodbav:"apiKey" json:"apiKey"`
	ApiKey      ApiKey   `dynamodbav:"-" json:"-"`
	Store       *Storage `dynamodbav:"-" json:"-"`

	// U2F fields
	AppId              string `dynamodbav:"-" json:"-"`
	EncryptedAppId     string `dynamodbav:"encryptedAppId" json:"encryptedAppId,omitempty"`
	KeyHandle          string `dynamodbav:"-" json:"-"`
	EncryptedKeyHandle string `dynamodbav:"encryptedKeyHandle" json:"encryptedKeyHandle,omitempty"`
	PublicKey          string `dynamodbav:"-" json:"-"`
	EncryptedPublicKey string `dynamodbav:"encryptedPublicKey" json:"encryptedPublicKey,omitempty"`

	// WebAuthn fields
	SessionData          webauthn.SessionData `dynamodbav:"-" json:"-"`
	EncryptedSessionData []byte               `dynamodbav:"EncryptedSessionData" json:"EncryptedSessionData,omitempty"`

	// These can be multiple Yubikeys or other WebAuthn entries
	Credentials          []webauthn.Credential `dynamodbav:"-" json:"-"`
	EncryptedCredentials []byte                `dynamodbav:"EncryptedCredentials" json:"EncryptedCredentials,omitempty"`

	WebAuthnClient *webauthn.WebAuthn `dynamodbav:"-" json:"-"`
	Name           string             `dynamodbav:"-" json:"-"`
	DisplayName    string             `dynamodbav:"-" json:"-"`
	Icon           string             `dynamodbav:"-" json:"-"`
}

WebauthnUser holds user data from DynamoDB, in both encrypted and unencrypted form. It also holds a Webauthn client and Webauthn API data.

func NewWebauthnUser

func NewWebauthnUser(apiConfig WebauthnMeta, storage *Storage, apiKey ApiKey, webAuthnClient *webauthn.WebAuthn) WebauthnUser

NewWebauthnUser creates a new WebauthnUser from API input data, a storage client and a Webauthn client.

func (*WebauthnUser) BeginLogin

func (u *WebauthnUser) BeginLogin() (*protocol.CredentialAssertion, error)

BeginLogin processes the first half of the Webauthn Authentication flow for the user and returns the CredentialAssertion data to pass back to the client. User session data is saved in the database.

func (*WebauthnUser) BeginRegistration

func (u *WebauthnUser) BeginRegistration() (*protocol.CredentialCreation, error)

BeginRegistration processes the first half of the Webauthn Registration flow for the user and returns the CredentialCreation data to pass back to the client. User session data is saved in the database.

func (*WebauthnUser) Delete

func (u *WebauthnUser) Delete() error

Delete removes the user from the database

func (*WebauthnUser) DeleteCredential

func (u *WebauthnUser) DeleteCredential(credIDHash string) (int, error)

DeleteCredential expects a hashed-encoded credential id. It finds a matching credential for that user and saves the user without that credential included. Alternatively, if the given credential id indicates that a legacy U2F key should be removed (i.e. by matching the string "u2f") then that user is saved with all of its legacy u2f fields blanked out. CAUTION: user data is refreshed from the database by this function. Any unsaved data will be lost.

func (*WebauthnUser) FinishLogin

func (u *WebauthnUser) FinishLogin(r *http.Request) (*webauthn.Credential, error)

FinishLogin processes the last half of the Webauthn Authentication flow for the user and returns the Credential data to pass back to the client. User session data is untouched by this function.

func (*WebauthnUser) FinishRegistration

func (u *WebauthnUser) FinishRegistration(r *http.Request) (string, error)

FinishRegistration processes the last half of the Webauthn Registration flow for the user and returns the key_handle_hash to pass back to the client. The client should store this value for later use. User session data is cleared from the database.

func (*WebauthnUser) Load

func (u *WebauthnUser) Load() error

Load refreshes a user object from the database record and decrypts the session data and credential list

func (*WebauthnUser) RemoveU2F

func (u *WebauthnUser) RemoveU2F()

RemoveU2F clears U2F fields in the user struct. To be used when a user has requested removal of their legacy U2F key. Should be followed by a database store operation.

func (*WebauthnUser) WebAuthnCredentials

func (u *WebauthnUser) WebAuthnCredentials() []webauthn.Credential

WebAuthnCredentials returns an array of credentials (passkeys) plus a U2F credential if present

func (*WebauthnUser) WebAuthnDisplayName

func (u *WebauthnUser) WebAuthnDisplayName() string

WebAuthnDisplayName returns the display name of the user

func (*WebauthnUser) WebAuthnID

func (u *WebauthnUser) WebAuthnID() []byte

WebAuthnID returns the user's ID according to the Relying Party

func (*WebauthnUser) WebAuthnIcon

func (u *WebauthnUser) WebAuthnIcon() string

WebAuthnIcon returns the user's icon URL

func (*WebauthnUser) WebAuthnName

func (u *WebauthnUser) WebAuthnName() string

WebAuthnName returns the user's name according to the Relying Party

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL