oauth2_proxy/providers/google_test.go
Justin Palpant 7d910c0ae8 Check Google group membership with hasMember and get. (#224)
* Check Google group membership with hasMember and get.

This PR is an enhancement built on
https://github.com/pusher/oauth2_proxy/pull/160. That PR reduces the
number of calls to the Google Admin API and simplifies the code by
using the hasMember method. It also supports checking membership in
nested groups.

However, the above message doesn't handle members who are not a part
of the domain. The hasMember API returns a 400 for that case. As a
fallback, when the API returns a 400, this change will try using the
`get` API which works as expected for members who aren't a part of the
domain. Supporting members who belong to the Google group but aren't
part of the domain is a requested feature from
https://github.com/pusher/oauth2_proxy/issues/95.

https://developers.google.com/admin-sdk/directory/v1/reference/members/get

Note that nested members who are not a part of the domain will not be
correctly detected with this change.

* Update CHANGELOG.

* Fix incorrect JSON and stop escaping strings.

* Add comments for each scenario.
2019-08-06 10:38:24 +01:00

240 lines
7.8 KiB
Go

package providers
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
admin "google.golang.org/api/admin/directory/v1"
option "google.golang.org/api/option"
)
func newRedeemServer(body []byte) (*url.URL, *httptest.Server) {
s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write(body)
}))
u, _ := url.Parse(s.URL)
return u, s
}
func newGoogleProvider() *GoogleProvider {
return NewGoogleProvider(
&ProviderData{
ProviderName: "",
LoginURL: &url.URL{},
RedeemURL: &url.URL{},
ProfileURL: &url.URL{},
ValidateURL: &url.URL{},
Scope: ""})
}
func TestGoogleProviderDefaults(t *testing.T) {
p := newGoogleProvider()
assert.NotEqual(t, nil, p)
assert.Equal(t, "Google", p.Data().ProviderName)
assert.Equal(t, "https://accounts.google.com/o/oauth2/auth?access_type=offline",
p.Data().LoginURL.String())
assert.Equal(t, "https://www.googleapis.com/oauth2/v3/token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://www.googleapis.com/oauth2/v1/tokeninfo",
p.Data().ValidateURL.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"},
ValidateURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/tokeninfo"},
Scope: "profile"})
assert.NotEqual(t, nil, p)
assert.Equal(t, "Google", p.Data().ProviderName)
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, "https://example.com/oauth/tokeninfo",
p.Data().ValidateURL.String())
assert.Equal(t, "profile", p.Data().Scope)
}
type redeemResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
IDToken string `json:"id_token"`
}
func TestGoogleProviderGetEmailAddress(t *testing.T) {
p := newGoogleProvider()
body, err := json.Marshal(redeemResponse{
AccessToken: "a1234",
ExpiresIn: 10,
RefreshToken: "refresh12345",
IDToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"email": "michael.bland@gsa.gov", "email_verified":true}`)),
})
assert.Equal(t, nil, err)
var server *httptest.Server
p.RedeemURL, server = newRedeemServer(body)
defer server.Close()
session, err := p.Redeem("http://redirect/", "code1234")
assert.Equal(t, nil, err)
assert.NotEqual(t, session, nil)
assert.Equal(t, "michael.bland@gsa.gov", session.Email)
assert.Equal(t, "a1234", session.AccessToken)
assert.Equal(t, "refresh12345", session.RefreshToken)
}
func TestGoogleProviderValidateGroup(t *testing.T) {
p := newGoogleProvider()
p.GroupValidator = func(email string) bool {
return email == "michael.bland@gsa.gov"
}
assert.Equal(t, true, p.ValidateGroup("michael.bland@gsa.gov"))
p.GroupValidator = func(email string) bool {
return email != "michael.bland@gsa.gov"
}
assert.Equal(t, false, p.ValidateGroup("michael.bland@gsa.gov"))
}
func TestGoogleProviderWithoutValidateGroup(t *testing.T) {
p := newGoogleProvider()
assert.Equal(t, true, p.ValidateGroup("michael.bland@gsa.gov"))
}
//
func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) {
p := newGoogleProvider()
body, err := json.Marshal(redeemResponse{
AccessToken: "a1234",
IDToken: "ignored prefix." + `{"email": "michael.bland@gsa.gov"}`,
})
assert.Equal(t, nil, err)
var server *httptest.Server
p.RedeemURL, server = newRedeemServer(body)
defer server.Close()
session, err := p.Redeem("http://redirect/", "code1234")
assert.NotEqual(t, nil, err)
if session != nil {
t.Errorf("expect nill session %#v", session)
}
}
func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) {
p := newGoogleProvider()
body, err := json.Marshal(redeemResponse{
AccessToken: "a1234",
IDToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"email": michael.bland@gsa.gov}`)),
})
assert.Equal(t, nil, err)
var server *httptest.Server
p.RedeemURL, server = newRedeemServer(body)
defer server.Close()
session, err := p.Redeem("http://redirect/", "code1234")
assert.NotEqual(t, nil, err)
if session != nil {
t.Errorf("expect nill session %#v", session)
}
}
func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
p := newGoogleProvider()
body, err := json.Marshal(redeemResponse{
AccessToken: "a1234",
IDToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"not_email": "missing"}`)),
})
assert.Equal(t, nil, err)
var server *httptest.Server
p.RedeemURL, server = newRedeemServer(body)
defer server.Close()
session, err := p.Redeem("http://redirect/", "code1234")
assert.NotEqual(t, nil, err)
if session != nil {
t.Errorf("expect nill session %#v", session)
}
}
func TestGoogleProviderUserInGroup(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/groups/group@example.com/hasMember/member-in-domain@example.com" {
fmt.Fprintln(w, `{"isMember": true}`)
} else if r.URL.Path == "/groups/group@example.com/hasMember/non-member-in-domain@example.com" {
fmt.Fprintln(w, `{"isMember": false}`)
} else if r.URL.Path == "/groups/group@example.com/hasMember/member-out-of-domain@otherexample.com" {
http.Error(
w,
`{"error": {"errors": [{"domain": "global","reason": "invalid","message": "Invalid Input: memberKey"}],"code": 400,"message": "Invalid Input: memberKey"}}`,
http.StatusBadRequest,
)
} else if r.URL.Path == "/groups/group@example.com/hasMember/non-member-out-of-domain@otherexample.com" {
http.Error(
w,
`{"error": {"errors": [{"domain": "global","reason": "invalid","message": "Invalid Input: memberKey"}],"code": 400,"message": "Invalid Input: memberKey"}}`,
http.StatusBadRequest,
)
} else if r.URL.Path == "/groups/group@example.com/members/non-member-out-of-domain@otherexample.com" {
// note that the client currently doesn't care what this response text or code is - any error here results in failure to match the group
http.Error(
w,
`{"error": {"errors": [{"domain": "global","reason": "notFound","message": "Resource Not Found: memberKey"}],"code": 404,"message": "Resource Not Found: memberKey"}}`,
http.StatusNotFound,
)
} else if r.URL.Path == "/groups/group@example.com/members/member-out-of-domain@otherexample.com" {
fmt.Fprintln(w,
`{"kind": "admin#directory#member","etag":"12345","id":"1234567890","email": "member-out-of-domain@otherexample.com","role": "MEMBER","type": "USER","status": "ACTIVE","delivery_settings": "ALL_MAIL"}}`,
)
}
}))
defer ts.Close()
client := ts.Client()
ctx := context.Background()
service, err := admin.NewService(ctx, option.WithHTTPClient(client))
service.BasePath = ts.URL
assert.Equal(t, nil, err)
result := userInGroup(service, []string{"group@example.com"}, "member-in-domain@example.com")
assert.True(t, result)
result = userInGroup(service, []string{"group@example.com"}, "member-out-of-domain@otherexample.com")
assert.True(t, result)
result = userInGroup(service, []string{"group@example.com"}, "non-member-in-domain@example.com")
assert.False(t, result)
result = userInGroup(service, []string{"group@example.com"}, "non-member-out-of-domain@otherexample.com")
assert.False(t, result)
}