or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

auth-handlers.mdclient-credentials.mdcore-oauth2.mdendpoints.mdgoogle-auth.mdgoogle-downscope.mdgoogle-external-account.mdindex.mdjira-oauth.mdjwt-jws.md
tile.json

jwt-jws.mddocs/

JWT and JWS Flows

JSON Web Token (JWT) and JSON Web Signature (JWS) support for 2-legged OAuth2 flows with service account authentication.

Packages

import "golang.org/x/oauth2/jwt"
import "golang.org/x/oauth2/jws"

JWT Package

The JWT package implements the OAuth 2.0 JSON Web Token flow (RFC 7523), commonly known as "two-legged OAuth 2.0". This is typically used for service account authentication.

See: https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12

JWT Configuration

type Config struct {
	// Email is the OAuth client identifier
	Email string

	// PrivateKey contains RSA private key or PEM file contents
	// PEM containers with a passphrase are not supported
	PrivateKey []byte

	// PrivateKeyID is an optional hint indicating which key is being used
	PrivateKeyID string

	// Subject is the optional user to impersonate
	Subject string

	// Scopes optionally specifies requested permission scopes
	Scopes []string

	// TokenURL is the endpoint required to complete the 2-legged JWT flow
	TokenURL string

	// Expires optionally specifies how long the token is valid for
	Expires time.Duration

	// Audience optionally specifies the intended audience
	// If empty, TokenURL is used as the intended audience
	Audience string

	// PrivateClaims optionally specifies custom private claims in the JWT
	PrivateClaims map[string]any

	// UseIDToken specifies whether to use ID token instead of access token
	// when the server returns both
	UseIDToken bool
}

Creating JWT Clients

func (c *Config) Client(ctx context.Context) *http.Client

Returns an HTTP client that automatically handles JWT token generation and includes authorization headers. The returned client and its Transport should not be modified.

JWT Token Source

func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource

Returns a JWT TokenSource using the configuration and HTTP client from the context. Most users should use Client instead.

Example: Service Account Authentication

import (
	"context"
	"io/ioutil"
	"log"

	"golang.org/x/oauth2/jwt"
)

func main() {
	// Read private key from PEM file
	privateKey, err := ioutil.ReadFile("service-account-key.pem")
	if err != nil {
		log.Fatal(err)
	}

	// Configure JWT
	conf := &jwt.Config{
		Email:      "service@example.iam.gserviceaccount.com",
		PrivateKey: privateKey,
		TokenURL:   "https://oauth2.googleapis.com/token",
		Scopes: []string{
			"https://www.googleapis.com/auth/cloud-platform",
		},
	}

	// Create authenticated client
	client := conf.Client(context.Background())

	// Use client for API requests
	resp, err := client.Get("https://www.googleapis.com/storage/v1/b")
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	// ...
}

Example: User Impersonation

conf := &jwt.Config{
	Email:      "service@example.iam.gserviceaccount.com",
	PrivateKey: privateKey,
	Subject:    "user@example.com", // Impersonate this user
	TokenURL:   "https://oauth2.googleapis.com/token",
	Scopes:     []string{"https://www.googleapis.com/auth/drive"},
}

client := conf.Client(ctx)

Example: Custom Claims

conf := &jwt.Config{
	Email:      "service@example.iam.gserviceaccount.com",
	PrivateKey: privateKey,
	TokenURL:   "https://oauth2.googleapis.com/token",
	Scopes:     []string{"custom_scope"},
	PrivateClaims: map[string]any{
		"tenant_id":     "tenant123",
		"custom_field":  "value",
	},
}

client := conf.Client(ctx)

Example: ID Token Usage

// Use ID token instead of access token
conf := &jwt.Config{
	Email:      "service@example.iam.gserviceaccount.com",
	PrivateKey: privateKey,
	TokenURL:   "https://oauth2.googleapis.com/token",
	UseIDToken: true,
}

client := conf.Client(ctx)

Converting PKCS12 to PEM

The JWT package requires RSA private keys in PEM format. To convert a PKCS12 file:

openssl pkcs12 -in key.p12 -out key.pem -nodes

JWS Package

The JWS package provides JSON Web Signature encoding and decoding (RFC 7515). It primarily exists to support the OAuth2 JWT flow.

Note: This package is deprecated and not intended for public use. It exists for internal OAuth2 support. For production JWS needs, use a dedicated JWS library or copy this package into your own source tree.

JWS Claim Set

type ClaimSet struct {
	// Iss is the email address of the client_id making the request
	Iss string `json:"iss"`

	// Scope is the space-delimited list of requested permissions
	Scope string `json:"scope,omitempty"`

	// Aud is the intended target descriptor (optional)
	Aud string `json:"aud"`

	// Exp is the expiration time (seconds since Unix epoch)
	Exp int64 `json:"exp"`

	// Iat is the time the assertion was issued (seconds since Unix epoch)
	Iat int64 `json:"iat"`

	// Typ is the token type (optional)
	Typ string `json:"typ,omitempty"`

	// Sub is the email for which delegated access is requested (optional)
	Sub string `json:"sub,omitempty"`

	// Prn is the old name of Sub for legacy OAuth 2.0 providers (optional)
	Prn string `json:"prn,omitempty"`

	// PrivateClaims are custom private claims
	PrivateClaims map[string]any `json:"-"`
}

JWS Header

type Header struct {
	// Algorithm is the algorithm used for signature
	Algorithm string `json:"alg"`

	// Typ represents the token type
	Typ string `json:"typ"`

	// KeyID is an optional hint of which key is being used
	KeyID string `json:"kid,omitempty"`
}

JWS Encoding

