go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/auth.go (about)

     1  // Copyright 2015 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 auth
    16  
    17  import (
    18  	"context"
    19  	"net"
    20  	"net/http"
    21  
    22  	"golang.org/x/oauth2"
    23  	"google.golang.org/grpc/codes"
    24  
    25  	"go.chromium.org/luci/auth/identity"
    26  	"go.chromium.org/luci/common/errors"
    27  	"go.chromium.org/luci/common/logging"
    28  	"go.chromium.org/luci/common/retry/transient"
    29  	"go.chromium.org/luci/grpc/grpcutil"
    30  
    31  	"go.chromium.org/luci/server/auth/authdb"
    32  	"go.chromium.org/luci/server/auth/delegation"
    33  	"go.chromium.org/luci/server/auth/internal/tracing"
    34  	"go.chromium.org/luci/server/auth/signing"
    35  	"go.chromium.org/luci/server/router"
    36  )
    37  
    38  var (
    39  	// Authenticate errors (must be grpc-tagged).
    40  
    41  	// ErrNotConfigured is returned by Authenticate and other functions if the
    42  	// context wasn't previously initialized via 'Initialize'.
    43  	ErrNotConfigured = errors.New("auth: the library is not properly configured", grpcutil.InternalTag)
    44  
    45  	// ErrBadClientID is returned by Authenticate if caller is using an OAuth2
    46  	// client ID not in the list of allowed IDs. More info is in the log.
    47  	ErrBadClientID = errors.New("auth: OAuth client_id is not in the allowlist", grpcutil.PermissionDeniedTag)
    48  
    49  	// ErrBadAudience is returned by Authenticate if token's audience is unknown.
    50  	ErrBadAudience = errors.New("auth: bad token audience", grpcutil.PermissionDeniedTag)
    51  
    52  	// ErrBadRemoteAddr is returned by Authenticate if request's remote_addr can't
    53  	// be parsed.
    54  	ErrBadRemoteAddr = errors.New("auth: bad remote addr", grpcutil.InternalTag)
    55  
    56  	// ErrForbiddenIP is returned when an account is restricted by an IP allowlist
    57  	// and request's remote_addr is not in it.
    58  	ErrForbiddenIP = errors.New("auth: IP is not in the allowlist", grpcutil.PermissionDeniedTag)
    59  
    60  	// ErrProjectHeaderForbidden is returned by Authenticate if an unknown caller
    61  	// tries to use X-Luci-Project header. Only a preapproved set of callers are
    62  	// allowed to use this header, see InternalServicesGroup.
    63  	ErrProjectHeaderForbidden = errors.New("auth: the caller is not allowed to use X-Luci-Project", grpcutil.PermissionDeniedTag)
    64  
    65  	// Other errors.
    66  
    67  	// ErrNoUsersAPI is returned by LoginURL and LogoutURL if none of
    68  	// the authentication methods support UsersAPI.
    69  	ErrNoUsersAPI = errors.New("auth: methods do not support login or logout URL")
    70  
    71  	// ErrNoForwardableCreds is returned by GetRPCTransport when attempting to
    72  	// forward credentials (via AsCredentialsForwarder) that are not forwardable.
    73  	ErrNoForwardableCreds = errors.New("auth: no forwardable credentials in the context")
    74  
    75  	// ErrNoStateEndpoint is returned by StateEndpointURL if the state endpoint is
    76  	// not exposed.
    77  	ErrNoStateEndpoint = errors.New("auth: the state endpoint is not available")
    78  )
    79  
    80  const (
    81  	// InternalServicesGroup is a name of a group with service accounts of LUCI
    82  	// microservices of the current LUCI deployment (and only them!).
    83  	//
    84  	// Accounts in this group are allowed to use X-Luci-Project header to specify
    85  	// that RPCs are done in a context of some particular project. For such
    86  	// requests CurrentIdentity() == 'project:<X-Luci-Project value>'.
    87  	//
    88  	// This group should contain only **fully trusted** services, deployed and
    89  	// managed by the LUCI deployment administrators. Adding "random" services
    90  	// here is a security risk, since they will be able to impersonate any LUCI
    91  	// project.
    92  	InternalServicesGroup = "auth-luci-services"
    93  )
    94  
    95  // RequestMetadata is metadata used when authenticating a request.
    96  //
    97  // Can be constructed by:
    98  //   - RequestMetadataForHTTP based on http.Request.
    99  //   - authtest.NewFakeRequestMetadata based on fakes for unit tests.
   100  type RequestMetadata interface {
   101  	// Header returns a value of a given header or an empty string.
   102  	//
   103  	// Headers are also known as simply "metadata" in gRPC world.
   104  	//
   105  	// The key is case-insensitive. If the request has multiple headers matching
   106  	// the key, returns only the first one.
   107  	Header(key string) string
   108  
   109  	// Cookie returns a cookie or an error if there's no such cookie.
   110  	//
   111  	// Transports that do not support cookies (e.g. gRPC) can always return
   112  	// an error. They will just not work with authentication schemes based on
   113  	// cookies.
   114  	Cookie(key string) (*http.Cookie, error)
   115  
   116  	// RemoteAddr returns the IP address the request came from or "" if unknown.
   117  	//
   118  	// It is used by default for IP allowlist checks if there's no EndUserIP
   119  	// callback set in the auth library configuration. The EndUserIP callback is
   120  	// usually set in environments where the server runs behind a proxy, when
   121  	// the real end user IP is passed via some trusted header or other form of
   122  	// metadata.
   123  	//
   124  	// If "", IP allowlist check will be skipped and the request will be assumed
   125  	// to come from "0.0.0.0" aka "unspecified IPv4".
   126  	RemoteAddr() string
   127  
   128  	// Host returns the hostname the request was sent to or "" if unknown.
   129  	//
   130  	// Also known as HTTP2 `:authority` pseudo-header.
   131  	Host() string
   132  }
   133  
   134  // Method implements a particular low-level authentication mechanism.
   135  //
   136  // It may also optionally implement a bunch of other interfaces:
   137  //
   138  //	UsersAPI: if the method supports login and logout URLs.
   139  //	Warmable: if the method supports warm up.
   140  //	HasHandlers: if the method needs to install HTTP handlers.
   141  //
   142  // Methods are not usually used directly, but passed to Authenticator{...} that
   143  // knows how to apply them.
   144  type Method interface {
   145  	// Authenticate extracts user information from the incoming request.
   146  	//
   147  	// It returns:
   148  	//   * (*User, Session or nil, nil) on success.
   149  	//   * (nil, nil, nil) if the method is not applicable.
   150  	//   * (nil, nil, error) if the method is applicable, but credentials are bad.
   151  	//
   152  	// The returned error may be tagged with an grpcutil error tag. Its code will
   153  	// be used to derive the response status code. Internal error messages (e.g.
   154  	// ones tagged with grpcutil.InternalTag or similar) are logged, but not sent
   155  	// to clients. All other errors are sent to clients as is.
   156  	Authenticate(context.Context, RequestMetadata) (*User, Session, error)
   157  }
   158  
   159  // UsersAPI may be additionally implemented by Method if it supports login and
   160  // logout URLs.
   161  type UsersAPI interface {
   162  	// LoginURL returns a URL that, when visited, prompts the user to sign in,
   163  	// then redirects the user to the URL specified by dest.
   164  	LoginURL(ctx context.Context, dest string) (string, error)
   165  
   166  	// LogoutURL returns a URL that, when visited, signs the user out,
   167  	// then redirects the user to the URL specified by dest.
   168  	LogoutURL(ctx context.Context, dest string) (string, error)
   169  }
   170  
   171  // Warmable may be additionally implemented by Method if it supports warm up.
   172  type Warmable interface {
   173  	// Warmup may be called to precache the data needed by the method.
   174  	//
   175  	// There's no guarantee when it will be called or if it will be called at all.
   176  	// Should always do best-effort initialization. Errors are logged and ignored.
   177  	Warmup(ctx context.Context) error
   178  }
   179  
   180  // HasHandlers may be additionally implemented by Method if it needs to
   181  // install HTTP handlers.
   182  type HasHandlers interface {
   183  	// InstallHandlers installs necessary HTTP handlers into the router.
   184  	InstallHandlers(r *router.Router, base router.MiddlewareChain)
   185  }
   186  
   187  // HasStateEndpoint may be additionally implemented by Method if it exposes
   188  // an HTTP endpoints that returns the authentication state, OAuth and ID tokens
   189  // for frontend applications.
   190  type HasStateEndpoint interface {
   191  	// StateEndpointURL returns an URL that serves StateEndpointResponse JSON.
   192  	//
   193  	// See StateEndpointResponse for the format and meaning of the response.
   194  	//
   195  	// Returns ErrNoStateEndpoint if the endpoint is not actually exposed. This
   196  	// can happen if the method generally supports the state endpoint, but it is
   197  	// turned off in the method's configuration.
   198  	StateEndpointURL(ctx context.Context) (string, error)
   199  }
   200  
   201  // UserCredentialsGetter may be additionally implemented by Method if it knows
   202  // how to extract end-user credentials from the incoming request. Currently
   203  // understands only OAuth2 tokens.
   204  type UserCredentialsGetter interface {
   205  	// GetUserCredentials extracts an OAuth access token from the incoming request
   206  	// or returns an error if it isn't possible.
   207  	//
   208  	// May omit token's expiration time if it isn't known.
   209  	//
   210  	// Guaranteed to be called only after the successful authentication, so it
   211  	// doesn't have to recheck the validity of the token.
   212  	GetUserCredentials(context.Context, RequestMetadata) (*oauth2.Token, error)
   213  }
   214  
   215  // Session holds some extra information pertaining to the request.
   216  //
   217  // It is stored in the context as part of State. Used by AsSessionUser RPC
   218  // authority kind.
   219  type Session interface {
   220  	// AccessToken returns an OAuth access token identifying the session user.
   221  	AccessToken(ctx context.Context) (*oauth2.Token, error)
   222  	// IDToken returns an ID token identifying the session user.
   223  	IDToken(ctx context.Context) (*oauth2.Token, error)
   224  }
   225  
   226  // User represents identity and profile of a user.
   227  type User struct {
   228  	// Identity is identity string of the user (may be AnonymousIdentity).
   229  	// If User is returned by Authenticate(...), Identity string is always present
   230  	// and valid.
   231  	Identity identity.Identity `json:"identity,omitempty"`
   232  
   233  	// Superuser is true if the user is site-level administrator. For example, on
   234  	// GAE this bit is set for GAE-level administrators. Optional, default false.
   235  	Superuser bool `json:"superuser,omitempty"`
   236  
   237  	// Email is email of the user. Optional, default "". Don't use it as a key
   238  	// in various structures. Prefer to use Identity() instead (it is always
   239  	// available).
   240  	Email string `json:"email,omitempty"`
   241  
   242  	// Name is full name of the user. Optional, default "".
   243  	Name string `json:"name,omitempty"`
   244  
   245  	// Picture is URL of the user avatar. Optional, default "".
   246  	Picture string `json:"picture,omitempty"`
   247  
   248  	// ClientID is the ID of the pre-registered OAuth2 client so its identity can
   249  	// be verified. Used only by authentication methods based on OAuth2.
   250  	// See https://developers.google.com/console/help/#generatingoauth2 for more.
   251  	ClientID string `json:"client_id,omitempty"`
   252  
   253  	// Extra is any additional information the authentication method produces.
   254  	//
   255  	// Its exact type depends on the authentication method used. Usually the
   256  	// authentication method will have an accompanying getter function that knows
   257  	// how to interpret this field.
   258  	Extra any `json:"-"`
   259  }
   260  
   261  // StateEndpointResponse defines a JSON structure of a state endpoint response.
   262  //
   263  // It represents the state of the authentication session based on the session
   264  // cookie (or other ambient credential) in the request metadata.
   265  //
   266  // It is intended to be called via a same origin URL fetch request by the
   267  // frontend code that needs an OAuth access token or an ID token representing
   268  // the signed in user.
   269  //
   270  // If there's a valid authentication credential, the state endpoint replies with
   271  // HTTP 200 status code and the JSON-serialized StateEndpointResponse struct
   272  // with state details. The handler refreshes access and ID tokens if they expire
   273  // soon.
   274  //
   275  // If there is no authentication credential or it has expired or was revoked,
   276  // the state endpoint still replies with HTTP 200 code and the JSON-serialized
   277  // StateEndpointResponse struct, except its `identity` field is
   278  // `anonymous:anonymous` and no other fields are populated.
   279  //
   280  // On errors the state endpoint replies with a non-200 HTTP status code with a
   281  // `plain/text` body containing the error message. This is an exceptional
   282  // situation (usually internal transient errors caused by the session store
   283  // unavailability or some misconfiguration in code). Replies with HTTP code
   284  // equal or larger than 500 indicate transient errors and can be retried.
   285  //
   286  // The state endpoint is exposed only by auth methods that implement
   287  // HasStateEndpoint interface (e.g. `encryptedcookies`), and only if they are
   288  // configured to expose it.
   289  type StateEndpointResponse struct {
   290  	// Identity is a LUCI identity string of the user or `anonymous:anonymous` if
   291  	// the user is not logged in.
   292  	Identity string `json:"identity"`
   293  
   294  	// Email is the email of the user account if the user is logged in.
   295  	Email string `json:"email,omitempty"`
   296  
   297  	// Picture is the https URL of the user profile picture if available.
   298  	Picture string `json:"picture,omitempty"`
   299  
   300  	// AccessToken is an OAuth access token of the logged in user.
   301  	//
   302  	// See RequiredScopes and OptionalScopes in AuthMethod for what scopes this
   303  	// token can have.
   304  	AccessToken string `json:"accessToken,omitempty"`
   305  
   306  	// AccessTokenExpiry is an absolute expiration time (as a unix timestamp) of
   307  	// the access token.
   308  	//
   309  	// It is at least 10 min in the future.
   310  	AccessTokenExpiry int64 `json:"accessTokenExpiry,omitempty"`
   311  
   312  	// AccessTokenExpiresIn is approximately how long the access token will be
   313  	// valid since when the response was generated, in seconds.
   314  	//
   315  	// It is at least 600 sec.
   316  	AccessTokenExpiresIn int32 `json:"accessTokenExpiresIn,omitempty"`
   317  
   318  	// IDToken is an identity token of the logged in user.
   319  	//
   320  	// Its `aud` claim is equal to ClientID in OpenIDConfig passed to AuthMethod.
   321  	IDToken string `json:"idToken,omitempty"`
   322  
   323  	// IDTokenExpiry is an absolute expiration time (as a unix timestamp) of
   324  	// the identity token.
   325  	//
   326  	// It is at least 10 min in the future.
   327  	IDTokenExpiry int64 `json:"idTokenExpiry,omitempty"`
   328  
   329  	// IDTokenExpiresIn is approximately how long the identity token will be
   330  	// valid since when the response was generated, in seconds.
   331  	//
   332  	// It is at least 600 sec.
   333  	IDTokenExpiresIn int32 `json:"idTokenExpiresIn,omitempty"`
   334  }
   335  
   336  // Authenticator performs authentication of incoming requests.
   337  //
   338  // It is a stateless object configured with a list of methods to try when
   339  // authenticating incoming requests. It implements Authenticate method that
   340  // performs high-level authentication logic using the provided list of low-level
   341  // auth methods.
   342  //
   343  // Note that most likely you don't need to instantiate this object directly.
   344  // Use Authenticate middleware instead. Authenticator is exposed publicly only
   345  // to be used in advanced cases, when you need to fine-tune authentication
   346  // behavior.
   347  type Authenticator struct {
   348  	Methods []Method // a list of authentication methods to try
   349  }
   350  
   351  // GetMiddleware returns a middleware that uses this Authenticator for
   352  // authentication.
   353  //
   354  // It uses a.Authenticate internally and handles errors appropriately.
   355  //
   356  // TODO(vadimsh): Refactor to be a function instead of a method and move to
   357  // http.go.
   358  func (a *Authenticator) GetMiddleware() router.Middleware {
   359  	return func(c *router.Context, next router.Handler) {
   360  		ctx, err := a.AuthenticateHTTP(c.Request.Context(), c.Request)
   361  		if err != nil {
   362  			code, ok := grpcutil.Tag.In(err)
   363  			if !ok {
   364  				if transient.Tag.In(err) {
   365  					code = codes.Internal
   366  				} else {
   367  					code = codes.Unauthenticated
   368  				}
   369  			}
   370  			replyError(c.Request.Context(), c.Writer, grpcutil.CodeStatus(code), err)
   371  		} else {
   372  			c.Request = c.Request.WithContext(ctx)
   373  			next(c)
   374  		}
   375  	}
   376  }
   377  
   378  // AuthenticateHTTP authenticates an HTTP request.
   379  //
   380  // See Authenticate for all details.
   381  //
   382  // This method is likely temporary until pRPC server switches to use gRPC
   383  // interceptors for authentication.
   384  func (a *Authenticator) AuthenticateHTTP(ctx context.Context, r *http.Request) (context.Context, error) {
   385  	return a.Authenticate(ctx, RequestMetadataForHTTP(r))
   386  }
   387  
   388  // Authenticate authenticates the request and adds State into the context.
   389  //
   390  // Returns an error if credentials are provided, but invalid. If no credentials
   391  // are provided (i.e. the request is anonymous), finishes successfully, but in
   392  // that case CurrentIdentity() returns AnonymousIdentity.
   393  //
   394  // The returned error may be tagged with an grpcutil error tag. Its code should
   395  // be used to derive the response status code. Internal error messages (e.g.
   396  // ones tagged with grpcutil.InternalTag or similar) should be logged, but not
   397  // sent to clients. All other errors should be sent to clients as is.
   398  func (a *Authenticator) Authenticate(ctx context.Context, r RequestMetadata) (_ context.Context, err error) {
   399  	tracedCtx, span := tracing.Start(ctx, "go.chromium.org/luci/server/auth.Authenticate")
   400  	report := durationReporter(tracedCtx, authenticateDuration)
   401  
   402  	// This variable is changed throughout the function's execution. It it used
   403  	// in the defer to figure out at what stage the call failed.
   404  	stage := ""
   405  
   406  	// This defer reports the outcome of the authentication to the monitoring.
   407  	defer func() {
   408  		switch {
   409  		case err == nil:
   410  			report(nil, "SUCCESS")
   411  		case err == ErrNotConfigured:
   412  			report(err, "ERROR_NOT_CONFIGURED")
   413  		case err == ErrBadClientID:
   414  			report(err, "ERROR_FORBIDDEN_OAUTH_CLIENT")
   415  		case err == ErrBadAudience:
   416  			report(err, "ERROR_FORBIDDEN_AUDIENCE")
   417  		case err == ErrBadRemoteAddr:
   418  			report(err, "ERROR_BAD_REMOTE_ADDR")
   419  		case err == ErrForbiddenIP:
   420  			report(err, "ERROR_FORBIDDEN_IP")
   421  		case err == ErrProjectHeaderForbidden:
   422  			report(err, "ERROR_PROJECT_HEADER_FORBIDDEN")
   423  		case transient.Tag.In(err):
   424  			report(err, "ERROR_TRANSIENT_IN_"+stage)
   425  		default:
   426  			report(err, "ERROR_IN_"+stage)
   427  		}
   428  		tracing.End(span, err)
   429  	}()
   430  
   431  	// We will need working DB factory below to check IP allowlist.
   432  	cfg := getConfig(tracedCtx)
   433  	if cfg == nil || cfg.DBProvider == nil || len(a.Methods) == 0 {
   434  		return nil, ErrNotConfigured
   435  	}
   436  
   437  	// The future state that will be placed into the context.
   438  	s := state{authenticator: a, endUserErr: ErrNoForwardableCreds}
   439  
   440  	// Pick the first authentication method that applies.
   441  	stage = "AUTH"
   442  	for _, m := range a.Methods {
   443  		var err error
   444  		if s.user, s.session, err = m.Authenticate(tracedCtx, r); err != nil {
   445  			return nil, err
   446  		}
   447  		if s.user != nil {
   448  			if err = s.user.Identity.Validate(); err != nil {
   449  				stage = "ID_REGEXP_CHECK"
   450  				return nil, err
   451  			}
   452  			s.method = m
   453  			break
   454  		}
   455  	}
   456  
   457  	// If no authentication method is applicable, default to anonymous identity.
   458  	if s.method == nil {
   459  		s.user = &User{Identity: identity.AnonymousIdentity}
   460  		s.session = nil
   461  	}
   462  
   463  	// peerIdent always matches the identity of a remote peer. It may end up being
   464  	// different from s.user.Identity if the delegation tokens or project
   465  	// identities are used (see below). They affect s.user.Identity but don't
   466  	// touch s.peerIdent.
   467  	s.peerIdent = s.user.Identity
   468  
   469  	// Grab a snapshot of auth DB to use it consistently for the duration of this
   470  	// request.
   471  	stage = "AUTHDB_FETCH"
   472  	s.db, err = cfg.DBProvider(tracedCtx)
   473  	if err != nil {
   474  		return nil, err
   475  	}
   476  
   477  	// If using OAuth2, make sure the ClientID is allowlisted.
   478  	if s.user.ClientID != "" {
   479  		stage = "OAUTH_CLIENT_ID_CHECK"
   480  		if err := checkClientID(tracedCtx, cfg, s.db, s.user.Email, s.user.ClientID); err != nil {
   481  			return nil, err
   482  		}
   483  	}
   484  
   485  	// Extract peer's IP address and, if necessary, check it against an allowlist.
   486  	stage = "IP_CHECK"
   487  	if s.peerIP, err = checkEndUserIP(tracedCtx, cfg, s.db, r, s.peerIdent); err != nil {
   488  		return nil, err
   489  	}
   490  
   491  	// Check X-Delegation-Token-V1 and X-Luci-Project headers. They are used in
   492  	// LUCI-specific protocols to allow LUCI micro-services to act on behalf of
   493  	// end-users or projects.
   494  	var delegationToken string
   495  	var projectHeader string
   496  	if delegationToken = r.Header(delegation.HTTPHeaderName); delegationToken != "" {
   497  		stage = "DELEGATION_TOKEN_CHECK"
   498  		if s.user, err = checkDelegationToken(tracedCtx, cfg, s.db, delegationToken, s.peerIdent); err != nil {
   499  			return nil, err
   500  		}
   501  	} else if projectHeader = r.Header(XLUCIProjectHeader); projectHeader != "" {
   502  		stage = "PROJECT_HEADER_CHECK"
   503  		if s.user, err = checkProjectHeader(tracedCtx, s.db, projectHeader, s.peerIdent); err != nil {
   504  			return nil, err
   505  		}
   506  	}
   507  
   508  	// If the main authentication mechanism is based on forwardable OAuth tokens,
   509  	// grab all forwardable headers for GetRPCTransport(AsCredentialsForwarder).
   510  	if credsGetter, _ := s.method.(UserCredentialsGetter); credsGetter != nil {
   511  		s.endUserTok, s.endUserErr = credsGetter.GetUserCredentials(tracedCtx, r)
   512  		if s.endUserErr == nil && (delegationToken != "" || projectHeader != "") {
   513  			s.endUserExtraHeaders = make(map[string]string, 2)
   514  			if delegationToken != "" {
   515  				s.endUserExtraHeaders[delegation.HTTPHeaderName] = delegationToken
   516  			}
   517  			if projectHeader != "" {
   518  				s.endUserExtraHeaders[XLUCIProjectHeader] = projectHeader
   519  			}
   520  		}
   521  	}
   522  
   523  	// Inject the auth state into the original context (not the traced one).
   524  	return WithState(ctx, &s), nil
   525  }
   526  
   527  // usersAPI returns implementation of UsersAPI by examining Methods.
   528  //
   529  // Returns nil if none of Methods implement UsersAPI.
   530  func (a *Authenticator) usersAPI() UsersAPI {
   531  	for _, m := range a.Methods {
   532  		if api, ok := m.(UsersAPI); ok {
   533  			return api
   534  		}
   535  	}
   536  	return nil
   537  }
   538  
   539  // LoginURL returns a URL that, when visited, prompts the user to sign in,
   540  // then redirects the user to the URL specified by dest.
   541  //
   542  // Returns ErrNoUsersAPI if none of the authentication methods support login
   543  // URLs.
   544  func (a *Authenticator) LoginURL(ctx context.Context, dest string) (string, error) {
   545  	if api := a.usersAPI(); api != nil {
   546  		return api.LoginURL(ctx, dest)
   547  	}
   548  	return "", ErrNoUsersAPI
   549  }
   550  
   551  // LogoutURL returns a URL that, when visited, signs the user out, then
   552  // redirects the user to the URL specified by dest.
   553  //
   554  // Returns ErrNoUsersAPI if none of the authentication methods support login
   555  // URLs.
   556  func (a *Authenticator) LogoutURL(ctx context.Context, dest string) (string, error) {
   557  	if api := a.usersAPI(); api != nil {
   558  		return api.LogoutURL(ctx, dest)
   559  	}
   560  	return "", ErrNoUsersAPI
   561  }
   562  
   563  ////
   564  
   565  // replyError logs the error and writes a response to ResponseWriter.
   566  //
   567  // For codes < 500, the error is logged at Warning level and written to the
   568  // response as is. For codes >= 500 the error is logged at Error level and
   569  // the generic error message is written instead.
   570  func replyError(ctx context.Context, rw http.ResponseWriter, code int, err error) {
   571  	if code < 500 {
   572  		logging.Warningf(ctx, "HTTP %d: %s", code, err)
   573  		http.Error(rw, err.Error(), code)
   574  	} else {
   575  		logging.Errorf(ctx, "HTTP %d: %s", code, err)
   576  		http.Error(rw, http.StatusText(code), code)
   577  	}
   578  }
   579  
   580  // checkClientID returns nil if the clientID is allowed, ErrBadClientID if not,
   581  // and a transient errors if the check itself failed.
   582  func checkClientID(ctx context.Context, cfg *Config, db authdb.DB, email, clientID string) error {
   583  	// Check the global allowlist in the AuthDB.
   584  	switch valid, err := db.IsAllowedOAuthClientID(ctx, email, clientID); {
   585  	case err != nil:
   586  		return errors.Annotate(err, "failed to check client ID allowlist").Tag(transient.Tag).Err()
   587  	case valid:
   588  		return nil
   589  	}
   590  
   591  	// It may be an app-specific client ID supplied via cfg.FrontendClientID.
   592  	if cfg.FrontendClientID != nil {
   593  		switch frontendClientID, err := cfg.FrontendClientID(ctx); {
   594  		case err != nil:
   595  			return errors.Annotate(err, "failed to grab frontend client ID").Tag(transient.Tag).Err()
   596  		case clientID == frontendClientID:
   597  			return nil
   598  		}
   599  	}
   600  
   601  	logging.Errorf(ctx, "auth: %q is using client_id %q not in the allowlist", email, clientID)
   602  	return ErrBadClientID
   603  }
   604  
   605  // checkEndUserIP parses the caller IP address and checks it against an
   606  // allowlist (if necessary). Returns ErrBadRemoteAddr if the IP is malformed,
   607  // ErrForbiddenIP if the IP is not allowlisted or a transient error if the check
   608  // itself failed.
   609  func checkEndUserIP(ctx context.Context, cfg *Config, db authdb.DB, r RequestMetadata, peerID identity.Identity) (net.IP, error) {
   610  	var ipAddr string
   611  	if cfg.EndUserIP != nil {
   612  		ipAddr = cfg.EndUserIP(r)
   613  	} else {
   614  		ipAddr = r.RemoteAddr()
   615  	}
   616  	peerIP, err := parseRemoteIP(ipAddr)
   617  	if err != nil {
   618  		logging.Errorf(ctx, "auth: bad remote_addr %q in a call from %q - %s", ipAddr, peerID, err)
   619  		return nil, ErrBadRemoteAddr
   620  	}
   621  	if peerIP.IsUnspecified() {
   622  		return peerIP, nil
   623  	}
   624  
   625  	// Some callers may be constrained by an IP allowlist.
   626  	switch ipAllowlist, err := db.GetAllowlistForIdentity(ctx, peerID); {
   627  	case err != nil:
   628  		return nil, errors.Annotate(err, "failed to get IP allowlist for identity %q", peerID).Tag(transient.Tag).Err()
   629  	case ipAllowlist != "":
   630  		switch allowed, err := db.IsAllowedIP(ctx, peerIP, ipAllowlist); {
   631  		case err != nil:
   632  			return nil, errors.Annotate(err, "failed to check IP %s is in the allowlist %q", peerIP, ipAllowlist).Tag(transient.Tag).Err()
   633  		case !allowed:
   634  			return nil, ErrForbiddenIP
   635  		}
   636  	}
   637  
   638  	return peerIP, nil
   639  }
   640  
   641  // checkDelegationToken checks correctness of a delegation token and returns
   642  // a delegated *User.
   643  func checkDelegationToken(ctx context.Context, cfg *Config, db authdb.DB, token string, peerID identity.Identity) (*User, error) {
   644  	// Log the token fingerprint (even before parsing the token), it can be used
   645  	// to grab the info about the token from the token server logs.
   646  	logging.Fields{
   647  		"fingerprint": tokenFingerprint(token),
   648  	}.Debugf(ctx, "auth: Received delegation token")
   649  
   650  	// Need to grab our own identity to verify that the delegation token is
   651  	// minted for consumption by us and not some other service.
   652  	ownServiceIdentity, err := getOwnServiceIdentity(ctx, cfg.Signer)
   653  	if err != nil {
   654  		return nil, err
   655  	}
   656  	delegatedIdentity, err := delegation.CheckToken(ctx, delegation.CheckTokenParams{
   657  		Token:                token,
   658  		PeerID:               peerID,
   659  		CertificatesProvider: db,
   660  		GroupsChecker:        db,
   661  		OwnServiceIdentity:   ownServiceIdentity,
   662  	})
   663  	if err != nil {
   664  		return nil, err
   665  	}
   666  
   667  	// Log that peerID is pretending to be delegatedIdentity.
   668  	logging.Fields{
   669  		"peerID":      peerID,
   670  		"delegatedID": delegatedIdentity,
   671  	}.Debugf(ctx, "auth: Using delegation")
   672  
   673  	return &User{Identity: delegatedIdentity}, nil
   674  }
   675  
   676  // checkProjectHeader verifies the caller is allowed to use X-Luci-Project
   677  // mechanism and returns a *User (with project-scoped identity) to use for
   678  // the request.
   679  func checkProjectHeader(ctx context.Context, db authdb.DB, project string, peerID identity.Identity) (*User, error) {
   680  	// See comment for InternalServicesGroup.
   681  	switch yes, err := db.IsMember(ctx, peerID, []string{InternalServicesGroup}); {
   682  	case err != nil:
   683  		return nil, errors.Annotate(err, "error when checking if %q is in %q", peerID, InternalServicesGroup).Tag(transient.Tag).Err()
   684  	case !yes:
   685  		return nil, ErrProjectHeaderForbidden
   686  	}
   687  
   688  	// Verify the actual value passes the regexp check.
   689  	projIdent, err := identity.MakeIdentity("project:" + project)
   690  	if err != nil {
   691  		return nil, errors.Annotate(err, "bad %s", XLUCIProjectHeader).Err()
   692  	}
   693  
   694  	// Log that peerID is using project-scoped identity.
   695  	logging.Fields{
   696  		"peerID":    peerID,
   697  		"projectID": projIdent,
   698  	}.Debugf(ctx, "auth: Using project identity")
   699  
   700  	return &User{Identity: projIdent}, nil
   701  }
   702  
   703  // getOwnServiceIdentity returns 'service:<appID>' identity of the current
   704  // service.
   705  func getOwnServiceIdentity(ctx context.Context, signer signing.Signer) (identity.Identity, error) {
   706  	if signer == nil {
   707  		return "", ErrNotConfigured
   708  	}
   709  	switch serviceInfo, err := signer.ServiceInfo(ctx); {
   710  	case err != nil:
   711  		return "", err
   712  	case serviceInfo.AppID == "":
   713  		return "", errors.Reason("auth: don't known our own app ID to check the delegation token is for us").Err()
   714  	default:
   715  		return identity.MakeIdentity("service:" + serviceInfo.AppID)
   716  	}
   717  }