package oidc_provider import ( "context" "encoding/json" "fmt" "net/http" "strconv" "golang.org/x/oauth2" ) const ( GoogleIssuer = "https://accounts.google.com" AppleIssuer = "https://appleid.apple.com" ) // NewGoogleProvider creates a Provider configured for Google Sign-In. func NewGoogleProvider(ctx context.Context, clientID, clientSecret, redirectURL string, deps Deps) (*Provider, error) { return New(ctx, Config{ Name: "google", Issuer: GoogleIssuer, ClientID: clientID, ClientSecret: clientSecret, RedirectURL: redirectURL, Scopes: []string{"openid", "email", "profile"}, AuthCodeOptions: []oauth2.AuthCodeOption{ oauth2.AccessTypeOffline, }, LoginPath: "/login", SuccessPath: "/", }, deps) } // NewAppleProvider creates a Provider configured for Sign in with Apple. // The clientSecret should be a JWT signed with your Apple private key. func NewAppleProvider(ctx context.Context, clientID, clientSecret, redirectURL string, deps Deps) (*Provider, error) { return New(ctx, Config{ Name: "apple", Issuer: AppleIssuer, ClientID: clientID, ClientSecret: clientSecret, RedirectURL: redirectURL, Scopes: []string{"openid", "email", "name"}, AuthCodeOptions: []oauth2.AuthCodeOption{ oauth2.SetAuthURLParam("response_mode", "form_post"), }, LoginPath: "/login", SuccessPath: "/", }, deps) } // NewGitHubProvider creates a Provider configured for GitHub OAuth. func NewGitHubProvider(_ context.Context, clientID, clientSecret, redirectURL string, deps Deps) (*Provider, error) { return New(context.Background(), Config{ Name: "github", ClientID: clientID, ClientSecret: clientSecret, RedirectURL: redirectURL, Scopes: []string{"user:email"}, Endpoint: &oauth2.Endpoint{ AuthURL: "https://github.com/login/oauth/authorize", TokenURL: "https://github.com/login/oauth/access_token", }, FetchUserInfo: githubFetchUserInfo, LoginPath: "/login", SuccessPath: "/", }, deps) } // githubFetchUserInfo calls the GitHub API to get user identity. func githubFetchUserInfo(ctx context.Context, token *oauth2.Token, _ string) (*UserInfo, error) { // Get user profile req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/user", nil) if err != nil { return nil, fmt.Errorf("creating user request: %w", err) } req.Header.Set("Authorization", "Bearer "+token.AccessToken) req.Header.Set("Accept", "application/vnd.github+json") resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("fetching user: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("github user API returned %d", resp.StatusCode) } var ghUser struct { ID int `json:"id"` Email string `json:"email"` } if err := json.NewDecoder(resp.Body).Decode(&ghUser); err != nil { return nil, fmt.Errorf("decoding user response: %w", err) } email := ghUser.Email // GitHub may return null email if the user's email is private. // Fetch from the emails endpoint instead. if email == "" { email, err = githubFetchPrimaryEmail(ctx, token) if err != nil { return nil, err } } return &UserInfo{ Subject: strconv.Itoa(ghUser.ID), Email: email, EmailVerified: true, }, nil } // githubFetchPrimaryEmail fetches the user's primary verified email from GitHub. func githubFetchPrimaryEmail(ctx context.Context, token *oauth2.Token) (string, error) { req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/user/emails", nil) if err != nil { return "", fmt.Errorf("creating emails request: %w", err) } req.Header.Set("Authorization", "Bearer "+token.AccessToken) req.Header.Set("Accept", "application/vnd.github+json") resp, err := http.DefaultClient.Do(req) if err != nil { return "", fmt.Errorf("fetching emails: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("github emails API returned %d", resp.StatusCode) } 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 "", fmt.Errorf("decoding emails response: %w", err) } for _, e := range emails { if e.Primary && e.Verified { return e.Email, nil } } return "", fmt.Errorf("no verified primary email found") }