Add GitHub OAuth login with provider registry
Implement OAuth 2.0 login via GitHub as an alternative to email/password. Uses a provider registry pattern (internal/auth/oauth/) so adding Google or other providers later requires only a new Provider implementation. Flow: GET /v1/auth/oauth/github redirects to GitHub, callback exchanges the code for a user profile, upserts the user + team atomically, and redirects to the frontend with a JWT token. Key changes: - Migration: make password_hash nullable, add oauth_providers table - Provider registry with GitHubProvider (profile + email fallback) - CSRF state cookie with HMAC-SHA256 validation - Race-safe registration (23505 collision retries as login) - Startup validation: CP_PUBLIC_URL required when OAuth is configured Not fully tested — needs integration tests with a real GitHub OAuth app and end-to-end testing with the frontend callback page.
This commit is contained in:
41
internal/auth/oauth/provider.go
Normal file
41
internal/auth/oauth/provider.go
Normal file
@ -0,0 +1,41 @@
|
||||
package oauth
|
||||
|
||||
import "context"
|
||||
|
||||
// UserProfile is the normalized user info returned by an OAuth provider.
|
||||
type UserProfile struct {
|
||||
ProviderID string
|
||||
Email string
|
||||
Name string
|
||||
}
|
||||
|
||||
// Provider abstracts an OAuth 2.0 identity provider.
|
||||
type Provider interface {
|
||||
// Name returns the provider identifier (e.g. "github", "google").
|
||||
Name() string
|
||||
// AuthCodeURL returns the URL to redirect the user to for authorization.
|
||||
AuthCodeURL(state string) string
|
||||
// Exchange trades an authorization code for a user profile.
|
||||
Exchange(ctx context.Context, code string) (UserProfile, error)
|
||||
}
|
||||
|
||||
// Registry maps provider names to Provider implementations.
|
||||
type Registry struct {
|
||||
providers map[string]Provider
|
||||
}
|
||||
|
||||
// NewRegistry creates an empty provider registry.
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{providers: make(map[string]Provider)}
|
||||
}
|
||||
|
||||
// Register adds a provider to the registry.
|
||||
func (r *Registry) Register(p Provider) {
|
||||
r.providers[p.Name()] = p
|
||||
}
|
||||
|
||||
// Get looks up a provider by name.
|
||||
func (r *Registry) Get(name string) (Provider, bool) {
|
||||
p, ok := r.providers[name]
|
||||
return p, ok
|
||||
}
|
||||
Reference in New Issue
Block a user