authentication

package
v13.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: AGPL-3.0 Imports: 1 Imported by: 0

Documentation

Overview

Package authentication holds the password engine and names the boundary the sign-in flow sits on the other side of.

Authenticator hashes a password and compares one against a stored hash, and github.com/primandproper/platform-go/v13/authentication/argon2 is the implementation this module recommends and the only one it ships. Every other piece a sign-in touches is a package beside this one: github.com/primandproper/platform-go/v13/authentication/totp verifies the second factor, github.com/primandproper/platform-go/v13/authentication/webauthn is the relying party a passkey answers to, github.com/primandproper/platform-go/v13/authentication/passwordreset holds the token for somebody who can present neither, github.com/primandproper/platform-go/v13/authentication/tokens mints the bearer credential a proven identity is exchanged for, github.com/primandproper/platform-go/v13/authentication/oauth2server is the authorization server, github.com/primandproper/platform-go/v13/identity's SignInReader is the read a submitted handle resolves through, and github.com/primandproper/platform-go/v13/sessions is what a proven identity becomes.

What no package here ships is the function that calls them in order. That is a decision rather than a gap, and this is the ruling.

The flow is the consumer's

The case for shipping a login manager is the case that produced passwordreset: security-critical boilerplate, short enough to look like it needs no library and dangerous enough that its mistakes are vulnerabilities rather than bugs. Four of them are named below, and every one of them can be got wrong twice.

The case against is what a manager would have to take to encode those four. It needs a directory reader, a password engine, a set of second factors and a policy deciding which of them this user must present now, a session store, a principal assembler, an event sink, a refusal vocabulary, and a limiter with a key function. That is eight seams to hold four conditionals, and each of the eight is somewhere a consumer can wire it wrong in a way that reintroduces exactly what the manager existed to prevent. A WithMFAPolicy hook that answers "not required" for a user holding a verified secret is the second-factor bypass, written in the manager's own vocabulary and now harder to see.

The difference from passwordreset is what the guarantee is made of. What that package ships is a table and three statements: a digest in the column, single use as the affected-row count of a guarded UPDATE inside one transaction, an expiry refused on read. Those hold however the caller sequences its code, and a consumer who wires them wrong gets an error rather than a silent bypass, because the correctness is inside the statement. The four mistakes below are properties of control flow — of what runs before what, and of what is minted before which check — and a seam cannot hold one of those on a caller's behalf. It can only offer to run the caller's code in the right order, which is the thing the caller was going to write anyway.

So: orchestration is the consumer's. What this module owes instead is engines that fail safe on their own, and this document, which names the mistakes and points at an example that makes them executable.

This is a ruling and not a law. What would overturn it is evidence rather than appetite: two consumers whose flows differ only in the event vocabulary and the principal's shape are a shape written twice rather than a policy written twice, and that is the case a manager would be built from. Example_loginFlow is what it would have to be specified against.

The order, and what each step's mistake costs

Rate-limit before reading anything. github.com/primandproper/platform-go/v13/ratelimiting is what this module ships for that, and it is a limiter rather than a lockout: nothing here counts a user's failures or freezes an account after N of them. That is left out because a per-account lockout is a denial of service anybody can aim at somebody else by guessing their handle badly on purpose, and whether to accept that — or to key the budget on the source, or on both — is a policy with no single right answer. Pick the key deliberately; there is no default that is safe everywhere.

Read the user, and spend a comparison whether or not there was one. A handle that resolves to nothing and a handle that resolves to a user with the wrong password have to cost the same and say the same. Returning early on the miss makes the response time the answer: an argon2id verification at 64 MiB is the most expensive thing on this path, and skipping it leaves a gap nobody needs statistics to read. Compare against a fixed decoy hash minted at startup, and refuse both with one error.

