Skip to content

feat: add hybrid authentication with pluggable providers#12

Merged
appleboy merged 8 commits into
mainfrom
user
Jan 14, 2026
Merged

feat: add hybrid authentication with pluggable providers#12
appleboy merged 8 commits into
mainfrom
user

Conversation

@appleboy
Copy link
Copy Markdown
Member

  • Add support for pluggable authentication providers: local database and external HTTP API
  • Introduce environment variables and configuration for selecting authentication mode and HTTP API settings
  • Implement LocalAuthProvider and HTTPAPIAuthProvider with unified authentication flow
  • Extend user model to include external authentication fields (external ID, auth source, email, full name)
  • Update UserService to route authentication per-user based on auth source and sync external user data
  • Add store methods for upserting and querying users by external ID and auth source
  • Add validation for authentication configuration at startup
  • Update documentation to describe hybrid authentication architecture and configuration options

appleboy and others added 3 commits January 10, 2026 18:57
- Add support for pluggable authentication providers: local database and external HTTP API
- Introduce environment variables and configuration for selecting authentication mode and HTTP API settings
- Implement LocalAuthProvider and HTTPAPIAuthProvider with unified authentication flow
- Extend user model to include external authentication fields (external ID, auth source, email, full name)
- Update UserService to route authentication per-user based on auth source and sync external user data
- Add store methods for upserting and querying users by external ID and auth source
- Add validation for authentication configuration at startup
- Update documentation to describe hybrid authentication architecture and configuration options

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
- Fix minor formatting issue in a markdown table
- Add blank lines for improved readability in several sections

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Resolved conflicts by combining features from both branches:
- Retained PostgreSQL support (DATABASE_DRIVER, DATABASE_DSN) from main
- Retained HTTP API authentication (AUTH_MODE, HTTP_API_*) from user
- Combined environment variables and documentation
- Both features now work together in the codebase

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings January 13, 2026 08:38
@netlify
Copy link
Copy Markdown

netlify Bot commented Jan 13, 2026

Deploy Preview for authgate-demo ready!

Name Link
🔨 Latest commit 8186f2b
🔍 Latest deploy log https://app.netlify.com/projects/authgate-demo/deploys/69675d44fe483b0007a6e425
😎 Deploy Preview https://deploy-preview-12--authgate-demo.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a hybrid authentication system with pluggable providers, enabling the application to authenticate users via either a local database or an external HTTP API. The system supports per-user authentication routing based on each user's auth_source field, allowing for mixed authentication modes within a single deployment.

Changes:

  • Added LocalAuthProvider and HTTPAPIAuthProvider with a unified authentication interface
  • Extended User model with external authentication fields (ExternalID, AuthSource, Email, FullName)
  • Updated UserService to route authentication per-user based on auth_source field and sync external user data automatically
  • Added configuration validation for authentication mode at startup and comprehensive documentation

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
main.go Added authentication provider initialization, configuration validation, and wiring of providers to UserService
internal/store/sqlite.go Added GetUserByExternalID and UpsertExternalUser methods for managing external users
internal/services/user.go Refactored Authenticate method to support per-user authentication routing and external user synchronization
internal/models/user.go Extended User model with ExternalID, AuthSource, Email, and FullName fields, plus IsExternal helper method
internal/config/config.go Added authentication configuration fields (AuthMode, HTTPAPIURL, HTTPAPITimeout, HTTPAPIInsecureSkipVerify) with parsing helpers
internal/auth/types.go Defined AuthResult structure for unified authentication response
internal/auth/local.go Implemented LocalAuthProvider for database-based authentication
internal/auth/http_api.go Implemented HTTPAPIAuthProvider for external HTTP API authentication
internal/auth/errors.go Defined authentication-specific error types
CLAUDE.md Added comprehensive documentation of hybrid authentication architecture and configuration
.env.example Added authentication configuration examples and comments

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/services/user.go Outdated
Comment on lines +93 to +99
fallthrough
default:
if s.localProvider == nil {
return nil, fmt.Errorf("%w: local provider not configured", ErrAuthProviderFailed)
}
providerName = AuthModeLocal
authResult, err = s.localProvider.Authenticate(user.Username, password)
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The switch statement uses fallthrough from AuthModeLocal to default, treating both local and any unrecognized auth_source values the same way. While this provides a safe fallback, it means users with typos or corrupted auth_source values will silently be treated as local users. Consider explicitly handling only "local" and returning an error for unrecognized auth_source values to catch configuration issues early.

Suggested change
fallthrough
default:
if s.localProvider == nil {
return nil, fmt.Errorf("%w: local provider not configured", ErrAuthProviderFailed)
}
providerName = AuthModeLocal
authResult, err = s.localProvider.Authenticate(user.Username, password)
if s.localProvider == nil {
return nil, fmt.Errorf("%w: local provider not configured", ErrAuthProviderFailed)
}
providerName = AuthModeLocal
authResult, err = s.localProvider.Authenticate(user.Username, password)
default:
return nil, fmt.Errorf("%w: unsupported auth source %q", ErrAuthProviderFailed, user.AuthSource)

