go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth/internal/iam.go (about) 1 // Copyright 2017 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 16 17 import ( 18 "context" 19 "fmt" 20 "net/http" 21 "time" 22 23 "golang.org/x/oauth2" 24 "google.golang.org/api/googleapi" 25 26 "go.chromium.org/luci/common/errors" 27 "go.chromium.org/luci/common/gcloud/iam" 28 "go.chromium.org/luci/common/retry/transient" 29 ) 30 31 type iamTokenProvider struct { 32 actAs string 33 scopes []string 34 audience string // not empty iff using ID tokens 35 transport http.RoundTripper 36 cacheKey CacheKey 37 } 38 39 // NewIAMTokenProvider returns TokenProvider that uses generateAccessToken IAM 40 // API to grab tokens belonging to some service account. 41 func NewIAMTokenProvider(ctx context.Context, actAs string, scopes []string, audience string, transport http.RoundTripper) (TokenProvider, error) { 42 return &iamTokenProvider{ 43 actAs: actAs, 44 scopes: scopes, 45 audience: audience, 46 transport: transport, 47 cacheKey: CacheKey{ 48 Key: fmt.Sprintf("iam/%s", actAs), 49 Scopes: scopes, 50 }, 51 }, nil 52 } 53 54 func (p *iamTokenProvider) RequiresInteraction() bool { 55 return false 56 } 57 58 func (p *iamTokenProvider) Lightweight() bool { 59 return false 60 } 61 62 func (p *iamTokenProvider) Email() string { 63 return p.actAs 64 } 65 66 func (p *iamTokenProvider) CacheKey(ctx context.Context) (*CacheKey, error) { 67 return &p.cacheKey, nil 68 } 69 70 func (p *iamTokenProvider) MintToken(ctx context.Context, base *Token) (*Token, error) { 71 client := &iam.CredentialsClient{ 72 Client: &http.Client{ 73 Transport: &tokenInjectingTransport{ 74 transport: p.transport, 75 token: &base.Token, 76 }, 77 }, 78 } 79 80 var err error 81 82 if p.audience != "" { 83 var tok string 84 tok, err = client.GenerateIDToken(ctx, p.actAs, p.audience, true, nil) 85 if err == nil { 86 claims, err := ParseIDTokenClaims(tok) 87 if err != nil { 88 return nil, errors.Annotate(err, "IAM service returned bad ID token").Err() 89 } 90 return &Token{ 91 Token: oauth2.Token{ 92 TokenType: "Bearer", 93 AccessToken: NoAccessToken, 94 Expiry: time.Unix(claims.Exp, 0), 95 }, 96 IDToken: tok, 97 Email: p.Email(), 98 }, nil 99 } 100 } else { 101 var tok *oauth2.Token 102 tok, err = client.GenerateAccessToken(ctx, p.actAs, p.scopes, nil, 0) 103 if err == nil { 104 return &Token{ 105 Token: *tok, 106 IDToken: NoIDToken, 107 Email: p.Email(), 108 }, nil 109 } 110 } 111 112 // Any 4** HTTP response is a fatal error. Everything else is transient. 113 if apiErr, _ := err.(*googleapi.Error); apiErr != nil && apiErr.Code < 500 { 114 return nil, err 115 } 116 return nil, transient.Tag.Apply(err) 117 } 118 119 func (p *iamTokenProvider) RefreshToken(ctx context.Context, prev, base *Token) (*Token, error) { 120 // Service account tokens are self sufficient, there's no need for refresh 121 // token. Minting a token and "refreshing" it is a same thing. 122 return p.MintToken(ctx, base) 123 } 124 125 //////////////////////////////////////////////////////////////////////////////// 126 127 type tokenInjectingTransport struct { 128 transport http.RoundTripper 129 token *oauth2.Token 130 } 131 132 func (t *tokenInjectingTransport) RoundTrip(req *http.Request) (*http.Response, error) { 133 clone := *req 134 clone.Header = make(http.Header, len(req.Header)+1) 135 for k, v := range req.Header { 136 clone.Header[k] = v 137 } 138 t.token.SetAuthHeader(&clone) 139 return t.transport.RoundTrip(&clone) 140 }