gcpjwt

package module
v2.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 3, 2021 License: MIT Imports: 24 Imported by: 1

README

gcp-jwt-go GoDoc Go Report Card Build Status Coverage Status

Google Cloud Platform (Cloud KMS, IAM API, & AppEngine App Identity API) jwt-go implementations

New with v2:

Google Cloud KMS now supports signatures and support has been added to gcp-jwt-go!

Breaking Changes with v2.2

  • Switched to new iamcredentials API - this no longer allows signBlob to be used on the service account the client is authenticated as.
  • IAMConfig.OAuth2HTTPClient is deprecrated and unused - Use IAMConfig.IAMClient instead.
  • IAMConfig.ProjectID is deprecrated and unused. The API will infer the project from the service account name.

Breaking Changes with v2.1

  • Dropping support for AppEngine Go 1.9 environment (last version with AppEngine App Identity support will be for Go 1.11)
  • KMSConfig no longer takes an optional HTTP Client, but rather the kms gRPC based client
  • Middleware will now return a 401 response for unauthenticated requests (previously was returning a 403 response)

Breaking Changes with v2

  • Package name changed from gcp_jwt to gcpjwt
  • Refactoring of code (including exported functions/structs)
  • Certificate caching is now opt-in vs opt-out
  • Helper jwt.Keyfunc implementations introduced
  • Expected key for sign/verify changed
  • KeyID() helper functions
  • Basic oauth2.TokenSource and http middleware sub packages for basic service-to-service authentication (currently only supports IAM Api)

To continue using the older version, please import as follows: import "gopkg.in/someone1/gcp-jwt-go.v1"

Features

gcp-jwt-go has basic implementations of using Google Cloud KMS, Google IAM API (both signJwt and signBlob), and the App Identity API from AppEngine Standard on Google Cloud Platform to sign JWT tokens using the dgrijalva/jwt-go package. Should work across virtually all environments, on or off of Google's Cloud Platform.

Getting Started

Please read the documentation at https://godoc.org/github.com/someone1/gcp-jwt-go

Performance

There are many tradeoffs which the various signing mechanism available from Google's Cloud Platform. Below you will find a chart of performance for the different algorithms and APIs. Here are some overall takeaways:

  • AppEngine: The fastest option available though limited in that it can only sign on behalf of the default service account and only runs on AppEngine Standard. Keys are auto-rotated, limited to RS256.
  • IAM Api: Flexible in that you can sign on behalf of various service accounts (see Tips below), runs on any platform, keys are auto-rotated, but is the slowest option available and limited to RS256.
  • Cloud KMS: Fast (enough?), highly flexible (come up with your own keys/usage/algorithm/etc.), runs on any platform, however key rotation is left to the user.

note: all latency numbers are ordered as (50th %ile, 95th %ile, 99th %ile). Tests were run on a F1 AppEngine Standard instance in the us-central region. All Cloud KMS keys are set to global.

Signing Performance
Signer Signature Length Sign Latency Samples
AppEngine 342 9.14 ms, 17.56 ms, 79.15 ms 100
IAMBlob 342 198.37 ms, 217.42 ms, 244.91 ms 100
IAMJWT 342 109.03 ms, 208.46 ms, 212.65 ms 100
KMSES256 86 31.57 ms, 44.09 ms, 44.54 ms 50
KMSES384 128 34.67 ms, 51.16 ms, 59.48 ms 50
KMSPS256 (2048) 342 38.20 ms, 57.75 ms, 70.47 ms 50
KMSPS256 (3072) 512 42.77 ms, 58.24 ms, 62.86 ms 50
KMSPS256 (4096) 683 52.02 ms, 64.70 ms, 92.15 ms 50
KMSRS256 (2048) 342 37.94 ms, 61.94 ms, 77.33 ms 50
KMSRS256 (3072) 512 39.85 ms, 50.52 ms, 56.17 ms 50
KMSRS256 (4096) 683 50.19 ms, 68.48 ms, 86.02 ms 50
Verify Performance
Verifier Cache Verify Latency Samples
AppEngineVerify false 6.42 ms, 9.33 ms, 10.86 ms 50
AppEngineVerify true 0.87 ms, 1.05 ms, 25.03 ms 50
IAMVerify false 12.52 ms, 21.45 ms, 30.63 ms 100
IAMVerify true 0.86 ms, 1.01 ms, 53.19 ms 100
KMSVerify (2048-PS256) always 0.88 ms, 1.01 ms, 32.15 ms 50
KMSVerify (2048-RS256) always 0.93 ms, 1.11 ms, 19.96 ms 50
KMSVerify (3072-PS256) always 1.53 ms, 1.71 ms, 43.35 ms 50
KMSVerify (3072-RS256) always 1.61 ms, 2.11 ms, 42.39 ms 50
KMSVerify (4096-PS256) always 2.94 ms, 66.88 ms, 71.60 ms 50
KMSVerify (4096-RS256) always 2.70 ms, 55.25 ms, 72.34 ms 50
KMSVerify (ES256) always 0.15 ms, 0.20 ms, 0.29 ms 50
KMSVerify (ES384) always 181.21 ms, 193.25 ms, 195.08 ms 50

