kataras/jwt

repository·main·Indexed 42 days ago

https://github.com/kataras/jwt

A zero-dependency, lightweight, and fast JWT (JSON Web Token) and JWKS (JSON Web Key Set) implementation for Go. It provides a predictable API for signing, verifying, and decoding tokens with support for HMAC, RSA, ECDSA, and EdDSA algorithms. Features include generic claim decoding, token blocklisting for server-side invalidation, and an encrypt-then-sign flow for securing payloads.

Tokens
86.2K
Snippets
252
Records
352
Agent score
51%

What's inside kataras/jwt

  1. Overview of the kataras/jwt package

    main
    The github.com/kataras/jwt package is a JWT (JSON Web Token) and JWKS (JSON Web Key Set) implementation for Go. It is designed to be a zero-dependency library. The package focuses on providing a predictable API that explicitly addresses common JWT concerns such as algorithm selection, key verification, claim integrity, and key rotation.
  2. Security against 'alg: none' attacks

    main

    The jwt package is designed to prevent the classic JWT attack where an attacker changes the header to {"alg":"none"} to bypass signature verification.

    This protection is achieved because the jwt.Verify function requires you to pass the expected algorithm as an argument. The package performs a byte-exact comparison between the algorithm you provided and the one in the token header. If they do not match exactly, it returns ErrTokenAlg before any signature check is even attempted.

    Key Security Rules:

    1. Always pin the algorithm: Never let the token header decide which algorithm to use. Always call jwt.Verify(jwt.HS256, ...) or similar.
    2. Avoid NONE for credentials: The Enrich function (which adds claims and signs) refuses to work with the NONE algorithm to prevent creating unsigned credentials.
    3. Explicit NONE usage: If you genuinely need an unsecured token (e.g., for client-side preferences), you must pass jwt.NONE explicitly to Sign and Verify.
  3. Handle the jwt.NONE algorithm safely

    main

    jwt.NONE is a valid, selectable algorithm that produces and accepts an empty signature. While Enrich and UnverifiedToken.Enrich explicitly refuse it, Sign and Verify do not prevent you from passing it.

    Never let request input choose the alg argument passed to Verify.

  4. Combine custom claims with standard claims

    main

    There are two primary ways to include RFC 7519 standard claims alongside your custom data:

    1. Struct Embedding: Embed jwt.Claims directly into your custom struct. This inlines the standard fields into a single flat JSON object.
    2. Sign Options: Pass standard claims as variadic SignOption arguments to jwt.Sign. The package merges these options into the final payload. If multiple options set the same field, the last one wins.

    Note on NoMaxAge: Because jwt.Claims options only write non-zero values (to avoid overwriting existing data with zeros), you cannot use a Claims struct to clear an existing expiry. Use jwt.NoMaxAge to explicitly clear Expiry and IssuedAt.

    // Method 1: Embedding
    type userClaims struct {
    	Username string `json:"username"`
    	Role     string `json:"role"`
    	jwt.Claims
    }
    
    // Method 2: Sign Options
    token, err := jwt.Sign(jwt.HS256, key, jwt.Map{"role": "admin"}, 
    	jwt.MaxAge(15*time.Minute), 
    	jwt.Issuer("auth.example.com"),
    )
  5. Use the correct JWT mark based on size

    main

    The brand uses two different versions of the mark (cuts) depending on the display size to ensure visual clarity:

    • Full Mark: Use for sizes 48px and above. This version contains three teeth.
    • Small Cut: Use for sizes 32px and below. This version uses two teeth (to prevent the gaps from merging), heavier strokes for the teeth and shaft, and a larger ring with a thinner stroke to protect the light gap in the bow.

    The boundary for switching between these is 48 pixels.

  6. Mock JWKS fetching with the HTTPClient interface

    main

    To test JWKS fetching without a real network, implement the HTTPClient interface. The FetchJWKS function and the WithHTTPClient option both accept any type that implements the Get(string) (*http.Response, error) method.

    By providing a fake client, you can simulate various network conditions, such as:

    • Returning specific HTTP status codes (e.g., 401, 500).
    • Returning large bodies to test MaxJWKSSize (1 MB).
    • Verifying that certain URLs were (or were not) requested before an error occurred.
    type recordingClient struct {
    	status    int
    	body      string
    	requested []string
    }
    
    func (c *recordingClient) Get(url string) (*http.Response, error) {
    	c.requested = append(c.requested, url)
    
    	status := c.status
    	if status == 0 {
    		status = http.StatusOK
    	}
    	return &http.Response{StatusCode: status,
    		Body: io.NopCloser(strings.NewReader(c.body))}, nil
    }
  7. Summary of Critical Hazards and 'Sharp Edges'

    main

    The jwt package contains several deliberate design choices that can lead to unexpected behavior if not understood. These 'sharp edges' fall into four categories:

    1. Misleading Names: Some names suggest behavior contrary to the implementation. For example, Leeway is noted as particularly counter-intuitive.
    2. Quiet Defaults: Certain defaults do not fail loudly when they should. Specifically:
      • MaxAge can collapse to 'no expiry' if misconfigured.
      • LoadHMAC can turn a file path into a secret if not used carefully.
    3. High-Performance Hazards: To maintain speed, the package uses specific patterns that require strict adherence to rules:
      • Unsafe String Conversion: Used in BytesToString (based on unsafe.String).
      • Raw JSON Splice: Used in Merge, which can emit duplicate keys.
    4. Breaking Changes: Significant behavioral shifts introduced in v0.2.0 to align with RFC standards.
  8. Understand the standard `jwt.Claims` struct

    main

    The jwt.Claims struct models the seven registered claims defined by RFC 7519. It uses short JSON keys (e.g., exp, sub, iss) for the wire format and longer Go names for code readability.

    Key characteristics:

    • Timestamp fields (NotBefore, IssuedAt, Expiry) use omitzero to ensure that an unset value (0) is omitted from the JSON payload rather than being emitted as 0.
    • Zero values are omitted: All fields use omitempty or omitzero, meaning a zero-value field (like an empty string or 0) will be absent from the resulting token. This allows Claims to be used as a partial template.
    • origin_jti: This is a package-specific convention (not an RFC standard) used to link a refresh token to an access token for revocation purposes.
    type Claims struct {
    	NotBefore int64    `json:"nbf,omitzero"` 
    	IssuedAt  int64    `json:"iat,omitzero"` 
    	Expiry    int64    `json:"exp,omitzero"` 
    	ID        string   `json:"jti,omitempty"` 
    	OriginID  string   `json:"origin_jti,omitempty"` 
    	Issuer    string   `json:"iss,omitempty"` 
    	Subject   string   `json:"sub,omitempty"` 
    	Audience  Audience `json:"aud,omitempty"` 
    }
  9. Revoke tokens using Blocklist and JTI

    main

    The Blocklist.InvalidateToken method uses the token's "jti" claim as a key.

    If a token does not have a "jti" claim, the blocklist falls back to using the entire raw token (including the signature) as the map key. This causes the full bearer credential to be pinned in memory until it expires. To ensure efficient revocation, always include a "jti" claim in any token you intend to revoke.

  10. Use `jwt.NONE` safely

    main

    The jwt.NONE algorithm is an exported value used for non-security-sensitive, client-inspectable data (like UI preferences).

    Security Risk: Never allow user-controlled input (query parameters, config, etc.) to reach the alg argument of Verify. If an attacker sets the algorithm to jwt.NONE, they can provide an unsigned token that the library will accept as genuine.

    Note that Enrich and UnverifiedToken.Enrich explicitly refuse alg == NONE to prevent silently signing attacker-influenced content with an unsigned output.

  11. Choose between Symmetric (HMAC) and Asymmetric algorithms

    main

    The choice between symmetric and asymmetric algorithms depends on your trust boundaries:

    Symmetric (HMAC)

    • Mechanism: The same []byte is used for both signing and verifying.
    • Security Model: Anyone who can verify a token can also mint (sign) one.
    • Best Use Case: Inside a single trust boundary (e.g., one process signing and verifying its own session tokens, or two closely related services sharing a secret).
    • Trade-offs: Faster and simpler, but lacks non-repudiation (you cannot prove which specific service issued a token if multiple services share the key).

    Asymmetric (RSA, ECDSA, EdDSA)

    • Mechanism: Uses a private key for signing and a public key for verification.
    • Security Model: The issuer holds the private key; verifiers only hold the public key. Verifiers cannot mint tokens.
    • Best Use Case: When third parties need to verify tokens, or when verifiers live in environments where you would not want to store a signing secret.
    • Trade-offs: Higher CPU cost for verification/signing and larger token sizes, but provides a clear separation of concerns.
  12. Understand the three JWT time claims

    main

    The package implements the three optional time claims defined in RFC 7519. These claims are stored as int64 fields on the Claims struct, representing seconds since the Unix epoch.

    ClaimNameMeaning
    expExpiration TimeWhen the token stops being valid. This is the primary security claim.
    nbfNot BeforeWhen the token starts being valid (used for future-dated tokens).
    iatIssued AtWhen the token was created. Used for metadata and calculating token age.

    Verify automatically checks all three claims if they are present in the payload. The checks occur after signature verification.

    type Claims struct {
    	NotBefore int64 `json:"nbf,omitempty"`
    	IssuedAt  int64 `json:"iat,omitempty"`
    	Expiry    int64 `json:"exp,omitempty"`
    	// ...
    }