Check the password before the status. github.com/primandproper/platform-go/v13/identity.AccountStatus.AdmitsSignIn is the rule for who may authenticate, and it is asked after the comparison, never before. A ban tested first tells anyone who can guess a username that the account exists and that its owner is suspended — two facts about somebody else, handed out for free. Tested afterwards, learning them costs the password, and whoever paid it is the account's owner, for whom the explanation on the status was written.

Gate the second factor, and mint nothing before it. A password that verified is not a sign-in. What says a user holds a second factor is github.com/primandproper/platform-go/v13/identity.User.TwoFactorSecretVerifiedAt rather than a non-empty secret — a secret issued and never proven is a QR code somebody may have closed — and when it is set and no code arrived, the answer is that a code is required and nothing else: no session, no token, no cookie, no value the client can present next time in place of the code. The bypass this forbids is rarely a missing check. It is a check that runs after something was already minted "for the second step". Whatever carries the request from the first step to the second is itself a credential, and wants the treatment one gets: short-lived, single-use, and bound to the account it was minted for.

The passkey branch is shorter, and it drops exactly two of these. There is no password, so there is no decoy comparison and no second factor to gate — the assertion is both. Everything else survives. github.com/primandproper/platform-go/v13/authentication/webauthn.RelyingParty.BeginLogin names a user and therefore answers with that user's credential IDs, which tells whoever asked that the handle exists and how many keys are on it; BeginDiscoverableLogin names nobody, and it is what a sign-in page open to the world should call. The status check still lands after the assertion verifies, for the reason it lands after the comparison. The identifier is still fresh. And FinishLogin hands back a credential carrying the authenticator's sign count, which is evidence of a cloned key only if the last one was written back — a step with no analog on the password path and no default that supplies it.

Establish a new session identifier, every time. github.com/primandproper/platform-go/v13/sessions.Store.NewFor mints one and records who holds it; anything the client was carrying before must stop resolving. sessions' documentation has the long form under "Renewal is not optional" — an identifier planted in a victim's browser before sign-in and still valid after it is session fixation, and it is a defect in the flow rather than in the cookie.

Record the outcome, refusals included. The event vocabulary is the consumer's — github.com/primandproper/platform-go/v13/audit for the tamper-evident trail, github.com/primandproper/platform-go/v13/eventstream for what other services react to — but the shape of the mistake is not: a sign-in recorded before the session exists records sign-ins that did not happen, and a refusal that records nothing is how a stuffing run stays invisible. A password that worked followed by a code that did not is the most interesting event this flow produces, which is why totp's verifier already records its own rejections.

Mismatch is (false, nil), and the sentinel is yours

Authenticator.PasswordMatches reports a wrong password as (false, nil) and populates err only when the comparison could not be performed — a malformed stored hash, a runtime failure. That is the shape github.com/primandproper/platform-go/v13/ratelimiting.RateLimiter uses for a refusal, for the same reason: the caller is deciding what to do next rather than propagating a failure, and a refusal delivered as an error is one that gets logged at error level, alerted on, and retried.

The module ships no mismatch sentinel and recommends the consumer's own, because of what that sentinel has to cover. A flow hands its transport one error meaning "these credentials sign nobody in", and that one error has to answer for the unknown handle, the wrong password, and the wrong second-factor code alike — three refusals from three packages, of which a sentinel declared here could speak for exactly one. Owning it here would make the other two the ones a caller forgot to translate. Declare it beside the flow, map it in github.com/primandproper/platform-go/v13/errors/http, and convert where the boolean is read:

matched, err := authenticator.PasswordMatches(ctx, user.HashedPassword, password)
if err != nil {
	return nil, err // the stored hash is broken; that is not a wrong password
}

if !matched {
	return nil, errBadCredentials // yours, and it covers the other refusals too
}

A caller that treats any error as a failed sign-in is correct. One that treats a failed sign-in as an error is not.

The principal a session carries is opaque, and that is a boundary

