devise

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

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 16 Imported by: 0

README

go-ruby-devise/devise

devise — go-ruby-devise

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the module logic of Ruby's Devise authentication framework, faithful to MRI Devise on Ruby 4.0.5. It ports the behavioural heart of Devise's authentication modules — the credential, token, lock, remember, confirm and tracking state machines — as plain Go operating on an injectable model, together with the Warden strategy that drives database authentication. No Ruby runtime required.

It is designed to be the Devise backend for go-embedded-ruby under a later rbgo binding, but is a standalone, reusable module. It reuses two sibling gem ports as real dependencies:

v0.1 — module cores. This first release ships the logic of Devise's modules and the database Warden strategy. Persistence (ActiveRecord), controllers, routes, views, mailers and the Rails engine are seams or roadmap (see Scope), because they are the host's job — a Rails app, or the rbgo binding, wires them in.

The model & finder seams

Devise's modules are mixed into an ActiveRecord model. This library keeps that shape but abstracts persistence behind two seams, so the same logic runs under Rails, under rbgo, or against an in-memory table in tests:

// A single authenticatable resource (a User row).
type Model interface {
    Get(attr string) any      // read a column by its Devise attribute name
    Set(attr string, val any) // stage a new value
    Save() error              // persist (Devise's save(validate: false))
}

// The class-level lookup (ActiveRecord's find_first / find_for_database_authentication).
type Finder func(attrs map[string]any) (Model, bool)

A Record binds a Model to a Config — the analogue of an ActiveRecord instance whose self.class carries the Devise settings. Every module method hangs off *Record; class-level flows (reset-by-token, confirm-by-token, cookie deserialisation) hang off *Config and use its Finder. Mailers are optional callbacks on Config (SendResetPasswordInstructions, SendConfirmationInstructions, SendUnlockInstructions).

Install

go get github.com/go-ruby-devise/devise

Usage

cfg := devise.DefaultConfig()      // Devise's out-of-the-box defaults
cfg.Stretches = 12                 // bcrypt cost
cfg.Finder = myUserTableFinder     // wire persistence

// Database Authenticatable
r := devise.New(cfg, user)
_ = r.SetPassword("s3cret!!")      // password=  (hash + pepper + cost)
ok := r.ValidPassword("s3cret!!")  // valid_password?

// Recoverable
raw, _ := r.SendResetPasswordInstructions()          // returns the mailed token
rec, err := cfg.ResetPasswordByToken(raw, "new", "new")

// Warden strategy (database_authenticatable)
res := devise.DatabaseAuthenticatableStrategy{Cfg: cfg}.
    Run(map[string]any{"email": "a@b.co"}, "s3cret!!")
// res is a warden.StrategyResult: Success carries res.User.

Modules shipped (v0.1)

Devise module Ported surface
DatabaseAuthenticatable valid_password?, password=, authenticatable_salt, Devise::Encryptor (digest/compare via go-ruby-bcrypt)
Validatable email presence/format/uniqueness, password presence/length/confirmation
Recoverable set_reset_password_token, send_reset_password_instructions, reset_password_by_token, period validity
Rememberable remember_me! / forget_me!, rememberable_value, remember_expired?, cookie serialise/deserialise
Confirmable generate_confirmation_token, confirm, confirmed?, confirmation period valid/expired, active_for_authentication?
Lockable lock_access! / unlock_access!, failed_attempts, valid_for_authentication? lock gate, unlock_in / maximum_attempts, unlock-by-token
Trackable update_tracked_fields (sign-in count, current/last at + IP)
Timeoutable timedout?, timeout_in
Registerable new_with_session, update_with_password, update_without_password, destroy_with_password (the model-level sign-up/edit flows)
Devise helpers friendly_token, secure_compare, TokenGenerator (HMAC-SHA256 digest over a byte-faithful PBKDF2 KeyGenerator), email_regexp, encryptor selection
Warden strategies Authenticatable / DatabaseAuthenticatable on warden.StrategyResult, plus a StrategyRun seam

Scope & roadmap

Deferred (host/roadmap — not this release):

  • The Rails controllers, routes, views and helpers (SessionsController, RegistrationsController, PasswordsController, ...) — the HTTP glue. The model-level flows they drive (sign in, register, edit, reset, confirm, unlock) are all here; a Rails app or the rbgo binding wires the request cycle.
  • The Rails engine, mailers and their templates — only the notification callbacks are wired here (SendResetPasswordInstructions, ...).
  • Omniauthable (OAuth), the Devise generators and i18n.

Fidelity notes

The crypto and token surfaces are byte-faithful to Devise on MRI 4.0.5:

  • Passwords flow through go-ruby-bcrypt, itself validated byte-for-byte against Ruby's bcrypt gem, so a hash produced here verifies under MRI Devise and vice-versa.
  • friendly_token reproduces SecureRandom.urlsafe_base64((n*3)/4) with the tr('lIO0', 'sxyz') substitution — byte-identical output for identical entropy.
  • secure_compare matches ActiveSupport::SecurityUtils.secure_compare (length check + constant-time comparison).
  • TokenGenerator emits OpenSSL::HMAC.hexdigest("SHA256", key_for(column), value) digests. NewDeviseTokenGenerator(secret) reproduces Devise.token_generator in full: the key is derived by a byte-faithful PBKDF2KeyGeneratorActiveSupport::KeyGenerator (PBKDF2-HMAC-SHA1, 2¹⁶ iterations, 64-byte key, salt "Devise <column>") wrapped in a CachingKeyGenerator. Given the same secret_key_base, the stored reset/confirm/unlock digests are byte-identical to the gem's, so a raw token issued by either side verifies against a digest stored by the other. (A simpler zero-config HMACKeyGenerator remains the default for tests and non-Rails hosts.)

