tinyauth

package module
v0.0.0-...-2ac4774 Latest Latest
Warning

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

Go to latest
Published: Nov 30, 2023 License: MIT Imports: 13 Imported by: 0

README

tinyauth (Tiny Token Auth)

import (
    // ...
    "github.com/bitdabbler/tinyauth"
)

Overview

tinyauth is intended to provide a simple, minimalist, token-based authentication.

With token-based authentication, there is a fundamental tradeoff between how much we trust our users and how much load we put on our datastore. If we have high trust, we can reduce the load on our datastore by authenticating the user with state stored in a cryptographically secure session token. But, the longer the trust window is, the longer a session will remain valid even after we "inactivate" a user in our database. At the extreme where we have zero trust, we can check the datastore to re-verify the user on every request, making it similar to simply storing sessions in the datastore, in terms of how many times we call the datastore.

tinyauth allows you to manage this trade off using 3 levers:

  • MaxTrustsSecs - the longest period between database checks
  • MaxStaleSecs - the longest period a token can remain valid without being actively used
  • MaxTokenSecs - the longest period a session can remain valid regardless of activity

MaxTrustSecs can be kept short, for high security without noticeably affecting users. The default is 10 minutes. At the end of MaxTrustSecs, tinyauth will check the datastore to ensure the user is still valid, then refresh the token transparently, all without bothering the user. That transparent refresh happens as long as (a) the user has been active within the last MaxStaleSecs, and (b) the last time the user logged in was within the last MaxTokenSecs.

If you use tinyauth middleware, it handles all of the token maintenance. The middleware, as well as the stock login and logout handlers, follow the standard library APIs for request handling. They will work directly with the standard library and all frameworks with compatible APIs, like Chi and Goji.

If you want to write different middleware for other routing frameworks, it should be straightforward to integrate tiny.Guard as the auth state coordinator.

Usage

To create a tinyauth.Guard, which will work as the auth state coordinator for your middleware, we need 3 things:

  1. a pointer to any custom user type (e.g. *Employee), which only needs to implement the single method Authable interface:
    • GetID() - returns the user's unique identifier
  2. a *tinycrypto.Keyset, to manage encryption transparently, including key rotation
  3. a tinyauth.Repo implemention, used for state persistence:
    • GetAuthable() - fetch a user by ID
    • BlacklistSession() - register a sessionID in the blacklist table, to prevent auto-refreshing a dead session after logout
    • CheckSessionBlacklist() - check if a sessionID is in the blacklist

To create a tinyauth.Guard:

guard := tinyauth.Guard(app.authKeyset, app.db, new(Employee))
router.Use(guard.Middleware)

Limitations / Design Decisions

tinyauth does not use JWTs, or in this case, JWEs. We expose only the APIs required for our narrow design goals, keeping things secure but minimalist. We avoid the risk of using unsafe algorithms, and the overhead of unneeded default fields in JWT (and Paseto). There are many unsupported use cases, like those that need tokens the client can inspect but not modify (i.e. signed, but not encrypted).

Documentation

Overview

Package tinyauth provides super simple token-based authentication tools that are compatible with the standard library HTTP handler APIs. It uses authenticated encryption by default, to ensure the authenticity, privacy, and integrity of the token content. tinyauth was designed to keep the API minimal while still being easy to integrate with existing services and user models.

Index

Constants

View Source
const (
	Second = int64(1)
	Minute = Second * 60
	Hour   = Minute * 60
)

Time constants for convenience.

Variables

View Source
var LoginErrorHandler = func(w http.ResponseWriter, r *http.Request, err *Error) {
	log.Println(err.Error())
	switch err.ErrType {
	case ErrBadInput:
		http.Error(w, err.Msg, http.StatusBadRequest)
	case ErrInternal:
		http.Error(w, err.Msg, http.StatusInternalServerError)
	case ErrAuthFailed:
		http.Error(w, err.Msg, http.StatusUnauthorized)
	}
}

LoginErrorHandler is called if the user cannot be authenticated. It is exposed so that users can replace it with custom logic if desired.

View Source
var LoginSuccessHandler = func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("login successful"))
}

LoginSuccessHandler returns status 200 with text "login successful". It is exposed so that users can replace it with custom logic if desired.

View Source
var LogoutErrorHandler = func(w http.ResponseWriter, r *http.Request, err *Error) {
	http.Error(w, err.Msg, http.StatusInternalServerError)
}

LogoutErrorHandler is called if the logout fails for any reason. It is exposed so that users can replace it with custom logic if desired.

View Source
var LogoutSuccessHandler = func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("session terminated"))
}

LogoutSuccessHandler returns status 200 with text "session terminated". It is exposed so that users can replace it with custom logic if desired.

View Source
var MiddlewareErrorHandler = func(w http.ResponseWriter, r *http.Request, err *Error) {
	log.Printf("auth failed: %v", err)
	http.Error(w, "authentication failed", http.StatusUnauthorized)
}

