Add complete auth subsystem with OAuth 2.1 authorization server using ory/fosite, local user accounts with bcrypt password hashing, session management, and HTTP handlers for the Web UI. Components: - User store with bcrypt hashing (configurable cost, default 12), CRUD, validation (username 3-64 chars alphanumeric+underscore, password 8-72 bytes) - Session store with secure random IDs, configurable lifetime (default 24h), expiration cleanup, and per-user invalidation - OAuth client store with client_id/secret generation and bcrypt verification - Fosite storage adapter implementing CoreStorage, TokenRevocationStorage, and PKCERequestStorage backed by SQLite - OAuth provider configured with authorization code (PKCE S256 mandatory), client credentials, refresh token rotation, and token introspection - HTTP handlers: POST /auth/register, POST /auth/login, POST /auth/logout, GET /auth/me, PUT /auth/password, GET /oauth/authorize, POST /oauth/token, POST /oauth/introspect - Middleware: RequireSession (cookie), RequireBearer (access token), RequireAuth (either), RequireAdmin (role check) - Structured auth event logging (login, token issuance, session lifecycle) - Schema migration 002_auth.sql extending users, oauth_clients, oauth_tokens tables and adding sessions, oauth_authorization_codes tables - Initial admin user auto-created on first run with random password printed to stdout - All tests pass with CGO_ENABLED=0, zero external runtime dependencies Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
64 lines
1.7 KiB
Go
64 lines
1.7 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"time"
|
|
|
|
"github.com/ory/fosite"
|
|
"github.com/ory/fosite/compose"
|
|
"github.com/ory/fosite/token/hmac"
|
|
)
|
|
|
|
// NewOAuthProvider creates a configured fosite OAuth 2.1 provider.
|
|
// It supports authorization code with PKCE (S256 only), client credentials, and refresh token rotation.
|
|
func NewOAuthProvider(cfg Config, store *FositeStore) fosite.OAuth2Provider {
|
|
secret := cfg.Secret
|
|
if len(secret) < 32 {
|
|
// Generate a random secret if not configured
|
|
secret = make([]byte, 32)
|
|
rand.Read(secret)
|
|
}
|
|
|
|
config := &fosite.Config{
|
|
AccessTokenLifespan: cfg.AccessTokenTTL,
|
|
RefreshTokenLifespan: cfg.RefreshTokenLifetime,
|
|
AuthorizeCodeLifespan: 10 * time.Minute,
|
|
GlobalSecret: secret,
|
|
SendDebugMessagesToClients: cfg.DevMode,
|
|
EnforcePKCE: true,
|
|
EnforcePKCEForPublicClients: true,
|
|
EnablePKCEPlainChallengeMethod: false,
|
|
TokenURL: cfg.IssuerURL + "/oauth/token",
|
|
HashCost: cfg.BcryptCost,
|
|
}
|
|
|
|
// HMACSHAStrategy for token generation
|
|
hmacStrategy := &hmac.HMACStrategy{
|
|
Config: config,
|
|
}
|
|
|
|
_ = hmacStrategy
|
|
|
|
return compose.Compose(
|
|
config,
|
|
store,
|
|
&compose.CommonStrategy{
|
|
CoreStrategy: compose.NewOAuth2HMACStrategy(config),
|
|
},
|
|
compose.OAuth2AuthorizeExplicitFactory,
|
|
compose.OAuth2ClientCredentialsGrantFactory,
|
|
compose.OAuth2RefreshTokenGrantFactory,
|
|
compose.OAuth2PKCEFactory,
|
|
compose.OAuth2TokenIntrospectionFactory,
|
|
)
|
|
}
|
|
|
|
// NewSession creates a new fosite session for a user.
|
|
func NewSession(user *User) fosite.Session {
|
|
return &fositeSession{
|
|
UserID: user.ID,
|
|
Username: user.Username,
|
|
Subject: user.Username,
|
|
}
|
|
}
|