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.
42 lines
1.1 KiB
Go
42 lines
1.1 KiB
Go
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
|
|
}
|