Where cache=false is where we get the most value from these numbers as it shows the time to fetch/parse public certificates, the other cases are just the time to use a cached certificate to validate the JWT.

Tips

  • If using the IAM API - create a separate service account to sign on behalf of for your projects unless you NEED to use your default service account (e.g. the AppEngine service account). This way you can limit the scope of access for any leaked credentials. You'll have to grant the roles/iam.serviceAccountTokenCreator role to any user/group/serviceaccount you want to be able to sign on behalf of the new service account (resource: projects/-/serviceAccounts/<serviceaccount>). For example, create an api-signer service account, do NOT furnish any keys for it, grant your AppEngine/GCE/etc. default service account the proper role for that serviceAccount, and use the api-signer@... service account address in your configuration. For example, to setup to use an AppEngine service account to sign on behalf of a service account api-signer (be sure to export PROJECT_ID=your-project-id before executing the below):
# First, create the api-signer service account
gcloud beta iam service-accounts create api-signer --description="Tokens must be signed by this service account in order to authenticate to the API" --display-name="API Signer" --project=$PROJECT_ID

# Grant the AppEngine service account proper permissions to sign tokens on behalf of the service account we just created
gcloud beta iam service-accounts add-iam-policy-binding  api-signer@$PROJECT_ID.iam.gserviceaccount.com --member=serviceAccount:$PROJECT_ID@appspot.gserviceaccount.com --role=roles/iam.serviceAccountTokenCreator --project=$PROJECT_ID

Understand this process by reading this article.

  • If using outside of GCP, be sure to put credentials for an account that can access the service account for signing tokens in a well known location:
    1. A JSON file whose path is specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable.
    2. A JSON file in a location known to the gcloud command-line tool. On Windows, this is %APPDATA%/gcloud/application_default_credentials.json. On other systems, $HOME/.config/gcloud/application_default_credentials.json.

Documentation

Overview

Package gcpjwt has Google Cloud Platform (Cloud KMS, IAM API, & AppEngine App Identity API) jwt-go implementations. Should work across virtually all environments, on or off of Google's Cloud Platform.

Getting Started

It is highly recommended that you override the default algorithm implementations that you want to leverage a GCP service for in dgrijalva/jwt-go. You otherwise will have to manually pick the verification method for your JWTs and they will place non-standard headers in the rendered JWT (with the exception of signJwt from the IAM API which overwrites the header with its own).

You should only need to override the algorithm(s) you plan to use. It is also incorrect to override overlapping, algorithms such as `gcpjwt.SigningMethodKMSRS256.Override()` and `gcpjwt.SigningMethodIAMJWT.Override()`

Example:

import (
	"github.com/someone1/gcp-jwt-go"
)