MiddlewareErrorHandler processes errors thrown by the middleware. It is exposed so that users of the library can replace it to provide their own custom error handling.

Functions

func CheckPassword

func CheckPassword(hash, password []byte) error

CheckPassword re-exports the hash/password authentication utility used.

func HashPassword

func HashPassword(password []byte) (hash []byte, err error)

HashPassword re-exports the password hash utility used (currently acrypt).

Types

type Authable

type Authable interface {
	GetID() (id string)
}

Authable represents a user.

func ExtractUser

func ExtractUser(r *http.Request) Authable

ExtractUser returns the Authable from the token that is carried in the context of the request.

Ex.

user, ok := tinyauth.ExtractUser(r).(*User)
if !ok {
    log.Warn("failed to find *User in request context")
}

type ErrType

type ErrType int

ErrType categorizes the Err stored in tinyauth.Error.

const (
	// ErrBadInput indicates that input was invalid.
	ErrBadInput ErrType = iota

	// ErrAuthFailed indicates that the authentication failed.
	ErrAuthFailed

	// ErrInternal indicates that an error was neither recognized as bad input
	// nor as explicit auth failure (for example, a data store was unreachable).
	ErrInternal
)

type Error

type Error struct {
	Err error
	Msg string
	ErrType
}

Error is an augmenting wrapper error type.

func NewErrorAuthFailed

func NewErrorAuthFailed(msg string, err error) *Error

NewErrorBadInput constructs an Error with ErrType ErrAuthFailed.

func NewErrorBadInput

func NewErrorBadInput(msg string, err error) *Error

NewErrorBadInput constructs an Error with ErrType ErrBadInput.

func NewErrorInternal

func NewErrorInternal(msg string, err error) *Error

NewErrorBadInput constructs an Error with ErrType ErrInternal.

func (*Error) Error

func (l *Error) Error() string

Error returns the string representation of the tinyauth.Error.

func (*Error) Unwrap

func (l *Error) Unwrap() error

Unwrap returns the wrapped error inside the tinyauth.Error.

type Guard

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

Guard holds the state used for authentication. Auth middleware are therefore defined as methods on the Guard.

func CustomGuard

func CustomGuard(cfg TokenConfig, keyset *tinycrypto.Keyset, db Repo, userPrototypePtr Authable) *Guard

CustomGuard creates an authenticator with a custom configuration.

func NewGuard

func NewGuard(keyset *tinycrypto.Keyset, db Repo, userPrototypePtr Authable) *Guard

NewGuard creates an authenticator using the default configuration.

func (*Guard) LoginHandler

func (g *Guard) LoginHandler() http.Handler

LoginHandler authenticates a user. If successful, it puts the auth token into the Authorization header and then calls the LoginSuccessHandler. If the user cannot be authenticated, it calls the LoginErrorHandler. The inbound POST request must contain: `{"user_id": "alice", password:"secret"}`.

func (*Guard) LogoutHandler

func (g *Guard) LogoutHandler() http.Handler

LogoutHandler returns an http.Handler that is used to add the session to a blacklist of cancelled sessions, to prevent further auto-refresh. If success- ful, it calls the LogoutSuccessHandler, otherwise it calls the LogoutErrorHandler.

func (*Guard) Middleware

func (g *Guard) Middleware(next http.Handler) http.Handler

Middleware provides token-based auth protection for routes. If it incurs an error, it calls the MiddlewareErrorHandler to process it. This middleware is compatible with the standard library API.

func (*Guard) RequestWithUpdatedAuthable

func (g *Guard) RequestWithUpdatedAuthable(w http.ResponseWriter, r *http.Request, a Authable) (*http.Request, error)

RequestWithUpdatedAuthable takes an updated Authable, regenerates the auth token, injects it into the response header, and returns a new request with the updated Authable value in context. Clients can use this to persist property changes for the current user into the auth token.

type Repo

type Repo interface {

	// GetAuthable returns the user with the given ID, IF valid. If the password
	// hash is required, it is returned separately and will immediately be
	// discarded after the user is authenticated.
	GetAuthable(id string, includePasswordHash bool) (user Authable, hash []byte, err error)

	// BlacklistSession registers a session as dead, so that it cannot be used
	// to auto-refresh an auth token. The repo is also provided with a timestamp
	// indicating when it will become safe to prune the session.
	BlacklistSession(sessionID string, until time.Time) error

	// CheckSessionBlacklist returns nil if the sessionID is NOT present in the
	// dead session blacklist.
	CheckSessionBlacklist(sessionID string) error
}

Repo defines the data persistence API for auth-related entities.

type TokenConfig

type TokenConfig struct {

	// Longest period between db checks.
	MaxTrustSecs int64

	// Longest inactive period between logins.
	MaxStaleSecs int64

	// Longest period between logins.
	MaxTokenSecs int64
}

TokenConfig defines the behavior of the auth token.

Jump to

Keyboard shortcuts

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