This document covers JWT claims implementations and custom claims creation.
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.
type ClaimsValidator interface {
Claims
Validate() error
}Optional interface for custom claims that need additional validation beyond the standard registered claims validation.
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 tokenSubject - sub claim: identifies the subject of the tokenAudience - aud claim: identifies the intended recipientsExpiresAt - exp claim: expiration timeNotBefore - nbf claim: time before which token must not be acceptedIssuedAt - iat claim: time at which token was issuedID - jti claim: unique identifier for the tokenJSON Tags: All fields use standard JWT claim names (iss, sub, aud, exp, nbf, iat, jti) and are optional (omitempty).
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.
type MapClaims map[string]anyA 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.
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.
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.
func NewNumericDate(t time.Time) *NumericDateConstructs a new NumericDate from a standard library time.Time. The timestamp is truncated according to the precision specified in the global TimePrecision variable.
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) errorDeserializes a NumericDate from a JSON number representing a UNIX epoch (with integer or fractional seconds).
type ClaimStrings []stringA 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.
func (s *ClaimStrings) UnmarshalJSON(data []byte) errorUnmarshals 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.
var TimePrecision time.DurationSets the precision of times and dates when serializing and comparing. Default is time.Second (no fractional timestamps for backward compatibility).
var MarshalSingleStringAsArray boolControls how single-element ClaimStrings are serialized:
true (default): Always serialize as array ["value"]false: Serialize as single string "value" if only one elementimport (
"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)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)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)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 executedimport (
"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)
}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)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.123import (
"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"]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
}Always include these standard claims for security:
exp (ExpiresAt): Limit token lifetimeiat (IssuedAt): Track when token was issuediss (Issuer): Identify token sourceaud (Audience): Identify intended recipientImplement 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
}When using custom claims with embedded pointers, allocate memory before parsing:
claims := &CustomClaims{
RegisteredClaims: jwt.RegisteredClaims{},
}
token, err := jwt.ParseWithClaims(tokenString, claims, keyFunc)Add application-specific claims alongside standard claims:
type AppClaims struct {
TenantID string `json:"tenant_id"`
Subscription string `json:"subscription"`
jwt.RegisteredClaims
}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
}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
}