Documentation
¶
Overview ¶
Package jwt authenticates requests carrying JSON Web Tokens from an external identity provider, such as Auth0, Clerk, Supabase, Firebase or Amazon Cognito (ADR-0085). It verifies the token's signature with the provider's published keys (JWKS), its issuer, audience and validity times, and puts the caller in the request context as an actor.
idp, err := jwt.New(ctx, jwt.Config{
Issuer: "https://example.eu.auth0.com/",
Audiences: []string{"https://api.example.com"},
JWKSURL: "https://example.eu.auth0.com/.well-known/jwks.json",
})
handler := idp.Middleware(logger)(mux)
Keys are fetched when the authenticator is created, cached as long as the provider's Cache-Control allows (between 5 minutes and 24 hours), and fetched again when a token names a key the cache doesn't have, at most once every 30 seconds. Nothing runs in the background.
Stability: experimental until v0.2.0 (ADR-0015, ADR-0085).
Index ¶
Examples ¶
Constants ¶
const ( // DefaultClockSkew is how far token times may be from this server's // clock unless [Config.ClockSkew] sets another. DefaultClockSkew = 30 * time.Second // MaxClockSkew is the largest [Config.ClockSkew] accepted. MaxClockSkew = 5 * time.Minute // DefaultPermissionsClaim is the claim permissions are read from unless // [Config.PermissionsClaim] sets another: Auth0's RBAC claim. DefaultPermissionsClaim = "permissions" // MaxTokenBytes is the longest token accepted. MaxTokenBytes = 16 << 10 )
Defaults and bounds of Config.
const ReservedPermissionPrefix = "ops."
ReservedPermissionPrefix is the permission namespace of the framework's own operations console (gorbital.dev/gorbital/opshttp). The default actor mapping drops permissions under it, because /ops authorizes on the actor's permissions alone and the claim a provider puts them in is often not fully under the operator's control: an OAuth "scope" claim is asked for by the client, and several providers map user-editable metadata into it. An app that does grant its operators /ops through the provider says so with Config.ActorFrom, which this never touches.
Variables ¶
var ( // ErrInvalidToken reports a token that isn't a signed JWT from the // provider for this API, or isn't valid now: malformed, signed with an // algorithm or key that isn't allowed, tampered with, expired, not yet // valid, or for another issuer or audience. The wrapping error says // which, for logs; never show it to clients. ErrInvalidToken = errors.New("jwt: invalid token") // fetched, so a token signed with a key that isn't cached can't be // verified. ErrKeysUnavailable = errors.New("jwt: signing keys unavailable") )
Errors returned by Authenticator.Verify. Check them with errors.Is.
var DefaultAlgorithms = []string{"RS256", "ES256", "EdDSA"}
DefaultAlgorithms are the signature algorithms accepted unless Config.Algorithms sets others.
var ErrNoClaim = errors.New("jwt: no such claim")
ErrNoClaim reports a claim the token doesn't carry.
Functions ¶
This section is empty.
Types ¶
type Authenticator ¶
type Authenticator struct {
// contains filtered or unexported fields
}
An Authenticator verifies tokens from one provider. Create it with New. It is safe for concurrent use.
Example ¶
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"time"
"github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"gorbital.dev/actor"
"gorbital.dev/modules/jwt"
)
// provider stands in for an identity provider: it publishes a JWKS and
// signs tokens.
type provider struct {
key *ecdsa.PrivateKey
srv *httptest.Server
}
func newProvider() *provider {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
p := &provider{key: key}
jwks, _ := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: key.Public(), KeyID: "key-1", Algorithm: "ES256", Use: "sig"}}})
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(jwks)
}))
return p
}
func (p *provider) jwksURL() string { return p.srv.URL + "/.well-known/jwks.json" }
// token signs claims, adding the issuer, audience and a one-hour expiry.
func (p *provider) token(claims map[string]any) string {
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: p.key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "key-1"))
if err != nil {
panic(err)
}
all := map[string]any{"iss": "https://idp.example.com/", "aud": "https://api.example.com", "exp": time.Now().Add(time.Hour).Unix(), "iat": time.Now().Unix()}
for k, v := range claims {
all[k] = v
}
token, err := josejwt.Signed(signer).Claims(all).Serialize()
if err != nil {
panic(err)
}
return token
}
func main() {
idp := newProvider()
defer idp.srv.Close()
auth, err := jwt.New(context.Background(), jwt.Config{
Issuer: "https://idp.example.com/",
Audiences: []string{"https://api.example.com"},
JWKSURL: idp.jwksURL(),
})
if err != nil {
panic(err)
}
// In a gorbital app: gorbital.WithAuth(auth). With net/http:
books := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := actor.Require(r.Context(), "books.book.read"); err != nil {
w.WriteHeader(http.StatusForbidden)
return
}
_, _ = w.Write([]byte("[]"))
})
handler := auth.Middleware(slog.Default())(books)
req := httptest.NewRequest(http.MethodGet, "/v1/books", nil)
req.Header.Set("Authorization", "Bearer "+idp.token(map[string]any{"sub": "usr_1", "permissions": []string{"books.book.read"}}))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
fmt.Println(rec.Code, rec.Body.String())
}
Output: 200 []
func New ¶
New validates cfg and fetches the provider's keys, within ctx. It returns an error for a missing issuer or audience, an unknown or disallowed algorithm, a JWKS URL that isn't https (or http on a loopback address), a clock skew out of bounds, an HMAC secret that is short or not matched by an HS algorithm, or keys that can't be fetched or contain no usable signing key, so a misconfigured app fails when it starts.
Example ¶
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"gorbital.dev/modules/jwt"
)
// provider stands in for an identity provider: it publishes a JWKS and
// signs tokens.
type provider struct {
key *ecdsa.PrivateKey
srv *httptest.Server
}
func newProvider() *provider {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
p := &provider{key: key}
jwks, _ := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: key.Public(), KeyID: "key-1", Algorithm: "ES256", Use: "sig"}}})
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(jwks)
}))
return p
}
func (p *provider) jwksURL() string { return p.srv.URL + "/.well-known/jwks.json" }
// token signs claims, adding the issuer, audience and a one-hour expiry.
func (p *provider) token(claims map[string]any) string {
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: p.key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "key-1"))
if err != nil {
panic(err)
}
all := map[string]any{"iss": "https://idp.example.com/", "aud": "https://api.example.com", "exp": time.Now().Add(time.Hour).Unix(), "iat": time.Now().Unix()}
for k, v := range claims {
all[k] = v
}
token, err := josejwt.Signed(signer).Claims(all).Serialize()
if err != nil {
panic(err)
}
return token
}
func main() {
idp := newProvider()
defer idp.srv.Close()
auth, err := jwt.New(context.Background(), jwt.Config{
Issuer: "https://idp.example.com/",
Audiences: []string{"https://api.example.com"},
JWKSURL: idp.jwksURL(),
})
fmt.Println(auth != nil, err)
_, err = jwt.New(context.Background(), jwt.Config{
Issuer: "https://idp.example.com/",
Audiences: []string{"https://api.example.com"},
JWKSURL: idp.jwksURL(),
Algorithms: []string{"none"},
})
fmt.Println(err)
}
Output: true <nil> jwt: algorithm "none" is not supported
func (*Authenticator) Middleware ¶
Middleware returns middleware that authenticates requests carrying a JWT in an "Authorization: Bearer" header and puts the caller in the context as an actor (actor.With), with the client's address and user agent (actor.WithClient) unless earlier middleware set them. Requests without a bearer token, with a token that isn't shaped like a JWT (such as an API key another authenticator handles), or that already have an actor continue unchanged; routes that need a caller answer them 401.
A JWT that fails verification is refused with 401 invalid_token and a WWW-Authenticate header (RFC 6750), rather than continuing anonymously, so a client learns to get a new token instead of silently losing access. When the provider's keys can't be fetched it answers 503 auth_unavailable. Refusals are logged at debug level; failed key fetches at warn level, at most once every 30 seconds.
Example ¶
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"time"
"github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"gorbital.dev/actor"
"gorbital.dev/modules/jwt"
)
// provider stands in for an identity provider: it publishes a JWKS and
// signs tokens.
type provider struct {
key *ecdsa.PrivateKey
srv *httptest.Server
}
func newProvider() *provider {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
p := &provider{key: key}
jwks, _ := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: key.Public(), KeyID: "key-1", Algorithm: "ES256", Use: "sig"}}})
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(jwks)
}))
return p
}
func (p *provider) jwksURL() string { return p.srv.URL + "/.well-known/jwks.json" }
// token signs claims, adding the issuer, audience and a one-hour expiry.
func (p *provider) token(claims map[string]any) string {
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: p.key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "key-1"))
if err != nil {
panic(err)
}
all := map[string]any{"iss": "https://idp.example.com/", "aud": "https://api.example.com", "exp": time.Now().Add(time.Hour).Unix(), "iat": time.Now().Unix()}
for k, v := range claims {
all[k] = v
}
token, err := josejwt.Signed(signer).Claims(all).Serialize()
if err != nil {
panic(err)
}
return token
}
func main() {
idp := newProvider()
defer idp.srv.Close()
auth, err := jwt.New(context.Background(), jwt.Config{
Issuer: "https://idp.example.com/",
Audiences: []string{"https://api.example.com"},
JWKSURL: idp.jwksURL(),
})
if err != nil {
panic(err)
}
handler := auth.Middleware(slog.Default())(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
a := actor.FromOrAnonymous(r.Context())
fmt.Fprintf(w, "%s:%s", a.Kind, a.ID)
}))
valid := idp.token(map[string]any{"sub": "usr_1"})
for _, authorization := range []string{
"Bearer " + valid,
"", // no token: anonymous
"Bearer " + valid[:len(valid)-4] + "AAAA", // a tampered JWT: refused
} {
req := httptest.NewRequest(http.MethodGet, "/", nil)
if authorization != "" {
req.Header.Set("Authorization", authorization)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
fmt.Printf("%d %q %s\n", rec.Code, rec.Header().Get("WWW-Authenticate"), rec.Body.String())
}
}
Output: 200 "" user:usr_1 200 "" anonymous: 401 "Bearer error=\"invalid_token\"" {"title":"Unauthorized","status":401,"code":"invalid_token","detail":"the access token is invalid or expired; get a new one"}
func (*Authenticator) Verify ¶
Verify checks token and returns its claims. It returns an error wrapping ErrInvalidToken when the token isn't acceptable, and ErrKeysUnavailable when its key isn't cached and the provider's keys can't be fetched. Use it outside HTTP middleware, such as for a WebSocket's first message.
Example ¶
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"gorbital.dev/modules/jwt"
)
// provider stands in for an identity provider: it publishes a JWKS and
// signs tokens.
type provider struct {
key *ecdsa.PrivateKey
srv *httptest.Server
}
func newProvider() *provider {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
p := &provider{key: key}
jwks, _ := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: key.Public(), KeyID: "key-1", Algorithm: "ES256", Use: "sig"}}})
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(jwks)
}))
return p
}
func (p *provider) jwksURL() string { return p.srv.URL + "/.well-known/jwks.json" }
// token signs claims, adding the issuer, audience and a one-hour expiry.
func (p *provider) token(claims map[string]any) string {
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: p.key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "key-1"))
if err != nil {
panic(err)
}
all := map[string]any{"iss": "https://idp.example.com/", "aud": "https://api.example.com", "exp": time.Now().Add(time.Hour).Unix(), "iat": time.Now().Unix()}
for k, v := range claims {
all[k] = v
}
token, err := josejwt.Signed(signer).Claims(all).Serialize()
if err != nil {
panic(err)
}
return token
}
func main() {
idp := newProvider()
defer idp.srv.Close()
auth, err := jwt.New(context.Background(), jwt.Config{
Issuer: "https://idp.example.com/",
Audiences: []string{"https://api.example.com"},
JWKSURL: idp.jwksURL(),
})
if err != nil {
panic(err)
}
claims, err := auth.Verify(context.Background(), idp.token(map[string]any{"sub": "usr_1"}))
fmt.Println(claims.Subject, err)
expired := idp.token(map[string]any{"sub": "usr_1", "exp": time.Now().Add(-time.Hour).Unix()})
_, err = auth.Verify(context.Background(), expired)
fmt.Println(errors.Is(err, jwt.ErrInvalidToken), err)
}
Output: usr_1 <nil> true jwt: invalid token: expired
type Claims ¶
type Claims struct {
// Issuer is "iss", equal to [Config.Issuer].
Issuer string
// Subject is "sub", the caller's ID at the provider. Never empty.
Subject string
// Audience is "aud".
Audience []string
// ExpiresAt is "exp"; NotBefore "nbf" and IssuedAt "iat", or zero when
// the token has none.
ExpiresAt, NotBefore, IssuedAt time.Time
// ID is "jti", or empty.
ID string
// contains filtered or unexported fields
}
Claims are a verified token's claims: the registered ones as fields, and any other through Claims.String, Claims.Strings and Claims.Decode.
Example ¶
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"gorbital.dev/modules/jwt"
)
// provider stands in for an identity provider: it publishes a JWKS and
// signs tokens.
type provider struct {
key *ecdsa.PrivateKey
srv *httptest.Server
}
func newProvider() *provider {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
p := &provider{key: key}
jwks, _ := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: key.Public(), KeyID: "key-1", Algorithm: "ES256", Use: "sig"}}})
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(jwks)
}))
return p
}
func (p *provider) jwksURL() string { return p.srv.URL + "/.well-known/jwks.json" }
// token signs claims, adding the issuer, audience and a one-hour expiry.
func (p *provider) token(claims map[string]any) string {
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: p.key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "key-1"))
if err != nil {
panic(err)
}
all := map[string]any{"iss": "https://idp.example.com/", "aud": "https://api.example.com", "exp": time.Now().Add(time.Hour).Unix(), "iat": time.Now().Unix()}
for k, v := range claims {
all[k] = v
}
token, err := josejwt.Signed(signer).Claims(all).Serialize()
if err != nil {
panic(err)
}
return token
}
func main() {
idp := newProvider()
defer idp.srv.Close()
auth, err := jwt.New(context.Background(), jwt.Config{Issuer: "https://idp.example.com/", Audiences: []string{"https://api.example.com"}, JWKSURL: idp.jwksURL()})
if err != nil {
panic(err)
}
c, err := auth.Verify(context.Background(), idp.token(map[string]any{"sub": "usr_1", "jti": "tok_9"}))
if err != nil {
panic(err)
}
fmt.Println(c.Issuer, c.Subject, c.Audience, c.ID, time.Until(c.ExpiresAt) > 59*time.Minute)
}
Output: https://idp.example.com/ usr_1 [https://api.example.com] tok_9 true
func (Claims) Decode ¶
Decode unmarshals a claim's JSON into v, such as a provider's nested metadata object. It returns an error wrapping ErrNoClaim when the claim is missing, or the JSON error when it doesn't fit v.
Example ¶
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"gorbital.dev/modules/jwt"
)
// provider stands in for an identity provider: it publishes a JWKS and
// signs tokens.
type provider struct {
key *ecdsa.PrivateKey
srv *httptest.Server
}
func newProvider() *provider {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
p := &provider{key: key}
jwks, _ := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: key.Public(), KeyID: "key-1", Algorithm: "ES256", Use: "sig"}}})
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(jwks)
}))
return p
}
func (p *provider) jwksURL() string { return p.srv.URL + "/.well-known/jwks.json" }
// token signs claims, adding the issuer, audience and a one-hour expiry.
func (p *provider) token(claims map[string]any) string {
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: p.key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "key-1"))
if err != nil {
panic(err)
}
all := map[string]any{"iss": "https://idp.example.com/", "aud": "https://api.example.com", "exp": time.Now().Add(time.Hour).Unix(), "iat": time.Now().Unix()}
for k, v := range claims {
all[k] = v
}
token, err := josejwt.Signed(signer).Claims(all).Serialize()
if err != nil {
panic(err)
}
return token
}
func verified(claims map[string]any) jwt.Claims {
idp := newProvider()
defer idp.srv.Close()
auth, err := jwt.New(context.Background(), jwt.Config{Issuer: "https://idp.example.com/", Audiences: []string{"https://api.example.com"}, JWKSURL: idp.jwksURL()})
if err != nil {
panic(err)
}
c, err := auth.Verify(context.Background(), idp.token(claims))
if err != nil {
panic(err)
}
return c
}
func main() {
// Supabase puts app metadata in a nested object.
c := verified(map[string]any{"sub": "usr_1", "app_metadata": map[string]any{"provider": "email", "tenant": "org_7"}})
var meta struct {
Tenant string `json:"tenant"`
}
err := c.Decode("app_metadata", &meta)
fmt.Println(meta.Tenant, err)
fmt.Println(errors.Is(c.Decode("user_metadata", &meta), jwt.ErrNoClaim))
}
Output: org_7 <nil> true
func (Claims) String ¶
String returns a string claim, or "" when the claim is missing or isn't a string.
Example ¶
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"gorbital.dev/modules/jwt"
)
// provider stands in for an identity provider: it publishes a JWKS and
// signs tokens.
type provider struct {
key *ecdsa.PrivateKey
srv *httptest.Server
}
func newProvider() *provider {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
p := &provider{key: key}
jwks, _ := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: key.Public(), KeyID: "key-1", Algorithm: "ES256", Use: "sig"}}})
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(jwks)
}))
return p
}
func (p *provider) jwksURL() string { return p.srv.URL + "/.well-known/jwks.json" }
// token signs claims, adding the issuer, audience and a one-hour expiry.
func (p *provider) token(claims map[string]any) string {
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: p.key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "key-1"))
if err != nil {
panic(err)
}
all := map[string]any{"iss": "https://idp.example.com/", "aud": "https://api.example.com", "exp": time.Now().Add(time.Hour).Unix(), "iat": time.Now().Unix()}
for k, v := range claims {
all[k] = v
}
token, err := josejwt.Signed(signer).Claims(all).Serialize()
if err != nil {
panic(err)
}
return token
}
func verified(claims map[string]any) jwt.Claims {
idp := newProvider()
defer idp.srv.Close()
auth, err := jwt.New(context.Background(), jwt.Config{Issuer: "https://idp.example.com/", Audiences: []string{"https://api.example.com"}, JWKSURL: idp.jwksURL()})
if err != nil {
panic(err)
}
c, err := auth.Verify(context.Background(), idp.token(claims))
if err != nil {
panic(err)
}
return c
}
func main() {
c := verified(map[string]any{"sub": "usr_1", "org_id": "org_7"})
fmt.Printf("%q %q\n", c.String("org_id"), c.String("missing"))
}
Output: "org_7" ""
func (Claims) Strings ¶
Strings returns a claim holding a list of strings, such as "permissions", or one space-separated string, such as "scope". It returns nil when the claim is missing or is neither.
Example ¶
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"gorbital.dev/modules/jwt"
)
// provider stands in for an identity provider: it publishes a JWKS and
// signs tokens.
type provider struct {
key *ecdsa.PrivateKey
srv *httptest.Server
}
func newProvider() *provider {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
p := &provider{key: key}
jwks, _ := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: key.Public(), KeyID: "key-1", Algorithm: "ES256", Use: "sig"}}})
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(jwks)
}))
return p
}
func (p *provider) jwksURL() string { return p.srv.URL + "/.well-known/jwks.json" }
// token signs claims, adding the issuer, audience and a one-hour expiry.
func (p *provider) token(claims map[string]any) string {
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: p.key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "key-1"))
if err != nil {
panic(err)
}
all := map[string]any{"iss": "https://idp.example.com/", "aud": "https://api.example.com", "exp": time.Now().Add(time.Hour).Unix(), "iat": time.Now().Unix()}
for k, v := range claims {
all[k] = v
}
token, err := josejwt.Signed(signer).Claims(all).Serialize()
if err != nil {
panic(err)
}
return token
}
func verified(claims map[string]any) jwt.Claims {
idp := newProvider()
defer idp.srv.Close()
auth, err := jwt.New(context.Background(), jwt.Config{Issuer: "https://idp.example.com/", Audiences: []string{"https://api.example.com"}, JWKSURL: idp.jwksURL()})
if err != nil {
panic(err)
}
c, err := auth.Verify(context.Background(), idp.token(claims))
if err != nil {
panic(err)
}
return c
}
func main() {
c := verified(map[string]any{"sub": "usr_1", "scope": "openid read:books", "permissions": []string{"books.book.read"}})
fmt.Println(c.Strings("scope"), c.Strings("permissions"))
}
Output: [openid read:books] [books.book.read]
type Config ¶
type Config struct {
// Issuer is the provider's issuer, compared exactly with the "iss"
// claim, such as "https://example.eu.auth0.com/" (with its trailing
// slash) or "https://securetoken.google.com/<project>". Required.
Issuer string
// Audiences identify this API. A token is accepted when its audience
// claim contains at least one of them, so tokens the same provider
// issues for other apps are refused. Required.
Audiences []string
// AudienceClaim is the claim holding the audience. Default: "aud".
// Cognito access tokens carry "client_id" instead.
AudienceClaim string
// JWKSURL is where the provider publishes its public keys: https, or
// http on a loopback address for a provider running locally. Required
// unless every algorithm is HS256, HS384 or HS512.
JWKSURL string
// Algorithms are the accepted "alg" values: RS256, RS384, RS512, PS256,
// PS384, PS512, ES256, ES384, ES512 or EdDSA, and HS256, HS384 or HS512
// only with [WithHMACSecret]. "none" is never accepted. Default:
// [DefaultAlgorithms].
Algorithms []string
// ClockSkew is how far the "exp", "nbf" and "iat" claims may be from
// this server's clock, at most [MaxClockSkew]. Default:
// [DefaultClockSkew].
ClockSkew time.Duration
// PermissionsClaim names the claim holding the caller's permissions: a
// list of strings, or one space-separated string such as "scope".
// Default: [DefaultPermissionsClaim]. The default actor mapping drops
// permissions under [ReservedPermissionPrefix], the operations console's
// namespace; grant those with ActorFrom instead.
PermissionsClaim string
// ActorFrom returns the actor for verified claims, replacing the
// default: a user whose ID is "sub" and whose permissions are
// PermissionsClaim, less those under [ReservedPermissionPrefix]. Return
// a service actor for machine tokens, map the provider's permission
// names to the app's, or set OrgID. An error refuses the token. The
// actor's kind must be user or service, with an ID.
//
// What it returns is trusted as it stands: the permissions it sets are
// what guard.Permission and the operations console check, and its StepUp
// is empty unless it fills it, so a permission an app grants only to a
// session with a second factor is granted outright. Map the provider's
// names to the app's rather than passing a claim through.
ActorFrom func(Claims) (actor.Actor, error)
}
Config configures New.
Example ¶
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"gorbital.dev/actor"
"gorbital.dev/modules/jwt"
)
// provider stands in for an identity provider: it publishes a JWKS and
// signs tokens.
type provider struct {
key *ecdsa.PrivateKey
srv *httptest.Server
}
func newProvider() *provider {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
p := &provider{key: key}
jwks, _ := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: key.Public(), KeyID: "key-1", Algorithm: "ES256", Use: "sig"}}})
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(jwks)
}))
return p
}
func (p *provider) jwksURL() string { return p.srv.URL + "/.well-known/jwks.json" }
// token signs claims, adding the issuer, audience and a one-hour expiry.
func (p *provider) token(claims map[string]any) string {
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: p.key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "key-1"))
if err != nil {
panic(err)
}
all := map[string]any{"iss": "https://idp.example.com/", "aud": "https://api.example.com", "exp": time.Now().Add(time.Hour).Unix(), "iat": time.Now().Unix()}
for k, v := range claims {
all[k] = v
}
token, err := josejwt.Signed(signer).Claims(all).Serialize()
if err != nil {
panic(err)
}
return token
}
func main() {
idp := newProvider()
defer idp.srv.Close()
auth, err := jwt.New(context.Background(), jwt.Config{
Issuer: "https://idp.example.com/",
Audiences: []string{"https://api.example.com"},
JWKSURL: idp.jwksURL(),
ClockSkew: time.Minute,
// Machine-to-machine tokens become service actors, and the
// provider's "read:books" scope becomes the app's permission.
ActorFrom: func(c jwt.Claims) (actor.Actor, error) {
kind := actor.KindUser
if c.String("gty") == "client-credentials" {
kind = actor.KindService
}
a := actor.Actor{Kind: kind, ID: c.Subject}
for _, scope := range c.Strings("scope") {
if scope == "read:books" {
a.Permissions = append(a.Permissions, "books.book.read")
}
}
return a, nil
},
})
if err != nil {
panic(err)
}
handler := auth.Middleware(nil)(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
a, _ := actor.From(r.Context())
fmt.Println(a.Kind, a.ID, a.Permissions)
}))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Bearer "+idp.token(map[string]any{"sub": "client_42@clients", "gty": "client-credentials", "scope": "read:books"}))
handler.ServeHTTP(httptest.NewRecorder(), req)
}
Output: service client_42@clients [books.book.read]
type Option ¶
type Option func(*options)
An Option configures New.
Example ¶
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"gorbital.dev/modules/jwt"
)
// provider stands in for an identity provider: it publishes a JWKS and
// signs tokens.
type provider struct {
key *ecdsa.PrivateKey
srv *httptest.Server
}
func newProvider() *provider {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
p := &provider{key: key}
jwks, _ := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: key.Public(), KeyID: "key-1", Algorithm: "ES256", Use: "sig"}}})
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(jwks)
}))
return p
}
func (p *provider) jwksURL() string { return p.srv.URL + "/.well-known/jwks.json" }
// token signs claims, adding the issuer, audience and a one-hour expiry.
func (p *provider) token(claims map[string]any) string {
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: p.key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "key-1"))
if err != nil {
panic(err)
}
all := map[string]any{"iss": "https://idp.example.com/", "aud": "https://api.example.com", "exp": time.Now().Add(time.Hour).Unix(), "iat": time.Now().Unix()}
for k, v := range claims {
all[k] = v
}
token, err := josejwt.Signed(signer).Claims(all).Serialize()
if err != nil {
panic(err)
}
return token
}
func main() {
idp := newProvider()
defer idp.srv.Close()
_, err := jwt.New(context.Background(),
jwt.Config{Issuer: "https://idp.example.com/", Audiences: []string{"https://api.example.com"}, JWKSURL: idp.jwksURL()},
jwt.WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
jwt.WithClock(time.Now),
)
fmt.Println(err)
}
Output: <nil>
func WithClock ¶
WithClock sets the clock used to check token times and cache ages, for tests. Default: time.Now.
Example ¶
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"gorbital.dev/modules/jwt"
)
// provider stands in for an identity provider: it publishes a JWKS and
// signs tokens.
type provider struct {
key *ecdsa.PrivateKey
srv *httptest.Server
}
func newProvider() *provider {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
p := &provider{key: key}
jwks, _ := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: key.Public(), KeyID: "key-1", Algorithm: "ES256", Use: "sig"}}})
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(jwks)
}))
return p
}
func (p *provider) jwksURL() string { return p.srv.URL + "/.well-known/jwks.json" }
// token signs claims, adding the issuer, audience and a one-hour expiry.
func (p *provider) token(claims map[string]any) string {
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: p.key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "key-1"))
if err != nil {
panic(err)
}
all := map[string]any{"iss": "https://idp.example.com/", "aud": "https://api.example.com", "exp": time.Now().Add(time.Hour).Unix(), "iat": time.Now().Unix()}
for k, v := range claims {
all[k] = v
}
token, err := josejwt.Signed(signer).Claims(all).Serialize()
if err != nil {
panic(err)
}
return token
}
func main() {
idp := newProvider()
defer idp.srv.Close()
tomorrow := func() time.Time { return time.Now().Add(24 * time.Hour) }
auth, err := jwt.New(context.Background(),
jwt.Config{Issuer: "https://idp.example.com/", Audiences: []string{"https://api.example.com"}, JWKSURL: idp.jwksURL()},
jwt.WithClock(tomorrow))
if err != nil {
panic(err)
}
_, err = auth.Verify(context.Background(), idp.token(map[string]any{"sub": "usr_1"}))
fmt.Println(err)
}
Output: jwt: invalid token: expired
func WithHMACSecret ¶
WithHMACSecret sets the shared secret for HS256, HS384 and HS512 tokens, such as a Supabase project's legacy JWT secret. It must be at least 32 bytes, and those algorithms must be listed in Config.Algorithms. Prefer a provider's asymmetric keys: anyone holding the secret can issue tokens.
Example ¶
package main
import (
"context"
"fmt"
"time"
"github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"gorbital.dev/modules/jwt"
)
func main() {
secret := []byte("a-32-byte-or-longer-project-secret") // from the environment, never in code
auth, err := jwt.New(context.Background(),
jwt.Config{Issuer: "https://project.supabase.co/auth/v1", Audiences: []string{"authenticated"}, Algorithms: []string{"HS256"}},
jwt.WithHMACSecret(secret))
if err != nil {
panic(err)
}
signer, _ := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: secret}, nil)
token, _ := josejwt.Signed(signer).Claims(map[string]any{
"iss": "https://project.supabase.co/auth/v1", "aud": "authenticated", "sub": "usr_1", "exp": time.Now().Add(time.Hour).Unix(),
}).Serialize()
c, err := auth.Verify(context.Background(), token)
fmt.Println(c.Subject, err)
}
Output: usr_1 <nil>
func WithHTTPClient ¶
WithHTTPClient sets the client used to fetch the provider's keys, such as one with a proxy or custom root certificates. Default: a client with a 10-second timeout that follows at most 3 redirects, each to an allowed URL.
Example ¶
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"gorbital.dev/modules/jwt"
)
// provider stands in for an identity provider: it publishes a JWKS and
// signs tokens.
type provider struct {
key *ecdsa.PrivateKey
srv *httptest.Server
}
func newProvider() *provider {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
p := &provider{key: key}
jwks, _ := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: key.Public(), KeyID: "key-1", Algorithm: "ES256", Use: "sig"}}})
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(jwks)
}))
return p
}
func (p *provider) jwksURL() string { return p.srv.URL + "/.well-known/jwks.json" }
// token signs claims, adding the issuer, audience and a one-hour expiry.
func (p *provider) token(claims map[string]any) string {
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: p.key}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "key-1"))
if err != nil {
panic(err)
}
all := map[string]any{"iss": "https://idp.example.com/", "aud": "https://api.example.com", "exp": time.Now().Add(time.Hour).Unix(), "iat": time.Now().Unix()}
for k, v := range claims {
all[k] = v
}
token, err := josejwt.Signed(signer).Claims(all).Serialize()
if err != nil {
panic(err)
}
return token
}
func main() {
idp := newProvider()
defer idp.srv.Close()
// A client with the proxy, timeout or root certificates the
// deployment needs.
client := &http.Client{Timeout: 3 * time.Second, Transport: http.DefaultTransport}
_, err := jwt.New(context.Background(),
jwt.Config{Issuer: "https://idp.example.com/", Audiences: []string{"https://api.example.com"}, JWKSURL: idp.jwksURL()},
jwt.WithHTTPClient(client))
fmt.Println(err)
}
Output: <nil>