diff options
| author | Niall Sheridan <nsheridan@gmail.com> | 2016-04-22 23:01:32 +0100 | 
|---|---|---|
| committer | Niall Sheridan <nsheridan@gmail.com> | 2016-04-22 23:16:11 +0100 | 
| commit | bd1e6a57fe354ccfe51d295fec3c06a1c878c3f7 (patch) | |
| tree | 8741e4537293221e64d5bf9dc6df3b9fb8816bc6 | |
| parent | 5a57870d808f1601d85a35d08429b9ae19dafe93 (diff) | |
Add github oauth provider.
| -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, | 
