passwordless

package module
v8.0.0 Latest Latest
Warning

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

Go to latest
Published: Feb 25, 2021 License: MIT Imports: 21 Imported by: 0

README

go-passwordless

go-passwordless is an implementation of backend services allowing users to sign in to websites without a password, inspired by the Node package of the same name.

Overview

The passwordless flow is very similar to the one-time-password (OTP) flow used for verification on many services. It works on the principle that if someone can prove ownership of an account such as an email address, then that is sufficient to prove they are that user. So, rather than storing passwords, the user is simply required to enter a secure code that is sent to their account when they want to log in (be it email, SMS, a Twitter DM, or some other means.)

This implementation concerns itself with generating codes, sending them to the user, storing them securely, and offering a means to verify the provided token.

Transports

A Transport provides a means to transmit a token (e.g. a PIN) to the user. There is one production implementation and one development implementation provided with this library:

  • SMTPTransport - emails tokens via an SMTP server.
  • LogTransport - prints tokens to stdout, for testing purposes only.

Custom transports must adhere to the Transport interface, which consists of just one function, making it easy to hook into third-party services (for example, your SMS provider.)

Token Stores

A Token Store provides a mean to securely store and verify a token against user input. There are three implementations provided with this library:

  • MemStore - stores encrypted tokens in ephemeral memory.
  • CookieStore - stores tokens in encrypted session cookies. Mandates that the user signs in on the same device that they generated the sign in request from.
  • RedisStore - stores encrypted tokens in a Redis instance.

Custom stores need to adhere to the TokenStore interface, which consists of 4 functions. This interface is intentionally simple to allow for easy integration with whatever database and structure you prefer.

Differences to Node's Passwordless

While heavily inspired by Passwordless, this implementation is unique and cannot be used interchangeably. The token generation, storage and verification procedures are all different.

This library does not provide a frontend/UI implementation - to integrate it, you'll need to create your own signin/verification pages and handlers. An example website is provided as reference, however.

Documentation

Overview

`go-passwordless` is an implementation of backend services allowing users to sign in to websites without a password, inspired by the [Node package of the same name](passwordless.net).

Install the library with `go get`:

$ go get github.com/johnsto/go-passwordless

Import the library into your project:

import "github.com/johnsto/go-passwordless"

Create an instance of Passwordless with your chosen token store. In this case, `MemStore` will hold tokens in memory until they expire.

pw = passwordless.New(passwordless.NewMemStore())

Then add a transport strategy that describes how to send a token to the user. In this case we're using the `LogTransport` which simply writes the token to the console for testing purposes. It will be registered under the name "log".

pw.SetTransport("log", passwordless.LogTransport{
    MessageFunc: func(token, uid string) string {
        return fmt.Sprintf("Your PIN is %s", token)
    },
}, passwordless.NewCrockfordGenerator(8), 30*time.Minute)

When the user wants to sign in, get a list of valid transports with `passwordless.ListTransports`, and display an appropriate form to the user. You can then send a token to the user:

strategy := r.FormValue("strategy")
recipient := r.FormValue("recipient")
user := Users.Find(recipient)
err := pw.RequestToken(ctx, strategy, user.ID, recipient)

Then prompt the user to enter the token they received:

token := r.FormValue("token")
uid := r.FormValue("uid")
valid, err := pw.VerifyToken(ctx, uid, token)

If `valid` is `true`, the user can be considered authenticated and the login process is complete. At this point, you may want to set a secure session cookie to keep the user logged in.

A complete implementation can be found in the "example" directory.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNoStore            = errors.New("no store has been configured")
	ErrNoTransport        = errors.New("no transports have been configured")
	ErrUnknownStrategy    = errors.New("unknown strategy")
	ErrNotValidForContext = errors.New("strategy not valid for context")
)
View Source
var (
	ErrTokenNotFound = errors.New("the token does not exist")
	ErrTokenNotValid = errors.New("the token is incorrect")
)
View Source
var (
	ErrNoResponseWriter = errors.New("Context passed to CookieStore.Store " +
		"does not contain a ResponseWriter")
	ErrInvalidTokenUID = errors.New("invalid UID in token")
	ErrInvalidTokenPIN = errors.New("invalid PIN in token")
	ErrWrongTokenUID   = errors.New("wrong UID in token")
)