Copilot uses AI. Check for mistakes.
Comment thread main.go
Comment on lines +291 to +294
case "http_api":
if cfg.HTTPAPIURL == "" {
return errors.New("HTTP_API_URL is required when AUTH_MODE=http_api")
}
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation checks if HTTPAPIURL is empty when AUTH_MODE is http_api, but doesn't validate if it's a valid URL format. An invalid URL like "not-a-url" will pass validation but cause runtime errors when the HTTP API provider attempts to make requests. Consider using url.Parse to validate the URL format during startup configuration validation.

Copilot uses AI. Check for mistakes.
Comment thread internal/auth/http_api.go
Comment thread main.go
Comment thread internal/models/user.go
@@ -9,11 +9,23 @@ type User struct {
Username string `gorm:"uniqueIndex;not null"`
PasswordHash string `gorm:"not null"`
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PasswordHash field has a 'not null' constraint in the GORM tag, but external users are created with an empty password hash (empty string). While an empty string satisfies the 'not null' constraint, this might be confusing. Consider either removing the 'not null' constraint to allow NULL values for external users, or explicitly documenting that external users have an empty string (not NULL) as their password hash.

Suggested change
PasswordHash string `gorm:"not null"`
PasswordHash string `gorm:"not null"` // For external users, this is an empty string (not NULL)

Copilot uses AI. Check for mistakes.
Comment thread internal/store/sqlite.go Outdated
Comment on lines +167 to +183
// User doesn't exist, create new
user = models.User{
ID: uuid.New().String(),
Username: username,
PasswordHash: "", // No local password for external users
Role: "user",
ExternalID: externalID,
AuthSource: authSource,
Email: email,
FullName: fullName,
}

if err := s.db.Create(&user).Error; err != nil {
return nil, err
}

return &user, nil
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UpsertExternalUser function doesn't check for username conflicts when creating a new user. If an external user has the same username as an existing local user, the database insert will fail due to the unique constraint on the username field. Consider checking for existing usernames and either returning a more specific error or appending a suffix to prevent conflicts.

Copilot uses AI. Check for mistakes.
Comment thread internal/store/sqlite.go
Comment on lines +156 to +164
if err == nil {
// User exists, update fields
user.Username = username
user.Email = email
user.FullName = fullName
if err := s.db.Save(&user).Error; err != nil {
return nil, err
}
return &user, nil
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When updating an existing external user during authentication, only the username, email, and full name are synced. The ExternalID is not updated. If an external system reassigns external IDs, this could lead to inconsistencies. Consider whether ExternalID should also be updated during sync, or document that external IDs are immutable.

Copilot uses AI. Check for mistakes.
Comment thread internal/auth/http_api.go
Comment thread internal/auth/http_api.go Outdated
Comment thread internal/config/config.go
Comment on lines +87 to +93
func getEnvDuration(key string, defaultValue time.Duration) time.Duration {
if value := os.Getenv(key); value != "" {
if d, err := time.ParseDuration(value); err == nil {
return d
}
}
return defaultValue
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getEnvDuration function silently falls back to the default value when it encounters an invalid duration format, without logging any warning. If a user misconfigures HTTP_API_TIMEOUT with an invalid value like "10seconds" instead of "10s", the application will start successfully but use the default 10s timeout without notifying the user of the configuration error. Consider logging a warning when duration parsing fails.

Copilot uses AI. Check for mistakes.
- Add context.Context parameter to authentication methods across providers and services
- Update authentication calls to pass context from HTTP requests
- Refactor internal service logic to propagate context during user authentication

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
- Refactor Authenticate method signatures to use multi-line parameter formatting for improved readability

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
- Improve error handling in HTTP API authentication by checking status codes and parsing error messages from JSON responses
- Add truncation for large error response bodies to prevent log overflow
- Include Email and FullName fields in local authentication response

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
- Document the required and optional fields for HTTP API authentication responses
- Enforce validation that user_id must be present when authentication succeeds
- Add comprehensive tests for HTTP API authentication, covering success, missing user_id, authentication failure, non-2xx responses, and invalid JSON cases

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
- Add detailed handling and documentation for username conflicts during external authentication
- Implement error propagation for username conflicts in user service and store layers
- Update login handler to display specific error message for username conflicts
- Add tests for username conflict scenarios when creating or updating external users
- Improve error reporting for user creation and update failures in the store
- Create a new error definitions file for store-level errors

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
@appleboy appleboy merged commit 721417a into main Jan 14, 2026
19 of 20 checks passed
@appleboy appleboy deleted the user branch January 14, 2026 09:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants