From bd1e6a57fe354ccfe51d295fec3c06a1c878c3f7 Mon Sep 17 00:00:00 2001
From: Niall Sheridan <nsheridan@gmail.com>
Date: Fri, 22 Apr 2016 23:01:32 +0100
Subject: [PATCH] Add github oauth provider.

---
 server/auth/github/github.go      | 95 +++++++++++++++++++++++++++++++
 server/auth/github/github_test.go | 49 ++++++++++++++++
 server/main.go                    | 24 +++++++-
 3 files changed, 166 insertions(+), 2 deletions(-)
 create mode 100644 server/auth/github/github.go
 create mode 100644 server/auth/github/github_test.go

diff --git a/server/auth/github/github.go b/server/auth/github/github.go
new file mode 100644
index 00000000..da125311
--- /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 00000000..383642f3
--- /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 3a204602..c597b2eb 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,
-- 
GitLab