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

Extract providers package #77

Merged
merged 2 commits into from
Mar 31, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}