go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth/internal/common.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package internal contains code used internally by auth/integration. 16 package internal 17 18 import ( 19 "bytes" 20 "context" 21 "reflect" 22 "strings" 23 "sync" 24 "time" 25 26 "golang.org/x/oauth2" 27 28 "go.chromium.org/luci/common/clock" 29 "go.chromium.org/luci/common/data/rand/mathrand" 30 "go.chromium.org/luci/common/errors" 31 "go.chromium.org/luci/common/retry/transient" 32 ) 33 34 // expiryRandInterval is used by TokenExpiresInRnd. 35 const expiryRandInterval = 30 * time.Second 36 37 const ( 38 // NoEmail indicates an OAuth2 token is not associated with an email. 39 // 40 // See Token below. We need this special value to distinguish "an email can 41 // not possibly be fetched ever" from "the cached token doesn't have an email 42 // yet" cases. 43 NoEmail = "-" 44 45 // UnknownEmail indicates an OAuth2 token may potentially be associated with 46 // an email, but we haven't tried to fetch the email yet. 47 UnknownEmail = "" 48 49 // NoIDToken indicates it was impossible to obtain an ID token, e.g. no 50 // "openid" scope in the refresh token or the provider doesn't support ID 51 // tokens at all. 52 NoIDToken = "-" 53 54 // NoAccessToken indicates the access token was not returned by the provider. 55 // 56 // This can happen with providers that support only ID tokens. 57 NoAccessToken = "-" 58 ) 59 60 var ( 61 // ErrInsufficientAccess is returned by MintToken() if token can't be minted 62 // for given OAuth scopes. For example, if GCE instance wasn't granted access 63 // to requested scopes when it was created. 64 ErrInsufficientAccess = errors.New("can't get access token for the given account and scopes") 65 66 // ErrBadRefreshToken is returned by RefreshToken if refresh token was revoked 67 // or otherwise invalid. It means MintToken must be used to get a new refresh 68 // token. 69 ErrBadRefreshToken = errors.New("refresh_token is not valid") 70 71 // ErrBadCredentials is returned by MintToken or RefreshToken if provided 72 // offline credentials (like service account key) are invalid. 73 ErrBadCredentials = errors.New("invalid or unavailable service account credentials") 74 ) 75 76 // Token is an oauth2.Token with an email and ID token that correspond to it. 77 // 78 // Email may be an empty string, in which case we assume the email hasn't been 79 // fetched yet. It can also be a special NoEmail string, which means the token 80 // is not associated with an email (happens for tokens without 'userinfo.email' 81 // scope). 82 type Token struct { 83 oauth2.Token 84 85 IDToken string // an ID token derived directly from the access token or NoIDToken 86 Email string // an email or NoEmail or empty string (aka UnknownEmail) 87 } 88 89 // TokenProvider knows how to mint new tokens or refresh existing ones. 90 type TokenProvider interface { 91 // RequiresInteraction is true if provider may start user interaction 92 // in MintToken. 93 RequiresInteraction() bool 94 95 // Lightweight is true if MintToken is very cheap to call. 96 // 97 // In this case the token is not being cached on disk (only in memory), since 98 // it's easy to get a new one each time the process starts. 99 // 100 // By avoiding the disk cache, we reduce the chance of a leak. 101 Lightweight() bool 102 103 // Email is email associated with tokens produced by the provider, if known. 104 // 105 // May return UnknownEmail, which means the provider doesn't know the email 106 // in advance and RefreshToken must be used to get the token and the email. 107 // This happens, for example, for interactive providers before user has 108 // logged in. 109 // 110 // It can also be NoEmail which means the email is not available, even if 111 // caller is using RefreshToken. 112 Email() string 113 114 // CacheKey identifies a slot in the token cache to store the token in. 115 // 116 // Note: CacheKey MAY change during lifetime of a TokenProvider. It happens, 117 // for example, for ServiceAccount token provider if the underlying service 118 // account key is replaced while the process is still running. 119 CacheKey(ctx context.Context) (*CacheKey, error) 120 121 // MintToken launches authentication flow (possibly interactive) and returns 122 // a new refreshable token (or error). It must never return (nil, nil). 123 // 124 // In actor mode 'base' is an IAM-scoped sufficiently fresh oauth token. It's 125 // nil otherwise. Used by IAM-based token provider. 126 MintToken(ctx context.Context, base *Token) (*Token, error) 127 128 // RefreshToken takes existing token (probably expired, but not necessarily) 129 // and returns a new refreshed token. It should never do any user interaction. 130 // If a user interaction is required, a error should be returned instead. 131 // 132 // In actor mode 'base' is an IAM-scoped sufficiently fresh oauth token. It's 133 // nil otherwise. Used by IAM-based token provider. 134 RefreshToken(ctx context.Context, prev, base *Token) (*Token, error) 135 } 136 137 // TokenCache stores access and refresh tokens to avoid requesting them all 138 // the time. 139 type TokenCache interface { 140 // GetToken reads the token from cache. 141 // 142 // Returns (nil, nil) if requested token is not in the cache. 143 GetToken(key *CacheKey) (*Token, error) 144 145 // PutToken writes the token to cache. 146 PutToken(key *CacheKey, tok *Token) error 147 148 // DeleteToken removes the token from cache. 149 DeleteToken(key *CacheKey) error 150 } 151 152 // CacheKey identifies a slot in the token cache to store the token in. 153 type CacheKey struct { 154 // Key identifies an auth method being used to get the token and its 155 // parameters. 156 // 157 // Its exact form is not important, since it is used only for string matching 158 // when searching for a token inside the cache. 159 // 160 // The following forms are being used currently: 161 // * user/<client_id> when using UserCredentialsMethod with some ClientID. 162 // * service_account/<email>/<key_id> when using ServiceAccountMethod. 163 // * gce/<account> when using GCEMetadataMethod. 164 // * iam/<account> when using IAM actor mode. 165 // * luci_ts/<account>/<host>/<realm> when using Token Server actor mode. 166 // * luci_ctx/<digest> when using LUCIContextMethod. 167 Key string `json:"key"` 168 169 // Scopes is the list of requested OAuth scopes or an ID token audience. 170 // 171 // The token audience is indicated by a fake scope that looks like 172 // "audience:<value>". Cache keys are used only for map indexing, their exact 173 // content doesn't matter. Adding a separate field (like `Audience`) to the 174 // key causes complication with older binaries that read the token cache and 175 // don't know about the new field, so we abuse `Scopes` field instead. 176 Scopes []string `json:"scopes,omitempty"` 177 } 178 179 var bufPool = sync.Pool{} 180 181 // ToMapKey returns a string that can be used as map[string] key. 182 // 183 // This string IS NOT PRINTABLE. It's a merely a string-looking []byte. 184 func (k *CacheKey) ToMapKey() string { 185 b, _ := bufPool.Get().(*bytes.Buffer) 186 if b == nil { 187 b = &bytes.Buffer{} 188 } else { 189 b.Reset() 190 } 191 defer bufPool.Put(b) 192 b.WriteString(k.Key) 193 b.WriteByte(0) 194 for _, s := range k.Scopes { 195 b.WriteString(s) 196 b.WriteByte(0) 197 } 198 return b.String() 199 } 200 201 // EqualCacheKeys returns true if keys are equal. 202 func EqualCacheKeys(a, b *CacheKey) bool { 203 return reflect.DeepEqual(a, b) 204 } 205 206 // TokenExpiresIn returns True if the token is not valid or expires within given 207 // duration. 208 // 209 // The function returns True in any of the following conditions: 210 // - The token is not valid. 211 // - The token expires before now+lifetime. 212 // 213 // In all other cases it returns False. 214 func TokenExpiresIn(ctx context.Context, t *Token, lifetime time.Duration) bool { 215 if t == nil || t.AccessToken == "" { 216 return true 217 } 218 if t.Expiry.IsZero() { 219 return false 220 } 221 return t.Expiry.Round(0).Before(clock.Now(ctx).Add(lifetime)) 222 } 223 224 // TokenExpiresInRnd is like TokenExpiresIn, except it slightly randomizes the 225 // token expiration time. 226 // 227 // If the function returns False, the token expires past now+lifetime. In other 228 // words, it is totally safe to use the token until now+lifetime. The inverse of 229 // this statement is not correct though: if the function returns True, it 230 // doesn't necessarily imply the token will expire before now+lifetime. 231 // 232 // The function returns True in any of the following conditions: 233 // - The token is not valid. 234 // - The token expires before now+lifetime. 235 // - The token expiration time is between (now+lifetime, now+lifetime+rnd), 236 // where rnd is a uniformly distributed random number between 0 and 237 // expiryRandInterval sec (which is set to 30 sec). 238 // 239 // This is useful for processes that use multiple service account keys at 240 // around the same time. Without randomization, access tokens for such keys 241 // expire at the same time (strictly 1h after process startup, where 1h is 242 // the default token lifetime). This causes unnecessary contention on the token 243 // cache file. 244 func TokenExpiresInRnd(ctx context.Context, t *Token, lifetime time.Duration) bool { 245 if t == nil || t.AccessToken == "" { 246 return true 247 } 248 if t.Expiry.IsZero() { 249 return false 250 } 251 expiry := t.Expiry.Round(0) // force to use wall clock time 252 deadline := clock.Now(ctx).Add(lifetime) 253 if expiry.Before(deadline) { 254 // Definitely expires within 'lifetime'. 255 return true 256 } 257 if expiry.After(deadline.Add(expiryRandInterval)) { 258 // Definitely expires much later than 'lifetime', no need to involve RNG. 259 return false 260 } 261 // Semi-randomly declare it as expired. 262 rnd := time.Duration(mathrand.Int63n(ctx, int64(expiryRandInterval))) 263 return expiry.Before(deadline.Add(rnd)) 264 } 265 266 // EqualTokens returns true if tokens are equal. 267 // 268 // 'nil' token corresponds to an empty access token. 269 func EqualTokens(a, b *Token) bool { 270 if a == b { 271 return true 272 } 273 if a == nil { 274 a = &Token{} 275 } 276 if b == nil { 277 b = &Token{} 278 } 279 return a.AccessToken == b.AccessToken && 280 a.Expiry.Equal(b.Expiry) && 281 a.IDToken == b.IDToken && 282 a.Email == b.Email 283 } 284 285 // isBadTokenError sniffs out HTTP 400/401 from token source errors. 286 func isBadTokenError(err error) bool { 287 if rerr, _ := err.(*oauth2.RetrieveError); rerr != nil { 288 return rerr.Response.StatusCode == 400 || rerr.Response.StatusCode == 401 289 } 290 return false 291 } 292 293 // isBadKeyError sniffs out errors related to malformed private keys. 294 func isBadKeyError(err error) bool { 295 if err == nil { 296 return false 297 } 298 // See https://go.googlesource.com/oauth2.git/+/197281d4/internal/oauth2.go#32 299 // Unfortunately, if uses fmt.Errorf. 300 s := err.Error() 301 return strings.Contains(s, "private key should be a PEM") || 302 s == "private key is invalid" 303 } 304 305 // grabToken uses token source to create a new *oauth2.Token. 306 // 307 // It recognizes transient errors. 308 func grabToken(src oauth2.TokenSource) (*oauth2.Token, error) { 309 switch tok, err := src.Token(); { 310 case isBadTokenError(err): 311 return nil, err 312 case isBadKeyError(err): 313 return nil, err 314 case err != nil: 315 // More often than not errors here are transient (network connectivity 316 // errors, HTTP 500 responses, etc). Retrying a fatal error a bunch of times 317 // is not very bad, so pick safer approach and assume any error is 318 // transient. Revoked refresh token or bad credentials (most common source 319 // of fatal errors) is already handled above. 320 return nil, transient.Tag.Apply(err) 321 default: 322 return tok, nil 323 } 324 }