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  }