Tests & coverage

100% line coverage, enforced in CI, across all module cores — password valid/invalid, token generation + expiry, lock/unlock thresholds, remember round-trip, confirm, register/edit/destroy, and the validatable rules — exercised through fake Model / Finder seams, with bcrypt driven through go-ruby-bcrypt. These deterministic tests keep coverage at 100% on their own.

Differential oracle vs MRI. On top of them, a set of oracle tests shell out to real Ruby and assert byte-parity against the gem's own code paths:

  • oracle_ruby_test.go needs only Ruby's stdlib (no gems): the reset/confirm/unlock token digest (PBKDF2-HMAC-SHA1 key → HMAC-SHA256) and friendly_token's encoding, checked byte-for-byte and in both round-trip directions. These run on the CI lanes that install Ruby.
  • oracle_gem_test.go drives Devise::Encryptor, Devise::TokenGenerator and Devise.secure_compare directly; it skip-gates when the devise/bcrypt gems are absent (run it locally with the gems on GEM_PATH).

The library cross-compiles and is tested on the six supported 64-bit architectures (amd64, arm64, riscv64, loong64, ppc64le, s390x — including big-endian s390x) and three operating systems.

go test -race -cover ./...

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-devise/devise authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package devise is a pure-Go (no cgo) reimplementation of the module logic of Ruby's Devise authentication framework, faithful to MRI Devise on Ruby 4.0.5.

Scope

This is the v0.1 module-core foundation. It ports the behavioural heart of Devise's authentication modules — the credential, token, lock, remember, confirm and tracking state machines — as plain Go operating on an injectable model, together with the Warden strategy that drives database authentication. It deliberately has no dependency on a Ruby runtime and is a sibling of the other go-ruby-* stdlib/gem ports.

Persistence (ActiveRecord), controllers, routes, views, mailers and the Rails engine are NOT reimplemented here — they are seams (see Model, Finder and the notification callbacks on Config) or roadmap items. A later go-embedded-ruby binding wires those seams to the host.

Model seam

