aboutsummaryrefslogtreecommitdiff
path: root/server/auth/github/github.go
blob: a46876d608bf698a9b46d6c5fb65dce23e79142b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package github

import (
	"context"
	"errors"
	"net/http"
	"time"

	"github.com/nsheridan/cashier/server/config"
	"github.com/nsheridan/cashier/server/metrics"

	githubapi "github.com/google/go-github/github"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/github"
)

const (
	name = "github"
)

// Config is an implementation of `auth.Provider` for authenticating using a
// Github account.
type Config struct {
	config        *oauth2.Config
	orgWhitelist  map[string]bool
	userWhitelist map[string]bool
}

// New creates a new Github provider from a configuration.
func New(c *config.Github) (*Config, error) {
	uw := make(map[string]bool)
	for _, u := range c.UsersWhitelist {
		uw[u] = true
	}
	ow := make(map[string]bool)
	for _, o := range c.OrgsWhitelist {
		ow[o] = true
	}
	if len(uw) == 0 && len(ow) == 0 {
		return nil, errors.New("either GitHub organizations or users whitelist must be specified")
	}
	return &Config{
		config: &oauth2.Config{
			ClientID:     c.OauthClientID,
			ClientSecret: c.OauthClientSecret,
			RedirectURL:  c.OauthCallbackURL,
			Endpoint:     github.Endpoint,
			Scopes: []string{
				string(githubapi.ScopeUser),
				string(githubapi.ScopeReadOrg),
			},
		},
		orgWhitelist:  ow,
		userWhitelist: uw,
	}, nil
}

// A new oauth2 http client.
func (c *Config) newClient(token *oauth2.Token) *http.Client {
	return c.config.Client(oauth2.NoContext, token)
}

// Name returns the name of the provider.
func (c *Config) Name() string {
	return name
}

// Valid validates the oauth token.
func (c *Config) Valid(token *oauth2.Token) bool {
	if c.isUserWhitelisted(token) {
		return true
	}
	if c.isMemberOfWhitelistedOrg(token) {
		return true
	}
	return false
}

func (c *Config) isUserWhitelisted(token *oauth2.Token) bool {
	username := c.Username(token)
	_, ok := c.userWhitelist[username]
	return ok
}

func (c *Config) isMemberOfWhitelistedOrg(token *oauth2.Token) bool {
	client := githubapi.NewClient(c.newClient(token))
	username := c.Username(token)
	for org, _ := range c.orgWhitelist {
		member, _, err := client.Organizations.IsMember(context.TODO(), org, username)
		if err != nil {
			return false
		}
		if member {
			metrics.M.AuthValid.WithLabelValues("github").Inc()
			return true
		}
	}
	return false
}

// StartSession retrieves an authentication endpoint from Github.
func (c *Config) StartSession(state string) string {
	return c.config.AuthCodeURL(state)
}

// Exchange authorizes the session and returns an access token.
func (c *Config) Exchange(code string) (*oauth2.Token, error) {
	t, err := c.config.Exchange(oauth2.NoContext, code)
	if err != nil {
		return nil, err
	}
	// Github tokens don't have an expiry. Set one so that the session expires
	// after a period.
	if t.Expiry.Unix() <= 0 {
		t.Expiry = time.Now().Add(1 * time.Hour)
	}
	metrics.M.AuthExchange.WithLabelValues("github").Inc()
	return t, nil
}

// Username retrieves the username portion of the user's email address.
func (c *Config) Username(token *oauth2.Token) string {
	client := githubapi.NewClient(c.newClient(token))
	u, _, err := client.Users.Get(context.TODO(), "")
	if err != nil {
		return ""
	}
	return *u.Login
}