client/jwt/jwt.go (61 lines of code) (raw):
// Copyright 2021 Google LLC
//
// 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 jwt contains utility functions for generating JWTs from a service account.
package jwt
import (
"context"
"fmt"
"net/url"
"os"
"cloud.google.com/go/compute/metadata"
"cloud.google.com/go/iam/credentials/apiv1"
"golang.org/x/oauth2/google"
iamcredspb "cloud.google.com/go/iam/credentials/apiv1/credentialspb"
)
const (
googleCredsEnvVar string = "GOOGLE_APPLICATION_CREDENTIALS"
instanceIdentityURL string = "instance/service-accounts/default/identity?audience=%v&format=full"
serviceAccountPrefix string = "projects/-/serviceAccounts/"
)
// instanceIdentityToken returns the instance identity token obtained by
// querying the metadata server on a GCE VM. See:
// https://cloud.google.com/compute/docs/instances/verifying-instance-identity
func instanceIdentityToken(audience string) (string, error) {
// The metadata package doesn't have a convenient getter method to grab
// the instance identity token, so format the URL manually and use .Get().
return metadata.Get(fmt.Sprintf(instanceIdentityURL, audience))
}
// GenerateTokenWithAudience generates a JWT with the FQDN of the given
// address as its audience.
func GenerateTokenWithAudience(ctx context.Context, address string) (string, error) {
u, err := url.Parse(address)
if err != nil {
return "", fmt.Errorf("could not parse EKM address: %v", err)
}
audience := fmt.Sprintf("%v://%v", u.Scheme, u.Hostname())
var authToken string
if authToken, err = GenerateJWT(ctx, audience); err != nil {
return "", fmt.Errorf("failed to generate JWT: %v", err)
}
return authToken, nil
}
// GenerateJWT returns a signed JWT derived from a Google service account.
// By default, it will generate it based on the service account key defined
// in the GOOGLE_APPLICATION_CREDENTIALS environment variable. If not, it
// will assume we are running in a GCE VM, and attempt to use the default
// service account credentials to generate the JWT instead.
func GenerateJWT(ctx context.Context, audience string) (string, error) {
// First, check to see if the GOOGLE_APPLICATION_CREDENTIALS environment
// variable has been set. If so, we can assume we are running on either
// an on-prem environment (ie. not in GCE), or alternatively, we *are*
// running in a GCE VM, but the user has chosen to override the default
// service account with explicit credentials from another account. In
// either case, we want to use this private key file to generate our JWT.
if saKeyFile := os.Getenv(googleCredsEnvVar); saKeyFile != "" {
// Read the service account file manually, as we need the email.
sa, err := os.ReadFile(saKeyFile)
if err != nil {
return "", fmt.Errorf("failed to read service account file: %v", err)
}
conf, err := google.JWTConfigFromJSON(sa)
if err != nil {
return "", fmt.Errorf("could not parse service account JSON: %v", err)
}
// Request an OIDC token from IAM. Creating a new IAM credentials client
// implicitly will look for the private key file specified in the
// GOOGLE_APPLICATION_CREDENTIALS env var, so we don't need to pass
// option.WithCredentials(saKeyFile) as an argument here.
c, err := credentials.NewIamCredentialsClient(ctx)
if err != nil {
return "", fmt.Errorf("could not create a new IAM credentials client: %v", err)
}
defer c.Close()
resp, err := c.GenerateIdToken(ctx, &iamcredspb.GenerateIdTokenRequest{
Name: serviceAccountPrefix + conf.Email,
Audience: audience,
IncludeEmail: true,
})
if err != nil {
return "", fmt.Errorf("error generating ID token: %v", err)
}
return resp.GetToken(), nil
}
// Otherwise, if we're not running in a GCE VM, we can't generate a signed
// JWT from a service account, so return an error.
if !metadata.OnGCE() {
return "", fmt.Errorf("could not find GOOGLE_APPLICATION_CREDENTIALS and not running on GCE")
}
return instanceIdentityToken(audience)
}