1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
22 "git.arvados.org/arvados.git/lib/controller/rpc"
23 "git.arvados.org/arvados.git/sdk/go/arvados"
24 "git.arvados.org/arvados.git/sdk/go/auth"
25 "git.arvados.org/arvados.git/sdk/go/ctxlog"
26 "git.arvados.org/arvados.git/sdk/go/httpserver"
27 "github.com/coreos/go-oidc"
29 "google.golang.org/api/option"
30 "google.golang.org/api/people/v1"
33 type googleLoginController struct {
34 Cluster *arvados.Cluster
35 RailsProxy *railsProxy
37 issuer string // override OIDC issuer URL (normally https://accounts.google.com) for testing
38 peopleAPIBasePath string // override Google People API base URL (normally set by google pkg to https://people.googleapis.com/)
39 provider *oidc.Provider
43 func (ctrl *googleLoginController) getProvider() (*oidc.Provider, error) {
45 defer ctrl.mu.Unlock()
46 if ctrl.provider == nil {
49 issuer = "https://accounts.google.com"
51 provider, err := oidc.NewProvider(context.Background(), issuer)
55 ctrl.provider = provider
57 return ctrl.provider, nil
60 func (ctrl *googleLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
61 return noopLogout(ctrl.Cluster, opts)
64 func (ctrl *googleLoginController) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
65 provider, err := ctrl.getProvider()
67 return loginError(fmt.Errorf("error setting up OpenID Connect provider: %s", err))
69 redirURL, err := (*url.URL)(&ctrl.Cluster.Services.Controller.ExternalURL).Parse("/login")
71 return loginError(fmt.Errorf("error making redirect URL: %s", err))
73 conf := &oauth2.Config{
74 ClientID: ctrl.Cluster.Login.Google.ClientID,
75 ClientSecret: ctrl.Cluster.Login.Google.ClientSecret,
76 Endpoint: provider.Endpoint(),
77 Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
78 RedirectURL: redirURL.String(),
80 verifier := provider.Verifier(&oidc.Config{
81 ClientID: conf.ClientID,
84 // Initiate Google sign-in.
85 if opts.ReturnTo == "" {
86 return loginError(errors.New("missing return_to parameter"))
88 me := url.URL(ctrl.Cluster.Services.Controller.ExternalURL)
89 callback, err := me.Parse("/" + arvados.EndpointLogin.Path)
91 return loginError(err)
93 conf.RedirectURL = callback.String()
94 state := ctrl.newOAuth2State([]byte(ctrl.Cluster.SystemRootToken), opts.Remote, opts.ReturnTo)
95 return arvados.LoginResponse{
96 RedirectLocation: conf.AuthCodeURL(state.String(),
97 // prompt=select_account tells Google
98 // to show the "choose which Google
99 // account" page, even if the client
100 // is currently logged in to exactly
101 // one Google account.
102 oauth2.SetAuthURLParam("prompt", "select_account")),
105 // Callback after Google sign-in.
106 state := ctrl.parseOAuth2State(opts.State)
107 if !state.verify([]byte(ctrl.Cluster.SystemRootToken)) {
108 return loginError(errors.New("invalid OAuth2 state"))
110 oauth2Token, err := conf.Exchange(ctx, opts.Code)
112 return loginError(fmt.Errorf("error in OAuth2 exchange: %s", err))
114 rawIDToken, ok := oauth2Token.Extra("id_token").(string)
116 return loginError(errors.New("error in OAuth2 exchange: no ID token in OAuth2 token"))
118 idToken, err := verifier.Verify(ctx, rawIDToken)
120 return loginError(fmt.Errorf("error verifying ID token: %s", err))
122 authinfo, err := ctrl.getAuthInfo(ctx, ctrl.Cluster, conf, oauth2Token, idToken)
124 return loginError(err)
126 ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{ctrl.Cluster.SystemRootToken}})
127 return ctrl.RailsProxy.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
128 ReturnTo: state.Remote + "," + state.ReturnTo,
134 func (ctrl *googleLoginController) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
135 return arvados.APIClientAuthorization{}, httpserver.ErrorWithStatus(errors.New("username/password authentication is not available"), http.StatusBadRequest)
138 // Use a person's token to get all of their email addresses, with the
139 // primary address at index 0. The provided defaultAddr is always
140 // included in the returned slice, and is used as the primary if the
141 // Google API does not indicate one.
142 func (ctrl *googleLoginController) getAuthInfo(ctx context.Context, cluster *arvados.Cluster, conf *oauth2.Config, token *oauth2.Token, idToken *oidc.IDToken) (*rpc.UserSessionAuthInfo, error) {
143 var ret rpc.UserSessionAuthInfo
144 defer ctxlog.FromContext(ctx).WithField("ret", &ret).Debug("getAuthInfo returned")
147 Name string `json:"name"`
148 Email string `json:"email"`
149 Verified bool `json:"email_verified"`
151 if err := idToken.Claims(&claims); err != nil {
152 return nil, fmt.Errorf("error extracting claims from ID token: %s", err)
153 } else if claims.Verified {
154 // Fall back to this info if the People API call
155 // (below) doesn't return a primary && verified email.
156 if names := strings.Fields(strings.TrimSpace(claims.Name)); len(names) > 1 {
157 ret.FirstName = strings.Join(names[0:len(names)-1], " ")
158 ret.LastName = names[len(names)-1]
160 ret.FirstName = names[0]
162 ret.Email = claims.Email
165 if !ctrl.Cluster.Login.Google.AlternateEmailAddresses {
167 return nil, fmt.Errorf("cannot log in with unverified email address %q", claims.Email)
172 svc, err := people.NewService(ctx, option.WithTokenSource(conf.TokenSource(ctx, token)), option.WithScopes(people.UserEmailsReadScope))
174 return nil, fmt.Errorf("error setting up People API: %s", err)
176 if p := ctrl.peopleAPIBasePath; p != "" {
177 // Override normal API endpoint (for testing)
180 person, err := people.NewPeopleService(svc).Get("people/me").PersonFields("emailAddresses,names").Do()
182 if strings.Contains(err.Error(), "Error 403") && strings.Contains(err.Error(), "accessNotConfigured") {
183 // Log the original API error, but display
184 // only the "fix config" advice to the user.
185 ctxlog.FromContext(ctx).WithError(err).WithField("email", ret.Email).Error("People API is not enabled")
186 return nil, errors.New("configuration error: Login.GoogleAlternateEmailAddresses is true, but Google People API is not enabled")
188 return nil, fmt.Errorf("error getting profile info from People API: %s", err)
192 // The given/family names returned by the People API and
193 // flagged as "primary" (if any) take precedence over the
194 // split-by-whitespace result from above.
195 for _, name := range person.Names {
196 if name.Metadata != nil && name.Metadata.Primary {
197 ret.FirstName = name.GivenName
198 ret.LastName = name.FamilyName
203 altEmails := map[string]bool{}
205 altEmails[ret.Email] = true
207 for _, ea := range person.EmailAddresses {
208 if ea.Metadata == nil || !ea.Metadata.Verified {
209 ctxlog.FromContext(ctx).WithField("address", ea.Value).Info("skipping unverified email address")
212 altEmails[ea.Value] = true
213 if ea.Metadata.Primary || ret.Email == "" {
217 if len(altEmails) == 0 {
218 return nil, errors.New("cannot log in without a verified email address")
220 for ae := range altEmails {
222 ret.AlternateEmails = append(ret.AlternateEmails, ae)
223 if i := strings.Index(ae, "@"); i > 0 && strings.ToLower(ae[i+1:]) == strings.ToLower(ctrl.Cluster.Users.PreferDomainForUsername) {
224 ret.Username = strings.SplitN(ae[:i], "+", 2)[0]
231 func loginError(sendError error) (resp arvados.LoginResponse, err error) {
232 tmpl, err := template.New("error").Parse(`<h2>Login error:</h2><p>{{.}}</p>`)
236 err = tmpl.Execute(&resp.HTML, sendError.Error())
240 func (ctrl *googleLoginController) newOAuth2State(key []byte, remote, returnTo string) oauth2State {
242 Time: time.Now().Unix(),
246 s.HMAC = s.computeHMAC(key)
250 type oauth2State struct {
251 HMAC []byte // hash of other fields; see computeHMAC()
252 Time int64 // creation time (unix timestamp)
253 Remote string // remote cluster if requesting a salted token, otherwise blank
254 ReturnTo string // redirect target
257 func (ctrl *googleLoginController) parseOAuth2State(encoded string) (s oauth2State) {
258 // Errors are not checked. If decoding/parsing fails, the
259 // token will be rejected by verify().
260 decoded, _ := base64.RawURLEncoding.DecodeString(encoded)
261 f := strings.Split(string(decoded), "\n")
265 fmt.Sscanf(f[0], "%x", &s.HMAC)
266 fmt.Sscanf(f[1], "%x", &s.Time)
267 fmt.Sscanf(f[2], "%s", &s.Remote)
268 fmt.Sscanf(f[3], "%s", &s.ReturnTo)
272 func (s oauth2State) verify(key []byte) bool {
273 if delta := time.Now().Unix() - s.Time; delta < 0 || delta > 300 {
276 return hmac.Equal(s.computeHMAC(key), s.HMAC)
279 func (s oauth2State) String() string {
281 enc := base64.NewEncoder(base64.RawURLEncoding, &buf)
282 fmt.Fprintf(enc, "%x\n%x\n%s\n%s", s.HMAC, s.Time, s.Remote, s.ReturnTo)
287 func (s oauth2State) computeHMAC(key []byte) []byte {
288 mac := hmac.New(sha256.New, key)
289 fmt.Fprintf(mac, "%x %s %s", s.Time, s.Remote, s.ReturnTo)