Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions docs/01-jwt.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,16 @@ func Example_jwt_ParseFS() {
fmt.Fprint(f, exampleJWTSignedHMAC)
f.Close()

// Note: this JWT has NOT been verified because we have not passed jwt.WithKey() and used
// jwt.WithVerify(false). You need to pass jwt.WithKey() if you want the token to be parsed and
// verified in one go.
// This example calls ParseFS with both jwt.WithVerify(false) and
// jwt.WithValidate(false) only because there is no key context
// here — it demonstrates the FS-loading mechanics, nothing more.
// Production code reading a JWT from any source MUST pass
// jwt.WithKey() / jwt.WithKeySet() and MUST NOT disable
// jwt.WithValidate. The library exposes jwt.ParseInsecure for the
// inspect-without-verifying path when consuming raw bytes
// directly; ParseFS has no corresponding ParseFSInsecure today,
// so the two-option chant is the explicit way to express the
// same intent here.
tok, err := jwt.ParseFS(os.DirFS(filepath.Dir(f.Name())), filepath.Base(f.Name()), jwt.WithVerify(false), jwt.WithValidate(false))
if err != nil {
fmt.Printf("failed to read file %q: %s\n", f.Name(), err)
Expand Down Expand Up @@ -201,6 +208,13 @@ func Example_jwt_parse_request_authorization() {
}

for _, tc := range testcases {
// jwt.WithVerify(false) + jwt.WithValidate(false) below is only
// because this example has no key context — it demonstrates
// where ParseRequest looks for a token, nothing more.
// Production code MUST pass jwt.WithKey() / jwt.WithKeySet()
// and MUST NOT disable jwt.WithValidate. (ParseRequest has no
// ParseRequestInsecure variant; jwt.ParseInsecure exists for
// the raw-bytes path when you genuinely just want to inspect.)
options := append(tc.options, []jwt.ParseOption{jwt.WithVerify(false), jwt.WithValidate(false)}...)
tok, err := jwt.ParseRequest(req, options...)
if err != nil {
Expand Down Expand Up @@ -610,6 +624,18 @@ func Example_jwt_parse_with_key_provider_use_token() {
}

_, err = jws.Verify(signed, jws.WithKeyProvider(jws.KeyProviderFunc(func(_ context.Context, sink jws.KeySink, sig *jws.Signature, msg *jws.Message) error {
// `iss` came from `parsed`, which was produced by
// jwt.Parse(... jwt.WithVerify(false)). It is
// UNVERIFIED, caller-controlled input. The only safe
// way to use it here is as a lookup key against a
// closed allowlist of trusted issuers — exactly the
// switch below. Never use `iss` as a filesystem path,
// URL, cache key, or any other unbounded input: a
// malicious sender controls the string and will gladly
// inject `..`, NUL bytes, control characters, or
// anything else. The jws.Verify call this provider
// feeds into is what gates trust; before that, claims
// from `parsed` are just bytes off the wire.
iss, ok := parsed.Issuer()
if !ok {
return fmt.Errorf("no issuer found")
Expand Down
108 changes: 86 additions & 22 deletions jwk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,67 +24,131 @@ package examples_test

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"strings"

"encoding/json"
"github.com/jwx-go/jwkfetch/v4"
"github.com/lestrrat-go/jwx/v4/jwk"
)

// googleJWKSURL is the canonical OAuth 2.0 / OpenID Connect JWKS
// endpoint for accounts.google.com. Real production code calling
// jwkfetch against Google would pass this string verbatim.
const googleJWKSURL = "https://www.googleapis.com/oauth2/v3/certs"

// googleJWKSFixture is a small inline JWK Set used by the example
// when running offline (the default). It mirrors the shape Google
// returns — two RSA public keys with kid + alg — but the key
// material is fake.
const googleJWKSFixture = `{
"keys":[
{"kty":"RSA",
"n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB",
"alg":"RS256",
"kid":"example-key-1"},
{"kty":"RSA",
"n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB",
"alg":"RS256",
"kid":"example-key-2"}
]
}`

// roundTripFunc adapts a function value to http.RoundTripper.
type roundTripFunc func(*http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return f(r)
}

func Example_jwk_usage() {
// HTTP JWK Set retrieval lives in the jwkfetch extension module
// (github.com/jwx-go/jwkfetch). For a one-shot fetch, use
// jwkfetch.NewClient; for background-refreshed caching of a fixed
// set of trusted URLs, use jwkfetch.NewCache.
set, err := jwkfetch.NewClient().Fetch(context.Background(), "https://www.googleapis.com/oauth2/v3/certs")
// jwkfetch.NewClient; for background-refreshed caching of a
// fixed set of trusted URLs, use jwkfetch.NewCache.
//
// In production this single line is all you need:
//
// client := jwkfetch.NewClient()
//
// The branch below stands up a local httptest server and routes
// requests for the Google URL through it, so the example does
// not depend on Google being reachable in CI. Set
// JWX_EXAMPLE_FETCH_LIVE=1 in your environment to skip the local
// server and hit https://www.googleapis.com/oauth2/v3/certs
// directly.
var client *jwkfetch.Client
if os.Getenv("JWX_EXAMPLE_FETCH_LIVE") != "" {
client = jwkfetch.NewClient()
} else {
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprint(w, googleJWKSFixture)
}))
defer srv.Close()

// Route every request through the local httptest server,
// regardless of the URL the example passes to Fetch. The
// Fetch call below stays byte-identical to production code
// against Google.
hc := srv.Client()
origTransport := hc.Transport
target := strings.TrimPrefix(srv.URL, "https://")
hc.Transport = roundTripFunc(func(req *http.Request) (*http.Response, error) {
req.URL.Host = target
return origTransport.RoundTrip(req)
})
client = jwkfetch.NewClient(jwkfetch.WithHTTPClient(hc))
}

set, err := client.Fetch(context.Background(), googleJWKSURL)
if err != nil {
log.Printf("failed to parse JWK: %s", err)
fmt.Printf("failed to fetch JWKS: %s\n", err)
return
}

// Key sets can be serialized back to JSON
{
jsonbuf, err := json.Marshal(set)
if err != nil {
log.Printf("failed to marshal key set into JSON: %s", err)
return
}
log.Printf("%s", jsonbuf)
// Key sets can be serialized back to JSON.
if _, err := json.Marshal(set); err != nil {
fmt.Printf("failed to marshal key set into JSON: %s\n", err)
return
}

for i := 0; i < set.Len(); i++ {
key, ok := set.Key(i) // This retrieves the corresponding jwk.Key
if !ok {
log.Printf("failed to get key at index %d", i)
fmt.Printf("failed to get key at index %d\n", i)
return
}

// jws and jwe operations can be performed using jwk.Key, but you could also
// covert it to their "raw" forms, such as *rsa.PrivateKey or *ecdsa.PrivateKey
// convert it to its "raw" form, such as *rsa.PrivateKey or *ecdsa.PrivateKey.
rawkeyV, err := jwk.Export[any](key)
if err != nil {
log.Printf("failed to create public key: %s", err)
fmt.Printf("failed to export to raw key: %s\n", err)
return
}

// You can create jwk.Key from a raw key, too
// You can create jwk.Key from a raw key, too.
fromRawKey, err := jwk.Import[jwk.Key](rawkeyV)
if err != nil {
log.Printf("failed to acquire raw key from jwk.Key: %s", err)
fmt.Printf("failed to import raw key into jwk.Key: %s\n", err)
return
}

// Keys can be serialized back to JSON
// Keys can be serialized back to JSON.
jsonbuf, err := json.Marshal(key)
if err != nil {
log.Printf("failed to marshal key into JSON: %s", err)
fmt.Printf("failed to marshal key into JSON: %s\n", err)
return
}

fromJSONKey, err := jwk.Parse(jsonbuf)
if err != nil {
log.Printf("failed to parse json: %s", err)
fmt.Printf("failed to parse json: %s\n", err)
return
}
_ = fromJSONKey
Expand Down