Initial QSfera import
This commit is contained in:
+72
@@ -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
@@ -0,0 +1,224 @@
|
||||
# JWT [](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"github.com/lestrrat-go/jwx/v3/internal/json"
|
||||
)
|
||||
|
||||
type DecodeCtx = json.DecodeCtx
|
||||
type TokenWithDecodeCtx = json.DecodeCtxContainer
|
||||
+16
@@ -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__"],
|
||||
)
|
||||
+185
@@ -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)}
|
||||
}
|
||||
+35
@@ -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
@@ -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
|
||||
}
|
||||
+43
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user