token

package
v0.26.0 Latest Latest
Warning

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

Go to latest
Published: Jun 25, 2026 License: MIT Imports: 11 Imported by: 0

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

Examples

Constants

View Source
const AbilityAll = "*"

AbilityAll is the wildcard ability granting every scope. A token holding it passes Can(x) for any x.

Variables

View Source
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 NewIssuer

func NewIssuer(store Store) *Issuer

NewIssuer returns an Issuer backed by store.

func (*Issuer) Find

func (i *Issuer) Find(ctx context.Context, plain string) (*PersonalAccessToken, error)

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) List

func (i *Issuer) List(ctx context.Context, userID uint64) ([]*PersonalAccessToken, error)

List returns all tokens owned by userID.

func (*Issuer) Revoke

func (i *Issuer) Revoke(ctx context.Context, id string) error

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

func (i *Issuer) RevokePlain(ctx context.Context, plain string) error

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) Delete

func (s *MemoryStore) Delete(_ context.Context, id string) error

func (*MemoryStore) Get

func (*MemoryStore) ListByUser

func (s *MemoryStore) ListByUser(_ context.Context, userID uint64) ([]*PersonalAccessToken, error)

func (*MemoryStore) Save

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.

Jump to

Keyboard shortcuts

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