diff --git a/providers/google.go b/providers/google.go new file mode 100644 index 0000000..9442113 --- /dev/null +++ b/providers/google.go @@ -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) +} diff --git a/providers/google_test.go b/providers/google_test.go new file mode 100644 index 0000000..68f2ff0 --- /dev/null +++ b/providers/google_test.go @@ -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) +} diff --git a/providers/provider_data.go b/providers/provider_data.go new file mode 100644 index 0000000..a533b6e --- /dev/null +++ b/providers/provider_data.go @@ -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 } diff --git a/providers/providers.go b/providers/providers.go new file mode 100644 index 0000000..8076497 --- /dev/null +++ b/providers/providers.go @@ -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) + } +}