go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth/internal/service.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 16 17 import ( 18 "context" 19 "crypto/sha256" 20 "encoding/hex" 21 "fmt" 22 "os" 23 24 "golang.org/x/oauth2/google" 25 "golang.org/x/oauth2/jwt" 26 27 "go.chromium.org/luci/common/logging" 28 "go.chromium.org/luci/common/retry/transient" 29 ) 30 31 type serviceAccountTokenProvider struct { 32 ctx context.Context // only for logging 33 jsonKey []byte 34 path string 35 scopes []string 36 audience string // not empty iff using ID tokens 37 } 38 39 // NewServiceAccountTokenProvider returns TokenProvider that uses service 40 // account private key (on disk or in memory) to make access tokens. 41 func NewServiceAccountTokenProvider(ctx context.Context, jsonKey []byte, path string, scopes []string, audience string) (TokenProvider, error) { 42 return &serviceAccountTokenProvider{ 43 ctx: ctx, 44 jsonKey: jsonKey, 45 path: path, 46 scopes: scopes, 47 audience: audience, 48 }, nil 49 } 50 51 func (p *serviceAccountTokenProvider) jwtConfig(ctx context.Context) (*jwt.Config, error) { 52 jsonKey := p.jsonKey 53 if p.path != "" { 54 var err error 55 logging.Debugf(ctx, "Reading private key from %s", p.path) 56 jsonKey, err = os.ReadFile(p.path) 57 if err != nil { 58 return nil, err 59 } 60 } 61 scopes := p.scopes 62 if p.audience != "" { 63 scopes = nil // can't specify both scopes and target audience 64 } 65 cfg, err := google.JWTConfigFromJSON(jsonKey, scopes...) 66 if err != nil { 67 return nil, err 68 } 69 if p.audience != "" { 70 cfg.UseIDToken = true 71 cfg.PrivateClaims = map[string]any{"target_audience": p.audience} 72 } 73 return cfg, nil 74 } 75 76 func (p *serviceAccountTokenProvider) RequiresInteraction() bool { 77 return false 78 } 79 80 func (p *serviceAccountTokenProvider) Lightweight() bool { 81 return false 82 } 83 84 func (p *serviceAccountTokenProvider) Email() string { 85 switch cfg, err := p.jwtConfig(p.ctx); { 86 case err != nil: 87 // Return UnknownEmail since we couldn't load it. This will trigger a code 88 // path that attempts to refresh the token, where this error will be hit 89 // again and properly reported. 90 return UnknownEmail 91 case cfg.Email == "": 92 // Service account JSON file doesn't have 'email' field. Assume the email 93 // is not available in that case. Strictly speaking we may try to generate 94 // an OAuth token and then ask token info endpoint for an email, but this is 95 // too much work. We require 'email' field to be present instead. 96 return NoEmail 97 default: 98 return cfg.Email 99 } 100 } 101 102 func (p *serviceAccountTokenProvider) CacheKey(ctx context.Context) (*CacheKey, error) { 103 cfg, err := p.jwtConfig(ctx) 104 if err != nil { 105 logging.Errorf(ctx, "Failed to load private key JSON - %s", err) 106 return nil, ErrBadCredentials 107 } 108 109 // PrivateKeyID is optional part of the private key JSON. If not given, use 110 // a digest of the private key itself. This ID is used strictly locally, it 111 // doesn't matter how we get it as long as it is repeatable between process 112 // invocations. 113 pkeyID := cfg.PrivateKeyID 114 if pkeyID == "" { 115 h := sha256.New() 116 h.Write(cfg.PrivateKey) 117 pkeyID = "custom:" + hex.EncodeToString(h.Sum(nil)) 118 } 119 120 return &CacheKey{ 121 Key: fmt.Sprintf("service_account/%s/%s", cfg.Email, pkeyID), 122 Scopes: p.scopes, 123 }, nil 124 } 125 126 func (p *serviceAccountTokenProvider) MintToken(ctx context.Context, base *Token) (*Token, error) { 127 cfg, err := p.jwtConfig(ctx) 128 if err != nil { 129 logging.Errorf(ctx, "Failed to load private key JSON - %s", err) 130 return nil, ErrBadCredentials 131 } 132 switch newTok, err := grabToken(cfg.TokenSource(ctx)); { 133 case err == nil: 134 email := cfg.Email 135 if email == "" { 136 email = NoEmail 137 } 138 ret := &Token{ 139 Token: *newTok, 140 IDToken: NoIDToken, 141 Email: email, 142 } 143 // When jwt.Config.UseIDToken is true, the produced oauth2.Token actually 144 // contains the ID token, not the access token. Perform a compensating 145 // switcheroo. 146 if cfg.UseIDToken { 147 ret.IDToken = ret.AccessToken 148 ret.AccessToken = NoAccessToken 149 } 150 return ret, nil 151 case transient.Tag.In(err): 152 logging.Warningf(ctx, "Error when creating token - %s", err) 153 return nil, err 154 default: 155 logging.Warningf(ctx, "Invalid or revoked service account key - %s", err) 156 return nil, ErrBadCredentials 157 } 158 } 159 160 func (p *serviceAccountTokenProvider) RefreshToken(ctx context.Context, prev, base *Token) (*Token, error) { 161 // JWT tokens are self sufficient, there's no need for refresh_token. Minting 162 // a token and "refreshing" it is a same thing. 163 return p.MintToken(ctx, base) 164 }