-func (ctrl *googleLoginController) Login(ctx context.Context, cluster *arvados.Cluster, railsproxy *railsProxy, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
- provider, err := ctrl.getProvider()
- if err != nil {
- return ctrl.loginError(fmt.Errorf("error setting up OpenID Connect provider: %s", err))
- }
- redirURL, err := (*url.URL)(&cluster.Services.Controller.ExternalURL).Parse("/login")
- if err != nil {
- return ctrl.loginError(fmt.Errorf("error making redirect URL: %s", err))
- }
- conf := &oauth2.Config{
- ClientID: cluster.Login.GoogleClientID,
- ClientSecret: cluster.Login.GoogleClientSecret,
- Endpoint: provider.Endpoint(),
- Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
- RedirectURL: redirURL.String(),
- }
- verifier := provider.Verifier(&oidc.Config{
- ClientID: conf.ClientID,
- })
- if opts.State == "" {
- // Initiate Google sign-in.
- if opts.ReturnTo == "" {
- return ctrl.loginError(errors.New("missing return_to parameter"))
- }
- me := url.URL(cluster.Services.Controller.ExternalURL)
- callback, err := me.Parse("/" + arvados.EndpointLogin.Path)
- if err != nil {
- return ctrl.loginError(err)
- }
- conf.RedirectURL = callback.String()
- state := ctrl.newOAuth2State([]byte(cluster.SystemRootToken), opts.Remote, opts.ReturnTo)
- return arvados.LoginResponse{
- RedirectLocation: conf.AuthCodeURL(state.String(),
- // prompt=select_account tells Google
- // to show the "choose which Google
- // account" page, even if the client
- // is currently logged in to exactly
- // one Google account.
- oauth2.SetAuthURLParam("prompt", "select_account")),
- }, nil
- } else {
- // Callback after Google sign-in.
- state := ctrl.parseOAuth2State(opts.State)
- if !state.verify([]byte(cluster.SystemRootToken)) {
- return ctrl.loginError(errors.New("invalid OAuth2 state"))
- }
- oauth2Token, err := conf.Exchange(ctx, opts.Code)
- if err != nil {
- return ctrl.loginError(fmt.Errorf("error in OAuth2 exchange: %s", err))
- }
- rawIDToken, ok := oauth2Token.Extra("id_token").(string)
- if !ok {
- return ctrl.loginError(errors.New("error in OAuth2 exchange: no ID token in OAuth2 token"))
- }
- idToken, err := verifier.Verify(ctx, rawIDToken)
- if err != nil {
- return ctrl.loginError(fmt.Errorf("error verifying ID token: %s", err))
- }
- authinfo, err := ctrl.getAuthInfo(ctx, cluster, conf, oauth2Token, idToken)
- if err != nil {
- return ctrl.loginError(err)
- }
- ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{cluster.SystemRootToken}})
- return railsproxy.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
- ReturnTo: state.Remote + "," + state.ReturnTo,
- AuthInfo: *authinfo,
- })
- }
-}
-
-// Use a person's token to get all of their email addresses, with the
-// 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, cluster *arvados.Cluster, conf *oauth2.Config, token *oauth2.Token, idToken *oidc.IDToken) (*rpc.UserSessionAuthInfo, error) {
- var ret rpc.UserSessionAuthInfo
- defer ctxlog.FromContext(ctx).WithField("ret", &ret).Debug("getAuthInfo returned")
-
- var claims struct {
- Name string `json:"name"`
- Email string `json:"email"`
- Verified bool `json:"email_verified"`
- }
- if err := idToken.Claims(&claims); err != nil {
- return nil, fmt.Errorf("error extracting claims from ID token: %s", err)
- } else if claims.Verified {
- // Fall back to this info if the People API call
- // (below) doesn't return a primary && verified email.
- if names := strings.Fields(strings.TrimSpace(claims.Name)); len(names) > 1 {
- ret.FirstName = strings.Join(names[0:len(names)-1], " ")
- ret.LastName = names[len(names)-1]
- } else {
- ret.FirstName = names[0]
- }
- 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)
- }
- if p := ctrl.peopleAPIBasePath; p != "" {
- // Override normal API endpoint (for testing)
- svc.BasePath = p
- }
- person, err := people.NewPeopleService(svc).Get("people/me").PersonFields("emailAddresses,names").Do()
- if err != 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 {
- return nil, fmt.Errorf("error getting profile info from People API: %s", err)
- }
- }
-
- // The given/family names returned by the People API and
- // flagged as "primary" (if any) take precedence over the
- // split-by-whitespace result from above.
- for _, name := range person.Names {
- if name.Metadata != nil && name.Metadata.Primary {
- ret.FirstName = name.GivenName
- ret.LastName = name.FamilyName
- break
- }
- }