github.com/primandproper/platform-go/v13/sessions.Holder's principal is a string, and Store is generic over whatever the application puts in the record. Neither knows what a user is. identity has a Principal of its own — the user, their memberships, and the account the request is against — and it is deliberately not what sessions stores.

A consumer's principal is usually fatter still: memberships plus a permission map resolved from roles, which is a snapshot of an authorization decision. Storing that in the session record makes the session a cache of policy, and a role revoked while somebody is signed in does not take effect until they sign in again. Storing only the user ID and calling identity's GetPrincipal per request costs four statements on every authenticated request, which that method's documentation says out loud so the trade can be made with the number in hand. Neither answer is wrong, which is precisely why the record's shape belongs to the consumer. What this module will not do is settle it by shipping a principal type that sessions stores and authorization reads.

One constraint the shape does have to respect: sessions.Record carries a version, and a record written under a different shape reads as absent rather than being decoded into the current one. Widening a principal is a wave of re-logins, so it is worth deciding once whether the permissions go in.

Impersonation is not a platform notion

Nothing in this module has one, and that is worth saying plainly because the absence is easy to paper over. A support engineer acting as a user is two identities — the actor and the subject — and every layer beneath this one has room for exactly one: a session holder is one principal, identity's Principal is one user with their memberships, authorization resolves one subject's permissions, and an github.com/primandproper/platform-go/v13/audit.Entry has one Actor.

The papering-over is to put the subject's ID where the actor's belongs. That produces a system which works and an audit trail which says the user did it, discovered during the incident it was supposed to explain. A shape that does not lie carries both — the actor in the session's own record, the subject as a field the flow set explicitly — and every read answering "who is this" has to say which of the two it means. audit's Entry can carry the second in its Metadata today, which is a place to put it rather than a model of it.

Making it a platform notion means a second principal on the session record, a second subject through authorization, and an actor/subject pair in audit: a change to three packages rather than a helper in this one. It stays out until a consumer needs it enough to specify it, and a consumer that needs it now should model it explicitly rather than by substitution.

The worked example

Example_loginFlow is the order above, executable: an identity store over SQLite, argon2, totp, and a session store, wired into the function this package declines to ship. It is a test rather than prose so that what it demonstrates is checked — the enumeration parity, the status check landing after the comparison, the required-code answer minting nothing, and the identifier changing across a re-login are assertions in the file beside it.

Example (LoginFlow)

Example_loginFlow is the sign-in this module declines to ship, written the way the package documentation says to write it.

Everything it calls is a package beside this one. What is written here — the order, the decoy comparison, the single refusal, the second-factor gate, the fresh identifier — is the part the package documentation rules is the consumer's, and the part a second copy can get wrong.

package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/primandproper/platform-go/v13/authentication"
	"github.com/primandproper/platform-go/v13/authentication/argon2"
	"github.com/primandproper/platform-go/v13/authentication/totp"
	"github.com/primandproper/platform-go/v13/cache/memory"
	"github.com/primandproper/platform-go/v13/database"
	"github.com/primandproper/platform-go/v13/database/dialect"
	"github.com/primandproper/platform-go/v13/database/sqlite"
	"github.com/primandproper/platform-go/v13/identity"
	"github.com/primandproper/platform-go/v13/identity/migrations"
	"github.com/primandproper/platform-go/v13/sessions"
	sessionscache "github.com/primandproper/platform-go/v13/sessions/cache"
	"github.com/primandproper/platform-go/v13/tenancy"

	otp "github.com/pquerna/otp/totp"
)

// Example_loginFlow is the sign-in this module declines to ship, written the way
// the package documentation says to write it.
//
// Everything it calls is a package beside this one. What is written here — the
// order, the decoy comparison, the single refusal, the second-factor gate, the
// fresh identifier — is the part the package documentation rules is the
// consumer's, and the part a second copy can get wrong.
func main() {
	ctx := context.Background()
	flow, scope := exampleFlow()

	// Ada holds a password and a proven TOTP secret.
	user := exampleUser(ctx, flow, "ada", "correct horse battery staple", exampleTOTPSecret)

	// A handle nobody registered, and a registered handle with the wrong
	// password, are the same answer at the same cost.
	_, err := flow.SignIn(ctx, scope, &signInRequest{Handle: "nobody", Password: "hunter2"})
	fmt.Println("unknown handle:", errors.Is(err, errBadCredentials))

	_, err = flow.SignIn(ctx, scope, &signInRequest{Handle: "ada", Password: "hunter2"})
	fmt.Println("wrong password:", errors.Is(err, errBadCredentials))

	// The right password and no code is not a sign-in. What comes back says a
	// code is required and carries nothing a client could present instead of
	// one.
	outcome, err := flow.SignIn(ctx, scope, &signInRequest{
		Handle:   "ada",
		Password: "correct horse battery staple",
	})
	if err != nil {
		panic(err)
	}

	fmt.Println("code required:", outcome.SecondFactorRequired, "- session minted:", outcome.Session != nil)

	// With the code, a session — established under a fresh identifier, so
	// whatever the browser was carrying before signing in is now worthless.
	priorSessionID := exampleAnonymousSession(ctx, flow)

	outcome, err = flow.SignIn(ctx, scope, &signInRequest{
		Handle:         "ada",
		Password:       "correct horse battery staple",
		Code:           exampleCode(),
		PriorSessionID: priorSessionID,
	})
	if err != nil {
		panic(err)
	}

	fmt.Println("signed in:", outcome.Session.Data.UserID == user.ID)
	fmt.Println("identifier rotated:", outcome.Session.ID != priorSessionID)

	_, err = flow.sessions.Get(ctx, priorSessionID)
	fmt.Println("pre-sign-in session dead:", errors.Is(err, sessions.ErrNotFound))

	// A ban is answered after the password has been proven, so the explanation
	// reaches the person it was written for and nobody else can ask for it.
	exampleBan(ctx, flow, scope, user.ID, "chargebacks")

	_, err = flow.SignIn(ctx, scope, &signInRequest{Handle: "ada", Password: "hunter2"})
	fmt.Println("banned, wrong password:", errors.Is(err, errBadCredentials))

	_, err = flow.SignIn(ctx, scope, &signInRequest{
		Handle:   "ada",
		Password: "correct horse battery staple",
	})

	refusal := new(accountStatusError)
	if errors.As(err, &refusal) {
		fmt.Println("banned, right password:", refusal.Explanation)
	}

}

// principal is what this application's sessions carry. The user ID and nothing
// else: see the package documentation on why the permission map is a decision
// rather than a default.
type principal struct {
	UserID string
}

type (
	// signInRequest is what a sign-in handler has after parsing its form.
	signInRequest struct {
		Handle   string
		Password string
		// Code is the second factor, empty on the first of the two submissions
		// a user with one makes.
		Code string
		// PriorSessionID is whatever identifier the client arrived carrying,
		// which is not evidence of anything and is destroyed rather than reused.
		PriorSessionID string
		Metadata       sessions.Metadata
	}

	// signInOutcome is what the flow reports. Exactly one of its two states is
	// ever populated: a session, or the fact that a code is still owed.
	signInOutcome struct {
		Session              *sessions.Session[principal]
		SecondFactorRequired bool
	}
)

// errBadCredentials is the one refusal three packages' failures collapse into:
// an unknown handle, a wrong password, and a wrong code. It is declared here
// rather than in the module because it has to cover all three — see the package
// documentation.
var errBadCredentials = errors.New("those credentials do not sign anyone in")

// accountStatusError is the answer for somebody who proved their password and
// still may not sign in. It carries the operator's explanation, which is written
// to be read by the account's owner and by nobody who has not proven they are.
type accountStatusError struct {
	Status      identity.AccountStatus
	Explanation string
}

