@@ -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