or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

claims.mdindex.mdkey-parsing.mdrequest-extraction.mdsigning-methods.mdtoken-creation.mdtoken-parsing.md
tile.json

claims.mddocs/

Claims Types and Handling

This document covers JWT claims implementations and custom claims creation.

Claims Interface

type Claims interface {
    GetExpirationTime() (*NumericDate, error)
    GetIssuedAt() (*NumericDate, error)
    GetNotBefore() (*NumericDate, error)
    GetIssuer() (string, error)
    GetSubject() (string, error)
    GetAudience() (ClaimStrings, error)
}

The base interface that all claims implementations must satisfy. Provides access to the standard registered claims as defined in RFC 7519 section 4.1.

Claims Validator Interface

type ClaimsValidator interface {
    Claims
    Validate() error
}

Optional interface for custom claims that need additional validation beyond the standard registered claims validation.

Built-in Claims Types

RegisteredClaims

type RegisteredClaims struct {
    Issuer    string
    Subject   string
    Audience  ClaimStrings
    ExpiresAt *NumericDate
    NotBefore *NumericDate
    IssuedAt  *NumericDate
    ID        string
}

A structured claims type containing the standard registered claim names defined in RFC 7519 section 4.1:

  • Issuer - iss claim: identifies who issued the token
  • Subject - sub claim: identifies the subject of the token
  • Audience - aud claim: identifies the intended recipients
  • ExpiresAt - exp claim: expiration time
  • NotBefore - nbf claim: time before which token must not be accepted
  • IssuedAt - iat claim: time at which token was issued
  • ID - jti claim: unique identifier for the token

JSON Tags: All fields use standard JWT claim names (iss, sub, aud, exp, nbf, iat, jti) and are optional (omitempty).

RegisteredClaims Methods

func (c RegisteredClaims) GetExpirationTime() (*NumericDate, error)
func (c RegisteredClaims) GetIssuedAt() (*NumericDate, error)
func (c RegisteredClaims) GetNotBefore() (*NumericDate, error)
func (c RegisteredClaims) GetIssuer() (string, error)
func (c RegisteredClaims) GetSubject() (string, error)
func (c RegisteredClaims) GetAudience() (ClaimStrings, error)

Implements the Claims interface.

MapClaims

type MapClaims map[string]any

A flexible claims type using a map for JSON encoding/decoding. This is the default claims type when you don't specify custom claims. Allows for arbitrary claim names and values.

MapClaims Methods

func (m MapClaims) GetExpirationTime() (*NumericDate, error)
func (m MapClaims) GetIssuedAt() (*NumericDate, error)
func (m MapClaims) GetNotBefore() (*NumericDate, error)
func (m MapClaims) GetIssuer() (string, error)
func (m MapClaims) GetSubject() (string, error)
func (m MapClaims) GetAudience() (ClaimStrings, error)

Implements the Claims interface by extracting standard claims from the map.

Supporting Types

NumericDate

type NumericDate struct {
    time.Time
}

Represents a JSON numeric date value as defined in RFC 7519 section 2. Embeds time.Time and provides JWT-specific JSON marshaling/unmarshaling.

NumericDate Constructor

func NewNumericDate(t time.Time) *NumericDate

Constructs a new NumericDate from a standard library time.Time. The timestamp is truncated according to the precision specified in the global TimePrecision variable.

NumericDate Methods

func (date NumericDate) MarshalJSON() ([]byte, error)

Serializes the NumericDate as a UNIX epoch timestamp, using the precision specified in TimePrecision.

func (date *NumericDate) UnmarshalJSON(b []byte) error

Deserializes a NumericDate from a JSON number representing a UNIX epoch (with integer or fractional seconds).

ClaimStrings

type ClaimStrings []string

A slice of strings that can be marshaled from either a single string or a string array. This type is necessary because the aud (audience) claim can be either a single string or an array of strings according to the JWT specification.

ClaimStrings Methods

func (s *ClaimStrings) UnmarshalJSON(data []byte) error

Unmarshals from either a single JSON string or a JSON array of strings.

func (s ClaimStrings) MarshalJSON() ([]byte, error)

Marshals to either a single string (if length is 1 and MarshalSingleStringAsArray is false) or an array of strings. The behavior is controlled by the global MarshalSingleStringAsArray variable.