func init() {
	// Pick one or more of the following to override

	// Cloud KMS
	gcpjwt.SigningMethodKMSRS256.Override() // RS256
	gcpjwt.SigningMethodKMSPS256.Override() // PS256
	gcpjwt.SigningMethodKMSES256.Override() // ES256
	gcpjwt.SigningMethodKMSES384.Override() // ES384

	// IAM API - This implements RS256 exclusively
	gcpjwt.SigningMethodIAMJWT.Override() // For signJwt
	gcpjwt.SigningMethodIAMBlob.Override() // For signBlob

	// AppEngine - Standard runtime only, does not apply to Flexible runtime, implements RS256 exclusively
	// You can also use any of the above methods on AppEngine Standard
	gcpjwt.SigningMethodAppEngine.Override()
}

As long as a you override a default algorithm implementation as shown above, using the dgrijalva/jwt-go is mostly unchanged.

Create a Token

Token creation is more/less done the same way as in the dgrijalva/jwt-go package. The key that you need to provide is always going to be a context.Context, usuaully with a configuration object loaded in:

  • use gcpjwt.IAMConfig for the SigningMethodIAMJWT and SigningMethodIAMBlob signing methods
  • use an appengine.NewContext for the SigningMethodAppEngine signing method
  • use gcpjwt.KMSConfig for any of the KMS signing methods

Example:

import (
	"context"
	"net/http"

	"github.com/dgrijalva/jwt-go"
	"github.com/someone1/gcp-jwt-go"
	"google.golang.org/appengine" // only on AppEngine Standard when using the SigningMethodAppEngine signing method
)

func makeToken(ctx context.Context) (string, error) string {
	// Important - if on AppEngine standard, even if you aren't using the SigningMethodAppEngine signing method
	// you must pass around the appengine.NewContext context as it is required for the API calls all methods must
	// make.

	var key interface{}
	claims := &jwt.StandardClaims{
		ExpiresAt: 15000,
		Issuer:    "test",
	}
	token := jwt.NewWithClaims(gcpjwt.SigningMethodGCPJWT, claims)

	// Prepare your signing key

	// For SigningMethodIAMJWT or SigningMethodIAMBlob
	config := &gcpjwt.IAMConfig{
		ServiceAccount: "app-id@appspot.gserviceaccount.com",
		IAMType:        gcpjwt.IAMJwtType, // or gcpjwt.IAMBlobType
	}
	key = gcpjwt.NewIAMContext(ctx, config)

	// For any KMS signing method
	config := &gcpjwt.KMSConfig{
		KeyPath: "name=projects/<project-id>/locations/<location>/keyRings/<key-ring-name>/cryptoKeys/<key-name>/cryptoKeyVersions/<key-version>",
	}
	key = gcpjwt.NewKMSContext(ctx, config)

	// For SigningMethodAppEngine
	key = ctx

	// For all signing methods EXCEPT signJWT
	return token.SignedString(key)

	// For signJwt
	// !!IMPORTANT!! Due to the way the signJwt API returns tokens, we can't use the standard signing process
	// to sign
	signingString, err := token.SigningString()
	if err != nil {
		return "", err
	}

	return token.Method.Sign(signingString, key)
}

Validate a Token

Finally, the steps to validate a token should be straight forward. This library provides you with helper jwt.Keyfunc implementations to do the heavy lifting around getting the public certificates for verification:

  • gcpjwt.IAMVerfiyKeyfunc can be used for the IAM API and the AppEngine Standard signing methods
  • gcpjwt.AppEngineVerfiyKeyfunc is only available on AppEngine standard and can only be used on JWT signed from the same default service account as the running application
  • gcp.KMSVerfiyKeyfunc can be used for the Cloud KMS signing methods

Example:

import (
	"context"
	"time"
	"strings"

	"github.com/dgrijalva/jwt-go"
	"github.com/someone1/gcp-jwt-go"
)