func (e *accountStatusError) Error() string {
	return fmt.Sprintf("account is %s: %s", e.Status, e.Explanation)
}

// loginFlow is the orchestration. Every field is a seam this module ships and
// none of the logic between them is.
type loginFlow struct {
	directory    identity.SignInReader
	passwords    authentication.Authenticator
	secondFactor totp.Verifier
	sessions     sessions.Store[principal]

	// decoyHash is compared against when the handle resolved to nobody, so the
	// miss costs an argon2id verification like every other answer does.
	decoyHash string
}

// exampleDeployment is the flow plus the scaffolding around it: the write side
// of the identity store, which a sign-in has no business holding, and which the
// example needs to arrange somebody to sign in as.
type exampleDeployment struct {
	*loginFlow

	client database.Client
	store  identity.Store
}

// SignIn is the order the package documentation names, and the reason each step
// is where it is.
func (f *loginFlow) SignIn(ctx context.Context, scope tenancy.Scope, req *signInRequest) (*signInOutcome, error) {
	// A limiter belongs here, keyed on whatever the deployment decided to spend
	// its budget on. ratelimiting.RateLimiter is the seam; the key is the
	// policy, and this module has no opinion about it.

	user, err := f.directory.GetUserByUsername(ctx, scope, req.Handle)
	switch {
	case errors.Is(err, identity.ErrUserNotFound):
		// Spend the comparison anyway. Returning here would make the response
		// time say whether the handle exists.
		if _, decoyErr := f.passwords.PasswordMatches(ctx, f.decoyHash, req.Password); decoyErr != nil {
			return nil, decoyErr
		}

		return nil, errBadCredentials
	case err != nil:
		return nil, err
	}

	// The password, before anything is said about the account behind it.
	matched, err := f.passwords.PasswordMatches(ctx, user.HashedPassword, req.Password)
	if err != nil {
		// A stored hash that will not parse is an operational failure, not a
		// wrong password, and collapsing it into the refusal would hide it.
		return nil, err
	}

	if !matched {
		return nil, errBadCredentials
	}

	// Only now: whether this user may sign in at all.
	if !user.AccountStatus.AdmitsSignIn() {
		return nil, &accountStatusError{Status: user.AccountStatus, Explanation: user.AccountStatusExplanation}
	}

	// A verified secret, not a non-empty one — an unproven secret is a QR code
	// somebody may have closed, and treating it as a factor locks them out.
	if user.TwoFactorSecretVerifiedAt != nil {
		if req.Code == "" {
			// Nothing is minted here. Whatever carries this user to their second
			// submission is a credential of its own, and this application asks
			// them to submit the password again rather than hold one.
			return &signInOutcome{SecondFactorRequired: true}, nil
		}

		if err = f.secondFactor.Verify(ctx, user.TwoFactorSecret, req.Code); err != nil {
			return nil, errBadCredentials
		}
	}

	// Whatever identifier the client arrived with stops resolving, so an
	// identifier planted before sign-in is not one afterwards.
	if req.PriorSessionID != "" {
		if err = f.sessions.Delete(ctx, req.PriorSessionID); err != nil && !errors.Is(err, sessions.ErrNotFound) {
			return nil, err
		}
	}

	session, err := f.sessions.NewFor(ctx,
		sessions.Holder{Scope: scope, Principal: user.ID},
		req.Metadata,
		&principal{UserID: user.ID},
	)
	if err != nil {
		return nil, err
	}

	// The event goes here, after the session exists, and a refusal above wants
	// one too. audit and eventstream are where it goes; the vocabulary is this
	// application's.

	return &signInOutcome{Session: session}, nil
}

// exampleTOTPSecret is a fixed secret so the example's output does not move.
const exampleTOTPSecret = "JBSWY3DPEHPK3PXP"

func exampleCode() string {
	code, err := otp.GenerateCode(exampleTOTPSecret, time.Now().UTC())
	if err != nil {
		panic(err)
	}

	return code
}