Global Configuration

Time Precision

var TimePrecision time.Duration

Sets the precision of times and dates when serializing and comparing. Default is time.Second (no fractional timestamps for backward compatibility).

Audience Array Marshaling

var MarshalSingleStringAsArray bool

Controls how single-element ClaimStrings are serialized:

  • true (default): Always serialize as array ["value"]
  • false: Serialize as single string "value" if only one element

Usage Examples

Using RegisteredClaims

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

claims := jwt.RegisteredClaims{
    Issuer:    "my-service",
    Subject:   "user123",
    Audience:  jwt.ClaimStrings{"web-app", "mobile-app"},
    ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
    NotBefore: jwt.NewNumericDate(time.Now()),
    IssuedAt:  jwt.NewNumericDate(time.Now()),
    ID:        "unique-token-id",
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

Using MapClaims

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

claims := jwt.MapClaims{
    "iss":   "my-service",
    "sub":   "user456",
    "aud":   []string{"api", "web"},
    "exp":   time.Now().Add(1 * time.Hour).Unix(),
    "iat":   time.Now().Unix(),
    "email": "user@example.com",
    "role":  "admin",
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

Custom Claims with Embedded RegisteredClaims

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

type CustomClaims struct {
    Username    string   `json:"username"`
    Email       string   `json:"email"`
    Roles       []string `json:"roles"`
    Permissions []string `json:"permissions"`
    jwt.RegisteredClaims
}

claims := CustomClaims{
    Username:    "johndoe",
    Email:       "john@example.com",
    Roles:       []string{"admin", "user"},
    Permissions: []string{"read", "write", "delete"},
    RegisteredClaims: jwt.RegisteredClaims{
        Issuer:    "auth-service",
        Subject:   "user-uuid-123",
        Audience:  jwt.ClaimStrings{"api"},
        ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)),
        IssuedAt:  jwt.NewNumericDate(time.Now()),
    },
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
secretKey := []byte("secret")
tokenString, err := token.SignedString(secretKey)

Custom Claims with Validation

import (
    "errors"
    "time"
    "github.com/golang-jwt/jwt/v5"
)

type CustomClaims struct {
    Username string `json:"username"`
    Role     string `json:"role"`
    jwt.RegisteredClaims
}

// Implement ClaimsValidator interface
func (c CustomClaims) Validate() error {
    // Custom validation logic
    if c.Username == "" {
        return errors.New("username claim is required")
    }
    if c.Role != "admin" && c.Role != "user" && c.Role != "guest" {
        return errors.New("invalid role claim")
    }
    return nil
}

// The Validate method will be called automatically during token parsing
claims := &CustomClaims{}
token, err := jwt.ParseWithClaims(tokenString, claims, keyFunc)
// Custom validation is automatically executed

Parsing Claims from Token

import (
    "fmt"
    "github.com/golang-jwt/jwt/v5"
)

// Parse with MapClaims
token, err := jwt.Parse(tokenString, keyFunc)
if err != nil {
    panic(err)
}

if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
    fmt.Println("Subject:", claims["sub"])
    fmt.Println("Email:", claims["email"])
}

// Parse with RegisteredClaims
claims := &jwt.RegisteredClaims{}
token, err := jwt.ParseWithClaims(tokenString, claims, keyFunc)
if err != nil {
    panic(err)
}

if token.Valid {
    fmt.Println("Issuer:", claims.Issuer)
    fmt.Println("Subject:", claims.Subject)
    fmt.Println("Expires:", claims.ExpiresAt.Time)
}

Working with NumericDate

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

// Create NumericDate from time.Time
now := time.Now()
expirationTime := now.Add(24 * time.Hour)
numericDate := jwt.NewNumericDate(expirationTime)

// Use in claims
claims := jwt.RegisteredClaims{
    ExpiresAt: numericDate,
    IssuedAt:  jwt.NewNumericDate(now),
}

// Access the underlying time.Time
underlyingTime := numericDate.Time
fmt.Println("Expires at:", underlyingTime)

Configuring Time Precision

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

// Set time precision to milliseconds (instead of default seconds)
jwt.TimePrecision = time.Millisecond

// Now NumericDates will include millisecond precision
claims := jwt.RegisteredClaims{
    IssuedAt: jwt.NewNumericDate(time.Now()),
}

// Token will have fractional timestamp like 1234567890.123

Handling Audience Claims

import (
    "github.com/golang-jwt/jwt/v5"
)

// Single audience
claims1 := jwt.RegisteredClaims{
    Audience: jwt.ClaimStrings{"web-app"},
}

// Multiple audiences
claims2 := jwt.RegisteredClaims{
    Audience: jwt.ClaimStrings{"web-app", "mobile-app", "api"},
}

// Control single audience serialization
jwt.MarshalSingleStringAsArray = false  // Serialize as "web-app"
jwt.MarshalSingleStringAsArray = true   // Serialize as ["web-app"]

Custom Claims Without Embedding

import (
    "github.com/golang-jwt/jwt/v5"
)

type MyCustomClaims struct {
    Username string             `json:"username"`
    Email    string             `json:"email"`
    Issuer   string             `json:"iss"`
    Subject  string             `json:"sub"`
    Audience jwt.ClaimStrings   `json:"aud,omitempty"`
    Expires  *jwt.NumericDate   `json:"exp,omitempty"`
    NotBefore *jwt.NumericDate  `json:"nbf,omitempty"`
    IssuedAt *jwt.NumericDate   `json:"iat,omitempty"`
    ID       string             `json:"jti,omitempty"`
}

// Implement Claims interface
func (c MyCustomClaims) GetExpirationTime() (*jwt.NumericDate, error) {
    return c.Expires, nil
}

func (c MyCustomClaims) GetIssuedAt() (*jwt.NumericDate, error) {
    return c.IssuedAt, nil
}

func (c MyCustomClaims) GetNotBefore() (*jwt.NumericDate, error) {
    return c.NotBefore, nil
}

func (c MyCustomClaims) GetIssuer() (string, error) {
    return c.Issuer, nil
}

func (c MyCustomClaims) GetSubject() (string, error) {
    return c.Subject, nil
}

func (c MyCustomClaims) GetAudience() (jwt.ClaimStrings, error) {
    return c.Audience, nil
}

Best Practices

Claim Selection

  • Use RegisteredClaims for type safety and standard JWT claims
  • Use MapClaims for flexibility when claim structure varies
  • Use custom claims with embedded RegisteredClaims for application-specific data

Required Claims

Always include these standard claims for security:

  • exp (ExpiresAt): Limit token lifetime
  • iat (IssuedAt): Track when token was issued
  • iss (Issuer): Identify token source
  • aud (Audience): Identify intended recipient

Expiration Times

  • Use short expiration times for sensitive operations (minutes to hours)
  • Use longer expiration times for refresh tokens (days to weeks)
  • Always validate expiration during parsing

Custom Validation

Implement ClaimsValidator for business logic validation:

func (c CustomClaims) Validate() error {
    // Validate custom fields
    if c.Username == "" {
        return errors.New("username required")
    }
    // Additional validation...
    return nil
}

Memory Allocation

When using custom claims with embedded pointers, allocate memory before parsing:

claims := &CustomClaims{
    RegisteredClaims: jwt.RegisteredClaims{},
}
token, err := jwt.ParseWithClaims(tokenString, claims, keyFunc)

Common Patterns

Private Claims

Add application-specific claims alongside standard claims:

type AppClaims struct {
    TenantID     string `json:"tenant_id"`
    Subscription string `json:"subscription"`
    jwt.RegisteredClaims
}

Nested Claims

Use nested structures for complex claim data:

type UserInfo struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

type ClaimsWithUser struct {
    User UserInfo `json:"user"`
    jwt.RegisteredClaims
}

Claim Extraction Helpers

Create helper functions for safe claim extraction:

func GetStringClaim(claims jwt.MapClaims, key string) (string, bool) {
    val, ok := claims[key].(string)
    return val, ok
}

func GetSliceClaim(claims jwt.MapClaims, key string) ([]string, bool) {
    val, ok := claims[key].([]interface{})
    if !ok {
        return nil, false
    }
    result := make([]string, len(val))
    for i, v := range val {
        result[i], ok = v.(string)
        if !ok {
            return nil, false
        }
    }
    return result, true
}