func validateToken(ctx context.Context, tokenString string) (*jwt.Token, error) {
	// Important - if on AppEngine standard, even if you aren't using the SigningMethodAppEngine signing method
	// you must pass around the appengine.NewContext context as it is required for the API calls all methods must
	// make.

	var keyFunc jwt.Keyfunc

	// Prepare your key function

	// For SigningMethodIAMJWT or SigningMethodIAMBlob or SigningMethodAppEngine
	config := &gcpjwt.IAMConfig{
		ServiceAccount: "app-id@appspot.gserviceaccount.com",
		IAMType:        gcpjwt.IAMJwtType, // or gcpjwt.IAMBlobType (use the Blob type if you used the SigningMethodAppEngine before)
		EnableCache:    true, // Enable the certificate cache so we don't fetch it on every verification - RECOMMENDED
	}
	keyFunc = gcpjwt.IAMVerfiyKeyfunc(ctx, config)

	// For any KMS signing method
	config := &gcpjwt.KMSConfig{
		KeyPath: "name=projects/<project-id>/locations/<location>/keyRings/<key-ring-name>/cryptoKeys/<key-name>/cryptoKeyVersions/<key-version>",
	}
	keyFunc = gcpjwt.KMSVerfiyKeyfunc(ctx, config)

	// For SigningMethodAppEngine only on AppEngine Standard
	keyFunc = gcpjwt.AppEngineVerfiyKeyfunc(ctx, true, time.Hour)

	// If you called an Override function as recommended above, for both signing and verifying a token, you can use
	// the regular verification steps - and the same goes if you did NOT call it for both signing and verifying (using non-standard 'alg' headers)
	// EXCEPT for the signJwt IAM API signing method which overwrites the header's alg to RS256
	return jwt.Parse(tokenString, keyFunc)

	// The following is an extreme and advanced use-case - it is NOT recommended but here for those who need it.
	//
	// If we need to manually override the detected jwt.SigningMethod based on the 'alg' header
	// This is basically copying the https://github.com/dgrijalva/jwt-go/blob/master/parser.go#L23 ParseWithClaims function here but forcing our own method vs getting one based on the Alg field
	// Or Try and parse, Ignore the result and try with the proper method:
	token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		return nil, nil
	})
	parts := strings.Split(token.Raw, ".")
	token.Method = gcpjwt.SigningMethodIAMJWT // or whichever method you want to force
	if err := token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, keyFunc); err != nil {
		return nil, err
	} else {
		token.Valid = true
	}
	return token, nil
}

Index

Constants

View Source
const (
	// IAMBlobType is used as a hint in the config to let various parts of the library know you intend to use the
	// signBlob IAM API.
	IAMBlobType iamType = iota + 1
	// IAMJwtType is used as a hint in the config to let various parts of the library know you intend to use the
	// signJwt IAM API.
	IAMJwtType
)

Variables

View Source
var (
	// ErrMissingConfig is returned when Sign or Verify did not find a configuration in the provided context
	ErrMissingConfig = errors.New("gcpjwt: missing configuration in provided context")
)

Functions

func AppEngineVerfiyKeyfunc

func AppEngineVerfiyKeyfunc(ctx context.Context, enableCache bool, cacheExpiration time.Duration) jwt.Keyfunc

AppEngineVerfiyKeyfunc is a helper meant that returns a jwt.Keyfunc. It will handle pulling and selecting the certificates to verify signatures with, caching when enabled.

func IAMVerfiyKeyfunc

func IAMVerfiyKeyfunc(ctx context.Context, config *IAMConfig) jwt.Keyfunc

IAMVerfiyKeyfunc is a helper meant that returns a jwt.Keyfunc. It will handle pulling and selecting the certificates to verify signatures with, caching when enabled.

func KMSVerfiyKeyfunc

func KMSVerfiyKeyfunc(ctx context.Context, config *KMSConfig) (jwt.Keyfunc, error)

KMSVerfiyKeyfunc is a helper meant that returns a jwt.Keyfunc. It will handle pulling and selecting the certificates to verify signatures with, caching the public key in memory. It is not valid to modify the KMSConfig provided after calling this function, you must call this again if changes to the config's KeyPath are made. Note that the public key is retrieved when creating the key func and returned for each call to the returned jwt.Keyfunc. https://cloud.google.com/kms/docs/retrieve-public-key#kms-howto-retrieve-public-key-go

func NewIAMContext

func NewIAMContext(parent context.Context, val *IAMConfig) context.Context

NewIAMContext returns a new context.Context that carries a provided IAMConfig value

func NewKMSContext

func NewKMSContext(parent context.Context, val *KMSConfig) context.Context

