authenticator

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2020 License: BSD-3-Clause Imports: 7 Imported by: 0

README

Build codecov

authenticator

A generic user authentication service supporting FIDO U2F, TOTP, Email, and SMS.

Contents

Overview

Account management is one of the more boring and yet necessary portions of most user facing systems. Here we attempt to provide some sane, secure defaults so you can focus on building your product instead.

For an overview of the API, refer to the documentation here

For an example clientside implementation of some of the core API's provided here, refer to the client repository.

Although there are missing features, the most noteworthy of which is password reset, this project is fully functional. It was originally written as an opportunity to explore the recent addition of the Webauthn browser spec and snowballed into a fully featured authenticator under the premise that it could one day be used for future hobby projects.

Authentication Tokens

JWT tokens are used for authentication. Their stateless nature allows us to check verification without managing a session in a database. Additionally signed tokens provide data integrity, providing our other applications a degree of trust with the user identity information contained within it.

JWT Tokens are embeded with a fingerprint, which we refer to here as a client ID to help prevent token sidejacking.

JWT Tokens are short lived by default (20 minutes) but may be refreshed after expiry with a valid refresh token. Refresh tokens have their own, configurable long lived expiry time (15 days by default) and set on the client securely along side the client ID.

Passwordless Authentication

Passwordless authentication is planned as an optional system wide configuration. It is often used to ease onboarding flows. Popular examples can be seen by popular start ups such as Uber, Grab, and Square Cash. We support this this as we can argue that randomly generated, time sensitive multi-character codes are oftentimes more secure then common user generated passwords and mitigates password reuse.

Registration

Registration requires either a phone number or email address as it is a requirement to verify the authenticity of a user. The service may be required to enforce email only registration, phone only, or a combination of both.

Authentication and 2FA

Authentication requires a password (unless passwordless authentication is enabled) and an assertion of identity. The assertion may be one of the following 2FA methods:

  • OTP: A one time password (by default, a 6 digit code) will be delivered to the user's email address or phone number. This is the default setting and may be disabled after the user enables an alternative 2FA method.

  • TOTP: Users may generate a time based one time password through a supported application.

  • FIDO Users may submit a signed WebAuthn challenge to authenticate with any standard FIDO device (e.g. MacOS fingerprint reader, YubiKey)

Client Flow/Storage

While secure cookie storage is available on web browsers, tokens are instead expected to be stored in either LocalStorage or SessionStorage (or some secure storage in a native mobile app). This ensure clients have access to the token to retrieve basic information about the user's idenitty. To authenticate, clients are expected to create an authentication header and pass their tokens with a Bearer prefix. This eliminates the complexity of additionally supporting CSRF tokens.

To mitigate XSS attacks targeted at token storages, tokens are fingerprinted with a random string's hash. The hash value (the client ID) should be securely stored on the client. After authentication, the client ID is set as a secure cookie on the browser. It is additionally available as part of the response payload so mobile clients can store it themselves.

Revocation and Invalidation

Older tokens may be explicitly revoked by a user or automatically invalidated by us (for example, when refreshing a token). Handling this is an inherent problem with JWT tokens as revocation typlically relies on setting a short enough expiry period for service owners to consider the risk minimal. This allows us to make use of one of the major benefits of JWT tokens by allowing simple validation without a session store.

For this project, I've opted to take the middle ground and support revocation using a fast storage (here Redis is used) and maintaing a blacklist of Token IDs.

Auditability

Records for login history are created upon each successful login and associated with a JWT token ID. This history allows us to provide users a way audit their account and revoke tokens. After revocations, tokens may no longer refresh and the user must login in again to retrieve a new JWT token and accompanying refresh token.

Design Rationale

Token storage: We avoid setting authentication tokens to cookies to avoid the need to provide CSRF token support and allow us to rely solely on the contents of a JWT token for authenticaiton. Fingerprinting the token with a securely stored value is instead used to mitigate risks of XSS attacks that may occur by allowing clients to save their tokens in other storages. It's use case is similar but allows us to complete validation without storing the additional token.

Token invalidation: While not typical in JWT support, we support token invalidation as it provides an additional layer of security and allows us to manage OTP codes without persisting them to a DB as we can now use the token as a transport mechanism for the OTP code (OTP hashes are embeded in the token). The cost to support invalidation was shown to increase validation time by around 3ms.

OTP Message delivery: OTP codes may be delivered through email or SMS. SMS uses the Twilio API however any other API wrapper that is set up to adhere to the same interface may be swapped in. Email delivery may be completed through Sendgrid or Go's standard net/smtp library. Because OTP codes are short lived, and users may request new codes on delivery failure, they are only stored in an in-memory queue during sending as it is acceptable for messages to be lost (e.g. application is restarted) with no attempts made to re-send it. We validate OTP codes by comparing it to an embeded hash in each JWT token. The generation of a new token automatically invalidates an old token with an embeded OTP hash.

