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 }