NewKMSContext returns a new context.Context that carries a provided KMSConfig value

Types

type IAMConfig

type IAMConfig struct {
	// ProjectID is the project id that contains the service account you want to sign with. Defaults to "-" to infer the project from the account
	// Depcrecated: This field is no longer used as the API will reject all values other than "-".
	ProjectID string

	// Service account can be the email address or the uniqueId of the service account used to sign the JWT with
	ServiceAccount string

	// EnableCache will enable the in-memory caching of public certificates.
	// The cache will expire certificates when an expiration is known or fallback to the configured CacheExpiration
	EnableCache bool

	// CacheExpiration is the default time to keep the certificates in cache if no expiration time is provided
	// Use a value of 0 to disable the expiration time fallback. Max reccomneded value is 24 hours.
	// https://cloud.google.com/iam/docs/understanding-service-accounts#managing_service_account_keys
	CacheExpiration time.Duration

	// IAMType is a helper used to help clarify which IAM signing method this config is meant for.
	// Used for the jwtmiddleware and oauth2 packages.
	IAMType iamType

	// IAMService is a user provided service client that should be used when communicating with the iamcredentials API,
	// otherwuse the default service will be used.
	IAMService *iamcredentials.Service

	// OAuth2HTTPClient is a user provided oauth2 authenticated *http.Client to use, google.DefaultClient used otherwise
	// Used for signing requests
	// Depcrecated: This field is no longer used. Use IAMClient instead
	OAuth2HTTPClient *http.Client

	// Client is a user provided *http.Client to use, http.DefaultClient is used otherwise (AppEngine URL Fetch Supported)
	// Used for verify requests
	Client *http.Client

	sync.RWMutex
	// contains filtered or unexported fields
}

IAMConfig is relevant for both the signBlob and signJWT IAM API use-cases

func IAMFromContext

func IAMFromContext(ctx context.Context) (*IAMConfig, bool)

IAMFromContext extracts a IAMConfig from a context.Context

func (*IAMConfig) KeyID

func (i *IAMConfig) KeyID() string

KeyID will return the last used KeyID to sign the JWT - though it should be noted the signJwt method will always add its own token header which is not parsed back to the token. Helper function for adding the kid header to your token.

type KMSConfig

type KMSConfig struct {
	// KeyPath is the name of the key to use in the format of:
	// "name=projects/*/locations/*/keyRings/*/cryptoKeys/*/cryptoKeyVersions/*"
	KeyPath string

	// KMSClient to use for calls to the API. If nil, a standard one will be initiated
	KMSClient *kms.KeyManagementClient
}

KMSConfig is used to sign/verify JWTs with Google Cloud KMS

func KMSFromContext

func KMSFromContext(ctx context.Context) (*KMSConfig, bool)

KMSFromContext extracts a KMSConfig from a context.Context

func (*KMSConfig) KeyID

func (k *KMSConfig) KeyID() string

KeyID will return the SHA1 hash of the configured KeyPath. Helper function for adding the kid header to your token.

type SigningMethodAppEngineImpl

type SigningMethodAppEngineImpl struct {
	*SigningMethodIAM
	sync.RWMutex
	// contains filtered or unexported fields
}

SigningMethodAppEngineImpl implements singing JWT's using the built-in AppEngine signing method. This method uses a private key unique to your AppEngine application and the key may rotate from time to time. https://cloud.google.com/appengine/docs/go/reference#SignBytes https://cloud.google.com/appengine/docs/go/appidentity/#Go_Asserting_identity_to_other_systems

var (
	SigningMethodAppEngine *SigningMethodAppEngineImpl
)

func (*SigningMethodAppEngineImpl) KeyID

KeyID will return the last used KeyID to sign the JWT. Helper function for adding the kid header to your token.

func (*SigningMethodAppEngineImpl) Sign

func (s *SigningMethodAppEngineImpl) Sign(signingString string, key interface{}) (string, error)

Sign implements the Sign method from jwt.SigningMethod. For this signing method, a valid AppEngine context.Context must be passed as the key.

type SigningMethodIAM

type SigningMethodIAM struct {
	// contains filtered or unexported fields
}

