diff options
-rw-r--r-- | server/auth/github/github.go | 95 | ||||
-rw-r--r-- | server/auth/github/github_test.go | 49 | ||||
-rw-r--r-- | server/main.go | 24 |
3 files changed, 166 insertions, 2 deletions
diff --git a/server/auth/github/github.go b/server/auth/github/github.go new file mode 100644 index 0000000..da12531 --- /dev/null +++ b/server/auth/github/github.go @@ -0,0 +1,95 @@ +package github + +import ( + "net/http" + + "github.com/nsheridan/cashier/server/auth" + "github.com/nsheridan/cashier/server/config" + + githubapi "github.com/google/go-github/github" + "golang.org/x/oauth2" + "golang.org/x/oauth2/github" +) + +const ( + // revokeURL = "https://accounts.google.com/o/oauth2/revoke?token=%s" + name = "github" +) + +// Config is an implementation of `auth.Provider` for authenticating using a +// Github account. +type Config struct { + config *oauth2.Config + organization string +} + +// New creates a new Github provider from a configuration. +func New(c *config.Auth) auth.Provider { + 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), + }, + }, + organization: c.ProviderOpts["organization"], + } +} + +// 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 !token.Valid() { + return false + } + if c.organization == "" { + return true + } + client := githubapi.NewClient(c.newClient(token)) + member, _, err := client.Organizations.IsMember(c.organization, c.Username(token)) + if err != nil { + return false + } + return member +} + +// Revoke disables the access token. +func (c *Config) Revoke(token *oauth2.Token) error { + return nil +} + +// StartSession retrieves an authentication endpoint from Github. +func (c *Config) StartSession(state string) *auth.Session { + return &auth.Session{ + AuthURL: c.config.AuthCodeURL(state), + State: state, + } +} + +// Exchange authorizes the session and returns an access token. +func (c *Config) Exchange(code string) (*oauth2.Token, error) { + return c.config.Exchange(oauth2.NoContext, code) +} + +// 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("") + if err != nil { + return "" + } + return *u.Login +} diff --git a/server/auth/github/github_test.go b/server/auth/github/github_test.go new file mode 100644 index 0000000..383642f --- /dev/null +++ b/server/auth/github/github_test.go @@ -0,0 +1,49 @@ +package github + +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" + organization = "exampleorg" +) + +func TestNew(t *testing.T) { + a := assert.New(t) + + p := newGithub() + g := p.(*Config) + a.Equal(g.config.ClientID, oauthClientID) + a.Equal(g.config.ClientSecret, oauthClientSecret) + a.Equal(g.config.RedirectURL, oauthCallbackURL) + a.Equal(g.organization, organization) +} + +func TestStartSession(t *testing.T) { + a := assert.New(t) + + p := newGithub() + s := p.StartSession("test_state") + a.Equal(s.State, "test_state") + a.Contains(s.AuthURL, "github.com/login/oauth/authorize") + a.Contains(s.AuthURL, "state=test_state") + a.Contains(s.AuthURL, fmt.Sprintf("client_id=%s", oauthClientID)) +} + +func newGithub() auth.Provider { + c := &config.Auth{ + OauthClientID: oauthClientID, + OauthClientSecret: oauthClientSecret, + OauthCallbackURL: oauthCallbackURL, + ProviderOpts: map[string]string{"organization": organization}, + } + return New(c) +} diff --git a/server/main.go b/server/main.go index 3a20460..c597b2e 100644 --- a/server/main.go +++ b/server/main.go @@ -21,6 +21,7 @@ import ( "github.com/gorilla/sessions" "github.com/nsheridan/cashier/lib" "github.com/nsheridan/cashier/server/auth" + "github.com/nsheridan/cashier/server/auth/github" "github.com/nsheridan/cashier/server/auth/google" "github.com/nsheridan/cashier/server/config" "github.com/nsheridan/cashier/server/signer" @@ -51,7 +52,7 @@ func (a *appContext) getAuthCookie(r *http.Request) *oauth2.Token { if err := json.Unmarshal(t.([]byte), &tok); err != nil { return nil } - if !a.authprovider.Valid(&tok) { + if !tok.Valid() { return nil } return &tok @@ -136,6 +137,12 @@ func callbackHandler(a *appContext, w http.ResponseWriter, r *http.Request) (int if err := a.authsession.Authorize(a.authprovider, code); err != nil { return http.StatusInternalServerError, err } + // Github tokens don't have an expiry. Set one so that the session expires + // after a period. + if a.authsession.Token.Expiry.Unix() <= 0 { + a.authsession.Token.Expiry = time.Now().Add(1 * time.Hour) + } + fmt.Println(a.authsession.Token) a.setAuthCookie(w, r, a.authsession.Token) http.Redirect(w, r, "/", http.StatusFound) return http.StatusFound, nil @@ -148,6 +155,9 @@ func rootHandler(a *appContext, w http.ResponseWriter, r *http.Request) (int, er http.Redirect(w, r, "/auth/login", http.StatusSeeOther) return http.StatusSeeOther, nil } + if !a.authprovider.Valid(tok) { + return http.StatusUnauthorized, errors.New(http.StatusText(http.StatusUnauthorized)) + } j := jwt.New(jwt.SigningMethodHS256) j.Claims["token"] = tok.AccessToken j.Claims["exp"] = tok.Expiry.Unix() @@ -203,7 +213,17 @@ func main() { if err != nil { log.Fatal(err) } - authprovider := google.New(&config.Auth) + + var authprovider auth.Provider + switch config.Auth.Provider { + case "google": + authprovider = google.New(&config.Auth) + case "github": + authprovider = github.New(&config.Auth) + default: + log.Fatalln("Unknown provider %s", config.Auth.Provider) + } + ctx := &appContext{ cookiestore: sessions.NewCookieStore([]byte(config.Server.CookieSecret)), authprovider: authprovider, |