Skip to content
This repository has been archived by the owner on Jan 24, 2019. It is now read-only.

Commit

Permalink
Github provider
Browse files Browse the repository at this point in the history
  • Loading branch information
jehiah committed May 21, 2015
1 parent 8471f97 commit e6c2f07
Show file tree
Hide file tree
Showing 16 changed files with 292 additions and 108 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ Usage of google_auth_proxy:
-version=false: print version string
```

See below for provider specific options

### Environment variables

The environment variables `GOOGLE_AUTH_PROXY_CLIENT_ID`, `GOOGLE_AUTH_PROXY_CLIENT_SECRET`, `GOOGLE_AUTH_PROXY_COOKIE_SECRET`, `GOOGLE_AUTH_PROXY_COOKIE_DOMAIN` and `GOOGLE_AUTH_PROXY_COOKIE_EXPIRE` can be used in place of the corresponding command-line arguments.
Expand Down Expand Up @@ -173,6 +175,10 @@ directive. Right now this includes:
* `myusa` - The [MyUSA](https://alpha.my.usa.gov) authentication service
([GitHub](https://github.com/18F/myusa))
* `linkedin` - The [LinkedIn](https://developer.linkedin.com/docs/signin-with-linkedin) Sign In service.
* `github` - Via [Github][https://github.com/settings/developers] OAuth App. Also supports restricting via org and team.

-github-org="": restrict logins to members of this organisation
-github-team="": restrict logins to members of this team

## Adding a new Provider

Expand Down
3 changes: 1 addition & 2 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import (
)

func Request(req *http.Request) (*simplejson.Json, error) {
httpclient := &http.Client{}
resp, err := httpclient.Do(req)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
Expand Down
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
)

func main() {
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
flagSet := flag.NewFlagSet("google_auth_proxy", flag.ExitOnError)

googleAppsDomains := StringArray{}
Expand All @@ -35,6 +36,8 @@ func main() {
flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)")

flagSet.Var(&googleAppsDomains, "google-apps-domain", "authenticate against the given Google apps domain (may be given multiple times)")
flagSet.String("github-org", "", "restrict logins to members of this organisation")
flagSet.String("github-team", "", "restrict logins to members of this team")
flagSet.String("client-id", "", "the Google OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
flagSet.String("client-secret", "", "the OAuth Client Secret")
flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
Expand Down
54 changes: 17 additions & 37 deletions oauthproxy.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
Expand All @@ -17,7 +16,6 @@ import (
"strings"
"time"

"github.com/bitly/google_auth_proxy/api"
"github.com/bitly/google_auth_proxy/providers"
)

Expand All @@ -39,7 +37,6 @@ type OauthProxy struct {

redirectUrl *url.URL // the url to receive requests at
provider providers.Provider
oauthRedemptionUrl *url.URL // endpoint to redeem the code
oauthLoginUrl *url.URL // to redirect the user to
oauthValidateUrl *url.URL // to validate the access token
oauthScope string
Expand Down Expand Up @@ -143,21 +140,20 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
CookieRefresh: opts.CookieRefresh,
Validator: validator,

clientID: opts.ClientID,
clientSecret: opts.ClientSecret,
oauthScope: opts.provider.Data().Scope,
provider: opts.provider,
oauthRedemptionUrl: opts.provider.Data().RedeemUrl,
oauthLoginUrl: opts.provider.Data().LoginUrl,
oauthValidateUrl: opts.provider.Data().ValidateUrl,
serveMux: serveMux,
redirectUrl: redirectUrl,
skipAuthRegex: opts.SkipAuthRegex,
compiledRegex: opts.CompiledRegex,
PassBasicAuth: opts.PassBasicAuth,
PassAccessToken: opts.PassAccessToken,
AesCipher: aes_cipher,
templates: loadTemplates(opts.CustomTemplatesDir),
clientID: opts.ClientID,
clientSecret: opts.ClientSecret,
oauthScope: opts.provider.Data().Scope,
provider: opts.provider,
oauthLoginUrl: opts.provider.Data().LoginUrl,
oauthValidateUrl: opts.provider.Data().ValidateUrl,
serveMux: serveMux,
redirectUrl: redirectUrl,
skipAuthRegex: opts.SkipAuthRegex,
compiledRegex: opts.CompiledRegex,
PassBasicAuth: opts.PassBasicAuth,
PassAccessToken: opts.PassAccessToken,
AesCipher: aes_cipher,
templates: loadTemplates(opts.CustomTemplatesDir),
}
}

Expand Down Expand Up @@ -200,29 +196,13 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) {
if code == "" {
return "", "", errors.New("missing code")
}
params := url.Values{}
params.Add("redirect_uri", p.GetRedirectUrl(host))
params.Add("client_id", p.clientID)
params.Add("client_secret", p.clientSecret)
params.Add("code", code)
params.Add("grant_type", "authorization_code")
req, err := http.NewRequest("POST", p.oauthRedemptionUrl.String(), bytes.NewBufferString(params.Encode()))
if err != nil {
log.Printf("failed building request %s", err.Error())
return "", "", err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
json, err := api.Request(req)
if err != nil {
log.Printf("failed making request %s", err)
return "", "", err
}
access_token, err := json.Get("access_token").String()
redirectUri := p.GetRedirectUrl(host)
body, access_token, err := p.provider.Redeem(redirectUri, code)
if err != nil {
return "", "", err
}

email, err := p.provider.GetEmailAddress(json, access_token)
email, err := p.provider.GetEmailAddress(body, access_token)
if err != nil {
return "", "", err
}
Expand Down
20 changes: 11 additions & 9 deletions oauthproxy_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package main

import (
"github.com/bitly/go-simplejson"
"github.com/bitly/google_auth_proxy/providers"
"github.com/bmizerany/assert"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/httptest"
Expand All @@ -15,6 +15,11 @@ import (
"time"
)

func init() {
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)

}

func TestNewReverseProxy(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
Expand Down Expand Up @@ -89,8 +94,7 @@ type TestProvider struct {
ValidToken bool
}

func (tp *TestProvider) GetEmailAddress(unused_auth_response *simplejson.Json,
unused_access_token string) (string, error) {
func (tp *TestProvider) GetEmailAddress(body []byte, access_token string) (string, error) {
return tp.EmailAddress, nil
}

Expand All @@ -113,16 +117,15 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes

t.provider_server = httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%#v", r)
url := r.URL
payload := ""
switch url.Path {
case "/oauth/token":
payload = `{"access_token": "my_auth_token"}`
default:
token_header := r.Header["X-Forwarded-Access-Token"]
if len(token_header) != 0 {
payload = token_header[0]
} else {
payload = r.Header.Get("X-Forwarded-Access-Token")
if payload == "" {
payload = "No access token found."
}
}
Expand Down Expand Up @@ -189,8 +192,7 @@ func (pat_test *PassAccessTokenTest) getCallbackEndpoint() (http_code int,
return rw.Code, rw.HeaderMap["Set-Cookie"][0]
}

func (pat_test *PassAccessTokenTest) getRootEndpoint(
cookie string) (http_code int, access_token string) {
func (pat_test *PassAccessTokenTest) getRootEndpoint(cookie string) (http_code int, access_token string) {
cookie_key := pat_test.proxy.CookieKey
var value string
key_prefix := cookie_key + "="
Expand Down
9 changes: 8 additions & 1 deletion options.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type Options struct {

AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
GoogleAppsDomains []string `flag:"google-apps-domain" cfg:"google_apps_domains"`
GitHubOrg string `flag:"github-org" cfg:"github_org"`
GitHubTeam string `flag:"github-team" cfg:"github_team"`
HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"`
DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form"`
CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir"`
Expand Down Expand Up @@ -153,11 +155,16 @@ func (o *Options) Validate() error {
}

