@@ -25,10 +25,16 @@ 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+
31+ // groupValidator is a function that determines if the user in the passed
32+ // session is a member of any of the configured Google groups.
33+ //
34+ // This hits the Google API for each group, so it is called on Redeem &
35+ // Refresh. `Authorize` uses the results of this saved in `session.Groups`
36+ // Since it is called on every request.
37+ groupValidator func(*sessions.SessionState) bool
3238}
3339
3440var _ Provider = (*GoogleProvider)(nil)
@@ -84,9 +90,9 @@ func NewGoogleProvider(p *ProviderData) *GoogleProvider {
8490 })
8591 return &GoogleProvider{
8692 ProviderData: p,
87− // Set a default GroupValidator to just always return valid (true), it will
93+ // Set a default groupValidator to just always return valid (true), it will
8894 // be overwritten if we configured a Google group restriction.
89− GroupValidator: func(email string) bool {
95+ groupValidator: func(*sessions.SessionState) bool {
9096 return true
9197 },
9298 }
@@ -118,14 +124,13 @@ func claimsFromIDToken(idToken string) (*claims, error) {
118124}
119125
120126// 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) {
127+func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code string) (*sessions.SessionState, error) {
122128 if code == "" {
123− err = errors.New("missing code")
124− return
129+ return nil, ErrMissingCode
125130 }
126131 clientSecret, err := p.GetClientSecret()
127132 if err != nil {
128− return
133+ return nil, err
129134 }
130135
131136 params := url.Values{}
@@ -155,12 +160,13 @@ func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code string) (
155160
156161 c, err := claimsFromIDToken(jsonResponse.IDToken)
157162 if err != nil {
158− return
163+ return nil, err
159164 }
160165
161166 created := time.Now()
162167 expires := time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second)
163− s = &sessions.SessionState{
168+
169+ return &sessions.SessionState{
164170 AccessToken: jsonResponse.AccessToken,
165171 IDToken: jsonResponse.IDToken,
166172 CreatedAt: &created,
@@ -168,18 +174,40 @@ func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code string) (
168174 RefreshToken: jsonResponse.RefreshToken,
169175 Email: c.Email,
170176 User: c.Subject,
171− }
172− return
177+ }, nil
178+}
179+
180+// EnrichSessionState checks the listed Google Groups configured and adds any
181+// that the user is a member of to session.Groups.
182+func (p *GoogleProvider) EnrichSessionState(ctx context.Context, s *sessions.SessionState) error {
183+ // TODO (@NickMeves) - Move to pure EnrichSessionState logic and stop
184+ // reusing legacy `groupValidator`.
185+ //
186+ // This is called here to get the validator to do the `session.Groups`
187+ // populating logic.
188+ p.groupValidator(s)
189+
190+ return nil
173191}
174192
175193// SetGroupRestriction configures the GoogleProvider to restrict access to the
176194// specified group(s). AdminEmail has to be an administrative email on the domain that is
177195// checked. CredentialsFile is the path to a json file containing a Google service
178196// account credentials.
197+//
198+// TODO (@NickMeves) - Unit Test this OR refactor away from groupValidator func
179199func (p *GoogleProvider) SetGroupRestriction(groups []string, adminEmail string, credentialsReader io.Reader) {
180200 adminService := getAdminService(adminEmail, credentialsReader)
181− p.GroupValidator = func(email string) bool {
182− return userInGroup(adminService, groups, email)
201+ p.groupValidator = func(s *sessions.SessionState) bool {
202+ // Reset our saved Groups in case membership changed
203+ // This is used by `Authorize` on every request
204+ s.Groups = make([]string, 0, len(groups))
205+ for _, group := range groups {
206+ if userInGroup(adminService, group, s.Email) {
207+ s.Groups = append(s.Groups, group)
208+ }
209+ }
210+ return len(s.Groups) > 0
183211 }
184212}
185213
@@ -203,52 +231,41 @@ func getAdminService(adminEmail string, credentialsReader io.Reader) *admin.Serv
203231 return adminService
204232}
205233
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)
234+func userInGroup(service *admin.Service, group string, email string) bool {
235+ // Use the HasMember API to checking for the user's presence in each group or nested subgroups
236+ req := service.Members.HasMember(group, email)
237+ r, err := req.Do()
238+ if err == nil {
239+ return r.IsMember
240+ }
241+
242+ gerr, ok := err.(*googleapi.Error)
243+ switch {
244+ case ok && gerr.Code == 404:
245+ logger.Errorf("error checking membership in group %s: group does not exist", group)
246+ case ok && gerr.Code == 400:
247+ // It is possible for Members.HasMember to return false even if the email is a group member.
248+ // One case that can cause this is if the user email is from a different domain than the group,
249+ // e.g. "member@otherdomain.com" in the group "group@mydomain.com" will result in a 400 error
250+ // from the HasMember API. In that case, attempt to query the member object directly from the group.
251+ req := service.Members.Get(group, email)
210252 r, err := req.Do()
211253 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
254+ logger.Errorf("error using get API to check member %s of google group %s: user not in the group", email, group)
255+ return false
238256 }
239− if r.IsMember {
257+
258+ // If the non-domain user is found within the group, still verify that they are "ACTIVE".
259+ // Do not count the user as belonging to a group if they have another status ("ARCHIVED", "SUSPENDED", or "UNKNOWN").
260+ if r.Status == "ACTIVE" {
240261 return true
241262 }
263+ default:
264+ logger.Errorf("error checking group membership: %v", err)
242265 }
243266 return false
244267}
245268
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−
252269// RefreshSessionIfNeeded checks if the session has expired and uses the
253270// RefreshToken to fetch a new ID token if required
254271func (p *GoogleProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) {
@@ -261,8 +278,11 @@ func (p *GoogleProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions
261278 return false, err
262279 }
263280
281+ // TODO (@NickMeves) - Align Group authorization needs with other providers'
282+ // behavior in the `RefreshSession` case.
283+ //
264284 // re-check that the user is in the proper google group(s)
265− if !p.ValidateGroup(s.Email) {
285+ if !p.groupValidator(s) {
266286 return false, fmt.Errorf("%s is no longer in the group(s)", s.Email)
267287 }
268288