Functions

func RequestToken

func RequestToken(ctx context.Context, s TokenStore, t Strategy, uid, recipient string) error

RequestToken generates, saves and delivers a token to the specified recipient.

func SetContext

func SetContext(ctx context.Context, rw http.ResponseWriter, r *http.Request) context.Context

SetContext returns a Context containing the specified `ResponseWriter` and `Request`. If a nil Context is provided, a new one is returned.

func VerifyToken

func VerifyToken(ctx context.Context, s TokenStore, uid, token string) (bool, error)

VerifyToken checks the given token against the provided token store.

Types

type ByteGenerator

type ByteGenerator struct {
	Bytes  []byte
	Length int
}

ByteGenerator generates random sequences of bytes from the specified set of the specified length.

func NewByteGenerator

func NewByteGenerator(b []byte, l int) *ByteGenerator

NewByteGenerator creates and returns a ByteGenerator.

func (ByteGenerator) Generate

func (g ByteGenerator) Generate(ctx context.Context) (string, error)

Generate returns a string generated from random bytes of the configured set, of the given length. An error may be returned if there is insufficient entropy to generate a result.

func (ByteGenerator) Sanitize

func (g ByteGenerator) Sanitize(ctx context.Context, s string) (string, error)

type ComposerFunc

type ComposerFunc func(ctx context.Context, token, user, recipient string, w io.Writer) error

ComposerFunc is called when writing the contents of an email, including preamble headers.

type CookieStore

type CookieStore struct {
	Path string
	Key  string
	// contains filtered or unexported fields
}

CookieStore stores tokens in a encrypted cookie on the user's browser. This token is then decrypted and checked against the provided value to determine of the token is valid.

func NewCookieStore

func NewCookieStore(signingKey, authKey, encrKey []byte) *CookieStore

NewCookieStore creates a new signed and encrypted CookieStore.

func (*CookieStore) Delete

func (s *CookieStore) Delete(ctx context.Context, uid string) error

Delete deletes the cookie.

This function requires that a ResponseWriter is present in the context.

func (*CookieStore) Exists

func (s *CookieStore) Exists(ctx context.Context, uid string) (bool, time.Time, error)

func (*CookieStore) Store

func (s *CookieStore) Store(ctx context.Context, token, uid string, ttl time.Duration) error

Store encrypts and writes the token to the curent response.

The cookie is set with an expiry equal to that of the token, but the token expiry *must* be validated on receipt.

This function requires that a ResponseWriter is present in the context.

func (*CookieStore) Verify

func (s *CookieStore) Verify(ctx context.Context, pin, uid string) (bool, error)

Verify reads the cookie from the request and verifies it against the provided values, returning true on success.

type CrockfordGenerator

type CrockfordGenerator struct {
	Length int
}

CrockfordGenerator generates random tokens using Douglas Crockford's base 32 alphabet which limits characters of similar appearances. The Sanitize method of this generator will deal with transcribing incorrect characters back to the correct value.

func NewCrockfordGenerator

func NewCrockfordGenerator(l int) *CrockfordGenerator

NewCrockfordGenerator returns a new Crockford token generator that creates tokens of the specified length.

func (CrockfordGenerator) Generate

func (g CrockfordGenerator) Generate(ctx context.Context) (string, error)

func (CrockfordGenerator) Sanitize

func (g CrockfordGenerator) Sanitize(ctx context.Context, s string) (string, error)

Sanitize attempts to translate strings back to the correct Crockford alphabet, in case of user transcribe errors.

type Email

