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}