SigningMethodIAM is the base implementation for the signBlob and signJwt IAM API JWT signing methods. Not to be used on its own!

var (
	// SigningMethodIAMBlob implements signing JWTs with the IAM signBlob API.
	// https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signBlob
	SigningMethodIAMBlob *SigningMethodIAM
)
var (
	// SigningMethodIAMJWT implements signing JWTs with the IAM signJwt API.
	// https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signJwt
	SigningMethodIAMJWT *SigningMethodIAM
)

func (*SigningMethodIAM) Alg

func (s *SigningMethodIAM) Alg() string

Alg will return the JWT header algorithm identifier this method is configured for.

func (*SigningMethodIAM) Override

func (s *SigningMethodIAM) Override()

Override will override the default JWT implementation of the signing function this IAM API type implements.

func (*SigningMethodIAM) Sign

func (s *SigningMethodIAM) Sign(signingString string, key interface{}) (string, error)

Sign implements the Sign method from jwt.SigningMethod. For this signing method, a valid context.Context must be passed as the key containing a IAMConfig value. NOTE: The HEADER IS IGNORED for the signJWT API as the API will add its own

func (*SigningMethodIAM) Verify

func (s *SigningMethodIAM) Verify(signingString, signature string, key interface{}) error

Verify implements the Verify method from jwt.SigningMethod. This will expect key type of []*rsa.PublicKey. https://firebase.google.com/docs/auth/admin/verify-id-tokens

type SigningMethodKMS

type SigningMethodKMS struct {
	// contains filtered or unexported fields
}

SigningMethodKMS implements the jwt.SiginingMethod interface for Google's Cloud KMS service

var (
	// SigningMethodKMSRS256 leverages Cloud KMS for RS256 algorithms, use with:
	// RSA_SIGN_PKCS1_2048_SHA256
	// RSA_SIGN_PKCS1_3072_SHA256
	// RSA_SIGN_PKCS1_4096_SHA256
	SigningMethodKMSRS256 *SigningMethodKMS
	// SigningMethodKMSPS256 leverages Cloud KMS for PS256 algorithms, use with:
	// RSA_SIGN_PSS_2048_SHA256
	// RSA_SIGN_PSS_3072_SHA256
	// RSA_SIGN_PSS_4096_SHA256
	SigningMethodKMSPS256 *SigningMethodKMS
	// SigningMethodKMSES256 leverages Cloud KMS for the ES256 algorithm, use with:
	// EC_SIGN_P256_SHA256
	SigningMethodKMSES256 *SigningMethodKMS
	// SigningMethodKMSES384 leverages Cloud KMS for the ES256 algorithm, use with:
	// EC_SIGN_P384_SHA384
	SigningMethodKMSES384 *SigningMethodKMS
)

Support for the Google Cloud KMS Asymmetric Signing Algorithms: https://cloud.google.com/kms/docs/algorithms

func (*SigningMethodKMS) Alg

func (s *SigningMethodKMS) Alg() string

Alg will return the JWT header algorithm identifier this method is configured for.

func (*SigningMethodKMS) Hash

func (s *SigningMethodKMS) Hash() crypto.Hash

Hash will return the crypto.Hash used for this signing method

func (*SigningMethodKMS) Override

func (s *SigningMethodKMS) Override()

Override will override the default JWT implementation of the signing function this Cloud KMS type implements.

func (*SigningMethodKMS) Sign

func (s *SigningMethodKMS) Sign(signingString string, key interface{}) (string, error)

Sign implements the Sign method from jwt.SigningMethod. For this signing method, a valid context.Context must be passed as the key containing a KMSConfig value. https://cloud.google.com/kms/docs/create-validate-signatures#kms-howto-sign-go

func (*SigningMethodKMS) Verify

func (s *SigningMethodKMS) Verify(signingString, signature string, key interface{}) error

Verify does a pass-thru to the appropriate jwt.SigningMethod for this signing algorithm and expects the same key https://cloud.google.com/kms/docs/create-validate-signatures#validate_ec_signature https://cloud.google.com/kms/docs/create-validate-signatures#validate_rsa_signature

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL