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

     1  // Copyright 2016 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  	"crypto/sha256"
    20  	"encoding/hex"
    21  	"net/http"
    22  	"net/http/httptrace"
    23  	"net/url"
    24  	"regexp"
    25  	"sort"
    26  	"strings"
    27  	"time"
    28  
    29  	"go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
    30  	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    31  	"golang.org/x/oauth2"
    32  	"google.golang.org/grpc/credentials"
    33  
    34  	"go.chromium.org/luci/auth"
    35  	"go.chromium.org/luci/auth/identity"
    36  	"go.chromium.org/luci/common/errors"
    37  	"go.chromium.org/luci/common/logging"
    38  	"go.chromium.org/luci/common/tsmon/metric"
    39  
    40  	"go.chromium.org/luci/server/auth/delegation"
    41  	"go.chromium.org/luci/server/auth/internal"
    42  )
    43  
    44  // CloudOAuthScopes is a list of OAuth scopes recommended to use when
    45  // authenticating to Google Cloud services.
    46  //
    47  // Besides the actual cloud-platform scope also includes userinfo.email scope,
    48  // so that it is possible to examine the token email.
    49  //
    50  // Note that it is preferable to use the exact same list of scopes in all
    51  // Cloud API clients. That way when the server runs locally in a development
    52  // mode, we need to go through the login flow only once. Using different scopes
    53  // for different clients would require to "login" for each unique set of scopes.
    54  var CloudOAuthScopes = []string{
    55  	"https://www.googleapis.com/auth/cloud-platform",
    56  	"https://www.googleapis.com/auth/userinfo.email",
    57  }
    58  
    59  // RPCAuthorityKind defines under whose authority RPCs are made.
    60  type RPCAuthorityKind int
    61  
    62  const (
    63  	// NoAuth is used for outbound RPCs that don't have any implicit auth headers.
    64  	NoAuth RPCAuthorityKind = iota
    65  
    66  	// AsSelf is used for outbound RPCs sent with the authority of the current
    67  	// service itself.
    68  	//
    69  	// RPC requests done in this mode will have 'Authorization' header set to
    70  	// either an OAuth2 access token or an ID token, depending on a presence of
    71  	// WithIDTokenAudience option.
    72  	//
    73  	// If WithIDTokenAudience is not given, RPCs will be authenticated with
    74  	// an OAuth2 access token of the service's own service account. The set of
    75  	// OAuth scopes can be customized via WithScopes option, and by default it
    76  	// is ["https://www.googleapis.com/auth/userinfo.email"].
    77  	//
    78  	// If WithIDTokenAudience is given, RPCs will be authenticated with an ID
    79  	// token that has `aud` claim set to the supplied value. WithScopes can't be
    80  	// used in this case, providing it will cause an error.
    81  	//
    82  	// In LUCI services AsSelf should be used very sparingly, only for internal
    83  	// "maintenance" RPCs that happen outside of the context of any LUCI project.
    84  	// Using AsSelf to authorize RPCs that touch project data leads to "confused
    85  	// deputy" problems. Prefer to use AsProject when possible.
    86  	AsSelf
    87  
    88  	// AsUser is used for outbound RPCs that inherit the authority of a user
    89  	// that initiated the request that is currently being handled, regardless of
    90  	// how exactly the user was authenticated.
    91  	//
    92  	// DEPRECATED.
    93  	//
    94  	// The implementation is based on LUCI-specific protocol that uses special
    95  	// delegation tokens. Only LUCI backends can understand them.
    96  	//
    97  	// If you need to call non-LUCI services, and incoming requests are
    98  	// authenticated via OAuth access tokens, use AsCredentialsForwarder instead.
    99  	//
   100  	// If the current request was initiated by an anonymous caller, the RPC will
   101  	// have no auth headers (just like in NoAuth mode).
   102  	//
   103  	// Can also be used together with MintDelegationToken to make requests on
   104  	// user behalf asynchronously. For example, to associate end-user authority
   105  	// with some delayed task, call MintDelegationToken (in a context of a user
   106  	// initiated request) when this task is created and store the resulting token
   107  	// along with the task. Then, to make an RPC on behalf of the user from the
   108  	// task use GetRPCTransport(ctx, AsUser, WithDelegationToken(token)).
   109  	AsUser
   110  
   111  	// AsSessionUser is used for outbound RPCs that inherit the authority of
   112  	// an end-user by using credentials stored in the current auth session.
   113  	//
   114  	// Works only if the method used to authenticate the incoming request supports
   115  	// this mechanism. Currently this is only go.chromium.org/luci/server/encryptedcookies.
   116  	//
   117  	// Unlike deprecated AsUser, which uses LUCI delegation tokens, AsSessionUser
   118  	// authenticates outbound RPCs using standard OAuth2 or ID tokens, making this
   119  	// mechanism more widely applicable.
   120  	//
   121  	// On a flip side, the implementation relies on OpenID Connect refresh tokens,
   122  	// which limits it only to real human accounts that can click buttons in the
   123  	// browser to go through the OpenID Connect sign in flow to get a refresh
   124  	// token and establish a session (i.e. service accounts are not supported).
   125  	// Thus this mechanism is primarily useful when implementing Web UIs that use
   126  	// session cookies for authentication and want to call other services on
   127  	// user's behalf from the backend side.
   128  	//
   129  	// By default RPCs performed with AsSessionUser use email-scoped OAuth2 access
   130  	// tokens with the client ID matching the current service OAuth2 client ID.
   131  	// There's no way to ask for more scopes (using WithScopes option would result
   132  	// in an error).
   133  	//
   134  	// If WithIDToken option is specified, RPCs use ID tokens with the audience
   135  	// matching the current service OAuth2 client ID. There's no way to customize
   136  	// the audience.
   137  	AsSessionUser
   138  
   139  	// AsCredentialsForwarder is used for outbound RPCs that just forward the
   140  	// user credentials, exactly as they were received by the service.
   141  	//
   142  	// For authenticated calls, works only if the current request was
   143  	// authenticated via a forwardable token, e.g. an OAuth2 access token.
   144  	//
   145  	// If the current request was initiated by an anonymous caller, the RPC will
   146  	// have no auth headers (just like in NoAuth mode).
   147  	//
   148  	// An attempt to use GetRPCTransport(ctx, AsCredentialsForwarder) with
   149  	// unsupported credentials results in an error.
   150  	AsCredentialsForwarder
   151  
   152  	// AsActor is used for outbound RPCs sent with the authority of some service
   153  	// account that the current service has "iam.serviceAccountTokenCreator" role
   154  	// in.
   155  	//
   156  	// RPC requests done in this mode will have 'Authorization' header set to
   157  	// either an OAuth2 access token or an ID token of the service account
   158  	// specified by WithServiceAccount option.
   159  	//
   160  	// What kind of token is used depends on a presence of WithIDTokenAudience
   161  	// option and it follows the rules described in AsSelf comment.
   162  	//
   163  	// TODO(crbug.com/1081932): Implement WithIDTokenAudience mode.
   164  	AsActor
   165  
   166  	// AsProject is used for outbounds RPCs sent with the authority of some LUCI
   167  	// project (specified via WithProject option).
   168  	//
   169  	// When used to call external services (anything that is not a part of the
   170  	// current LUCI deployment), uses 'Authorization' header with either an OAuth2
   171  	// access token or an ID token of the project-specific service account
   172  	// (specified in the LUCI project definition in 'projects.cfg' deployment
   173  	// configuration file).
   174  	//
   175  	// What kind of token is used in this case depends on a presence of
   176  	// WithIDTokenAudience option and it follows the rules described in AsSelf
   177  	// comment.
   178  	//
   179  	// When used to call LUCI services belonging the same LUCI deployment (per
   180  	// 'internal_service_regexp' setting in 'security.cfg' deployment
   181  	// configuration file) uses the current service's OAuth2 access token plus
   182  	// 'X-Luci-Project' header with the project name. Such calls are authenticated
   183  	// by the peer as coming from 'project:<name>' identity. Options WithScopes
   184  	// and WithIDTokenAudience are ignored in this case.
   185  	//
   186  	// TODO(crbug.com/1081932): Implement WithIDTokenAudience mode.
   187  	AsProject
   188  )
   189  
   190  // XLUCIProjectHeader is a header with the current project for internal LUCI
   191  // RPCs done via AsProject authority.
   192  const XLUCIProjectHeader = "X-Luci-Project"
   193  
   194  // RPCOption is an option for GetRPCTransport, GetPerRPCCredentials and
   195  // GetTokenSource functions.
   196  type RPCOption interface {
   197  	apply(opts *rpcOptions)
   198  }
   199  
   200  type rpcOption func(opts *rpcOptions)
   201  
   202  func (o rpcOption) apply(opts *rpcOptions) { o(opts) }
   203  
   204  // WithIDToken indicates to use ID tokens instead of OAuth2 tokens.
   205  //
   206  // If no audience is given via WithIDTokenAudience, uses "https://${host}"
   207  // by default.
   208  func WithIDToken() RPCOption {
   209  	return rpcOption(func(opts *rpcOptions) {
   210  		opts.idToken = true
   211  	})
   212  }
   213  
   214  // WithIDTokenAudience indicates to use ID tokens with a specific audience
   215  // instead of OAuth2 tokens.
   216  //
   217  // Implies WithIDToken.
   218  //
   219  // The token's `aud` claim will be set to the given value. It can be customized
   220  // per-request by using `${host}` which will be substituted with a host name of
   221  // the request URI.
   222  //
   223  // Usage example:
   224  //
   225  //	tr, err := auth.GetRPCTransport(ctx,
   226  //	  auth.AsSelf,
   227  //	  auth.WithIDTokenAudience("https://${host}"),
   228  //	)
   229  //	if err != nil {
   230  //	  return err
   231  //	}
   232  //	client := &http.Client{Transport: tr}
   233  //	...
   234  //
   235  // Not compatible with WithScopes.
   236  func WithIDTokenAudience(aud string) RPCOption {
   237  	return rpcOption(func(opts *rpcOptions) {
   238  		opts.idToken = true
   239  		opts.idTokenAud = aud
   240  	})
   241  }
   242  
   243  // WithScopes can be used to customize OAuth scopes for outbound RPC requests.
   244  //
   245  // Not compatible with WithIDTokenAudience.
   246  func WithScopes(scopes ...string) RPCOption {
   247  	return rpcOption(func(opts *rpcOptions) {
   248  		opts.scopes = append(opts.scopes, scopes...)
   249  	})
   250  }
   251  
   252  // WithProject can be used to generate an OAuth token with an identity of that
   253  // particular LUCI project.
   254  //
   255  // See AsProject for more info.
   256  func WithProject(project string) RPCOption {
   257  	return rpcOption(func(opts *rpcOptions) {
   258  		opts.project = project
   259  	})
   260  }
   261  
   262  // WithServiceAccount option must be used with AsActor authority kind to specify
   263  // what service account to act as.
   264  func WithServiceAccount(email string) RPCOption {
   265  	return rpcOption(func(opts *rpcOptions) {
   266  		opts.serviceAccount = email
   267  	})
   268  }
   269  
   270  // WithDelegationToken can be used to attach an existing delegation token to
   271  // requests made in AsUser mode.
   272  //
   273  // DEPRECATED.
   274  //
   275  // The token can be obtained earlier via MintDelegationToken call. The transport
   276  // doesn't attempt to validate it and just blindly sends it to the other side.
   277  func WithDelegationToken(token string) RPCOption {
   278  	return rpcOption(func(opts *rpcOptions) {
   279  		opts.delegationToken = token
   280  	})
   281  }
   282  
   283  // WithDelegationTags can be used to attach tags to the delegation token used
   284  // internally in AsUser mode.
   285  //
   286  // DEPRECATED.
   287  //
   288  // The recipient of the RPC that uses the delegation will be able to extract
   289  // them, if necessary. They are also logged in the token server logs.
   290  //
   291  // Each tag is a key:value string.
   292  //
   293  // Note that any delegation tags are ignored if the current request was
   294  // initiated by an anonymous caller, since delegation protocol is not actually
   295  // used in this case.
   296  func WithDelegationTags(tags ...string) RPCOption {
   297  	return rpcOption(func(opts *rpcOptions) {
   298  		opts.delegationTags = tags
   299  	})
   300  }
   301  
   302  // WithMonitoringClient allows to override 'client' field that goes into HTTP
   303  // client monitoring metrics (such as 'http/response_status').
   304  //
   305  // The default value of the field is "luci-go-server".
   306  //
   307  // Note that the metrics also include hostname of the target service (in 'name'
   308  // field), so in most cases it is fine to use the default client name.
   309  // Overriding it may be useful if you want to differentiate between requests
   310  // made to the same host from a bunch of different places in the code.
   311  //
   312  // This option has absolutely no effect when passed to GetPerRPCCredentials() or
   313  // GetTokenSource(). It applies only to GetRPCTransport().
   314  func WithMonitoringClient(client string) RPCOption {
   315  	return rpcOption(func(opts *rpcOptions) {
   316  		opts.monitoringClient = client
   317  	})
   318  }
   319  
   320  // GetRPCTransport returns http.RoundTripper to use for outbound HTTP RPC
   321  // requests.
   322  //
   323  // Usage:
   324  //
   325  //	tr, err := auth.GetRPCTransport(c, auth.AsSelf, auth.WithScopes("..."))
   326  //	if err != nil {
   327  //	  return err
   328  //	}
   329  //	client := &http.Client{Transport: tr}
   330  //	...
   331  func GetRPCTransport(ctx context.Context, kind RPCAuthorityKind, opts ...RPCOption) (http.RoundTripper, error) {
   332  	options, err := makeRPCOptions(kind, opts)
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  
   337  	config := getConfig(ctx)
   338  	if config == nil || config.AnonymousTransport == nil {
   339  		return nil, ErrNotConfigured
   340  	}
   341  
   342  	if options.checkCtx != nil {
   343  		if err := options.checkCtx(ctx); err != nil {
   344  			return nil, err
   345  		}
   346  	}
   347  
   348  	baseTransport := otelhttp.NewTransport(
   349  		// Wrap with tsmon metrics.
   350  		metric.InstrumentTransport(ctx,
   351  			config.AnonymousTransport(ctx),
   352  			options.monitoringClient,
   353  		),
   354  		// Further tweak OpenTelemetry tracing wrapper.
   355  		otelhttp.WithSpanNameFormatter(func(op string, r *http.Request) string {
   356  			return r.URL.Path
   357  		}),
   358  		otelhttp.WithClientTrace(func(ctx context.Context) *httptrace.ClientTrace {
   359  			return otelhttptrace.NewClientTrace(ctx, otelhttptrace.WithoutSubSpans())
   360  		}),
   361  	)
   362  	if options.kind == NoAuth {
   363  		return baseTransport, nil
   364  	}
   365  
   366  	rootState := GetState(ctx)
   367  	isRootStateBackground := isBackgroundState(rootState)
   368  
   369  	return auth.NewModifyingTransport(baseTransport, func(req *http.Request) error {
   370  		// Use the request context as the base to inherit its fields and deadlines,
   371  		// but substitute the auth state there with the state from the root context,
   372  		// if necessary. This allows to create an AsSelf RPC transport during
   373  		// the server startup and then share it from RPCs that have some non-trivial
   374  		// auth state.
   375  		reqCtx := req.Context()
   376  		if reqCtx == context.Background() {
   377  			reqCtx = ctx
   378  		} else {
   379  			reqState := GetState(reqCtx)
   380  			isReqStateBackground := isBackgroundState(reqState)
   381  			switch {
   382  			case reqState == rootState:
   383  				// Good, the exact same state (background or not), no need to create
   384  				// a new context.
   385  			case isRootStateBackground && isReqStateBackground:
   386  				// Good, both are background states and therefore equivalent, no need
   387  				// to create a new context.
   388  			case isRootStateBackground && !isReqStateBackground:
   389  				// Transports created from the background state can be used from any
   390  				// other state. Inherit `reqCtx` deadlines, but inject the background
   391  				// state there.
   392  				reqCtx = WithState(reqCtx, rootState)
   393  			case !isRootStateBackground && isReqStateBackground:
   394  				// A transport created from a user-authenticated state is attempted to
   395  				// be used from a background state, this smells like a bug.
   396  				panic("an RPC transport created from a user context is used from a background server context, this is not allowed")
   397  			case !isRootStateBackground && !isReqStateBackground:
   398  				// A transport created from inside one request handler is attempted to
   399  				// be used from another request handler, this also smells like a bug.
   400  				panic("a non-background RPC transport is shared between different request contexts, this is not allowed")
   401  			}
   402  		}
   403  		tok, extra, err := options.getRPCHeaders(reqCtx, options, req)
   404  		if err != nil {
   405  			return err
   406  		}
   407  		if tok != nil {
   408  			req.Header.Set("Authorization", tok.TokenType+" "+tok.AccessToken)
   409  		}
   410  		for k, v := range extra {
   411  			req.Header.Set(k, v)
   412  		}
   413  		return nil
   414  	}), nil
   415  }
   416  
   417  // GetPerRPCCredentials returns gRPC's PerRPCCredentials implementation.
   418  //
   419  // It can be used to authenticate outbound gPRC RPC's.
   420  func GetPerRPCCredentials(ctx context.Context, kind RPCAuthorityKind, opts ...RPCOption) (credentials.PerRPCCredentials, error) {
   421  	options, err := makeRPCOptions(kind, opts)
   422  	if err != nil {
   423  		return nil, err
   424  	}
   425  	if options.checkCtx != nil {
   426  		if err := options.checkCtx(ctx); err != nil {
   427  			return nil, err
   428  		}
   429  	}
   430  	return perRPCCreds{ctx, options}, nil
   431  }
   432  
   433  type perRPCCreds struct {
   434  	ctx     context.Context
   435  	options *rpcOptions
   436  }
   437  
   438  func (creds perRPCCreds) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
   439  	// Don't transfer tokens in clear text.
   440  	ri, _ := credentials.RequestInfoFromContext(ctx)
   441  	if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
   442  		return nil, errors.Annotate(err, "can't use per RPC credentials").Err()
   443  	}
   444  
   445  	// URI is needed for some auth modes to "lock" tokens to a concrete audience.
   446  	if len(uri) == 0 {
   447  		panic("perRPCCreds: no URI given")
   448  	}
   449  	u, err := url.Parse(uri[0])
   450  	if err != nil {
   451  		return nil, errors.Annotate(err, "malformed URI %q", uri[0]).Err()
   452  	}
   453  
   454  	// Some libraries (in particular Spanner), pass very bare bones `ctx` here
   455  	// (essentially context.Background() with gRPC metadata on top). Such contexts
   456  	// are not sufficient to call getRPCHeaders, so we merge it with creds.ctx
   457  	// to get a full-featured LUCI context that at the same time has the same
   458  	// deadline and cancellation as `ctx`.
   459  	ctx = &internal.MergedContext{
   460  		Root:     ctx,
   461  		Fallback: creds.ctx,
   462  	}
   463  
   464  	tok, extra, err := creds.options.getRPCHeaders(ctx, creds.options, &http.Request{URL: u})
   465  	switch {
   466  	case err != nil:
   467  		return nil, err
   468  	case tok == nil && len(extra) == 0:
   469  		return nil, nil
   470  	}
   471  
   472  	// gRPC metadata uses lower case keys by convention.
   473  	metadata := make(map[string]string, 1+len(extra))
   474  	if tok != nil {
   475  		metadata["authorization"] = tok.TokenType + " " + tok.AccessToken
   476  	}
   477  	for k, v := range extra {
   478  		metadata[strings.ToLower(k)] = v
   479  	}
   480  	return metadata, nil
   481  }
   482  
   483  func (creds perRPCCreds) RequireTransportSecurity() bool {
   484  	return true
   485  }
   486  
   487  // GetTokenSource returns an oauth2.TokenSource bound to the supplied Context.
   488  //
   489  // Supports only AsSelf, AsCredentialsForwarder and AsActor authority kinds,
   490  // since they are the only ones that exclusively use only Authorization header.
   491  //
   492  // While GetPerRPCCredentials is preferred, this can be used by packages that
   493  // cannot or do not properly handle this gRPC option.
   494  func GetTokenSource(ctx context.Context, kind RPCAuthorityKind, opts ...RPCOption) (oauth2.TokenSource, error) {
   495  	if kind != AsSelf && kind != AsCredentialsForwarder && kind != AsActor {
   496  		return nil, errors.Reason("GetTokenSource can only be used with AsSelf, AsCredentialsForwarder or AsActor authority kind").Err()
   497  	}
   498  	options, err := makeRPCOptions(kind, opts)
   499  	if err != nil {
   500  		return nil, err
   501  	}
   502  	if options.checkCtx != nil {
   503  		if err := options.checkCtx(ctx); err != nil {
   504  			return nil, err
   505  		}
   506  	}
   507  	if options.idTokenAudGen != nil {
   508  		// There's no access to an URI in oauth2.TokenSource.Token() method, can't
   509  		// use patterned audiences there.
   510  		return nil, errors.Reason("WithIDTokenAudience with patterned audience is not supported by GetTokenSource, " +
   511  			"use GetRPCTransport or GetPerRPCCredentials instead").Err()
   512  	}
   513  	return &tokenSource{ctx, options}, nil
   514  }
   515  
   516  type tokenSource struct {
   517  	ctx     context.Context
   518  	options *rpcOptions
   519  }
   520  
   521  func (ts *tokenSource) Token() (*oauth2.Token, error) {
   522  	tok, extra, err := ts.options.getRPCHeaders(ts.ctx, ts.options, nil)
   523  	switch {
   524  	case err != nil:
   525  		return nil, err
   526  	case tok == nil:
   527  		return nil, errors.Reason("using non-OAuth2 based credentials in TokenSource").Err()
   528  	case len(extra) != 0:
   529  		keys := make([]string, 0, len(extra))
   530  		for k := range extra {
   531  			keys = append(keys, k)
   532  		}
   533  		sort.Strings(keys)
   534  		return nil, errors.Reason("extra headers %q with credentials are not supported in TokenSource", keys).Err()
   535  	}
   536  	return tok, nil
   537  }
   538  
   539  ////////////////////////////////////////////////////////////////////////////////
   540  // Internal stuff.
   541  
   542  func init() {
   543  	// This is needed to allow packages imported by 'server/auth' to make
   544  	// authenticated calls. They can't use GetRPCTransport directly, since they
   545  	// can't import 'server/auth' (it creates an import cycle).
   546  	internal.RegisterClientFactory(func(ctx context.Context, scopes []string) (*http.Client, error) {
   547  		var t http.RoundTripper
   548  		var err error
   549  		if len(scopes) == 0 {
   550  			t, err = GetRPCTransport(ctx, NoAuth)
   551  		} else {
   552  			t, err = GetRPCTransport(ctx, AsSelf, WithScopes(scopes...))
   553  		}
   554  		if err != nil {
   555  			return nil, err
   556  		}
   557  		return &http.Client{Transport: t}, nil
   558  	})
   559  }
   560  
   561  // tokenFingerprint returns first 16 bytes of SHA256 of the token, as hex.
   562  //
   563  // Token fingerprints can be used to identify tokens without parsing them.
   564  func tokenFingerprint(tok string) string {
   565  	digest := sha256.Sum256([]byte(tok))
   566  	return hex.EncodeToString(digest[:16])
   567  }
   568  
   569  // rpcMocks are used exclusively in unit tests.
   570  type rpcMocks struct {
   571  	MintDelegationToken              func(context.Context, DelegationTokenParams) (*Token, error)
   572  	MintAccessTokenForServiceAccount func(context.Context, MintAccessTokenParams) (*Token, error)
   573  	MintIDTokenForServiceAccount     func(context.Context, MintIDTokenParams) (*Token, error)
   574  	MintProjectToken                 func(context.Context, ProjectTokenParams) (*Token, error)
   575  }
   576  
   577  // apply implements RPCOption interface.
   578  func (o *rpcMocks) apply(opts *rpcOptions) {
   579  	opts.rpcMocks = o
   580  }
   581  
   582  var defaultOAuthScopes = []string{auth.OAuthScopeEmail}
   583  
   584  // headersGetter returns a main Authorization token and optional additional
   585  // headers.
   586  //
   587  // `req` is an outbound request if known. May be nil. May not be fully
   588  // initialized for the gRPC case.
   589  type headersGetter func(ctx context.Context, opts *rpcOptions, req *http.Request) (*oauth2.Token, map[string]string, error)
   590  
   591  // audGenerator takes a request and returns an audience string derived from it.
   592  type audGenerator func(r *http.Request) (string, error)
   593  
   594  type rpcOptions struct {
   595  	kind             RPCAuthorityKind
   596  	project          string       // for AsProject
   597  	idToken          bool         // for AsSelf, AsProject, AsActor and AsSessionUser
   598  	idTokenAud       string       // for AsSelf, AsProject and AsActor
   599  	idTokenAudGen    audGenerator // non-nil iff idTokenAud is a pattern
   600  	scopes           []string     // for AsSelf, AsProject and AsActor
   601  	serviceAccount   string       // for AsActor
   602  	delegationToken  string       // for AsUser
   603  	delegationTags   []string     // for AsUser
   604  	monitoringClient string
   605  	checkCtx         func(ctx context.Context) error // optional, may be skipped
   606  	getRPCHeaders    headersGetter
   607  	rpcMocks         *rpcMocks
   608  }
   609  
   610  // makeRPCOptions applies all options and validates them.
   611  func makeRPCOptions(kind RPCAuthorityKind, opts []RPCOption) (*rpcOptions, error) {
   612  	options := &rpcOptions{kind: kind}
   613  	for _, o := range opts {
   614  		o.apply(options)
   615  	}
   616  
   617  	asSelfOrActorOrProject := options.kind == AsSelf ||
   618  		options.kind == AsActor ||
   619  		options.kind == AsProject
   620  
   621  	// Set default scopes.
   622  	if asSelfOrActorOrProject && !options.idToken && len(options.scopes) == 0 {
   623  		options.scopes = defaultOAuthScopes
   624  	}
   625  	// Set the default audience.
   626  	if options.kind != AsSessionUser && options.idToken && options.idTokenAud == "" {
   627  		options.idTokenAud = "https://${host}"
   628  	}
   629  
   630  	// Validate options.
   631  	if !asSelfOrActorOrProject && options.kind != AsSessionUser && options.idToken {
   632  		return nil, errors.Reason("WithIDToken can only be used with AsSelf, AsActor, AsProject or AsSessionUser authority kind").Err()
   633  	}
   634  	if !asSelfOrActorOrProject && options.idTokenAud != "" {
   635  		return nil, errors.Reason("WithIDTokenAudience can only be used with AsSelf, AsActor or AsProject authority kind").Err()
   636  	}
   637  	if !asSelfOrActorOrProject && len(options.scopes) != 0 {
   638  		return nil, errors.Reason("WithScopes can only be used with AsSelf, AsActor or AsProject authority kind").Err()
   639  	}
   640  	if options.idToken && len(options.scopes) != 0 {
   641  		return nil, errors.Reason("WithIDToken and WithScopes cannot be used together").Err()
   642  	}
   643  	if options.serviceAccount != "" && options.kind != AsActor {
   644  		return nil, errors.Reason("WithServiceAccount can only be used with AsActor authority kind").Err()
   645  	}
   646  	if options.serviceAccount == "" && options.kind == AsActor {
   647  		return nil, errors.Reason("AsActor authority kind requires WithServiceAccount option").Err()
   648  	}
   649  	if options.delegationToken != "" && options.kind != AsUser {
   650  		return nil, errors.Reason("WithDelegationToken can only be used with AsUser authority kind").Err()
   651  	}
   652  	if len(options.delegationTags) != 0 && options.kind != AsUser {
   653  		return nil, errors.Reason("WithDelegationTags can only be used with AsUser authority kind").Err()
   654  	}
   655  	if len(options.delegationTags) != 0 && options.delegationToken != "" {
   656  		return nil, errors.Reason("WithDelegationTags and WithDelegationToken cannot be used together").Err()
   657  	}
   658  	if options.project == "" && options.kind == AsProject {
   659  		return nil, errors.Reason("AsProject authority kind requires WithProject option").Err()
   660  	}
   661  
   662  	// Temporarily not supported combinations of options.
   663  	//
   664  	// TODO(crbug.com/1081932): Support.
   665  	if options.idToken && (options.kind == AsActor || options.kind == AsProject) {
   666  		return nil, errors.Reason("WithIDToken is not supported here yet").Err()
   667  	}
   668  
   669  	// Convert `idTokenAud` into a callback {http.Request => aud}. This is needed
   670  	// to support "${host}" substitution.
   671  	if options.idTokenAud != "" {
   672  		gen, err := parseAudPattern(options.idTokenAud)
   673  		if err != nil {
   674  			return nil, errors.Annotate(err, "bad WithIDTokenAudience value").Err()
   675  		}
   676  		options.idTokenAudGen = gen // this is nil if idTokenAud is not a pattern
   677  	}
   678  
   679  	// Validate 'kind' and pick correct implementation of getRPCHeaders.
   680  	switch options.kind {
   681  	case NoAuth:
   682  		options.getRPCHeaders = noAuthHeaders
   683  	case AsSelf:
   684  		if options.idTokenAud != "" {
   685  			options.getRPCHeaders = asSelfIDTokenHeaders
   686  		} else {
   687  			options.getRPCHeaders = asSelfOAuthHeaders
   688  		}
   689  	case AsUser:
   690  		options.getRPCHeaders = asUserHeaders
   691  	case AsSessionUser:
   692  		options.checkCtx = func(ctx context.Context) error {
   693  			_, err := currentSession(ctx)
   694  			return err
   695  		}
   696  		options.getRPCHeaders = asSessionUserHeaders
   697  	case AsCredentialsForwarder:
   698  		options.checkCtx = func(ctx context.Context) error {
   699  			_, _, err := forwardedCreds(ctx)
   700  			return err
   701  		}
   702  		options.getRPCHeaders = func(ctx context.Context, _ *rpcOptions, _ *http.Request) (*oauth2.Token, map[string]string, error) {
   703  			return forwardedCreds(ctx)
   704  		}
   705  	case AsActor:
   706  		options.getRPCHeaders = asActorHeaders
   707  	case AsProject:
   708  		options.getRPCHeaders = asProjectHeaders
   709  	default:
   710  		return nil, errors.Reason("unknown RPCAuthorityKind %d", options.kind).Err()
   711  	}
   712  
   713  	// Default value for "client" field in monitoring metrics.
   714  	if options.monitoringClient == "" {
   715  		options.monitoringClient = "luci-go-server"
   716  	}
   717  
   718  	return options, nil
   719  }
   720  
   721  // noAuthHeaders is getRPCHeaders for NoAuth mode.
   722  func noAuthHeaders(ctx context.Context, opts *rpcOptions, req *http.Request) (*oauth2.Token, map[string]string, error) {
   723  	return nil, nil, nil
   724  }
   725  
   726  // asSelfOAuthHeaders returns a map of authentication headers to add to outbound
   727  // RPC requests done in AsSelf mode when using OAuth2 access tokens.
   728  //
   729  // This will be called by the transport layer on each request.
   730  func asSelfOAuthHeaders(ctx context.Context, opts *rpcOptions, req *http.Request) (*oauth2.Token, map[string]string, error) {
   731  	cfg := getConfig(ctx)
   732  	if cfg == nil || cfg.AccessTokenProvider == nil {
   733  		return nil, nil, ErrNotConfigured
   734  	}
   735  	tok, err := cfg.AccessTokenProvider(ctx, opts.scopes)
   736  	if err != nil {
   737  		return nil, nil, errors.Annotate(err, "failed to get AsSelf access token").Err()
   738  	}
   739  	return tok, nil, nil
   740  }
   741  
   742  // asSelfIDTokenHeaders returns a map of authentication headers to add to
   743  // outbound RPC requests done in AsSelf mode when using ID tokens.
   744  //
   745  // This will be called by the transport layer on each request.
   746  func asSelfIDTokenHeaders(ctx context.Context, opts *rpcOptions, req *http.Request) (*oauth2.Token, map[string]string, error) {
   747  	cfg := getConfig(ctx)
   748  	if cfg == nil || cfg.Signer == nil {
   749  		return nil, nil, ErrNotConfigured
   750  	}
   751  
   752  	// Derive the audience string. It may have "${host}" var that is replaced
   753  	// based on the hostname in the `req`.
   754  	var aud string
   755  	if opts.idTokenAudGen != nil {
   756  		var err error
   757  		if aud, err = opts.idTokenAudGen(req); err != nil {
   758  			return nil, nil, errors.Annotate(err, "can't derive audience for ID token").Err()
   759  		}
   760  	} else {
   761  		// Using a static audience, not a pattern.
   762  		aud = opts.idTokenAud
   763  	}
   764  
   765  	// First try the environment-specific method of getting an ID token (e.g.
   766  	// querying it from the GCE metadata server). It may not be available (e.g.
   767  	// on GAE v1). We'll fall back to a more expensive generic method below.
   768  	if cfg.IDTokenProvider != nil {
   769  		tok, err := cfg.IDTokenProvider(ctx, aud)
   770  		return tok, nil, err
   771  	}
   772  
   773  	// The method below works almost everywhere, but it requires the service
   774  	// account to have iam.serviceAccountTokenCreator role on itself, which is
   775  	// a bit weird and not default.
   776  
   777  	// Discover our own service account name to use it as a target.
   778  	info, err := cfg.Signer.ServiceInfo(ctx)
   779  	switch {
   780  	case err != nil:
   781  		return nil, nil, errors.Annotate(err, "failed to get our own service info").Err()
   782  	case info.ServiceAccountName == "":
   783  		return nil, nil, errors.Reason("no service account name in our own service info").Err()
   784  	}
   785  
   786  	// Grab ID token for our own account. This uses our own IAM-scoped access
   787  	// token internally and also implements heavy caching of the result, so its
   788  	// fine to call it often.
   789  	mintTokenCall := MintIDTokenForServiceAccount
   790  	if opts.rpcMocks != nil && opts.rpcMocks.MintIDTokenForServiceAccount != nil {
   791  		mintTokenCall = opts.rpcMocks.MintIDTokenForServiceAccount
   792  	}
   793  	tok, err := mintTokenCall(ctx, MintIDTokenParams{
   794  		ServiceAccount: info.ServiceAccountName,
   795  		Audience:       aud,
   796  		MinTTL:         2 * time.Minute,
   797  	})
   798  	if err != nil {
   799  		return nil, nil, errors.Annotate(err, "failed to get our own ID token for %q with aud %q", info.ServiceAccountName, aud).Err()
   800  	}
   801  
   802  	return &oauth2.Token{
   803  		AccessToken: tok.Token,
   804  		TokenType:   "Bearer",
   805  		Expiry:      tok.Expiry,
   806  	}, nil, nil
   807  }
   808  
   809  // asUserHeaders returns a map of authentication headers to add to outbound
   810  // RPC requests done in AsUser mode.
   811  //
   812  // This will be called by the transport layer on each request.
   813  func asUserHeaders(ctx context.Context, opts *rpcOptions, req *http.Request) (*oauth2.Token, map[string]string, error) {
   814  	cfg := getConfig(ctx)
   815  	if cfg == nil || cfg.AccessTokenProvider == nil {
   816  		return nil, nil, ErrNotConfigured
   817  	}
   818  
   819  	delegationToken := ""
   820  	if opts.delegationToken != "" {
   821  		delegationToken = opts.delegationToken // WithDelegationToken was used
   822  	} else {
   823  		// Outbound RPC calls in the context of a request from anonymous caller are
   824  		// anonymous too. No need to use any authentication headers.
   825  		userIdent := CurrentIdentity(ctx)
   826  		if userIdent == identity.AnonymousIdentity {
   827  			return nil, nil, nil
   828  		}
   829  
   830  		// Only https:// are allowed, can't send bearer tokens in clear text.
   831  		if req.URL.Scheme != "https" {
   832  			return nil, nil, errors.Reason("refusing to use delegation tokens with non-https URL").Err()
   833  		}
   834  
   835  		// Grab a token that's good enough for at least 10 min. Outbound RPCs
   836  		// shouldn't last longer than that.
   837  		mintTokenCall := MintDelegationToken
   838  		if opts.rpcMocks != nil && opts.rpcMocks.MintDelegationToken != nil {
   839  			mintTokenCall = opts.rpcMocks.MintDelegationToken
   840  		}
   841  		tok, err := mintTokenCall(ctx, DelegationTokenParams{
   842  			TargetHost: req.URL.Hostname(),
   843  			Tags:       opts.delegationTags,
   844  			MinTTL:     10 * time.Minute,
   845  		})
   846  		if err != nil {
   847  			return nil, nil, errors.Annotate(err, "failed to mint AsUser delegation token").Err()
   848  		}
   849  		delegationToken = tok.Token
   850  	}
   851  
   852  	// Use our own OAuth token too, since the delegation token is bound to us.
   853  	oauthTok, err := cfg.AccessTokenProvider(ctx, []string{auth.OAuthScopeEmail})
   854  	if err != nil {
   855  		return nil, nil, errors.Annotate(err, "failed to get own access token").Err()
   856  	}
   857  
   858  	logging.Fields{
   859  		"fingerprint": tokenFingerprint(delegationToken),
   860  	}.Debugf(ctx, "auth: Sending delegation token")
   861  	return oauthTok, map[string]string{delegation.HTTPHeaderName: delegationToken}, nil
   862  }
   863  
   864  // forwardedCreds returns the end user token and any extra authentication
   865  // headers as they were received by the service.
   866  //
   867  // Returns (nil, nil, nil) if the incoming call was anonymous. Returns an error
   868  // if the incoming call was authenticated by non-forwardable credentials.
   869  func forwardedCreds(ctx context.Context) (*oauth2.Token, map[string]string, error) {
   870  	switch s := GetState(ctx); {
   871  	case s == nil:
   872  		return nil, nil, ErrNotConfigured
   873  	case s.User().Identity == identity.AnonymousIdentity:
   874  		return nil, nil, nil // nothing to forward if the call is anonymous
   875  	default:
   876  		// Grab the end user credentials (or an error) from the auth state, as
   877  		// put there by Authenticate(...).
   878  		return s.UserCredentials()
   879  	}
   880  }
   881  
   882  // asActorHeaders returns a map of authentication headers to add to outbound
   883  // RPC requests done in AsActor mode.
   884  //
   885  // This will be called by the transport layer on each request.
   886  func asActorHeaders(ctx context.Context, opts *rpcOptions, req *http.Request) (*oauth2.Token, map[string]string, error) {
   887  	mintTokenCall := MintAccessTokenForServiceAccount
   888  	if opts.rpcMocks != nil && opts.rpcMocks.MintAccessTokenForServiceAccount != nil {
   889  		mintTokenCall = opts.rpcMocks.MintAccessTokenForServiceAccount
   890  	}
   891  	tok, err := mintTokenCall(ctx, MintAccessTokenParams{
   892  		ServiceAccount: opts.serviceAccount,
   893  		Scopes:         opts.scopes,
   894  		MinTTL:         2 * time.Minute,
   895  	})
   896  	if err != nil {
   897  		return nil, nil, errors.Annotate(err, "failed to mint AsActor access token").Err()
   898  	}
   899  	return &oauth2.Token{
   900  		AccessToken: tok.Token,
   901  		TokenType:   "Bearer",
   902  		Expiry:      tok.Expiry,
   903  	}, nil, nil
   904  }
   905  
   906  // asProjectHeaders returns a map of authentication headers to add to outbound
   907  // RPC requests done in AsProject mode.
   908  //
   909  // This will be called by the transport layer on each request.
   910  func asProjectHeaders(ctx context.Context, opts *rpcOptions, req *http.Request) (*oauth2.Token, map[string]string, error) {
   911  	internal, err := isInternalURL(ctx, req.URL)
   912  	if err != nil {
   913  		return nil, nil, err
   914  	}
   915  
   916  	// For calls within a single LUCI deployment use the service's own OAuth2
   917  	// token and 'X-Luci-Project' header to convey the project identity to the
   918  	// peer.
   919  	if internal {
   920  		// TODO(vadimsh): Always use userinfo.email scope here, not the original
   921  		// one. The target of the call is a LUCI service, it generally doesn't care
   922  		// about non-email scopes, but *requires* userinfo.email.
   923  		tok, _, err := asSelfOAuthHeaders(ctx, opts, req)
   924  		return tok, map[string]string{XLUCIProjectHeader: opts.project}, err
   925  	}
   926  
   927  	// For calls to external (non-LUCI) services get an OAuth2 token of a project
   928  	// scoped service account.
   929  	mintTokenCall := MintProjectToken
   930  	if opts.rpcMocks != nil && opts.rpcMocks.MintProjectToken != nil {
   931  		mintTokenCall = opts.rpcMocks.MintProjectToken
   932  	}
   933  	mintParams := ProjectTokenParams{
   934  		MinTTL:      2 * time.Minute,
   935  		LuciProject: opts.project,
   936  		OAuthScopes: opts.scopes,
   937  	}
   938  
   939  	tok, err := mintTokenCall(ctx, mintParams)
   940  	if err != nil {
   941  		return nil, nil, errors.Annotate(err, "failed to mint AsProject access token").Err()
   942  	}
   943  
   944  	// TODO(fmatenaar): This is only during migration and needs to be removed
   945  	// eventually.
   946  	if tok == nil {
   947  		logging.Infof(ctx, "Project %s not found, fallback to service identity", opts.project)
   948  		return asSelfOAuthHeaders(ctx, opts, req)
   949  	}
   950  
   951  	return &oauth2.Token{
   952  		AccessToken: tok.Token,
   953  		TokenType:   "Bearer",
   954  		Expiry:      tok.Expiry,
   955  	}, nil, nil
   956  }
   957  
   958  // currentSession either returns the current session or ErrNotConfigured.
   959  func currentSession(ctx context.Context) (Session, error) {
   960  	if state := GetState(ctx); state != nil {
   961  		return state.Session(), nil
   962  	}
   963  	return nil, ErrNotConfigured
   964  }
   965  
   966  // asSessionUserHeaders returns a map of authentication headers to add to
   967  // outbound RPC requests done in AsSessionUser mode.
   968  //
   969  // This will be called by the transport layer on each request.
   970  func asSessionUserHeaders(ctx context.Context, opts *rpcOptions, _ *http.Request) (tok *oauth2.Token, _ map[string]string, err error) {
   971  	s, err := currentSession(ctx)
   972  	if err != nil {
   973  		return nil, nil, err
   974  	}
   975  	if s == nil {
   976  		return nil, nil, nil
   977  	}
   978  	if opts.idToken {
   979  		tok, err = s.IDToken(ctx)
   980  	} else {
   981  		tok, err = s.AccessToken(ctx)
   982  	}
   983  	return
   984  }
   985  
   986  // isInternalURL returns true if the URL points to a LUCI microservice belonging
   987  // to the same LUCI deployment as us.
   988  //
   989  // Returns an error if the URL is not https:// or there were errors accessing
   990  // the AuthDB to compare the URL against the list of LUCI services.
   991  func isInternalURL(ctx context.Context, u *url.URL) (bool, error) {
   992  	if u.Scheme != "https" {
   993  		return false, errors.Reason("AsProject can be used only with https:// targets, got %s", u).Err()
   994  	}
   995  	state := GetState(ctx)
   996  	if state == nil {
   997  		return false, ErrNotConfigured
   998  	}
   999  	return state.DB().IsInternalService(ctx, u.Hostname())
  1000  }
  1001  
  1002  var placeholderRe = regexp.MustCompile(`\${[^}]*}`)
  1003  
  1004  // parseAudPattern takes a pattern like "https://${host}" and produces
  1005  // a callback that knows how to fill it in given a *http.Request.
  1006  //
  1007  // Returns (nil, nil) if `pat` is not really a pattern but just a static string.
  1008  // Returns an error if `pat` looks like a malformed or unsupported pattern.
  1009  func parseAudPattern(pat string) (audGenerator, error) {
  1010  	// Recognized static string, use a cheesy check for mismatched curly braces.
  1011  	if !placeholderRe.MatchString(pat) {
  1012  		if strings.Contains(pat, "${") {
  1013  			return nil, errors.Reason("%q looks like a malformed pattern", pat).Err()
  1014  		}
  1015  		return nil, nil
  1016  	}
  1017  
  1018  	renderPat := func(req *http.Request) (out string, err error) {
  1019  		out = placeholderRe.ReplaceAllStringFunc(pat, func(match string) string {
  1020  			if err == nil {
  1021  				switch match {
  1022  				case "${host}":
  1023  					// Prefer a value of `Host` header when given.
  1024  					if req.Host != "" {
  1025  						return req.Host
  1026  					}
  1027  					return req.URL.Host
  1028  				default:
  1029  					err = errors.Reason("unknown var %s", match).Err()
  1030  				}
  1031  			}
  1032  			return ""
  1033  		})
  1034  		return
  1035  	}
  1036  
  1037  	// Verify all referenced vars are known by interpreting a phony request. That
  1038  	// way a set of supported vars is neatly referenced only in `renderPat`.
  1039  	_, err := renderPat(&http.Request{
  1040  		URL: &url.URL{
  1041  			Scheme: "https",
  1042  			Host:   "example.com",
  1043  			Path:   "/example",
  1044  		},
  1045  	})
  1046  	if err != nil {
  1047  		return nil, errors.Annotate(err, "bad pattern %q", pat).Err()
  1048  	}
  1049  
  1050  	return renderPat, nil
  1051  }