type Email struct {
	Body []struct {
		// contains filtered or unexported fields
	}
	To      string
	Subject string
	Date    time.Time
}

Email is a helper for creating multipart (text and html) emails

func (*Email) AddBody

func (e *Email) AddBody(contentType, body string)

AddBody adds a content section to the email. The `contentType` should be a known type, such as "text/html" or "text/plain". If no `contentType` is provided, "text/plain" is used. Call this method for each required body, with the most preferable type last.

func (Email) Buffer

func (e Email) Buffer() *bytes.Buffer

Buffer generates the email header and contents as a `Buffer`.

func (Email) Bytes

func (e Email) Bytes() []byte

Bytes returns the contents of the email as a series of bytes.

func (Email) Write

func (e Email) Write(w io.Writer) (int64, error)

Write emits the Email to the specified writer.

type LogTransport

type LogTransport struct {
	MessageFunc func(token, uid string) string
}

LogTransport is intended for testing/debugging purposes that simply logs the token to the console.

func (LogTransport) Send

func (lt LogTransport) Send(ctx context.Context, token, user, recipient string) error

type MemStore

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

MemStore is a Store that keeps tokens in memory, expiring them periodically when they expire.

func NewMemStore

func NewMemStore() *MemStore

NewMemStore creates and returns a new `MemStore`

func (*MemStore) Clean

func (s *MemStore) Clean()

Clean removes expired entries from the store.

func (*MemStore) Delete

func (s *MemStore) Delete(ctx context.Context, uid string) error

func (*MemStore) Exists

func (s *MemStore) Exists(ctx context.Context, uid string) (bool, time.Time, error)

func (*MemStore) Release

func (s *MemStore) Release()

Release disposes of the MemStore and any released resources

func (*MemStore) Store

func (s *MemStore) Store(ctx context.Context, token, uid string,
	ttl time.Duration) error

func (*MemStore) Verify

func (s *MemStore) Verify(ctx context.Context, token, uid string) (bool, error)

type PINGenerator

type PINGenerator struct {
	Length int
}

PINGenerator generates numerical PINs of the specifeid length.

func (PINGenerator) Generate

func (g PINGenerator) Generate(ctx context.Context) (string, error)

Generate returns a numerical PIN of the chosen length. If there is not enough random entropy, the returned string will be empty and an error value present.

func (PINGenerator) Sanitize

func (g PINGenerator) Sanitize(ctx context.Context, s string) (string, error)

type Passwordless

type Passwordless struct {
	Strategies map[string]Strategy
	Store      TokenStore
}

Passwordless holds a set of named strategies and an associated token store.

func New

func New(store TokenStore) *Passwordless

New returns a new Passwordless instance with the specified token store. Register strategies against this instance with either `SetStrategy` or `SetTransport`.

func (*Passwordless) GetStrategy

func (p *Passwordless) GetStrategy(ctx context.Context, name string) (Strategy, error)

GetStrategy returns the Strategy of the given name, or nil if one does not exist.

func (*Passwordless) ListStrategies

func (p *Passwordless) ListStrategies(ctx context.Context) map[string]Strategy

ListStrategies returns a list of strategies valid for the context mapped to their names. If you have multiple strategies, call this in order to provide a list of options for the user to pick from.

func (*Passwordless) RequestToken

func (p *Passwordless) RequestToken(ctx context.Context, s, uid, recipient string) error

RequestToken generates and delivers a token to the given user. If the specified strategy is not known or not valid, an error is returned.

func (*Passwordless) SetStrategy

func (p *Passwordless) SetStrategy(name string, s Strategy)

SetStrategy registers the given strategy.

func (*Passwordless) SetTransport

func (p *Passwordless) SetTransport(name string, t Transport, g TokenGenerator, ttl time.Duration) Strategy

SetTransport registers a transport strategy under a specified name. The TTL specifies for how long tokens generated with the provided TokenGenerator are valid. Some delivery mechanisms may require longer TTLs than others depending on the nature/punctuality of the transport.

