github.go

 1package oauth
 2
 3import (
 4	"context"
 5	"encoding/json"
 6	"fmt"
 7	"net/http"
 8
 9	"golang.org/x/oauth2"
10	"golang.org/x/oauth2/github"
11)
12
13var _ Provider = &GitHub{}
14
15// GitHub implements Provider for GitHub OAuth2.
16// It uses the standard authorization-code flow (not the device flow used by
17// the bridge) because the webui has a browser redirect available.
18type GitHub struct {
19	clientID     string
20	clientSecret string
21}
22
23func NewGitHub(clientID, clientSecret string) *GitHub {
24	return &GitHub{clientID: clientID, clientSecret: clientSecret}
25}
26
27func (g *GitHub) Name() string      { return "github" }
28func (g *GitHub) HumanName() string { return "GitHub" }
29
30func (g *GitHub) config(callbackURL string) *oauth2.Config {
31	return &oauth2.Config{
32		ClientID:     g.clientID,
33		ClientSecret: g.clientSecret,
34		Endpoint:     github.Endpoint,
35		RedirectURL:  callbackURL,
36		// read:user for profile; user:email to get the primary email even when
37		// the user's email is set to private on their GitHub profile.
38		Scopes: []string{"read:user", "user:email"},
39	}
40}
41
42func (g *GitHub) AuthURL(state, callbackURL string) string {
43	return g.config(callbackURL).AuthCodeURL(state, oauth2.AccessTypeOnline)
44}
45
46func (g *GitHub) Exchange(ctx context.Context, code, callbackURL string) (*UserInfo, error) {
47	token, err := g.config(callbackURL).Exchange(ctx, code)
48	if err != nil {
49		return nil, fmt.Errorf("github: token exchange: %w", err)
50	}
51
52	client := g.config(callbackURL).Client(ctx, token)
53	resp, err := client.Get("https://api.github.com/user")
54	if err != nil {
55		return nil, fmt.Errorf("github: fetch profile: %w", err)
56	}
57	defer resp.Body.Close()
58
59	if resp.StatusCode != http.StatusOK {
60		return nil, fmt.Errorf("github: unexpected status %d from /user", resp.StatusCode)
61	}
62
63	var u struct {
64		Login     string `json:"login"`
65		Email     string `json:"email"`
66		Name      string `json:"name"`
67		AvatarURL string `json:"avatar_url"`
68	}
69	if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
70		return nil, fmt.Errorf("github: decode profile: %w", err)
71	}
72
73	return &UserInfo{
74		Login:     u.Login,
75		Email:     u.Email,
76		Name:      u.Name,
77		AvatarURL: u.AvatarURL,
78	}, nil
79}