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  }