2FA: Device 2FA via a valid FIDO U2F device (through Webauthn API) is set as the default 2FA method when enabled, followed by TOTP code generation and finally delivery via Email or SMS. To maintain usability, we do not automatically disable one 2FA option when another is enabled. If users desires to disable a less secure method after enabling a new 2FA method, they are expected to explicitly disable it themsleves. The Client UI should budget for this and guide users through this flow.

SRP: SRP is an authentication protocol to mitigate MITM attacks. It was left out as an authentication protocol for this service as it would add significant complexity to client side auth flow and competes with building adoption for WebAuthn.

Components
  • PostgreSQL: Storage for users, login history, authorized FIDO devices
  • Redis: Blacklist for invalidated tokens, Webauthn session management, API ratelimiting
  • Twilio API: OTP code delivery via SMS
  • Sendgrid API: OTP code delivery via Email (optional)
  • Go stdlib net/smtp: OTP code delivery via Email (default)

Development

Getting Started

In order to complete send OTP codes through SMS or email, you will need a Twilio API key as well as either email credentials to be used with Go's net/smtp library or a Sendgrid API key.

1. Generate default config

config.json and a corresponding docker-compose.yml file will be created. It assumes you intend to run the client and backend on authenticator.local. Once you have a config file, make any necessary changes. The update any necessary (e.g. add API keys).

cd authenticator
make dev

2. Start the project and dependencies

By default the project will be exposed on port 8081.

cd authenticator
docker-compose -f docker-compose.stage.yml up -d

You can check the project is up and running via the healthcheck endpoint

curl http://localhost:8081/healthcheck

If you would like to build and run the project without docker, you can compile the binary directly and pass the location of your configuration file:

go build ./cmd/api
./api --config=./config.json

3. Setup database

If this is your first time running the project, you'll need to set up the initial DB schema found in schema.go.

docker-compose exec postgres psql -U auth -d authenticator_test
Test and Lint

Make sure golangci-lint is installed prior to running the linter.

docker-compose -f docker-compose.test.yml
make test
make lint
Load Testing

Artillery.io is used for load testing. In depth tests are not set up yet but we can get a general idea of performance on token validation with a Redis backed throttle.

First install Artillery (Node.js is a prerequisite)

npm install -g artillery
artillery -V

Set the target domain and run the tests.

export AUTHENTICATOR_DOMAIN=http://138.65.75.135
artillery run loadtest/token-verify.yml

Performance

An indepth review has not been completed yet. Although an initial test on a Digital Ocean droplet $5 droplet (1GB/1CPU, 25GB SSD) with PostgreSQL and Redis running together on the same instance for token validation shows we can reasonably expect handle around 200 concurrent requests per second while maintaining a response time of around 300ms for end users for 95% of requests on the single DO instance.

Ramping up to 800 concurrent users per second on the same DO instance over a 7 minute period shows degregation in response times to 500ms for 95% of requests with a 0.004% error rate.

Example report overview:

  • Server: Digital Ocean (1GB/1CPU, 25GB SSD)
  • Server Location: New York
  • Load Test Client Location: New York
  • Environment: Application, PostgreSQL, Redis, running dockerized on the single instance
  • Conditions: Maximum 800req/sec, average 167req/sec over 7 minutes

Report: Average 5req/sec

Report @ 14:43:47(-0400) 2020-08-07
Elapsed time: 1 minute, 10 seconds
  Scenarios launched:  56
  Scenarios completed: 56
  Requests completed:  56
  Mean response/sec: 5.61
  Response time (msec):
    min: 252.3
    max: 288.3
    median: 267.8
    p95: 283.9
    p99: 288.3
  Codes:
    401: 10
    429: 46

Report: Average 200req/sec

Report @ 14:59:38(-0400) 2020-08-07
Elapsed time: 5 minutes, 20 seconds
  Scenarios launched:  2430
  Scenarios completed: 2420
  Requests completed:  2420
  Mean response/sec: 243.34
  Response time (msec):
    min: 253.3
    max: 1729.4
    median: 278.9
    p95: 320.3
    p99: 727.5
  Codes:
    401: 10
    429: 2410

Report: Request ramp to 800req/sec