Every module operates on a Record — a Model paired with a Config. The Model interface is the whole persistence surface Devise needs: read an attribute, write an attribute, and save. The host (ActiveRecord in Rails, go-embedded-ruby's object model under rbgo) supplies it. Class-level lookups (uniqueness, find-by-token, cookie deserialisation) go through the Finder seam on Config.

Index

Constants

View Source
const (
	AttrEmail             = "email"
	AttrEncryptedPassword = "encrypted_password"

	AttrResetPasswordToken  = "reset_password_token"
	AttrResetPasswordSentAt = "reset_password_sent_at"

	AttrRememberToken     = "remember_token"
	AttrRememberCreatedAt = "remember_created_at"

	AttrConfirmationToken  = "confirmation_token"
	AttrConfirmedAt        = "confirmed_at"
	AttrConfirmationSentAt = "confirmation_sent_at"
	AttrUnconfirmedEmail   = "unconfirmed_email"

	AttrFailedAttempts = "failed_attempts"
	AttrUnlockToken    = "unlock_token"
	AttrLockedAt       = "locked_at"

	AttrSignInCount     = "sign_in_count"
	AttrCurrentSignInAt = "current_sign_in_at"
	AttrLastSignInAt    = "last_sign_in_at"
	AttrCurrentSignInIP = "current_sign_in_ip"
	AttrLastSignInIP    = "last_sign_in_ip"
)

Devise attribute names — the database columns Devise's modules read and write. They match the column names Devise's generators create so a binding can map them straight onto ActiveRecord attributes.

View Source
const KeyGeneratorIterations = 1 << 16

KeyGeneratorIterations is the PBKDF2 iteration count ActiveSupport::KeyGenerator uses by default (2**16), and thus what Devise's token generator uses.

View Source
const KeyGeneratorKeySize = 64

KeyGeneratorKeySize is the default derived-key length in bytes (ActiveSupport::KeyGenerator#generate_key's key_size default of 64, chosen to match OpenSSL::Digest::SHA1#block_length).

View Source
const StrategyDatabaseAuthenticatable = "database_authenticatable"

StrategyDatabaseAuthenticatable is the Warden label of the database strategy, matching Devise's :database_authenticatable.

Variables

View Source
var (
	// ErrAlreadyConfirmed is returned by Confirm when the record is already
	// confirmed (Devise adds :already_confirmed on email).
	ErrAlreadyConfirmed = errors.New("devise: already confirmed")
	// ErrConfirmationPeriodExpired is returned by Confirm when the token is past
	// confirm_within (Devise adds :confirmation_period_expired on email).
	ErrConfirmationPeriodExpired = errors.New("devise: confirmation period expired")
	// ErrConfirmationTokenNotFound is returned by ConfirmByToken when no record
	// matches the token.
	ErrConfirmationTokenNotFound = errors.New("devise: confirmation token not found")
)

Confirmable error reasons.

View Source
var (
	// ErrResetTokenNotFound is returned when no record matches the reset token
	// (Devise adds :not_found on the token, or :invalid).
	ErrResetTokenNotFound = errors.New("devise: reset password token not found")
	// ErrResetTokenExpired is returned when the token is past
	// reset_password_within (Devise adds :expired).
	ErrResetTokenExpired = errors.New("devise: reset password token expired")
)

Recoverable error reasons, mirroring the symbolic errors Devise adds to reset_password_token / password.

View Source
var DefaultEmailRegexp = regexp.MustCompile(`\A[^@\s]+@[^@\s]+\z`)

DefaultEmailRegexp is Devise.email_regexp: a non-empty local part, an @, and a non-empty domain part, neither containing whitespace or a further @.

View Source
var ErrNotDestroyable = errors.New("devise: model is not a Destroyer")

ErrNotDestroyable is returned by Record.DestroyWithPassword when the underlying Model does not implement Destroyer, i.e. cannot delete itself.

View Source
var ErrRememberCookieInvalid = errors.New("devise: invalid remember cookie")

ErrRememberCookieInvalid is returned by Config.SerializeFromCookie when the cookie is malformed or does not authenticate a record.

View Source
var ErrUnlockTokenNotFound = errors.New("devise: unlock token not found")

ErrUnlockTokenNotFound is returned by Config.UnlockAccessByToken when no record matches the unlock token.

Functions

func DecodeCookie

func DecodeCookie(s string) []string

DecodeCookie splits an EncodeCookie payload back into its parts.

func EncodeCookie

func EncodeCookie(parts []string) string

EncodeCookie joins a cookie payload with "/" the way Devise's cookie serializer does before signing. Provided as a convenience for a binding that stores the payload as a single string.

func FriendlyToken

func FriendlyToken(length int) string

FriendlyToken returns a URL-safe random token, faithful to Devise.friendly_token: it draws (length*3)/4 random bytes, URL-safe-base64 encodes them without padding, then maps the ambiguous characters l, I, O and 0 to s, x, y and z. With the default length of 20 it yields a 20-character token. A non-positive length yields the empty string (no entropy requested).

func SecureCompare

func SecureCompare(a, b string) bool

SecureCompare compares two strings in constant time, faithful to Devise.secure_compare (ActiveSupport::SecurityUtils.secure_compare): it returns false immediately when the byte lengths differ, and otherwise performs a fixed-length, timing-safe comparison. Two empty strings are equal.

func SerializeIntoCookie

func SerializeIntoCookie(id, rememberableValue, timestamp string) []string

SerializeIntoCookie returns the cookie payload for a record, faithful to serialize_into_cookie: [id, rememberable_value, timestamp]. The id and timestamp are supplied by the caller (the model's to_key and Time.now.to_f in Devise), keeping this free of a persistence-identity assumption.

Types

type CachingKeyGenerator

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

CachingKeyGenerator memoises another KeyGenerator by salt, the port of ActiveSupport::CachingKeyGenerator. PBKDF2 at 2**16 iterations is deliberately slow, and Devise re-derives the same per-column key on every token operation, so Devise wraps its KeyGenerator in a caching one; NewDeviseTokenGenerator does the same. It is safe for the sequential use a request makes; wrap access externally if you share one across goroutines.

func NewCachingKeyGenerator

func NewCachingKeyGenerator(inner KeyGenerator) *CachingKeyGenerator

NewCachingKeyGenerator wraps inner so repeated GenerateKey(salt) calls with the same salt reuse the first derivation.

func (*CachingKeyGenerator) GenerateKey

func (g *CachingKeyGenerator) GenerateKey(salt string) []byte

GenerateKey returns the cached key for salt, deriving and caching it on first use, faithful to ActiveSupport::CachingKeyGenerator#generate_key.

type Config

type Config struct {

	// Stretches is the bcrypt cost (Devise.stretches). Devise defaults to 12,
	// and to 1 in the test environment.
	Stretches int
	// Pepper is appended to passwords before hashing (Devise.pepper). Empty
	// means no pepper.
	Pepper string

	// EmailRegexp validates email format (Devise.email_regexp).
	EmailRegexp *regexp.Regexp
	// PasswordLengthMin / PasswordLengthMax bound password length
	// (Devise.password_length, default 6..128).
	PasswordLengthMin int
	PasswordLengthMax int

	// ResetPasswordWithin is how long a reset token stays valid
	// (Devise.reset_password_within, default 6 hours).
	ResetPasswordWithin time.Duration

	// RememberFor is how long a remember-me cookie lasts (Devise.remember_for,
	// default 2 weeks).
	RememberFor time.Duration
	// ExpireAllRememberMeOnSignOut clears remember_created_at on sign-out
	// (Devise.expire_all_remember_me_on_sign_out, default true).
	ExpireAllRememberMeOnSignOut bool

	// AllowUnconfirmedAccessFor is the grace window during which an unconfirmed
	// account may still sign in (Devise.allow_unconfirmed_access_for). A nil
	// pointer means "unlimited" (never expires); a zero duration means no grace.
	AllowUnconfirmedAccessFor *time.Duration
	// ConfirmWithin is how long a confirmation token stays valid
	// (Devise.confirm_within). A nil pointer means tokens never expire.
	ConfirmWithin *time.Duration

	// MaximumAttempts is the failed-attempt threshold that triggers a lock
	// (Devise.maximum_attempts, default 20).
	MaximumAttempts int
	// UnlockIn is the auto-unlock window for the time strategy
	// (Devise.unlock_in, default 1 hour).
	UnlockIn time.Duration
	// UnlockStrategy and LockStrategy select the lock/unlock behaviour.
	UnlockStrategy UnlockStrategy
	LockStrategy   LockStrategy

	// TimeoutIn is the idle window after which a session times out
	// (Devise.timeout_in). A nil pointer disables timeout.
	TimeoutIn *time.Duration

	// AuthenticationKeys are the columns used to find a record for
	// authentication (Devise.authentication_keys, default ["email"]).
	AuthenticationKeys []string
	// FriendlyTokenLength is the default length of a [FriendlyToken]
	// (Devise's friendly_token default is 20).
	FriendlyTokenLength int

	// Finder performs class-level record lookups. Required by uniqueness
	// validation, reset-by-token, remember-cookie deserialisation and the
	// database strategy; a nil Finder makes those treat "no match" as the
	// result.
	Finder Finder
	// TokenGenerator produces and digests the raw tokens Recoverable and
	// Confirmable store. Defaults to a [TokenGenerator] with a zero key; wire a
	// real [KeyGenerator] in production.
	TokenGenerator *TokenGenerator
	// Now is the clock (defaults to time.Now().UTC()). Devise stores all
	// timestamps in UTC.
	Now func() time.Time
	// Credentials extracts the authentication hash and password from a Rack env
	// for the database strategy. Defaults to reading env["devise.credentials"]
	// and env["devise.password"]; a binding overrides it to parse Rack params.
	Credentials Credentials

	// SendResetPasswordInstructions is invoked with the raw reset token after it
	// is set, standing in for the Recoverable mailer. Optional.
	SendResetPasswordInstructions func(r *Record, rawToken string)
	// SendConfirmationInstructions is invoked with the raw confirmation token,
	// standing in for the Confirmable mailer. Optional.
	SendConfirmationInstructions func(r *Record, rawToken string)
	// SendUnlockInstructions is invoked with the raw unlock token, standing in
	// for the Lockable mailer. Optional.
	SendUnlockInstructions func(r *Record, rawToken string)
}

Config carries the per-resource Devise settings (the knobs Devise exposes via the initializer and per-model devise :... declarations) together with the injectable seams a pure-Go core needs: the record lookup Finder, the TokenGenerator, and a clock. Build one with DefaultConfig and override fields, mirroring how a Rails app tweaks config.to_prepare / model options.

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns a Config populated with Devise's out-of-the-box defaults. Callers override individual fields (Stretches, Pepper, Finder, TokenGenerator, ...) as a Rails app would in its initializer.

func (*Config) ConfirmByToken

func (c *Config) ConfirmByToken(rawToken string) (*Record, error)

ConfirmByToken is the class-level confirmation flow, faithful to confirm_by_token: it digests the raw token, finds the record, and confirms it.

func (*Config) DatabaseStrategyRun

func (c *Config) DatabaseStrategyRun() warden.StrategyRun

DatabaseStrategyRun returns a warden.StrategyRun that dispatches the database_authenticatable label to this config's strategy, reading credentials via Config.Credentials. Plug it into a Manager with warden.WithStrategyRun(cfg.DatabaseStrategyRun()).

func (*Config) ResetPasswordByToken

func (c *Config) ResetPasswordByToken(rawToken, newPassword, newPasswordConfirmation string) (*Record, error)

ResetPasswordByToken is the class-level reset flow, faithful to reset_password_by_token: it digests the raw token, finds the record, checks the token has not expired, and resets the password. It returns the updated record, or an error (ErrResetTokenNotFound / ErrResetTokenExpired) plus any validation failures from the password rules.

func (*Config) SerializeFromCookie

func (c *Config) SerializeFromCookie(id, rememberableValue string) (*Record, error)

SerializeFromCookie re-identifies a record from a cookie payload, faithful to serialize_from_cookie: it finds the record by id, then confirms it is not expired and its rememberable value matches the cookie's, via SecureCompare. A mismatch, an expired cookie, or a missing record yields ErrRememberCookieInvalid.

func (*Config) UnlockAccessByToken

func (c *Config) UnlockAccessByToken(rawToken string) (*Record, error)

UnlockAccessByToken is the class-level unlock flow, faithful to unlock_access_by_token: it digests the raw token, finds the record, and unlocks it.

type Credentials

type Credentials func(env rack.Env) (authHash map[string]any, password string)

Credentials is the seam that extracts the authentication hash and password from a Rack env, standing in for Warden's params parsing. The default reads env["devise.credentials"] (a map[string]any of authentication keys) and env["devise.password"] (a string); a binding overrides Config.Credentials to parse real Rack params.

type DatabaseAuthenticatableStrategy

type DatabaseAuthenticatableStrategy struct {
	Cfg *Config
}

DatabaseAuthenticatableStrategy is Devise's password strategy: it looks a record up by the authentication keys and verifies its password, gated by Lockable's valid_for_authentication? and Confirmable's active_for_authentication?.

func (DatabaseAuthenticatableStrategy) Run

func (s DatabaseAuthenticatableStrategy) Run(authHash map[string]any, password string) warden.StrategyResult

Run executes the strategy, returning a warden.StrategyResult faithful to Devise's authenticate!: an invalid strategy is skipped (Valid=false); an unknown record fails with "not_found_in_database"; a record whose password fails or whose account is locked fails with "invalid"; an inactive (unconfirmed) record fails with its inactive message; otherwise the record is returned as the authenticated user.

func (DatabaseAuthenticatableStrategy) Valid

func (s DatabaseAuthenticatableStrategy) Valid(authHash map[string]any, password string) bool

Valid reports the strategy's valid? predicate (valid_for_params_auth?): a password must be present and every configured authentication key must be present and non-empty in authHash.

type Destroyer

type Destroyer interface {
	Destroy() error
}

Destroyer is the optional Model capability a record needs to delete itself, mirroring ActiveRecord#destroy. Record.DestroyWithPassword requires it; a binding wires it to the ORM's delete, tests to an in-memory tombstone.

type Encryptor

type Encryptor struct{}

Encryptor is Devise::Encryptor: the bcrypt-backed password hasher. Digest hashes a password at a cost, applying the pepper; Compare checks a candidate password against a stored hash in constant time. Both mirror the gem exactly, delegating the crypto to go-ruby-bcrypt.

func (Encryptor) Compare

func (Encryptor) Compare(hashedPassword, password, pepper string) bool

Compare reports whether password matches hashedPassword, faithful to Devise::Encryptor.compare: a blank stored hash is never a match; otherwise the candidate (with pepper applied) is hashed against the stored salt and the result is compared to the stored hash with SecureCompare. An unparseable stored hash is treated as no match.

func (Encryptor) Digest

func (Encryptor) Digest(password, pepper string, cost int) (string, error)

Digest hashes password with the pepper applied, at the given bcrypt cost, faithful to Devise::Encryptor.digest: when pepper is non-empty it is appended to the password before hashing, then BCrypt::Password.create(..., cost:) runs. The returned string is the "$2a$NN$...."-form hash to store in encrypted_password.

type Finder

type Finder func(attrs map[string]any) (Model, bool)

Finder is the class-level lookup seam, standing in for ActiveRecord's to_adapter.find_first / find_for_database_authentication. It returns the first record matching every attribute in attrs, and false when none matches. The binding wires it to a real query; tests wire it to an in-memory table.

type HMACKeyGenerator

type HMACKeyGenerator struct {
	Secret []byte
}

HMACKeyGenerator derives keys as HMAC-SHA256(Secret, salt). It is the default KeyGenerator; a zero-value (empty Secret) is valid and deterministic, which is why the token generator works out of the box in tests.

func (HMACKeyGenerator) GenerateKey

func (g HMACKeyGenerator) GenerateKey(salt string) []byte

GenerateKey returns HMAC-SHA256(Secret, salt).

type KeyGenerator

type KeyGenerator interface {
	GenerateKey(salt string) []byte
}

KeyGenerator derives a per-column signing key from a secret, mirroring the ActiveSupport::KeyGenerator that Devise::TokenGenerator is built on. Devise calls key_generator.generate_key("Devise <column>") to obtain the HMAC key for each tokenised column, so that the same raw token digests differently per column.

The default HMACKeyGenerator derives the key with HMAC-SHA256; a production binding may instead plug Rails' PBKDF2-based ActiveSupport::KeyGenerator here. Either way the resulting digests are HMAC-SHA256 hexdigests, exactly Devise's token shape.

type LockStrategy

type LockStrategy string

LockStrategy selects what triggers a lock, mirroring Devise.lock_strategy.

const (
	// LockFailedAttempts locks after too many failed sign-ins (Devise's default).
	LockFailedAttempts LockStrategy = "failed_attempts"
	// LockNone disables automatic locking (locks only via lock_access!).
	LockNone LockStrategy = "none"
)

type Model

type Model interface {
	Get(attr string) any
	Set(attr string, val any)
	Save() error
}

Model is the persistence seam Devise operates on: a single authenticatable resource (a User row in Rails). It is the minimal surface every Devise module needs — read an attribute, write an attribute, and persist. The host wires it to ActiveRecord (in Rails) or to go-embedded-ruby's object model (under rbgo).

Get returns the current value of a database column by its Devise attribute name (see the Attr* constants); a missing / SQL-NULL value is nil. Set stages a new value. Save persists the staged values, mirroring ActiveRecord's save(validate: false) that Devise uses throughout (validation is a separate concern handled by Record.ValidatableErrors).

type PBKDF2KeyGenerator

type PBKDF2KeyGenerator struct {
	Secret     []byte
	Iterations int
	KeySize    int
	Hash       func() hash.Hash
}

PBKDF2KeyGenerator is the byte-faithful port of Rails' ActiveSupport::KeyGenerator: it derives per-salt keys with PBKDF2-HMAC-SHA1 over a shared Secret (Devise.secret_key / the app's secret_key_base). This is the key generator Devise actually wires into Devise::TokenGenerator, so a generator built with the same Secret digests raw tokens to the exact bytes MRI Devise stores — the reset/confirm/unlock digest oracle.

The zero value is not usable (an empty Secret would derive from nothing); build one with NewPBKDF2KeyGenerator. Iterations, KeySize and the HMAC hash are exposed for the rare app that overrides ActiveSupport's defaults, but the defaults (2**16 iterations, 64-byte keys, SHA1) match a stock Rails app.

func NewPBKDF2KeyGenerator

func NewPBKDF2KeyGenerator(secret []byte) PBKDF2KeyGenerator

NewPBKDF2KeyGenerator builds a PBKDF2KeyGenerator over secret with ActiveSupport's defaults (2**16 iterations, 64-byte keys, HMAC-SHA1), matching ActiveSupport::KeyGenerator.new(secret).

func (PBKDF2KeyGenerator) GenerateKey

func (g PBKDF2KeyGenerator) GenerateKey(salt string) []byte

GenerateKey returns PBKDF2-HMAC(Secret, salt, Iterations, KeySize), faithful to ActiveSupport::KeyGenerator#generate_key(salt). Unset Iterations/KeySize/Hash fall back to ActiveSupport's defaults so a bare PBKDF2KeyGenerator{Secret: s} still behaves like the stock generator.

type Record

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

Record is a Devise resource: a Model bound to the Config that governs it, the equivalent of an ActiveRecord instance whose self.class carries the Devise settings. All module logic hangs off methods on *Record.

func New

func New(cfg *Config, m Model) *Record

New binds a model to a config, yielding the resource the module methods act on. A nil cfg uses DefaultConfig.

func NewWithSession

func NewWithSession(cfg *Config, m Model, params map[string]any, _ map[string]any) *Record

NewWithSession initialises a resource from sign-up params, faithful to Registerable.new_with_session: by default it discards the session and assigns the params onto the model, returning the bound Record. An OAuth binding overrides the discard to seed attributes from the session.

func (*Record) AccessLocked

func (r *Record) AccessLocked() bool

AccessLocked reports whether the account is currently locked, faithful to access_locked?: locked_at is set and the lock has not expired.

func (*Record) ActiveForAuthentication

func (r *Record) ActiveForAuthentication() bool

ActiveForAuthentication reports whether an unconfirmed record may still sign in, faithful to Confirmable#active_for_authentication?: confirmed records are always active; unconfirmed ones are active only while [ConfirmationPeriodValid].

func (*Record) AuthenticatableSalt

func (r *Record) AuthenticatableSalt() string

AuthenticatableSalt returns the salt portion of the stored hash — its first 29 characters — faithful to authenticatable_salt. It is "" when no password is set, and feeds Rememberable's cookie value and session invalidation.

func (*Record) Config

func (r *Record) Config() *Config

Config returns the governing config.

func (*Record) Confirm

func (r *Record) Confirm() error

Confirm marks the record confirmed, faithful to confirm: it fails with ErrAlreadyConfirmed when already confirmed and ErrConfirmationPeriodExpired when the token has expired, otherwise sets confirmed_at, clears confirmation_token, and saves.

func (*Record) ConfirmationPeriodExpired

func (r *Record) ConfirmationPeriodExpired() bool

ConfirmationPeriodExpired reports whether the confirmation token has expired, faithful to confirmation_period_expired?: only when confirm_within is set and confirmation_sent_at is older than it.

func (*Record) ConfirmationPeriodValid

func (r *Record) ConfirmationPeriodValid() bool

ConfirmationPeriodValid reports whether the record may still access resources while unconfirmed, faithful to confirmation_period_valid?: true when allow_unconfirmed_access_for is nil (unlimited), else confirmation_sent_at is within that window.

func (*Record) Confirmed

func (r *Record) Confirmed() bool

Confirmed reports whether the record has been confirmed, faithful to confirmed?: confirmed_at is set.

func (*Record) DestroyWithPassword

func (r *Record) DestroyWithPassword(currentPassword string) ([]ValidationError, error)

DestroyWithPassword deletes the record only when currentPassword matches, faithful to destroy_with_password. A blank or wrong current password yields a current_password validation error ("blank" / "invalid") and no deletion. On a match it calls the model's Destroyer; a model that is not a Destroyer yields ErrNotDestroyable.

func (*Record) FailedAttempts

func (r *Record) FailedAttempts() int

FailedAttempts returns the current failed-attempt count (failed_attempts).

func (*Record) ForgetMe

func (r *Record) ForgetMe() error

ForgetMe clears the remember state, faithful to forget_me!: it clears remember_token, clears remember_created_at when expire_all_remember_me_on_sign_out is set, and saves.

func (*Record) GenerateConfirmationToken

func (r *Record) GenerateConfirmationToken() string

GenerateConfirmationToken mints a confirmation token, faithful to generate_confirmation_token: it generates a (raw, enc) pair (unique via the finder), stores the enc digest and confirmation_sent_at, and returns the raw token. It does not save (Devise stages it before the record is persisted).

func (*Record) InactiveMessage

func (r *Record) InactiveMessage() string

InactiveMessage returns the reason an authenticated-but-inactive record is rejected, faithful to inactive_message: "unconfirmed" for a record outside its confirmation grace window, else "inactive".

func (*Record) LockAccess

func (r *Record) LockAccess(sendInstructions bool) error

LockAccess locks the account, faithful to lock_access!: it stamps locked_at, mints an unlock token when the email unlock strategy is enabled, saves, and (when sendInstructions is true and the strategy allows email) invokes the unlock mailer callback.

func (*Record) Model

func (r *Record) Model() Model

Model returns the underlying model.

func (*Record) RememberExpired

func (r *Record) RememberExpired() bool

RememberExpired reports whether the remember cookie has expired, faithful to remember_expired?: true when remember_created_at is nil or older than remember_for.

func (*Record) RememberMe

func (r *Record) RememberMe() error

RememberMe issues a remember token, faithful to remember_me!: it sets remember_token (unique via the finder) if unset, sets remember_created_at if unset, and saves. Devise only saves when the record changed; here it always saves after staging, which is observationally equivalent for a fresh token.

func (*Record) RememberableValue

func (r *Record) RememberableValue() string

RememberableValue is the value stored in the cookie to re-identify the record, faithful to rememberable_value: the remember_token when present, else the authenticatable salt. It is "" only when neither is available.

func (*Record) ResetPassword

func (r *Record) ResetPassword(newPassword string) error

ResetPassword sets a new password and clears the reset token, faithful to reset_password: it stages the hashed password, clears reset_password_token, and saves. It does not itself check the confirmation or period — the class method Config.ResetPasswordByToken orchestrates those, mirroring Devise.

func (*Record) ResetPasswordPeriodValid

func (r *Record) ResetPasswordPeriodValid() bool

ResetPasswordPeriodValid reports whether the stored reset token is still within reset_password_within, faithful to reset_password_period_valid?: false when no token was ever sent.

func (*Record) SendConfirmationInstructions

func (r *Record) SendConfirmationInstructions() (string, error)

SendConfirmationInstructions mints a token, saves it, and invokes the mailer callback, faithful to send_confirmation_instructions. It returns the raw token.

func (*Record) SendResetPasswordInstructions

func (r *Record) SendResetPasswordInstructions() (string, error)

SendResetPasswordInstructions mints a reset token and invokes the configured mailer callback, faithful to send_reset_password_instructions. It returns the raw token (Devise returns it too, for tests).

func (*Record) SetPassword

func (r *Record) SetPassword(newPassword string) error

SetPassword hashes new_password and stages it into encrypted_password, faithful to password=: a blank password is ignored (leaves the current hash untouched, as Devise does with "self.encrypted_password = ... if @password.present?"). It does not save; the caller persists, mirroring ActiveRecord.

func (*Record) SetResetPasswordToken

func (r *Record) SetResetPasswordToken() (string, error)

SetResetPasswordToken mints a reset token, faithful to set_reset_password_token: it generates a (raw, enc) pair via the token generator (unique against reset_password_token through the finder), stores the enc digest and the send timestamp, saves without validation, and returns the raw token to be mailed.

func (*Record) SignInCount

func (r *Record) SignInCount() int

SignInCount returns the number of recorded sign-ins (sign_in_count).

func (*Record) TimedOut

func (r *Record) TimedOut(lastAccess time.Time) bool

TimedOut reports whether a session whose last activity was at lastAccess has timed out, faithful to timedout?: never when timeout_in is nil, otherwise when lastAccess is at or before timeout_in ago. Devise also returns false while a valid remember cookie exists; that short-circuit is the caller's to apply via Record.RememberExpired, since it depends on request state.

func (*Record) TimeoutIn

func (r *Record) TimeoutIn() (time.Duration, bool)

TimeoutIn returns the configured idle window, or false when timeout is disabled (timeout_in nil), faithful to Timeoutable#timeout_in.

func (*Record) UnlockAccess

func (r *Record) UnlockAccess() error

UnlockAccess unlocks the account, faithful to unlock_access!: it clears locked_at, resets failed_attempts to 0, clears unlock_token, and saves.

func (*Record) UpdateTrackedFields

func (r *Record) UpdateTrackedFields(remoteIP string)

UpdateTrackedFields records a sign-in, faithful to update_tracked_fields: it shifts current_sign_in_at into last_sign_in_at (seeding last from current on the first sign-in), sets current_sign_in_at to now, does the same for the IP pair with remoteIP, and increments sign_in_count. It stages the changes without saving, exactly like Devise (the caller saves via Record.UpdateTrackedFieldsAndSave or its own save).

func (*Record) UpdateTrackedFieldsAndSave

func (r *Record) UpdateTrackedFieldsAndSave(remoteIP string) error

UpdateTrackedFieldsAndSave records a sign-in and persists it, faithful to update_tracked_fields! (save(validate: false)).

func (*Record) UpdateWithPassword

func (r *Record) UpdateWithPassword(p UpdateParams) ([]ValidationError, error)

UpdateWithPassword edits the record only when currentPassword matches, faithful to update_with_password. It first drops a blank password (and its blank confirmation) so a user may change their email without touching their password. When the current password verifies it assigns the attributes, stages any new password, validates and saves; otherwise it still assigns and validates the attributes (to surface their errors) and adds a current_password error ("blank" when currentPassword is empty, else "invalid"). It returns the validation failures (empty on success) and a separate error for a save/hash infrastructure failure.

func (*Record) UpdateWithoutPassword

func (r *Record) UpdateWithoutPassword(p UpdateParams) ([]ValidationError, error)

UpdateWithoutPassword edits the record without asking for the current password, faithful to update_without_password: it never changes the password (both password fields are dropped), then assigns the remaining attributes, validates and saves. Returns the validation failures (empty on success) and a separate error for a save failure.

func (*Record) ValidForAuthentication

func (r *Record) ValidForAuthentication(check func() bool) bool

ValidForAuthentication runs the password check through Lockable's gate, faithful to Lockable#valid_for_authentication?. When the failed-attempts strategy is off it is just check(). Otherwise: a successful check on an unlocked account passes; any other outcome increments failed_attempts, locks the account when the threshold is reached (else saves the new count), and fails. It mirrors Devise's return of the check result, gated by the lock.

func (*Record) ValidPassword

func (r *Record) ValidPassword(password string) bool

ValidPassword reports whether password matches the record's stored encrypted_password, faithful to valid_password?: it delegates to Encryptor.Compare with the config's pepper. A record without an encrypted_password never validates.

func (*Record) ValidatableErrors

func (r *Record) ValidatableErrors(p ValidateParams) []ValidationError

ValidatableErrors runs Devise's Validatable checks against the record and the transient params, returning every failure in declaration order (empty when valid). Uniqueness is resolved through the Config.Finder seam: an email is "taken" when the finder returns a record whose id differs from this one — but because the model seam has no identity concept, any finder match on a different Model pointer counts as taken.

type TokenGenerator

type TokenGenerator struct {

	// FriendlyLength is the length passed to [FriendlyToken] for the raw token
	// (Devise uses the default 20).
	FriendlyLength int
	// contains filtered or unexported fields
}

TokenGenerator mints and digests the raw tokens Recoverable and Confirmable persist, faithful to Devise::TokenGenerator. Generate returns a [raw, enc] pair — the raw token is mailed to the user, the enc digest is stored in the database — and Digest reproduces enc from a raw token to look the record up.

func NewDeviseTokenGenerator

func NewDeviseTokenGenerator(secret []byte) *TokenGenerator

NewDeviseTokenGenerator builds the byte-faithful equivalent of Devise.token_generator for a given secret: a TokenGenerator over a CachingKeyGenerator wrapping a PBKDF2KeyGenerator, with HMAC-SHA256 digests. Pass the app's Devise.secret_key (its secret_key_base). The resulting digests are byte-identical to MRI Devise's, so a reset/confirm/unlock digest stored by this library verifies a raw token issued by the gem and vice-versa.

func NewTokenGenerator

func NewTokenGenerator(keyGen KeyGenerator) *TokenGenerator

NewTokenGenerator builds a token generator over keyGen (defaulting to a zero-key HMACKeyGenerator) using HMAC-SHA256 digests, as Devise does.

func (*TokenGenerator) Digest

func (t *TokenGenerator) Digest(column, value string) string

Digest reproduces the stored digest for a raw token value, faithful to Devise::TokenGenerator#digest: an empty value yields "" (Devise returns nil), otherwise the HMAC-SHA256 hexdigest of value under the column key.

func (*TokenGenerator) Generate

func (t *TokenGenerator) Generate(column string, exists func(enc string) bool) (raw, enc string)

Generate returns a fresh (raw, enc) token pair for a column, faithful to Devise::TokenGenerator#generate: it draws a FriendlyToken and digests it, retrying while exists reports the digest is already taken (the uniqueness loop against the column). A nil exists never collides.

type UnlockStrategy

type UnlockStrategy string

UnlockStrategy selects how a locked account may be unlocked, mirroring Devise.unlock_strategy.

const (
	// UnlockTime unlocks automatically after Config.UnlockIn elapses.
	UnlockTime UnlockStrategy = "time"
	// UnlockEmail unlocks via an emailed unlock token.
	UnlockEmail UnlockStrategy = "email"
	// UnlockBoth enables both time and email unlocking (Devise's default).
	UnlockBoth UnlockStrategy = "both"
	// UnlockNone disables automatic unlocking.
	UnlockNone UnlockStrategy = "none"
)

type UpdateParams

type UpdateParams struct {
	Attributes           map[string]any
	Password             string
	PasswordConfirmation string
	CurrentPassword      string
}

UpdateParams carries a registration edit, mirroring the params hash Devise's update_with_password / update_without_password receive. Attributes are the non-password column changes to assign; Password / PasswordConfirmation are the virtual password fields; CurrentPassword is the user's existing password, verified by Record.UpdateWithPassword.

type ValidateParams

type ValidateParams struct {
	// Password / PasswordConfirmation are the virtual password attributes.
	Password             string
	PasswordConfirmation string
	// PasswordProvided reports whether a password was set on this save
	// (Devise's password_required? core: !persisted? || password present).
	PasswordProvided bool
	// EmailChanged reports whether email is being created or modified, gating
	// the format and uniqueness checks (will_save_change_to_email?).
	EmailChanged bool
}

ValidateParams carries the transient attributes Validatable needs that are not database columns: the plaintext password, its confirmation, and whether either the email or the password is being changed on this save. Devise reads these from the model (password / password_confirmation virtual attributes and the will_save_change_to_* dirty-tracking predicates); the host supplies them here.

type ValidationError

type ValidationError struct {
	Attribute string
	Reason    string
}

ValidationError is a single validation failure, mirroring an entry Devise adds to the record's errors: the offending attribute and a symbolic reason matching ActiveModel's error keys (:blank, :invalid, :taken, :too_short, :too_long, :confirmation).

func (ValidationError) Error

func (e ValidationError) Error() string

Error makes ValidationError usable as an error, so ResetPasswordByToken can surface a confirmation mismatch through the error return.

Jump to

Keyboard shortcuts

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