mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-05-21 11:35:26 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0308644075 | ||
|
|
e5984f5205 |
@@ -10,6 +10,7 @@ import (
|
|||||||
// Login and Logout are delegated to the primary authenticator.
|
// Login and Logout are delegated to the primary authenticator.
|
||||||
type ChainAuthenticator struct {
|
type ChainAuthenticator struct {
|
||||||
authenticators []Authenticator
|
authenticators []Authenticator
|
||||||
|
authenticateCallback func(r *http.Request) (*UserContext, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChainAuthenticator creates a ChainAuthenticator from the given authenticators.
|
// NewChainAuthenticator creates a ChainAuthenticator from the given authenticators.
|
||||||
@@ -29,13 +30,28 @@ func (c *ChainAuthenticator) Authenticate(r *http.Request) (*UserContext, error)
|
|||||||
lastErr = err
|
lastErr = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if c.authenticateCallback != nil {
|
||||||
|
return c.authenticateCallback(r)
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("all authenticators failed; last error: %w", lastErr)
|
return nil, fmt.Errorf("all authenticators failed; last error: %w", lastErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ChainAuthenticator) SetAuthenticateCallback(fn func(r *http.Request) (*UserContext, error)) {
|
||||||
|
c.authenticateCallback = fn
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ChainAuthenticator) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
|
func (c *ChainAuthenticator) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
|
||||||
return c.authenticators[0].Login(ctx, req)
|
return c.authenticators[0].Login(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ChainAuthenticator) LoginWithCookie(ctx context.Context, req LoginRequest, w http.ResponseWriter) (*LoginResponse, error) {
|
||||||
|
return c.authenticators[0].LoginWithCookie(ctx, req, w)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ChainAuthenticator) Logout(ctx context.Context, req LogoutRequest) error {
|
func (c *ChainAuthenticator) Logout(ctx context.Context, req LogoutRequest) error {
|
||||||
return c.authenticators[0].Logout(ctx, req)
|
return c.authenticators[0].Logout(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ChainAuthenticator) LogoutWithCookie(ctx context.Context, req LogoutRequest, w http.ResponseWriter) error {
|
||||||
|
return c.authenticators[0].LogoutWithCookie(ctx, req, w)
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,10 +25,20 @@ func (s *stubAuthenticator) Login(_ context.Context, _ LoginRequest) (*LoginResp
|
|||||||
return &LoginResponse{Token: "tok"}, nil
|
return &LoginResponse{Token: "tok"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *stubAuthenticator) LoginWithCookie(ctx context.Context, req LoginRequest, _ http.ResponseWriter) (*LoginResponse, error) {
|
||||||
|
return s.Login(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *stubAuthenticator) Logout(_ context.Context, _ LogoutRequest) error {
|
func (s *stubAuthenticator) Logout(_ context.Context, _ LogoutRequest) error {
|
||||||
return s.err
|
return s.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *stubAuthenticator) LogoutWithCookie(ctx context.Context, req LogoutRequest, _ http.ResponseWriter) error {
|
||||||
|
return s.Logout(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubAuthenticator) SetAuthenticateCallback(_ func(r *http.Request) (*UserContext, error)) {}
|
||||||
|
|
||||||
func TestChainAuthenticator_Authenticate(t *testing.T) {
|
func TestChainAuthenticator_Authenticate(t *testing.T) {
|
||||||
successCtx := &UserContext{UserID: 42, UserName: "alice"}
|
successCtx := &UserContext{UserID: 42, UserName: "alice"}
|
||||||
failStub := &stubAuthenticator{err: fmt.Errorf("no token")}
|
failStub := &stubAuthenticator{err: fmt.Errorf("no token")}
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ func (c *CompositeSecurityProvider) Authenticate(r *http.Request) (*UserContext,
|
|||||||
return c.auth.Authenticate(r)
|
return c.auth.Authenticate(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetAuthenticateCallback delegates to the authenticator
|
||||||
|
func (c *CompositeSecurityProvider) SetAuthenticateCallback(fn func(r *http.Request) (*UserContext, error)) {
|
||||||
|
c.auth.SetAuthenticateCallback(fn)
|
||||||
|
}
|
||||||
|
|
||||||
// GetColumnSecurity delegates to the column security provider
|
// GetColumnSecurity delegates to the column security provider
|
||||||
func (c *CompositeSecurityProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]ColumnSecurity, error) {
|
func (c *CompositeSecurityProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]ColumnSecurity, error) {
|
||||||
return c.colSec.GetColumnSecurity(ctx, userID, schema, table)
|
return c.colSec.GetColumnSecurity(ctx, userID, schema, table)
|
||||||
|
|||||||
@@ -23,14 +23,24 @@ func (m *mockAuth) Login(ctx context.Context, req LoginRequest) (*LoginResponse,
|
|||||||
return m.loginResp, m.loginErr
|
return m.loginResp, m.loginErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockAuth) LoginWithCookie(ctx context.Context, req LoginRequest, _ http.ResponseWriter) (*LoginResponse, error) {
|
||||||
|
return m.Login(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockAuth) Logout(ctx context.Context, req LogoutRequest) error {
|
func (m *mockAuth) Logout(ctx context.Context, req LogoutRequest) error {
|
||||||
return m.logoutErr
|
return m.logoutErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockAuth) LogoutWithCookie(ctx context.Context, req LogoutRequest, _ http.ResponseWriter) error {
|
||||||
|
return m.Logout(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockAuth) Authenticate(r *http.Request) (*UserContext, error) {
|
func (m *mockAuth) Authenticate(r *http.Request) (*UserContext, error) {
|
||||||
return m.authUser, m.authErr
|
return m.authUser, m.authErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockAuth) SetAuthenticateCallback(_ func(r *http.Request) (*UserContext, error)) {}
|
||||||
|
|
||||||
// Optional interface implementations
|
// Optional interface implementations
|
||||||
func (m *mockAuth) RefreshToken(ctx context.Context, refreshToken string) (*LoginResponse, error) {
|
func (m *mockAuth) RefreshToken(ctx context.Context, refreshToken string) (*LoginResponse, error) {
|
||||||
if !m.supportsRefresh {
|
if !m.supportsRefresh {
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ type Authenticator interface {
|
|||||||
// Authenticate extracts and validates user from HTTP request
|
// Authenticate extracts and validates user from HTTP request
|
||||||
// Returns UserContext or error if authentication fails
|
// Returns UserContext or error if authentication fails
|
||||||
Authenticate(r *http.Request) (*UserContext, error)
|
Authenticate(r *http.Request) (*UserContext, error)
|
||||||
|
|
||||||
|
// SetAuthenticateCallback registers a fallback called when primary authentication fails.
|
||||||
|
// If the callback returns a non-nil UserContext, that result is used instead of the error.
|
||||||
|
SetAuthenticateCallback(fn func(r *http.Request) (*UserContext, error))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registrable allows providers to support user registration
|
// Registrable allows providers to support user registration
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
type KeyStoreAuthenticator struct {
|
type KeyStoreAuthenticator struct {
|
||||||
keyStore KeyStore
|
keyStore KeyStore
|
||||||
keyType KeyType // empty = accept any type
|
keyType KeyType // empty = accept any type
|
||||||
|
authenticateCallback func(r *http.Request) (*UserContext, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewKeyStoreAuthenticator creates a KeyStoreAuthenticator.
|
// NewKeyStoreAuthenticator creates a KeyStoreAuthenticator.
|
||||||
@@ -32,21 +33,42 @@ func (a *KeyStoreAuthenticator) Login(_ context.Context, _ LoginRequest) (*Login
|
|||||||
return nil, fmt.Errorf("keystore authenticator does not support login")
|
return nil, fmt.Errorf("keystore authenticator does not support login")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoginWithCookie is not supported for keystore authentication.
|
||||||
|
func (a *KeyStoreAuthenticator) LoginWithCookie(_ context.Context, _ LoginRequest, _ http.ResponseWriter) (*LoginResponse, error) {
|
||||||
|
return nil, fmt.Errorf("keystore authenticator does not support login")
|
||||||
|
}
|
||||||
|
|
||||||
// Logout is not supported for keystore authentication.
|
// Logout is not supported for keystore authentication.
|
||||||
func (a *KeyStoreAuthenticator) Logout(_ context.Context, _ LogoutRequest) error {
|
func (a *KeyStoreAuthenticator) Logout(_ context.Context, _ LogoutRequest) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogoutWithCookie is not supported for keystore authentication.
|
||||||
|
func (a *KeyStoreAuthenticator) LogoutWithCookie(_ context.Context, _ LogoutRequest, _ http.ResponseWriter) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAuthenticateCallback registers a fallback called when key authentication fails.
|
||||||
|
func (a *KeyStoreAuthenticator) SetAuthenticateCallback(fn func(r *http.Request) (*UserContext, error)) {
|
||||||
|
a.authenticateCallback = fn
|
||||||
|
}
|
||||||
|
|
||||||
// Authenticate extracts an API key from the request and validates it against the KeyStore.
|
// Authenticate extracts an API key from the request and validates it against the KeyStore.
|
||||||
// Returns a UserContext built from the matching UserKey on success.
|
// Returns a UserContext built from the matching UserKey on success.
|
||||||
func (a *KeyStoreAuthenticator) Authenticate(r *http.Request) (*UserContext, error) {
|
func (a *KeyStoreAuthenticator) Authenticate(r *http.Request) (*UserContext, error) {
|
||||||
rawKey := extractAPIKey(r)
|
rawKey := extractAPIKey(r)
|
||||||
if rawKey == "" {
|
if rawKey == "" {
|
||||||
|
if a.authenticateCallback != nil {
|
||||||
|
return a.authenticateCallback(r)
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("API key required (Authorization: Bearer/ApiKey <key> or X-API-Key header)")
|
return nil, fmt.Errorf("API key required (Authorization: Bearer/ApiKey <key> or X-API-Key header)")
|
||||||
}
|
}
|
||||||
|
|
||||||
userKey, err := a.keyStore.ValidateKey(r.Context(), rawKey, a.keyType)
|
userKey, err := a.keyStore.ValidateKey(r.Context(), rawKey, a.keyType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if a.authenticateCallback != nil {
|
||||||
|
return a.authenticateCallback(r)
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("invalid API key: %w", err)
|
return nil, fmt.Errorf("invalid API key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,14 +22,24 @@ func (m *mockSecurityProvider) Login(ctx context.Context, req LoginRequest) (*Lo
|
|||||||
return m.loginResponse, m.loginError
|
return m.loginResponse, m.loginError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockSecurityProvider) LoginWithCookie(ctx context.Context, req LoginRequest, _ http.ResponseWriter) (*LoginResponse, error) {
|
||||||
|
return m.Login(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockSecurityProvider) Logout(ctx context.Context, req LogoutRequest) error {
|
func (m *mockSecurityProvider) Logout(ctx context.Context, req LogoutRequest) error {
|
||||||
return m.logoutError
|
return m.logoutError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockSecurityProvider) LogoutWithCookie(ctx context.Context, req LogoutRequest, _ http.ResponseWriter) error {
|
||||||
|
return m.Logout(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockSecurityProvider) Authenticate(r *http.Request) (*UserContext, error) {
|
func (m *mockSecurityProvider) Authenticate(r *http.Request) (*UserContext, error) {
|
||||||
return m.authUser, m.authError
|
return m.authUser, m.authError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockSecurityProvider) SetAuthenticateCallback(_ func(r *http.Request) (*UserContext, error)) {}
|
||||||
|
|
||||||
func (m *mockSecurityProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]ColumnSecurity, error) {
|
func (m *mockSecurityProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]ColumnSecurity, error) {
|
||||||
return m.columnSecurity, nil
|
return m.columnSecurity, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ type DatabaseAuthenticator struct {
|
|||||||
|
|
||||||
// Passkey provider (optional)
|
// Passkey provider (optional)
|
||||||
passkeyProvider PasskeyProvider
|
passkeyProvider PasskeyProvider
|
||||||
|
|
||||||
|
// Optional fallback called when primary authentication fails
|
||||||
|
authenticateCallback func(r *http.Request) (*UserContext, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DatabaseAuthenticatorOptions configures the database authenticator
|
// DatabaseAuthenticatorOptions configures the database authenticator
|
||||||
@@ -113,6 +116,10 @@ type DatabaseAuthenticatorOptions struct {
|
|||||||
// CookieOptions configures the session cookie written by LoginWithCookie.
|
// CookieOptions configures the session cookie written by LoginWithCookie.
|
||||||
// Only used when EnableCookieSession is true.
|
// Only used when EnableCookieSession is true.
|
||||||
CookieOptions SessionCookieOptions
|
CookieOptions SessionCookieOptions
|
||||||
|
// AuthenticateCallback is a fallback called when the primary authentication (database
|
||||||
|
// session lookup) fails. If non-nil and the callback returns a non-nil UserContext,
|
||||||
|
// that result is used in place of the failure.
|
||||||
|
AuthenticateCallback func(r *http.Request) (*UserContext, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDatabaseAuthenticator(db *sql.DB) *DatabaseAuthenticator {
|
func NewDatabaseAuthenticator(db *sql.DB) *DatabaseAuthenticator {
|
||||||
@@ -142,6 +149,7 @@ func NewDatabaseAuthenticatorWithOptions(db *sql.DB, opts DatabaseAuthenticatorO
|
|||||||
passkeyProvider: opts.PasskeyProvider,
|
passkeyProvider: opts.PasskeyProvider,
|
||||||
enableCookieSession: opts.EnableCookieSession,
|
enableCookieSession: opts.EnableCookieSession,
|
||||||
cookieOptions: opts.CookieOptions,
|
cookieOptions: opts.CookieOptions,
|
||||||
|
authenticateCallback: opts.AuthenticateCallback,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +189,10 @@ func (a *DatabaseAuthenticator) runDBOpWithReconnect(run func(*sql.DB) error) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *DatabaseAuthenticator) SetAuthenticateCallback(fn func(r *http.Request) (*UserContext, error)) {
|
||||||
|
a.authenticateCallback = fn
|
||||||
|
}
|
||||||
|
|
||||||
func (a *DatabaseAuthenticator) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
|
func (a *DatabaseAuthenticator) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
|
||||||
// Convert LoginRequest to JSON
|
// Convert LoginRequest to JSON
|
||||||
reqJSON, err := json.Marshal(req)
|
reqJSON, err := json.Marshal(req)
|
||||||
@@ -345,6 +357,9 @@ func (a *DatabaseAuthenticator) Authenticate(r *http.Request) (*UserContext, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(tokens) == 0 {
|
if len(tokens) == 0 {
|
||||||
|
if a.authenticateCallback != nil {
|
||||||
|
return a.authenticateCallback(r)
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("session token required")
|
return nil, fmt.Errorf("session token required")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,7 +422,10 @@ func (a *DatabaseAuthenticator) Authenticate(r *http.Request) (*UserContext, err
|
|||||||
return &userCtx, nil
|
return &userCtx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// All tokens failed
|
// All tokens failed — try callback before returning error
|
||||||
|
if a.authenticateCallback != nil {
|
||||||
|
return a.authenticateCallback(r)
|
||||||
|
}
|
||||||
if lastErr != nil {
|
if lastErr != nil {
|
||||||
return nil, lastErr
|
return nil, lastErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -511,6 +511,10 @@ func TestDatabaseAuthenticator(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("authenticate with cookie", func(t *testing.T) {
|
t.Run("authenticate with cookie", func(t *testing.T) {
|
||||||
|
cookieAuth := NewDatabaseAuthenticatorWithOptions(db, DatabaseAuthenticatorOptions{
|
||||||
|
EnableCookieSession: true,
|
||||||
|
})
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/test", nil)
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
req.AddCookie(&http.Cookie{
|
req.AddCookie(&http.Cookie{
|
||||||
Name: "session_token",
|
Name: "session_token",
|
||||||
@@ -524,7 +528,7 @@ func TestDatabaseAuthenticator(t *testing.T) {
|
|||||||
WithArgs("cookie-token-456", "cookie").
|
WithArgs("cookie-token-456", "cookie").
|
||||||
WillReturnRows(rows)
|
WillReturnRows(rows)
|
||||||
|
|
||||||
userCtx, err := auth.Authenticate(req)
|
userCtx, err := cookieAuth.Authenticate(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error, got %v", err)
|
t.Fatalf("expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,14 +43,24 @@ func (m *MockAuthenticator) Login(ctx context.Context, req security.LoginRequest
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockAuthenticator) LoginWithCookie(ctx context.Context, req security.LoginRequest, _ http.ResponseWriter) (*security.LoginResponse, error) {
|
||||||
|
return m.Login(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MockAuthenticator) Logout(ctx context.Context, req security.LogoutRequest) error {
|
func (m *MockAuthenticator) Logout(ctx context.Context, req security.LogoutRequest) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockAuthenticator) LogoutWithCookie(ctx context.Context, req security.LogoutRequest, _ http.ResponseWriter) error {
|
||||||
|
return m.Logout(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MockAuthenticator) Authenticate(r *http.Request) (*security.UserContext, error) {
|
func (m *MockAuthenticator) Authenticate(r *http.Request) (*security.UserContext, error) {
|
||||||
return m.users["testuser"], nil
|
return m.users["testuser"], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockAuthenticator) SetAuthenticateCallback(_ func(r *http.Request) (*security.UserContext, error)) {}
|
||||||
|
|
||||||
func TestTwoFactorAuthenticator_Setup(t *testing.T) {
|
func TestTwoFactorAuthenticator_Setup(t *testing.T) {
|
||||||
baseAuth := NewMockAuthenticator()
|
baseAuth := NewMockAuthenticator()
|
||||||
provider := security.NewMemoryTwoFactorProvider(nil)
|
provider := security.NewMemoryTwoFactorProvider(nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user