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 }