Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "jwt",
srcs = [
"builder_gen.go",
"errors.go",
"filter.go",
"fastpath.go",
"http.go",
"interface.go",
"io.go",
"jwt.go",
"options.go",
"options_gen.go",
"serialize.go",
"token_gen.go",
"token_options.go",
"token_options_gen.go",
"validate.go",
],
importpath = "github.com/lestrrat-go/jwx/v3/jwt",
visibility = ["//visibility:public"],
deps = [
"//:jwx",
"//internal/base64",
"//transform",
"//internal/json",
"//internal/tokens",
"//internal/pool",
"//jwa",
"//jwe",
"//jwk",
"//jws",
"//jws/jwsbb",
"//jwt/internal/types",
"//jwt/internal/errors",
"@com_github_lestrrat_go_blackmagic//:blackmagic",
"@com_github_lestrrat_go_option_v2//:option",
],
)
go_test(
name = "jwt_test",
srcs = [
"jwt_test.go",
"options_gen_test.go",
"token_options_test.go",
"token_test.go",
"validate_test.go",
"verify_test.go",
],
embed = [":jwt"],
deps = [
"//internal/json",
"//internal/jwxtest",
"//jwa",
"//jwe",
"//jwk",
"//jwk/ecdsa",
"//jws",
"//jwt/internal/types",
"@com_github_lestrrat_go_httprc_v3//:httprc",
"@com_github_stretchr_testify//require",
],
)
alias(
name = "go_default_library",
actual = ":jwt",
visibility = ["//visibility:public"],
)
+224
View File
@@ -0,0 +1,224 @@
# JWT [![Go Reference](https://pkg.go.dev/badge/github.com/lestrrat-go/jwx/v3/jwt.svg)](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jwt)
Package jwt implements JSON Web Tokens as described in [RFC7519](https://tools.ietf.org/html/rfc7519).
* Convenience methods for oft-used keys ("aud", "sub", "iss", etc)
* Convenience functions to extract/parse from http.Request, http.Header, url.Values
* Ability to Get/Set arbitrary keys
* Conversion to and from JSON
* Generate signed tokens
* Verify signed tokens
* Extra support for OpenID tokens via [github.com/lestrrat-go/jwx/v3/jwt/openid](./jwt/openid)
How-to style documentation can be found in the [docs directory](../docs).
More examples are located in the examples directory ([jwt_example_test.go](../examples/jwt_example_test.go))
# SYNOPSIS
## Verify a signed JWT
```go
token, err := jwt.Parse(payload, jwt.WithKey(alg, key))
if err != nil {
fmt.Printf("failed to parse payload: %s\n", err)
}
```
## Token Usage
```go
func ExampleJWT() {
const aLongLongTimeAgo = 233431200
t := jwt.New()
t.Set(jwt.SubjectKey, `https://github.com/lestrrat-go/jwx/v3/jwt`)
t.Set(jwt.AudienceKey, `Golang Users`)
t.Set(jwt.IssuedAtKey, time.Unix(aLongLongTimeAgo, 0))
t.Set(`privateClaimKey`, `Hello, World!`)
buf, err := json.MarshalIndent(t, "", " ")
if err != nil {
fmt.Printf("failed to generate JSON: %s\n", err)
return
}
fmt.Printf("%s\n", buf)
fmt.Printf("aud -> '%s'\n", t.Audience())
fmt.Printf("iat -> '%s'\n", t.IssuedAt().Format(time.RFC3339))
if v, ok := t.Get(`privateClaimKey`); ok {
fmt.Printf("privateClaimKey -> '%s'\n", v)
}
fmt.Printf("sub -> '%s'\n", t.Subject())
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.Printf("failed to generate private key: %s", err)
return
}
{
// Signing a token (using raw rsa.PrivateKey)
signed, err := jwt.Sign(t, jwt.WithKey(jwa.RS256, key))
if err != nil {
log.Printf("failed to sign token: %s", err)
return
}
_ = signed
}
{
// Signing a token (using JWK)
jwkKey, err := jwk.New(key)
if err != nil {
log.Printf("failed to create JWK key: %s", err)
return
}
signed, err := jwt.Sign(t, jwt.WithKey(jwa.RS256, jwkKey))
if err != nil {
log.Printf("failed to sign token: %s", err)
return
}
_ = signed
}
}
```
## OpenID Claims
`jwt` package can work with token types other than the default one.
For OpenID claims, use the token created by `openid.New()`, or
use the `jwt.WithToken(openid.New())`. If you need to use other specialized
claims, use `jwt.WithToken()` to specify the exact token type
```go
func Example_openid() {
const aLongLongTimeAgo = 233431200
t := openid.New()
t.Set(jwt.SubjectKey, `https://github.com/lestrrat-go/jwx/v3/jwt`)
t.Set(jwt.AudienceKey, `Golang Users`)
t.Set(jwt.IssuedAtKey, time.Unix(aLongLongTimeAgo, 0))
t.Set(`privateClaimKey`, `Hello, World!`)
addr := openid.NewAddress()
addr.Set(openid.AddressPostalCodeKey, `105-0011`)
addr.Set(openid.AddressCountryKey, `日本`)
addr.Set(openid.AddressRegionKey, `東京都`)
addr.Set(openid.AddressLocalityKey, `港区`)
addr.Set(openid.AddressStreetAddressKey, `芝公園 4-2-8`)
t.Set(openid.AddressKey, addr)
buf, err := json.MarshalIndent(t, "", " ")
if err != nil {
fmt.Printf("failed to generate JSON: %s\n", err)
return
}
fmt.Printf("%s\n", buf)
t2, err := jwt.Parse(buf, jwt.WithToken(openid.New()))
if err != nil {
fmt.Printf("failed to parse JSON: %s\n", err)
return
}
if _, ok := t2.(openid.Token); !ok {
fmt.Printf("using jwt.WithToken(openid.New()) creates an openid.Token instance")
return
}
}
```
# FAQ
## Why is `jwt.Token` an interface?
In this package, `jwt.Token` is an interface. This is not an arbitrary choice: there are actual reason for the type being an interface.
We understand that if you are migrating from another library this may be a deal breaker, but we hope you can at least appreciate the fact that this was not done arbitrarily, and that there were real technical trade offs that were evaluated.
### No uninitialized tokens
First and foremost, by making it an interface, you cannot use an uninitialized token:
```go
var token1 jwt.Token // this is nil, you can't just start using this
if err := json.Unmarshal(data, &token1); err != nil { // so you can't do this
...
}
// But you _can_ do this, and we _want_ you to do this so the object is properly initialized
token2 = jwt.New()
if err := json.Unmarshal(data, &token2); err != nil { // actually, in practice you should use jwt.Parse()
....
}
```
### But why does it need to be initialized?
There are several reasons, but one of the reasons is that I'm using a sync.Mutex to avoid races. We want this to be properly initialized.
The other reason is that we support custom claims out of the box. The `map[string]interface{}` container is initialized during new. This is important when checking for equality using reflect-y methods (akin to `reflect.DeepEqual`), because if you allowed zero values, you could end up with "empty" tokens, that actually differ. Consider the following:
```go
// assume jwt.Token was s struct, not an interface
token1 := jwt.Token{ privateClaims: make(map[string]interface{}) }
token2 := jwt.Token{ privateClaims: nil }
```
These are semantically equivalent, but users would need to be aware of this difference when comparing values. By forcing the user to use a constructor, we can force a uniform empty state.
### Standard way to store values
Unlike some other libraries, this library allows you to store standard claims and non-standard claims in the same token.
You _want_ to store standard claims in a properly typed field, which we do for fields like "iss", "nbf", etc.
But for non-standard claims, there is just no way of doing this, so we _have_ to use a container like `map[string]interface{}`
This means that if you allow direct access to these fields via a struct, you will have two different ways to access the claims, which is confusing:
```go
tok.Issuer = ...
tok.PrivateClaims["foo"] = ...
```
So we want to hide where this data is stored, and use a standard method like `Set()` and `Get()` to store all the values.
At this point you are effectively going to hide the implementation detail from the user, so you end up with a struct like below, which is fundamentally not so different from providing just an interface{}:
```go
type Token struct {
// unexported fields
}
func (tok *Token) Set(...) { ... }
```
### Use of pointers to store values
We wanted to differentiate the state between a claim being uninitialized, and a claim being initialized to empty.
So we use pointers to store values:
```go
type stdToken struct {
....
issuer *string // if nil, uninitialized. if &(""), initialized to empty
}
```
This is fine for us, but we doubt that this would be something users would want to do.
This is a subtle difference, but cluttering up the API with slight variations of the same type (i.e. pointers vs non-pointers) seemed like a bad idea to us.
```go
token.Issuer = &issuer // want to avoid this
token.Set(jwt.IssuerKey, "foobar") // so this is what we picked
```
This way users no longer need to care how the data is internally stored.
### Allow more than one type of token through the same interface
`dgrijalva/jwt-go` does this in a different way, but we felt that it would be more intuitive for all tokens to follow a single interface so there is fewer type conversions required.
See the `openid` token for an example.
+88
View File
@@ -0,0 +1,88 @@
// Code generated by tools/cmd/genjwt/main.go. DO NOT EDIT.
package jwt
import (
"fmt"
"sync"
"time"
)
// Builder is a convenience wrapper around the New() constructor
// and the Set() methods to assign values to Token claims.
// Users can successively call Claim() on the Builder, and have it
// construct the Token when Build() is called. This alleviates the
// need for the user to check for the return value of every single
// Set() method call.
// Note that each call to Claim() overwrites the value set from the
// previous call.
type Builder struct {
mu sync.Mutex
claims map[string]any
}
func NewBuilder() *Builder {
return &Builder{}
}
func (b *Builder) init() {
if b.claims == nil {
b.claims = make(map[string]any)
}
}
func (b *Builder) Claim(name string, value any) *Builder {
b.mu.Lock()
defer b.mu.Unlock()
b.init()
b.claims[name] = value
return b
}
func (b *Builder) Audience(v []string) *Builder {
return b.Claim(AudienceKey, v)
}
func (b *Builder) Expiration(v time.Time) *Builder {
return b.Claim(ExpirationKey, v)
}
func (b *Builder) IssuedAt(v time.Time) *Builder {
return b.Claim(IssuedAtKey, v)
}
func (b *Builder) Issuer(v string) *Builder {
return b.Claim(IssuerKey, v)
}
func (b *Builder) JwtID(v string) *Builder {
return b.Claim(JwtIDKey, v)
}
func (b *Builder) NotBefore(v time.Time) *Builder {
return b.Claim(NotBeforeKey, v)
}
func (b *Builder) Subject(v string) *Builder {
return b.Claim(SubjectKey, v)
}
// Build creates a new token based on the claims that the builder has received
// so far. If a claim cannot be set, then the method returns a nil Token with
// a en error as a second return value
//
// Once `Build()` is called, all claims are cleared from the Builder, and the
// Builder can be reused to build another token
func (b *Builder) Build() (Token, error) {
b.mu.Lock()
claims := b.claims
b.claims = nil
b.mu.Unlock()
tok := New()
for k, v := range claims {
if err := tok.Set(k, v); err != nil {
return nil, fmt.Errorf(`failed to set claim %q: %w`, k, err)
}
}
return tok, nil
}
+95
View File
@@ -0,0 +1,95 @@
package jwt
import (
jwterrs "github.com/lestrrat-go/jwx/v3/jwt/internal/errors"
)
// ClaimNotFoundError returns the opaque error value that is returned when
// `jwt.Get` fails to find the requested claim.
//
// This value should only be used for comparison using `errors.Is()`.
func ClaimNotFoundError() error {
return jwterrs.ErrClaimNotFound
}
// ClaimAssignmentFailedError returns the opaque error value that is returned
// when `jwt.Get` fails to assign the value to the destination. For example,
// this can happen when the value is a string, but you passed a &int as the
// destination.
//
// This value should only be used for comparison using `errors.Is()`.
func ClaimAssignmentFailedError() error {
return jwterrs.ErrClaimAssignmentFailed
}
// UnknownPayloadTypeError returns the opaque error value that is returned when
// `jwt.Parse` fails due to not being able to deduce the format of
// the incoming buffer.
//
// This value should only be used for comparison using `errors.Is()`.
func UnknownPayloadTypeError() error {
return jwterrs.ErrUnknownPayloadType
}
// ParseError returns the opaque error that is returned from jwt.Parse when
// the input is not a valid JWT.
//
// This value should only be used for comparison using `errors.Is()`.
func ParseError() error {
return jwterrs.ErrParse
}
// ValidateError returns the immutable error used for validation errors
//
// This value should only be used for comparison using `errors.Is()`.
func ValidateError() error {
return jwterrs.ErrValidateDefault
}
// InvalidIssuerError returns the immutable error used when `iss` claim
// is not satisfied
//
// This value should only be used for comparison using `errors.Is()`.
func InvalidIssuerError() error {
return jwterrs.ErrInvalidIssuerDefault
}
// TokenExpiredError returns the immutable error used when `exp` claim
// is not satisfied.
//
// This value should only be used for comparison using `errors.Is()`.
func TokenExpiredError() error {
return jwterrs.ErrTokenExpiredDefault
}
// InvalidIssuedAtError returns the immutable error used when `iat` claim
// is not satisfied
//
// This value should only be used for comparison using `errors.Is()`.
func InvalidIssuedAtError() error {
return jwterrs.ErrInvalidIssuedAtDefault
}
// TokenNotYetValidError returns the immutable error used when `nbf` claim
// is not satisfied
//
// This value should only be used for comparison using `errors.Is()`.
func TokenNotYetValidError() error {
return jwterrs.ErrTokenNotYetValidDefault
}
// InvalidAudienceError returns the immutable error used when `aud` claim
// is not satisfied
//
// This value should only be used for comparison using `errors.Is()`.
func InvalidAudienceError() error {
return jwterrs.ErrInvalidAudienceDefault
}
// MissingRequiredClaimError returns the immutable error used when the claim
// specified by `jwt.IsRequired()` is not present.
//
// This value should only be used for comparison using `errors.Is()`.
func MissingRequiredClaimError() error {
return jwterrs.ErrMissingRequiredClaimDefault
}
+71
View File
@@ -0,0 +1,71 @@
package jwt
import (
"fmt"
"github.com/lestrrat-go/jwx/v3/internal/base64"
"github.com/lestrrat-go/jwx/v3/internal/json"
"github.com/lestrrat-go/jwx/v3/internal/pool"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jws"
"github.com/lestrrat-go/jwx/v3/jws/jwsbb"
)
// signFast reinvents the wheel a bit to avoid the overhead of
// going through the entire jws.Sign() machinery.
func signFast(t Token, alg jwa.SignatureAlgorithm, key any) ([]byte, error) {
algstr := alg.String()
var kid string
if jwkKey, ok := key.(jwk.Key); ok {
if v, ok := jwkKey.KeyID(); ok && v != "" {
kid = v
}
}
// Setup headers
// {"alg":"","typ":"JWT"}
// 1234567890123456789012
want := len(algstr) + 22
// also, if kid != "", we need to add "kid":"$kid"
if kid != "" {
// "kid":""
// 12345689
want += len(kid) + 9
}
hdr := pool.ByteSlice().GetCapacity(want)
hdr = append(hdr, '{', '"', 'a', 'l', 'g', '"', ':', '"')
hdr = append(hdr, algstr...)
hdr = append(hdr, '"')
if kid != "" {
hdr = append(hdr, ',', '"', 'k', 'i', 'd', '"', ':', '"')
hdr = append(hdr, kid...)
hdr = append(hdr, '"')
}
hdr = append(hdr, ',', '"', 't', 'y', 'p', '"', ':', '"', 'J', 'W', 'T', '"', '}')
defer pool.ByteSlice().Put(hdr)
// setup the buffer to sign with
payload, err := json.Marshal(t)
if err != nil {
return nil, fmt.Errorf(`jwt.signFast: failed to marshal token payload: %w`, err)
}
combined := jwsbb.SignBuffer(nil, hdr, payload, base64.DefaultEncoder(), true)
signer, err := jws.SignerFor(alg)
if err != nil {
return nil, fmt.Errorf(`jwt.signFast: failed to get signer for %s: %w`, alg, err)
}
signature, err := signer.Sign(key, combined)
if err != nil {
return nil, fmt.Errorf(`jwt.signFast: failed to sign payload with %s: %w`, alg, err)
}
serialized, err := jwsbb.JoinCompact(nil, hdr, payload, signature, base64.DefaultEncoder(), true)
if err != nil {
return nil, fmt.Errorf("jwt.signFast: failed to join compact: %w", err)
}
return serialized, nil
}
+34
View File
@@ -0,0 +1,34 @@
package jwt
import (
"github.com/lestrrat-go/jwx/v3/transform"
)
// TokenFilter is an interface that allows users to filter JWT claims.
// It provides two methods: Filter and Reject; Filter returns a new token with only
// the claims that match the filter criteria, while Reject returns a new token with
// only the claims that DO NOT match the filter.
//
// EXPERIMENTAL: This API is experimental and its interface and behavior is
// subject to change in future releases. This API is not subject to semver
// compatibility guarantees.
type TokenFilter interface {
Filter(token Token) (Token, error)
Reject(token Token) (Token, error)
}
// StandardClaimsFilter returns a TokenFilter that filters out standard JWT claims.
//
// You can use this filter to create tokens that either only has standard claims
// or only custom claims. If you need to configure the filter more precisely, consider
// using the ClaimNameFilter directly.
func StandardClaimsFilter() TokenFilter {
return stdClaimsFilter
}
var stdClaimsFilter = NewClaimNameFilter(stdClaimNames...)
// NewClaimNameFilter creates a new ClaimNameFilter with the specified claim names.
func NewClaimNameFilter(names ...string) TokenFilter {
return transform.NewNameBasedFilter[Token](names...)
}
+295
View File
@@ -0,0 +1,295 @@
package jwt
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/lestrrat-go/jwx/v3/internal/pool"
"github.com/lestrrat-go/jwx/v3/internal/tokens"
)
// ParseCookie parses a JWT stored in a http.Cookie with the given name.
// If the specified cookie is not found, http.ErrNoCookie is returned.
func ParseCookie(req *http.Request, name string, options ...ParseOption) (Token, error) {
var dst **http.Cookie
for _, option := range options {
switch option.Ident() {
case identCookie{}:
if err := option.Value(&dst); err != nil {
return nil, fmt.Errorf(`jws.ParseCookie: value to option WithCookie must be **http.Cookie: %w`, err)
}
}
}
cookie, err := req.Cookie(name)
if err != nil {
return nil, err
}
tok, err := ParseString(cookie.Value, options...)
if err != nil {
return nil, fmt.Errorf(`jws.ParseCookie: failed to parse token stored in cookie: %w`, err)
}
if dst != nil {
*dst = cookie
}
return tok, nil
}
// ParseHeader parses a JWT stored in a http.Header.
//
// For the header "Authorization", it will strip the prefix "Bearer " and will
// treat the remaining value as a JWT.
func ParseHeader(hdr http.Header, name string, options ...ParseOption) (Token, error) {
key := http.CanonicalHeaderKey(name)
v := strings.TrimSpace(hdr.Get(key))
if v == "" {
return nil, fmt.Errorf(`empty header (%s)`, key)
}
if key == "Authorization" {
// Authorization header is an exception. We strip the "Bearer " from
// the prefix
v = strings.TrimSpace(strings.TrimPrefix(v, "Bearer"))
}
tok, err := ParseString(v, options...)
if err != nil {
return nil, fmt.Errorf(`failed to parse token stored in header (%s): %w`, key, err)
}
return tok, nil
}
// ParseForm parses a JWT stored in a url.Value.
func ParseForm(values url.Values, name string, options ...ParseOption) (Token, error) {
v := strings.TrimSpace(values.Get(name))
if v == "" {
return nil, fmt.Errorf(`empty value (%s)`, name)
}
return ParseString(v, options...)
}
// ParseRequest searches a http.Request object for a JWT token.
//
// Specifying WithHeaderKey() will tell it to search under a specific
// header key. Specifying WithFormKey() will tell it to search under
// a specific form field.
//
// If none of jwt.WithHeaderKey()/jwt.WithCookieKey()/jwt.WithFormKey() is
// used, "Authorization" header will be searched. If any of these options
// are specified, you must explicitly re-enable searching for "Authorization" header
// if you also want to search for it.
//
// # searches for "Authorization"
// jwt.ParseRequest(req)
//
// # searches for "x-my-token" ONLY.
// jwt.ParseRequest(req, jwt.WithHeaderKey("x-my-token"))
//
// # searches for "Authorization" AND "x-my-token"
// jwt.ParseRequest(req, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey("x-my-token"))
//
// Cookies are searched using (http.Request).Cookie(). If you have multiple
// cookies with the same name, and you want to search for a specific one that
// (http.Request).Cookie() would not return, you will need to implement your
// own logic to extract the cookie and use jwt.ParseString().
func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) {
var hdrkeys []string
var formkeys []string
var cookiekeys []string
var parseOptions []ParseOption
for _, option := range options {
switch option.Ident() {
case identHeaderKey{}:
var v string
if err := option.Value(&v); err != nil {
return nil, fmt.Errorf(`jws.ParseRequest: value to option WithHeaderKey must be string: %w`, err)
}
hdrkeys = append(hdrkeys, v)
case identFormKey{}:
var v string
if err := option.Value(&v); err != nil {
return nil, fmt.Errorf(`jws.ParseRequest: value to option WithFormKey must be string: %w`, err)
}
formkeys = append(formkeys, v)
case identCookieKey{}:
var v string
if err := option.Value(&v); err != nil {
return nil, fmt.Errorf(`jws.ParseRequest: value to option WithCookieKey must be string: %w`, err)
}
cookiekeys = append(cookiekeys, v)
default:
parseOptions = append(parseOptions, option)
}
}
if len(hdrkeys) == 0 && len(formkeys) == 0 && len(cookiekeys) == 0 {
hdrkeys = append(hdrkeys, "Authorization")
}
mhdrs := pool.KeyToErrorMap().Get()
defer pool.KeyToErrorMap().Put(mhdrs)
mfrms := pool.KeyToErrorMap().Get()
defer pool.KeyToErrorMap().Put(mfrms)
mcookies := pool.KeyToErrorMap().Get()
defer pool.KeyToErrorMap().Put(mcookies)
for _, hdrkey := range hdrkeys {
// Check presence via a direct map lookup
if _, ok := req.Header[http.CanonicalHeaderKey(hdrkey)]; !ok {
// if non-existent, not error
continue
}
tok, err := ParseHeader(req.Header, hdrkey, parseOptions...)
if err != nil {
mhdrs[hdrkey] = err
continue
}
return tok, nil
}
for _, name := range cookiekeys {
tok, err := ParseCookie(req, name, parseOptions...)
if err != nil {
if err == http.ErrNoCookie {
// not fatal
mcookies[name] = err
}
continue
}
return tok, nil
}
if cl := req.ContentLength; cl > 0 {
if err := req.ParseForm(); err != nil {
return nil, fmt.Errorf(`failed to parse form: %w`, err)
}
}
for _, formkey := range formkeys {
// Check presence via a direct map lookup
if _, ok := req.Form[formkey]; !ok {
// if non-existent, not error
continue
}
tok, err := ParseForm(req.Form, formkey, parseOptions...)
if err != nil {
mfrms[formkey] = err
continue
}
return tok, nil
}
// Everything below is a prelude to error reporting.
var triedHdrs strings.Builder
for i, hdrkey := range hdrkeys {
if i > 0 {
triedHdrs.WriteString(", ")
}
triedHdrs.WriteString(strconv.Quote(hdrkey))
}
var triedForms strings.Builder
for i, formkey := range formkeys {
if i > 0 {
triedForms.WriteString(", ")
}
triedForms.WriteString(strconv.Quote(formkey))
}
var triedCookies strings.Builder
for i, cookiekey := range cookiekeys {
if i > 0 {
triedCookies.WriteString(", ")
}
triedCookies.WriteString(strconv.Quote(cookiekey))
}
var b strings.Builder
b.WriteString(`failed to find a valid token in any location of the request (tried: `)
olen := b.Len()
if triedHdrs.Len() > 0 {
b.WriteString(`header keys: [`)
b.WriteString(triedHdrs.String())
b.WriteByte(tokens.CloseSquareBracket)
}
if triedForms.Len() > 0 {
if b.Len() > olen {
b.WriteString(", ")
}
b.WriteString("form keys: [")
b.WriteString(triedForms.String())
b.WriteByte(tokens.CloseSquareBracket)
}
if triedCookies.Len() > 0 {
if b.Len() > olen {
b.WriteString(", ")
}
b.WriteString("cookie keys: [")
b.WriteString(triedCookies.String())
b.WriteByte(tokens.CloseSquareBracket)
}
b.WriteByte(')')
lmhdrs := len(mhdrs)
lmfrms := len(mfrms)
lmcookies := len(mcookies)
var errors []any
if lmhdrs > 0 || lmfrms > 0 || lmcookies > 0 {
b.WriteString(". Additionally, errors were encountered during attempts to verify using:")
if lmhdrs > 0 {
b.WriteString(" headers: (")
count := 0
for hdrkey, err := range mhdrs {
if count > 0 {
b.WriteString(", ")
}
b.WriteString("[header key: ")
b.WriteString(strconv.Quote(hdrkey))
b.WriteString(", error: %w]")
errors = append(errors, err)
count++
}
b.WriteString(")")
}
if lmcookies > 0 {
count := 0
b.WriteString(" cookies: (")
for cookiekey, err := range mcookies {
if count > 0 {
b.WriteString(", ")
}
b.WriteString("[cookie key: ")
b.WriteString(strconv.Quote(cookiekey))
b.WriteString(", error: %w]")
errors = append(errors, err)
count++
}
}
if lmfrms > 0 {
count := 0
b.WriteString(" forms: (")
for formkey, err := range mfrms {
if count > 0 {
b.WriteString(", ")
}
b.WriteString("[form key: ")
b.WriteString(strconv.Quote(formkey))
b.WriteString(", error: %w]")
errors = append(errors, err)
count++
}
}
}
return nil, fmt.Errorf(b.String(), errors...)
}
+8
View File
@@ -0,0 +1,8 @@
package jwt
import (
"github.com/lestrrat-go/jwx/v3/internal/json"
)
type DecodeCtx = json.DecodeCtx
type TokenWithDecodeCtx = json.DecodeCtxContainer
@@ -0,0 +1,16 @@
load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "errors",
srcs = [
"errors.go",
],
importpath = "github.com/lestrrat-go/jwx/v3/jwt/internal/errors",
visibility = ["//jwt:__subpackages__"],
)
alias(
name = "go_default_library",
actual = ":errors",
visibility = ["//jwt:__subpackages__"],
)
@@ -0,0 +1,185 @@
// Package errors exist to store errors for jwt and openid packages.
//
// It's internal because we don't want to expose _anything_ about these errors
// so users absolutely cannot do anything other than use them as opaque errors.
//
//nolint:revive
package errors
import (
"errors"
"fmt"
)
var (
ErrClaimNotFound = ClaimNotFoundError{}
ErrClaimAssignmentFailed = ClaimAssignmentFailedError{Err: errors.New(`claim assignment failed`)}
ErrUnknownPayloadType = errors.New(`unknown payload type (payload is not JWT?)`)
ErrParse = ParseError{error: errors.New(`jwt.Parse: unknown error`)}
ErrValidateDefault = ValidationError{errors.New(`unknown error`)}
ErrInvalidIssuerDefault = InvalidIssuerError{errors.New(`"iss" not satisfied`)}
ErrTokenExpiredDefault = TokenExpiredError{errors.New(`"exp" not satisfied: token is expired`)}
ErrInvalidIssuedAtDefault = InvalidIssuedAtError{errors.New(`"iat" not satisfied`)}
ErrTokenNotYetValidDefault = TokenNotYetValidError{errors.New(`"nbf" not satisfied: token is not yet valid`)}
ErrInvalidAudienceDefault = InvalidAudienceError{errors.New(`"aud" not satisfied`)}
ErrMissingRequiredClaimDefault = &MissingRequiredClaimError{error: errors.New(`required claim is missing`)}
)
type ClaimNotFoundError struct {
Name string
}
func (e ClaimNotFoundError) Error() string {
// This error message uses "field" instead of "claim" for backwards compatibility,
// but it shuold really be "claim" since it refers to a JWT claim.
return fmt.Sprintf(`field "%s" not found`, e.Name)
}
func (e ClaimNotFoundError) Is(target error) bool {
_, ok := target.(ClaimNotFoundError)
return ok
}
type ClaimAssignmentFailedError struct {
Err error
}
func (e ClaimAssignmentFailedError) Error() string {
// This error message probably should be tweaked, but it is this way
// for backwards compatibility.
return fmt.Sprintf(`failed to assign value to dst: %s`, e.Err.Error())
}
func (e ClaimAssignmentFailedError) Unwrap() error {
return e.Err
}
func (e ClaimAssignmentFailedError) Is(target error) bool {
_, ok := target.(ClaimAssignmentFailedError)
return ok
}
type ParseError struct {
error
}
func (e ParseError) Unwrap() error {
return e.error
}
func (ParseError) Is(err error) bool {
_, ok := err.(ParseError)
return ok
}
func ParseErrorf(prefix, f string, args ...any) error {
return ParseError{fmt.Errorf(prefix+": "+f, args...)}
}
type ValidationError struct {
error
}
func (ValidationError) Is(err error) bool {
_, ok := err.(ValidationError)
return ok
}
func (err ValidationError) Unwrap() error {
return err.error
}
func ValidateErrorf(f string, args ...any) error {
return ValidationError{fmt.Errorf(`jwt.Validate: `+f, args...)}
}
type InvalidIssuerError struct {
error
}
func (err InvalidIssuerError) Is(target error) bool {
_, ok := target.(InvalidIssuerError)
return ok
}
func (err InvalidIssuerError) Unwrap() error {
return err.error
}
func IssuerErrorf(f string, args ...any) error {
return InvalidIssuerError{fmt.Errorf(`"iss" not satisfied: `+f, args...)}
}
type TokenExpiredError struct {
error
}
func (err TokenExpiredError) Is(target error) bool {
_, ok := target.(TokenExpiredError)
return ok
}
func (err TokenExpiredError) Unwrap() error {
return err.error
}
type InvalidIssuedAtError struct {
error
}
func (err InvalidIssuedAtError) Is(target error) bool {
_, ok := target.(InvalidIssuedAtError)
return ok
}
func (err InvalidIssuedAtError) Unwrap() error {
return err.error
}
type TokenNotYetValidError struct {
error
}
func (err TokenNotYetValidError) Is(target error) bool {
_, ok := target.(TokenNotYetValidError)
return ok
}
func (err TokenNotYetValidError) Unwrap() error {
return err.error
}
type InvalidAudienceError struct {
error
}
func (err InvalidAudienceError) Is(target error) bool {
_, ok := target.(InvalidAudienceError)
return ok
}
func (err InvalidAudienceError) Unwrap() error {
return err.error
}
func AudienceErrorf(f string, args ...any) error {
return InvalidAudienceError{fmt.Errorf(`"aud" not satisfied: `+f, args...)}
}
type MissingRequiredClaimError struct {
error
claim string
}
func (err *MissingRequiredClaimError) Is(target error) bool {
err1, ok := target.(*MissingRequiredClaimError)
if !ok {
return false
}
return err1 == ErrMissingRequiredClaimDefault || err1.claim == err.claim
}
func MissingRequiredClaimErrorf(name string) error {
return &MissingRequiredClaimError{claim: name, error: fmt.Errorf(`required claim "%s" is missing`, name)}
}
@@ -0,0 +1,35 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "types",
srcs = [
"date.go",
"string.go",
],
importpath = "github.com/lestrrat-go/jwx/v3/jwt/internal/types",
visibility = ["//jwt:__subpackages__"],
deps = [
"//internal/json",
"//internal/tokens",
],
)
go_test(
name = "types_test",
srcs = [
"date_test.go",
"string_test.go",
],
deps = [
":types",
"//internal/json",
"//jwt",
"@com_github_stretchr_testify//require",
],
)
alias(
name = "go_default_library",
actual = ":types",
visibility = ["//jwt:__subpackages__"],
)
+192
View File
@@ -0,0 +1,192 @@
package types
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/lestrrat-go/jwx/v3/internal/json"
"github.com/lestrrat-go/jwx/v3/internal/tokens"
)
const (
DefaultPrecision uint32 = 0 // second level
MaxPrecision uint32 = 9 // nanosecond level
)
var Pedantic uint32
var ParsePrecision = DefaultPrecision
var FormatPrecision = DefaultPrecision
// NumericDate represents the date format used in the 'nbf' claim
type NumericDate struct {
time.Time
}
func (n *NumericDate) Get() time.Time {
if n == nil {
return (time.Time{}).UTC()
}
return n.Time
}
func intToTime(v any, t *time.Time) bool {
var n int64
switch x := v.(type) {
case int64:
n = x
case int32:
n = int64(x)
case int16:
n = int64(x)
case int8:
n = int64(x)
case int:
n = int64(x)
default:
return false
}
*t = time.Unix(n, 0)
return true
}
func parseNumericString(x string) (time.Time, error) {
var t time.Time // empty time for empty return value
// Only check for the escape hatch if it's the pedantic
// flag is off
if Pedantic != 1 {
// This is an escape hatch for non-conformant providers
// that gives us RFC3339 instead of epoch time
for _, r := range x {
// 0x30 = '0', 0x39 = '9', 0x2E = tokens.Period
if (r >= 0x30 && r <= 0x39) || r == 0x2E {
continue
}
// if it got here, then it probably isn't epoch time
tv, err := time.Parse(time.RFC3339, x)
if err != nil {
return t, fmt.Errorf(`value is not number of seconds since the epoch, and attempt to parse it as RFC3339 timestamp failed: %w`, err)
}
return tv, nil
}
}
var fractional string
whole := x
if i := strings.IndexRune(x, tokens.Period); i > 0 {
if ParsePrecision > 0 && len(x) > i+1 {
fractional = x[i+1:] // everything after the tokens.Period
if int(ParsePrecision) < len(fractional) {
// Remove insignificant digits
fractional = fractional[:int(ParsePrecision)]
}
// Replace missing fractional diits with zeros
for len(fractional) < int(MaxPrecision) {
fractional = fractional + "0"
}
}
whole = x[:i]
}
n, err := strconv.ParseInt(whole, 10, 64)
if err != nil {
return t, fmt.Errorf(`failed to parse whole value %q: %w`, whole, err)
}
var nsecs int64
if fractional != "" {
v, err := strconv.ParseInt(fractional, 10, 64)
if err != nil {
return t, fmt.Errorf(`failed to parse fractional value %q: %w`, fractional, err)
}
nsecs = v
}
return time.Unix(n, nsecs).UTC(), nil
}
func (n *NumericDate) Accept(v any) error {
var t time.Time
switch x := v.(type) {
case float32:
tv, err := parseNumericString(fmt.Sprintf(`%.9f`, x))
if err != nil {
return fmt.Errorf(`failed to accept float32 %.9f: %w`, x, err)
}
t = tv
case float64:
tv, err := parseNumericString(fmt.Sprintf(`%.9f`, x))
if err != nil {
return fmt.Errorf(`failed to accept float32 %.9f: %w`, x, err)
}
t = tv
case json.Number:
tv, err := parseNumericString(x.String())
if err != nil {
return fmt.Errorf(`failed to accept json.Number %q: %w`, x.String(), err)
}
t = tv
case string:
tv, err := parseNumericString(x)
if err != nil {
return fmt.Errorf(`failed to accept string %q: %w`, x, err)
}
t = tv
case time.Time:
t = x
default:
if !intToTime(v, &t) {
return fmt.Errorf(`invalid type %T`, v)
}
}
n.Time = t.UTC()
return nil
}
func (n NumericDate) String() string {
if FormatPrecision == 0 {
return strconv.FormatInt(n.Unix(), 10)
}
// This is cheating, but it's better (easier) than doing floating point math
// We basically munge with strings after formatting an integer value
// for nanoseconds since epoch
s := strconv.FormatInt(n.UnixNano(), 10)
for len(s) < int(MaxPrecision) {
s = "0" + s
}
slwhole := len(s) - int(MaxPrecision)
s = s[:slwhole] + "." + s[slwhole:slwhole+int(FormatPrecision)]
if s[0] == tokens.Period {
s = "0" + s
}
return s
}
// MarshalJSON translates from internal representation to JSON NumericDate
// See https://tools.ietf.org/html/rfc7519#page-6
func (n *NumericDate) MarshalJSON() ([]byte, error) {
if n.IsZero() {
return json.Marshal(nil)
}
return json.Marshal(n.String())
}
func (n *NumericDate) UnmarshalJSON(data []byte) error {
var v any
if err := json.Unmarshal(data, &v); err != nil {
return fmt.Errorf(`failed to unmarshal date: %w`, err)
}
var n2 NumericDate
if err := n2.Accept(v); err != nil {
return fmt.Errorf(`invalid value for NumericDate: %w`, err)
}
*n = n2
return nil
}
@@ -0,0 +1,43 @@
package types
import (
"fmt"
"github.com/lestrrat-go/jwx/v3/internal/json"
)
type StringList []string
func (l StringList) Get() []string {
return []string(l)
}
func (l *StringList) Accept(v any) error {
switch x := v.(type) {
case string:
*l = StringList([]string{x})
case []string:
*l = StringList(x)
case []any:
list := make(StringList, len(x))
for i, e := range x {
if s, ok := e.(string); ok {
list[i] = s
continue
}
return fmt.Errorf(`invalid list element type %T`, e)
}
*l = list
default:
return fmt.Errorf(`invalid type: %T`, v)
}
return nil
}
func (l *StringList) UnmarshalJSON(data []byte) error {
var v any
if err := json.Unmarshal(data, &v); err != nil {
return fmt.Errorf(`failed to unmarshal data: %w`, err)
}
return l.Accept(v)
}
+42
View File
@@ -0,0 +1,42 @@
// Code generated by tools/cmd/genreadfile/main.go. DO NOT EDIT.
package jwt
import (
"fmt"
"io/fs"
"os"
)
type sysFS struct{}
func (sysFS) Open(path string) (fs.File, error) {
return os.Open(path)
}
func ReadFile(path string, options ...ReadFileOption) (Token, error) {
var parseOptions []ParseOption
for _, option := range options {
if po, ok := option.(ParseOption); ok {
parseOptions = append(parseOptions, po)
}
}
var srcFS fs.FS = sysFS{}
for _, option := range options {
switch option.Ident() {
case identFS{}:
if err := option.Value(&srcFS); err != nil {
return nil, fmt.Errorf("failed to set fs.FS: %w", err)
}
}
}
f, err := srcFS.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return ParseReader(f, parseOptions...)
}
+597
View File
@@ -0,0 +1,597 @@
//go:generate ../tools/cmd/genjwt.sh
//go:generate stringer -type=TokenOption -output=token_options_gen.go
// Package jwt implements JSON Web Tokens as described in https://tools.ietf.org/html/rfc7519
package jwt
import (
"bytes"
"fmt"
"io"
"sync/atomic"
"time"
"github.com/lestrrat-go/jwx/v3"
"github.com/lestrrat-go/jwx/v3/internal/json"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jws"
jwterrs "github.com/lestrrat-go/jwx/v3/jwt/internal/errors"
"github.com/lestrrat-go/jwx/v3/jwt/internal/types"
)
var defaultTruncation atomic.Int64
// Settings controls global settings that are specific to JWTs.
func Settings(options ...GlobalOption) {
var flattenAudience bool
var parsePedantic bool
var parsePrecision = types.MaxPrecision + 1 // illegal value, so we can detect nothing was set
var formatPrecision = types.MaxPrecision + 1 // illegal value, so we can detect nothing was set
truncation := time.Duration(-1)
for _, option := range options {
switch option.Ident() {
case identTruncation{}:
if err := option.Value(&truncation); err != nil {
panic(fmt.Sprintf("jwt.Settings: value for WithTruncation must be time.Duration: %s", err))
}
case identFlattenAudience{}:
if err := option.Value(&flattenAudience); err != nil {
panic(fmt.Sprintf("jwt.Settings: value for WithFlattenAudience must be bool: %s", err))
}
case identNumericDateParsePedantic{}:
if err := option.Value(&parsePedantic); err != nil {
panic(fmt.Sprintf("jwt.Settings: value for WithNumericDateParsePedantic must be bool: %s", err))
}
case identNumericDateParsePrecision{}:
var v int
if err := option.Value(&v); err != nil {
panic(fmt.Sprintf("jwt.Settings: value for WithNumericDateParsePrecision must be int: %s", err))
}
// only accept this value if it's in our desired range
if v >= 0 && v <= int(types.MaxPrecision) {
parsePrecision = uint32(v)
}
case identNumericDateFormatPrecision{}:
var v int
if err := option.Value(&v); err != nil {
panic(fmt.Sprintf("jwt.Settings: value for WithNumericDateFormatPrecision must be int: %s", err))
}
// only accept this value if it's in our desired range
if v >= 0 && v <= int(types.MaxPrecision) {
formatPrecision = uint32(v)
}
}
}
if parsePrecision <= types.MaxPrecision { // remember we set default to max + 1
v := atomic.LoadUint32(&types.ParsePrecision)
if v != parsePrecision {
atomic.CompareAndSwapUint32(&types.ParsePrecision, v, parsePrecision)
}
}
if formatPrecision <= types.MaxPrecision { // remember we set default to max + 1
v := atomic.LoadUint32(&types.FormatPrecision)
if v != formatPrecision {
atomic.CompareAndSwapUint32(&types.FormatPrecision, v, formatPrecision)
}
}
{
v := atomic.LoadUint32(&types.Pedantic)
if (v == 1) != parsePedantic {
var newVal uint32
if parsePedantic {
newVal = 1
}
atomic.CompareAndSwapUint32(&types.Pedantic, v, newVal)
}
}
{
defaultOptionsMu.Lock()
if flattenAudience {
defaultOptions.Enable(FlattenAudience)
} else {
defaultOptions.Disable(FlattenAudience)
}
defaultOptionsMu.Unlock()
}
if truncation >= 0 {
defaultTruncation.Store(int64(truncation))
}
}
var registry = json.NewRegistry()
// ParseString calls Parse against a string
func ParseString(s string, options ...ParseOption) (Token, error) {
tok, err := parseBytes([]byte(s), options...)
if err != nil {
return nil, jwterrs.ParseErrorf(`jwt.ParseString`, `failed to parse string: %w`, err)
}
return tok, nil
}
// Parse parses the JWT token payload and creates a new `jwt.Token` object.
// The token must be encoded in JWS compact format, or a raw JSON form of JWT
// without any signatures.
//
// If you need JWE support on top of JWS, you will need to rollout your
// own workaround.
//
// If the token is signed, and you want to verify the payload matches the signature,
// you must pass the jwt.WithKey(alg, key) or jwt.WithKeySet(jwk.Set) option.
// If you do not specify these parameters, no verification will be performed.
//
// During verification, if the JWS headers specify a key ID (`kid`), the
// key used for verification must match the specified ID. If you are somehow
// using a key without a `kid` (which is highly unlikely if you are working
// with a JWT from a well-know provider), you can work around this by modifying
// the `jwk.Key` and setting the `kid` header.
//
// If you also want to assert the validity of the JWT itself (i.e. expiration
// and such), use the `Validate()` function on the returned token, or pass the
// `WithValidate(true)` option. Validate options can also be passed to
// `Parse`
//
// This function takes both ParseOption and ValidateOption types:
// ParseOptions control the parsing behavior, and ValidateOptions are
// passed to `Validate()` when `jwt.WithValidate` is specified.
func Parse(s []byte, options ...ParseOption) (Token, error) {
tok, err := parseBytes(s, options...)
if err != nil {
return nil, jwterrs.ParseErrorf(`jwt.Parse`, `failed to parse token: %w`, err)
}
return tok, nil
}
// ParseInsecure is exactly the same as Parse(), but it disables
// signature verification and token validation.
//
// You cannot override `jwt.WithVerify()` or `jwt.WithValidate()`
// using this function. Providing these options would result in
// an error
func ParseInsecure(s []byte, options ...ParseOption) (Token, error) {
for _, option := range options {
switch option.Ident() {
case identVerify{}, identValidate{}:
return nil, jwterrs.ParseErrorf(`jwt.ParseInsecure`, `jwt.WithVerify() and jwt.WithValidate() may not be specified`)
}
}
options = append(options, WithVerify(false), WithValidate(false))
tok, err := Parse(s, options...)
if err != nil {
return nil, jwterrs.ParseErrorf(`jwt.ParseInsecure`, `failed to parse token: %w`, err)
}
return tok, nil
}
// ParseReader calls Parse against an io.Reader
func ParseReader(src io.Reader, options ...ParseOption) (Token, error) {
// We're going to need the raw bytes regardless. Read it.
data, err := io.ReadAll(src)
if err != nil {
return nil, jwterrs.ParseErrorf(`jwt.ParseReader`, `failed to read from token data source: %w`, err)
}
tok, err := parseBytes(data, options...)
if err != nil {
return nil, jwterrs.ParseErrorf(`jwt.ParseReader`, `failed to parse token: %w`, err)
}
return tok, nil
}
type parseCtx struct {
token Token
validateOpts []ValidateOption
verifyOpts []jws.VerifyOption
localReg *json.Registry
pedantic bool
skipVerification bool
validate bool
withKeyCount int
withKey *withKey // this is used to detect if we have a WithKey option
}
func parseBytes(data []byte, options ...ParseOption) (Token, error) {
var ctx parseCtx
// Validation is turned on by default. You need to specify
// jwt.WithValidate(false) if you want to disable it
ctx.validate = true
// Verification is required (i.e., it is assumed that the incoming
// data is in JWS format) unless the user explicitly asks for
// it to be skipped.
verification := true
var verifyOpts []Option
for _, o := range options {
if v, ok := o.(ValidateOption); ok {
ctx.validateOpts = append(ctx.validateOpts, v)
// context is used for both verification and validation, so we can't just continue
switch o.Ident() {
case identContext{}:
default:
continue
}
}
switch o.Ident() {
case identKey{}:
// it would be nice to be able to detect if ctx.verifyOpts[0]
// is a WithKey option, but unfortunately at that point we have
// already converted the options to a jws option, which means
// we can no longer compare its Ident() to jwt.identKey{}.
// So let's just count this here
ctx.withKeyCount++
if ctx.withKeyCount == 1 {
if err := o.Value(&ctx.withKey); err != nil {
return nil, fmt.Errorf("jws.parseBytes: value for WithKey option must be a *jwt.withKey: %w", err)
}
}
verifyOpts = append(verifyOpts, o)
case identKeySet{}, identVerifyAuto{}, identKeyProvider{}, identBase64Encoder{}, identContext{}:
verifyOpts = append(verifyOpts, o)
case identToken{}:
var token Token
if err := o.Value(&token); err != nil {
return nil, fmt.Errorf("jws.parseBytes: value for WithToken option must be a jwt.Token: %w", err)
}
ctx.token = token
case identPedantic{}:
if err := o.Value(&ctx.pedantic); err != nil {
return nil, fmt.Errorf("jws.parseBytes: value for WithPedantic option must be a bool: %w", err)
}
case identValidate{}:
if err := o.Value(&ctx.validate); err != nil {
return nil, fmt.Errorf("jws.parseBytes: value for WithValidate option must be a bool: %w", err)
}
case identVerify{}:
if err := o.Value(&verification); err != nil {
return nil, fmt.Errorf("jws.parseBytes: value for WithVerify option must be a bool: %w", err)
}
case identTypedClaim{}:
var pair claimPair
if err := o.Value(&pair); err != nil {
return nil, fmt.Errorf("jws.parseBytes: value for WithTypedClaim option must be claimPair: %w", err)
}
if ctx.localReg == nil {
ctx.localReg = json.NewRegistry()
}
ctx.localReg.Register(pair.Name, pair.Value)
}
}
if !verification {
ctx.skipVerification = true
}
lvo := len(verifyOpts)
if lvo == 0 && verification {
return nil, fmt.Errorf(`jwt.Parse: no keys for verification are provided (use jwt.WithVerify(false) to explicitly skip)`)
}
if lvo > 0 {
converted, err := toVerifyOptions(verifyOpts...)
if err != nil {
return nil, fmt.Errorf(`jwt.Parse: failed to convert options into jws.VerifyOption: %w`, err)
}
ctx.verifyOpts = converted
}
data = bytes.TrimSpace(data)
return parse(&ctx, data)
}
const (
_JwsVerifyInvalid = iota
_JwsVerifyDone
_JwsVerifyExpectNested
_JwsVerifySkipped
)
var _ = _JwsVerifyInvalid
func verifyJWS(ctx *parseCtx, payload []byte) ([]byte, int, error) {
lvo := len(ctx.verifyOpts)
if lvo == 0 {
return nil, _JwsVerifySkipped, nil
}
if lvo == 1 && ctx.withKeyCount == 1 {
wk := ctx.withKey
alg, ok := wk.alg.(jwa.SignatureAlgorithm)
if ok && len(wk.options) == 0 {
verified, err := jws.VerifyCompactFast(wk.key, payload, alg)
if err != nil {
return nil, _JwsVerifyDone, err
}
return verified, _JwsVerifyDone, nil
}
}
verifyOpts := append(ctx.verifyOpts, jws.WithCompact())
verified, err := jws.Verify(payload, verifyOpts...)
return verified, _JwsVerifyDone, err
}
// verify parameter exists to make sure that we don't accidentally skip
// over verification just because alg == "" or key == nil or something.
func parse(ctx *parseCtx, data []byte) (Token, error) {
payload := data
const maxDecodeLevels = 2
// If cty = `JWT`, we expect this to be a nested structure
var expectNested bool
OUTER:
for i := range maxDecodeLevels {
switch kind := jwx.GuessFormat(payload); kind {
case jwx.JWT:
if ctx.pedantic {
if expectNested {
return nil, fmt.Errorf(`expected nested encrypted/signed payload, got raw JWT`)
}
}
if i == 0 {
// We were NOT enveloped in other formats
if !ctx.skipVerification {
if _, _, err := verifyJWS(ctx, payload); err != nil {
return nil, err
}
}
}
break OUTER
case jwx.InvalidFormat:
return nil, UnknownPayloadTypeError()
case jwx.UnknownFormat:
// "Unknown" may include invalid JWTs, for example, those who lack "aud"
// claim. We could be pedantic and reject these
if ctx.pedantic {
return nil, fmt.Errorf(`unknown JWT format (pedantic)`)
}
if i == 0 {
// We were NOT enveloped in other formats
if !ctx.skipVerification {
if _, _, err := verifyJWS(ctx, payload); err != nil {
return nil, err
}
}
}
break OUTER
case jwx.JWS:
// Food for thought: This is going to break if you have multiple layers of
// JWS enveloping using different keys. It is highly unlikely use case,
// but it might happen.
// skipVerification should only be set to true by us. It's used
// when we just want to parse the JWT out of a payload
if !ctx.skipVerification {
// nested return value means:
// false (next envelope _may_ need to be processed)
// true (next envelope MUST be processed)
v, state, err := verifyJWS(ctx, payload)
if err != nil {
return nil, err
}
if state != _JwsVerifySkipped {
payload = v
// We only check for cty and typ if the pedantic flag is enabled
if !ctx.pedantic {
continue
}
if state == _JwsVerifyExpectNested {
expectNested = true
continue OUTER
}
// if we're not nested, we found our target. bail out of this loop
break OUTER
}
}
// No verification.
m, err := jws.Parse(data, jws.WithCompact())
if err != nil {
return nil, fmt.Errorf(`invalid jws message: %w`, err)
}
payload = m.Payload()
default:
return nil, fmt.Errorf(`unsupported format (layer: #%d)`, i+1)
}
expectNested = false
}
if ctx.token == nil {
ctx.token = New()
}
if ctx.localReg != nil {
dcToken, ok := ctx.token.(TokenWithDecodeCtx)
if !ok {
return nil, fmt.Errorf(`typed claim was requested, but the token (%T) does not support DecodeCtx`, ctx.token)
}
dc := json.NewDecodeCtx(ctx.localReg)
dcToken.SetDecodeCtx(dc)
defer func() { dcToken.SetDecodeCtx(nil) }()
}
if err := json.Unmarshal(payload, ctx.token); err != nil {
return nil, fmt.Errorf(`failed to parse token: %w`, err)
}
if ctx.validate {
if err := Validate(ctx.token, ctx.validateOpts...); err != nil {
return nil, err
}
}
return ctx.token, nil
}
// Sign is a convenience function to create a signed JWT token serialized in
// compact form.
//
// It accepts either a raw key (e.g. rsa.PrivateKey, ecdsa.PrivateKey, etc)
// or a jwk.Key, and the name of the algorithm that should be used to sign
// the token.
//
// For well-known algorithms with no special considerations (e.g. detached
// payloads, extra protected heders, etc), this function will automatically
// take the fast path and bypass the jws.Sign() machinery, which improves
// performance significantly.
//
// If the key is a jwk.Key and the key contains a key ID (`kid` field),
// then it is added to the protected header generated by the signature
//
// The algorithm specified in the `alg` parameter must be able to support
// the type of key you provided, otherwise an error is returned.
// For convenience `alg` is of type jwa.KeyAlgorithm so you can pass
// the return value of `(jwk.Key).Algorithm()` directly, but in practice
// it must be an instance of jwa.SignatureAlgorithm, otherwise an error
// is returned.
//
// The protected header will also automatically have the `typ` field set
// to the literal value `JWT`, unless you provide a custom value for it
// by jws.WithProtectedHeaders option, that can be passed to `jwt.WithKey“.
func Sign(t Token, options ...SignOption) ([]byte, error) {
// fast path; can only happen if there is exactly one option
if len(options) == 1 && (options[0].Ident() == identKey{}) {
// The option must be a withKey option.
var wk *withKey
if err := options[0].Value(&wk); err == nil {
alg, ok := wk.alg.(jwa.SignatureAlgorithm)
if !ok {
return nil, fmt.Errorf(`jwt.Sign: invalid algorithm type %T. jwa.SignatureAlgorithm is required`, wk.alg)
}
// Check if option contains anything other than alg/key
if len(wk.options) == 0 {
// yay, we have something we can put in the FAST PATH!
return signFast(t, alg, wk.key)
}
// fallthrough
}
// fallthrough
}
var soptions []jws.SignOption
if l := len(options); l > 0 {
// we need to from SignOption to Option because ... reasons
// (todo: when go1.18 prevails, use type parameters
rawoptions := make([]Option, l)
for i, option := range options {
rawoptions[i] = option
}
converted, err := toSignOptions(rawoptions...)
if err != nil {
return nil, fmt.Errorf(`jwt.Sign: failed to convert options into jws.SignOption: %w`, err)
}
soptions = converted
}
return NewSerializer().sign(soptions...).Serialize(t)
}
// Equal compares two JWT tokens. Do not use `reflect.Equal` or the like
// to compare tokens as they will also compare extra detail such as
// sync.Mutex objects used to control concurrent access.
//
// The comparison for values is currently done using a simple equality ("=="),
// except for time.Time, which uses time.Equal after dropping the monotonic
// clock and truncating the values to 1 second accuracy.
//
// if both t1 and t2 are nil, returns true
func Equal(t1, t2 Token) bool {
if t1 == nil && t2 == nil {
return true
}
// we already checked for t1 == t2 == nil, so safe to do this
if t1 == nil || t2 == nil {
return false
}
j1, err := json.Marshal(t1)
if err != nil {
return false
}
j2, err := json.Marshal(t2)
if err != nil {
return false
}
return bytes.Equal(j1, j2)
}
func (t *stdToken) Clone() (Token, error) {
dst := New()
dst.Options().Set(*(t.Options()))
for _, k := range t.Keys() {
var v any
if err := t.Get(k, &v); err != nil {
return nil, fmt.Errorf(`jwt.Clone: failed to get %s: %w`, k, err)
}
if err := dst.Set(k, v); err != nil {
return nil, fmt.Errorf(`jwt.Clone failed to set %s: %w`, k, err)
}
}
return dst, nil
}
type CustomDecoder = json.CustomDecoder
type CustomDecodeFunc = json.CustomDecodeFunc
// RegisterCustomField allows users to specify that a private field
// be decoded as an instance of the specified type. This option has
// a global effect.
//
// For example, suppose you have a custom field `x-birthday`, which
// you want to represent as a string formatted in RFC3339 in JSON,
// but want it back as `time.Time`.
//
// In such case you would register a custom field as follows
//
// jwt.RegisterCustomField(`x-birthday`, time.Time{})
//
// Then you can use a `time.Time` variable to extract the value
// of `x-birthday` field, instead of having to use `any`
// and later convert it to `time.Time`
//
// var bday time.Time
// _ = token.Get(`x-birthday`, &bday)
//
// If you need a more fine-tuned control over the decoding process,
// you can register a `CustomDecoder`. For example, below shows
// how to register a decoder that can parse RFC822 format string:
//
// jwt.RegisterCustomField(`x-birthday`, jwt.CustomDecodeFunc(func(data []byte) (any, error) {
// return time.Parse(time.RFC822, string(data))
// }))
//
// Please note that use of custom fields can be problematic if you
// are using a library that does not implement MarshalJSON/UnmarshalJSON
// and you try to roundtrip from an object to JSON, and then back to an object.
// For example, in the above example, you can _parse_ time values formatted
// in the format specified in RFC822, but when you convert an object into
// JSON, it will be formatted in RFC3339, because that's what `time.Time`
// likes to do. To avoid this, it's always better to use a custom type
// that wraps your desired type (in this case `time.Time`) and implement
// MarshalJSON and UnmashalJSON.
func RegisterCustomField(name string, object any) {
registry.Register(name, object)
}
func getDefaultTruncation() time.Duration {
return time.Duration(defaultTruncation.Load())
}
+332
View File
@@ -0,0 +1,332 @@
package jwt
import (
"context"
"fmt"
"strings"
"time"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwe"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jws"
"github.com/lestrrat-go/option/v2"
)
type identInsecureNoSignature struct{}
type identKey struct{}
type identKeySet struct{}
type identTypedClaim struct{}
type identVerifyAuto struct{}
func toSignOptions(options ...Option) ([]jws.SignOption, error) {
soptions := make([]jws.SignOption, 0, len(options))
for _, option := range options {
switch option.Ident() {
case identInsecureNoSignature{}:
soptions = append(soptions, jws.WithInsecureNoSignature())
case identKey{}:
var wk withKey
if err := option.Value(&wk); err != nil {
return nil, fmt.Errorf(`toSignOtpions: failed to convert option value to withKey: %w`, err)
}
var wksoptions []jws.WithKeySuboption
for _, subopt := range wk.options {
wksopt, ok := subopt.(jws.WithKeySuboption)
if !ok {
return nil, fmt.Errorf(`expected optional arguments in jwt.WithKey to be jws.WithKeySuboption, but got %T`, subopt)
}
wksoptions = append(wksoptions, wksopt)
}
soptions = append(soptions, jws.WithKey(wk.alg, wk.key, wksoptions...))
case identSignOption{}:
var sigOpt jws.SignOption
if err := option.Value(&sigOpt); err != nil {
return nil, fmt.Errorf(`failed to decode SignOption: %w`, err)
}
soptions = append(soptions, sigOpt)
case identBase64Encoder{}:
var enc jws.Base64Encoder
if err := option.Value(&enc); err != nil {
return nil, fmt.Errorf(`failed to decode Base64Encoder: %w`, err)
}
soptions = append(soptions, jws.WithBase64Encoder(enc))
}
}
return soptions, nil
}
func toEncryptOptions(options ...Option) ([]jwe.EncryptOption, error) {
soptions := make([]jwe.EncryptOption, 0, len(options))
for _, option := range options {
switch option.Ident() {
case identKey{}:
var wk withKey
if err := option.Value(&wk); err != nil {
return nil, fmt.Errorf(`toEncryptOptions: failed to convert option value to withKey: %w`, err)
}
var wksoptions []jwe.WithKeySuboption
for _, subopt := range wk.options {
wksopt, ok := subopt.(jwe.WithKeySuboption)
if !ok {
return nil, fmt.Errorf(`expected optional arguments in jwt.WithKey to be jwe.WithKeySuboption, but got %T`, subopt)
}
wksoptions = append(wksoptions, wksopt)
}
soptions = append(soptions, jwe.WithKey(wk.alg, wk.key, wksoptions...))
case identEncryptOption{}:
var encOpt jwe.EncryptOption
if err := option.Value(&encOpt); err != nil {
return nil, fmt.Errorf(`failed to decode EncryptOption: %w`, err)
}
soptions = append(soptions, encOpt)
}
}
return soptions, nil
}
func toVerifyOptions(options ...Option) ([]jws.VerifyOption, error) {
voptions := make([]jws.VerifyOption, 0, len(options))
for _, option := range options {
switch option.Ident() {
case identKey{}:
var wk withKey
if err := option.Value(&wk); err != nil {
return nil, fmt.Errorf(`toVerifyOptions: failed to convert option value to withKey: %w`, err)
}
var wksoptions []jws.WithKeySuboption
for _, subopt := range wk.options {
wksopt, ok := subopt.(jws.WithKeySuboption)
if !ok {
return nil, fmt.Errorf(`expected optional arguments in jwt.WithKey to be jws.WithKeySuboption, but got %T`, subopt)
}
wksoptions = append(wksoptions, wksopt)
}
voptions = append(voptions, jws.WithKey(wk.alg, wk.key, wksoptions...))
case identKeySet{}:
var wks withKeySet
if err := option.Value(&wks); err != nil {
return nil, fmt.Errorf(`failed to convert option value to withKeySet: %w`, err)
}
var wkssoptions []jws.WithKeySetSuboption
for _, subopt := range wks.options {
wkssopt, ok := subopt.(jws.WithKeySetSuboption)
if !ok {
return nil, fmt.Errorf(`expected optional arguments in jwt.WithKey to be jws.WithKeySetSuboption, but got %T`, subopt)
}
wkssoptions = append(wkssoptions, wkssopt)
}
voptions = append(voptions, jws.WithKeySet(wks.set, wkssoptions...))
case identVerifyAuto{}:
var vo jws.VerifyOption
if err := option.Value(&vo); err != nil {
return nil, fmt.Errorf(`failed to decode VerifyOption: %w`, err)
}
voptions = append(voptions, vo)
case identKeyProvider{}:
var kp jws.KeyProvider
if err := option.Value(&kp); err != nil {
return nil, fmt.Errorf(`failed to decode KeyProvider: %w`, err)
}
voptions = append(voptions, jws.WithKeyProvider(kp))
case identBase64Encoder{}:
var enc jws.Base64Encoder
if err := option.Value(&enc); err != nil {
return nil, fmt.Errorf(`failed to decode Base64Encoder: %w`, err)
}
voptions = append(voptions, jws.WithBase64Encoder(enc))
case identContext{}:
var ctx context.Context
if err := option.Value(&ctx); err != nil {
return nil, fmt.Errorf(`failed to decode Context: %w`, err)
}
voptions = append(voptions, jws.WithContext(ctx))
default:
return nil, fmt.Errorf(`invalid jws.VerifyOption %q passed`, `With`+strings.TrimPrefix(fmt.Sprintf(`%T`, option.Ident()), `jws.ident`))
}
}
return voptions, nil
}
type withKey struct {
alg jwa.KeyAlgorithm
key any
options []Option
}
// WithKey is a multipurpose option. It can be used for either jwt.Sign, jwt.Parse (and
// its siblings), and jwt.Serializer methods. For signatures, please see the documentation
// for `jws.WithKey` for more details. For encryption, please see the documentation
// for `jwe.WithKey`.
//
// It is the caller's responsibility to match the suboptions to the operation that they
// are performing. For example, you are not allowed to do this, because the operation
// is to generate a signature, and yet you are passing options for jwe:
//
// jwt.Sign(token, jwt.WithKey(alg, key, jweOptions...))
//
// In the above example, the creation of the option via `jwt.WithKey()` will work, but
// when `jwt.Sign()` is called, the fact that you passed JWE suboptions will be
// detected, and an error will occur.
func WithKey(alg jwa.KeyAlgorithm, key any, suboptions ...Option) SignEncryptParseOption {
return &signEncryptParseOption{option.New(identKey{}, &withKey{
alg: alg,
key: key,
options: suboptions,
})}
}
type withKeySet struct {
set jwk.Set
options []any
}
// WithKeySet forces the Parse method to verify the JWT message
// using one of the keys in the given key set.
//
// Key IDs (`kid`) in the JWS message and the JWK in the given `jwk.Set`
// must match in order for the key to be a candidate to be used for
// verification.
//
// This is for security reasons. If you must disable it, you can do so by
// specifying `jws.WithRequireKid(false)` in the suboptions. But we don't
// recommend it unless you know exactly what the security implications are
//
// When using this option, keys MUST have a proper 'alg' field
// set. This is because we need to know the exact algorithm that
// you (the user) wants to use to verify the token. We do NOT
// trust the token's headers, because they can easily be tampered with.
//
// However, there _is_ a workaround if you do understand the risks
// of allowing a library to automatically choose a signature verification strategy,
// and you do not mind the verification process having to possibly
// attempt using multiple times before succeeding to verify. See
// `jws.InferAlgorithmFromKey` option
//
// If you have only one key in the set, and are sure you want to
// use that key, you can use the `jwt.WithDefaultKey` option.
func WithKeySet(set jwk.Set, options ...any) ParseOption {
return &parseOption{option.New(identKeySet{}, &withKeySet{
set: set,
options: options,
})}
}
// WithIssuer specifies that expected issuer value. If not specified,
// the value of issuer is not verified at all.
func WithIssuer(s string) ValidateOption {
return WithValidator(issuerClaimValueIs(s))
}
// WithSubject specifies that expected subject value. If not specified,
// the value of subject is not verified at all.
func WithSubject(s string) ValidateOption {
return WithValidator(ClaimValueIs(SubjectKey, s))
}
// WithJwtID specifies that expected jti value. If not specified,
// the value of jti is not verified at all.
func WithJwtID(s string) ValidateOption {
return WithValidator(ClaimValueIs(JwtIDKey, s))
}
// WithAudience specifies that expected audience value.
// `Validate()` will return true if one of the values in the `aud` element
// matches this value. If not specified, the value of `aud` is not
// verified at all.
func WithAudience(s string) ValidateOption {
return WithValidator(audienceClaimContainsString(s))
}
// WithClaimValue specifies the expected value for a given claim
func WithClaimValue(name string, v any) ValidateOption {
return WithValidator(ClaimValueIs(name, v))
}
// WithTypedClaim allows a private claim to be parsed into the object type of
// your choice. It works much like the RegisterCustomField, but the effect
// is only applicable to the jwt.Parse function call which receives this option.
//
// While this can be extremely useful, this option should be used with caution:
// There are many caveats that your entire team/user-base needs to be aware of,
// and therefore in general its use is discouraged. Only use it when you know
// what you are doing, and you document its use clearly for others.
//
// First and foremost, this is a "per-object" option. Meaning that given the same
// serialized format, it is possible to generate two objects whose internal
// representations may differ. That is, if you parse one _WITH_ the option,
// and the other _WITHOUT_, their internal representation may completely differ.
// This could potentially lead to problems.
//
// Second, specifying this option will slightly slow down the decoding process
// as it needs to consult multiple definitions sources (global and local), so
// be careful if you are decoding a large number of tokens, as the effects will stack up.
//
// Finally, this option will also NOT work unless the tokens themselves support such
// parsing mechanism. For example, while tokens obtained from `jwt.New()` and
// `openid.New()` will respect this option, if you provide your own custom
// token type, it will need to implement the TokenWithDecodeCtx interface.
func WithTypedClaim(name string, object any) ParseOption {
return &parseOption{option.New(identTypedClaim{}, claimPair{Name: name, Value: object})}
}
// WithRequiredClaim specifies that the claim identified the given name
// must exist in the token. Only the existence of the claim is checked:
// the actual value associated with that field is not checked.
func WithRequiredClaim(name string) ValidateOption {
return WithValidator(IsRequired(name))
}
// WithMaxDelta specifies that given two claims `c1` and `c2` that represent time, the difference in
// time.Duration must be less than equal to the value specified by `d`. If `c1` or `c2` is the
// empty string, the current time (as computed by `time.Now` or the object passed via
// `WithClock()`) is used for the comparison.
//
// `c1` and `c2` are also assumed to be required, therefore not providing either claim in the
// token will result in an error.
//
// Because there is no way of reliably knowing how to parse private claims, we currently only
// support `iat`, `exp`, and `nbf` claims.
//
// If the empty string is passed to c1 or c2, then the current time (as calculated by time.Now() or
// the clock object provided via WithClock()) is used.
//
// For example, in order to specify that `exp` - `iat` should be less than 10*time.Second, you would write
//
// jwt.Validate(token, jwt.WithMaxDelta(10*time.Second, jwt.ExpirationKey, jwt.IssuedAtKey))
//
// If AcceptableSkew of 2 second is specified, the above will return valid for any value of
// `exp` - `iat` between 8 (10-2) and 12 (10+2).
func WithMaxDelta(dur time.Duration, c1, c2 string) ValidateOption {
return WithValidator(MaxDeltaIs(c1, c2, dur))
}
// WithMinDelta is almost exactly the same as WithMaxDelta, but force validation to fail if
// the difference between time claims are less than dur.
//
// For example, in order to specify that `exp` - `iat` should be greater than 10*time.Second, you would write
//
// jwt.Validate(token, jwt.WithMinDelta(10*time.Second, jwt.ExpirationKey, jwt.IssuedAtKey))
//
// The validation would fail if the difference is less than 10 seconds.
func WithMinDelta(dur time.Duration, c1, c2 string) ValidateOption {
return WithValidator(MinDeltaIs(c1, c2, dur))
}
// WithVerifyAuto specifies that the JWS verification should be attempted
// by using the data available in the JWS message. Currently only verification
// method available is to use the keys available in the JWKS URL pointed
// in the `jku` field.
//
// Please read the documentation for `jws.VerifyAuto` for more details.
func WithVerifyAuto(f jwk.Fetcher, options ...jwk.FetchOption) ParseOption {
return &parseOption{option.New(identVerifyAuto{}, jws.WithVerifyAuto(f, options...))}
}
func WithInsecureNoSignature() SignOption {
return &signEncryptParseOption{option.New(identInsecureNoSignature{}, (any)(nil))}
}
+274
View File
@@ -0,0 +1,274 @@
package_name: jwt
output: jwt/options_gen.go
interfaces:
- name: GlobalOption
comment: |
GlobalOption describes an Option that can be passed to `Settings()`.
- name: EncryptOption
comment: |
EncryptOption describes an Option that can be passed to (jwt.Serializer).Encrypt
- name: ParseOption
methods:
- parseOption
- readFileOption
comment: |
ParseOption describes an Option that can be passed to `jwt.Parse()`.
ParseOption also implements ReadFileOption, therefore it may be
safely pass them to `jwt.ReadFile()`
- name: SignOption
comment: |
SignOption describes an Option that can be passed to `jwt.Sign()` or
(jwt.Serializer).Sign
- name: SignParseOption
methods:
- signOption
- parseOption
- readFileOption
comment: |
SignParseOption describes an Option that can be passed to both `jwt.Sign()` or
`jwt.Parse()`
- name: SignEncryptParseOption
methods:
- parseOption
- encryptOption
- readFileOption
- signOption
comment: |
SignEncryptParseOption describes an Option that can be passed to both `jwt.Sign()` or
`jwt.Parse()`
- name: ValidateOption
methods:
- parseOption
- readFileOption
- validateOption
comment: |
ValidateOption describes an Option that can be passed to Validate().
ValidateOption also implements ParseOption, therefore it may be
safely passed to `Parse()` (and thus `jwt.ReadFile()`)
- name: ReadFileOption
comment: |
ReadFileOption is a type of `Option` that can be passed to `jws.ReadFile`
- name: GlobalValidateOption
methods:
- globalOption
- parseOption
- readFileOption
- validateOption
comment: |
GlobalValidateOption describes an Option that can be passed to `jwt.Settings()` and `jwt.Validate()`
options:
- ident: AcceptableSkew
interface: ValidateOption
argument_type: time.Duration
comment: |
WithAcceptableSkew specifies the duration in which exp, iat and nbf
claims may differ by. This value should be positive
- ident: Truncation
interface: GlobalValidateOption
argument_type: time.Duration
comment: |
WithTruncation specifies the amount that should be used when
truncating time values used during time-based validation routines,
and by default this is disabled.
In v2 of this library, time values were truncated down to second accuracy, i.e.
1.0000001 seconds is truncated to 1 second. To restore this behavior, set
this value to `time.Second`
Since v3, this option can be passed to `jwt.Settings()` to set the truncation
value globally, as well as per invocation of `jwt.Validate()`
- ident: Clock
interface: ValidateOption
argument_type: Clock
comment: |
WithClock specifies the `Clock` to be used when verifying
exp, iat and nbf claims.
- ident: Context
interface: ValidateOption
argument_type: context.Context
comment: |
WithContext allows you to specify a context.Context object to be used
with `jwt.Validate()` option.
Please be aware that in the next major release of this library,
`jwt.Validate()`'s signature will change to include an explicit
`context.Context` object.
- ident: ResetValidators
interface: ValidateOption
argument_type: bool
comment: |
WithResetValidators specifies that the default validators should be
reset before applying the custom validators. By default `jwt.Validate()`
checks for the validity of JWT by checking `exp`, `nbf`, and `iat`, even
when you specify more validators through other options.
You SHOULD NOT use this option unless you know exactly what you are doing,
as this will pose significant security issues when used incorrectly.
Using this option with the value `true` will remove all default checks,
and will expect you to specify validators as options. This is useful when you
want to skip the default validators and only use specific validators, such as
for https://openid.net/specs/openid-connect-rpinitiated-1_0.html, where
the token could be accepted even if the token is expired.
If you set this option to true and you do not specify any validators,
`jwt.Validate()` will return an error.
The default value is `false` (`iat`, `exp`, and `nbf` are automatically checked).
- ident: FlattenAudience
interface: GlobalOption
argument_type: bool
comment: |
WithFlattenAudience specifies the the `jwt.FlattenAudience` option on
every token defaults to enabled. You can still disable this on a per-object
basis using the `jwt.Options().Disable(jwt.FlattenAudience)` method call.
See the documentation for `jwt.TokenOptionSet`, `(jwt.Token).Options`, and
`jwt.FlattenAudience` for more details
- ident: FormKey
interface: ParseOption
argument_type: string
comment: |
WithFormKey is used to specify header keys to search for tokens.
While the type system allows this option to be passed to jwt.Parse() directly,
doing so will have no effect. Only use it for HTTP request parsing functions
- ident: HeaderKey
interface: ParseOption
argument_type: string
comment: |
WithHeaderKey is used to specify header keys to search for tokens.
While the type system allows this option to be passed to `jwt.Parse()` directly,
doing so will have no effect. Only use it for HTTP request parsing functions
- ident: Cookie
interface: ParseOption
argument_type: '**http.Cookie'
comment: |
WithCookie is used to specify a variable to store the cookie used when `jwt.ParseCookie()`
is called. This allows you to inspect the cookie for additional information after a successful
parsing of the JWT token stored in the cookie.
While the type system allows this option to be passed to `jwt.Parse()` directly,
doing so will have no effect. Only use it for HTTP request parsing functions
- ident: CookieKey
interface: ParseOption
argument_type: string
comment: |
WithCookieKey is used to specify cookie keys to search for tokens.
While the type system allows this option to be passed to `jwt.Parse()` directly,
doing so will have no effect. Only use it for HTTP request parsing functions
- ident: Token
interface: ParseOption
argument_type: Token
comment: |
WithToken specifies the token instance in which the resulting JWT is stored
when parsing JWT tokens
- ident: Validate
interface: ParseOption
argument_type: bool
comment: |
WithValidate is passed to `Parse()` method to denote that the
validation of the JWT token should be performed (or not) after
a successful parsing of the incoming payload.
This option is enabled by default.
If you would like disable validation,
you must use `jwt.WithValidate(false)` or use `jwt.ParseInsecure()`
- ident: Verify
interface: ParseOption
argument_type: bool
comment: |
WithVerify is passed to `Parse()` method to denote that the
signature verification should be performed after a successful
deserialization of the incoming payload.
This option is enabled by default.
If you do not provide any verification key sources, `jwt.Parse()`
would return an error.
If you would like to only parse the JWT payload and not verify it,
you must use `jwt.WithVerify(false)` or use `jwt.ParseInsecure()`
- ident: KeyProvider
interface: ParseOption
argument_type: jws.KeyProvider
comment: |
WithKeyProvider allows users to specify an object to provide keys to
sign/verify tokens using arbitrary code. Please read the documentation
for `jws.KeyProvider` in the `jws` package for details on how this works.
- ident: Pedantic
interface: ParseOption
argument_type: bool
comment: |
WithPedantic enables pedantic mode for parsing JWTs. Currently this only
applies to checking for the correct `typ` and/or `cty` when necessary.
- ident: EncryptOption
interface: EncryptOption
argument_type: jwe.EncryptOption
comment: |
WithEncryptOption provides an escape hatch for cases where extra options to
`(jws.Serializer).Encrypt()` must be specified when using `jwt.Sign()`. Normally you do not
need to use this.
- ident: SignOption
interface: SignOption
argument_type: jws.SignOption
comment: |
WithSignOption provides an escape hatch for cases where extra options to
`jws.Sign()` must be specified when using `jwt.Sign()`. Normally you do not
need to use this.
- ident: Validator
interface: ValidateOption
argument_type: Validator
comment: |
WithValidator validates the token with the given Validator.
For example, in order to validate tokens that are only valid during August, you would write
validator := jwt.ValidatorFunc(func(_ context.Context, t jwt.Token) error {
if time.Now().Month() != 8 {
return fmt.Errorf(`tokens are only valid during August!`)
}
return nil
})
err := jwt.Validate(token, jwt.WithValidator(validator))
- ident: FS
interface: ReadFileOption
argument_type: fs.FS
comment: |
WithFS specifies the source `fs.FS` object to read the file from.
- ident: NumericDateParsePrecision
interface: GlobalOption
argument_type: int
comment: |
WithNumericDateParsePrecision sets the precision up to which the
library uses to parse fractional dates found in the numeric date
fields. Default is 0 (second, no fractions), max is 9 (nanosecond)
- ident: NumericDateFormatPrecision
interface: GlobalOption
argument_type: int
comment: |
WithNumericDateFormatPrecision sets the precision up to which the
library uses to format fractional dates found in the numeric date
fields. Default is 0 (second, no fractions), max is 9 (nanosecond)
- ident: NumericDateParsePedantic
interface: GlobalOption
argument_type: bool
comment: |
WithNumericDateParsePedantic specifies if the parser should behave
in a pedantic manner when parsing numeric dates. Normally this library
attempts to interpret timestamps as a numeric value representing
number of seconds (with an optional fractional part), but if that fails
it tries to parse using a RFC3339 parser. This allows us to parse
payloads from non-conforming servers.
However, when you set WithNumericDateParePedantic to `true`, the
RFC3339 parser is not tried, and we expect a numeric value strictly
- ident: Base64Encoder
interface: SignParseOption
argument_type: jws.Base64Encoder
comment: |
WithBase64Encoder specifies the base64 encoder to use for signing
tokens and verifying JWS signatures.
+495
View File
@@ -0,0 +1,495 @@
// Code generated by tools/cmd/genoptions/main.go. DO NOT EDIT.
package jwt
import (
"context"
"io/fs"
"net/http"
"time"
"github.com/lestrrat-go/jwx/v3/jwe"
"github.com/lestrrat-go/jwx/v3/jws"
"github.com/lestrrat-go/option/v2"
)
type Option = option.Interface
// EncryptOption describes an Option that can be passed to (jwt.Serializer).Encrypt
type EncryptOption interface {
Option
encryptOption()
}
type encryptOption struct {
Option
}
func (*encryptOption) encryptOption() {}
// GlobalOption describes an Option that can be passed to `Settings()`.
type GlobalOption interface {
Option
globalOption()
}
type globalOption struct {
Option
}
func (*globalOption) globalOption() {}
// GlobalValidateOption describes an Option that can be passed to `jwt.Settings()` and `jwt.Validate()`
type GlobalValidateOption interface {
Option
globalOption()
parseOption()
readFileOption()
validateOption()
}
type globalValidateOption struct {
Option
}
func (*globalValidateOption) globalOption() {}
func (*globalValidateOption) parseOption() {}
func (*globalValidateOption) readFileOption() {}
func (*globalValidateOption) validateOption() {}
// ParseOption describes an Option that can be passed to `jwt.Parse()`.
// ParseOption also implements ReadFileOption, therefore it may be
// safely pass them to `jwt.ReadFile()`
type ParseOption interface {
Option
parseOption()
readFileOption()
}
type parseOption struct {
Option
}
func (*parseOption) parseOption() {}
func (*parseOption) readFileOption() {}
// ReadFileOption is a type of `Option` that can be passed to `jws.ReadFile`
type ReadFileOption interface {
Option
readFileOption()
}
type readFileOption struct {
Option
}
func (*readFileOption) readFileOption() {}
// SignEncryptParseOption describes an Option that can be passed to both `jwt.Sign()` or
// `jwt.Parse()`
type SignEncryptParseOption interface {
Option
parseOption()
encryptOption()
readFileOption()
signOption()
}
type signEncryptParseOption struct {
Option
}
func (*signEncryptParseOption) parseOption() {}
func (*signEncryptParseOption) encryptOption() {}
func (*signEncryptParseOption) readFileOption() {}
func (*signEncryptParseOption) signOption() {}
// SignOption describes an Option that can be passed to `jwt.Sign()` or
// (jwt.Serializer).Sign
type SignOption interface {
Option
signOption()
}
type signOption struct {
Option
}
func (*signOption) signOption() {}
// SignParseOption describes an Option that can be passed to both `jwt.Sign()` or
// `jwt.Parse()`
type SignParseOption interface {
Option
signOption()
parseOption()
readFileOption()
}
type signParseOption struct {
Option
}
func (*signParseOption) signOption() {}
func (*signParseOption) parseOption() {}
func (*signParseOption) readFileOption() {}
// ValidateOption describes an Option that can be passed to Validate().
// ValidateOption also implements ParseOption, therefore it may be
// safely passed to `Parse()` (and thus `jwt.ReadFile()`)
type ValidateOption interface {
Option
parseOption()
readFileOption()
validateOption()
}
type validateOption struct {
Option
}
func (*validateOption) parseOption() {}
func (*validateOption) readFileOption() {}
func (*validateOption) validateOption() {}
type identAcceptableSkew struct{}
type identBase64Encoder struct{}
type identClock struct{}
type identContext struct{}
type identCookie struct{}
type identCookieKey struct{}
type identEncryptOption struct{}
type identFS struct{}
type identFlattenAudience struct{}
type identFormKey struct{}
type identHeaderKey struct{}
type identKeyProvider struct{}
type identNumericDateFormatPrecision struct{}
type identNumericDateParsePedantic struct{}
type identNumericDateParsePrecision struct{}
type identPedantic struct{}
type identResetValidators struct{}
type identSignOption struct{}
type identToken struct{}
type identTruncation struct{}
type identValidate struct{}
type identValidator struct{}
type identVerify struct{}
func (identAcceptableSkew) String() string {
return "WithAcceptableSkew"
}
func (identBase64Encoder) String() string {
return "WithBase64Encoder"
}
func (identClock) String() string {
return "WithClock"
}
func (identContext) String() string {
return "WithContext"
}
func (identCookie) String() string {
return "WithCookie"
}
func (identCookieKey) String() string {
return "WithCookieKey"
}
func (identEncryptOption) String() string {
return "WithEncryptOption"
}
func (identFS) String() string {
return "WithFS"
}
func (identFlattenAudience) String() string {
return "WithFlattenAudience"
}
func (identFormKey) String() string {
return "WithFormKey"
}
func (identHeaderKey) String() string {
return "WithHeaderKey"
}
func (identKeyProvider) String() string {
return "WithKeyProvider"
}
func (identNumericDateFormatPrecision) String() string {
return "WithNumericDateFormatPrecision"
}
func (identNumericDateParsePedantic) String() string {
return "WithNumericDateParsePedantic"
}
func (identNumericDateParsePrecision) String() string {
return "WithNumericDateParsePrecision"
}
func (identPedantic) String() string {
return "WithPedantic"
}
func (identResetValidators) String() string {
return "WithResetValidators"
}
func (identSignOption) String() string {
return "WithSignOption"
}
func (identToken) String() string {
return "WithToken"
}
func (identTruncation) String() string {
return "WithTruncation"
}
func (identValidate) String() string {
return "WithValidate"
}
func (identValidator) String() string {
return "WithValidator"
}
func (identVerify) String() string {
return "WithVerify"
}
// WithAcceptableSkew specifies the duration in which exp, iat and nbf
// claims may differ by. This value should be positive
func WithAcceptableSkew(v time.Duration) ValidateOption {
return &validateOption{option.New(identAcceptableSkew{}, v)}
}
// WithBase64Encoder specifies the base64 encoder to use for signing
// tokens and verifying JWS signatures.
func WithBase64Encoder(v jws.Base64Encoder) SignParseOption {
return &signParseOption{option.New(identBase64Encoder{}, v)}
}
// WithClock specifies the `Clock` to be used when verifying
// exp, iat and nbf claims.
func WithClock(v Clock) ValidateOption {
return &validateOption{option.New(identClock{}, v)}
}
// WithContext allows you to specify a context.Context object to be used
// with `jwt.Validate()` option.
//
// Please be aware that in the next major release of this library,
// `jwt.Validate()`'s signature will change to include an explicit
// `context.Context` object.
func WithContext(v context.Context) ValidateOption {
return &validateOption{option.New(identContext{}, v)}
}
// WithCookie is used to specify a variable to store the cookie used when `jwt.ParseCookie()`
// is called. This allows you to inspect the cookie for additional information after a successful
// parsing of the JWT token stored in the cookie.
//
// While the type system allows this option to be passed to `jwt.Parse()` directly,
// doing so will have no effect. Only use it for HTTP request parsing functions
func WithCookie(v **http.Cookie) ParseOption {
return &parseOption{option.New(identCookie{}, v)}
}
// WithCookieKey is used to specify cookie keys to search for tokens.
//
// While the type system allows this option to be passed to `jwt.Parse()` directly,
// doing so will have no effect. Only use it for HTTP request parsing functions
func WithCookieKey(v string) ParseOption {
return &parseOption{option.New(identCookieKey{}, v)}
}
// WithEncryptOption provides an escape hatch for cases where extra options to
// `(jws.Serializer).Encrypt()` must be specified when using `jwt.Sign()`. Normally you do not
// need to use this.
func WithEncryptOption(v jwe.EncryptOption) EncryptOption {
return &encryptOption{option.New(identEncryptOption{}, v)}
}
// WithFS specifies the source `fs.FS` object to read the file from.
func WithFS(v fs.FS) ReadFileOption {
return &readFileOption{option.New(identFS{}, v)}
}
// WithFlattenAudience specifies the the `jwt.FlattenAudience` option on
// every token defaults to enabled. You can still disable this on a per-object
// basis using the `jwt.Options().Disable(jwt.FlattenAudience)` method call.
//
// See the documentation for `jwt.TokenOptionSet`, `(jwt.Token).Options`, and
// `jwt.FlattenAudience` for more details
func WithFlattenAudience(v bool) GlobalOption {
return &globalOption{option.New(identFlattenAudience{}, v)}
}
// WithFormKey is used to specify header keys to search for tokens.
//
// While the type system allows this option to be passed to jwt.Parse() directly,
// doing so will have no effect. Only use it for HTTP request parsing functions
func WithFormKey(v string) ParseOption {
return &parseOption{option.New(identFormKey{}, v)}
}
// WithHeaderKey is used to specify header keys to search for tokens.
//
// While the type system allows this option to be passed to `jwt.Parse()` directly,
// doing so will have no effect. Only use it for HTTP request parsing functions
func WithHeaderKey(v string) ParseOption {
return &parseOption{option.New(identHeaderKey{}, v)}
}
// WithKeyProvider allows users to specify an object to provide keys to
// sign/verify tokens using arbitrary code. Please read the documentation
// for `jws.KeyProvider` in the `jws` package for details on how this works.
func WithKeyProvider(v jws.KeyProvider) ParseOption {
return &parseOption{option.New(identKeyProvider{}, v)}
}
// WithNumericDateFormatPrecision sets the precision up to which the
// library uses to format fractional dates found in the numeric date
// fields. Default is 0 (second, no fractions), max is 9 (nanosecond)
func WithNumericDateFormatPrecision(v int) GlobalOption {
return &globalOption{option.New(identNumericDateFormatPrecision{}, v)}
}
// WithNumericDateParsePedantic specifies if the parser should behave
// in a pedantic manner when parsing numeric dates. Normally this library
// attempts to interpret timestamps as a numeric value representing
// number of seconds (with an optional fractional part), but if that fails
// it tries to parse using a RFC3339 parser. This allows us to parse
// payloads from non-conforming servers.
//
// However, when you set WithNumericDateParePedantic to `true`, the
// RFC3339 parser is not tried, and we expect a numeric value strictly
func WithNumericDateParsePedantic(v bool) GlobalOption {
return &globalOption{option.New(identNumericDateParsePedantic{}, v)}
}
// WithNumericDateParsePrecision sets the precision up to which the
// library uses to parse fractional dates found in the numeric date
// fields. Default is 0 (second, no fractions), max is 9 (nanosecond)
func WithNumericDateParsePrecision(v int) GlobalOption {
return &globalOption{option.New(identNumericDateParsePrecision{}, v)}
}
// WithPedantic enables pedantic mode for parsing JWTs. Currently this only
// applies to checking for the correct `typ` and/or `cty` when necessary.
func WithPedantic(v bool) ParseOption {
return &parseOption{option.New(identPedantic{}, v)}
}
// WithResetValidators specifies that the default validators should be
// reset before applying the custom validators. By default `jwt.Validate()`
// checks for the validity of JWT by checking `exp`, `nbf`, and `iat`, even
// when you specify more validators through other options.
//
// You SHOULD NOT use this option unless you know exactly what you are doing,
// as this will pose significant security issues when used incorrectly.
//
// Using this option with the value `true` will remove all default checks,
// and will expect you to specify validators as options. This is useful when you
// want to skip the default validators and only use specific validators, such as
// for https://openid.net/specs/openid-connect-rpinitiated-1_0.html, where
// the token could be accepted even if the token is expired.
//
// If you set this option to true and you do not specify any validators,
// `jwt.Validate()` will return an error.
//
// The default value is `false` (`iat`, `exp`, and `nbf` are automatically checked).
func WithResetValidators(v bool) ValidateOption {
return &validateOption{option.New(identResetValidators{}, v)}
}
// WithSignOption provides an escape hatch for cases where extra options to
// `jws.Sign()` must be specified when using `jwt.Sign()`. Normally you do not
// need to use this.
func WithSignOption(v jws.SignOption) SignOption {
return &signOption{option.New(identSignOption{}, v)}
}
// WithToken specifies the token instance in which the resulting JWT is stored
// when parsing JWT tokens
func WithToken(v Token) ParseOption {
return &parseOption{option.New(identToken{}, v)}
}
// WithTruncation specifies the amount that should be used when
// truncating time values used during time-based validation routines,
// and by default this is disabled.
//
// In v2 of this library, time values were truncated down to second accuracy, i.e.
// 1.0000001 seconds is truncated to 1 second. To restore this behavior, set
// this value to `time.Second`
//
// Since v3, this option can be passed to `jwt.Settings()` to set the truncation
// value globally, as well as per invocation of `jwt.Validate()`
func WithTruncation(v time.Duration) GlobalValidateOption {
return &globalValidateOption{option.New(identTruncation{}, v)}
}
// WithValidate is passed to `Parse()` method to denote that the
// validation of the JWT token should be performed (or not) after
// a successful parsing of the incoming payload.
//
// This option is enabled by default.
//
// If you would like disable validation,
// you must use `jwt.WithValidate(false)` or use `jwt.ParseInsecure()`
func WithValidate(v bool) ParseOption {
return &parseOption{option.New(identValidate{}, v)}
}
// WithValidator validates the token with the given Validator.
//
// For example, in order to validate tokens that are only valid during August, you would write
//
// validator := jwt.ValidatorFunc(func(_ context.Context, t jwt.Token) error {
// if time.Now().Month() != 8 {
// return fmt.Errorf(`tokens are only valid during August!`)
// }
// return nil
// })
// err := jwt.Validate(token, jwt.WithValidator(validator))
func WithValidator(v Validator) ValidateOption {
return &validateOption{option.New(identValidator{}, v)}
}
// WithVerify is passed to `Parse()` method to denote that the
// signature verification should be performed after a successful
// deserialization of the incoming payload.
//
// This option is enabled by default.
//
// If you do not provide any verification key sources, `jwt.Parse()`
// would return an error.
//
// If you would like to only parse the JWT payload and not verify it,
// you must use `jwt.WithVerify(false)` or use `jwt.ParseInsecure()`
func WithVerify(v bool) ParseOption {
return &parseOption{option.New(identVerify{}, v)}
}
+264
View File
@@ -0,0 +1,264 @@
package jwt
import (
"fmt"
"github.com/lestrrat-go/jwx/v3/internal/json"
"github.com/lestrrat-go/jwx/v3/jwe"
"github.com/lestrrat-go/jwx/v3/jws"
)
type SerializeCtx interface {
Step() int
Nested() bool
}
type serializeCtx struct {
step int
nested bool
}
func (ctx *serializeCtx) Step() int {
return ctx.step
}
func (ctx *serializeCtx) Nested() bool {
return ctx.nested
}
type SerializeStep interface {
Serialize(SerializeCtx, any) (any, error)
}
// errStep is always an error. used to indicate that a method like
// serializer.Sign or Encrypt already errored out on configuration
type errStep struct {
err error
}
func (e errStep) Serialize(_ SerializeCtx, _ any) (any, error) {
return nil, e.err
}
// Serializer is a generic serializer for JWTs. Whereas other convenience
// functions can only do one thing (such as generate a JWS signed JWT),
// Using this construct you can serialize the token however you want.
//
// By default, the serializer only marshals the token into a JSON payload.
// You must set up the rest of the steps that should be taken by the
// serializer.
//
// For example, to marshal the token into JSON, then apply JWS and JWE
// in that order, you would do:
//
// serialized, err := jwt.NewSerializer().
// Sign(jwa.RS256, key).
// Encrypt(jwe.WithEncryptOption(jwe.WithKey(jwa.RSA_OAEP(), publicKey))).
// Serialize(token)
//
// The `jwt.Sign()` function is equivalent to
//
// serialized, err := jwt.NewSerializer().
// Sign(...args...).
// Serialize(token)
type Serializer struct {
steps []SerializeStep
}
// NewSerializer creates a new empty serializer.
func NewSerializer() *Serializer {
return &Serializer{}
}
// Reset clears all of the registered steps.
func (s *Serializer) Reset() *Serializer {
s.steps = nil
return s
}
// Step adds a new Step to the serialization process
func (s *Serializer) Step(step SerializeStep) *Serializer {
s.steps = append(s.steps, step)
return s
}
type jsonSerializer struct{}
func (jsonSerializer) Serialize(_ SerializeCtx, v any) (any, error) {
token, ok := v.(Token)
if !ok {
return nil, fmt.Errorf(`invalid input: expected jwt.Token`)
}
buf, err := json.Marshal(token)
if err != nil {
return nil, fmt.Errorf(`failed to serialize as JSON: %w`, err)
}
return buf, nil
}
type genericHeader interface {
Get(string, any) error
Set(string, any) error
Has(string) bool
}
func setTypeOrCty(ctx SerializeCtx, hdrs genericHeader) error {
// cty and typ are common between JWE/JWS, so we don't use
// the constants in jws/jwe package here
const typKey = `typ`
const ctyKey = `cty`
if ctx.Step() == 1 {
// We are executed immediately after json marshaling
if !hdrs.Has(typKey) {
if err := hdrs.Set(typKey, `JWT`); err != nil {
return fmt.Errorf(`failed to set %s key to "JWT": %w`, typKey, err)
}
}
} else {
if ctx.Nested() {
// If this is part of a nested sequence, we should set cty = 'JWT'
// https://datatracker.ietf.org/doc/html/rfc7519#section-5.2
if err := hdrs.Set(ctyKey, `JWT`); err != nil {
return fmt.Errorf(`failed to set %s key to "JWT": %w`, ctyKey, err)
}
}
}
return nil
}
type jwsSerializer struct {
options []jws.SignOption
}
func (s *jwsSerializer) Serialize(ctx SerializeCtx, v any) (any, error) {
payload, ok := v.([]byte)
if !ok {
return nil, fmt.Errorf(`expected []byte as input`)
}
for _, option := range s.options {
var pc interface{ Protected(jws.Headers) jws.Headers }
if err := option.Value(&pc); err != nil {
continue
}
hdrs := pc.Protected(jws.NewHeaders())
if err := setTypeOrCty(ctx, hdrs); err != nil {
return nil, err // this is already wrapped
}
// JWTs MUST NOT use b64 = false
// https://datatracker.ietf.org/doc/html/rfc7797#section-7
var b64 bool
if err := hdrs.Get("b64", &b64); err == nil {
if !b64 { // b64 = false
return nil, fmt.Errorf(`b64 cannot be false for JWTs`)
}
}
}
return jws.Sign(payload, s.options...)
}
func (s *Serializer) Sign(options ...SignOption) *Serializer {
var soptions []jws.SignOption
if l := len(options); l > 0 {
// we need to from SignOption to Option because ... reasons
// (todo: when go1.18 prevails, use type parameters
rawoptions := make([]Option, l)
for i, option := range options {
rawoptions[i] = option
}
converted, err := toSignOptions(rawoptions...)
if err != nil {
return s.Step(errStep{fmt.Errorf(`(jwt.Serializer).Sign: failed to convert options into jws.SignOption: %w`, err)})
}
soptions = converted
}
return s.sign(soptions...)
}
func (s *Serializer) sign(options ...jws.SignOption) *Serializer {
return s.Step(&jwsSerializer{
options: options,
})
}
type jweSerializer struct {
options []jwe.EncryptOption
}
func (s *jweSerializer) Serialize(ctx SerializeCtx, v any) (any, error) {
payload, ok := v.([]byte)
if !ok {
return nil, fmt.Errorf(`expected []byte as input`)
}
hdrs := jwe.NewHeaders()
if err := setTypeOrCty(ctx, hdrs); err != nil {
return nil, err // this is already wrapped
}
options := append([]jwe.EncryptOption{jwe.WithMergeProtectedHeaders(true), jwe.WithProtectedHeaders(hdrs)}, s.options...)
return jwe.Encrypt(payload, options...)
}
// Encrypt specifies the JWT to be serialized as an encrypted payload.
//
// One notable difference between this method and `jwe.Encrypt()` is that
// while `jwe.Encrypt()` OVERWRITES the previous headers when `jwe.WithProtectedHeaders()`
// is provided, this method MERGES them. This is due to the fact that we
// MUST add some extra headers to construct a proper JWE message.
// Be careful when you pass multiple `jwe.EncryptOption`s.
func (s *Serializer) Encrypt(options ...EncryptOption) *Serializer {
var eoptions []jwe.EncryptOption
if l := len(options); l > 0 {
// we need to from SignOption to Option because ... reasons
// (todo: when go1.18 prevails, use type parameters
rawoptions := make([]Option, l)
for i, option := range options {
rawoptions[i] = option
}
converted, err := toEncryptOptions(rawoptions...)
if err != nil {
return s.Step(errStep{fmt.Errorf(`(jwt.Serializer).Encrypt: failed to convert options into jwe.EncryptOption: %w`, err)})
}
eoptions = converted
}
return s.encrypt(eoptions...)
}
func (s *Serializer) encrypt(options ...jwe.EncryptOption) *Serializer {
return s.Step(&jweSerializer{
options: options,
})
}
func (s *Serializer) Serialize(t Token) ([]byte, error) {
steps := make([]SerializeStep, len(s.steps)+1)
steps[0] = jsonSerializer{}
for i, step := range s.steps {
steps[i+1] = step
}
var ctx serializeCtx
ctx.nested = len(s.steps) > 1
var payload any = t
for i, step := range steps {
ctx.step = i
v, err := step.Serialize(&ctx, payload)
if err != nil {
return nil, fmt.Errorf(`failed to serialize token at step #%d: %w`, i+1, err)
}
payload = v
}
res, ok := payload.([]byte)
if !ok {
return nil, fmt.Errorf(`invalid serialization produced`)
}
return res, nil
}
+635
View File
@@ -0,0 +1,635 @@
// Code generated by tools/cmd/genjwt/main.go. DO NOT EDIT.
package jwt
import (
"bytes"
"fmt"
"sort"
"sync"
"time"
"github.com/lestrrat-go/blackmagic"
"github.com/lestrrat-go/jwx/v3/internal/json"
"github.com/lestrrat-go/jwx/v3/internal/pool"
"github.com/lestrrat-go/jwx/v3/internal/tokens"
jwterrs "github.com/lestrrat-go/jwx/v3/jwt/internal/errors"
"github.com/lestrrat-go/jwx/v3/jwt/internal/types"
)
const (
AudienceKey = "aud"
ExpirationKey = "exp"
IssuedAtKey = "iat"
IssuerKey = "iss"
JwtIDKey = "jti"
NotBeforeKey = "nbf"
SubjectKey = "sub"
)
// stdClaimNames is a list of all standard claim names defined in the JWT specification.
var stdClaimNames = []string{AudienceKey, ExpirationKey, IssuedAtKey, IssuerKey, JwtIDKey, NotBeforeKey, SubjectKey}
// Token represents a generic JWT token.
// which are type-aware (to an extent). Other claims may be accessed via the `Get`/`Set`
// methods but their types are not taken into consideration at all. If you have non-standard
// claims that you must frequently access, consider creating accessors functions
// like the following
//
// func SetFoo(tok jwt.Token) error
// func GetFoo(tok jwt.Token) (*Customtyp, error)
//
// Embedding jwt.Token into another struct is not recommended, because
// jwt.Token needs to handle private claims, and this really does not
// work well when it is embedded in other structure
type Token interface {
// Audience returns the value for "aud" field of the token
Audience() ([]string, bool)
// Expiration returns the value for "exp" field of the token
Expiration() (time.Time, bool)
// IssuedAt returns the value for "iat" field of the token
IssuedAt() (time.Time, bool)
// Issuer returns the value for "iss" field of the token
Issuer() (string, bool)
// JwtID returns the value for "jti" field of the token
JwtID() (string, bool)
// NotBefore returns the value for "nbf" field of the token
NotBefore() (time.Time, bool)
// Subject returns the value for "sub" field of the token
Subject() (string, bool)
// Get is used to extract the value of any claim, including non-standard claims, out of the token.
//
// The first argument is the name of the claim. The second argument is a pointer
// to a variable that will receive the value of the claim. The method returns
// an error if the claim does not exist, or if the value cannot be assigned to
// the destination variable. Note that a field is considered to "exist" even if
// the value is empty-ish (e.g. 0, false, ""), as long as it is explicitly set.
//
// For standard claims, you can use the corresponding getter method, such as
// `Issuer()`, `Subject()`, `Audience()`, `IssuedAt()`, `NotBefore()`, `ExpiresAt()`
//
// Note that fields of JWS/JWE are NOT accessible through this method. You need
// to use `jws.Parse` and `jwe.Parse` to obtain the JWS/JWE message (and NOT
// the payload, which presumably is the JWT), and then use their `Get` methods in their respective packages
Get(string, any) error
// Set assigns a value to the corresponding field in the token. Some
// pre-defined fields such as `nbf`, `iat`, `iss` need their values to
// be of a specific type. See the other getter methods in this interface
// for the types of each of these fields
Set(string, any) error
// Has returns true if the specified claim has a value, even if
// the value is empty-ish (e.g. 0, false, "") as long as it has been
// explicitly set.
Has(string) bool
Remove(string) error
// Options returns the per-token options associated with this token.
// The options set value will be copied when the token is cloned via `Clone()`
// but it will not survive when the token goes through marshaling/unmarshaling
// such as `json.Marshal` and `json.Unmarshal`
Options() *TokenOptionSet
Clone() (Token, error)
Keys() []string
}
type stdToken struct {
mu *sync.RWMutex
dc DecodeCtx // per-object context for decoding
options TokenOptionSet // per-object option
audience types.StringList // https://tools.ietf.org/html/rfc7519#section-4.1.3
expiration *types.NumericDate // https://tools.ietf.org/html/rfc7519#section-4.1.4
issuedAt *types.NumericDate // https://tools.ietf.org/html/rfc7519#section-4.1.6
issuer *string // https://tools.ietf.org/html/rfc7519#section-4.1.1
jwtID *string // https://tools.ietf.org/html/rfc7519#section-4.1.7
notBefore *types.NumericDate // https://tools.ietf.org/html/rfc7519#section-4.1.5
subject *string // https://tools.ietf.org/html/rfc7519#section-4.1.2
privateClaims map[string]any
}
// New creates a standard token, with minimal knowledge of
// possible claims. Standard claims include"aud", "exp", "iat", "iss", "jti", "nbf" and "sub".
// Convenience accessors are provided for these standard claims
func New() Token {
return &stdToken{
mu: &sync.RWMutex{},
privateClaims: make(map[string]any),
options: DefaultOptionSet(),
}
}
func (t *stdToken) Options() *TokenOptionSet {
return &t.options
}
func (t *stdToken) Has(name string) bool {
t.mu.RLock()
defer t.mu.RUnlock()
switch name {
case AudienceKey:
return t.audience != nil
case ExpirationKey:
return t.expiration != nil
case IssuedAtKey:
return t.issuedAt != nil
case IssuerKey:
return t.issuer != nil
case JwtIDKey:
return t.jwtID != nil
case NotBeforeKey:
return t.notBefore != nil
case SubjectKey:
return t.subject != nil
default:
_, ok := t.privateClaims[name]
return ok
}
}
func (t *stdToken) Get(name string, dst any) error {
t.mu.RLock()
defer t.mu.RUnlock()
switch name {
case AudienceKey:
if t.audience == nil {
return jwterrs.ClaimNotFoundError{Name: name}
}
if err := blackmagic.AssignIfCompatible(dst, t.audience.Get()); err != nil {
return jwterrs.ClaimAssignmentFailedError{Err: err}
}
return nil
case ExpirationKey:
if t.expiration == nil {
return jwterrs.ClaimNotFoundError{Name: name}
}
if err := blackmagic.AssignIfCompatible(dst, t.expiration.Get()); err != nil {
return jwterrs.ClaimAssignmentFailedError{Err: err}
}
return nil
case IssuedAtKey:
if t.issuedAt == nil {
return jwterrs.ClaimNotFoundError{Name: name}
}
if err := blackmagic.AssignIfCompatible(dst, t.issuedAt.Get()); err != nil {
return jwterrs.ClaimAssignmentFailedError{Err: err}
}
return nil
case IssuerKey:
if t.issuer == nil {
return jwterrs.ClaimNotFoundError{Name: name}
}
if err := blackmagic.AssignIfCompatible(dst, *(t.issuer)); err != nil {
return jwterrs.ClaimAssignmentFailedError{Err: err}
}
return nil
case JwtIDKey:
if t.jwtID == nil {
return jwterrs.ClaimNotFoundError{Name: name}
}
if err := blackmagic.AssignIfCompatible(dst, *(t.jwtID)); err != nil {
return jwterrs.ClaimAssignmentFailedError{Err: err}
}
return nil
case NotBeforeKey:
if t.notBefore == nil {
return jwterrs.ClaimNotFoundError{Name: name}
}
if err := blackmagic.AssignIfCompatible(dst, t.notBefore.Get()); err != nil {
return jwterrs.ClaimAssignmentFailedError{Err: err}
}
return nil
case SubjectKey:
if t.subject == nil {
return jwterrs.ClaimNotFoundError{Name: name}
}
if err := blackmagic.AssignIfCompatible(dst, *(t.subject)); err != nil {
return jwterrs.ClaimAssignmentFailedError{Err: err}
}
return nil
default:
v, ok := t.privateClaims[name]
if !ok {
return jwterrs.ClaimNotFoundError{Name: name}
}
if err := blackmagic.AssignIfCompatible(dst, v); err != nil {
return jwterrs.ClaimAssignmentFailedError{Err: err}
}
return nil
}
}
func (t *stdToken) Remove(key string) error {
t.mu.Lock()
defer t.mu.Unlock()
switch key {
case AudienceKey:
t.audience = nil
case ExpirationKey:
t.expiration = nil
case IssuedAtKey:
t.issuedAt = nil
case IssuerKey:
t.issuer = nil
case JwtIDKey:
t.jwtID = nil
case NotBeforeKey:
t.notBefore = nil
case SubjectKey:
t.subject = nil
default:
delete(t.privateClaims, key)
}
return nil
}
func (t *stdToken) Set(name string, value any) error {
t.mu.Lock()
defer t.mu.Unlock()
return t.setNoLock(name, value)
}
func (t *stdToken) DecodeCtx() DecodeCtx {
t.mu.RLock()
defer t.mu.RUnlock()
return t.dc
}
func (t *stdToken) SetDecodeCtx(v DecodeCtx) {
t.mu.Lock()
defer t.mu.Unlock()
t.dc = v
}
func (t *stdToken) setNoLock(name string, value any) error {
switch name {
case AudienceKey:
var acceptor types.StringList
if err := acceptor.Accept(value); err != nil {
return fmt.Errorf(`invalid value for %s key: %w`, AudienceKey, err)
}
t.audience = acceptor
return nil
case ExpirationKey:
var acceptor types.NumericDate
if err := acceptor.Accept(value); err != nil {
return fmt.Errorf(`invalid value for %s key: %w`, ExpirationKey, err)
}
t.expiration = &acceptor
return nil
case IssuedAtKey:
var acceptor types.NumericDate
if err := acceptor.Accept(value); err != nil {
return fmt.Errorf(`invalid value for %s key: %w`, IssuedAtKey, err)
}
t.issuedAt = &acceptor
return nil
case IssuerKey:
if v, ok := value.(string); ok {
t.issuer = &v
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, IssuerKey, value)
case JwtIDKey:
if v, ok := value.(string); ok {
t.jwtID = &v
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, JwtIDKey, value)
case NotBeforeKey:
var acceptor types.NumericDate
if err := acceptor.Accept(value); err != nil {
return fmt.Errorf(`invalid value for %s key: %w`, NotBeforeKey, err)
}
t.notBefore = &acceptor
return nil
case SubjectKey:
if v, ok := value.(string); ok {
t.subject = &v
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, SubjectKey, value)
default:
if t.privateClaims == nil {
t.privateClaims = map[string]any{}
}
t.privateClaims[name] = value
}
return nil
}
func (t *stdToken) Audience() ([]string, bool) {
t.mu.RLock()
defer t.mu.RUnlock()
if t.audience != nil {
return t.audience.Get(), true
}
return nil, false
}
func (t *stdToken) Expiration() (time.Time, bool) {
t.mu.RLock()
defer t.mu.RUnlock()
if t.expiration != nil {
return t.expiration.Get(), true
}
return time.Time{}, false
}
func (t *stdToken) IssuedAt() (time.Time, bool) {
t.mu.RLock()
defer t.mu.RUnlock()
if t.issuedAt != nil {
return t.issuedAt.Get(), true
}
return time.Time{}, false
}
func (t *stdToken) Issuer() (string, bool) {
t.mu.RLock()
defer t.mu.RUnlock()
if t.issuer != nil {
return *(t.issuer), true
}
return "", false
}
func (t *stdToken) JwtID() (string, bool) {
t.mu.RLock()
defer t.mu.RUnlock()
if t.jwtID != nil {
return *(t.jwtID), true
}
return "", false
}
func (t *stdToken) NotBefore() (time.Time, bool) {
t.mu.RLock()
defer t.mu.RUnlock()
if t.notBefore != nil {
return t.notBefore.Get(), true
}
return time.Time{}, false
}
func (t *stdToken) Subject() (string, bool) {
t.mu.RLock()
defer t.mu.RUnlock()
if t.subject != nil {
return *(t.subject), true
}
return "", false
}
func (t *stdToken) PrivateClaims() map[string]any {
t.mu.RLock()
defer t.mu.RUnlock()
return t.privateClaims
}
func (t *stdToken) UnmarshalJSON(buf []byte) error {
t.mu.Lock()
defer t.mu.Unlock()
t.audience = nil
t.expiration = nil
t.issuedAt = nil
t.issuer = nil
t.jwtID = nil
t.notBefore = nil
t.subject = nil
dec := json.NewDecoder(bytes.NewReader(buf))
LOOP:
for {
tok, err := dec.Token()
if err != nil {
return fmt.Errorf(`error reading token: %w`, err)
}
switch tok := tok.(type) {
case json.Delim:
// Assuming we're doing everything correctly, we should ONLY
// get either tokens.OpenCurlyBracket or tokens.CloseCurlyBracket here.
if tok == tokens.CloseCurlyBracket { // End of object
break LOOP
} else if tok != tokens.OpenCurlyBracket {
return fmt.Errorf(`expected '%c', but got '%c'`, tokens.OpenCurlyBracket, tok)
}
case string: // Objects can only have string keys
switch tok {
case AudienceKey:
var decoded types.StringList
if err := dec.Decode(&decoded); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, AudienceKey, err)
}
t.audience = decoded
case ExpirationKey:
var decoded types.NumericDate
if err := dec.Decode(&decoded); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, ExpirationKey, err)
}
t.expiration = &decoded
case IssuedAtKey:
var decoded types.NumericDate
if err := dec.Decode(&decoded); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, IssuedAtKey, err)
}
t.issuedAt = &decoded
case IssuerKey:
if err := json.AssignNextStringToken(&t.issuer, dec); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, IssuerKey, err)
}
case JwtIDKey:
if err := json.AssignNextStringToken(&t.jwtID, dec); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, JwtIDKey, err)
}
case NotBeforeKey:
var decoded types.NumericDate
if err := dec.Decode(&decoded); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, NotBeforeKey, err)
}
t.notBefore = &decoded
case SubjectKey:
if err := json.AssignNextStringToken(&t.subject, dec); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, SubjectKey, err)
}
default:
if dc := t.dc; dc != nil {
if localReg := dc.Registry(); localReg != nil {
decoded, err := localReg.Decode(dec, tok)
if err == nil {
t.setNoLock(tok, decoded)
continue
}
}
}
decoded, err := registry.Decode(dec, tok)
if err == nil {
t.setNoLock(tok, decoded)
continue
}
return fmt.Errorf(`could not decode field %s: %w`, tok, err)
}
default:
return fmt.Errorf(`invalid token %T`, tok)
}
}
return nil
}
func (t *stdToken) Keys() []string {
t.mu.RLock()
defer t.mu.RUnlock()
keys := make([]string, 0, 7+len(t.privateClaims))
if t.audience != nil {
keys = append(keys, AudienceKey)
}
if t.expiration != nil {
keys = append(keys, ExpirationKey)
}
if t.issuedAt != nil {
keys = append(keys, IssuedAtKey)
}
if t.issuer != nil {
keys = append(keys, IssuerKey)
}
if t.jwtID != nil {
keys = append(keys, JwtIDKey)
}
if t.notBefore != nil {
keys = append(keys, NotBeforeKey)
}
if t.subject != nil {
keys = append(keys, SubjectKey)
}
for k := range t.privateClaims {
keys = append(keys, k)
}
return keys
}
type claimPair struct {
Name string
Value any
}
var claimPairPool = sync.Pool{
New: func() any {
return make([]claimPair, 0, 7)
},
}
func getClaimPairList() []claimPair {
return claimPairPool.Get().([]claimPair)
}
func putClaimPairList(list []claimPair) {
list = list[:0]
claimPairPool.Put(list)
}
// makePairs creates a list of claimPair objects that are sorted by
// their key names. The key names are always their JSON names, and
// the values are already JSON encoded.
// Because makePairs needs to allocate a slice, it _slows_ down
// marshaling of the token to JSON. The upside is that it allows us to
// marshal the token keys in a deterministic order.
// Do we really need it...? Well, technically we don't, but it's so
// much nicer to have this to make the example tests actually work
// deterministically. Also if for whatever reason this becomes a
// performance issue, we can always/ add a flag to use a more _optimized_ code path.
//
// The caller is responsible to call putClaimPairList() to return the
// allocated slice back to the pool.
func (t *stdToken) makePairs() ([]claimPair, error) {
pairs := getClaimPairList()
if t.audience != nil {
buf, err := json.MarshalAudience(t.audience, t.options.IsEnabled(FlattenAudience))
if err != nil {
return nil, fmt.Errorf(`failed to encode "aud": %w`, err)
}
pairs = append(pairs, claimPair{Name: AudienceKey, Value: buf})
}
if t.expiration != nil {
buf, err := json.Marshal(t.expiration.Unix())
if err != nil {
return nil, fmt.Errorf(`failed to encode "exp": %w`, err)
}
pairs = append(pairs, claimPair{Name: ExpirationKey, Value: buf})
}
if t.issuedAt != nil {
buf, err := json.Marshal(t.issuedAt.Unix())
if err != nil {
return nil, fmt.Errorf(`failed to encode "iat": %w`, err)
}
pairs = append(pairs, claimPair{Name: IssuedAtKey, Value: buf})
}
if t.issuer != nil {
buf, err := json.Marshal(*(t.issuer))
if err != nil {
return nil, fmt.Errorf(`failed to encode field "iss": %w`, err)
}
pairs = append(pairs, claimPair{Name: IssuerKey, Value: buf})
}
if t.jwtID != nil {
buf, err := json.Marshal(*(t.jwtID))
if err != nil {
return nil, fmt.Errorf(`failed to encode field "jti": %w`, err)
}
pairs = append(pairs, claimPair{Name: JwtIDKey, Value: buf})
}
if t.notBefore != nil {
buf, err := json.Marshal(t.notBefore.Unix())
if err != nil {
return nil, fmt.Errorf(`failed to encode "nbf": %w`, err)
}
pairs = append(pairs, claimPair{Name: NotBeforeKey, Value: buf})
}
if t.subject != nil {
buf, err := json.Marshal(*(t.subject))
if err != nil {
return nil, fmt.Errorf(`failed to encode field "sub": %w`, err)
}
pairs = append(pairs, claimPair{Name: SubjectKey, Value: buf})
}
for k, v := range t.privateClaims {
buf, err := json.Marshal(v)
if err != nil {
return nil, fmt.Errorf(`failed to encode field %q: %w`, k, err)
}
pairs = append(pairs, claimPair{Name: k, Value: buf})
}
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].Name < pairs[j].Name
})
return pairs, nil
}
func (t stdToken) MarshalJSON() ([]byte, error) {
buf := pool.BytesBuffer().Get()
defer pool.BytesBuffer().Put(buf)
pairs, err := t.makePairs()
if err != nil {
return nil, fmt.Errorf(`failed to make pairs: %w`, err)
}
buf.WriteByte(tokens.OpenCurlyBracket)
for i, pair := range pairs {
if i > 0 {
buf.WriteByte(tokens.Comma)
}
fmt.Fprintf(buf, "%q: %s", pair.Name, pair.Value)
}
buf.WriteByte(tokens.CloseCurlyBracket)
ret := make([]byte, buf.Len())
copy(ret, buf.Bytes())
putClaimPairList(pairs)
return ret, nil
}
+78
View File
@@ -0,0 +1,78 @@
package jwt
import "sync"
// TokenOptionSet is a bit flag containing per-token options.
type TokenOptionSet uint64
var defaultOptions TokenOptionSet
var defaultOptionsMu sync.RWMutex
// TokenOption describes a single token option that can be set on
// the per-token option set (TokenOptionSet)
type TokenOption uint64
const (
// FlattenAudience option controls whether the "aud" claim should be flattened
// to a single string upon the token being serialized to JSON.
//
// This is sometimes important when a JWT consumer does not understand that
// the "aud" claim can actually take the form of an array of strings.
// (We have been notified by users that AWS Cognito has manifested this behavior
// at some point)
//
// Unless the global option is set using `jwt.Settings()`, the default value is
// `disabled`, which means that "aud" claims are always rendered as a arrays of
// strings when serialized to JSON.
FlattenAudience TokenOption = 1 << iota
// MaxPerTokenOption is a marker to denote the last value that an option can take.
// This value has no meaning other than to be used as a marker.
MaxPerTokenOption
)
// Value returns the uint64 value of a single option
func (o TokenOption) Value() uint64 {
return uint64(o)
}
// Value returns the uint64 bit flag value of an option set
func (o TokenOptionSet) Value() uint64 {
return uint64(o)
}
// DefaultOptionSet creates a new TokenOptionSet using the default
// option set. This may differ depending on if/when functions that
// change the global state has been called, such as `jwt.Settings`
func DefaultOptionSet() TokenOptionSet {
return TokenOptionSet(defaultOptions.Value())
}
// Clear sets all bits to zero, effectively disabling all options
func (o *TokenOptionSet) Clear() {
*o = TokenOptionSet(uint64(0))
}
// Set sets the value of this option set, effectively *replacing*
// the entire option set with the new value. This is NOT the same
// as Enable/Disable.
func (o *TokenOptionSet) Set(s TokenOptionSet) {
*o = s
}
// Enable sets the appropriate value to enable the option in the
// option set
func (o *TokenOptionSet) Enable(flag TokenOption) {
*o = TokenOptionSet(o.Value() | uint64(flag))
}
// Disable sets the appropriate value to disable the option in the
// option set
func (o *TokenOptionSet) Disable(flag TokenOption) {
*o = TokenOptionSet(o.Value() & ^uint64(flag))
}
// IsEnabled returns true if the given bit on the option set is enabled.
func (o TokenOptionSet) IsEnabled(flag TokenOption) bool {
return (uint64(o)&uint64(flag) == uint64(flag))
}
+25
View File
@@ -0,0 +1,25 @@
// Code generated by "stringer -type=TokenOption -output=token_options_gen.go"; DO NOT EDIT.
package jwt
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[FlattenAudience-1]
_ = x[MaxPerTokenOption-2]
}
const _TokenOption_name = "FlattenAudienceMaxPerTokenOption"
var _TokenOption_index = [...]uint8{0, 15, 32}
func (i TokenOption) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_TokenOption_index)-1 {
return "TokenOption(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _TokenOption_name[_TokenOption_index[idx]:_TokenOption_index[idx+1]]
}
+417
View File
@@ -0,0 +1,417 @@
package jwt
import (
"context"
"fmt"
"slices"
"strconv"
"time"
jwterrs "github.com/lestrrat-go/jwx/v3/jwt/internal/errors"
)
type Clock interface {
Now() time.Time
}
type ClockFunc func() time.Time
func (f ClockFunc) Now() time.Time {
return f()
}
func isSupportedTimeClaim(c string) error {
switch c {
case ExpirationKey, IssuedAtKey, NotBeforeKey:
return nil
}
return fmt.Errorf(`unsupported time claim %s`, strconv.Quote(c))
}
func timeClaim(t Token, clock Clock, c string) time.Time {
// We don't check if the claims already exist. It should have been done
// by piggybacking on `required` check.
switch c {
case ExpirationKey:
tv, _ := t.Expiration()
return tv
case IssuedAtKey:
tv, _ := t.IssuedAt()
return tv
case NotBeforeKey:
tv, _ := t.NotBefore()
return tv
case "":
return clock.Now()
}
return time.Time{} // should *NEVER* reach here, but...
}
// Validate makes sure that the essential claims stand.
//
// See the various `WithXXX` functions for optional parameters
// that can control the behavior of this method.
func Validate(t Token, options ...ValidateOption) error {
ctx := context.Background()
trunc := getDefaultTruncation()
var clock Clock = ClockFunc(time.Now)
var skew time.Duration
var baseValidators = []Validator{
IsIssuedAtValid(),
IsExpirationValid(),
IsNbfValid(),
}
var extraValidators []Validator
var resetValidators bool
for _, o := range options {
switch o.Ident() {
case identClock{}:
if err := o.Value(&clock); err != nil {
return fmt.Errorf(`jwt.Validate: value for WithClock() option must be jwt.Clock: %w`, err)
}
case identAcceptableSkew{}:
if err := o.Value(&skew); err != nil {
return fmt.Errorf(`jwt.Validate: value for WithAcceptableSkew() option must be time.Duration: %w`, err)
}
case identTruncation{}:
if err := o.Value(&trunc); err != nil {
return fmt.Errorf(`jwt.Validate: value for WithTruncation() option must be time.Duration: %w`, err)
}
case identContext{}:
if err := o.Value(&ctx); err != nil {
return fmt.Errorf(`jwt.Validate: value for WithContext() option must be context.Context: %w`, err)
}
case identResetValidators{}:
if err := o.Value(&resetValidators); err != nil {
return fmt.Errorf(`jwt.Validate: value for WithResetValidators() option must be bool: %w`, err)
}
case identValidator{}:
var v Validator
if err := o.Value(&v); err != nil {
return fmt.Errorf(`jwt.Validate: value for WithValidator() option must be jwt.Validator: %w`, err)
}
switch v := v.(type) {
case *isInTimeRange:
if v.c1 != "" {
if err := isSupportedTimeClaim(v.c1); err != nil {
return err
}
extraValidators = append(extraValidators, IsRequired(v.c1))
}
if v.c2 != "" {
if err := isSupportedTimeClaim(v.c2); err != nil {
return err
}
extraValidators = append(extraValidators, IsRequired(v.c2))
}
}
extraValidators = append(extraValidators, v)
}
}
ctx = SetValidationCtxSkew(ctx, skew)
ctx = SetValidationCtxClock(ctx, clock)
ctx = SetValidationCtxTruncation(ctx, trunc)
var validators []Validator
if !resetValidators {
validators = append(baseValidators, extraValidators...)
} else {
if len(extraValidators) == 0 {
return jwterrs.ValidateErrorf(`no validators specified: jwt.WithResetValidators(true) and no jwt.WithValidator() specified`)
}
validators = extraValidators
}
for _, v := range validators {
if err := v.Validate(ctx, t); err != nil {
return jwterrs.ValidateErrorf(`validation failed: %w`, err)
}
}
return nil
}
type isInTimeRange struct {
c1 string
c2 string
dur time.Duration
less bool // if true, d =< c1 - c2. otherwise d >= c1 - c2
}
// MaxDeltaIs implements the logic behind `WithMaxDelta()` option
func MaxDeltaIs(c1, c2 string, dur time.Duration) Validator {
return &isInTimeRange{
c1: c1,
c2: c2,
dur: dur,
less: true,
}
}
// MinDeltaIs implements the logic behind `WithMinDelta()` option
func MinDeltaIs(c1, c2 string, dur time.Duration) Validator {
return &isInTimeRange{
c1: c1,
c2: c2,
dur: dur,
less: false,
}
}
func (iitr *isInTimeRange) Validate(ctx context.Context, t Token) error {
clock := ValidationCtxClock(ctx) // MUST be populated
skew := ValidationCtxSkew(ctx) // MUST be populated
// We don't check if the claims already exist, because we already did that
// by piggybacking on `required` check.
t1 := timeClaim(t, clock, iitr.c1)
t2 := timeClaim(t, clock, iitr.c2)
if iitr.less { // t1 - t2 <= iitr.dur
// t1 - t2 < iitr.dur + skew
if t1.Sub(t2) > iitr.dur+skew {
return fmt.Errorf(`iitr between %s and %s exceeds %s (skew %s)`, iitr.c1, iitr.c2, iitr.dur, skew)
}
} else {
if t1.Sub(t2) < iitr.dur-skew {
return fmt.Errorf(`iitr between %s and %s is less than %s (skew %s)`, iitr.c1, iitr.c2, iitr.dur, skew)
}
}
return nil
}
// Validator describes interface to validate a Token.
type Validator interface {
// Validate should return an error if a required conditions is not met.
Validate(context.Context, Token) error
}
// ValidatorFunc is a type of Validator that does not have any
// state, that is implemented as a function
type ValidatorFunc func(context.Context, Token) error
func (vf ValidatorFunc) Validate(ctx context.Context, tok Token) error {
return vf(ctx, tok)
}
type identValidationCtxClock struct{}
type identValidationCtxSkew struct{}
type identValidationCtxTruncation struct{}
func SetValidationCtxClock(ctx context.Context, cl Clock) context.Context {
return context.WithValue(ctx, identValidationCtxClock{}, cl)
}
func SetValidationCtxTruncation(ctx context.Context, dur time.Duration) context.Context {
return context.WithValue(ctx, identValidationCtxTruncation{}, dur)
}
func SetValidationCtxSkew(ctx context.Context, dur time.Duration) context.Context {
return context.WithValue(ctx, identValidationCtxSkew{}, dur)
}
// ValidationCtxClock returns the Clock object associated with
// the current validation context. This value will always be available
// during validation of tokens.
func ValidationCtxClock(ctx context.Context) Clock {
//nolint:forcetypeassert
return ctx.Value(identValidationCtxClock{}).(Clock)
}
func ValidationCtxSkew(ctx context.Context) time.Duration {
//nolint:forcetypeassert
return ctx.Value(identValidationCtxSkew{}).(time.Duration)
}
func ValidationCtxTruncation(ctx context.Context) time.Duration {
//nolint:forcetypeassert
return ctx.Value(identValidationCtxTruncation{}).(time.Duration)
}
// IsExpirationValid is one of the default validators that will be executed.
// It does not need to be specified by users, but it exists as an
// exported field so that you can check what it does.
//
// The supplied context.Context object must have the "clock" and "skew"
// populated with appropriate values using SetValidationCtxClock() and
// SetValidationCtxSkew()
func IsExpirationValid() Validator {
return ValidatorFunc(isExpirationValid)
}
func isExpirationValid(ctx context.Context, t Token) error {
tv, ok := t.Expiration()
if !ok {
return nil
}
clock := ValidationCtxClock(ctx) // MUST be populated
skew := ValidationCtxSkew(ctx) // MUST be populated
trunc := ValidationCtxTruncation(ctx) // MUST be populated
now := clock.Now().Truncate(trunc)
ttv := tv.Truncate(trunc)
// expiration date must be after NOW
if !now.Before(ttv.Add(skew)) {
return TokenExpiredError()
}
return nil
}
// IsIssuedAtValid is one of the default validators that will be executed.
// It does not need to be specified by users, but it exists as an
// exported field so that you can check what it does.
//
// The supplied context.Context object must have the "clock" and "skew"
// populated with appropriate values using SetValidationCtxClock() and
// SetValidationCtxSkew()
func IsIssuedAtValid() Validator {
return ValidatorFunc(isIssuedAtValid)
}
func isIssuedAtValid(ctx context.Context, t Token) error {
tv, ok := t.IssuedAt()
if !ok {
return nil
}
clock := ValidationCtxClock(ctx) // MUST be populated
skew := ValidationCtxSkew(ctx) // MUST be populated
trunc := ValidationCtxTruncation(ctx) // MUST be populated
now := clock.Now().Truncate(trunc)
ttv := tv.Truncate(trunc)
if now.Before(ttv.Add(-1 * skew)) {
return InvalidIssuedAtError()
}
return nil
}
// IsNbfValid is one of the default validators that will be executed.
// It does not need to be specified by users, but it exists as an
// exported field so that you can check what it does.
//
// The supplied context.Context object must have the "clock" and "skew"
// populated with appropriate values using SetValidationCtxClock() and
// SetValidationCtxSkew()
func IsNbfValid() Validator {
return ValidatorFunc(isNbfValid)
}
func isNbfValid(ctx context.Context, t Token) error {
tv, ok := t.NotBefore()
if !ok {
return nil
}
clock := ValidationCtxClock(ctx) // MUST be populated
skew := ValidationCtxSkew(ctx) // MUST be populated
trunc := ValidationCtxTruncation(ctx) // MUST be populated
// Truncation always happens even for trunc = 0 because
// we also use this to strip monotonic clocks
now := clock.Now().Truncate(trunc)
ttv := tv.Truncate(trunc)
// "now" cannot be before t - skew, so we check for now > t - skew
ttv = ttv.Add(-1 * skew)
if now.Before(ttv) {
return TokenNotYetValidError()
}
return nil
}
type claimContainsString struct {
name string
value string
makeErr func(string, ...any) error
}
// ClaimContainsString can be used to check if the claim called `name`, which is
// expected to be a list of strings, contains `value`. Currently, because of the
// implementation, this will probably only work for `aud` fields.
func ClaimContainsString(name, value string) Validator {
return claimContainsString{
name: name,
value: value,
makeErr: fmt.Errorf,
}
}
func (ccs claimContainsString) Validate(_ context.Context, t Token) error {
var list []string
if err := t.Get(ccs.name, &list); err != nil {
return ccs.makeErr(`claim %q does not exist or is not a []string: %w`, ccs.name, err)
}
if !slices.Contains(list, ccs.value) {
return ccs.makeErr(`%q not satisfied`, ccs.name)
}
return nil
}
// audienceClaimContainsString can be used to check if the audience claim, which is
// expected to be a list of strings, contains `value`.
func audienceClaimContainsString(value string) Validator {
return claimContainsString{
name: AudienceKey,
value: value,
makeErr: jwterrs.AudienceErrorf,
}
}
type claimValueIs struct {
name string
value any
makeErr func(string, ...any) error
}
// ClaimValueIs creates a Validator that checks if the value of claim `name`
// matches `value`. The comparison is done using a simple `==` comparison,
// and therefore complex comparisons may fail using this code. If you
// need to do more, use a custom Validator.
func ClaimValueIs(name string, value any) Validator {
return &claimValueIs{
name: name,
value: value,
makeErr: fmt.Errorf,
}
}
func (cv *claimValueIs) Validate(_ context.Context, t Token) error {
var v any
if err := t.Get(cv.name, &v); err != nil {
return cv.makeErr(`claim %[1]q does not exist or is not a []string: %[2]w`, cv.name, err)
}
if v != cv.value {
return cv.makeErr(`claim %[1]q does not have the expected value`, cv.name)
}
return nil
}
// issuerClaimValueIs creates a Validator that checks if the issuer claim
// matches `value`.
func issuerClaimValueIs(value string) Validator {
return &claimValueIs{
name: IssuerKey,
value: value,
makeErr: jwterrs.IssuerErrorf,
}
}
// IsRequired creates a Validator that checks if the required claim `name`
// exists in the token
func IsRequired(name string) Validator {
return isRequired(name)
}
type isRequired string
func (ir isRequired) Validate(_ context.Context, t Token) error {
name := string(ir)
if !t.Has(name) {
return jwterrs.MissingRequiredClaimErrorf(name)
}
return nil
}