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

     1  // Copyright 2020 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  //go:build !copybara
    16  // +build !copybara
    17  
    18  package internal
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"net/http"
    24  	"time"
    25  
    26  	"golang.org/x/oauth2"
    27  	"google.golang.org/grpc/codes"
    28  	"google.golang.org/grpc/metadata"
    29  	"google.golang.org/grpc/status"
    30  
    31  	"go.chromium.org/luci/common/retry/transient"
    32  	"go.chromium.org/luci/grpc/grpcutil"
    33  	"go.chromium.org/luci/grpc/prpc"
    34  	"go.chromium.org/luci/tokenserver/api/minter/v1"
    35  )
    36  
    37  // requestedMinValidityDuration defines a range of expiration durations of
    38  // tokens returned by luciTSTokenProvider.
    39  //
    40  // They expire within [now+requestedMinValidityDuration, now+1h) interval.
    41  //
    42  // If requestedMinValidityDuration is small, there's a greater cache hit ratio
    43  // on the Token Server side, but lazy luci-auth users that just grab a single
    44  // token and try to reuse it for e.g. 50 min without refreshing will suffer.
    45  //
    46  // If it is large such lazy abuse would work, but the Token Server will be
    47  // forced to call Cloud IAM API each time MintServiceAccountToken is called,
    48  // threatening to hit a quota on this call.
    49  //
    50  // We pick a value somewhere in the middle. Note that for better UX it must be
    51  // larger than a maximum allowed `-lifetime` parameter in `luci-auth token`,
    52  // which is currently 30 min.
    53  const requestedMinValidityDuration = 35 * time.Minute
    54  
    55  type luciTSTokenProvider struct {
    56  	host      string
    57  	actAs     string
    58  	realm     string
    59  	scopes    []string
    60  	audience  string // not empty iff using ID tokens
    61  	transport http.RoundTripper
    62  	cacheKey  CacheKey
    63  }
    64  
    65  func init() {
    66  	NewLUCITSTokenProvider = func(ctx context.Context, host, actAs, realm string, scopes []string, audience string, transport http.RoundTripper) (TokenProvider, error) {
    67  		return &luciTSTokenProvider{
    68  			host:      host,
    69  			actAs:     actAs,
    70  			realm:     realm,
    71  			scopes:    scopes,
    72  			audience:  audience,
    73  			transport: transport,
    74  			cacheKey: CacheKey{
    75  				Key:    fmt.Sprintf("luci_ts/%s/%s/%s", actAs, host, realm),
    76  				Scopes: scopes,
    77  			},
    78  		}, nil
    79  	}
    80  }
    81  
    82  func (p *luciTSTokenProvider) RequiresInteraction() bool {
    83  	return false
    84  }
    85  
    86  func (p *luciTSTokenProvider) Lightweight() bool {
    87  	return false
    88  }
    89  
    90  func (p *luciTSTokenProvider) Email() string {
    91  	return p.actAs
    92  }
    93  
    94  func (p *luciTSTokenProvider) CacheKey(ctx context.Context) (*CacheKey, error) {
    95  	return &p.cacheKey, nil
    96  }
    97  
    98  func (p *luciTSTokenProvider) MintToken(ctx context.Context, base *Token) (*Token, error) {
    99  	client := minter.NewTokenMinterClient(&prpc.Client{
   100  		C: &http.Client{
   101  			Transport: &tokenInjectingTransport{
   102  				transport: p.transport,
   103  				token:     &base.Token,
   104  			},
   105  		},
   106  		Host: p.host,
   107  		Options: &prpc.Options{
   108  			Retry:         nil,              // the caller of MintToken retries itself
   109  			PerRPCTimeout: 30 * time.Second, // the call should be relatively fast
   110  		},
   111  	})
   112  
   113  	// TODO(crbug.com/1179629): pRPC doesn't handle outgoing meatadata well.
   114  	ctx = metadata.NewOutgoingContext(ctx, nil)
   115  
   116  	req := &minter.MintServiceAccountTokenRequest{
   117  		ServiceAccount:      p.actAs,
   118  		Realm:               p.realm,
   119  		MinValidityDuration: int64(requestedMinValidityDuration / time.Second),
   120  	}
   121  	if p.audience != "" {
   122  		req.TokenKind = minter.ServiceAccountTokenKind_SERVICE_ACCOUNT_TOKEN_ID_TOKEN
   123  		req.IdTokenAudience = p.audience
   124  	} else {
   125  		req.TokenKind = minter.ServiceAccountTokenKind_SERVICE_ACCOUNT_TOKEN_ACCESS_TOKEN
   126  		req.OauthScope = p.scopes
   127  	}
   128  
   129  	resp, err := client.MintServiceAccountToken(ctx, req)
   130  	if err != nil {
   131  		// Want to retry on per-RPC deadlines.
   132  		if status.Code(err) == codes.DeadlineExceeded {
   133  			return nil, transient.Tag.Apply(err)
   134  		}
   135  		// And also on standard retriable errors like Unavailable.
   136  		return nil, grpcutil.WrapIfTransient(err)
   137  	}
   138  
   139  	accessToken := NoAccessToken
   140  	idToken := NoIDToken
   141  	if p.audience != "" {
   142  		idToken = resp.Token
   143  	} else {
   144  		accessToken = resp.Token
   145  	}
   146  
   147  	return &Token{
   148  		Token: oauth2.Token{
   149  			AccessToken: accessToken,
   150  			Expiry:      resp.Expiry.AsTime(),
   151  			TokenType:   "Bearer",
   152  		},
   153  		IDToken: idToken,
   154  		Email:   p.Email(),
   155  	}, nil
   156  }
   157  
   158  func (p *luciTSTokenProvider) RefreshToken(ctx context.Context, prev, base *Token) (*Token, error) {
   159  	return p.MintToken(ctx, base)
   160  }