diff options
author | Ben Burwell <ben@benburwell.com> | 2020-04-13 23:57:13 -0400 |
---|---|---|
committer | Ben Burwell <ben@benburwell.com> | 2020-04-13 23:57:13 -0400 |
commit | 2ce3b86e0ff69538935db3149d1ed2f24aea09a3 (patch) | |
tree | 1c0329a5c1191690e57e7160bd3150c9a2851866 /server/auth | |
parent | 8b1ee3e95010681d98d1b31af98f0ce0832cedd2 (diff) |
Simplify
Diffstat (limited to 'server/auth')
-rw-r--r-- | server/auth/github/github.go | 70 | ||||
-rw-r--r-- | server/auth/gitlab/gitlab.go | 225 | ||||
-rw-r--r-- | server/auth/gitlab/gitlab_test.go | 98 | ||||
-rw-r--r-- | server/auth/google/google.go | 135 | ||||
-rw-r--r-- | server/auth/google/google_test.go | 75 | ||||
-rw-r--r-- | server/auth/microsoft/microsoft.go | 201 | ||||
-rw-r--r-- | server/auth/microsoft/microsoft_test.go | 72 | ||||
-rw-r--r-- | server/auth/provider.go | 13 | ||||
-rw-r--r-- | server/auth/testprovider/testprovider.go | 56 |
9 files changed, 36 insertions, 909 deletions
diff --git a/server/auth/github/github.go b/server/auth/github/github.go index 38009e1..a46876d 100644 --- a/server/auth/github/github.go +++ b/server/auth/github/github.go @@ -6,7 +6,6 @@ import ( "net/http" "time" - "github.com/nsheridan/cashier/server/auth" "github.com/nsheridan/cashier/server/config" "github.com/nsheridan/cashier/server/metrics" @@ -22,21 +21,23 @@ const ( // Config is an implementation of `auth.Provider` for authenticating using a // Github account. type Config struct { - config *oauth2.Config - organization string - whitelist map[string]bool + config *oauth2.Config + orgWhitelist map[string]bool + userWhitelist map[string]bool } -var _ auth.Provider = (*Config)(nil) - // New creates a new Github provider from a configuration. -func New(c *config.Auth) (*Config, error) { +func New(c *config.Github) (*Config, error) { uw := make(map[string]bool) for _, u := range c.UsersWhitelist { uw[u] = true } - if c.ProviderOpts["organization"] == "" && len(uw) == 0 { - return nil, errors.New("either GitHub organization or users whitelist must be specified") + 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{ @@ -49,8 +50,8 @@ func New(c *config.Auth) (*Config, error) { string(githubapi.ScopeReadOrg), }, }, - organization: c.ProviderOpts["organization"], - whitelist: uw, + orgWhitelist: ow, + userWhitelist: uw, }, nil } @@ -66,34 +67,35 @@ func (c *Config) Name() string { // Valid validates the oauth token. func (c *Config) Valid(token *oauth2.Token) bool { - if len(c.whitelist) > 0 && !c.whitelist[c.Username(token)] { - return false - } - if !token.Valid() { - return false - } - if c.organization == "" { - // There's no organization and the token is valid. Can only reach here - // if there's a user whitelist set and the user is in the whitelist. - metrics.M.AuthValid.WithLabelValues("github").Inc() + if c.isUserWhitelisted(token) { return true } - client := githubapi.NewClient(c.newClient(token)) - member, _, err := client.Organizations.IsMember(context.TODO(), c.organization, c.Username(token)) - if err != nil { - return false - } - if member { - metrics.M.AuthValid.WithLabelValues("github").Inc() + if c.isMemberOfWhitelistedOrg(token) { + return true } - return member + return false +} + +func (c *Config) isUserWhitelisted(token *oauth2.Token) bool { + username := c.Username(token) + _, ok := c.userWhitelist[username] + return ok } -// Revoke is a no-op revoke method. GitHub doesn't seem to allow token -// revocation - tokens are indefinite and there are no refresh options etc. -// Returns nil to satisfy the Provider interface. -func (c *Config) Revoke(token *oauth2.Token) error { - return nil +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. diff --git a/server/auth/gitlab/gitlab.go b/server/auth/gitlab/gitlab.go deleted file mode 100644 index 70d3d1c..0000000 --- a/server/auth/gitlab/gitlab.go +++ /dev/null @@ -1,225 +0,0 @@ -package gitlab - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "log" - "net/http" - "strconv" - - "github.com/nsheridan/cashier/server/config" - "github.com/nsheridan/cashier/server/metrics" - - "golang.org/x/oauth2" -) - -const ( - name = "gitlab" -) - -// Config is an implementation of `auth.Provider` for authenticating using a -// Gitlab account. -type Config struct { - config *oauth2.Config - group string - whitelist map[string]bool - allusers bool - apiurl string - log bool -} - -// Note on Gitlab REST API calls. We don't parse errors because it's -// kind of a pain: -// https://gitlab.com/help/api/README.md#data-validation-and-error-reporting -// The two v4 api calls used are /user and /groups/:group/members/:uid -// https://gitlab.com/help/api/users.md#for-normal-users-1 -// https://gitlab.com/help/api/members.md#get-a-member-of-a-group-or-project -type serviceUser struct { - ID int `json:"id"` - Username string `json:"username"` - Email string `json:"email"` -} - -type serviceGroupMember struct { - ID int `json:"id"` - State string `json:"state"` - AccessLevel int `json:"access_level"` -} - -func (c *Config) logMsg(message error) { - if c.log { - log.Print(message) - } -} - -// A new oauth2 http client. -func (c *Config) newClient(token *oauth2.Token) *http.Client { - return c.config.Client(oauth2.NoContext, token) -} - -func (c *Config) getURL(token *oauth2.Token, url string) (*bytes.Buffer, error) { - client := c.newClient(token) - resp, err := client.Get(url) - if err != nil { - return nil, fmt.Errorf("Failed to get groups: %s", err) - } - defer resp.Body.Close() - var body bytes.Buffer - io.Copy(&body, resp.Body) - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Gitlab error(http: %d) getting %s: '%s'", - resp.StatusCode, url, body.String()) - } - return &body, nil -} - -// Gets info on the current user. -func (c *Config) getUser(token *oauth2.Token) *serviceUser { - url := c.apiurl + "user" - body, err := c.getURL(token, url) - if err != nil { - c.logMsg(err) - return nil - } - var user serviceUser - if err := json.NewDecoder(body).Decode(&user); err != nil { - c.logMsg(fmt.Errorf("Failed to decode user (%s): %s", url, err)) - return nil - } - return &user -} - -// Gets current user group membership info. -func (c *Config) checkGroupMembership(token *oauth2.Token, uid int, group string) bool { - url := fmt.Sprintf("%sgroups/%s/members/%d", c.apiurl, group, uid) - body, err := c.getURL(token, url) - if err != nil { - c.logMsg(err) - return false - } - var m serviceGroupMember - if err := json.NewDecoder(body).Decode(&m); err != nil { - c.logMsg(fmt.Errorf("Failed to parse groups (%s): %s", url, err)) - return false - } - return m.ID == uid -} - -// New creates a new Gitlab provider from a configuration. -func New(c *config.Auth) (*Config, error) { - logOpt, _ := strconv.ParseBool(c.ProviderOpts["log"]) - uw := make(map[string]bool) - for _, u := range c.UsersWhitelist { - uw[u] = true - } - allUsers, _ := strconv.ParseBool(c.ProviderOpts["allusers"]) - if !allUsers && c.ProviderOpts["group"] == "" && len(uw) == 0 { - return nil, errors.New("gitlab_opts group and the users whitelist must not be both empty if allusers isn't true") - } - siteURL := "https://gitlab.com/" - if c.ProviderOpts["siteurl"] != "" { - siteURL = c.ProviderOpts["siteurl"] - if siteURL[len(siteURL)-1] != '/' { - return nil, errors.New("gitlab_opts siteurl must end in /") - } - } else { - if allUsers { - return nil, errors.New("gitlab_opts if allusers is set, siteurl must be set") - } - } - // TODO: Should make sure siteURL is just the host bit. - oauth2.RegisterBrokenAuthHeaderProvider(siteURL) - - return &Config{ - config: &oauth2.Config{ - ClientID: c.OauthClientID, - ClientSecret: c.OauthClientSecret, - RedirectURL: c.OauthCallbackURL, - Endpoint: oauth2.Endpoint{ - AuthURL: siteURL + "oauth/authorize", - TokenURL: siteURL + "oauth/token", - }, - Scopes: []string{ - "api", - }, - }, - group: c.ProviderOpts["group"], - whitelist: uw, - allusers: allUsers, - apiurl: siteURL + "api/v4/", - log: logOpt, - }, nil -} - -// 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 !token.Valid() { - log.Printf("Auth fail (oauth2 Valid failure)") - return false - } - if c.allusers { - log.Printf("Auth success (allusers)") - metrics.M.AuthValid.WithLabelValues("gitlab").Inc() - return true - } - u := c.getUser(token) - if u == nil { - return false - } - if len(c.whitelist) > 0 && !c.whitelist[c.Username(token)] { - c.logMsg(errors.New("Auth fail (not in whitelist)")) - return false - } - if c.group == "" { - // There's no group and token is valid. Can only reach - // here if user whitelist is set and user is in whitelist. - c.logMsg(errors.New("Auth success (no groups specified in server config)")) - metrics.M.AuthValid.WithLabelValues("gitlab").Inc() - return true - } - if !c.checkGroupMembership(token, u.ID, c.group) { - c.logMsg(errors.New("Auth failure (not in allowed group)")) - return false - } - metrics.M.AuthValid.WithLabelValues("gitlab").Inc() - c.logMsg(errors.New("Auth success (in allowed group)")) - return true -} - -// Revoke is a no-op revoke method. Gitlab doesn't allow token -// revocation - tokens live for an hour. -// Returns nil to satisfy the Provider interface. -func (c *Config) Revoke(token *oauth2.Token) error { - return nil -} - -// StartSession retrieves an authentication endpoint from Gitlab. -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 { - metrics.M.AuthExchange.WithLabelValues("gitlab").Inc() - } - return t, err -} - -// Username retrieves the username of the Gitlab user. -func (c *Config) Username(token *oauth2.Token) string { - u := c.getUser(token) - if u == nil { - return "" - } - return u.Username -} diff --git a/server/auth/gitlab/gitlab_test.go b/server/auth/gitlab/gitlab_test.go deleted file mode 100644 index 93b348b..0000000 --- a/server/auth/gitlab/gitlab_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package gitlab - -import ( - "fmt" - "testing" - - "github.com/nsheridan/cashier/server/auth" - "github.com/nsheridan/cashier/server/config" - "github.com/stretchr/testify/assert" -) - -var ( - oauthClientID = "id" - oauthClientSecret = "secret" - oauthCallbackURL = "url" - allusers = "" - siteurl = "https://exampleorg/" - group = "exampleorg" -) - -func TestNew(t *testing.T) { - a := assert.New(t) - - p, _ := newGitlab() - g := p.(*Config) - a.Equal(g.config.ClientID, oauthClientID) - a.Equal(g.config.ClientSecret, oauthClientSecret) - a.Equal(g.config.RedirectURL, oauthCallbackURL) -} - -func TestNewBrokenSiteURL(t *testing.T) { - siteurl = "https://exampleorg" - a := assert.New(t) - - _, err := newGitlab() - a.EqualError(err, "gitlab_opts siteurl must end in /") - - siteurl = "https://exampleorg/" -} - -func TestBadAllUsers(t *testing.T) { - allusers = "true" - siteurl = "" - a := assert.New(t) - - _, err := newGitlab() - a.EqualError(err, "gitlab_opts if allusers is set, siteurl must be set") - - allusers = "" - siteurl = "https://exampleorg/" -} - -func TestGoodAllUsers(t *testing.T) { - allusers = "true" - a := assert.New(t) - - p, _ := newGitlab() - s := p.StartSession("test_state") - a.Contains(s, "exampleorg/oauth/authorize") - a.Contains(s, "state=test_state") - a.Contains(s, fmt.Sprintf("client_id=%s", oauthClientID)) - - allusers = "" -} - -func TestNewEmptyGroupList(t *testing.T) { - group = "" - a := assert.New(t) - - _, err := newGitlab() - a.EqualError(err, "gitlab_opts group and the users whitelist must not be both empty if allusers isn't true") - - group = "exampleorg" -} - -func TestStartSession(t *testing.T) { - a := assert.New(t) - - p, _ := newGitlab() - s := p.StartSession("test_state") - a.Contains(s, "exampleorg/oauth/authorize") - a.Contains(s, "state=test_state") - a.Contains(s, fmt.Sprintf("client_id=%s", oauthClientID)) -} - -func newGitlab() (auth.Provider, error) { - c := &config.Auth{ - OauthClientID: oauthClientID, - OauthClientSecret: oauthClientSecret, - OauthCallbackURL: oauthCallbackURL, - ProviderOpts: map[string]string{ - "group": group, - "siteurl": siteurl, - "allusers": allusers, - }, - } - return New(c) -} diff --git a/server/auth/google/google.go b/server/auth/google/google.go deleted file mode 100644 index b707310..0000000 --- a/server/auth/google/google.go +++ /dev/null @@ -1,135 +0,0 @@ -package google - -import ( - "errors" - "fmt" - "net/http" - "strings" - - "github.com/nsheridan/cashier/server/auth" - "github.com/nsheridan/cashier/server/config" - "github.com/nsheridan/cashier/server/metrics" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" - googleapi "google.golang.org/api/oauth2/v2" -) - -const ( - revokeURL = "https://accounts.google.com/o/oauth2/revoke?token=%s" - name = "google" -) - -// Config is an implementation of `auth.Provider` for authenticating using a -// Google account. -type Config struct { - config *oauth2.Config - domain string - whitelist map[string]bool -} - -var _ auth.Provider = (*Config)(nil) - -// New creates a new Google provider from a configuration. -func New(c *config.Auth) (*Config, error) { - uw := make(map[string]bool) - for _, u := range c.UsersWhitelist { - uw[u] = true - } - if c.ProviderOpts["domain"] == "" && len(uw) == 0 { - return nil, errors.New("either Google Apps domain or users whitelist must be specified") - } - - return &Config{ - config: &oauth2.Config{ - ClientID: c.OauthClientID, - ClientSecret: c.OauthClientSecret, - RedirectURL: c.OauthCallbackURL, - Endpoint: google.Endpoint, - Scopes: []string{googleapi.UserinfoEmailScope, googleapi.UserinfoProfileScope}, - }, - domain: c.ProviderOpts["domain"], - whitelist: 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 len(c.whitelist) > 0 && !c.whitelist[c.Email(token)] { - return false - } - if !token.Valid() { - return false - } - svc, err := googleapi.New(c.newClient(token)) - if err != nil { - return false - } - t := svc.Tokeninfo() - t.AccessToken(token.AccessToken) - ti, err := t.Do() - if err != nil { - return false - } - if ti.Audience != c.config.ClientID { - return false - } - ui, err := svc.Userinfo.Get().Do() - if err != nil { - return false - } - if c.domain != "" && ui.Hd != c.domain { - return false - } - metrics.M.AuthValid.WithLabelValues("google").Inc() - return true -} - -// Revoke disables the access token. -func (c *Config) Revoke(token *oauth2.Token) error { - h := c.newClient(token) - _, err := h.Get(fmt.Sprintf(revokeURL, token.AccessToken)) - return err -} - -// StartSession retrieves an authentication endpoint from Google. -func (c *Config) StartSession(state string) string { - return c.config.AuthCodeURL(state, oauth2.SetAuthURLParam("hd", c.domain)) -} - -// 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 { - metrics.M.AuthExchange.WithLabelValues("google").Inc() - } - return t, err -} - -// Email retrieves the email address of the user. -func (c *Config) Email(token *oauth2.Token) string { - svc, err := googleapi.New(c.newClient(token)) - if err != nil { - return "" - } - ui, err := svc.Userinfo.Get().Do() - if err != nil { - return "" - } - return ui.Email -} - -// Username retrieves the username portion of the user's email address. -func (c *Config) Username(token *oauth2.Token) string { - return strings.Split(c.Email(token), "@")[0] -} diff --git a/server/auth/google/google_test.go b/server/auth/google/google_test.go deleted file mode 100644 index 92e4ca0..0000000 --- a/server/auth/google/google_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package google - -import ( - "fmt" - "testing" - - "github.com/nsheridan/cashier/server/config" - "github.com/stretchr/testify/assert" -) - -var ( - oauthClientID = "id" - oauthClientSecret = "secret" - oauthCallbackURL = "url" - domain = "example.com" - users = []string{"user"} -) - -func TestNew(t *testing.T) { - a := assert.New(t) - p, err := newGoogle() - a.NoError(err) - a.Equal(p.config.ClientID, oauthClientID) - a.Equal(p.config.ClientSecret, oauthClientSecret) - a.Equal(p.config.RedirectURL, oauthCallbackURL) - a.Equal(p.domain, domain) - a.Equal(p.whitelist, map[string]bool{"user": true}) -} - -func TestWhitelist(t *testing.T) { - c := &config.Auth{ - OauthClientID: oauthClientID, - OauthClientSecret: oauthClientSecret, - OauthCallbackURL: oauthCallbackURL, - ProviderOpts: map[string]string{"domain": ""}, - UsersWhitelist: []string{}, - } - if _, err := New(c); err == nil { - t.Error("creating a provider without a domain set should return an error") - } - // Set a user whitelist but no domain - c.UsersWhitelist = users - if _, err := New(c); err != nil { - t.Error("creating a provider with users but no domain should not return an error") - } - // Unset the user whitelist and set a domain - c.UsersWhitelist = []string{} - c.ProviderOpts = map[string]string{"domain": domain} - if _, err := New(c); err != nil { - t.Error("creating a provider with a domain set but without a user whitelist should not return an error") - } -} - -func TestStartSession(t *testing.T) { - a := assert.New(t) - - p, err := newGoogle() - a.NoError(err) - s := p.StartSession("test_state") - a.Contains(s, "accounts.google.com/o/oauth2/auth") - a.Contains(s, "state=test_state") - a.Contains(s, fmt.Sprintf("hd=%s", domain)) - a.Contains(s, fmt.Sprintf("client_id=%s", oauthClientID)) -} - -func newGoogle() (*Config, error) { - c := &config.Auth{ - OauthClientID: oauthClientID, - OauthClientSecret: oauthClientSecret, - OauthCallbackURL: oauthCallbackURL, - ProviderOpts: map[string]string{"domain": domain}, - UsersWhitelist: users, - } - return New(c) -} diff --git a/server/auth/microsoft/microsoft.go b/server/auth/microsoft/microsoft.go deleted file mode 100644 index 8463ccf..0000000 --- a/server/auth/microsoft/microsoft.go +++ /dev/null @@ -1,201 +0,0 @@ -package microsoft - -import ( - "encoding/json" - "errors" - "net/http" - "path" - "strings" - - "github.com/nsheridan/cashier/server/auth" - "github.com/nsheridan/cashier/server/config" - "github.com/nsheridan/cashier/server/metrics" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/microsoft" -) - -const ( - name = "microsoft" -) - -// Config is an implementation of `auth.Provider` for authenticating using a -// Office 365 account. -type Config struct { - config *oauth2.Config - tenant string - groups map[string]bool - whitelist map[string]bool -} - -var _ auth.Provider = (*Config)(nil) - -// New creates a new Microsoft provider from a configuration. -func New(c *config.Auth) (*Config, error) { - whitelist := make(map[string]bool) - for _, u := range c.UsersWhitelist { - whitelist[u] = true - } - if c.ProviderOpts["tenant"] == "" && len(whitelist) == 0 { - return nil, errors.New("either Office 365 tenant or users whitelist must be specified") - } - groupMap := make(map[string]bool) - if groups, ok := c.ProviderOpts["groups"]; ok { - for _, group := range strings.Split(groups, ",") { - groupMap[strings.Trim(group, " ")] = true - } - } - - return &Config{ - config: &oauth2.Config{ - ClientID: c.OauthClientID, - ClientSecret: c.OauthClientSecret, - RedirectURL: c.OauthCallbackURL, - Endpoint: microsoft.AzureADEndpoint(c.ProviderOpts["tenant"]), - Scopes: []string{"user.Read.All", "Directory.Read.All"}, - }, - tenant: c.ProviderOpts["tenant"], - whitelist: whitelist, - groups: groupMap, - }, nil -} - -// A new oauth2 http client. -func (c *Config) newClient(token *oauth2.Token) *http.Client { - return c.config.Client(oauth2.NoContext, token) -} - -// Gets a response for an graph api call. -func (c *Config) getDocument(token *oauth2.Token, pathElements ...string) map[string]interface{} { - client := c.newClient(token) - url := "https://" + path.Join("graph.microsoft.com/v1.0", path.Join(pathElements...)) - resp, err := client.Get(url) - if err != nil { - return nil - } - defer resp.Body.Close() - var document map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&document); err != nil { - return nil - } - return document -} - -// Get info from the "/me" endpoint of the Microsoft Graph API (MSG-API). -// https://developer.microsoft.com/en-us/graph/docs/concepts/v1-overview -func (c *Config) getMe(token *oauth2.Token, item string) string { - document := c.getDocument(token, "/me") - if value, ok := document[item].(string); ok { - return value - } - return "" -} - -// Check against verified domains from "/organization" endpoint of MSG-API. -func (c *Config) verifyTenant(token *oauth2.Token) bool { - document := c.getDocument(token, "/organization") - // The domains for an organisation are in an array of structs under - // verifiedDomains, which is in a struct which is in turn an array - // of such structs under value in the document. Which in json looks - // like this: - // { "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#organization", - // "value": [ { - // ... - // "verifiedDomains": [ { - // ... - // "name": "M365x214355.onmicrosoft.com", - // } ] - // } ] - //} - var value []interface{} - var ok bool - if value, ok = document["value"].([]interface{}); !ok { - return false - } - for _, valueEntry := range value { - if value, ok = valueEntry.(map[string]interface{})["verifiedDomains"].([]interface{}); !ok { - continue - } - for _, val := range value { - domain := val.(map[string]interface{})["name"].(string) - if domain == c.tenant { - return true - } - } - } - return false -} - -// Check against groups from /users/{id}/memberOf endpoint of MSG-API. -func (c *Config) verifyGroups(token *oauth2.Token) bool { - document := c.getDocument(token, "/users/me/memberOf") - var value []interface{} - var ok bool - if value, ok = document["value"].([]interface{}); !ok { - return false - } - for _, valueEntry := range value { - if group, ok := valueEntry.(map[string]interface{})["displayName"].(string); ok { - if c.groups[group] { - return true - } - } - } - return false -} - -// 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 len(c.whitelist) > 0 && !c.whitelist[c.Email(token)] { - return false - } - if !token.Valid() { - return false - } - metrics.M.AuthValid.WithLabelValues("microsoft").Inc() - if c.tenant != "" { - if c.verifyTenant(token) { - if len(c.groups) > 0 { - return c.verifyGroups(token) - } - return true - } - } - return false -} - -// Revoke disables the access token. -func (c *Config) Revoke(token *oauth2.Token) error { - return nil -} - -// StartSession retrieves an authentication endpoint from Microsoft. -func (c *Config) StartSession(state string) string { - return c.config.AuthCodeURL(state, - oauth2.SetAuthURLParam("hd", c.tenant), - oauth2.SetAuthURLParam("prompt", "login")) -} - -// 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 { - metrics.M.AuthExchange.WithLabelValues("microsoft").Inc() - } - return t, err -} - -// Email retrieves the email address of the user. -func (c *Config) Email(token *oauth2.Token) string { - return c.getMe(token, "mail") -} - -// Username retrieves the username portion of the user's email address. -func (c *Config) Username(token *oauth2.Token) string { - return strings.Split(c.Email(token), "@")[0] -} diff --git a/server/auth/microsoft/microsoft_test.go b/server/auth/microsoft/microsoft_test.go deleted file mode 100644 index e362ef9..0000000 --- a/server/auth/microsoft/microsoft_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package microsoft - -import ( - "fmt" - "testing" - - "github.com/nsheridan/cashier/server/config" - "github.com/stretchr/testify/assert" -) - -var ( - oauthClientID = "id" - oauthClientSecret = "secret" - oauthCallbackURL = "url" - tenant = "example.com" - users = []string{"user"} -) - -func TestNew(t *testing.T) { - a := assert.New(t) - p, err := newMicrosoft() - a.NoError(err) - a.Equal(p.config.ClientID, oauthClientID) - a.Equal(p.config.ClientSecret, oauthClientSecret) - a.Equal(p.config.RedirectURL, oauthCallbackURL) - a.Equal(p.tenant, tenant) - a.Equal(p.whitelist, map[string]bool{"user": true}) -} - -func TestWhitelist(t *testing.T) { - c := &config.Auth{ - OauthClientID: oauthClientID, - OauthClientSecret: oauthClientSecret, - OauthCallbackURL: oauthCallbackURL, - ProviderOpts: map[string]string{"tenant": ""}, - UsersWhitelist: []string{}, - } - if _, err := New(c); err == nil { - t.Error("creating a provider without a tenant set should return an error") - } - // Set a user whitelist but no tenant - c.UsersWhitelist = users - if _, err := New(c); err != nil { - t.Error("creating a provider with users but no tenant should not return an error") - } - // Unset the user whitelist and set a tenant - c.UsersWhitelist = []string{} - c.ProviderOpts = map[string]string{"tenant": tenant} - if _, err := New(c); err != nil { - t.Error("creating a provider with a tenant set but without a user whitelist should not return an error") - } -} - -func TestStartSession(t *testing.T) { - a := assert.New(t) - - p, err := newMicrosoft() - a.NoError(err) - s := p.StartSession("test_state") - a.Contains(s, fmt.Sprintf("login.microsoftonline.com/%s/oauth2/v2.0/authorize", tenant)) -} - -func newMicrosoft() (*Config, error) { - c := &config.Auth{ - OauthClientID: oauthClientID, - OauthClientSecret: oauthClientSecret, - OauthCallbackURL: oauthCallbackURL, - ProviderOpts: map[string]string{"tenant": tenant}, - UsersWhitelist: users, - } - return New(c) -} diff --git a/server/auth/provider.go b/server/auth/provider.go deleted file mode 100644 index 9d1c8bd..0000000 --- a/server/auth/provider.go +++ /dev/null @@ -1,13 +0,0 @@ -package auth - -import "golang.org/x/oauth2" - -// Provider is an abstraction of different auth methods. -type Provider interface { - Name() string - StartSession(string) string - Exchange(string) (*oauth2.Token, error) - Username(*oauth2.Token) string - Valid(*oauth2.Token) bool - Revoke(*oauth2.Token) error -} diff --git a/server/auth/testprovider/testprovider.go b/server/auth/testprovider/testprovider.go deleted file mode 100644 index f785081..0000000 --- a/server/auth/testprovider/testprovider.go +++ /dev/null @@ -1,56 +0,0 @@ -package testprovider - -import ( - "time" - - "github.com/nsheridan/cashier/server/auth" - - "golang.org/x/oauth2" -) - -const ( - name = "testprovider" -) - -// Config is an implementation of `auth.Provider` for testing. -type Config struct{} - -var _ auth.Provider = (*Config)(nil) - -// New creates a new provider. -func New() *Config { - return &Config{} -} - -// 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 { - return true -} - -// Revoke disables the access token. -func (c *Config) Revoke(token *oauth2.Token) error { - return nil -} - -// StartSession retrieves an authentication endpoint. -func (c *Config) StartSession(state string) string { - return "https://www.example.com/auth" -} - -// Exchange authorizes the session and returns an access token. -func (c *Config) Exchange(code string) (*oauth2.Token, error) { - return &oauth2.Token{ - AccessToken: "token", - Expiry: time.Now().Add(1 * time.Hour), - }, nil -} - -// Username retrieves the username portion of the user's email address. -func (c *Config) Username(token *oauth2.Token) string { - return "test" -} |