Merge pull request #77 from 18F/extract-providers-package
Extract providers package
This commit is contained in:
commit
0136ab01fd
6
main.go
6
main.go
@ -50,6 +50,12 @@ func main() {
|
|||||||
|
|
||||||
flagSet.Bool("request-logging", true, "Log requests to stdout")
|
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:])
|
flagSet.Parse(os.Args[1:])
|
||||||
|
|
||||||
if *showVersion {
|
if *showVersion {
|
||||||
|
@ -15,8 +15,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bitly/go-simplejson"
|
|
||||||
"github.com/bitly/google_auth_proxy/api"
|
"github.com/bitly/google_auth_proxy/api"
|
||||||
|
"github.com/bitly/google_auth_proxy/providers"
|
||||||
)
|
)
|
||||||
|
|
||||||
const pingPath = "/ping"
|
const pingPath = "/ping"
|
||||||
@ -34,6 +34,7 @@ type OauthProxy struct {
|
|||||||
Validator func(string) bool
|
Validator func(string) bool
|
||||||
|
|
||||||
redirectUrl *url.URL // the url to receive requests at
|
redirectUrl *url.URL // the url to receive requests at
|
||||||
|
provider providers.Provider
|
||||||
oauthRedemptionUrl *url.URL // endpoint to redeem the code
|
oauthRedemptionUrl *url.URL // endpoint to redeem the code
|
||||||
oauthLoginUrl *url.URL // to redirect the user to
|
oauthLoginUrl *url.URL // to redirect the user to
|
||||||
oauthScope string
|
oauthScope string
|
||||||
@ -83,8 +84,6 @@ func setProxyDirector(proxy *httputil.ReverseProxy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
|
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()
|
serveMux := http.NewServeMux()
|
||||||
for _, u := range opts.proxyUrls {
|
for _, u := range opts.proxyUrls {
|
||||||
path := u.Path
|
path := u.Path
|
||||||
@ -128,9 +127,10 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
|
|||||||
|
|
||||||
clientID: opts.ClientID,
|
clientID: opts.ClientID,
|
||||||
clientSecret: opts.ClientSecret,
|
clientSecret: opts.ClientSecret,
|
||||||
oauthScope: "profile email",
|
oauthScope: opts.provider.Data().Scope,
|
||||||
oauthRedemptionUrl: redeem,
|
provider: opts.provider,
|
||||||
oauthLoginUrl: login,
|
oauthRedemptionUrl: opts.provider.Data().RedeemUrl,
|
||||||
|
oauthLoginUrl: opts.provider.Data().LoginUrl,
|
||||||
serveMux: serveMux,
|
serveMux: serveMux,
|
||||||
redirectUrl: redirectUrl,
|
redirectUrl: redirectUrl,
|
||||||
skipAuthRegex: opts.SkipAuthRegex,
|
skipAuthRegex: opts.SkipAuthRegex,
|
||||||
@ -201,23 +201,7 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) {
|
|||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
idToken, err := json.Get("id_token").String()
|
email, err := p.provider.GetEmailAddress(json, access_token)
|
||||||
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 {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
@ -225,14 +209,6 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) {
|
|||||||
return access_token, email, nil
|
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) {
|
func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
|
||||||
domain := req.Host
|
domain := req.Host
|
||||||
if h, _, err := net.SplitHostPort(domain); err == nil {
|
if h, _, err := net.SplitHostPort(domain); err == nil {
|
||||||
|
37
options.go
37
options.go
@ -6,6 +6,8 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/bitly/google_auth_proxy/providers"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Configuration Options that can be set by Command Line Flag, or Config File
|
// Configuration Options that can be set by Command Line Flag, or Config File
|
||||||
@ -33,12 +35,21 @@ type Options struct {
|
|||||||
PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
|
PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
|
||||||
PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"`
|
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"`
|
RequestLogging bool `flag:"request-logging" cfg:"request_logging"`
|
||||||
|
|
||||||
// internal values that are set after config validation
|
// internal values that are set after config validation
|
||||||
redirectUrl *url.URL
|
redirectUrl *url.URL
|
||||||
proxyUrls []*url.URL
|
proxyUrls []*url.URL
|
||||||
CompiledRegex []*regexp.Regexp
|
CompiledRegex []*regexp.Regexp
|
||||||
|
provider providers.Provider
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOptions() *Options {
|
func NewOptions() *Options {
|
||||||
@ -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 {
|
func (o *Options) Validate() error {
|
||||||
msgs := make([]string, 0)
|
msgs := make([]string, 0)
|
||||||
if len(o.Upstreams) < 1 {
|
if len(o.Upstreams) < 1 {
|
||||||
@ -70,12 +90,7 @@ func (o *Options) Validate() error {
|
|||||||
msgs = append(msgs, "missing setting: client-secret")
|
msgs = append(msgs, "missing setting: client-secret")
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectUrl, err := url.Parse(o.RedirectUrl)
|
o.redirectUrl, msgs = parseUrl(o.RedirectUrl, "redirect", msgs)
|
||||||
if err != nil {
|
|
||||||
msgs = append(msgs, fmt.Sprintf(
|
|
||||||
"error parsing redirect-url=%q %s", o.RedirectUrl, err))
|
|
||||||
}
|
|
||||||
o.redirectUrl = redirectUrl
|
|
||||||
|
|
||||||
for _, u := range o.Upstreams {
|
for _, u := range o.Upstreams {
|
||||||
upstreamUrl, err := url.Parse(u)
|
upstreamUrl, err := url.Parse(u)
|
||||||
@ -98,6 +113,7 @@ func (o *Options) Validate() error {
|
|||||||
}
|
}
|
||||||
o.CompiledRegex = append(o.CompiledRegex, CompiledRegex)
|
o.CompiledRegex = append(o.CompiledRegex, CompiledRegex)
|
||||||
}
|
}
|
||||||
|
msgs = parseProviderInfo(o, msgs)
|
||||||
|
|
||||||
if len(msgs) != 0 {
|
if len(msgs) != 0 {
|
||||||
return fmt.Errorf("Invalid configuration:\n %s",
|
return fmt.Errorf("Invalid configuration:\n %s",
|
||||||
@ -105,3 +121,12 @@ func (o *Options) Validate() error {
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
@ -90,3 +90,15 @@ func TestCompiledRegexError(t *testing.T) {
|
|||||||
"unexpected ): `barquux)`"})
|
"unexpected ): `barquux)`"})
|
||||||
assert.Equal(t, expected, err.Error())
|
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
providers/google.go
Normal file
62
providers/google.go
Normal file
@ -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
providers/google_test.go
Normal file
94
providers/google_test.go
Normal file
@ -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
providers/provider_data.go
Normal file
14
providers/provider_data.go
Normal file
@ -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
providers/providers.go
Normal file
18
providers/providers.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user