// exampleFlow wires the seams: an identity store over SQLite, argon2, totp, and
// a session store over an in-memory cache.
func exampleFlow() (*exampleDeployment, tenancy.Scope) {
	ctx := context.Background()

	dir, err := os.MkdirTemp("", "authentication-example")
	if err != nil {
		panic(err)
	}

	client, err := sqlite.NewDatabaseClient(ctx, &exampleClientConfig{
		connectionString: filepath.Join(dir, "identity.db"),
	})
	if err != nil {
		panic(err)
	}

	stmts, err := migrations.Statements(dialect.SQLite, identity.DefaultTablePrefix)
	if err != nil {
		panic(err)
	}

	for _, stmt := range stmts {
		if _, err = client.Writer().ExecContext(ctx, stmt); err != nil {
			panic(err)
		}
	}

	store, err := identity.NewSQLStore(client)
	if err != nil {
		panic(err)
	}

	c, err := memory.NewInMemoryCache[sessions.Record[principal]](time.Hour)
	if err != nil {
		panic(err)
	}

	backend, err := sessionscache.NewBackend(c)
	if err != nil {
		panic(err)
	}

	sessionStore, err := sessions.NewStore[principal](backend)
	if err != nil {
		panic(err)
	}

	authenticator := argon2.NewArgon2Authenticator()

	// Minted once, at startup, and compared against for every handle that
	// resolves to nobody.
	decoy, err := authenticator.HashPassword(ctx, "a password nobody has")
	if err != nil {
		panic(err)
	}

	return &exampleDeployment{
		loginFlow: &loginFlow{
			directory:    store,
			passwords:    authenticator,
			secondFactor: totp.NewVerifier(),
			sessions:     sessionStore,
			decoyHash:    decoy,
		},
		client: client,
		store:  store,
	}, tenancy.Global()
}

// exampleUser registers somebody who can sign in: good standing, a hashed
// password, and a second-factor secret they have proven they hold.
func exampleUser(ctx context.Context, flow *exampleDeployment, username, password, secret string) *identity.User {
	hashed, err := flow.passwords.HashPassword(ctx, password)
	if err != nil {
		panic(err)
	}

	scope := tenancy.Global()

	user := &identity.User{
		Scope:           scope,
		Username:        username,
		EmailAddress:    username + "@example.com",
		HashedPassword:  hashed,
		TwoFactorSecret: secret,
		AccountStatus:   identity.StatusGood,
	}

	if err = flow.client.WithTransaction(ctx, func(q database.Tx) error {
		return flow.store.CreateUser(ctx, q, user)
	}); err != nil {
		panic(err)
	}

	// Issuing a secret is not holding one. This is the write that turns the
	// column into a second factor.
	if err = flow.store.MarkUserTwoFactorSecretVerified(ctx, scope, user.ID); err != nil {
		panic(err)
	}

	return user
}

func exampleAnonymousSession(ctx context.Context, flow *exampleDeployment) string {
	session, err := flow.sessions.New(ctx, &principal{})
	if err != nil {
		panic(err)
	}

	return session.ID
}

func exampleBan(ctx context.Context, flow *exampleDeployment, scope tenancy.Scope, userID, explanation string) {
	if err := flow.store.UpdateUserAccountStatus(ctx, scope, userID, identity.StatusBanned, explanation); err != nil {
		panic(err)
	}
}

type exampleClientConfig struct {
	connectionString string
}

func (c *exampleClientConfig) GetReadConnectionString() string   { return c.connectionString }
func (c *exampleClientConfig) GetWriteConnectionString() string  { return c.connectionString }
func (c *exampleClientConfig) GetMaxPingAttempts() uint64        { return 1 }
func (c *exampleClientConfig) GetPingWaitPeriod() time.Duration  { return time.Millisecond }
func (c *exampleClientConfig) GetMaxIdleConns() int              { return 2 }
func (c *exampleClientConfig) GetMaxOpenConns() int              { return 1 }
func (c *exampleClientConfig) GetConnMaxLifetime() time.Duration { return time.Minute }
Output:
unknown handle: true
wrong password: true
code required: true - session minted: false
signed in: true
identifier rotated: true
pre-sign-in session dead: true
banned, wrong password: true
banned, right password: chargebacks

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Authenticator