func (*Passwordless) VerifyToken

func (p *Passwordless) VerifyToken(ctx context.Context, uid, token string) (bool, error)

VerifyToken verifies the provided token is valid.

type RedisStore

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

RedisStore is a Store that keeps tokens in Redis.

func NewRedisStore

func NewRedisStore(client redis.Cmdable) *RedisStore

NewRedisStore creates and returns a new `RedisStore`.

func (RedisStore) Delete

func (s RedisStore) Delete(ctx context.Context, uid string) error

Delete removes a key from the store.

func (RedisStore) Exists

func (s RedisStore) Exists(ctx context.Context, uid string) (bool, time.Time, error)

Exists checks to see if a token exists.

func (RedisStore) Store

func (s RedisStore) Store(ctx context.Context, token, uid string, ttl time.Duration) error

Store a generated token in redis for a user.

func (RedisStore) Verify

func (s RedisStore) Verify(ctx context.Context, token, uid string) (bool, error)

Verify checks to see if a token exists and is valid for a user.

type SMTPTransport

type SMTPTransport struct {
	UseSSL bool
	// contains filtered or unexported fields
}

SMTPTransport delivers a user token via e-mail.

func NewSMTPTransport

func NewSMTPTransport(addr, from string, auth smtp.Auth, c ComposerFunc) *SMTPTransport

NewSMTPTransport returns a new transport capable of sending emails via SMTP. `addr` should be in the form "host:port" of the email server.

func (*SMTPTransport) Send

func (t *SMTPTransport) Send(ctx context.Context, token, uid, recipient string) error

Send sends an email to the email address specified in `recipient`, containing the user token provided.

type SimpleStrategy

type SimpleStrategy struct {
	Transport
	TokenGenerator
	// contains filtered or unexported fields
}

SimpleStrategy is a convenience wrapper combining a Transport, TokenGenerator, and TTL.

func (SimpleStrategy) TTL

TTL returns the time-to-live of tokens generated with this strategy.

func (SimpleStrategy) Valid

Valid always returns true for SimpleStrategy.

type Strategy

type Strategy interface {
	Transport
	TokenGenerator
	// TTL should return the time-to-live of generated tokens.
	TTL(context.Context) time.Duration
	// Valid should return true if this strategy is valid for the provided
	// context.
	Valid(context.Context) bool
}

Strategy defines how to send and what tokens to send to users.

type TokenGenerator

type TokenGenerator interface {
	// Generate should return a token and nil error on success, or an empty
	// string and error on failure.
	Generate(ctx context.Context) (string, error)

	// Sanitize should take a user provided input and sanitize it such that
	// it can be passed to a function that expects the same input as
	// `Generate()`. Useful for cases where the token may be subject to
	// minor transcription errors by a user. (e.g. 0 == O)
	Sanitize(ctx context.Context, s string) (string, error)
}

TokenGenerator defines an interface for generating and sanitising cryptographically-secure tokens.

type TokenStore

type TokenStore interface {
	// Store securely stores the given token with the given expiry time
	Store(ctx context.Context, token, uid string, ttl time.Duration) error
	// Exists returns true if a token is stored for the user. If the expiry
	// time is available this is also returned, otherwise it will be zero
	// and can be tested with `Time.IsZero()`.
	Exists(ctx context.Context, uid string) (bool, time.Time, error)
	// Verify returns true if the given token is valid for the user
	Verify(ctx context.Context, token, uid string) (bool, error)
	// Delete removes the token for the specified  user
	Delete(ctx context.Context, uid string) error
}

TokenStore is a storage mechanism for tokens.

type Transport

type Transport interface {
	// Send instructs the transport to send the given token for the specified
	// user to the given recipient, which could be an email address, phone
	// number, or something else.
	Send(ctx context.Context, token, user, recipient string) error
}

Transport represents a mechanism that sends a named recipient a token.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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