From fd9261b4fc84c86dbbedacb0beccc56d41867c19 Mon Sep 17 00:00:00 2001 From: Kevin Lyda Date: Thu, 31 May 2018 06:52:16 +0100 Subject: [PATCH] First pass at microsoft provider. Code to get email address is missing. Code to only authorise certain groups is missing. --- server/auth/microsoft/microsoft.go | 103 ++++++++++++++++++++++++ server/auth/microsoft/microsoft_test.go | 72 +++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 server/auth/microsoft/microsoft.go create mode 100644 server/auth/microsoft/microsoft_test.go diff --git a/server/auth/microsoft/microsoft.go b/server/auth/microsoft/microsoft.go new file mode 100644 index 00000000..42664b1e --- /dev/null +++ b/server/auth/microsoft/microsoft.go @@ -0,0 +1,103 @@ +package microsoft + +import ( + "errors" + "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/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 + 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) { + uw := make(map[string]bool) + for _, u := range c.UsersWhitelist { + uw[u] = true + } + if c.ProviderOpts["tenant"] == "" && len(uw) == 0 { + return nil, errors.New("either Office 365 tenant or users whitelist must be specified") + } + + return &Config{ + config: &oauth2.Config{ + ClientID: c.OauthClientID, + ClientSecret: c.OauthClientSecret, + RedirectURL: c.OauthCallbackURL, + Endpoint: microsoft.AzureADEndpoint(c.ProviderOpts["tenant"]), + }, + tenant: c.ProviderOpts["tenant"], + 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 + } + metrics.M.AuthValid.WithLabelValues("microsoft").Inc() + return true +} + +// 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) *auth.Session { + return &auth.Session{ + AuthURL: c.config.AuthCodeURL(state, oauth2.SetAuthURLParam("hd", c.tenant)), + } +} + +// 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 "nobody@nowhere" +} + +// 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 new file mode 100644 index 00000000..c2c2c172 --- /dev/null +++ b/server/auth/microsoft/microsoft_test.go @@ -0,0 +1,72 @@ +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.AuthURL, 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) +} -- GitLab