feat: authentication with local accounts, sessions, CSRF, RBAC, and OIDC PKCE (phase 3)
- argon2id (t=3, m=64MiB, p=2) PHC hashing honoring embedded params - token-bucket rate limiting (10/15min per IP+email) on login/register - opaque 32B session tokens in Mongo, 30-day sliding expiry, logout-all - CSRF double-submit cookie/header on authenticated mutations - bootstrap admin from env with forced first-login password change - requireAuth/requireRole middleware with disabled-account enforcement - OIDC code flow with PKCE: lazy discovery, account linking only on verified email, auto-created developer accounts - unit tests (RBAC matrix, CSRF, password, rate limiter) + integration suite covering the full auth matrix incl. an in-test fake IdP Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
// Session is an opaque server-side session (§4.8). The random token is the
|
||||
// document id; the Mongo TTL index on expiresAt reaps stale rows.
|
||||
type Session struct {
|
||||
Token string `bson:"_id"`
|
||||
UserID string `bson:"userId"`
|
||||
CreatedAt time.Time `bson:"createdAt"`
|
||||
ExpiresAt time.Time `bson:"expiresAt"`
|
||||
RefreshedAt time.Time `bson:"refreshedAt"`
|
||||
IP string `bson:"ip"`
|
||||
UA string `bson:"ua"`
|
||||
}
|
||||
|
||||
// SessionTTL is the sliding session lifetime (§7).
|
||||
const SessionTTL = 30 * 24 * time.Hour
|
||||
@@ -0,0 +1,82 @@
|
||||
// Package domain holds the core data model types and business rules shared
|
||||
// across handlers, stores, and workers. It has no Mongo or HTTP dependencies
|
||||
// beyond bson struct tags.
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type Roles struct {
|
||||
Admin bool `bson:"admin" json:"admin"`
|
||||
Consultant bool `bson:"consultant" json:"consultant"`
|
||||
Developer bool `bson:"developer" json:"developer"`
|
||||
}
|
||||
|
||||
// Has reports whether the role named r is set. Known names: admin,
|
||||
// consultant, developer.
|
||||
func (r Roles) Has(role string) bool {
|
||||
switch role {
|
||||
case "admin":
|
||||
return r.Admin
|
||||
case "consultant":
|
||||
return r.Consultant
|
||||
case "developer":
|
||||
return r.Developer
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type LocalAuth struct {
|
||||
PasswordHash string `bson:"passwordHash" json:"-"`
|
||||
MustChange bool `bson:"mustChange" json:"mustChange"`
|
||||
}
|
||||
|
||||
type OIDCAuth struct {
|
||||
Issuer string `bson:"issuer" json:"issuer"`
|
||||
Subject string `bson:"subject" json:"subject"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Local *LocalAuth `bson:"local" json:"local,omitempty"`
|
||||
OIDC *OIDCAuth `bson:"oidc" json:"oidc,omitempty"`
|
||||
}
|
||||
|
||||
type Contact struct {
|
||||
Phone string `bson:"phone" json:"phone"`
|
||||
Location string `bson:"location" json:"location"`
|
||||
Links []string `bson:"links" json:"links"`
|
||||
}
|
||||
|
||||
type NotificationPrefs struct {
|
||||
Email bool `bson:"email" json:"email"`
|
||||
InApp bool `bson:"inApp" json:"inApp"`
|
||||
}
|
||||
|
||||
type UserSettings struct {
|
||||
Theme string `bson:"theme" json:"theme"`
|
||||
Notifications NotificationPrefs `bson:"notifications" json:"notifications"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `bson:"_id" json:"id"`
|
||||
Email string `bson:"email" json:"email"`
|
||||
Name string `bson:"name" json:"name"`
|
||||
AvatarFileID string `bson:"avatarFileId,omitempty" json:"avatarFileId,omitempty"`
|
||||
Bio string `bson:"bio" json:"bio"`
|
||||
Contact Contact `bson:"contact" json:"contact"`
|
||||
Extra map[string]any `bson:"extra" json:"extra"`
|
||||
Roles Roles `bson:"roles" json:"roles"`
|
||||
Auth Auth `bson:"auth" json:"-"`
|
||||
Settings UserSettings `bson:"settings" json:"settings"`
|
||||
Disabled bool `bson:"disabled" json:"disabled"`
|
||||
LastSeenAt time.Time `bson:"lastSeenAt" json:"lastSeenAt"`
|
||||
CreatedAt time.Time `bson:"createdAt" json:"createdAt"`
|
||||
UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"`
|
||||
Version int64 `bson:"version" json:"version"`
|
||||
}
|
||||
|
||||
// MustChangePassword reports whether the user is locked to the
|
||||
// password-change flow.
|
||||
func (u *User) MustChangePassword() bool {
|
||||
return u.Auth.Local != nil && u.Auth.Local.MustChange
|
||||
}
|
||||
Reference in New Issue
Block a user