git.iko.soy/typeable/oauth2-proxy
on master help sign in
Replace ValidateGroup with Authorize for Provider
commite7ac79304402e9a1829d4ad9a4d8d04b5b384887 authorNick Meves <nick.meves@greenhouse.io> dateSat 26 Sep 2020, 17:29 (6 years ago) parentdf56a34
5 files changed+139−76
oauthproxy.go+9−5 providers/google.go+71−51 providers/google_test.go+52−19 providers/provider_default.go+6 providers/providers.go+1−1
oauthproxy.go+9−5
@@ -909,7 +909,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
909909 }
910910
911911 // set cookie, or deny
912− if p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {
912+ if p.Validator(session.Email) {
913913 logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session)
914914 err := p.SaveSession(rw, req, session)
915915 if err != nil {
@@ -991,15 +991,19 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
991991 return nil, ErrNeedsLogin
992992 }
993993
994− invalidEmail := session != nil && session.Email != "" && !p.Validator(session.Email)
994+ invalidEmail := session.Email != "" && !p.Validator(session.Email)
995995 invalidGroups := session != nil && !p.validateGroups(session.Groups)
996+ authorized, err := p.provider.Authorize(req.Context(), session)
997+ if err != nil {
998+ logger.Errorf("Error with authorization: %v", err)
999+ }
9961000
997− if invalidEmail || invalidGroups {
998− logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
1001+ if invalidEmail || invalidGroups || !authorized {
1002+ logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
9991003 // Invalid session, clear it
10001004 err := p.ClearSessionCookie(rw, req)
10011005 if err != nil {
1002− logger.Printf("Error clearing session cookie: %v", err)
1006+ logger.Errorf("Error clearing session cookie: %v", err)
10031007 }
10041008 return nil, ErrNeedsLogin
10051009 }
providers/google.go+71−51
@@ -25,10 +25,13 @@ import (
2525// GoogleProvider represents an Google based Identity Provider
2626type GoogleProvider struct {
2727 *ProviderData
28+
2829 RedeemRefreshURL *url.URL
29− // GroupValidator is a function that determines if the passed email is in
30− // the configured Google group.
31− GroupValidator func(string) bool
30+ // GroupValidator is a function that determines if the user in the passed
31+ // session is a member of any of the configured Google groups.
32+ GroupValidator func(*sessions.SessionState, bool) bool
33+
34+ allowedGroups map[string]struct{}
3235}
3336
3437var _ Provider = (*GoogleProvider)(nil)
@@ -86,7 +89,7 @@ func NewGoogleProvider(p *ProviderData) *GoogleProvider {
8689 ProviderData: p,
8790 // Set a default GroupValidator to just always return valid (true), it will
8891 // be overwritten if we configured a Google group restriction.
89− GroupValidator: func(email string) bool {
92+ GroupValidator: func(*sessions.SessionState, bool) bool {
9093 return true
9194 },
9295 }
@@ -118,14 +121,14 @@ func claimsFromIDToken(idToken string) (*claims, error) {
118121}
119122
120123// Redeem exchanges the OAuth2 authentication token for an ID token
121−func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) {
124+func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code string) (*sessions.SessionState, error) {
122125 if code == "" {
123− err = errors.New("missing code")
124− return
126+ err := errors.New("missing code")
127+ return nil, err
125128 }
126129 clientSecret, err := p.GetClientSecret()
127130 if err != nil {
128− return
131+ return nil, err
129132 }
130133
131134 params := url.Values{}
@@ -155,12 +158,12 @@ func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code string) (
155158
156159 c, err := claimsFromIDToken(jsonResponse.IDToken)
157160 if err != nil {
158− return
161+ return nil, err
159162 }
160163
161164 created := time.Now()
162165 expires := time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second)
163− s = &sessions.SessionState{
166+ s := &sessions.SessionState{
164167 AccessToken: jsonResponse.AccessToken,
165168 IDToken: jsonResponse.IDToken,
166169 CreatedAt: &created,
@@ -169,7 +172,13 @@ func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code string) (
169172 Email: c.Email,
170173 User: c.Subject,
171174 }
172− return
175+ p.GroupValidator(s, true)
176+
177+ return s, nil
178+}
179+
180+func (p *GoogleProvider) Authorize(ctx context.Context, s *sessions.SessionState) (bool, error) {
181+ return p.GroupValidator(s, false), nil
173182}
174183
175184// SetGroupRestriction configures the GoogleProvider to restrict access to the
@@ -178,8 +187,30 @@ func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code string) (
178187// account credentials.
179188func (p *GoogleProvider) SetGroupRestriction(groups []string, adminEmail string, credentialsReader io.Reader) {
180189 adminService := getAdminService(adminEmail, credentialsReader)
181− p.GroupValidator = func(email string) bool {
182− return userInGroup(adminService, groups, email)
190+ for _, group := range groups {
191+ p.allowedGroups[group] = struct{}{}
192+ }
193+
194+ p.GroupValidator = func(s *sessions.SessionState, sync bool) bool {
195+ if sync {
196+ // Reset our saved Groups in case membership changed
197+ s.Groups = make([]string, 0, len(groups))
198+ for _, group := range groups {
199+ if userInGroup(adminService, group, s.Email) {
200+ s.Groups = append(s.Groups, group)
201+ }
202+ }
203+ return len(s.Groups) > 0
204+ }
205+
206+ // Don't resync with Google, handles when OAuth2-Proxy settings
207+ // alter allowed groups but existing sessions are still valid
208+ for _, group := range s.Groups {
209+ if _, ok := p.allowedGroups[group]; ok {
210+ return true
211+ }
212+ }
213+ return false
183214 }
184215}
185216
@@ -203,52 +234,41 @@ func getAdminService(adminEmail string, credentialsReader io.Reader) *admin.Serv
203234 return adminService
204235}
205236
206−func userInGroup(service *admin.Service, groups []string, email string) bool {
207− for _, group := range groups {
208− // Use the HasMember API to checking for the user's presence in each group or nested subgroups
209− req := service.Members.HasMember(group, email)
237+func userInGroup(service *admin.Service, group string, email string) bool {
238+ // Use the HasMember API to checking for the user's presence in each group or nested subgroups
239+ req := service.Members.HasMember(group, email)
240+ r, err := req.Do()
241+ if err == nil {
242+ return r.IsMember
243+ }
244+
245+ gerr, ok := err.(*googleapi.Error)
246+ switch {
247+ case ok && gerr.Code == 404:
248+ logger.Errorf("error checking membership in group %s: group does not exist", group)
249+ case ok && gerr.Code == 400:
250+ // It is possible for Members.HasMember to return false even if the email is a group member.
251+ // One case that can cause this is if the user email is from a different domain than the group,
252+ // e.g. "member@otherdomain.com" in the group "group@mydomain.com" will result in a 400 error
253+ // from the HasMember API. In that case, attempt to query the member object directly from the group.
254+ req := service.Members.Get(group, email)
210255 r, err := req.Do()
211256 if err != nil {
212− gerr, ok := err.(*googleapi.Error)
213− switch {
214− case ok && gerr.Code == 404:
215− logger.Errorf("error checking membership in group %s: group does not exist", group)
216− case ok && gerr.Code == 400:
217− // It is possible for Members.HasMember to return false even if the email is a group member.
218− // One case that can cause this is if the user email is from a different domain than the group,
219− // e.g. "member@otherdomain.com" in the group "group@mydomain.com" will result in a 400 error
220− // from the HasMember API. In that case, attempt to query the member object directly from the group.
221− req := service.Members.Get(group, email)
222− r, err := req.Do()
223−
224− if err != nil {
225− logger.Errorf("error using get API to check member %s of google group %s: user not in the group", email, group)
226− continue
227− }
228−
229− // If the non-domain user is found within the group, still verify that they are "ACTIVE".
230− // Do not count the user as belonging to a group if they have another status ("ARCHIVED", "SUSPENDED", or "UNKNOWN").
231− if r.Status == "ACTIVE" {
232− return true
233− }
234− default:
235− logger.Errorf("error checking group membership: %v", err)
236− }
237− continue
257+ logger.Errorf("error using get API to check member %s of google group %s: user not in the group", email, group)
258+ return false
238259 }
239− if r.IsMember {
260+
261+ // If the non-domain user is found within the group, still verify that they are "ACTIVE".
262+ // Do not count the user as belonging to a group if they have another status ("ARCHIVED", "SUSPENDED", or "UNKNOWN").
263+ if r.Status == "ACTIVE" {
240264 return true
241265 }
266+ default:
267+ logger.Errorf("error checking group membership: %v", err)
242268 }
243269 return false
244270}
245271
246−// ValidateGroup validates that the provided email exists in the configured Google
247−// group(s).
248−func (p *GoogleProvider) ValidateGroup(email string) bool {
249− return p.GroupValidator(email)
250−}
251−
252272// RefreshSessionIfNeeded checks if the session has expired and uses the
253273// RefreshToken to fetch a new ID token if required
254274func (p *GoogleProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) {
@@ -262,7 +282,7 @@ func (p *GoogleProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions
262282 }
263283
264284 // re-check that the user is in the proper google group(s)
265− if !p.ValidateGroup(s.Email) {
285+ if !p.GroupValidator(s, true) {
266286 return false, fmt.Errorf("%s is no longer in the group(s)", s.Email)
267287 }
268288
providers/google_test.go+52−19
@@ -10,6 +10,7 @@ import (
1010 "net/url"
1111 "testing"
1212
13+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
1314 . "github.com/onsi/gomega"
1415 "github.com/stretchr/testify/assert"
1516 admin "google.golang.org/api/admin/directory/v1"
@@ -109,21 +110,52 @@ func TestGoogleProviderGetEmailAddress(t *testing.T) {
109110 assert.Equal(t, "refresh12345", session.RefreshToken)
110111}
111112
112−func TestGoogleProviderValidateGroup(t *testing.T) {
113− p := newGoogleProvider()
114− p.GroupValidator = func(email string) bool {
115− return email == "michael.bland@gsa.gov"
113+func TestGoogleProviderAuthorize(t *testing.T) {
114+ const sessionEmail = "michael.bland@gsa.gov"
115+
116+ testCases := map[string]struct {
117+ session *sessions.SessionState
118+ validatorFunc func(*sessions.SessionState, bool) bool
119+ expectedAuthZ bool
120+ }{
121+ "Email is authorized with GroupValidator": {
122+ session: &sessions.SessionState{
123+ Email: sessionEmail,
124+ },
125+ validatorFunc: func(s *sessions.SessionState, _ bool) bool {
126+ return s.Email == sessionEmail
127+ },
128+ expectedAuthZ: true,
129+ },
130+ "Email is denied with GroupValidator": {
131+ session: &sessions.SessionState{
132+ Email: sessionEmail,
133+ },
134+ validatorFunc: func(s *sessions.SessionState, _ bool) bool {
135+ return s.Email != sessionEmail
136+ },
137+ expectedAuthZ: false,
138+ },
139+ "Default does no authorization checks": {
140+ session: &sessions.SessionState{
141+ Email: sessionEmail,
142+ },
143+ validatorFunc: nil,
144+ expectedAuthZ: true,
145+ },
116146 }
117− assert.Equal(t, true, p.ValidateGroup("michael.bland@gsa.gov"))
118− p.GroupValidator = func(email string) bool {
119− return email != "michael.bland@gsa.gov"
147+ for name, tc := range testCases {
148+ t.Run(name, func(t *testing.T) {
149+ g := NewWithT(t)
150+ p := newGoogleProvider()
151+ if tc.validatorFunc != nil {
152+ p.GroupValidator = tc.validatorFunc
153+ }
154+ authorized, err := p.Authorize(context.Background(), tc.session)
155+ g.Expect(err).ToNot(HaveOccurred())
156+ g.Expect(authorized).To(Equal(tc.expectedAuthZ))
157+ })
120158 }
121− assert.Equal(t, false, p.ValidateGroup("michael.bland@gsa.gov"))
122−}
123−
124−func TestGoogleProviderWithoutValidateGroup(t *testing.T) {
125− p := newGoogleProvider()
126− assert.Equal(t, true, p.ValidateGroup("michael.bland@gsa.gov"))
127159}
128160
129161//
@@ -196,7 +228,7 @@ func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
196228
197229}
198230
199−func TestGoogleProviderUserInGroup(t *testing.T) {
231+func TestGoogleProvider_userInGroup(t *testing.T) {
200232 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
201233 if r.URL.Path == "/groups/group@example.com/hasMember/member-in-domain@example.com" {
202234 fmt.Fprintln(w, `{"isMember": true}`)
@@ -233,18 +265,19 @@ func TestGoogleProviderUserInGroup(t *testing.T) {
233265 ctx := context.Background()
234266
235267 service, err := admin.NewService(ctx, option.WithHTTPClient(client))
268+ assert.NoError(t, err)
269+
236270 service.BasePath = ts.URL
237− assert.Equal(t, nil, err)
238271
239− result := userInGroup(service, []string{"group@example.com"}, "member-in-domain@example.com")
272+ result := userInGroup(service, "group@example.com", "member-in-domain@example.com")
240273 assert.True(t, result)
241274
242− result = userInGroup(service, []string{"group@example.com"}, "member-out-of-domain@otherexample.com")
275+ result = userInGroup(service, "group@example.com", "member-out-of-domain@otherexample.com")
243276 assert.True(t, result)
244277
245− result = userInGroup(service, []string{"group@example.com"}, "non-member-in-domain@example.com")
278+ result = userInGroup(service, "group@example.com", "non-member-in-domain@example.com")
246279 assert.False(t, result)
247280
248− result = userInGroup(service, []string{"group@example.com"}, "non-member-out-of-domain@otherexample.com")
281+ result = userInGroup(service, "group@example.com", "non-member-out-of-domain@otherexample.com")
249282 assert.False(t, result)
250283}
providers/provider_default.go+6
@@ -104,6 +104,12 @@ func (p *ProviderData) EnrichSessionState(_ context.Context, _ *sessions.Session
104104 return nil
105105}
106106
107+// Authorize performs global authorization on an authenticated session.
108+// This is not used for fine-grained per route authorization rules.
109+func (p *ProviderData) Authorize(ctx context.Context, s *sessions.SessionState) (bool, error) {
110+ return true, nil
111+}
112+
107113// ValidateSessionState validates the AccessToken
108114func (p *ProviderData) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool {
109115 return validateToken(ctx, p, s.AccessToken, nil)
providers/providers.go+1−1
@@ -13,8 +13,8 @@ type Provider interface {
1313 // DEPRECATED: Migrate to EnrichSessionState
1414 GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error)
1515 Redeem(ctx context.Context, redirectURI, code string) (*sessions.SessionState, error)
16− ValidateGroup(string) bool
1716 EnrichSessionState(ctx context.Context, s *sessions.SessionState) error
17+ Authorize(ctx context.Context, s *sessions.SessionState) (bool, error)
1818 ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool
1919 GetLoginURL(redirectURI, finalRedirect string) string
2020 RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error)