lastlogin

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

README

github.com/ubgo/auth/plugin/lastlogin

Remembers how each user last signed in — password, a provider, a magic link, a code, a passkey — so a sign-in page can put the method the person used last in front, and so an app can see which methods its users actually use.

Install

go get github.com/ubgo/auth

The package is part of the core module, so it adds no dependency.

Wire it

From examples/last-login:

a, err := auth.New(
	auth.WithDB(db, sqlb.SQLiteDialect{}),
	auth.WithBaseURL(baseURL),
	auth.WithPlugins(
		email.New(email.Login(), email.Recover(), email.WithMailer(m)),
		password.New(),
		magiclink.New(),
		lastlogin.New(),
	),
)

The plugin has no route (two under WithCookie) and no required option. It records on a data hook on session creation, which every sign-in method ends with, so a method added later is recorded without knowing this plugin exists.

Options

Option What it does Default
WithResolver(fn) Names the method from the operation that created the session and the session itself; "" records nothing for that sign-in (Resolver) DefaultResolver: the plugin that ran the operation (password, oauth, magiclink, email, phone, passkey, anonymous, …)
WithCookie(name, maxAge) Also keeps the method in a cookie, better-auth's default: every sign-in that records a method sets it ("" takes DefaultCookieName, last_used_login_method, sent by authhttp as __Host-auth_last_used_login_method; 0 takes DefaultCookieMaxAge, 30 days), and adds the routes below. The cookie has the core's fixed attributes, HttpOnly included, where better-auth's is script-readable: a script reads it through GET /lastlogin/method. auth.New refuses a name outside auth.CookieNamePattern and a negative age (ErrBadCookieAge) no cookie
WithCookieConsent(fn) Asks fn(ctx) before each cookie (better-auth's beforeStoreCookie); false skips the cookie for that sign-in, the database record is still written always set under WithCookie

Go API

Method What it returns
Of(ctx, userID) LastLoginColumns{LastLoginMethod, LastLoginAt}; the method is "" and the time nil for a user who never signed in since the column existed

Without WithCookie there is no HTTP route: the sign-in page reads the hint from the user row (lastlogin.Bindings in a composite user, or Of). With it, a page served before any session exists reads the cookie: server-side through auth.RequestCookie(ctx, lastlogin.DefaultCookieName), or from a script:

Route Requirement What it does
GET /lastlogin/method Public {"method":"password"} from this browser's cookie, "" without one
POST /lastlogin/clear Public Deletes the cookie (better-auth's clearLastUsedLoginMethod)

The cookie follows the database record's rules: none for the promotion after a second factor (the first step set it), none for an impersonation. It holds a method name, never an identifier, so it tells someone at this browser how it last signed in, not who.

What is recorded

Session Recorded
Any sign-in or sign-up operation that creates a session yes: the resolver's name and the session's creation time
A pending second-factor session yes: the first factor was used; the promotion that follows (a PendingSecondFactor operation) is skipped so it does not overwrite it
An impersonation by an admin no: not the user's own sign-in
A session made from Go outside any operation (a.CreateSession) no: there is no operation to name
A hand-off exchange (plugin/handoff) yes, as handoff; resolve it to "" with WithResolver to keep the original method

Tables and columns

The plugin owns no table. It adds two columns to auth_user:

Column Type Meaning
last_login_method text, default '' the method of the last sign-in
last_login_at timestamp, null when it happened

Read them in a composite user through lastlogin.LastLoginColumns and lastlogin.Bindings, or in your own queries through lastlogin.Method and lastlogin.At.

Errors

The plugin returns none of its own; Of returns auth.ErrUserNotFound for an unknown id and auth.ErrOperationNotWired when the plugin was not passed to auth.New.

Security notes

Property Pinned by
Password, magic link and email code each record their method and time; a Go-made session records nothing; a user with no sign-in reads empty integration/TestLastLogin_RecordsTheMethodOfEverySignIn
A pending session records the first factor, the promotion keeps it, an impersonation records nothing integration/TestLastLogin_PendingKeepsTheFirstFactorAndImpersonationIsSkipped
WithResolver names the method or skips it integration/TestLastLogin_Resolver
Every database failure fails the sign-in integration/TestIdentFaults_LastLogin

Limits:

  • The method is a hint for the person who owns the account, read by the app after a session exists or from the user row. Shown to a visitor before sign-in it tells them which method an address uses; keep it behind the app's own rule, as better-auth's consent hook does.

Example

Documentation

Overview

Package lastlogin remembers how each user last signed in — password, a provider, a magic link, a code, a passkey — so a sign-in page can put the method the person used last in front, and so an app can see which methods its users actually use.

It is one column on auth_user, written by a data hook on session creation, which every sign-in method ends with. The method's name is the plugin that ran the operation, so a method added later is recorded without knowing this plugin exists; WithResolver names it differently when the app wants the provider rather than "oauth".

Index

Constants

View Source
const (
	// ColMethod is the method of the last sign-in: a plugin name such as
	// "password", "oauth", "magiclink", "phone", "passkey", "anonymous", or
	// what WithResolver returned. Empty for a user who never signed in since
	// the column existed.
	//ds:def id=colmethod-7q2nv8kx owner=@khanakia stability=stable
	ColMethod = "last_login_method"
	// ColAt is when that sign-in happened.
	ColAt = "last_login_at"
)

Columns this plugin adds to auth_user.

View Source
const (
	// DefaultCookieName is the plugin cookie's name; authhttp sends it as
	// __Host-auth_last_used_login_method, beside the core's cookies.
	//ds:def id=lastlogin-defaultcookiename-5qk8nw2v owner=@khanakia stability=stable
	DefaultCookieName = "last_used_login_method"
	// DefaultCookieMaxAge is 30 days, better-auth's default: the hint is
	// for the next sign-in, which may be weeks away.
	//ds:def id=lastlogin-defaultcookiemaxage-8wr3mt6q owner=@khanakia stability=stable
	DefaultCookieMaxAge = 30 * 24 * time.Hour
)

The cookie (WithCookie), better-auth's last_used_login_method hint.

View Source
const (
	EndpointMethod auth.EndpointID = "lastlogin.method"
	EndpointClear  auth.EndpointID = "lastlogin.clear"
)

Operation ids of the cookie routes, served only under WithCookie.

View Source
const PluginName = "lastlogin"

PluginName is the plugin's permanent identity: migration owner.

Variables

Typed columns on auth_user, for an app that reports on sign-in methods in its own queries.

View Source
var ErrBadCookieAge = errors.New("lastlogin: WithCookie needs a maxAge of zero (the default) or more")

ErrBadCookieAge is a configuration error from auth.New: WithCookie was given a negative maxAge, a cookie that would be deleted as it is set.

Functions

func Bindings

func Bindings(c *LastLoginColumns) []sqlb.Binding

Bindings binds LastLoginColumns for a query on auth_user.

func DefaultResolver

func DefaultResolver(_ context.Context, op auth.OpInfo, _ auth.Session) string

DefaultResolver names the method after the plugin that ran the operation: "password", "oauth", "magiclink", … The plugin is the first segment of the operation id ("<plugin>.<name>").

Types

type ConsentFunc

type ConsentFunc func(ctx context.Context) bool

ConsentFunc decides, per sign-in, whether the cookie may be set, such as after the visitor accepted the app's cookie banner: better-auth's beforeStoreCookie. The database record does not ask: it is the app's own data about its user, not a cookie in the visitor's browser.

type LastLoginColumns

type LastLoginColumns struct {
	LastLoginMethod string
	LastLoginAt     *time.Time
}

LastLoginColumns is this plugin's flat part of a composite user (the columns are on auth_user, ADR-0001). The name stutters on purpose: Go names an embedded field after its type, and two plugins exporting Columns could not be embedded together.

type MethodResult

type MethodResult struct {
	Method string `json:"method"`
}

MethodResult is the answer of GET lastlogin/method: the method this browser last signed in with, or "" when its cookie is absent.

type Option

type Option func(*Plugin)

Option configures the plugin; options are applied in order by New.

func WithCookie

func WithCookie(name string, maxAge time.Duration) Option

WithCookie also keeps the last method in a cookie, better-auth's default behaviour: every sign-in that records a method sets it for maxAge (0 takes DefaultCookieMaxAge), under name ("" takes DefaultCookieName; it must match auth.CookieNamePattern). A login page reads it before anyone is signed in: server-side through auth.RequestCookie, a script through GET <mount>/lastlogin/method, and POST <mount>/lastlogin/clear forgets it (better-auth's clearLastUsedLoginMethod).

One deliberate difference: better-auth's cookie is readable by page scripts (httpOnly false). Here every plugin cookie has the core's fixed attributes, HttpOnly among them, so no plugin can weaken the cookie rules; the route is how a script reads it. The value is a method name only, never an identifier, so it says how someone on this browser signed in, not who.

func WithCookieConsent

func WithCookieConsent(fn ConsentFunc) Option

WithCookieConsent asks fn before each cookie is set (better-auth's beforeStoreCookie); false skips the cookie for that sign-in only.

func WithResolver

func WithResolver(fn Resolver) Option

WithResolver replaces DefaultResolver, for an app that wants "google" rather than "oauth", or that records nothing for some operations.

type Plugin

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

Plugin records the last sign-in method. Build it with New.

func New

func New(opts ...Option) *Plugin

New builds the plugin.

func (*Plugin) Columns

func (p *Plugin) Columns() []auth.ColumnExtension

Columns adds the method and its time to auth_user.

func (*Plugin) DataHooks

func (p *Plugin) DataHooks() auth.DataHooks

DataHooks record the method on every session a sign-in operation creates.

A pending second-factor session counts: the method was used, and the promotion that follows (a PendingSecondFactor operation) is skipped so the second step does not overwrite the first. An impersonation is not the user's own sign-in, and a session created outside an operation (a. CreateSession from Go) has no method to record.

func (*Plugin) Init

func (p *Plugin) Init(a *auth.Auth) error

Init captures the core handle the hook writes through.

func (*Plugin) Name

func (p *Plugin) Name() string

Name returns PluginName, the key the core registers this plugin under.

func (*Plugin) Of

func (p *Plugin) Of(ctx context.Context, userID string) (LastLoginColumns, error)

Of returns how and when userID last signed in; the method is "" when nothing was recorded.

func (*Plugin) OperationHooks

func (p *Plugin) OperationHooks() []auth.OperationHook

OperationHooks adds, under WithCookie, the after-hook that sets the cookie for the session a sign-in issued.

func (*Plugin) Operations

func (p *Plugin) Operations() []auth.Operation

Operations lists the cookie routes under WithCookie, and nothing otherwise: the database record needs no route.

func (*Plugin) OptionErr

func (p *Plugin) OptionErr() error

OptionErr is every error the options recorded, such as auth.ErrNilOption naming an option given nil; auth.New refuses to start while it is not nil (auth.OptionChecker).

func (*Plugin) Validate

func (p *Plugin) Validate(*auth.Config) error

Validate refuses a cookie name the core would refuse at the first sign-in, and a negative lifetime.

type Resolver

type Resolver func(ctx context.Context, op auth.OpInfo, s auth.Session) string

Resolver names the method of a sign-in from the operation that created the session and the session itself. It returns "" to record nothing for this sign-in.

Jump to

Keyboard shortcuts

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