Documentation
¶
Overview ¶
Package token implements Sanctum-style personal access tokens for lagodev-based applications.
A personal access token is an opaque, high-entropy string handed to an API client once at creation time. The server never stores the plaintext: only a SHA-256 hash is persisted, so a database leak does not expose usable credentials. Each token carries a set of abilities (scopes), an optional expiry, and a last-used timestamp.
The wire format is "<id>|<secret>" (Sanctum-compatible): the id selects the stored record in O(1) and the secret is hash-compared in constant time. Lookups therefore never require scanning every row.
The Store interface is intentionally small so a SQL or Redis driver can replace MemoryStore without touching call sites. All times are UTC.
Index ¶
- Constants
- Variables
- type Issuer
- func (i *Issuer) Find(ctx context.Context, plain string) (*PersonalAccessToken, error)
- func (i *Issuer) Issue(ctx context.Context, userID uint64, name string, abilities []string, ...) (*PersonalAccessToken, string, error)
- func (i *Issuer) List(ctx context.Context, userID uint64) ([]*PersonalAccessToken, error)
- func (i *Issuer) Revoke(ctx context.Context, id string) error
- func (i *Issuer) RevokePlain(ctx context.Context, plain string) error
- type MemoryStore
- func (s *MemoryStore) Delete(_ context.Context, id string) error
- func (s *MemoryStore) Get(_ context.Context, id string) (*PersonalAccessToken, bool, error)
- func (s *MemoryStore) ListByUser(_ context.Context, userID uint64) ([]*PersonalAccessToken, error)
- func (s *MemoryStore) Save(_ context.Context, t *PersonalAccessToken) error
- type PersonalAccessToken
- type Store
Examples ¶
Constants ¶
const AbilityAll = "*"
AbilityAll is the wildcard ability granting every scope. A token holding it passes Can(x) for any x.
Variables ¶
var ( // ErrNotFound is returned when no token matches the lookup. ErrNotFound = errors.New("token: not found") // ErrExpired is returned when a token matched but its expiry has passed. ErrExpired = errors.New("token: expired") // ErrRevoked is returned when a token matched but was revoked. ErrRevoked = errors.New("token: revoked") // ErrMalformed is returned when a plaintext token is not in the // "<id>|<secret>" form. ErrMalformed = errors.New("token: malformed") )
Errors returned by this package. Callers map these to HTTP responses (404/401/403) without inspecting strings.
Functions ¶
This section is empty.
Types ¶
type Issuer ¶
type Issuer struct {
// contains filtered or unexported fields
}
Issuer mints and verifies personal access tokens against a Store.
Example ¶
ExampleIssuer shows the personal-access-token lifecycle: issue a token with a scoped ability, find it back from the plaintext, check the ability, then revoke it and show the revoked token is rejected.
package main
import (
"context"
"errors"
"fmt"
"github.com/devituz/lagodev/auth/token"
)
func main() {
ctx := context.Background()
issuer := token.NewIssuer(token.NewMemoryStore())
// Issue a never-expiring token for user 7, scoped to "posts:read".
rec, plain, err := issuer.Issue(ctx, 7, "CI deploy key", []string{"posts:read"}, 0)
if err != nil {
panic(err)
}
fmt.Println("name:", rec.Name)
// A client presents the plaintext; resolve it back to its record.
found, err := issuer.Find(ctx, plain)
if err != nil {
panic(err)
}
fmt.Println("found user:", found.UserID)
fmt.Println("can posts:read:", found.Can("posts:read"))
fmt.Println("can posts:write:", found.Can("posts:write"))
// Revoke it, then show the same plaintext is now rejected.
if err := issuer.Revoke(ctx, rec.ID); err != nil {
panic(err)
}
_, err = issuer.Find(ctx, plain)
fmt.Println("revoked rejected:", errors.Is(err, token.ErrRevoked))
}
Output: name: CI deploy key found user: 7 can posts:read: true can posts:write: false revoked rejected: true
func (*Issuer) Find ¶
Find resolves a plaintext token to its record. It validates the format, looks up the record by id, constant-time-compares the secret hash, and rejects expired/revoked tokens. On success it stamps LastUsedAt (best effort) and returns the (updated) record.
Errors: ErrMalformed, ErrNotFound, ErrExpired, ErrRevoked.
func (*Issuer) Issue ¶
func (i *Issuer) Issue(ctx context.Context, userID uint64, name string, abilities []string, ttl time.Duration) (*PersonalAccessToken, string, error)
Issue creates a new token for userID with the given name, abilities, and TTL (ttl<=0 means "never expires"). It returns the stored record plus the one-time plaintext token ("<id>|<secret>") which the caller must show the user immediately — it is unrecoverable afterwards.
func (*Issuer) Revoke ¶
Revoke marks the token with the given id revoked. It is idempotent and returns ErrNotFound only when no such id exists. Revoked tokens are kept (not deleted) so audit/listing still shows them; use Delete to purge.
func (*Issuer) RevokePlain ¶
RevokePlain revokes the token identified by a plaintext value. It does not require the secret to match the stored hash beyond locating the id, but it still validates format. Useful for "log out this token" flows where the client presents its own token.
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is an in-process Store backed by a sync.RWMutex. It is meant for single-replica apps and tests; swap for a DB-backed Store in production multi-replica deployments.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
NewMemoryStore returns an empty in-memory Store.
func (*MemoryStore) Get ¶
func (s *MemoryStore) Get(_ context.Context, id string) (*PersonalAccessToken, bool, error)
func (*MemoryStore) ListByUser ¶
func (s *MemoryStore) ListByUser(_ context.Context, userID uint64) ([]*PersonalAccessToken, error)
func (*MemoryStore) Save ¶
func (s *MemoryStore) Save(_ context.Context, t *PersonalAccessToken) error
type PersonalAccessToken ¶
type PersonalAccessToken struct {
// ID is the public selector (random, URL-safe). It is also the lookup key.
ID string
// UserID owns the token.
UserID uint64
// Name is a human label ("CI deploy key").
Name string
// Hash is the hex-encoded SHA-256 of the secret half. Never the plaintext.
Hash string
// Abilities are the granted scopes. AbilityAll ("*") grants everything.
Abilities []string
// CreatedAt / ExpiresAt are UTC. ExpiresAt zero means "never expires".
CreatedAt time.Time
ExpiresAt time.Time
// LastUsedAt is UTC and zero until the token is first matched by Find.
LastUsedAt time.Time
// RevokedAt is UTC and zero unless the token has been revoked.
RevokedAt time.Time
}
PersonalAccessToken is the stored record. The plaintext secret is never persisted — only Hash (hex SHA-256 of the secret half).
func (*PersonalAccessToken) Can ¶
func (t *PersonalAccessToken) Can(ability string) bool
Can reports whether the token grants ability. A token holding AbilityAll grants every ability.
func (*PersonalAccessToken) Cant ¶
func (t *PersonalAccessToken) Cant(ability string) bool
Cant is the negation of Can.
func (*PersonalAccessToken) Expired ¶
func (t *PersonalAccessToken) Expired() bool
Expired reports whether the token has an expiry in the past.
func (*PersonalAccessToken) Revoked ¶
func (t *PersonalAccessToken) Revoked() bool
Revoked reports whether the token has been revoked.
type Store ¶
type Store interface {
// Save inserts or replaces a token record.
Save(ctx context.Context, t *PersonalAccessToken) error
// Get returns the record for id. ok=false on miss.
Get(ctx context.Context, id string) (*PersonalAccessToken, bool, error)
// Delete removes the record for id (no-op if absent).
Delete(ctx context.Context, id string) error
// ListByUser returns every token owned by userID (including expired and
// revoked records, so management UIs can show them).
ListByUser(ctx context.Context, userID uint64) ([]*PersonalAccessToken, error)
}
Store persists access tokens keyed by their public ID. Implementations must be safe for concurrent use.