go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/encryptedcookies/method.go (about) 1 // Copyright 2021 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package encryptedcookies 16 17 import ( 18 "context" 19 "encoding/base64" 20 "net/http" 21 "net/url" 22 "strings" 23 "sync" 24 25 "github.com/google/tink/go/tink" 26 "golang.org/x/oauth2" 27 "google.golang.org/protobuf/proto" 28 "google.golang.org/protobuf/types/known/timestamppb" 29 30 "go.chromium.org/luci/auth/identity" 31 "go.chromium.org/luci/common/clock" 32 "go.chromium.org/luci/common/data/stringset" 33 "go.chromium.org/luci/common/errors" 34 "go.chromium.org/luci/common/logging" 35 "go.chromium.org/luci/common/retry/transient" 36 37 "go.chromium.org/luci/server/auth" 38 "go.chromium.org/luci/server/auth/openid" 39 "go.chromium.org/luci/server/encryptedcookies/internal" 40 "go.chromium.org/luci/server/encryptedcookies/internal/encryptedcookiespb" 41 "go.chromium.org/luci/server/encryptedcookies/session" 42 "go.chromium.org/luci/server/encryptedcookies/session/sessionpb" 43 "go.chromium.org/luci/server/router" 44 ) 45 46 // OpenIDConfig is a configuration related to OpenID Connect provider. 47 // 48 // All parameters are required. 49 type OpenIDConfig struct { 50 // DiscoveryURL is where to grab discovery document with provider's config. 51 DiscoveryURL string 52 53 // ClientID identifies OAuth2 Web client representing the application. 54 // 55 // Can be obtained by registering the OAuth2 client with the identity 56 // provider. 57 ClientID string 58 59 // ClientSecret is a secret associated with ClientID. 60 // 61 // Can be obtained by registering the OAuth2 client with the identity 62 // provider. 63 ClientSecret string 64 65 // RedirectURI must be `https://<host>/auth/openid/callback`. 66 // 67 // The OAuth2 client should be configured to allow this redirect URL. 68 RedirectURI string 69 } 70 71 // discoveryDoc returns the cached OpenID discovery document. 72 func (cfg *OpenIDConfig) discoveryDoc(ctx context.Context) (*openid.DiscoveryDoc, error) { 73 // FetchDiscoveryDoc implements caching inside. 74 doc, err := openid.FetchDiscoveryDoc(ctx, cfg.DiscoveryURL) 75 if err != nil { 76 return nil, errors.Annotate(err, "failed to fetch the discovery doc").Tag(transient.Tag).Err() 77 } 78 return doc, nil 79 } 80 81 // Method is an auth.Method implementation that uses encrypted cookies. 82 // 83 // Uses OpenID Connect to establish sessions and refresh tokens to verify 84 // OpenID identity provider still knows about the user. 85 type AuthMethod struct { 86 // Configuration returns OpenID Connect configuration parameters. 87 // 88 // Required. 89 OpenIDConfig func(ctx context.Context) (*OpenIDConfig, error) 90 91 // AEADProvider returns an implementation of Authenticated Encryption with 92 // Additional Authenticated primitive used to encrypt the cookies and other 93 // sensitive state. 94 AEADProvider func(ctx context.Context) tink.AEAD 95 96 // Sessions keeps user sessions in some permanent storage. 97 // 98 // Required. 99 Sessions session.Store 100 101 // Insecure is true to allow http:// URLs and non-https cookies. Useful for 102 // local development. 103 Insecure bool 104 105 // IncompatibleCookies is a list of cookies to remove when setting or clearing 106 // the session cookie. It is useful to get rid of cookies from previously used 107 // authentication methods. 108 IncompatibleCookies []string 109 110 // LimitCookieExposure, if set, limits the cookie to be set only on 111 // "/auth/openid/" HTTP path and makes it `SameSite: strict`. 112 // 113 // This is useful for SPAs that exchange cookies for authentication tokens via 114 // fetch(...) requests to "/auth/openid/state". In this case the cookie is 115 // not normally used by any other HTTP handler and it makes no sense to send 116 // it in every request. 117 LimitCookieExposure bool 118 119 // RequiredScopes is a list of required OAuth scopes that will be requested 120 // when making the OAuth authorization request, in addition to the default 121 // scopes (openid email profile) and the OptionalScopes. 122 // 123 // Existing sessions that don't have the required scopes will be closed. All 124 // scopes in the RequiredScopes must be in the RequiredScopes or 125 // OptionalScopes of other running instances of the app. Otherwise a session 126 // opened by other running instances could be closed immediately. 127 RequiredScopes []string 128 129 // OptionalScopes is a list of optional OAuth scopes that will be requested 130 // when making the OAuth authorization request, in addition to the default 131 // scopes (openid email profile) and the RequiredScopes. 132 // 133 // Existing sessions that don't have the optional scopes will not be closed. 134 // This is useful for rolling out changes incrementally. Once the new version 135 // takes over all the traffic, promote the optional scopes to RequiredScopes. 136 OptionalScopes []string 137 138 // ExposeStateEndpoint controls whether "/auth/openid/state" endpoint should 139 // be exposed. 140 // 141 // See auth.StateEndpointResponse struct for details. 142 // 143 // It is off by default since it can potentially make XSS vulnerabilities more 144 // severe by exposing OAuth and ID tokens to malicious injected code. It 145 // should be enabled only if the frontend code needs it and it is aware of 146 // XSS risks. 147 ExposeStateEndpoint bool 148 } 149 150 var _ interface { 151 auth.Method 152 auth.UsersAPI 153 auth.Warmable 154 auth.HasHandlers 155 auth.HasStateEndpoint 156 } = (*AuthMethod)(nil) 157 158 const ( 159 loginURL = "/auth/openid/login" 160 logoutURL = "/auth/openid/logout" 161 callbackURL = "/auth/openid/callback" 162 stateURL = "/auth/openid/state" 163 ) 164 165 // InstallHandlers installs HTTP handlers used in the login protocol. 166 // 167 // Implements auth.HasHandlers. 168 func (m *AuthMethod) InstallHandlers(r *router.Router, base router.MiddlewareChain) { 169 r.GET(loginURL, base, m.loginHandler) 170 r.GET(logoutURL, base, m.logoutHandler) 171 r.GET(callbackURL, base, m.callbackHandler) 172 173 // Need to build an authenticator that uses this method to properly populate 174 // the auth state for stateHandler. `base` here usually doesn't include 175 // authentication yet (because we are still setting it up). 176 if m.ExposeStateEndpoint { 177 authenticator := auth.Authenticator{Methods: []auth.Method{m}} 178 r.GET(stateURL, base.Extend(authenticator.GetMiddleware()), m.stateHandler) 179 } 180 } 181 182 // Warmup prepares local caches. 183 // 184 // Implements auth.Warmable. 185 func (m *AuthMethod) Warmup(ctx context.Context) error { 186 cfg, err := m.checkConfigured(ctx) 187 if err != nil { 188 return err 189 } 190 doc, err := cfg.discoveryDoc(ctx) 191 if err != nil { 192 return err 193 } 194 if _, err := doc.SigningKeys(ctx); err != nil { 195 return err 196 } 197 _ = m.AEADProvider(ctx) 198 return nil 199 } 200 201 // Authenticate authenticates the request. 202 // 203 // Implements auth.Method. 204 func (m *AuthMethod) Authenticate(ctx context.Context, r auth.RequestMetadata) (*auth.User, auth.Session, error) { 205 encryptedCookie, _ := r.Cookie(internal.SessionCookieName) 206 if encryptedCookie == nil { 207 return nil, nil, nil // the method is not applicable, skip it 208 } 209 210 // Decrypt the cookie to get the session ID. Ignore undecryptable cookies. 211 // This may happen if we no longer have the encryption key due to rotations 212 // or we changed the cookie format. We just assume such cookies are expired. 213 aead := m.AEADProvider(ctx) 214 if aead == nil { 215 return nil, nil, errors.Reason("the encryption key is not configured").Err() 216 } 217 cookie, err := internal.DecryptSessionCookie(aead, encryptedCookie) 218 if err != nil { 219 logging.Warningf(ctx, "Failed to decrypt the session cookie, ignoring it: %s", err) 220 return nil, nil, nil 221 } 222 sid := session.ID(cookie.SessionId) 223 224 // Load the session to verify it still exists. 225 session, err := m.Sessions.FetchSession(ctx, sid) 226 switch { 227 case err != nil: 228 logging.Warningf(ctx, "Failed to fetch session %q: %s", sid, err) 229 return nil, nil, errors.Reason("failed to fetch the session").Tag(transient.Tag).Err() 230 case session == nil: 231 logging.Warningf(ctx, "No session %q in the store, ignoring the session cookie", sid) 232 return nil, nil, nil 233 case session.State != sessionpb.State_STATE_OPEN: 234 logging.Warningf(ctx, "Session %q is in state %q, ignoring the session cookie", sid, session.State) 235 return nil, nil, nil 236 } 237 238 additionalScopes := stringset.NewFromSlice(session.AdditionalScopes...) 239 if !additionalScopes.HasAll(m.RequiredScopes...) { 240 logging.Warningf(ctx, 241 "Session %q's scope (%v) isn't a subset of the required scope (%v), closing the session cookie", 242 sid, additionalScopes, m.RequiredScopes) 243 if err := m.closeSession(ctx, aead, encryptedCookie); err != nil { 244 logging.Errorf(ctx, "An error closing the session: %s", err) 245 return nil, nil, errors.Reason("transient error when closing the session").Tag(transient.Tag).Err() 246 } 247 return nil, nil, nil 248 } 249 250 // authSessionImpl implements auth.Session. 251 authSession := &authSessionImpl{method: m, cookie: cookie, session: session} 252 253 // Check if we need to refresh the short-lived tokens stored in the session. 254 ttl := session.NextRefresh.AsTime().Sub(clock.Now(ctx)) 255 if internal.ShouldRefreshSession(ctx, ttl) { 256 ctx := logging.SetField(ctx, "sid", sid.String()) 257 if ttl > 0 { 258 logging.Infof(ctx, "Refreshing the session, goes stale in %s", ttl) 259 } else { 260 logging.Infof(ctx, "Refreshing the session, went stale %s ago", -ttl) 261 } 262 var private *sessionpb.Private 263 switch session, private, err = m.refreshSession(ctx, cookie, session); { 264 case err != nil: 265 logging.Warningf(ctx, "Failed to refresh the session: %s", err) 266 return nil, nil, errors.Reason("failed to refresh the session, see server logs").Tag(transient.Tag).Err() 267 case session == nil: 268 return nil, nil, nil // the session is no longer valid, just ignore it 269 default: 270 logging.Infof(ctx, "The session was refreshed") 271 authSession.session = session 272 authSession.unsealed(private, nil) // have it decrypted already 273 } 274 } 275 276 return &auth.User{ 277 Identity: identity.Identity("user:" + session.Email), 278 Email: session.Email, 279 Name: session.Name, 280 Picture: session.Picture, 281 }, authSession, nil 282 } 283 284 // LoginURL returns a URL that, when visited, prompts the user to sign in, 285 // then redirects the user to the URL specified by dest. 286 // 287 // Implements auth.UsersAPI. 288 func (m *AuthMethod) LoginURL(ctx context.Context, dest string) (string, error) { 289 if _, err := m.checkConfigured(ctx); err != nil { 290 return "", err 291 } 292 return internal.MakeRedirectURL(loginURL, dest) 293 } 294 295 // LogoutURL returns a URL that, when visited, signs the user out, 296 // then redirects the user to the URL specified by dest. 297 // 298 // Implements auth.UsersAPI. 299 func (m *AuthMethod) LogoutURL(ctx context.Context, dest string) (string, error) { 300 if _, err := m.checkConfigured(ctx); err != nil { 301 return "", err 302 } 303 return internal.MakeRedirectURL(logoutURL, dest) 304 } 305 306 // StateEndpointURL returns an URL that serves the authentication state. 307 // 308 // Implements auth.HasStateEndpoint. 309 func (m *AuthMethod) StateEndpointURL(ctx context.Context) (string, error) { 310 if m.ExposeStateEndpoint { 311 return stateURL, nil 312 } 313 return "", auth.ErrNoStateEndpoint 314 } 315 316 //////////////////////////////////////////////////////////////////////////////// 317 318 var ( 319 // errCodeReuse is returned if the authorization code is reused. 320 errCodeReuse = errors.Reason("the authorization code has already been used").Err() 321 // errBadIDToken is returned if the produced ID token is not valid. 322 errBadIDToken = errors.Reason("ID token validation error").Err() 323 // errSessionClosed is used internally to signal the session is closed. 324 errSessionClosed = errors.Reason("the session is already closed").Err() 325 ) 326 327 // handler is one of .../login, .../logout or .../callback handlers. 328 type handler func(ctx context.Context, r *http.Request, rw http.ResponseWriter, cfg *OpenIDConfig, discovery *openid.DiscoveryDoc) error 329 330 // checkConfigured verifies the method is configured. 331 // 332 // Panics on API violations (i.e. coding errors) and merely returns an error if 333 // OpenIDConfig callback doesn't produce a valid config. 334 // 335 // Returns the resulting OpenIDConfig. 336 func (m *AuthMethod) checkConfigured(ctx context.Context) (*OpenIDConfig, error) { 337 if m.OpenIDConfig == nil { 338 panic("bad encryptedcookies.AuthMethod usage: OpenIDConfig is nil") 339 } 340 if m.AEADProvider == nil { 341 panic("bad encryptedcookies.AuthMethod usage: AEADProvider is nil") 342 } 343 if m.Sessions == nil { 344 panic("bad encryptedcookies.AuthMethod usage: Sessions is nil") 345 } 346 cfg, err := m.OpenIDConfig(ctx) 347 if err != nil { 348 return nil, errors.Annotate(err, "failed to fetch OpenID config").Err() 349 } 350 switch { 351 case cfg.DiscoveryURL == "": 352 return nil, errors.Reason("bad OpenID config: no discovery URL").Err() 353 case cfg.ClientID == "": 354 return nil, errors.Reason("bad OpenID config: no client ID").Err() 355 case cfg.ClientSecret == "": 356 return nil, errors.Reason("bad OpenID config: no client secret").Err() 357 case cfg.RedirectURI == "": 358 return nil, errors.Reason("bad OpenID config: no redirect URI").Err() 359 } 360 return cfg, nil 361 } 362 363 // handler is a common wrapper for routes registered in InstallHandlers. 364 func (m *AuthMethod) handler(ctx *router.Context, cb handler) { 365 cfg, err := m.checkConfigured(ctx.Request.Context()) 366 if err == nil { 367 var discovery *openid.DiscoveryDoc 368 discovery, err = cfg.discoveryDoc(ctx.Request.Context()) 369 if err == nil { 370 err = cb(ctx.Request.Context(), ctx.Request, ctx.Writer, cfg, discovery) 371 } 372 } 373 if err != nil { 374 code := http.StatusBadRequest 375 if transient.Tag.In(err) { 376 code = http.StatusInternalServerError 377 } 378 http.Error(ctx.Writer, err.Error(), code) 379 } 380 } 381 382 // loginHandler initiates the login flow. 383 func (m *AuthMethod) loginHandler(ctx *router.Context) { 384 m.handler(ctx, func(ctx context.Context, r *http.Request, rw http.ResponseWriter, cfg *OpenIDConfig, discovery *openid.DiscoveryDoc) error { 385 dest, err := internal.NormalizeURL(r.URL.Query().Get("r")) 386 if err != nil { 387 return errors.Annotate(err, "bad redirect URI").Err() 388 } 389 390 scopesSet := stringset.New(len(m.RequiredScopes) + len(m.OptionalScopes)) 391 scopesSet.AddAll(m.RequiredScopes) 392 scopesSet.AddAll(m.OptionalScopes) 393 additionalScopes := scopesSet.ToSortedSlice() 394 395 // Generate `state` that will be passed back to us in the callbackHandler. 396 state := &encryptedcookiespb.OpenIDState{ 397 SessionId: session.GenerateID(), 398 Nonce: internal.GenerateNonce(), 399 CodeVerifier: internal.GenerateCodeVerifier(), 400 DestHost: r.Host, 401 DestPath: dest, 402 // We could get the scope from the exchange code response[1]. However 403 // OpenID provider can replace scopes with their aliases or add other 404 // scopes, making it hard to check the scope during authentication. 405 // Passing the scope though state solves the issue but makes the URL 406 // longer. If the URL length become an issue, we can convert the scope 407 // into hashes. 408 // [1]: https://developers.google.com/identity/protocols/oauth2/openid-connect#exchangecode 409 AdditionalScopes: additionalScopes, 410 } 411 412 // Encrypt it using service-global AEAD, since we are going to expose it. 413 aead := m.AEADProvider(ctx) 414 if aead == nil { 415 return errors.Reason("the service encryption key is not configured").Err() 416 } 417 stateEnc, err := internal.EncryptStateB64(aead, state) 418 if err != nil { 419 return errors.Annotate(err, "failed to encrypt the state").Err() 420 } 421 422 // Prepare parameters for the OpenID Connect authorization endpoint. 423 v := url.Values{ 424 "response_type": {"code"}, 425 "scope": {"openid email profile " + strings.Join(additionalScopes, " ")}, 426 "access_type": {"offline"}, // want a refresh token 427 "prompt": {"consent"}, // want a NEW refresh token 428 "client_id": {cfg.ClientID}, 429 "redirect_uri": {cfg.RedirectURI}, 430 "nonce": {base64.RawURLEncoding.EncodeToString(state.Nonce)}, 431 "code_challenge": {internal.DeriveCodeChallenge(state.CodeVerifier)}, 432 "code_challenge_method": {"S256"}, 433 "state": {stateEnc}, 434 } 435 436 // Finally, redirect to the OpenID provider's authorization endpoint. 437 // It will eventually redirect user's browser to callbackHandler. 438 http.Redirect(rw, r, discovery.AuthorizationEndpoint+"?"+v.Encode(), http.StatusFound) 439 return nil 440 }) 441 } 442 443 // logoutHandler closes the session. 444 func (m *AuthMethod) logoutHandler(ctx *router.Context) { 445 m.handler(ctx, func(ctx context.Context, r *http.Request, rw http.ResponseWriter, cfg *OpenIDConfig, discovery *openid.DiscoveryDoc) error { 446 dest, err := internal.NormalizeURL(r.URL.Query().Get("r")) 447 if err != nil { 448 return errors.Annotate(err, "bad redirect URI").Err() 449 } 450 451 // If we have a cookie, mark the session as closed. 452 if encryptedCookie, _ := r.Cookie(internal.SessionCookieName); encryptedCookie != nil { 453 aead := m.AEADProvider(ctx) 454 if aead == nil { 455 return errors.Reason("the encryption key is not configured").Err() 456 } 457 if err := m.closeSession(ctx, aead, encryptedCookie); err != nil { 458 logging.Errorf(ctx, "An error closing the session: %s", err) 459 return errors.Reason("transient error when closing the session").Tag(transient.Tag).Err() 460 } 461 } 462 463 // Nuke all session cookies to get to a completely clean state. 464 internal.RemoveCookie(rw, r, internal.SessionCookieName, internal.UnlimitedCookiePath) 465 internal.RemoveCookie(rw, r, internal.SessionCookieName, internal.LimitedCookiePath) 466 for _, name := range m.IncompatibleCookies { 467 internal.RemoveCookie(rw, r, name, "/") 468 } 469 470 http.Redirect(rw, r, dest, http.StatusFound) 471 return nil 472 }) 473 } 474 475 // callbackHandler handles a redirect from the OpenID provider. 476 func (m *AuthMethod) callbackHandler(ctx *router.Context) { 477 m.handler(ctx, func(ctx context.Context, r *http.Request, rw http.ResponseWriter, cfg *OpenIDConfig, discovery *openid.DiscoveryDoc) error { 478 q := r.URL.Query() 479 480 // This code path is hit when user clicks "Deny" on the consent page or 481 // if the OAuth client is misconfigured. 482 if errorMsg := q.Get("error"); errorMsg != "" { 483 return errors.Reason("%s", errorMsg).Err() 484 } 485 486 // On success we must receive the authorization code and the state. 487 code := q.Get("code") 488 if code == "" { 489 return errors.Reason("missing `code` parameter").Err() 490 } 491 state := q.Get("state") 492 if state == "" { 493 return errors.Reason("missing `state` parameter").Err() 494 } 495 496 // Decrypt/verify `state`. 497 aead := m.AEADProvider(ctx) 498 if aead == nil { 499 return errors.Reason("the encryption key is not configured").Err() 500 } 501 statepb, err := internal.DecryptStateB64(aead, state) 502 if err != nil { 503 logging.Errorf(ctx, "Failed to decrypt the state: %s", err) 504 return errors.Reason("bad `state` parameter").Err() 505 } 506 507 // The callback URI is hardcoded in the OAuth2 client config and must always 508 // point to the default version on GAE. Yet we want to support signing-in 509 // into non-default versions that have different hostnames. Do some redirect 510 // dance here to pass the control to the required version if necessary 511 // (so that it can set the cookie on a non-default version domain). This is 512 // safe, since statepb.DestHost comes from an AEAD-encrypted state, and it 513 // was produced based on "Host" header in loginHandler, which we assume 514 // was verified as belonging to our service by the layer that terminates 515 // TLS (e.g. GAE load balancer). Of course, if `Insecure` is true, all bets 516 // are off. 517 if statepb.DestHost != r.Host { 518 // There's no Scheme in r.URL. Append one, otherwise url.String() returns 519 // relative (broken) URL. And replace the hostname with desired one. 520 url := *r.URL 521 if m.Insecure { 522 url.Scheme = "http" 523 } else { 524 url.Scheme = "https" 525 } 526 url.Host = statepb.DestHost 527 http.Redirect(rw, r, url.String(), http.StatusFound) 528 return nil 529 } 530 531 // Check there is no such session in the store. If there's, `code` has been 532 // used already and we should reject this replay attempt. 533 switch session, err := m.Sessions.FetchSession(ctx, statepb.SessionId); { 534 case err != nil: 535 logging.Errorf(ctx, "Failed to check the session: %s", err) 536 return errors.Reason("transient error when checking the session").Tag(transient.Tag).Err() 537 case session != nil: 538 return errCodeReuse 539 } 540 541 // Exchange the authorization code for authentication tokens. 542 tokens, exp, err := internal.HitTokenEndpoint(ctx, discovery, map[string]string{ 543 "client_id": cfg.ClientID, 544 "client_secret": cfg.ClientSecret, 545 "redirect_uri": cfg.RedirectURI, 546 "grant_type": "authorization_code", 547 "code": code, 548 "code_verifier": statepb.CodeVerifier, 549 }) 550 if err != nil { 551 logging.Errorf(ctx, "Code exchange failed: %s", err) // only log on the server 552 if transient.Tag.In(err) { 553 return errors.Reason("transient error during code exchange").Tag(transient.Tag).Err() 554 } 555 return errors.Reason("fatal error during code exchange").Err() 556 } 557 558 // Verify and unpack the ID token to grab the user info and `nonce` from it. 559 tok, _, err := openid.UserFromIDToken(ctx, tokens.IdToken, discovery) 560 if err != nil { 561 logging.Errorf(ctx, "ID token validation error: %s", err) 562 if transient.Tag.In(err) { 563 return transient.Tag.Apply(errBadIDToken) 564 } 565 return errBadIDToken 566 } 567 568 // Make sure the token was created via the expected OAuth client and used 569 // the expected nonce. 570 // 571 // The `nonce` check essentially binds `code` to the session ID (which is 572 // a true nonce here, with the state stored in the session store). The chain 573 // is: 574 // 1. `code` is bound to `nonce` per OpenID Connect protocol contract. 575 // 2. `nonce` is bound to session ID by the signature on `state`. 576 // 577 // Note that this is unrelated to `code_verifier` check, which ensures that 578 // `code` can't be used by someone who knows `client_secret`, but not 579 // `code_verifier`. 580 if tok.Aud != cfg.ClientID { 581 logging.Errorf(ctx, "Bad ID token: expecting audience %q, got %q", cfg.ClientID, tok.Aud) 582 return errBadIDToken 583 } 584 if tok.Nonce != base64.RawURLEncoding.EncodeToString(statepb.Nonce) { 585 logging.Errorf(ctx, "Bad ID token: wrong nonce") 586 return errBadIDToken 587 } 588 589 // Make sure we've got other required tokens. 590 if tokens.AccessToken == "" { 591 return errors.Reason("the ID provider didn't produce access token").Err() 592 } 593 if tokens.RefreshToken == "" { 594 return errors.Reason("the ID provider didn't produce refresh token").Err() 595 } 596 597 // Everything looks good and we can open the session! 598 599 // Generate per-session encryption keys, put them into the future cookie. 600 cookie, sessionAEAD := internal.NewSessionCookie(statepb.SessionId) 601 602 // Encrypt sensitive session tokens using the per-session keys. 603 encryptedPrivate, err := internal.EncryptPrivate(sessionAEAD, tokens) 604 if err != nil { 605 logging.Errorf(ctx, "EncryptPrivate error: %s", err) 606 return errors.Reason("failed to prepare the session").Err() 607 } 608 609 // Prep the session we are about to store. 610 now := timestamppb.Now() 611 session := &sessionpb.Session{ 612 State: sessionpb.State_STATE_OPEN, 613 Generation: 1, 614 Created: now, 615 LastRefresh: now, 616 NextRefresh: timestamppb.New(exp), // refresh when short-lived tokens expire 617 Sub: tok.Sub, 618 Email: tok.Email, 619 Name: tok.Name, 620 Picture: tok.Picture, 621 AdditionalScopes: statepb.AdditionalScopes, 622 EncryptedPrivate: encryptedPrivate, 623 } 624 625 // Actually create the new session in the store. 626 err = m.Sessions.UpdateSession(ctx, statepb.SessionId, func(s *sessionpb.Session) error { 627 if s.State != sessionpb.State_STATE_UNDEFINED { 628 // We might be on a second try of a transaction that "failed" 629 // transiently, but actually succeeded. If so, we may have stored 630 // `session` already. Note that session.EncryptedPrivate is derived 631 // using a random key generated just above in NewSessionCookie and not 632 // exposed anywhere. There's a *very* small chance someone else managed 633 // to create this session already with the exact same EncryptedPrivate. 634 if proto.Equal(s, session) { 635 return nil 636 } 637 return errCodeReuse 638 } 639 proto.Reset(s) 640 proto.Merge(s, session) 641 return nil 642 }) 643 if err != nil { 644 if err == errCodeReuse { 645 return err 646 } 647 logging.Errorf(ctx, "Failure when storing the session: %s", err) 648 return errors.Reason("failed to store the session").Tag(transient.Tag).Err() 649 } 650 651 // Best effort at properly closing the previous session. We are going to 652 // override the cookie anyway. 653 if existingCookie, _ := r.Cookie(internal.SessionCookieName); existingCookie != nil { 654 logging.Infof(ctx, "Closing the previous session") 655 if err := m.closeSession(ctx, aead, existingCookie); err != nil { 656 logging.Warningf(ctx, "An error closing the previous session, ignoring: %s", err) 657 } 658 } 659 660 // Encrypt the session cookie with the *global* AEAD key. 661 httpCookie, err := internal.EncryptSessionCookie(aead, cookie) 662 if err != nil { 663 logging.Errorf(ctx, "Cookie encryption error: %s", err) 664 return errors.Reason("failed to prepare the cookie").Err() 665 } 666 667 // Set the cookie at an appropriate path and remove a potentially stale 668 // cookie on a different path. 669 var curPath, prevPath string 670 var sameSite http.SameSite 671 if m.LimitCookieExposure { 672 curPath = internal.LimitedCookiePath 673 prevPath = internal.UnlimitedCookiePath 674 sameSite = http.SameSiteStrictMode 675 } else { 676 curPath = internal.UnlimitedCookiePath 677 prevPath = internal.LimitedCookiePath 678 sameSite = 0 // use browser's default 679 } 680 httpCookie.Path = curPath 681 httpCookie.SameSite = sameSite 682 httpCookie.Secure = !m.Insecure 683 http.SetCookie(rw, httpCookie) 684 internal.RemoveCookie(rw, r, internal.SessionCookieName, prevPath) 685 for _, name := range m.IncompatibleCookies { 686 internal.RemoveCookie(rw, r, name, "/") 687 } 688 689 // Finally redirect the user to the originally requested destination. 690 http.Redirect(rw, r, statepb.DestPath, http.StatusFound) 691 return nil 692 }) 693 } 694 695 // stateHandler serves JSON with the session state, see StateEndpointResponse. 696 func (m *AuthMethod) stateHandler(ctx *router.Context) { 697 stateHandlerImpl(ctx, func(s auth.Session) bool { 698 impl, ok := s.(*authSessionImpl) 699 return ok && impl.method == m 700 }) 701 } 702 703 // refreshSession refreshes the short-lived tokens stored in the session, thus 704 // checking that the refresh token (also stored there) is still valid. 705 // 706 // Returns: 707 // 708 // session, private, nil: if the session was successfully refreshed. 709 // nil, nil, nil: if the refresh token was revoked and the session is closed. 710 // nil, nil, err: if there was some unexpected error refreshing the session. 711 // 712 // Note that errors may contain sensitive details and should not be returned to 713 // the caller as is. 714 func (m *AuthMethod) refreshSession(ctx context.Context, cookie *encryptedcookiespb.SessionCookie, session *sessionpb.Session) (*sessionpb.Session, *sessionpb.Private, error) { 715 // Need the discovery doc to hit the OpenID provider's endpoint. 716 cfg, err := m.checkConfigured(ctx) 717 if err != nil { 718 return nil, nil, err 719 } 720 discovery, err := cfg.discoveryDoc(ctx) 721 if err != nil { 722 return nil, nil, err 723 } 724 725 // Unseal the private part of the session to get the refresh token. 726 private, sessionAEAD, err := internal.UnsealPrivate(cookie, session) 727 if err != nil { 728 return nil, nil, errors.Annotate(err, "failed to unseal the session").Err() 729 } 730 731 // Use the refresh token to get the new access and ID tokens. A fatal error 732 // here means the refresh token is no longer valid. 733 tokens, exp, err := internal.HitTokenEndpoint(ctx, discovery, map[string]string{ 734 "client_id": cfg.ClientID, 735 "client_secret": cfg.ClientSecret, 736 "redirect_uri": cfg.RedirectURI, 737 "grant_type": "refresh_token", 738 "refresh_token": private.RefreshToken, 739 }) 740 if err != nil { 741 if transient.Tag.In(err) { 742 return nil, nil, errors.Annotate(err, "transient error when fetching new tokens").Err() 743 } 744 logging.Warningf(ctx, "Refresh failed, closing the session: %s", err) 745 } 746 stillGood := err == nil 747 748 var tok *openid.IDToken 749 var encryptedPrivate []byte 750 if stillGood { 751 // Grab the updated user info from the ID token. 752 if tok, _, err = openid.UserFromIDToken(ctx, tokens.IdToken, discovery); err != nil { 753 return nil, nil, errors.Annotate(err, "failed to check the ID token").Err() 754 } 755 // Make sure we've also got the access token 756 if tokens.AccessToken == "" { 757 return nil, nil, errors.Reason("the ID provider didn't produce an access token").Err() 758 } 759 // Reencrypt new tokens using the per-session key. The refresh token stays 760 // the same. 761 tokens.RefreshToken = private.RefreshToken 762 if encryptedPrivate, err = internal.EncryptPrivate(sessionAEAD, tokens); err != nil { 763 return nil, nil, errors.Annotate(err, "failed to encrypt the private part of the session").Err() 764 } 765 } 766 767 // Here we either successfully refreshed the session or the ID provider 768 // rejected the refresh token. Either way, update the session state in 769 // the storage. 770 var refreshedSession *sessionpb.Session 771 err = m.Sessions.UpdateSession(ctx, cookie.SessionId, func(s *sessionpb.Session) error { 772 bumpGeneration(ctx, s, session.Generation) 773 if s.State != sessionpb.State_STATE_OPEN { 774 return errSessionClosed 775 } 776 s.LastRefresh = timestamppb.New(clock.Now(ctx)) 777 if stillGood { 778 s.NextRefresh = timestamppb.New(exp) 779 s.Sub = tok.Sub 780 s.Email = tok.Email 781 // User profile information inside the token can be randomly missing. 782 // Update it only when it is present. 783 // See https://github.com/googleapis/google-api-dotnet-client/issues/1141 784 if tok.Name != "" { 785 s.Name = tok.Name 786 } 787 if tok.Picture != "" { 788 s.Picture = tok.Picture 789 } 790 s.EncryptedPrivate = encryptedPrivate 791 } else { 792 s.State = sessionpb.State_STATE_REVOKED 793 s.NextRefresh = nil 794 s.Closed = timestamppb.New(clock.Now(ctx)) 795 s.EncryptedPrivate = nil 796 } 797 refreshedSession = s 798 return nil 799 }) 800 if err != nil { 801 if err == errSessionClosed { 802 return nil, nil, nil 803 } 804 return nil, nil, errors.Annotate(err, "failed to update the session in the storage").Err() 805 } 806 807 if stillGood { 808 return refreshedSession, tokens, nil 809 } 810 return nil, nil, nil 811 } 812 813 // closeSession closes the session and forgets the refresh token. 814 // 815 // Does nothing if the session is already closed or the cookie can't be 816 // decrypted. 817 // 818 // Note that errors may contain sensitive details and should not be returned to 819 // the caller as is. 820 // 821 // TODO(crbug/1226922): Since the refresh token is not revoked but simply 822 // forgotten, the token is still observable through ID provider UI. We can't 823 // revoke the refresh token because the associated access token cached in the 824 // frontend will stop working. In the future, we can migrate to use ID tokens 825 // instead of access tokens. After that, we can safely revoke the refresh token 826 // (users will still need to sign in after the ID token expired). 827 func (m *AuthMethod) closeSession(ctx context.Context, aead tink.AEAD, encryptedCookie *http.Cookie) error { 828 cookie, err := internal.DecryptSessionCookie(aead, encryptedCookie) 829 if err != nil { 830 logging.Warningf(ctx, "Failed to decrypt the session cookie, ignoring it: %s", err) 831 return nil 832 } 833 sid := session.ID(cookie.SessionId) 834 835 session, err := m.Sessions.FetchSession(ctx, sid) 836 switch { 837 case err != nil: 838 return errors.Annotate(err, "failed to fetch the session").Tag(transient.Tag).Err() 839 case session == nil || session.State != sessionpb.State_STATE_OPEN: 840 logging.Infof(ctx, "The session is already closed") 841 return nil 842 } 843 844 // Mark the session as closed in the storage. 845 err = m.Sessions.UpdateSession(ctx, sid, func(s *sessionpb.Session) error { 846 bumpGeneration(ctx, s, session.Generation) 847 if s.State != sessionpb.State_STATE_OPEN { 848 return errSessionClosed 849 } 850 s.State = sessionpb.State_STATE_CLOSED 851 s.NextRefresh = nil 852 s.Closed = timestamppb.New(clock.Now(ctx)) 853 s.EncryptedPrivate = nil 854 return nil 855 }) 856 if err != nil { 857 if err == errSessionClosed { 858 logging.Infof(ctx, "The session is already closed") 859 return nil 860 } 861 return errors.Annotate(err, "failed to update the session in the storage").Err() 862 } 863 864 return nil 865 } 866 867 //////////////////////////////////////////////////////////////////////////////// 868 869 // bumpGeneration bumps Generation counter in the session. 870 // 871 // It is used to detect race conditions between session update transactions. 872 // They should presumably be harmless, but some logging won't hurt. 873 func bumpGeneration(ctx context.Context, s *sessionpb.Session, expected int32) { 874 if s.Generation != expected { 875 logging.Warningf(ctx, 876 "The session was already updated by another handler (gen %d != gen %d). Overwriting...", 877 s.Generation, expected) 878 } 879 s.Generation++ 880 } 881 882 //////////////////////////////////////////////////////////////////////////////// 883 884 // authSessionImpl implements auth.Session by lazily decrypting tokens stored 885 // in the session's private section using the keys from the cookie. 886 // 887 // It doesn't try to refresh tokens dynamically if they expire midway through 888 // the request handler. AuthMethod.Authenticate makes sure tokens live for at 889 // least 10 min. We assume it is enough to handle any request. If this is not 890 // enough, we'll have to teach the authSessionImpl to refresh tokens on the fly 891 // (and encrypt them and write them back into the datastore). This will be 892 // messy. 893 type authSessionImpl struct { 894 method *AuthMethod 895 cookie *encryptedcookiespb.SessionCookie 896 session *sessionpb.Session 897 898 once sync.Once 899 done bool 900 err error 901 accessToken *oauth2.Token 902 idToken *oauth2.Token 903 } 904 905 // unseal makes sure accessToken/idToken are decrypted. 906 func (s *authSessionImpl) unseal() error { 907 s.once.Do(func() { 908 if !s.done { 909 private, _, err := internal.UnsealPrivate(s.cookie, s.session) 910 s.unsealed(private, errors.Annotate(err, "failed to unseal the session").Err()) 911 } 912 }) 913 return s.err 914 } 915 916 // unsealed is called to interpret result of sessionpb.Private decryption. 917 func (s *authSessionImpl) unsealed(p *sessionpb.Private, err error) { 918 s.done = true 919 s.err = err 920 if err == nil { 921 s.accessToken = &oauth2.Token{ 922 TokenType: "Bearer", 923 AccessToken: p.AccessToken, 924 Expiry: s.session.NextRefresh.AsTime(), 925 } 926 s.idToken = &oauth2.Token{ 927 TokenType: "Bearer", 928 AccessToken: p.IdToken, 929 Expiry: s.session.NextRefresh.AsTime(), 930 } 931 } else { 932 s.accessToken = nil 933 s.idToken = nil 934 } 935 } 936 937 // AccessToken is a part of auth.Session interface. 938 func (s *authSessionImpl) AccessToken(ctx context.Context) (*oauth2.Token, error) { 939 if err := s.unseal(); err != nil { 940 return nil, err 941 } 942 if err := checkStaleToken(ctx, s.accessToken); err != nil { 943 return nil, err 944 } 945 return s.accessToken, nil 946 } 947 948 // IDToken is a part of auth.Session interface. 949 func (s *authSessionImpl) IDToken(ctx context.Context) (*oauth2.Token, error) { 950 if err := s.unseal(); err != nil { 951 return nil, err 952 } 953 if err := checkStaleToken(ctx, s.idToken); err != nil { 954 return nil, err 955 } 956 return s.idToken, nil 957 } 958 959 // checkStaleToken returns an error if the token is already stale. 960 // 961 // If you see this error, either make sure your request is shorter than 10 min, 962 // or, if this is impossible, file a bug to implement the dynamic token refresh. 963 func checkStaleToken(ctx context.Context, t *oauth2.Token) error { 964 if clock.Now(ctx).After(t.Expiry) { 965 return errors.Reason("encryptedcookies: the tokens stored in the session expired midway through the request handler").Err() 966 } 967 return nil 968 }