Report @ 15:01:18(-0400) 2020-08-07
Elapsed time: 7 minutes, 0 seconds
  Scenarios launched:  7045
  Scenarios completed: 7009
  Requests completed:  7009
  Mean response/sec: 705.91
  Response time (msec):
    min: 266.4
    max: 3362.7
    median: 359.3
    p95: 507.6
    p99: 1585.1
  Codes:
    401: 10
    429: 6999

Report: Summary

Summary report @ 15:01:23(-0400) 2020-08-07
  Scenarios launched:  71063
  Scenarios completed: 71060
  Requests completed:  71060
  Mean response/sec: 167.23
  Response time (msec):
    min: 250.1
    max: 4032.2
    median: 296.2
    p95: 496.3
    p99: 1529.3
  Scenario counts:
    0: 71063 (100%)
  Codes:
    401: 425
    429: 70635
  Errors:
    ECONNRESET: 3

Alternatives

  • Auth0 Packages authentication as a service but results in leaving your user account management up to an external third party which may not be feasible for some compliance or business needs. It's free plan provides limited support and paid plans are arguably expensive for several thousands of users.

  • AuthRocket Provides similar features to Auth0. The service is less mature than it's competitor and more expensive at low tier plans.

References

Pending

Following features are still pending and will be implemented in order:

  • Retrieval of login history: A simple, paginated login history API will be provided so users may be revoke authenticated sessions. The revocation API is already implemented.

  • Password reset: Password reset will be inspired by the advice provided from OWASP. Users will be required to complete 2FA, after which a password reset email will be sent with a time sensitive code to change their password.

  • User enumeration: User enumeration prevention can be improved once password reset is enabled. User lookup errors on the signup flow should trigger the 2FA step on password reset. This strategy is currently employed by Facebook (as of 2020). At the moment we simply return a generic error.

  • Passwordless authentication: As discussed above in this document, passwordless authentication is one of the planned features and will be implemented as an optional system wide configuration.

Documentation

Overview

Package authenticator defines the domain model for user authentication.

Index

Constants

View Source
const (
	// OTPEmail allows a user to complete TFA with an OTP
	// code delivered via email.
	OTPEmail TFAOptions = "otp_email"
	// OTPPhone allows a user to complete TFA with an OTP
	// code delivered via phone.
	OTPPhone = "otp_phone"
	// TOTP allows a user to complete TFA with a TOTP
	// device or application.
	TOTP = "totp"
	// FIDODevice allows a user to complete TFA with a Webauthn
	// compliant device.
	FIDODevice = "device"
)
View Source
const Schema = `` /* 1267-byte string literal not displayed */

Schema contains sql commands to setup the database to work for the authenticator app.

Variables

This section is empty.

Functions

This section is empty.

Types

type ContactAPI

