package auth import ( "context" "encoding/json" "fmt" "net/http" "golang.org/x/oauth2" "golang.org/x/oauth2/github" "golang.org/x/oauth2/google" ) // OAuthProvider represents an OAuth2 provider type OAuthProvider string const ( ProviderGitHub OAuthProvider = "github" ProviderGoogle OAuthProvider = "google" ) // OAuthUser contains user information from OAuth provider type OAuthUser struct { Provider OAuthProvider ID string Email string Name string Avatar string } // OAuthManager handles OAuth2 authentication type OAuthManager struct { githubConfig *oauth2.Config googleConfig *oauth2.Config } // NewOAuthManager creates a new OAuth manager func NewOAuthManager(baseURL, githubClientID, githubClientSecret, googleClientID, googleClientSecret string) *OAuthManager { m := &OAuthManager{} if githubClientID != "" && githubClientSecret != "" { m.githubConfig = &oauth2.Config{ ClientID: githubClientID, ClientSecret: githubClientSecret, Endpoint: github.Endpoint, Scopes: []string{"read:user", "user:email"}, RedirectURL: baseURL + "/v1/auth/oauth/github/callback", } } if googleClientID != "" && googleClientSecret != "" { m.googleConfig = &oauth2.Config{ ClientID: googleClientID, ClientSecret: googleClientSecret, Endpoint: google.Endpoint, Scopes: []string{"openid", "email", "profile"}, RedirectURL: baseURL + "/v1/auth/oauth/google/callback", } } return m } // GetAuthURL returns the OAuth authorization URL for the given provider func (m *OAuthManager) GetAuthURL(provider OAuthProvider, state string) (string, error) { config, err := m.getConfig(provider) if err != nil { return "", err } return config.AuthCodeURL(state, oauth2.AccessTypeOffline), nil } // Exchange exchanges an authorization code for user information func (m *OAuthManager) Exchange(ctx context.Context, provider OAuthProvider, code string) (*OAuthUser, error) { config, err := m.getConfig(provider) if err != nil { return nil, err } token, err := config.Exchange(ctx, code) if err != nil { return nil, fmt.Errorf("exchange code: %w", err) } return m.fetchUserInfo(ctx, provider, token) } func (m *OAuthManager) getConfig(provider OAuthProvider) (*oauth2.Config, error) { switch provider { case ProviderGitHub: if m.githubConfig == nil { return nil, fmt.Errorf("github oauth not configured") } return m.githubConfig, nil case ProviderGoogle: if m.googleConfig == nil { return nil, fmt.Errorf("google oauth not configured") } return m.googleConfig, nil default: return nil, fmt.Errorf("unknown provider: %s", provider) } } func (m *OAuthManager) fetchUserInfo(ctx context.Context, provider OAuthProvider, token *oauth2.Token) (*OAuthUser, error) { switch provider { case ProviderGitHub: return m.fetchGitHubUser(ctx, token) case ProviderGoogle: return m.fetchGoogleUser(ctx, token) default: return nil, fmt.Errorf("unknown provider: %s", provider) } } func (m *OAuthManager) fetchGitHubUser(ctx context.Context, token *oauth2.Token) (*OAuthUser, error) { client := m.githubConfig.Client(ctx, token) // Fetch user info resp, err := client.Get("https://api.github.com/user") if err != nil { return nil, fmt.Errorf("fetch user: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("github api returned %d", resp.StatusCode) } var ghUser struct { ID int64 `json:"id"` Login string `json:"login"` Name string `json:"name"` Email string `json:"email"` AvatarURL string `json:"avatar_url"` } if err := json.NewDecoder(resp.Body).Decode(&ghUser); err != nil { return nil, fmt.Errorf("decode user: %w", err) } // If email not public, fetch from emails endpoint email := ghUser.Email if email == "" { email, _ = m.fetchGitHubEmail(ctx, client) } name := ghUser.Name if name == "" { name = ghUser.Login } return &OAuthUser{ Provider: ProviderGitHub, ID: fmt.Sprintf("%d", ghUser.ID), Email: email, Name: name, Avatar: ghUser.AvatarURL, }, nil } func (m *OAuthManager) fetchGitHubEmail(ctx context.Context, client *http.Client) (string, error) { resp, err := client.Get("https://api.github.com/user/emails") if err != nil { return "", err } defer resp.Body.Close() var emails []struct { Email string `json:"email"` Primary bool `json:"primary"` Verified bool `json:"verified"` } if err := json.NewDecoder(resp.Body).Decode(&emails); err != nil { return "", err } // Find primary verified email for _, e := range emails { if e.Primary && e.Verified { return e.Email, nil } } // Fall back to any verified email for _, e := range emails { if e.Verified { return e.Email, nil } } return "", nil } func (m *OAuthManager) fetchGoogleUser(ctx context.Context, token *oauth2.Token) (*OAuthUser, error) { client := m.googleConfig.Client(ctx, token) resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo") if err != nil { return nil, fmt.Errorf("fetch user: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("google api returned %d", resp.StatusCode) } var gUser struct { ID string `json:"id"` Email string `json:"email"` VerifiedEmail bool `json:"verified_email"` Name string `json:"name"` Picture string `json:"picture"` } if err := json.NewDecoder(resp.Body).Decode(&gUser); err != nil { return nil, fmt.Errorf("decode user: %w", err) } return &OAuthUser{ Provider: ProviderGoogle, ID: gUser.ID, Email: gUser.Email, Name: gUser.Name, Avatar: gUser.Picture, }, nil } // IsGitHubConfigured returns true if GitHub OAuth is configured func (m *OAuthManager) IsGitHubConfigured() bool { return m.githubConfig != nil } // IsGoogleConfigured returns true if Google OAuth is configured func (m *OAuthManager) IsGoogleConfigured() bool { return m.googleConfig != nil }