Skip to content

JWT

The pkg/jwt package provides RS256-based JSON Web Token issuance and verification, plus a JWKS endpoint helper. It wraps github.com/golang-jwt/jwt/v5 for token operations and github.com/go-jose/go-jose/v3 for JWKS serialisation.

Initialization

NewProvider generates a fresh 2048-bit RSA key pair and pre-builds the JWKS document:

import pkgjwt "github.com/mikolajgasior/gorestapi/pkg/jwt"

p, err := pkgjwt.NewProvider("https://auth.example.com")
if err != nil {
    log.Fatal(err)
}

NewProvider takes one argument:

Argument Type Description
issuer string The iss claim written into every issued token and validated on parse

Provider

type Provider struct { /* unexported */ }

func (p *Provider) Issuer()     string
func (p *Provider) PrivateKey() *rsa.PrivateKey
func (p *Provider) PublicKey()  *rsa.PublicKey
func (p *Provider) JWKS()       []byte
func (p *Provider) Issue(audience, subject, role string) (string, string, *time.Time, *time.Time, error)
func (p *Provider) Parse(rawToken, audience string) (*CustomClaims, bool)
Method Description
Issuer() Returns the issuer string passed to NewProvider
PrivateKey() Returns the RSA private key (used for signing)
PublicKey() Returns the RSA public key (used for verification)
JWKS() Returns the pre-built JWKS document as JSON bytes
Issue(...) Signs and returns a new token (see below)
Parse(...) Validates and parses a token (see below)

Issuing tokens

signed, jti, issuedAt, expiresAt, err := p.Issue("myapp", "user:42", "admin")
Parameter Type Description
audience string Written as the aud claim; must match what Parse expects
subject string Written as the sub claim — typically a user ID or unique identifier
role string Written as the role claim in CustomClaims

Returns:

Value Type Description
signed string The compact-serialised JWT string
jti string Unique token ID (UUID v4) — store this to support revocation
issuedAt *time.Time Timestamp the token was created
expiresAt *time.Time Timestamp the token expires (1 hour after issuedAt)

Parsing and verifying tokens

claims, ok := p.Parse(rawToken, "myapp")
if !ok {
    // token is invalid, expired, or has wrong issuer/audience
}

subject := claims.Subject
role    := claims.Role

Parse validates the signature, expiry, issuer, and audience in one call. It returns (nil, false) on any failure so callers never need to inspect a partial claims struct.

Parameter Type Description
rawToken string Compact-serialised JWT from the request
audience string Expected aud value

A clock skew tolerance of 30 seconds is applied automatically.

Claims

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

CustomClaims embeds the standard jwt.RegisteredClaims (which carries Subject, Issuer, Audience, ExpiresAt, IssuedAt, NotBefore, ID) and adds a Role field. Access standard fields directly via the embedded struct:

subject := claims.Subject   // whatever was passed to Issue
expiry  := claims.ExpiresAt // *jwt.NumericDate
role    := claims.Role      // "admin"

Validating tokens from a remote issuer

When your service consumes tokens issued by a separate auth server, use ValidateWithJWKS. It fetches the JWKS document at the given URI, extracts the first RSA public key, and verifies the token — no local Provider needed.

claims, err := pkgjwt.ValidateWithJWKS(
    ctx,
    "https://auth.example.com/.well-known/jwks.json",
    "https://auth.example.com", // expected issuer
    "myapp",                    // expected audience
    bearerToken,
)
if err != nil {
    // fetch failed, key missing, or token invalid
}
Parameter Type Description
ctx context.Context Controls the HTTP request to the JWKS endpoint
jwksURI string Full URL of the JWKS document
issuer string Expected iss claim value
audience string Expected aud claim value
bearerToken string The raw JWT string from the request

Returns a descriptive error if the JWKS cannot be fetched or decoded, if no RSA key is found in the set, or if the token fails signature, expiry, issuer, or audience validation.

JWKS endpoint

Expose the public key to external verifiers (e.g. an API gateway or a frontend using a JWKS library):

http.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Write(p.JWKS())
})

The document is generated once at startup by NewProvider and cached on the Provider. To generate a JWKS from an existing public key directly:

jwksBytes, err := pkgjwt.JWKS(publicKey)

Complete example

package main

import (
    "fmt"
    "log"
    "net/http"
    "strings"

    pkgjwt "github.com/mikolajgasior/gorestapi/pkg/jwt"
)

const audience = "myapp"

func main() {
    p, err := pkgjwt.NewProvider("https://auth.example.com")
    if err != nil {
        log.Fatal(err)
    }

    // Issue a token for user 42.
    token, _, _, _, err := p.Issue(audience, "42", "member")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("token:", token)

    mux := http.NewServeMux()

    mux.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write(p.JWKS())
    })

    mux.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) {
        raw := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
        claims, ok := p.Parse(raw, audience)
        if !ok {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        fmt.Fprintf(w, "hello %s (role: %s)", claims.Subject, claims.Role)
    })

    http.ListenAndServe(":8080", mux)
}