func parseProviderInfo(o *Options, msgs []string) []string {
p := &providers.ProviderData{Scope: o.Scope}
p := &providers.ProviderData{Scope: o.Scope, ClientID: o.ClientID, ClientSecret: o.ClientSecret}
p.LoginUrl, msgs = parseUrl(o.LoginUrl, "login", msgs)
p.RedeemUrl, msgs = parseUrl(o.RedeemUrl, "redeem", msgs)
p.ProfileUrl, msgs = parseUrl(o.ProfileUrl, "profile", msgs)
p.ValidateUrl, msgs = parseUrl(o.ValidateUrl, "validate", msgs)

o.provider = providers.New(o.Provider, p)
switch p := o.provider.(type) {
case *providers.GitHubProvider:
p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
}
return msgs
}
125 changes: 125 additions & 0 deletions providers/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package providers

import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
)

type GitHubProvider struct {
*ProviderData
Org string
Team string
}

func NewGitHubProvider(p *ProviderData) *GitHubProvider {
p.ProviderName = "GitHub"
if p.LoginUrl.String() == "" {
p.LoginUrl = &url.URL{
Scheme: "https",
Host: "github.com",
Path: "/login/oauth/authorize",
}
}
if p.RedeemUrl.String() == "" {
p.RedeemUrl = &url.URL{
Scheme: "https",
Host: "github.com",
Path: "/login/oauth/access_token",
}
}
if p.Scope == "" {
p.Scope = "user:email"
}
return &GitHubProvider{ProviderData: p}
}
func (p *GitHubProvider) SetOrgTeam(org, team string) {
p.Org = org
p.Team = team
if org != "" || team != "" {
p.Scope += " read:org"
}
}

func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) {

var teams []struct {
Name string `json:"name"`
Slug string `json:"slug"`
Org struct {
Login string `json:"login"`
} `json:"organization"`
}

params := url.Values{
"access_token": {accessToken},
}

req, _ := http.NewRequest("GET", "https://api.github.com/user/teams?"+params.Encode(), nil)
req.Header.Set("Accept", "application/vnd.github.moondragon+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return false, err
}

body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return false, err
}

if err := json.Unmarshal(body, &teams); err != nil {
return false, err
}

for _, team := range teams {
if p.Org == team.Org.Login {
if p.Team == "" || p.Team == team.Slug {
return true, nil
}
}
}
return false, nil
}

func (p *GitHubProvider) GetEmailAddress(body []byte, access_token string) (string, error) {

var emails []struct {
Email string `json:"email"`
Primary bool `json:"primary"`
}

params := url.Values{
"access_token": {access_token},
}

// if we require an Org or Team, check that first
if p.Org != "" || p.Team != "" {
if ok, err := p.hasOrgAndTeam(access_token); err != nil || !ok {
return "", err
}
}

resp, err := http.DefaultClient.Get("https://api.github.com/user/emails?" + params.Encode())
if err != nil {
return "", err
}
body, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return "", err
}

if err := json.Unmarshal(body, &emails); err != nil {
return "", err
}

for _, email := range emails {
if email.Primary {
return email.Email, nil
}
}

return "", nil
}
Loading

0 comments on commit e6c2f07

Please sign in to comment.