mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-06-06 05:43:46 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5984f5205 | |||
| 76909ae869 | |||
| c90c2984ac |
@@ -25,10 +25,18 @@ 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 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")}
|
||||||
|
|||||||
@@ -43,11 +43,21 @@ func (c *CompositeSecurityProvider) Login(ctx context.Context, req LoginRequest)
|
|||||||
return c.auth.Login(ctx, req)
|
return c.auth.Login(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoginWithCookie delegates to the authenticator
|
||||||
|
func (c *CompositeSecurityProvider) LoginWithCookie(ctx context.Context, req LoginRequest, w http.ResponseWriter) (*LoginResponse, error) {
|
||||||
|
return c.auth.LoginWithCookie(ctx, req, w)
|
||||||
|
}
|
||||||
|
|
||||||
// Logout delegates to the authenticator
|
// Logout delegates to the authenticator
|
||||||
func (c *CompositeSecurityProvider) Logout(ctx context.Context, req LogoutRequest) error {
|
func (c *CompositeSecurityProvider) Logout(ctx context.Context, req LogoutRequest) error {
|
||||||
return c.auth.Logout(ctx, req)
|
return c.auth.Logout(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogoutWithCookie delegates to the authenticator
|
||||||
|
func (c *CompositeSecurityProvider) LogoutWithCookie(ctx context.Context, req LogoutRequest, w http.ResponseWriter) error {
|
||||||
|
return c.auth.LogoutWithCookie(ctx, req, w)
|
||||||
|
}
|
||||||
|
|
||||||
// Authenticate delegates to the authenticator
|
// Authenticate delegates to the authenticator
|
||||||
func (c *CompositeSecurityProvider) Authenticate(r *http.Request) (*UserContext, error) {
|
func (c *CompositeSecurityProvider) Authenticate(r *http.Request) (*UserContext, error) {
|
||||||
return c.auth.Authenticate(r)
|
return c.auth.Authenticate(r)
|
||||||
|
|||||||
@@ -23,10 +23,18 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,9 +83,19 @@ type Authenticator interface {
|
|||||||
// Login authenticates credentials and returns a token
|
// Login authenticates credentials and returns a token
|
||||||
Login(ctx context.Context, req LoginRequest) (*LoginResponse, error)
|
Login(ctx context.Context, req LoginRequest) (*LoginResponse, error)
|
||||||
|
|
||||||
|
// LoginWithCookie authenticates credentials and, when cookie sessions are enabled,
|
||||||
|
// writes the session cookie to w. Implementations that do not support cookies
|
||||||
|
// should delegate to Login and ignore w.
|
||||||
|
LoginWithCookie(ctx context.Context, req LoginRequest, w http.ResponseWriter) (*LoginResponse, error)
|
||||||
|
|
||||||
// Logout invalidates a user's session/token
|
// Logout invalidates a user's session/token
|
||||||
Logout(ctx context.Context, req LogoutRequest) error
|
Logout(ctx context.Context, req LogoutRequest) error
|
||||||
|
|
||||||
|
// LogoutWithCookie invalidates a user's session/token and, when cookie sessions are
|
||||||
|
// enabled, clears the session cookie on w. Implementations that do not support cookies
|
||||||
|
// should delegate to Logout and ignore w.
|
||||||
|
LogoutWithCookie(ctx context.Context, req LogoutRequest, w http.ResponseWriter) error
|
||||||
|
|
||||||
// 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)
|
||||||
|
|||||||
@@ -22,10 +22,18 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-10
@@ -30,10 +30,18 @@ func (a *HeaderAuthenticator) Login(ctx context.Context, req LoginRequest) (*Log
|
|||||||
return nil, fmt.Errorf("header authentication does not support login")
|
return nil, fmt.Errorf("header authentication does not support login")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *HeaderAuthenticator) LoginWithCookie(ctx context.Context, req LoginRequest, w http.ResponseWriter) (*LoginResponse, error) {
|
||||||
|
return a.Login(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *HeaderAuthenticator) Logout(ctx context.Context, req LogoutRequest) error {
|
func (a *HeaderAuthenticator) Logout(ctx context.Context, req LogoutRequest) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *HeaderAuthenticator) LogoutWithCookie(ctx context.Context, req LogoutRequest, w http.ResponseWriter) error {
|
||||||
|
return a.Logout(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *HeaderAuthenticator) Authenticate(r *http.Request) (*UserContext, error) {
|
func (a *HeaderAuthenticator) Authenticate(r *http.Request) (*UserContext, error) {
|
||||||
userIDStr := r.Header.Get("X-User-ID")
|
userIDStr := r.Header.Get("X-User-ID")
|
||||||
if userIDStr == "" {
|
if userIDStr == "" {
|
||||||
@@ -70,6 +78,10 @@ type DatabaseAuthenticator struct {
|
|||||||
cacheTTL time.Duration
|
cacheTTL time.Duration
|
||||||
sqlNames *SQLNames
|
sqlNames *SQLNames
|
||||||
|
|
||||||
|
// Cookie session support (optional, gated by enableCookieSession)
|
||||||
|
enableCookieSession bool
|
||||||
|
cookieOptions SessionCookieOptions
|
||||||
|
|
||||||
// OAuth2 providers registry (multiple providers supported)
|
// OAuth2 providers registry (multiple providers supported)
|
||||||
oauth2Providers map[string]*OAuth2Provider
|
oauth2Providers map[string]*OAuth2Provider
|
||||||
oauth2ProvidersMutex sync.RWMutex
|
oauth2ProvidersMutex sync.RWMutex
|
||||||
@@ -93,6 +105,14 @@ type DatabaseAuthenticatorOptions struct {
|
|||||||
// DBFactory is called to obtain a fresh *sql.DB when the existing connection is closed.
|
// DBFactory is called to obtain a fresh *sql.DB when the existing connection is closed.
|
||||||
// If nil, reconnection is disabled.
|
// If nil, reconnection is disabled.
|
||||||
DBFactory func() (*sql.DB, error)
|
DBFactory func() (*sql.DB, error)
|
||||||
|
// EnableCookieSession enables cookie-based session management.
|
||||||
|
// When true, Authenticate reads the session token from the cookie named by
|
||||||
|
// CookieOptions.Name (default "session_token") in addition to the Authorization header,
|
||||||
|
// and LoginWithCookie / LogoutWithCookie automatically set / clear the cookie.
|
||||||
|
EnableCookieSession bool
|
||||||
|
// CookieOptions configures the session cookie written by LoginWithCookie.
|
||||||
|
// Only used when EnableCookieSession is true.
|
||||||
|
CookieOptions SessionCookieOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDatabaseAuthenticator(db *sql.DB) *DatabaseAuthenticator {
|
func NewDatabaseAuthenticator(db *sql.DB) *DatabaseAuthenticator {
|
||||||
@@ -114,12 +134,14 @@ func NewDatabaseAuthenticatorWithOptions(db *sql.DB, opts DatabaseAuthenticatorO
|
|||||||
sqlNames := MergeSQLNames(DefaultSQLNames(), opts.SQLNames)
|
sqlNames := MergeSQLNames(DefaultSQLNames(), opts.SQLNames)
|
||||||
|
|
||||||
return &DatabaseAuthenticator{
|
return &DatabaseAuthenticator{
|
||||||
db: db,
|
db: db,
|
||||||
dbFactory: opts.DBFactory,
|
dbFactory: opts.DBFactory,
|
||||||
cache: cacheInstance,
|
cache: cacheInstance,
|
||||||
cacheTTL: opts.CacheTTL,
|
cacheTTL: opts.CacheTTL,
|
||||||
sqlNames: sqlNames,
|
sqlNames: sqlNames,
|
||||||
passkeyProvider: opts.PasskeyProvider,
|
passkeyProvider: opts.PasskeyProvider,
|
||||||
|
enableCookieSession: opts.EnableCookieSession,
|
||||||
|
cookieOptions: opts.CookieOptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +287,33 @@ func (a *DatabaseAuthenticator) Logout(ctx context.Context, req LogoutRequest) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoginWithCookie performs a login and, when EnableCookieSession is true, writes the
|
||||||
|
// session cookie to w using the configured CookieOptions. The LoginResponse is returned
|
||||||
|
// regardless of whether cookie sessions are enabled.
|
||||||
|
func (a *DatabaseAuthenticator) LoginWithCookie(ctx context.Context, req LoginRequest, w http.ResponseWriter) (*LoginResponse, error) {
|
||||||
|
resp, err := a.Login(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if a.enableCookieSession {
|
||||||
|
SetSessionCookie(w, resp, a.cookieOptions)
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogoutWithCookie performs a logout and, when EnableCookieSession is true, clears the
|
||||||
|
// session cookie on w. The logout itself is performed regardless of the cookie flag.
|
||||||
|
func (a *DatabaseAuthenticator) LogoutWithCookie(ctx context.Context, req LogoutRequest, w http.ResponseWriter) error {
|
||||||
|
err := a.Logout(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if a.enableCookieSession {
|
||||||
|
ClearSessionCookie(w, a.cookieOptions)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *DatabaseAuthenticator) Authenticate(r *http.Request) (*UserContext, error) {
|
func (a *DatabaseAuthenticator) Authenticate(r *http.Request) (*UserContext, error) {
|
||||||
// Extract session token from header or cookie
|
// Extract session token from header or cookie
|
||||||
sessionToken := r.Header.Get("Authorization")
|
sessionToken := r.Header.Get("Authorization")
|
||||||
@@ -272,10 +321,11 @@ func (a *DatabaseAuthenticator) Authenticate(r *http.Request) (*UserContext, err
|
|||||||
var tokens []string
|
var tokens []string
|
||||||
|
|
||||||
if sessionToken == "" {
|
if sessionToken == "" {
|
||||||
// Try cookie
|
if a.enableCookieSession {
|
||||||
if token := GetSessionCookie(r); token != "" {
|
if token := GetSessionCookie(r, a.cookieOptions); token != "" {
|
||||||
tokens = []string{token}
|
tokens = []string{token}
|
||||||
reference = "cookie"
|
reference = "cookie"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Parse Authorization header which may contain multiple comma-separated tokens
|
// Parse Authorization header which may contain multiple comma-separated tokens
|
||||||
@@ -583,6 +633,14 @@ func (a *JWTAuthenticator) Logout(ctx context.Context, req LogoutRequest) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *JWTAuthenticator) LoginWithCookie(ctx context.Context, req LoginRequest, w http.ResponseWriter) (*LoginResponse, error) {
|
||||||
|
return a.Login(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *JWTAuthenticator) LogoutWithCookie(ctx context.Context, req LogoutRequest, w http.ResponseWriter) error {
|
||||||
|
return a.Logout(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *JWTAuthenticator) Authenticate(r *http.Request) (*UserContext, error) {
|
func (a *JWTAuthenticator) Authenticate(r *http.Request) (*UserContext, error) {
|
||||||
authHeader := r.Header.Get("Authorization")
|
authHeader := r.Header.Get("Authorization")
|
||||||
if authHeader == "" {
|
if authHeader == "" {
|
||||||
|
|||||||
@@ -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,10 +43,18 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user