blob: ba26b685871b2790c6102694507e180eed82c67d [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package internal
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io/ioutil"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/jwt"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/retry/transient"
)
type serviceAccountTokenProvider struct {
ctx context.Context // only for logging
jsonKey []byte
path string
scopes []string
}
// NewServiceAccountTokenProvider returns TokenProvider that uses service
// account private key (on disk or in memory) to make access tokens.
func NewServiceAccountTokenProvider(ctx context.Context, jsonKey []byte, path string, scopes []string) (TokenProvider, error) {
return &serviceAccountTokenProvider{
ctx: ctx,
jsonKey: jsonKey,
path: path,
scopes: scopes,
}, nil
}
func (p *serviceAccountTokenProvider) jwtConfig(ctx context.Context) (*jwt.Config, error) {
jsonKey := p.jsonKey
if p.path != "" {
var err error
logging.Debugf(ctx, "Reading private key from %s", p.path)
jsonKey, err = ioutil.ReadFile(p.path)
if err != nil {
return nil, err
}
}
return google.JWTConfigFromJSON(jsonKey, p.scopes...)
}
func (p *serviceAccountTokenProvider) RequiresInteraction() bool {
return false
}
func (p *serviceAccountTokenProvider) Lightweight() bool {
return false
}
func (p *serviceAccountTokenProvider) Email() string {
switch cfg, err := p.jwtConfig(p.ctx); {
case err != nil:
// Return UnknownEmail since we couldn't load it. This will trigger a code
// path that attempts to refresh the token, where this error will be hit
// again and properly reported.
return UnknownEmail
case cfg.Email == "":
// Service account JSON file doesn't have 'email' field. Assume the email
// is not available in that case. Strictly speaking we may try to generate
// an OAuth token and then ask token info endpoint for an email, but this is
// too much work. We require 'email' field to be present instead.
return NoEmail
default:
return cfg.Email
}
}
func (p *serviceAccountTokenProvider) CacheKey(ctx context.Context) (*CacheKey, error) {
cfg, err := p.jwtConfig(ctx)
if err != nil {
logging.Errorf(ctx, "Failed to load private key JSON - %s", err)
return nil, ErrBadCredentials
}
// PrivateKeyID is optional part of the private key JSON. If not given, use
// a digest of the private key itself. This ID is used strictly locally, it
// doesn't matter how we get it as long as it is repeatable between process
// invocations.
pkeyID := cfg.PrivateKeyID
if pkeyID == "" {
h := sha256.New()
h.Write(cfg.PrivateKey)
pkeyID = "custom:" + hex.EncodeToString(h.Sum(nil))
}
return &CacheKey{
Key: fmt.Sprintf("service_account/%s/%s", cfg.Email, pkeyID),
Scopes: p.scopes,
}, nil
}
func (p *serviceAccountTokenProvider) MintToken(ctx context.Context, base *Token) (*Token, error) {
cfg, err := p.jwtConfig(ctx)
if err != nil {
logging.Errorf(ctx, "Failed to load private key JSON - %s", err)
return nil, ErrBadCredentials
}
switch newTok, err := grabToken(cfg.TokenSource(ctx)); {
case err == nil:
email := cfg.Email
if email == "" {
email = NoEmail
}
return &Token{
Token: *newTok,
Email: email,
}, nil
case transient.Tag.In(err):
logging.Warningf(ctx, "Error when creating access token - %s", err)
return nil, err
default:
logging.Warningf(ctx, "Invalid or revoked service account key - %s", err)
return nil, ErrBadCredentials
}
}
func (p *serviceAccountTokenProvider) RefreshToken(ctx context.Context, prev, base *Token) (*Token, error) {
// JWT tokens are self sufficient, there's no need for refresh_token. Minting
// a token and "refreshing" it is a same thing.
return p.MintToken(ctx, base)
}