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

Commit

Permalink
Merge pull request #77 from 18F/extract-providers-package
Browse files Browse the repository at this point in the history
Extract providers package
  • Loading branch information
jehiah committed Mar 31, 2015
2 parents 41044ec + d9a945e commit 0136ab0
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 37 deletions.
6 changes: 6 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ func main() {

flagSet.Bool("request-logging", true, "Log requests to stdout")

flagSet.String("provider", "", "Oauth provider (defaults to Google)")
flagSet.String("login-url", "", "Authentication endpoint")
flagSet.String("redeem-url", "", "Token redemption endpoint")
flagSet.String("profile-url", "", "Profile access endpoint")
flagSet.String("scope", "", "Oauth scope specification")

flagSet.Parse(os.Args[1:])

if *showVersion {
Expand Down
38 changes: 7 additions & 31 deletions oauthproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import (
"strings"
"time"

"github.com/bitly/go-simplejson"
"github.com/bitly/google_auth_proxy/api"
"github.com/bitly/google_auth_proxy/providers"
)

const pingPath = "/ping"
Expand All @@ -34,6 +34,7 @@ type OauthProxy struct {
Validator func(string) bool

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
oauthScope string
Expand Down Expand Up @@ -83,8 +84,6 @@ func setProxyDirector(proxy *httputil.ReverseProxy) {
}

func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
login, _ := url.Parse("https://accounts.google.com/o/oauth2/auth")
redeem, _ := url.Parse("https://accounts.google.com/o/oauth2/token")
serveMux := http.NewServeMux()
for _, u := range opts.proxyUrls {
path := u.Path
Expand Down Expand Up @@ -128,9 +127,10 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {

clientID: opts.ClientID,
clientSecret: opts.ClientSecret,
oauthScope: "profile email",
oauthRedemptionUrl: redeem,
oauthLoginUrl: login,
oauthScope: opts.provider.Data().Scope,
provider: opts.provider,
oauthRedemptionUrl: opts.provider.Data().RedeemUrl,
oauthLoginUrl: opts.provider.Data().LoginUrl,
serveMux: serveMux,
redirectUrl: redirectUrl,
skipAuthRegex: opts.SkipAuthRegex,
Expand Down Expand Up @@ -201,38 +201,14 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) {
return "", "", err
}

idToken, err := json.Get("id_token").String()
if err != nil {
return "", "", err
}

// id_token is a base64 encode ID token payload
// https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo
jwt := strings.Split(idToken, ".")
b, err := jwtDecodeSegment(jwt[1])
if err != nil {
return "", "", err
}
data, err := simplejson.NewJson(b)
if err != nil {
return "", "", err
}
email, err := data.Get("email").String()
email, err := p.provider.GetEmailAddress(json, access_token)
if err != nil {
return "", "", err
}

return access_token, email, nil
}

func jwtDecodeSegment(seg string) ([]byte, error) {
if l := len(seg) % 4; l > 0 {
seg += strings.Repeat("=", 4-l)
}

return base64.URLEncoding.DecodeString(seg)
}

func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
domain := req.Host
if h, _, err := net.SplitHostPort(domain); err == nil {
Expand Down
37 changes: 31 additions & 6 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"regexp"
"strings"
"time"

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

// Configuration Options that can be set by Command Line Flag, or Config File
Expand Down Expand Up @@ -33,12 +35,21 @@ type Options struct {
PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"`

// These options allow for other providers besides Google, with
// potential overrides.
Provider string `flag:"provider" cfg:"provider"`
LoginUrl string `flag:"login-url" cfg:"login_url"`
RedeemUrl string `flag:"redeem-url" cfg:"redeem_url"`
ProfileUrl string `flag:"profile-url" cfg:"profile_url"`
Scope string `flag:"scope" cfg:"scope"`

RequestLogging bool `flag:"request-logging" cfg:"request_logging"`

// internal values that are set after config validation
redirectUrl *url.URL
proxyUrls []*url.URL
CompiledRegex []*regexp.Regexp
provider providers.Provider
}

func NewOptions() *Options {
Expand All @@ -55,6 +66,15 @@ func NewOptions() *Options {
}
}

func parseUrl(to_parse string, urltype string, msgs []string) (*url.URL, []string) {
parsed, err := url.Parse(to_parse)
if err != nil {
return nil, append(msgs, fmt.Sprintf(
"error parsing %s-url=%q %s", urltype, to_parse, err))
}
return parsed, msgs
}

func (o *Options) Validate() error {
msgs := make([]string, 0)
if len(o.Upstreams) < 1 {
Expand All @@ -70,12 +90,7 @@ func (o *Options) Validate() error {
msgs = append(msgs, "missing setting: client-secret")
}

redirectUrl, err := url.Parse(o.RedirectUrl)
if err != nil {
msgs = append(msgs, fmt.Sprintf(
"error parsing redirect-url=%q %s", o.RedirectUrl, err))
}
o.redirectUrl = redirectUrl
o.redirectUrl, msgs = parseUrl(o.RedirectUrl, "redirect", msgs)

for _, u := range o.Upstreams {
upstreamUrl, err := url.Parse(u)
Expand All @@ -98,10 +113,20 @@ func (o *Options) Validate() error {
}
o.CompiledRegex = append(o.CompiledRegex, CompiledRegex)
}
msgs = parseProviderInfo(o, msgs)

if len(msgs) != 0 {
return fmt.Errorf("Invalid configuration:\n %s",
strings.Join(msgs, "\n "))
}
return nil
}

func parseProviderInfo(o *Options, msgs []string) []string {
p := &providers.ProviderData{Scope: o.Scope}
p.LoginUrl, msgs = parseUrl(o.LoginUrl, "login", msgs)
p.RedeemUrl, msgs = parseUrl(o.RedeemUrl, "redeem", msgs)
p.ProfileUrl, msgs = parseUrl(o.ProfileUrl, "profile", msgs)
o.provider = providers.New(o.Provider, p)
return msgs
}
12 changes: 12 additions & 0 deletions options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,15 @@ func TestCompiledRegexError(t *testing.T) {
"unexpected ): `barquux)`"})
assert.Equal(t, expected, err.Error())
}

func TestDefaultProviderApiSettings(t *testing.T) {
o := testOptions()
assert.Equal(t, nil, o.Validate())
p := o.provider.Data()
assert.Equal(t, "https://accounts.google.com/o/oauth2/auth",
p.LoginUrl.String())
assert.Equal(t, "https://accounts.google.com/o/oauth2/token",
p.RedeemUrl.String())
assert.Equal(t, "", p.ProfileUrl.String())
assert.Equal(t, "profile email", p.Scope)
}
62 changes: 62 additions & 0 deletions providers/google.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package providers

import (
"encoding/base64"
"net/url"
"strings"

"github.com/bitly/go-simplejson"
)

type GoogleProvider struct {
*ProviderData
}

func NewGoogleProvider(p *ProviderData) *GoogleProvider {
if p.LoginUrl.String() == "" {
p.LoginUrl = &url.URL{Scheme: "https",
Host: "accounts.google.com",
Path: "/o/oauth2/auth"}
}
if p.RedeemUrl.String() == "" {
p.RedeemUrl = &url.URL{Scheme: "https",
Host: "accounts.google.com",
Path: "/o/oauth2/token"}
}
if p.Scope == "" {
p.Scope = "profile email"
}
return &GoogleProvider{ProviderData: p}
}

func (s *GoogleProvider) GetEmailAddress(auth_response *simplejson.Json,
unused_access_token string) (string, error) {
idToken, err := auth_response.Get("id_token").String()
if err != nil {
return "", err
}
// id_token is a base64 encode ID token payload
// https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo
jwt := strings.Split(idToken, ".")
b, err := jwtDecodeSegment(jwt[1])
if err != nil {
return "", err
}
data, err := simplejson.NewJson(b)
if err != nil {
return "", err
}
email, err := data.Get("email").String()
if err != nil {
return "", err
}
return email, nil
}

func jwtDecodeSegment(seg string) ([]byte, error) {
if l := len(seg) % 4; l > 0 {
seg += strings.Repeat("=", 4-l)
}

return base64.URLEncoding.DecodeString(seg)
}
94 changes: 94 additions & 0 deletions providers/google_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package providers

import (
"encoding/base64"
"github.com/bitly/go-simplejson"
"github.com/bmizerany/assert"
"net/url"
"testing"
)

func newGoogleProvider() *GoogleProvider {
return NewGoogleProvider(
&ProviderData{
LoginUrl: &url.URL{},
RedeemUrl: &url.URL{},
ProfileUrl: &url.URL{},
Scope: ""})
}

func TestGoogleProviderDefaults(t *testing.T) {
p := newGoogleProvider()
assert.NotEqual(t, nil, p)
assert.Equal(t, "https://accounts.google.com/o/oauth2/auth",
p.Data().LoginUrl.String())
assert.Equal(t, "https://accounts.google.com/o/oauth2/token",
p.Data().RedeemUrl.String())
assert.Equal(t, "", p.Data().ProfileUrl.String())
assert.Equal(t, "profile email", p.Data().Scope)
}

func TestGoogleProviderOverrides(t *testing.T) {
p := NewGoogleProvider(
&ProviderData{
LoginUrl: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/auth"},
RedeemUrl: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/token"},
ProfileUrl: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/profile"},
Scope: "profile"})
assert.NotEqual(t, nil, p)
assert.Equal(t, "https://example.com/oauth/auth",
p.Data().LoginUrl.String())
assert.Equal(t, "https://example.com/oauth/token",
p.Data().RedeemUrl.String())
assert.Equal(t, "https://example.com/oauth/profile",
p.Data().ProfileUrl.String())
assert.Equal(t, "profile", p.Data().Scope)
}

func TestGoogleProviderGetEmailAddress(t *testing.T) {
p := newGoogleProvider()
j := simplejson.New()
j.Set("id_token", "ignored prefix."+base64.URLEncoding.EncodeToString(
[]byte("{\"email\": \"michael.bland@gsa.gov\"}")))
email, err := p.GetEmailAddress(j, "ignored access_token")
assert.Equal(t, "michael.bland@gsa.gov", email)
assert.Equal(t, nil, err)
}

func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) {
p := newGoogleProvider()
j := simplejson.New()
j.Set("id_token", "ignored prefix.{\"email\": \"michael.bland@gsa.gov\"}")
email, err := p.GetEmailAddress(j, "ignored access_token")
assert.Equal(t, "", email)
assert.NotEqual(t, nil, err)
}

func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) {
p := newGoogleProvider()
j := simplejson.New()
j.Set("id_token", "ignored prefix."+base64.URLEncoding.EncodeToString(
[]byte("{email: michael.bland@gsa.gov}")))
email, err := p.GetEmailAddress(j, "ignored access_token")
assert.Equal(t, "", email)
assert.NotEqual(t, nil, err)
}

func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
p := newGoogleProvider()
j := simplejson.New()
j.Set("id_token", "ignored prefix."+base64.URLEncoding.EncodeToString(
[]byte("{\"not_email\": \"missing!\"}")))
email, err := p.GetEmailAddress(j, "ignored access_token")
assert.Equal(t, "", email)
assert.NotEqual(t, nil, err)
}
14 changes: 14 additions & 0 deletions providers/provider_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package providers

import (
"net/url"
)

type ProviderData struct {
LoginUrl *url.URL
RedeemUrl *url.URL
ProfileUrl *url.URL
Scope string
}

func (p *ProviderData) Data() *ProviderData { return p }
18 changes: 18 additions & 0 deletions providers/providers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package providers

import (
"github.com/bitly/go-simplejson"
)

type Provider interface {
Data() *ProviderData
GetEmailAddress(auth_response *simplejson.Json,
access_token string) (string, error)
}

func New(provider string, p *ProviderData) Provider {
switch provider {
default:
return NewGoogleProvider(p)
}
}

0 comments on commit 0136ab0

Please sign in to comment.