type Authenticator interface {
	Hasher

	// PasswordMatches reports whether password matches hash. A non-match
	// returns (false, nil); only genuine errors (malformed hash, runtime
	// failure) populate err.
	//
	// This module ships no mismatch sentinel, deliberately. A sign-in has
	// three refusals to collapse into one — an unknown handle, a wrong
	// password, a wrong second-factor code — and the package documentation
	// says why the error that covers all three belongs to the caller.
	PasswordMatches(ctx context.Context, hash, password string) (bool, error)
}

Authenticator hashes passwords and verifies them against a stored hash.

Second-factor verification (TOTP, WebAuthn, backup codes, etc.) is intentionally NOT part of this interface. Callers compose password verification with any second-factor verifier they need — see the authentication/totp package for the TOTP verifier.

type Hasher

type Hasher interface {
	HashPassword(ctx context.Context, password string) (string, error)
}

Hasher hashes passwords.

Directories

Path Synopsis
Package argon2 is the argon2id authentication.Authenticator: the password hasher this module recommends, and the only implementation of that interface it ships.
Package argon2 is the argon2id authentication.Authenticator: the password hasher this module recommends, and the only implementation of that interface it ships.
Package oauth2server is an OAuth 2.1 authorization server: the endpoints, the grant logic, and a Store seam underneath them.
Package oauth2server is an OAuth 2.1 authorization server: the endpoints, the grant logic, and a Store seam underneath them.
config
Package oauth2servercfg assembles an OAuth 2.1 authorization server, and the Store behind it, from environment configuration.
Package oauth2servercfg assembles an OAuth 2.1 authorization server, and the Store behind it, from environment configuration.
database
Package database keeps an authorization server's state in SQL tables.
Package database keeps an authorization server's state in SQL tables.
database/internal/queries
Package queries is the authorization server's schema described as data: the four canonical table names, each table's columns in the order every read projects them, and the columns a write may leave NULL.
Package queries is the authorization server's schema described as data: the four canonical table names, each table's columns in the order every read projects them, and the columns a write may leave NULL.
database/internal/queriesgen command
Command queriesgen writes the canonical sqlc input for the authorization server's schema, one file per dialect, from authentication/oauth2server/database/internal/queries.
Command queriesgen writes the canonical sqlc input for the authorization server's schema, one file per dialect, from authentication/oauth2server/database/internal/queries.
database/migrations
Package migrations supplies the authorization server's DDL, rendered for a dialect and table prefix.
Package migrations supplies the authorization server's DDL, rendered for a dialect and table prefix.
memory
Package memory keeps an authorization server's state in maps.
Package memory keeps an authorization server's state in maps.
oauth2servertest
Package oauth2servertest holds the behavior every oauth2server.Store owes its callers, written once and run against each implementation.
Package oauth2servertest holds the behavior every oauth2server.Store owes its callers, written once and run against each implementation.
Package passwordreset stores the token that lets somebody who cannot sign in prove they own the address the account was registered with.
Package passwordreset stores the token that lets somebody who cannot sign in prove they own the address the account was registered with.
internal/queries
Package queries is the password reset token schema described as data: the canonical table name, its columns in the order every read projects them, the subsets a write assigns, and the five statements the store executes over them.
Package queries is the password reset token schema described as data: the canonical table name, its columns in the order every read projects them, the subsets a write assigns, and the five statements the store executes over them.
internal/queriesgen command
Command queriesgen writes the canonical sqlc input for the password reset token schema, one file per dialect, from authentication/passwordreset/internal/queries.
Command queriesgen writes the canonical sqlc input for the password reset token schema, one file per dialect, from authentication/passwordreset/internal/queries.
migrations
Package migrations supplies the password reset token table's DDL, rendered for a dialect and table prefix.
Package migrations supplies the password reset token table's DDL, rendered for a dialect and table prefix.
mock
Package passwordresetmock provides moq-generated mock implementations of interfaces in the passwordreset package.
Package passwordresetmock provides moq-generated mock implementations of interfaces in the passwordreset package.
Package tokens is the seam for bearer tokens: an Issuer mints them and parses them back, and the jwt and paseto subpackages implement it.
Package tokens is the seam for bearer tokens: an Issuer mints them and parses them back, and the jwt and paseto subpackages implement it.
config
Package tokenscfg selects and builds a tokens.Issuer from configuration: either the JWT signer or the PASETO one.
Package tokenscfg selects and builds a tokens.Issuer from configuration: either the JWT signer or the PASETO one.
jwt
Package jwt is the HS256 tokens.Issuer: JSON Web Tokens signed with a shared secret.
Package jwt is the HS256 tokens.Issuer: JSON Web Tokens signed with a shared secret.
mock
Package tokensmock provides moq-generated mock implementations of interfaces in the tokens package.
Package tokensmock provides moq-generated mock implementations of interfaces in the tokens package.
paseto
Package paseto is the PASETO v2.local tokens.Issuer: tokens whose claims are encrypted rather than merely signed.
Package paseto is the PASETO v2.local tokens.Issuer: tokens whose claims are encrypted rather than merely signed.
Package totp provides a TOTP (RFC 6238) second-factor verifier.
Package totp provides a TOTP (RFC 6238) second-factor verifier.
mock
Package totpmock provides moq-generated mock implementations of interfaces in the totp package.
Package totpmock provides moq-generated mock implementations of interfaces in the totp package.
Package webauthn provides passkey registration and login over github.com/go-webauthn/webauthn, and the ceremony store that makes it work on more than one replica.
Package webauthn provides passkey registration and login over github.com/go-webauthn/webauthn, and the ceremony store that makes it work on more than one replica.
cache
Package cache stores WebAuthn ceremony state in a cache.Cache.
Package cache stores WebAuthn ceremony state in a cache.Cache.
config
Package webauthncfg assembles a WebAuthn relying party, and the ceremony store under it, from environment configuration.
Package webauthncfg assembles a WebAuthn relying party, and the ceremony store under it, from environment configuration.
database
Package database stores WebAuthn ceremony state in a SQL table.
Package database stores WebAuthn ceremony state in a SQL table.
database/internal/queries
Package queries is the WebAuthn ceremony session schema described as data: the canonical table name, its columns in the order every read projects them, and the four statements the store executes over them.
Package queries is the WebAuthn ceremony session schema described as data: the canonical table name, its columns in the order every read projects them, and the four statements the store executes over them.
database/internal/queriesgen command
Command queriesgen writes the canonical sqlc input for the WebAuthn ceremony session schema, one file per dialect, from authentication/webauthn/database/internal/queries.
Command queriesgen writes the canonical sqlc input for the WebAuthn ceremony session schema, one file per dialect, from authentication/webauthn/database/internal/queries.
database/migrations
Package migrations supplies the WebAuthn ceremony session table's DDL, rendered for a dialect and table prefix.
Package migrations supplies the WebAuthn ceremony session table's DDL, rendered for a dialect and table prefix.
mock
Package webauthnmock provides moq-generated mock implementations of interfaces in the webauthn package.
Package webauthnmock provides moq-generated mock implementations of interfaces in the webauthn package.
webauthntest
Package webauthntest holds the behavior every webauthn.SessionStore owes its callers, written once and run against each implementation.
Package webauthntest holds the behavior every webauthn.SessionStore owes its callers, written once and run against each implementation.

Jump to

Keyboard shortcuts

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