func Encode(header *Header, c *ClaimSet, key *rsa.PrivateKey) (string, error)

Encodes a signed JWS with the provided header and claim set using RSA PKCS1v15 signing with SHA256.

type Signer func(data []byte) (sig []byte, err error)

func EncodeWithSigner(header *Header, c *ClaimSet, sg Signer) (string, error)

Encodes a header and claim set with a custom signer function for alternative signing methods.

JWS Decoding

func Decode(payload string) (*ClaimSet, error)

Decodes a claim set from a JWS payload string.

JWS Verification

func Verify(token string, key *rsa.PublicKey) error

Verifies that a JWT token's signature was produced by the private key associated with the supplied public key.

Example: Manual JWS Encoding

import (
	"crypto/rsa"
	"time"

	"golang.org/x/oauth2/jws"
)

func createJWT(privateKey *rsa.PrivateKey) (string, error) {
	// Create header
	header := &jws.Header{
		Algorithm: "RS256",
		Typ:       "JWT",
		KeyID:     "key-id-123",
	}

	// Create claim set
	now := time.Now()
	claims := &jws.ClaimSet{
		Iss:   "service@example.com",
		Scope: "read write",
		Aud:   "https://oauth2.googleapis.com/token",
		Exp:   now.Add(time.Hour).Unix(),
		Iat:   now.Unix(),
		Sub:   "user@example.com",
		PrivateClaims: map[string]any{
			"custom": "value",
		},
	}

	// Encode and sign
	token, err := jws.Encode(header, claims, privateKey)
	if err != nil {
		return "", err
	}

	return token, nil
}

Example: Custom Signer

import (
	"crypto/ecdsa"
	"crypto/rand"
	"crypto/sha256"
)

func createJWTWithECDSA(privateKey *ecdsa.PrivateKey) (string, error) {
	header := &jws.Header{
		Algorithm: "ES256",
		Typ:       "JWT",
	}

	claims := &jws.ClaimSet{
		Iss: "service@example.com",
		Aud: "https://oauth2.example.com/token",
	}

	// Custom ECDSA signer
	signer := func(data []byte) ([]byte, error) {
		hash := sha256.Sum256(data)
		return ecdsa.SignASN1(rand.Reader, privateKey, hash[:])
	}

	return jws.EncodeWithSigner(header, claims, signer)
}

Example: JWT Verification

func verifyJWT(token string, publicKey *rsa.PublicKey) error {
	// Verify signature
	if err := jws.Verify(token, publicKey); err != nil {
		return fmt.Errorf("invalid signature: %w", err)
	}

	// Decode and check claims
	claims, err := jws.Decode(token)
	if err != nil {
		return fmt.Errorf("invalid token: %w", err)
	}

	// Check expiration
	if time.Now().Unix() > claims.Exp {
		return fmt.Errorf("token expired")
	}

	// Check issuer
	if claims.Iss != "expected-issuer@example.com" {
		return fmt.Errorf("unexpected issuer: %s", claims.Iss)
	}

	return nil
}

Security Considerations

  1. Private Key Protection: Store private keys securely, never in source code or version control
  2. Key Rotation: Implement regular key rotation policies
  3. Scope Limitation: Request only necessary scopes
  4. Token Expiration: Use reasonable expiration times (typically 1 hour or less)
  5. Audience Validation: Always validate the audience claim when verifying JWTs
  6. HTTPS Only: Always use HTTPS for token endpoint communication

Common Use Cases

Service Account Authentication

JWT flows are commonly used for:

  • Google Cloud Platform service accounts
  • Firebase service accounts
  • Server-to-server API authentication
  • Batch processing and automation
  • CI/CD pipeline authentication

Domain-Wide Delegation

For Google Workspace domain-wide delegation:

conf := &jwt.Config{
	Email:      "service@project.iam.gserviceaccount.com",
	PrivateKey: privateKey,
	Subject:    "admin@example.com", // Admin user to impersonate
	TokenURL:   "https://oauth2.googleapis.com/token",
	Scopes: []string{
		"https://www.googleapis.com/auth/admin.directory.user",
	},
}

Error Handling

token, err := conf.TokenSource(ctx).Token()
if err != nil {
	if rerr, ok := err.(*oauth2.RetrieveError); ok {
		log.Printf("OAuth error: %s - %s\n",
			rerr.ErrorCode, rerr.ErrorDescription)
	}
	return err
}

Differences from Client Credentials Flow

FeatureJWT FlowClient Credentials
AuthenticationRSA key signingClient ID/Secret
Use caseService accountsMachine-to-machine
Token requestSelf-signed JWTStandard OAuth2
Key managementRSA key pairsShared secrets
Google CloudPrimary methodAlternative method

Complete Example

package main

import (
	"context"
	"fmt"
	"io/ioutil"
	"log"

	"golang.org/x/oauth2/jwt"
)

func main() {
	ctx := context.Background()

	// Load service account key
	keyData, err := ioutil.ReadFile("service-account.pem")
	if err != nil {
		log.Fatal(err)
	}

	// Configure JWT
	conf := &jwt.Config{
		Email:      "my-service@project.iam.gserviceaccount.com",
		PrivateKey: keyData,
		PrivateKeyID: "key-123",
		TokenURL:   "https://oauth2.googleapis.com/token",
		Scopes: []string{
			"https://www.googleapis.com/auth/cloud-platform",
		},
		Expires: 3600, // 1 hour
	}

	// Create client
	client := conf.Client(ctx)

	// Make authenticated request
	resp, err := client.Get("https://cloudresourcemanager.googleapis.com/v1/projects")
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Response: %s\n", body)
}