GoogleClientID: ""
GoogleClientSecret: ""
+ # Allow users to log in to existing accounts using any verified
+ # email address listed by their Google account. If true, the
+ # Google People API must be enabled in order for Google login to
+ # work. If false, only the primary email address will be used.
+ GoogleAlternateEmailAddresses: true
+
# The cluster ID to delegate the user database. When set,
# logins on this cluster will be redirected to the login cluster
# (login cluster must appear in RemoteHosts with Proxy: true)
"Login": true,
"Login.GoogleClientID": false,
"Login.GoogleClientSecret": false,
+ "Login.GoogleAlternateEmailAddresses": false,
"Login.ProviderAppID": false,
"Login.ProviderAppSecret": false,
"Login.LoginCluster": true,
GoogleClientID: ""
GoogleClientSecret: ""
+ # Allow users to log in to existing accounts using any verified
+ # email address listed by their Google account. If true, the
+ # Google People API must be enabled in order for Google login to
+ # work. If false, only the primary email address will be used.
+ GoogleAlternateEmailAddresses: true
+
# The cluster ID to delegate the user database. When set,
# logins on this cluster will be redirected to the login cluster
# (login cluster must appear in RemoteHosts with Proxy: true)
if err != nil {
return ctrl.loginError(fmt.Errorf("error verifying ID token: %s", err))
}
- authinfo, err := ctrl.getAuthInfo(ctx, conf, oauth2Token, idToken)
+ authinfo, err := ctrl.getAuthInfo(ctx, cluster, conf, oauth2Token, idToken)
if err != nil {
return ctrl.loginError(err)
}
// primary address at index 0. The provided defaultAddr is always
// included in the returned slice, and is used as the primary if the
// Google API does not indicate one.
-func (ctrl *googleLoginController) getAuthInfo(ctx context.Context, conf *oauth2.Config, token *oauth2.Token, idToken *oidc.IDToken) (*rpc.UserSessionAuthInfo, error) {
+func (ctrl *googleLoginController) getAuthInfo(ctx context.Context, cluster *arvados.Cluster, conf *oauth2.Config, token *oauth2.Token, idToken *oidc.IDToken) (*rpc.UserSessionAuthInfo, error) {
var ret rpc.UserSessionAuthInfo
defer ctxlog.FromContext(ctx).Infof("ret: %#v", &ret) // debug
ret.Email = claims.Email
}
+ if !cluster.Login.GoogleAlternateEmailAddresses {
+ if ret.Email == "" {
+ return nil, fmt.Errorf("cannot log in with unverified email address %q", claims.Email)
+ }
+ return &ret, nil
+ }
+
svc, err := people.NewService(ctx, option.WithTokenSource(conf.TokenSource(ctx, token)), option.WithScopes(people.UserEmailsReadScope))
if err != nil {
return nil, fmt.Errorf("error setting up People API: %s", err)
}
person, err := people.NewPeopleService(svc).Get("people/me").Fields("emailAddresses,names").Do()
if err != nil {
- if strings.Contains(err.Error(), "Error 403") && strings.Contains(err.Error(), "accessNotConfigured") && ret.Email != "" {
- // Fall back on the primary email from the OAuth2 token.
- ctxlog.FromContext(ctx).WithError(err).WithField("email", ret.Email).Warn("cannot look up alternate email addresses because People API is not enabled")
- return &ret, nil
+ if strings.Contains(err.Error(), "Error 403") && strings.Contains(err.Error(), "accessNotConfigured") {
+ // Log the original API error, but display
+ // only the "fix config" advice to the user.
+ ctxlog.FromContext(ctx).WithError(err).WithField("email", ret.Email).Error("People API is not enabled")
+ return nil, errors.New("configuration error: Login.GoogleAlternateEmailAddresses is true, but Google People API is not enabled")
} else {
- // Unexpected error, or no email to fall back on.
return nil, fmt.Errorf("error getting profile info from People API: %s", err)
}
}
c.Check(resp.HTML.String(), check.Matches, `(?ms).*invalid OAuth2 state.*`)
}
+func (s *LoginSuite) setupPeopleAPIError(c *check.C) {
+ s.fakePeopleAPI = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ w.WriteHeader(http.StatusForbidden)
+ fmt.Fprintln(w, `Error 403: accessNotConfigured`)
+ }))
+ s.localdb.googleLoginController.peopleAPIBasePath = s.fakePeopleAPI.URL
+}
+
+func (s *LoginSuite) TestGoogleLogin_PeopleAPIDisabled(c *check.C) {
+ s.cluster.Login.GoogleAlternateEmailAddresses = false
+ s.authEmail = "joe.smith@primary.example.com"
+ s.setupPeopleAPIError(c)
+ state := s.startLogin(c)
+ _, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
+ Code: s.validCode,
+ State: state,
+ })
+ c.Check(err, check.IsNil)
+ authinfo := s.getCallbackAuthInfo(c)
+ c.Check(authinfo.Email, check.Equals, "joe.smith@primary.example.com")
+}
+
+func (s *LoginSuite) TestGoogleLogin_PeopleAPIError(c *check.C) {
+ s.setupPeopleAPIError(c)
+ state := s.startLogin(c)
+ resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
+ Code: s.validCode,
+ State: state,
+ })
+ c.Check(err, check.IsNil)
+ c.Check(resp.RedirectLocation, check.Equals, "")
+}
+
func (s *LoginSuite) TestGoogleLogin_Success(c *check.C) {
state := s.startLogin(c)
resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
Repositories string
}
Login struct {
- GoogleClientID string
- GoogleClientSecret string
- ProviderAppID string
- ProviderAppSecret string
- LoginCluster string
- RemoteTokenRefresh Duration
+ GoogleClientID string
+ GoogleClientSecret string
+ GoogleAlternateEmailAddresses bool
+ ProviderAppID string
+ ProviderAppSecret string
+ LoginCluster string
+ RemoteTokenRefresh Duration
}
Mail struct {
MailchimpAPIKey string