authbyemail

package module
v0.0.0-...-e811a57 Latest Latest
Warning

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

Go to latest
Published: Feb 23, 2023 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

The goal of this project is to provide a web server module for Caddy that allows authenticated access to a website based on e-mail only. That is, no account or password should be necessary for access.

See README.md for general usage information.

Index

Constants

View Source
const MAILDATA_APPROVE = `` /* 469-byte string literal not displayed */

This is an e-mail sent to an administrator when a new user wants to log in. You can replace this page with your own by putting a file called `mail_login.html` in the `auth` subdirectory of your website root.

When supplying your own template, take care to include the fields {{.Admin}}, {{.User}}, {{.SiteName}} and {{.Link}} as shown below. Be mindful of the fact that many e-mail clients block external resources.

View Source
const MAILDATA_LOGIN = `` /* 317-byte string literal not displayed */

This is an e-mail sent to a user that wishes to log in. You can replace this page with your own by putting a file called `mail_login.html` in the `auth` subdirectory of your website root.

When supplying your own template, take care to include the fields {{.User}}, {{.SiteName}} and {{.Link}} as shown below. Be mindful of the fact that many e-mail clients block external resources.

View Source
const PAGEDATA_ACK_APPROVE = `` /* 180-byte string literal not displayed */

This page is shown to an administrator when they approve a user (by filling out the form in the `approve` template. You can replace this page with your own by putting a file called `ack_approve.html` in the `auth` subdirectory of your website root.

View Source
const PAGEDATA_ACK_LOGIN = `` /* 251-byte string literal not displayed */

This page is shown to any non-logged in user when they log in by entering their e-mail address, and are recognised as an existing user. You can replace this page with your own by putting a file called `ack_login.html` in the `auth` subdirectory of your website root.

View Source
const PAGEDATA_ACK_REMOVE = `` /* 146-byte string literal not displayed */

This page is shown to an administrator when they reject or delete a user (by filling out the form in the `approve` template. You can replace this page with your own by putting a file called `ack_remove.html` in the `auth` subdirectory of your website root.

View Source
const PAGEDATA_APPROVE = `` /* 1159-byte string literal not displayed */

This page is shown to a website administrator when they follow a link in an e-mail to approve or reject a new user. You can replace this page with your own by putting a file called `approve.html` in the `auth` subdirectory of your website root.

When supplying your own template, take care to include the fields {{.User}} and {{.EncEmail}} as shown below.

View Source
const PAGEDATA_DELETE = `` /* 347-byte string literal not displayed */

This page is shown to a user when they visit the /auth/delete endpoint with a GET request. It should ask them if they're sure.

View Source
const PAGEDATA_KIOSK = `` /* 831-byte string literal not displayed */

This page is shown to a user when they log in using a link that was created on another device than the one they're on. This may happen if they are e.g. in an internet kiosk, but receive mail on their phone.

The user is asked whether they want to log in the "remote" (kiosk) computer.

When supplying your own template, take care to include the fields {{.Browser}} and {{.Cookie}} as shown below.

View Source
const PAGEDATA_LOGIN = `` /* 411-byte string literal not displayed */

This page is shown to any non-logged in user when they try to access a protected resource. You can replace this page with your own by putting a file called `login.html` in the `auth` subdirectory of your website root.

Variables

View Source
var Templates = map[TemplateID]HtmlTemplate{
	TplLogin: {
		Filename:    "auth/login.html",
		DefaultText: PAGEDATA_LOGIN,
	},
	TplApprove: {
		Filename:    "auth/approve.html",
		DefaultText: PAGEDATA_APPROVE,
	},
	TplKiosk: {
		Filename:    "auth/kiosk.html",
		DefaultText: PAGEDATA_KIOSK,
	},
	TplDelete: {
		Filename:    "auth/delete.html",
		DefaultText: PAGEDATA_DELETE,
	},
	TplAckLogin: {
		Filename:    "auth/ack_login.html",
		DefaultText: PAGEDATA_ACK_LOGIN,
	},
	TplAckApprove: {
		Filename:    "auth/ack_approve.html",
		DefaultText: PAGEDATA_ACK_APPROVE,
	},
	TplAckRemove: {
		Filename:    "auth/ack_remove.html",
		DefaultText: PAGEDATA_ACK_REMOVE,
	},
	TplMailLogin: {
		Filename:    "auth/mail_login.html",
		DefaultText: MAILDATA_LOGIN,
	},
	TplMailApprove: {
		Filename:    "auth/mail_approve.html",
		DefaultText: MAILDATA_APPROVE,
	},
}

This is a mapping from TemplateIDs to HTML templates used in this package.

Functions

func GetBrowserContext

func GetBrowserContext(r *http.Request) string

GetBrowserContext gives a deterministic but human-readable representation of the browser that sent the request, like "Firefox/5.0 (Windows) at 12.13.14.1".

func GetCookie

func GetCookie(r *http.Request) string

GetCookie returns our cookie from this request, if applicable.

func InitializeCrypto

func InitializeCrypto()

InitializeCrypto initializes the global CRYPTO struct with an hmac function and a block cipher. It uses the key in the environment variable AUTH_BY_EMAIL_KEY and panics if that variable is not present or not properly defined.

The keys for the cipher and the hmac function are each derived from the given key using a hmac function with a fixed key.

Types

type AuthByEmailHandler

type AuthByEmailHandler struct {
	Next httpserver.Handler
	// contains filtered or unexported fields
}

func NewHandler

func NewHandler(next httpserver.Handler, config *Config) AuthByEmailHandler

NewHandler initialises the package's various parts and returns the new Handler.

All errors cause a panic, since we can not run if some of the parts don't work. This may happen if it is impossible to initialize the cryptographic functions (for example because the cryptographic key is not present in the environment), if the database can not be initialised (for example because a location for the file was given, but can not be written to), or if the mailer can not be initialised (for example because the SendInBlue API key is not present in the environment).

func (AuthByEmailHandler) ServeHTTP

func (h AuthByEmailHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)

ServeHTTP serves a response in response to an HTTP request. The AuthByEmail handler will check whether the user is sufficiently authorized before passing the request on to the "next" handler, and if not, will instead serve an appropriate authorization interface.

The authentication module exposes a virtual directory /auth/, as in example.com/auth/. In it, the following endpoints exist:

auth/login - can be POSTed to with an email= field. This will initiate a Login check: if the email is approved, send a login email, if not, send email to an admin asking for access.

auth/wait - will wait after "login" in case the user approves the cookie elsewhere.

auth/logout - will log out a logged in user by removing their cookie from the database.

auth/welcome - can be GETted with a token, which if correct, sets a cookie, and forwards the request to the Next handler.

auth/approve - can be GETted with an e-mail, and will produce a form for an admin to decide whether to accept or refuse membership to that user. A POST request to the same endpoint executes that decision.

auth/delete - can be GETed, in which case it will ask for confirmation. A POST request to the same endpoint deletes the logged-in user from the database.

type Config

type Config struct {
	Admins           []*EmailAddr
	WhitelistDomains []string
	FilesystemRoot   string
	Database         string
	UnprotectedPaths []string
	Redirect         string
	CookieValidity   time.Duration
	SiteName         string
	SiteURL          string
	MailerFrom       *EmailAddr
}

The Config type contains parsed configuration information from the Caddyfile.

func NewConfigFromCaddy

func NewConfigFromCaddy(c *caddy.Controller) (*Config, error)

NewConfigFromCaddy parses the caddyfile, and populates a new Config with values found there. It returns an error if these values are erroneous, or if mandatory parameters were not given.

func (*Config) IsDomainWhitelisted

func (c *Config) IsDomainWhitelisted(domain string) bool

The helper function IsDomainWhitelisted checks whether the given domain is whitelisted by checking all members of WhitelistDomains

type CookieToken

type CookieToken struct {
	// UserID corresponding to this token.
	UserID UserID

	// Whether this token has been validated (i.e. the user generating it can access the e-mail address belonging to UserID)
	IsValidated bool

	// Information about the browser to which this cookie was sent, for the user to identify the session later on.
	BrowserContext string
}

A CookieToken contains all the information in a valid cookie

type Crypto

type Crypto struct {
	// contains filtered or unexported fields
}
var CRYPTO *Crypto

func (*Crypto) UserIDfromEmail

func (c *Crypto) UserIDfromEmail(email *EmailAddr) UserID

type Database

type Database interface {
	// GetCookieToken checks if the given string corresponds to a valid cookie
	// and returns the cookie information if so.
	GetCookieToken(cookieText string) *CookieToken

	// GetLinkToken checks if the given string corresponds to a sent email
	// and returns the result. If it does correspond to a valid user, the token is
	// returned
	GetLinkToken(linkText string) *LinkToken

	// IsKnownUser checks whether the UserID is valid
	IsKnownUser(user UserID) bool

	// NewCookieToken makes a fresh cookie token for the given user
	// and saves it to the database.
	NewCookieToken(cookieToken CookieToken) (string, error)

	// ValidateCookieToken sets the Validated property of this cookie to true.
	// If there is no such cookie, an error is returned.
	ValidateCookieToken(cookieText string) error

	// DeleteCookieToken removes a given cookie. If none exists, an error is returned.
	DeleteCookieToken(cookieText string) error

	// NewLinkToken makes a fresh link token for the given user
	// and saves it to the database
	NewLinkToken(linkToken LinkToken, validityPeriod time.Duration) (string, error)

	// AddUser adds the given user to the database
	AddUser(user UserID)

	// DelUser removes a user from the database
	DelUser(user UserID) error
}

type DiskBackedDatabase

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

A DiskBackedDatabase is an interface to an SQL database, which is disk-backed and therefore survives restarts of the webserver. This is the "canonical" database to use; the alternative (MapBasedDatabase) is used when no path to a database file is given and is intended for debugging or trial usage.

func NewDiskBackedDatabase

func NewDiskBackedDatabase(config *Config, logger *log.Logger) *DiskBackedDatabase

NewDiskBackedDatabase opens or creates the database file, and sets up the database struct that interacts with it. If the database file does not exist, one is created. If creating the database file is not possible, this function panics.

func (*DiskBackedDatabase) AddUser

func (d *DiskBackedDatabase) AddUser(user UserID)

AddUser adds the given user to the database

func (*DiskBackedDatabase) DelUser

func (d *DiskBackedDatabase) DelUser(user UserID) error

DelUser removes a user from the database. Tokens corresponding to a non-existent user are invalid; if you re-add a user, tokens that were valid before deletion will become valid once more.

func (*DiskBackedDatabase) DeleteCookieToken

func (d *DiskBackedDatabase) DeleteCookieToken(cookieToken string) error

DeleteCookieToken validates a cookie matching the given token

func (*DiskBackedDatabase) GetCookieToken

func (d *DiskBackedDatabase) GetCookieToken(cookieText string) *CookieToken

GetCookieContents returns a given cookie if it exists and has not expired, nil otherwise.

func (*DiskBackedDatabase) GetLinkToken

func (d *DiskBackedDatabase) GetLinkToken(linkText string) *LinkToken

GetLinkToken checks if the given string corresponds to a sent email and returns the result. If it does correspond to a valid user, that user's ID and the parsed link token are returned as well.

func (*DiskBackedDatabase) IsKnownUser

func (d *DiskBackedDatabase) IsKnownUser(user UserID) bool

IsKnownUser checks whether the UserID is valid

func (*DiskBackedDatabase) NewCookieToken

func (d *DiskBackedDatabase) NewCookieToken(cookieToken CookieToken) (string, error)

NewCookieToken makes a fresh cookie token for the given user

func (*DiskBackedDatabase) NewLinkToken

func (d *DiskBackedDatabase) NewLinkToken(linkToken LinkToken, validityPeriod time.Duration) (string, error)

NewLinkToken makes a fresh link token for the given user

func (*DiskBackedDatabase) ValidateCookieToken

func (d *DiskBackedDatabase) ValidateCookieToken(cookieToken string) error

ValidateCookieToken validates a cookie matching the given token

type EmailAddr

type EmailAddr struct {
	User   string
	Domain string
}

func NewEmailAddrFromString

func NewEmailAddrFromString(e string) (*EmailAddr, error)

NewEmailAddrFromString parses a string to find an e-mail address.

func (*EmailAddr) LocalPartIsASCII

func (e *EmailAddr) LocalPartIsASCII() bool

func (*EmailAddr) String

func (e *EmailAddr) String() string

type EmailMessage

type EmailMessage struct {
	ReplyTo *EmailAddr
	To      *EmailAddr
	Subject string
	Body    string
}

EmailMessage represents a message sent by this mailer. There is no From address, since that is forced by SendInBlue to be the globally configured From address. Instead, we provide a setting for the Reply To address.

type HtmlTemplate

type HtmlTemplate struct {
	Filename    string
	DefaultText string
}

HtmlTemplate is the pair of custom and default file contents for each template

type LinkToken

type LinkToken struct {
	// UserID corresponding to this token.
	UserID UserID

	// Cookie token set on the browser from which this link was sent.
	CorrespondingCookie string
}

A LinkToken contains all the information in a valid e-mail link

type LogMailer

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

The LogMailer is a dummy mailer that does not send mail. but instead prints messages to the log.

func (*LogMailer) DecryptEmail

func (m *LogMailer) DecryptEmail(encryptedEmail string) (*EmailAddr, error)

func (*LogMailer) SendAdminLoginRequest

func (m *LogMailer) SendAdminLoginRequest(email *EmailAddr) error
func (m *LogMailer) SendLoginLink(email *EmailAddr, token string) error

type Mailer

type Mailer interface {
	// SendLoginLink sends a user an email with a login link using the given token
	SendLoginLink(email *EmailAddr, token string) error

	// SendAdminLoginRequest sends a user an email with an approval link for the given user
	SendAdminLoginRequest(email *EmailAddr) error

	// DecryptEmail decrypts an e-mail address that was given in an admin approval link
	DecryptEmail(encryptedEmail string) (*EmailAddr, error)
}

type MailerInternal

type MailerInternal interface {
	// Initialise the mailer (read config etc). Panics on error (e.g. no api-key in environment)
	Initialise(config *Config, logger *log.Logger)

	// Send a mail message
	SendMail(msg *EmailMessage) error
}

type MapBasedDatabase

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

A database type for AuthByEmail that naively implements all functions of Database using maps. Not very fast or good, but useful as a first implementation, and as a guide for designing more mature databases.

See DiskBasedDatabase for function-level documentation.

func NewMapBasedDatabase

func NewMapBasedDatabase() *MapBasedDatabase

func (*MapBasedDatabase) AddUser

func (m *MapBasedDatabase) AddUser(user UserID)

AddUser adds the given user to the database

func (*MapBasedDatabase) DelUser

func (m *MapBasedDatabase) DelUser(user UserID) error

DelUser removes a user from the database and invalidates all corresponding tokens

func (*MapBasedDatabase) DeleteCookieToken

func (m *MapBasedDatabase) DeleteCookieToken(cookieText string) error

DeleteCookieToken validates a cookie matching the given token

func (*MapBasedDatabase) GetCookieToken

func (m *MapBasedDatabase) GetCookieToken(cookieText string) *CookieToken

func (*MapBasedDatabase) GetLinkToken

func (m *MapBasedDatabase) GetLinkToken(linkText string) *LinkToken

func (*MapBasedDatabase) IsKnownUser

func (m *MapBasedDatabase) IsKnownUser(user UserID) bool

IsKnownUser checks whether the UserID is valid

func (*MapBasedDatabase) NewCookieToken

func (m *MapBasedDatabase) NewCookieToken(cookieToken CookieToken) (string, error)

NewCookieToken makes a fresh cookie token for the given user and saves it to the database

func (*MapBasedDatabase) NewLinkToken

func (m *MapBasedDatabase) NewLinkToken(linkToken LinkToken, validityPeriod time.Duration) (string, error)

NewLinkToken makes a fresh link token for the given user and saves it to the database

func (*MapBasedDatabase) ValidateCookieToken

func (m *MapBasedDatabase) ValidateCookieToken(cookieText string) error

ValidateCookieToken validates a cookie matching the given token

type RealMailer

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

func NewRealMailer

func NewRealMailer(config *Config, logger *log.Logger) *RealMailer

NewRealMailer returns a mailer with the given configuration. In this case, we use the SendInBlue implementation.

func (*RealMailer) DecryptEmail

func (m *RealMailer) DecryptEmail(encryptedEmail string) (*EmailAddr, error)

DecryptEmail decrypts an e-mail address encrypted by encryptEmail. These are sent in the admin approval e-mails.

func (*RealMailer) SendAdminLoginRequest

func (m *RealMailer) SendAdminLoginRequest(email *EmailAddr) error

SendAdminLoginRequest sends an approve/reject link for the given user to their admin.

func (m *RealMailer) SendLoginLink(email *EmailAddr, token string) error

SendLoginLink sends a login link with the given token to a user. The admin is given as the reply-to address.

type SendInBlueMailer

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

func (*SendInBlueMailer) Initialise

func (m *SendInBlueMailer) Initialise(config *Config, logger *log.Logger)

NewSendInBlueMailer returns a new mailer with the given configuration. It reads an API key from the environment and will panic if it is not there, so make sure to set SENDINBLUE_API_KEY.

func (*SendInBlueMailer) SendMail

func (m *SendInBlueMailer) SendMail(msg *EmailMessage) error

sendMail sends an e-mail message using the SendInBlue API. Normally we use the custom Send___ methods instead.

type TemplateID

type TemplateID uint
const (
	TplLogin TemplateID = iota
	TplApprove
	TplKiosk
	TplDelete
	TplAckLogin
	TplAckApprove
	TplAckRemove
	TplMailLogin
	TplMailApprove
)

This is an enum listing the possible HTML templates used in this package. They are associated with a filename and default data in the Templates map.

type UserID

type UserID string

UserID is a keyed hash of an email address

Jump to

Keyboard shortcuts

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