type ContactAPI interface {
	// CheckAddress requests an OTP code to be delivered to a user through
	// an email address or phone number.
	CheckAddress(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// Disable disables a verified email or phone number on a user's profile
	// from receiving OTP codes.
	Disable(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// Verify verifies an OTP code sent to an email or phone number. If
	// delivery address is new to the user, we set it on the profile.
	// By default, verified addresses are enabled for future OTP
	// code delivery unless the client explicitly says otherwise.
	Verify(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// Remove removes a verified email or phone number from the User's profile.
	// Removed email addresses and phone numbers cannot be re-added without
	// requesting a new OTP.
	Remove(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// Send allows a user to request an OTP code to be delivered to them through
	// a pre-approved channel.
	Send(w http.ResponseWriter, r *http.Request) (interface{}, error)
}

ContactAPI provides HTTP handlers to manage email/SMS configuration for a User.

type DeliveryMethod

type DeliveryMethod string

DeliveryMethod represents a mechanism to send messages to users.

const (
	// Phone is a delivery method for text messages.
	Phone DeliveryMethod = "phone"
	// Email is a delivery method for email.
	Email = "email"
)

type Device

type Device struct {
	// ID is a unique service ID for the device.
	ID string
	// UserID is the User's ID associated with the device
	UserID string
	// ClientID is a non unique ID generated by the client
	// during device registration.
	ClientID []byte
	// PublicKey is the public key of a device used for signing
	// purposes.
	PublicKey []byte
	// Name is a User supplied human readable name for a device.
	Name string
	// AAGUID is the globally unique identifier of the authentication
	// device.
	AAGUID []byte
	// SignCount is the stored signature counter of the device.
	// This value is increment to match the device counter on
	// each successive authentication. If the our value is larger
	// than or equal to the device value, it is indicative that the
	// device may be cloned or malfunctioning.
	SignCount uint32
	CreatedAt time.Time
	UpdatedAt time.Time
}

Device represents a device capable of attesting to a User's identity. Examples options include a FIDO U2F key or fingerprint sensor.

type DeviceAPI

type DeviceAPI interface {
	// Verify validates ownership of a new Device for a User.
	Verify(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// Create is an initial request to add a new Device for a User.
	Create(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// Remove removes a Device associated with a User.
	Remove(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// List returns all active devices for a User.
	List(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// Rename renames a Device for a User.
	Rename(w http.ResponseWriter, r *http.Request) (interface{}, error)
}

DeviceAPI provides HTTP handlers to manage Devices for a User.

type DeviceRepository

type DeviceRepository interface {
	// ByID returns a Device by it's ID.
	ByID(ctx context.Context, deviceID string) (*Device, error)
	// ByClientID retrieves a Device associated with a User
	// by a ClientID.
	ByClientID(ctx context.Context, userID string, clientID []byte) (*Device, error)
	// ByUserID retreives all Devices associated with a User.
	ByUserID(ctx context.Context, userID string) ([]*Device, error)
	// Create creates a new Device record.
	Create(ctx context.Context, device *Device) error
	// GetForUpdate retrieves a Device by ID for updating.
	GetForUpdate(ctx context.Context, deviceID string) (*Device, error)
	// Update updates a Device.
	Update(ctx context.Context, device *Device) error
	// Removes a Devie associated with a User.
	Remove(ctx context.Context, deviceID, userID string) error
}

DeviceRepository represents a local storage for Device.

type Emailer added in v0.5.0

type Emailer interface {
	// Email sends an email to an email address
	Email(ctx context.Context, email, subject, message string) error
}

Emailer exposes an email API.

type ErrBadRequest

type ErrBadRequest string

ErrBadRequest represents an error where we fail to read a JSON requst body.

func (ErrBadRequest) Code

func (e ErrBadRequest) Code() ErrCode

func (ErrBadRequest) Error

func (e ErrBadRequest) Error() string

func (ErrBadRequest) Message

func (e ErrBadRequest) Message() string

type ErrCode

type ErrCode string

ErrCode is a machine readable code representing an error within the authenticator domain.

const (
	// EInvalidToken represents an invalid JWT token error.
	EInvalidToken ErrCode = "invalid_token"
	// EInvalidCode represents an invalid OTP code.
	EInvalidCode ErrCode = "invalid_code"
	// EInvalidField represents an entity field error in a repository.
	EInvalidField ErrCode = "invalid_field"
	// EInternal represents an internal error outside of our domain.
	EInternal ErrCode = "internal"
	// EBadRequest represents a bad JSON request body.
	EBadRequest ErrCode = "bad_request"
	// ENotFound represents a non existent entity.
	ENotFound ErrCode = "not_found"
	// EWebAuthn represents a webauthn authentication error.
	EWebAuthn ErrCode = "webauthn"
	// EThrottle represents a rate limiting error.
	EThrottle ErrCode = "too_many_requests"
)

func ErrorCode

func ErrorCode(err error) ErrCode

ErrorCode returns the code associated with a domain error. If an error is not part of the authenticator domain, it returns Internal.

type ErrInvalidCode

type ErrInvalidCode string

ErrInvalidCode represents an error related to an invalid TOTP/OTP code.

func (ErrInvalidCode) Code

func (e ErrInvalidCode) Code() ErrCode

func (ErrInvalidCode) Error

func (e ErrInvalidCode) Error() string

func (ErrInvalidCode) Message

func (e ErrInvalidCode) Message() string

type ErrInvalidField

type ErrInvalidField string

ErrInvalidField represents an error related to missing or invalid entity fields.

func (ErrInvalidField) Code

func (e ErrInvalidField) Code() ErrCode

func (ErrInvalidField) Error

func (e ErrInvalidField) Error() string

func (ErrInvalidField) Message

func (e ErrInvalidField) Message() string

type ErrInvalidToken

type ErrInvalidToken string

ErrInvalidToken represents an error related to JWT token invalidation such as expiry, revocation, or signing errors.

func (ErrInvalidToken) Code

func (e ErrInvalidToken) Code() ErrCode

func (ErrInvalidToken) Error

func (e ErrInvalidToken) Error() string

func (ErrInvalidToken) Message

func (e ErrInvalidToken) Message() string

type ErrNotFound

type ErrNotFound string

ErrNotFound represents an error where we fail to read a JSON requst body.

func (ErrNotFound) Code

func (e ErrNotFound) Code() ErrCode

func (ErrNotFound) Error

func (e ErrNotFound) Error() string

func (ErrNotFound) Message

func (e ErrNotFound) Message() string

type ErrThrottle added in v0.3.0

type ErrThrottle string

ErrThrottle represents a rate limiting error.

func (ErrThrottle) Code added in v0.3.0

func (e ErrThrottle) Code() ErrCode

func (ErrThrottle) Error added in v0.3.0

func (e ErrThrottle) Error() string

func (ErrThrottle) Message added in v0.3.0

func (e ErrThrottle) Message() string

type ErrWebAuthn

type ErrWebAuthn string

ErrWebAuthn represents an error related to webauthn authentication.

func (ErrWebAuthn) Code

func (e ErrWebAuthn) Code() ErrCode

func (ErrWebAuthn) Error

func (e ErrWebAuthn) Error() string

func (ErrWebAuthn) Message

func (e ErrWebAuthn) Message() string

type Error

type Error interface {
	Error() string
	Message() string
	Code() ErrCode
}

Error represents an error within the authenticator domain.

func DomainError

func DomainError(err error) Error

DomainError returns a domain error if available.

type LoginAPI

type LoginAPI interface {
	// Login is the initial login step to identify a User.
	// On success it will return a JWT token in a pre_authorized state.
	Login(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// DeviceChallenge retrieves a device challenge to be signed by the client.
	DeviceChallenge(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// VerifyDevice verifies a User's authenticity by verifying
	// a signing device owned by the user. On success it will return
	// a JWT token in an authorized state.
	VerifyDevice(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// VerifyCode verifies a User's authenticity by verifying
	// a TOTP or randomly generated code delivered by SMS/Email.
	// On success it will return a JWT token in an auhtorized state.
	VerifyCode(w http.ResponseWriter, r *http.Request) (interface{}, error)
}

LoginAPI provides HTTP handlers for user authentication.

type LoginHistory

type LoginHistory struct {
	// TokenID is the ID of a JWT token.
	TokenID string
	// UserID is the User's ID associated with the login record.
	UserID string
	// IsRevoked is a boolean indicating the token has
	// been revoked. Tokens are invalidated through
	// expiry or revocation.
	IsRevoked bool
	// ExpiresAt is the expiry time of the JWT token.
	ExpiresAt time.Time
	CreatedAt time.Time
	UpdatedAt time.Time
}

LoginHistory represents a login associated with a user.

type LoginHistoryRepository

type LoginHistoryRepository interface {
	// ByTokenID retrieves a LoginHistory record by a JWT token ID.
	ByTokenID(ctx context.Context, tokenID string) (*LoginHistory, error)
	// ByUserID retrieves recent LoginHistory associated with a User's ID.
	// It supports pagination through a limit or offset value.
	ByUserID(ctx context.Context, userID string, limit, offset int) ([]*LoginHistory, error)
	// Create creates a new LoginHistory.
	Create(ctx context.Context, login *LoginHistory) error
	// GetForUpdate retrieves a LoginHistory by TokenID for updating.
	GetForUpdate(ctx context.Context, tokenID string) (*LoginHistory, error)
	// Update updates a LoginHistory.
	Update(ctx context.Context, login *LoginHistory) error
}

LoginHistoryRepository represents a local storage for LoginHistory.

type Message

type Message struct {
	// Type describes the classification of a Message.
	Type MessageType
	// Subject is a human readable subject describe the Message.
	Subject string
	// Delivery type of the message (e.g. phone or email).
	Delivery DeliveryMethod
	// Vars contains key/value variables to populate
	// templated content.
	Vars map[string]string
	// Content of the message.
	Content string
	// Delivery address of the user (e.g. phone or email).
	Address string
	// ExpiresAt is the latest time we can attempt delivery.
	ExpiresAt time.Time
	// DeliveryAttempts is the total amount of delivery attempts made.
	DeliveryAttempts int
}

Message is a message to be delivered to a user.

type MessageRepository

type MessageRepository interface {
	// Publish prepares a message for a user. Behind the scenes we write the
	// message into a channel to be processed by a consumer.
	Publish(ctx context.Context, msg *Message) error
	// Recent retrieves a list of messages to be delivered.
	Recent(ctx context.Context) (<-chan *Message, <-chan error)
}

MessageRepository represents a local storage for outgoing messages. This service will deliver OTP codes via email or SMS if enabled for the user.

type MessageType added in v0.5.0

type MessageType string

MessageType describes a classification of a Message

const (
	// OTPAddress is a message containing an OTP code for contact verification
	OTPAddress MessageType = "otp_address"
	// OTPResend is a message containing an OTP code
	OTPResend MessageType = "otp_resend"
	// OTPLogin is a message containing an OTP code for login.
	OTPLogin MessageType = "otp_login"
	// OTPSignup is a message containing an OTP code for signup.
	OTPSignup MessageType = "otp_signup"
)

type MessagingService

type MessagingService interface {
	// Send sends a message to a user.
	Send(ctx context.Context, msg *Message) error
}

MessagingService sends messages through email or SMS.

type OTPService

type OTPService interface {
	// TOTPQRString returns a URL string used for TOTP code generation.
	TOTPQRString(u *User) (string, error)
	// TOTPSecret creates a TOTP secret for code generation.
	TOTPSecret(u *User) (string, error)
	// OTPCode creates a random OTP code and hash.
	OTPCode(address string, method DeliveryMethod) (code, hash string, err error)
	// ValidateOTP checks if a User email/sms delivered OTP code is valid.
	ValidateOTP(code, hash string) error
	// ValidateTOTP checks if a User TOTP code is valid.
	ValidateTOTP(ctx context.Context, user *User, code string) error
}

OTPService manages the protocol for SMS/Email 2FA codes and TOTP codes.

type PasswordService

type PasswordService interface {
	// Hash hashes a password for storage.
	Hash(password string) ([]byte, error)
	// Validate determines if a submitted pasword is valid for a stored
	// password hash.
	Validate(user *User, password string) error
	// OKForUser checks if a password may be used for a user.
	OKForUser(password string) error
}

PasswordService manages the protocol for password management and validation.

type RepositoryManager

type RepositoryManager interface {
	// NewWithTransaction returns a new manager to with a transaction
	// enabled.
	NewWithTransaction(ctx context.Context) (RepositoryManager, error)
	// WithAtomic performs an operation inside of a transaction.
	// On success, it will return an entity.
	WithAtomic(operation func() (interface{}, error)) (interface{}, error)
	// LoginHistory returns a LoginHistoryRepository.
	LoginHistory() LoginHistoryRepository
	// Device returns a DeviceRepository.
	Device() DeviceRepository
	// User returns a UserRepository.
	User() UserRepository
}

RepositoryManager manages repositories stored in storages with atomic properties.

type SMSer added in v0.5.0

type SMSer interface {
	// SMS sends an SMS to an phone number
	SMS(ctx context.Context, phoneNumber string, message string) error
}

SMSer exposes an SMS API.

type SignUpAPI

type SignUpAPI interface {
	// SignUp is the initial registration step to identify a User.
	// On success it will return a JWT token in an unverified state.
	SignUp(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// Verify is the final registration step to validate a new
	// User's authenticity. On success it will return a JWT
	// token in an authozied state.
	Verify(w http.ResponseWriter, r *http.Request) (interface{}, error)
}

SignUpAPI provides HTTP handlers for user registration.

type TFAOptions

type TFAOptions string

TFAOptions represents options a user may use to complete 2FA.

type TOTPAPI

type TOTPAPI interface {
	// Secret requests a TOTP secret to allow a user to generate TOTP codes
	// via a supported application (e.g. Google Authenticator).
	Secret(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// Verify enables TOTP as an TFA option for a user by accepting a
	// TOTP code and validating it against the TFA secret configured
	// on the user.
	Verify(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// Remove disables TOTP as an TFA option for a user by accepting a
	// TOTP code and validating it against the TFA secret configured
	// on the user.
	Remove(w http.ResponseWriter, r *http.Request) (interface{}, error)
}

TOTPAPI provides HTTP handlers to manage TOTP configuration for a User.

type Token

type Token struct {
	// jwt.StandardClaims provides standard JWT fields
	// such as Audience, ExpiresAt, Id, Issuer.
	jwt.StandardClaims
	// ClientID is the unhashed ID stored securely on the client and used
	// to validate the token request source. It is not embedded in the
	// the JWT token body.
	ClientID string `json:"-"`
	// ClientIDHash is hash of an ID stored in the client for which
	// the token was delivered to. A token is only valid when the
	// hash's corresponding ClientID is delivered alongside the JWT token.
	ClientIDHash string `json:"client_id"`
	// UserID is the User's ID.
	UserID string `json:"user_id"`
	// Email is a User's email.
	Email string `json:"email"`
	// Phone is a User's phone number.
	Phone string `json:"phone_number"`
	// State is the current state of the user at the time
	// the token was issued.
	State TokenState `json:"state"`
	// CodeHash is the hash of a randomly generated code used
	// to validate an OTP code and escalate the token to an
	// authorized token.
	CodeHash string `json:"code,omitempty"`
	// Code is the unhashed value of CodeHash. This value is
	// not persisted and returned to the client outside of the JWT
	// response through an alternative mechanism (e.g. Email). It is
	// validated by ensuring the SHA512 hash of the value matches the
	// CodeHash embedded in the token.
	Code string `json:"-"`
	// RefreshToken is the unhashed refresh token stored securely on
	// the client and used to refresh an expired JWT token. RefreshTokens
	// have separate expiry times compared to the JWT token.
	RefreshToken string `json:"-"`
	// RefreshTokenHash is the hash of a RefreshToken stored on the client
	// for which the JWT token was delivered to. A RefreshToken is validated
	// by ensuring the SHA512 hash of the value matches the RefreshTokenHash.
	RefreshTokenHash string `json:"refresh_token"`
	// TFAOptions represents available options a user may use to complete
	// 2FA.
	TFAOptions []TFAOptions `json:"tfa_options"`
	// DefaultTFA is the develop TFA method clients should offer a user.
	DefaultTFA TFAOptions `json:"default_tfa"`
}

Token is a token that provides proof of User authentication.

type TokenAPI

type TokenAPI interface {
	// Revoke revokes a User's token for a logged in session.
	Revoke(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// Verify verifies a User's token is authenticated and
	// valid. A valid token is not expired and not revoked.
	Verify(w http.ResponseWriter, r *http.Request) (interface{}, error)
	// Refresh refreshes an expired token with a new expiry time.
	// Refreshed tokens share a token's original ID and client ID.
	Refresh(w http.ResponseWriter, r *http.Request) (interface{}, error)
}

TokenAPI provides HTTP handlers to manage a User's tokens.

type TokenConfiguration added in v0.2.0

type TokenConfiguration struct {
	DeliveryMethod   DeliveryMethod
	DeliveryAddress  string
	RefreshableToken *Token
}

TokenConfiguration provides configurable settings for a JWT token.

type TokenOption added in v0.2.0

type TokenOption func(*TokenConfiguration)

TokenOption configures a new JWT token.

type TokenService

type TokenService interface {
	// Create creates a new JWT token with optional configuration settings.
	Create(ctx context.Context, user *User, state TokenState, options ...TokenOption) (*Token, error)
	// Sign creates a signed JWT token string from a token struct.
	Sign(ctx context.Context, token *Token) (string, error)
	// Validate checks that a JWT token is signed by us, unexpired,
	// unrevoked, and from the a valid client. On success it will return the unpacked
	// Token struct.
	Validate(ctx context.Context, signedToken string, clientID string) (*Token, error)
	// Revoke Revokes a token by it's ID.
	Revoke(ctx context.Context, tokenID string) error
	// Cookies returns secure cookies to accompany a token.
	Cookies(ctx context.Context, token *Token) []*http.Cookie
	// Refreshable checks if a provided token can be refreshed.
	Refreshable(ctx context.Context, token *Token, refreshToken string) error
	// RefreshableTill returns the latest validity time for a token's accompanying refresh token.
	RefreshableTill(ctx context.Context, token *Token, refreshToken string) time.Time
}

TokenService represents a service to manage JWT tokens.

type TokenState

type TokenState string

TokenState represents a state of a JWT token. A token may represent an intermediary state prior to authorization (ex. TOTP code is required)

const (
	// JWTPreAuthorized represents the state of a user before completing
	// the TFA step of signup or login.
	JWTPreAuthorized TokenState = "pre_authorized"
	// JWTAuthorized represents a the state of a user after completing
	// the final step of login or signup.
	JWTAuthorized TokenState = "authorized"
)

type User

type User struct {
	// ID is a unique ID for the user.
	ID string
	// Phone is a phone number associated with the account.
	Phone sql.NullString
	// Email is an email address associated with the account.
	Email sql.NullString
	// Password is the current User provided password.
	Password string
	// TFASecret is a a secret string used to generate 2FA TOTP codes.
	TFASecret string
	// IsPhoneAllowed specifies a user may complete authentication
	// by verifying an OTP code delivered through SMS.
	IsPhoneOTPAllowed bool
	// IsEmailOTPAllowed specifies a user may complete authentication
	// by verifying an OTP code delivered through email.
	IsEmailOTPAllowed bool
	// IsTOTPAllowed specifies a user may complete authentication
	// by verifying a TOTP code.
	IsTOTPAllowed bool
	// IsDeviceAllowed specifies a user may complete authentication
	// by verifying a WebAuthn capable device.
	IsDeviceAllowed bool
	// IsVerified tells us if a user confirmed ownership of
	// an email or phone number by validating a one time code
	// after registration.
	IsVerified bool
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

User represents a user who is registered with the service.

func (*User) CanSendDefaultOTP

func (u *User) CanSendDefaultOTP() bool

CanSendDefaultOTP determines if an OTP code should be sent out to a user immediately as a 2FA option.

func (*User) DefaultName

func (u *User) DefaultName() string

DefaultName returns the default name for a user (email or phone).

func (*User) DefaultOTPDelivery

func (u *User) DefaultOTPDelivery() DeliveryMethod

DefaultOTPDelivery returns the default OTP delivery method.

func (*User) DefaultTFA added in v0.2.0

func (u *User) DefaultTFA() TFAOptions

DefaultTFA is the recommended enabled TFA option clients should offer a user.

type UserAPI

type UserAPI interface {
	// UpdatePassword change's a User's password.
	UpdatePassword(w http.ResponseWriter, r *http.Request) (interface{}, error)
}

UserAPI proivdes HTTP handlers to configure a registered User's account.

type UserRepository

type UserRepository interface {
	// ByIdentity retrieves a User by some whitelisted identity
	// value such as email, phone, username, ID.
	ByIdentity(ctx context.Context, attribute, value string) (*User, error)
	// GetForUpdate retrieves a User by ID for updating.
	GetForUpdate(ctx context.Context, userID string) (*User, error)
	// Create creates a new User Record.
	Create(ctx context.Context, u *User) error
	// ReCreate updates an existing, unverified User record,
	// to treat the entry as a new unverified User registration.
	// User's are considered unverified until completing OTP
	// verification to prove ownership of a phone or email address.
	ReCreate(ctx context.Context, u *User) error
	// Update updates a User.
	Update(ctx context.Context, u *User) error
	// DisableOTP disables an OTP method for a User.
	DisableOTP(ctx context.Context, userID string, method DeliveryMethod) (*User, error)
	// RemoveDeliveryMethod removes a phone or email from a User.
	RemoveDeliveryMethod(ctx context.Context, userID string, method DeliveryMethod) (*User, error)
}

UserRepository represents a local storage for User.

type WebAuthnService

type WebAuthnService interface {
	// BeginSignUp attempts to register a new WebAuthn device.
	BeginSignUp(ctx context.Context, user *User) ([]byte, error)
	// FinishSignUp confirms a challenge signature for registration.
	FinishSignUp(ctx context.Context, user *User, r *http.Request) (*Device, error)
	// BeginLogin starts the authentication flow to validate a device.
	BeginLogin(ctx context.Context, user *User) ([]byte, error)
	// FinishLogin confirms that a device successfully signed a challenge.
	FinishLogin(ctx context.Context, user *User, r *http.Request) error
}

WebAuthnService manages the protocol for WebAuthn authentication.

Directories

Path Synopsis
cmd
api
Command API exposes user authentication HTTP API.
Command API exposes user authentication HTTP API.
internal
contactapi
Package contactapi provides an HTTP API for email/SMS OTP management.
Package contactapi provides an HTTP API for email/SMS OTP management.
contactchecker
Package contactchecker offers utility functions for validating addresses.
Package contactchecker offers utility functions for validating addresses.
crypto
Package crypto provides cryptographic utility functions.
Package crypto provides cryptographic utility functions.
deviceapi
Package deviceapi provides an HTTP API for device registration.
Package deviceapi provides an HTTP API for device registration.
entropy
Package entropy provides safe entropy to use in concurrent tasks
Package entropy provides safe entropy to use in concurrent tasks
httpapi
Package httpapi provides common encoding and middleware for an HTTP API.
Package httpapi provides common encoding and middleware for an HTTP API.
loginapi
Package loginapi provides an HTTP API for user authentication.
Package loginapi provides an HTTP API for user authentication.
msgconsumer
Package msgconsumer reads and sends SMS/Email messages from a repository.
Package msgconsumer reads and sends SMS/Email messages from a repository.
msgpublisher
Package msgpublisher publishes outgoing SMS/Email messages.
Package msgpublisher publishes outgoing SMS/Email messages.
msgrepo
Package msgrepo provides message storage for consumers and publishers.
Package msgrepo provides message storage for consumers and publishers.
otp
Package otp provides 2FA codes generation.
Package otp provides 2FA codes generation.
password
Package password provides password management through bcrypt.
Package password provides password management through bcrypt.
sendgrid
Package sendgrid adapts sendgrid-go to our Email interface.
Package sendgrid adapts sendgrid-go to our Email interface.
signupapi
Package signupapi provides an HTTP API for user registration.
Package signupapi provides an HTTP API for user registration.
tokenapi
Package tokenapi provides an HTTP API for managing JWT tokens.
Package tokenapi provides an HTTP API for managing JWT tokens.
totpapi
Package totpapi provides an HTTP API for TOTP management.
Package totpapi provides an HTTP API for TOTP management.
twilio
Package twilio exposes Twilio's REST API.
Package twilio exposes Twilio's REST API.

Jump to

Keyboard shortcuts

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