lib/jwt: remove memory allocation from token parsing

This commit adds `Reset()` method to the Token struct.
It allows to re-use `Token` object, which reduces memory allocations
needed for parsing `Token` and CPU pressure on GarbageCollector.

 Additionally, it adds fastjson parser, which allows efficiently perform
 claims matching based on dynamic value input.

 Benchmark stats:

```
                                         │ profiles/jwt_parse_before.txt │    profiles/jwt_parse_after.txt     │
                                         │            sec/op             │   sec/op     vs base                │
TokenParse/simple-10                                       3375.0n ± 41%   335.6n ± 4%  -90.05% (p=0.000 n=10)
TokenParse/gateway_labels_and_filters-10                   4259.0n ±  6%   423.3n ± 5%  -90.06% (p=0.000 n=10)
TokenParse/scope_as_slice_string-10                        3781.5n ±  2%   374.7n ± 5%  -90.09% (p=0.000 n=10)
TokenParse/access_claim_string-10                          2974.5n ±  1%   290.9n ± 4%  -90.22% (p=0.000 n=10)
TokenParse/vmauth_related_fields-10                        4340.5n ±  2%   389.2n ± 2%  -91.03% (p=0.000 n=10)
geomean                                                     3.709µ         359.8n       -90.30%

                                         │ profiles/jwt_parse_before.txt │       profiles/jwt_parse_after.txt        │
                                         │             B/op              │     B/op      vs base                     │
TokenParse/simple-10                                        5.195Ki ± 0%   0.000Ki ± 0%  -100.00% (p=0.000 n=10)
TokenParse/gateway_labels_and_filters-10                    6312.00 ± 0%     16.00 ± 0%   -99.75% (p=0.000 n=10)
TokenParse/scope_as_slice_string-10                         6312.00 ± 0%     16.00 ± 0%   -99.75% (p=0.000 n=10)
TokenParse/access_claim_string-10                           4.789Ki ± 0%   0.000Ki ± 0%  -100.00% (p=0.000 n=10)
TokenParse/vmauth_related_fields-10                         6.327Ki ± 0%   0.000Ki ± 0%  -100.00% (p=0.000 n=10)
geomean                                                     5.693Ki                      ?                       ¹ ²
¬π summaries must be >0 to compute geomean
² ratios must be >0 to compute geomean

                                         │ profiles/jwt_parse_before.txt │      profiles/jwt_parse_after.txt       │
                                         │           allocs/op           │ allocs/op   vs base                     │
TokenParse/simple-10                                          39.00 ± 0%    0.00 ± 0%  -100.00% (p=0.000 n=10)
TokenParse/gateway_labels_and_filters-10                     53.000 ± 0%   1.000 ± 0%   -98.11% (p=0.000 n=10)
TokenParse/scope_as_slice_string-10                          54.000 ± 0%   1.000 ± 0%   -98.15% (p=0.000 n=10)
TokenParse/access_claim_string-10                             41.00 ± 0%    0.00 ± 0%  -100.00% (p=0.000 n=10)
TokenParse/vmauth_related_fields-10                           57.00 ± 0%    0.00 ± 0%  -100.00% (p=0.000 n=10)
geomean                                                       48.23                    ?                       ¹ ²
```

Related to
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10492
This commit is contained in:
Nikolay
2026-03-04 17:31:30 +01:00
committed by GitHub
parent a1a35fd870
commit f8a101e45e
3 changed files with 726 additions and 245 deletions

View File

@@ -3,12 +3,14 @@ package jwt
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"math"
"net/http"
"slices"
"strings"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/valyala/fastjson"
)
const (
@@ -32,8 +34,8 @@ var (
// Token represents jwt token
// https://auth0.com/docs/tokens/json-web-tokens
type Token struct {
header *header
body *body
header header
body body
payload, signature []byte
}
@@ -41,55 +43,380 @@ type header struct {
Alg string `json:"alg"`
Typ string `json:"typ"`
Kid string `json:"kid"`
buf []byte
p *fastjson.Parser
}
func (h *header) parse(src string) error {
var err error
h.buf, err = decodeB64(h.buf[:0], src)
if err != nil {
return err
}
h.p = parserPool.Get()
jv, err := h.p.ParseBytes(h.buf)
if err != nil {
return err
}
if jv == nil {
return fmt.Errorf("unexpected empty json")
}
if jv.Type() != fastjson.TypeObject {
return fmt.Errorf("unexpected non json object {} type: %q", jv.Type())
}
h.Alg, err = stringFromJSONValue(jv, "alg")
if err != nil {
return err
}
h.Typ, err = stringFromJSONValue(jv, "typ")
if err != nil {
return err
}
h.Kid, err = stringFromJSONValue(jv, "kid")
if err != nil {
return err
}
return nil
}
func (h *header) reset() {
h.Alg = ""
h.Typ = ""
h.Kid = ""
h.buf = h.buf[:0]
if h.p != nil {
parserPool.Put(h.p)
h.p = nil
}
}
type body struct {
// expired at time unix_ts
Exp int64 `json:"exp"`
// issued at time unix_ts
Iat int64 `json:"iat"`
Jti string `json:"jti,omitempty"`
Scope string `json:"scope,omitempty"`
VMAccess *VMAccessClaim `json:"vm_access"`
Iat int64 `json:"iat"`
Jti string `json:"jti,omitempty"`
Scope string `json:"scope,omitempty"`
vmAccessClaim VMAccessClaim
buf []byte
p *fastjson.Parser
// allClaims holds entire json body
// for the HasClaims() method
allClaims *fastjson.Value
// claimsParser holds optional parser for `vm_access` string representation
claimsParser *fastjson.Parser
}
// Labels defines labels added to filters or incoming time series.
type Labels map[string]string
func (b *body) parse(src string) error {
// AsExtraLabels - converts labels to label=value pairs.
func (l Labels) AsExtraLabels() []string {
if len(l) == 0 {
return nil
var err error
b.buf, err = decodeB64(b.buf[:0], src)
if err != nil {
return err
}
res := make([]string, 0, len(l))
for k, v := range l {
res = append(res, k+"="+v)
b.p = parserPool.Get()
jv, err := b.p.ParseBytes(b.buf)
if err != nil {
return err
}
// sort for consistent uri.
slices.Sort(res)
return res
if expObject := jv.Get("exp"); expObject != nil {
b.Exp, err = expObject.Int64()
if err != nil {
return fmt.Errorf("cannot parse `exp` field: %w", err)
}
}
if iatObject := jv.Get("iat"); iatObject != nil {
b.Iat, err = iatObject.Int64()
if err != nil {
return fmt.Errorf("cannot parse `iat` field: %w", err)
}
}
vaObject := jv.Get("vm_access")
if vaObject == nil {
return ErrVMAccessFieldMissing
}
// some IDPs encode custom claims as a string
// try parsing as an object and fallback to a string
switch vaObject.Type() {
case fastjson.TypeObject:
if err := b.vmAccessClaim.parseFrom(vaObject); err != nil {
return err
}
case fastjson.TypeString:
b.claimsParser = parserPool.Get()
va, err := b.claimsParser.ParseBytes(vaObject.GetStringBytes())
if err != nil {
return fmt.Errorf("cannot parse `vm_access` string json: %w", err)
}
if err := b.vmAccessClaim.parseFrom(va); err != nil {
return fmt.Errorf("cannot parse `vm_access` values from string json: %w", err)
}
case fastjson.TypeNull:
return ErrVMAccessFieldMissing
default:
return fmt.Errorf("unexpected type for `vm_access` field; got: %q, want object {}", vaObject.Type())
}
b.Jti = bytesutil.ToUnsafeString(jv.GetStringBytes("jti"))
if scopeObject := jv.Get("scope"); scopeObject != nil {
// some IDPs encode scope as a string and some as an array
switch scopeObject.Type() {
case fastjson.TypeString:
sb := scopeObject.GetStringBytes()
b.Scope = bytesutil.ToUnsafeString(sb)
case fastjson.TypeArray:
var sizeNeeded int
ss := scopeObject.GetArray()
for _, v := range ss {
sizeNeeded += len(v.GetStringBytes()) + 1
}
dst := make([]byte, 0, sizeNeeded)
for idx, v := range ss {
dst = append(dst, v.GetStringBytes()...)
if idx < len(ss)-1 {
dst = append(dst, ' ')
}
}
b.Scope = bytesutil.ToUnsafeString(dst)
default:
return fmt.Errorf("unexpected type for `scope` field; got %q, want String or []String", scopeObject.Type())
}
}
b.allClaims = jv
return nil
}
func (b *body) reset() {
b.Exp = 0
b.Iat = 0
b.Jti = ""
b.Scope = ""
b.buf = b.buf[:0]
b.allClaims = nil
b.vmAccessClaim.reset()
if b.p != nil {
parserPool.Put(b.p)
b.p = nil
}
if b.claimsParser != nil {
parserPool.Put(b.claimsParser)
b.claimsParser = nil
}
}
// Parse parses JWT token from given source string
//
// Token field is valid until src is reachable
func (t *Token) Parse(src string, enforceAuthPrefix bool) error {
if enforceAuthPrefix && (len(src) < len(prefix) || !strings.EqualFold(src[:len(prefix)], prefix)) {
return fmt.Errorf("wrong format, prefix: %s is missing", prefix)
}
// While https://datatracker.ietf.org/doc/html/rfc6750#section-2.1 states that only Bearer prefix is allowed,
// it claims to be conformant to the generic syntax defined in https://datatracker.ietf.org/doc/html/rfc2617#section-1.2
// which permits case-insensitive auth scheme.
// So we should be tolerant to different cases of "Bearer" prefix.
if len(src) >= len(prefix) && strings.EqualFold(src[:len(prefix)], prefix) {
src = src[len(prefix):]
}
// assume jwt token has the following structure:
// header.body.signature
var header, body, signature string
idx := strings.IndexByte(src, '.')
if idx <= 0 {
return ErrBadTokenFormat
}
header = src[:idx]
src = src[idx+1:]
idx = strings.IndexByte(src, '.')
if idx <= 0 {
return ErrBadTokenFormat
}
body = src[:idx]
signature = src[idx+1:]
if err := t.parse(header, body, signature); err != nil {
return err
}
return nil
}
// HasClaims checks if Token has all given claim key value pairs
func (t *Token) HasClaims(claims map[string]string) bool {
for k, v := range claims {
gotV := t.body.allClaims.Get(k)
if gotV == nil || gotV.Type() != fastjson.TypeString {
return false
}
tcv := bytesutil.ToUnsafeString(gotV.GetStringBytes())
if tcv != v {
return false
}
}
return true
}
// VMAccess return a reference to the VMAccessClaim
// all data are valid until Token is reachable
func (t *Token) VMAccess() *VMAccessClaim {
return &t.body.vmAccessClaim
}
// Reset release memory used by token
// Token cannot be used after this call
func (t *Token) Reset() {
t.header.reset()
t.body.reset()
t.payload = t.payload[:0]
t.signature = t.signature[:0]
}
// VMAccessClaim represent JWT claim object
type VMAccessClaim struct {
Tenant TenantID `json:"tenant_id"`
Labels Labels `json:"extra_labels,omitempty"`
// promql filters applied to each select query
ExtraFilters []string `json:"extra_filters,omitempty"`
MetricsExtraFilters []string `json:"metrics_extra_filters,omitempty"`
MetricsExtraLabels []string `json:"metrics_extra_labels,omitempty"`
LogsExtraFilters []string `json:"logs_extra_filters,omitempty"`
LogsExtraStreamFilters []string `json:"logs_extra_stream_filters,omitempty"`
Labels []string `json:"extra_labels,omitempty"`
// labelsBuf holds allocated memory for Labels
labelsBuf []byte
Tenant TenantID `json:"tenant_id"`
// role can be denied as 1 = read, 2 = write, 3 = read and write
// 0 = unconfigured - read and write
Mode int `json:"mode,omitempty"`
// TODO: use different claim struct for vmauth and vmgateway
// parsing must be dynamic based on provided hint
MetricsAccountID uint32 `json:"metrics_account_id,omitempty"`
MetricsProjectID uint32 `json:"metrics_project_id,omitempty"`
MetricsExtraFilters []string `json:"metrics_extra_filters,omitempty"`
MetricsExtraLabels []string `json:"metrics_extra_labels,omitempty"`
MetricsAccountID uint32 `json:"metrics_account_id,omitempty"`
MetricsProjectID uint32 `json:"metrics_project_id,omitempty"`
LogsAccountID uint32 `json:"logs_account_id,omitempty"`
LogsProjectID uint32 `json:"logs_project_id,omitempty"`
LogsExtraFilters []string `json:"logs_extra_filters,omitempty"`
LogsExtraStreamFilters []string `json:"logs_extra_stream_filters,omitempty"`
LogsAccountID uint32 `json:"logs_account_id,omitempty"`
LogsProjectID uint32 `json:"logs_project_id,omitempty"`
}
func (vac *VMAccessClaim) reset() {
vac.Tenant.AccountID = 0
vac.Tenant.ProjectID = 0
clear(vac.Labels)
vac.Labels = vac.Labels[:0]
vac.labelsBuf = vac.labelsBuf[:0]
clear(vac.ExtraFilters)
vac.ExtraFilters = vac.ExtraFilters[:0]
vac.Mode = 0
vac.MetricsAccountID = 0
vac.MetricsProjectID = 0
clear(vac.MetricsExtraFilters)
vac.MetricsExtraFilters = vac.MetricsExtraFilters[:0]
clear(vac.MetricsExtraLabels)
vac.MetricsExtraLabels = vac.MetricsExtraLabels[:0]
vac.LogsAccountID = 0
vac.LogsProjectID = 0
clear(vac.LogsExtraFilters)
vac.LogsExtraFilters = vac.LogsExtraFilters[:0]
clear(vac.LogsExtraStreamFilters)
vac.LogsExtraStreamFilters = vac.LogsExtraStreamFilters[:0]
}
func (vac *VMAccessClaim) parseFrom(jv *fastjson.Value) error {
if err := vac.Tenant.parseFrom(jv); err != nil {
return err
}
var err error
vac.ExtraFilters, err = stringSliceFromJSONValue(vac.ExtraFilters, jv, "extra_filters")
if err != nil {
return err
}
efs := jv.Get("extra_labels")
if efs != nil {
efsO, err := efs.Object()
if err != nil {
return fmt.Errorf("cannot parse `extra_labels` field: %w", err)
}
buf := vac.labelsBuf[:0]
var visitErr error
efsO.Visit(func(key []byte, v *fastjson.Value) {
if visitErr != nil {
return
}
vs, err := v.StringBytes()
if err != nil {
visitErr = fmt.Errorf("unexpected value for key=%q: %w", string(key), err)
}
start := len(buf)
sizeNeeded := len(key) + 1 + len(vs)
if len(buf)+sizeNeeded >= cap(buf) {
// allocate new slice without memory fragmentation
// old slice will be referenced by vac.Labels
start = 0
buf = make([]byte, 0, len(buf)+sizeNeeded)
}
buf = append(buf, key...)
buf = append(buf, '=')
buf = append(buf, vs...)
ef := bytesutil.ToUnsafeString(buf[start:])
vac.Labels = append(vac.Labels, ef)
})
vac.labelsBuf = buf
if visitErr != nil {
return fmt.Errorf("cannot parse `extra_labels` field: %w", visitErr)
}
}
mode := jv.Get("mode")
if mode != nil {
vac.Mode, err = mode.Int()
if err != nil {
return fmt.Errorf("unexpected `mode` value: %w", err)
}
}
vac.MetricsAccountID, err = uint32FromJSONValue(jv, "metrics_account_id")
if err != nil {
return err
}
vac.MetricsProjectID, err = uint32FromJSONValue(jv, "metrics_project_id")
if err != nil {
return err
}
vac.MetricsExtraFilters, err = stringSliceFromJSONValue(vac.MetricsExtraFilters, jv, "metrics_extra_filters")
if err != nil {
return err
}
vac.MetricsExtraLabels, err = stringSliceFromJSONValue(vac.MetricsExtraLabels, jv, "metrics_extra_labels")
if err != nil {
return err
}
vac.LogsAccountID, err = uint32FromJSONValue(jv, "logs_account_id")
if err != nil {
return err
}
vac.LogsProjectID, err = uint32FromJSONValue(jv, "logs_project_id")
if err != nil {
return err
}
vac.LogsExtraFilters, err = stringSliceFromJSONValue(vac.LogsExtraFilters, jv, "logs_extra_filters")
if err != nil {
return err
}
vac.LogsExtraStreamFilters, err = stringSliceFromJSONValue(vac.LogsExtraStreamFilters, jv, "logs_extra_stream_filters")
if err != nil {
return err
}
return nil
}
// TenantID represents tenantID.
@@ -98,12 +425,33 @@ type TenantID struct {
AccountID int32 `json:"account_id"`
}
func (tid *TenantID) parseFrom(jv *fastjson.Value) error {
tidObject := jv.Get("tenant_id")
if tidObject == nil {
return nil
}
var err error
tid.AccountID, err = int32FromJSONValue(tidObject, "account_id")
if err != nil {
return err
}
tid.ProjectID, err = int32FromJSONValue(tidObject, "project_id")
if err != nil {
return err
}
return nil
}
// String implements interface.
func (tid TenantID) String() string {
return fmt.Sprintf("%d:%d", tid.AccountID, tid.ProjectID)
}
// NewToken creates token from raw string.
//
// Deprecated: allocates a new Token on every call.
// Prefer acquiring a Token from a sync.Pool, calling t.Parse(), and returning it after use.
func NewToken(auth string, enforceAuthPrefix bool) (*Token, error) {
if enforceAuthPrefix && (len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix)) {
return nil, fmt.Errorf("wrong format, prefix: %s is missing", prefix)
@@ -122,10 +470,16 @@ func NewToken(auth string, enforceAuthPrefix bool) (*Token, error) {
return nil, ErrBadTokenFormat
}
var t Token
return t.parse(jwt[0], jwt[1], jwt[2])
if err := t.parse(jwt[0], jwt[1], jwt[2]); err != nil {
return nil, err
}
return &t, nil
}
// NewTokenFromRequestWithCustomHeader return new jwt token from request by provided header
//
// Deprecated: allocates a new Token on every call.
// Prefer acquiring a Token from a sync.Pool, calling t.Parse(), and returning it after use.
func NewTokenFromRequestWithCustomHeader(r *http.Request, headerName string, enforceAuthPrefix bool) (*Token, error) {
auth := r.Header.Get(headerName)
if len(auth) == 0 {
@@ -134,28 +488,25 @@ func NewTokenFromRequestWithCustomHeader(r *http.Request, headerName string, enf
return NewToken(auth, enforceAuthPrefix)
}
func (t *Token) parse(header, body, signature string) (*Token, error) {
b, err := parseJWTBody(body)
if err != nil {
return nil, err
func (t *Token) parse(header, body, signature string) error {
if err := t.body.parse(body); err != nil {
return fmt.Errorf("cannot parse token body: %w", err)
}
if b.VMAccess == nil {
return nil, ErrVMAccessFieldMissing
}
t.body = b
h, err := parseJWTHeader(header)
if err != nil {
return nil, err
}
t.header = h
t.payload = []byte(header + "." + body)
t.signature, err = decodeB64([]byte(signature))
if err != nil {
return nil, fmt.Errorf("failed to decode signature as b64: %w", err)
if err := t.header.parse(header); err != nil {
return fmt.Errorf("cannot parse token header: %w", err)
}
return t, nil
t.payload = bytesutil.ResizeNoCopyNoOverallocate(t.payload, len(header)+len(body)+1)
t.payload = append(t.payload[:0], header...)
t.payload = append(t.payload, '.')
t.payload = append(t.payload, body...)
var err error
t.signature, err = decodeB64(t.signature[:0], signature)
if err != nil {
return fmt.Errorf("cannot decode token signature: %w", err)
}
return nil
}
// IsExpired checks if jwt token is expired.
@@ -166,10 +517,10 @@ func (t *Token) IsExpired(currentTime time.Time) bool {
// CanWrite checks if token has write permissions.
func (t *Token) CanWrite() bool {
// unconfigured
if t.body.VMAccess.Mode == 0 {
if t.body.vmAccessClaim.Mode == 0 {
return true
}
if write&t.body.VMAccess.Mode > 0 {
if write&t.body.vmAccessClaim.Mode > 0 {
return true
}
return false
@@ -178,10 +529,10 @@ func (t *Token) CanWrite() bool {
// CanRead check if token has read permissions.
func (t *Token) CanRead() bool {
// unconfigured
if t.body.VMAccess.Mode == 0 {
if t.body.vmAccessClaim.Mode == 0 {
return true
}
if read&t.body.VMAccess.Mode > 0 {
if read&t.body.vmAccessClaim.Mode > 0 {
return true
}
return false
@@ -189,101 +540,36 @@ func (t *Token) CanRead() bool {
// AccessLabels returns vm_access labels for given JWT token,
// in key=value format.
//
// Returned value is only valid until Token is reachable
func (t *Token) AccessLabels() []string {
return t.body.VMAccess.Labels.AsExtraLabels()
return t.body.vmAccessClaim.Labels
}
// Tenant returns tenantID for token.
func (t *Token) Tenant() TenantID {
return t.body.VMAccess.Tenant
return t.body.vmAccessClaim.Tenant
}
// ExtraFilters metricsql filters for select queries
//
// Returned value is only valid until Token is reachable
func (t *Token) ExtraFilters() []string {
return t.body.VMAccess.ExtraFilters
return t.body.vmAccessClaim.ExtraFilters
}
func (t *Token) VMAccess() *VMAccessClaim {
return t.body.VMAccess
}
func parseJWTHeader(data string) (*header, error) {
var jh header
decoded, err := decodeB64([]byte(data))
if err != nil {
return nil, fmt.Errorf("cannot decode jwt header as b64: %w", err)
}
if err := json.Unmarshal(decoded, &jh); err != nil {
return nil, fmt.Errorf("cannot parse jwt header: %w", err)
}
return &jh, nil
}
func parseJWTBody(data string) (*body, error) {
type tbody struct {
// expired at time unix_ts
Exp int64 `json:"exp"`
// issued at time unix_ts
Iat int64 `json:"iat"`
Jti string `json:"jti,omitempty"`
Scope json.RawMessage `json:"scope,omitempty"`
// store as raw message to support different types
VMAccess *json.RawMessage `json:"vm_access"`
}
var tb tbody
decoded, err := decodeB64([]byte(data))
if err != nil {
return nil, fmt.Errorf("cannot decode jwt body as b64: %w", err)
}
if err := json.Unmarshal(decoded, &tb); err != nil {
return nil, fmt.Errorf("cannot parse jwt body: %w", err)
}
if tb.VMAccess == nil {
return nil, ErrVMAccessFieldMissing
}
// some IDPs encode custom claims as a string
// try parsing as an object and fallback to a string
var a VMAccessClaim
if err := json.Unmarshal(*tb.VMAccess, &a); err != nil {
var s string
if err := json.Unmarshal(*tb.VMAccess, &s); err != nil {
return nil, fmt.Errorf("cannot parse jwt body vm_access: %w", err)
}
if err := json.Unmarshal([]byte(s), &a); err != nil {
return nil, fmt.Errorf("cannot parse jwt body vm_access: %w", err)
}
}
// some IDPs encode scope as a string and some as an array
var scope string
if tb.Scope != nil {
if err := json.Unmarshal(tb.Scope, &scope); err != nil {
var scopeSlice []string
if err := json.Unmarshal(tb.Scope, &scopeSlice); err != nil {
return nil, fmt.Errorf("cannot parse jwt body scope: %w", err)
}
scope = strings.Join(scopeSlice, " ")
}
}
parsedBody := &body{
Exp: tb.Exp,
Iat: tb.Iat,
Jti: tb.Jti,
Scope: scope,
VMAccess: &a,
}
return parsedBody, nil
}
func decodeB64(data []byte) ([]byte, error) {
func decodeB64(dst []byte, src string) ([]byte, error) {
data := bytesutil.ToUnsafeBytes(src)
idx := bytes.IndexAny(data, "+/")
// slow path, std base64 encoding convert it to url encoding
// it could be encoded with standard Base64 (+/) instead of Base64URL (-_).
if idx >= 0 {
// make a copy of provided input, src cannot be modified by parser
bb := decodeb64BufferPool.Get()
defer decodeb64BufferPool.Put(bb)
b := bb.B[:0]
b = append(b, data...)
data = b
for idx, c := range data {
switch c {
case '+':
@@ -292,12 +578,94 @@ func decodeB64(data []byte) ([]byte, error) {
data[idx] = '_'
}
}
}
dst := make([]byte, base64.RawURLEncoding.DecodedLen(len(data)))
dst = bytesutil.ResizeNoCopyNoOverallocate(dst, base64.RawURLEncoding.DecodedLen(len(data)))
_, err := base64.RawURLEncoding.Decode(dst, data)
if err != nil {
return nil, fmt.Errorf("cannot decode jwt body as b64: %w", err)
return nil, err
}
return dst, nil
}
// stringFromJSONValue is a helper with missing String parse method from fastjson package
//
// If key is required, perform check with Exists() call
func stringFromJSONValue(jv *fastjson.Value, key string) (string, error) {
jvInner := jv.Get(key)
if jvInner == nil {
return "", nil
}
b, err := jvInner.StringBytes()
if err != nil {
return "", fmt.Errorf("unexpected non-string value for key=%q: %w", key, err)
}
return bytesutil.ToUnsafeString(b), nil
}
// uint32FromJSONValue is a helper for missing Uint32 parse method from fastjson package
//
// If key is required, perform check with Exists() call
func uint32FromJSONValue(jv *fastjson.Value, key string) (uint32, error) {
jvInner := jv.Get(key)
if jvInner == nil {
return 0, nil
}
u64, err := jvInner.Uint64()
if err != nil {
return 0, fmt.Errorf("unexpected non-uint32 value for key=%q: %w", key, err)
}
if u64 > math.MaxUint32 {
return 0, fmt.Errorf("value cannot exceed uint32 for key=%q", key)
}
return uint32(u64), nil
}
// int32FromJSONValue is a helper for missing Int32 parse method from fastjson package
//
// If key is required, perform check with Exists() call
func int32FromJSONValue(jv *fastjson.Value, key string) (int32, error) {
jvInner := jv.Get(key)
if jvInner == nil {
return 0, nil
}
i64, err := jvInner.Int64()
if err != nil {
return 0, fmt.Errorf("unexpected non-int32 value for key=%q: %w", key, err)
}
if i64 > math.MaxInt32 || i64 < math.MinInt32 {
return 0, fmt.Errorf("value cannot exceed int32 for key=%q", key)
}
return int32(i64), nil
}
// stringSliceFromJSONValue is a helper for missing StringArray parse method from fastjson package
//
// If key is required, perform check with Exists() call
func stringSliceFromJSONValue(dst []string, jv *fastjson.Value, key string) ([]string, error) {
jvInner := jv.Get(key)
if jvInner == nil {
return dst, nil
}
if jvInner.Type() != fastjson.TypeArray {
return nil, fmt.Errorf("unexpected type for key=%q, got: %s, want: array string", key, jvInner.Type())
}
for _, ef := range jvInner.GetArray() {
if ef == nil {
continue
}
efs, err := ef.StringBytes()
if err != nil {
return nil, fmt.Errorf("unexpected non string array[] type for key=%q: %w", key, err)
}
dst = append(dst, bytesutil.ToUnsafeString(efs))
}
return dst, nil
}
var parserPool fastjson.ParserPool
var decodeb64BufferPool bytesutil.ByteBufferPool

View File

@@ -17,54 +17,97 @@ func TestParseJWTHeader_Failure(t *testing.T) {
base64.RawURLEncoding.Encode(encoded, []byte(data))
data = string(encoded)
}
if _, err := parseJWTHeader(data); err != nil {
var h header
if err := h.parse(data); err != nil {
if err.Error() != expectedErr {
t.Errorf("unexpected error message: \ngot\n%s\nwant\n%s", err.Error(), expectedErr)
t.Fatalf("unexpected error message: \ngot\n%s\nwant\n%s", err.Error(), expectedErr)
}
} else {
t.Errorf("expecting non-nil error")
t.Fatalf("expecting non-nil error")
}
}
// invalid input
f(
`bad input`,
`cannot decode jwt header as b64: cannot decode jwt body as b64: illegal base64 data at input byte 3`,
`illegal base64 data at input byte 3`,
false,
)
// invalid b644
f(
`YmFk`,
`cannot parse jwt header: invalid character 'b' looking for beginning of value`,
`cannot parse JSON: cannot parse number: unexpected char: "b"; unparsed tail: "bad"`,
false,
)
// invalid header json
f(`{]`,
`cannot parse jwt header: invalid character ']' looking for beginning of object key string`,
`cannot parse JSON: cannot parse object: cannot find opening '"" for object key; unparsed tail: "]"`,
true,
)
// invalid header type json
f(`[]`,
`cannot parse jwt header: json: cannot unmarshal array into Go value of type jwt.header`,
`unexpected non json object {} type: "array"`,
true,
)
// alg field is not a string
f(
`{"alg": 123, "typ": "JWT", "kid": "key-1"}`,
`unexpected non-string value for key="alg": value doesn't contain string; it contains number`,
true,
)
// typ field is not a string
f(
`{"alg": "RS256", "typ": 123, "kid": "key-1"}`,
`unexpected non-string value for key="typ": value doesn't contain string; it contains number`,
true,
)
// kid field is not a string
f(
`{"alg": "RS256", "typ": "JWT", "kid": 123}`,
`unexpected non-string value for key="kid": value doesn't contain string; it contains number`,
true,
)
// standard Base64 with + character (slow path in decodeB64)
f(
`{"alg": "RS256", "typ": "JWT/"}`,
`illegal base64 data at input byte 0`,
false,
)
// invalid header type json
f(`[]`,
`unexpected non json object {} type: "array"`,
true,
)
}
func TestParseJWTHeader_Success(t *testing.T) {
f := func(data string, expected *header) {
f := func(data string, expected header) {
t.Helper()
encodedLen := base64.RawURLEncoding.EncodedLen(len(data))
encoded := make([]byte, encodedLen)
base64.RawURLEncoding.Encode(encoded, []byte(data))
header, err := parseJWTHeader(string(encoded))
var h header
err := h.parse(string(encoded))
if err != nil {
t.Fatalf("parseJWTHeader() error: %s", err)
}
if !reflect.DeepEqual(header, expected) {
t.Fatalf("unexpected token header;\ngot\n%v\nwant\n%v", header, expected)
if h.Alg != expected.Alg {
t.Fatalf("unexpected Alg:\ngot\n%s\nwant\n%s", h.Alg, expected.Alg)
}
if h.Typ != expected.Typ {
t.Fatalf("unexpected Typ:\ngot\n%s\nwant\n%s", h.Typ, expected.Typ)
}
if h.Kid != expected.Kid {
t.Fatalf("unexpected Kid:\ngot\n%s\nwant\n%s", h.Kid, expected.Kid)
}
}
@@ -77,7 +120,7 @@ func TestParseJWTHeader_Success(t *testing.T) {
"alg": %q,
"kid": "test"
}`, supportedAlgorithms[i]),
&header{
header{
Alg: supportedAlgorithms[i],
Kid: "test",
},
@@ -94,40 +137,41 @@ func TestParseJWTBody_Failure(t *testing.T) {
base64.RawURLEncoding.Encode(encoded, []byte(data))
data = string(encoded)
}
if _, err := parseJWTBody(data); err != nil {
var b body
if err := b.parse(data); err != nil {
if err.Error() != expectedErr {
t.Errorf("unexpected error message: \ngot\n%s\nwant\n%s", err.Error(), expectedErr)
t.Fatalf("unexpected error message: \ngot\n%s\nwant\n%s", err.Error(), expectedErr)
}
} else {
t.Errorf("expecting non-nil error")
t.Fatalf("expecting non-nil error")
}
}
// invalid input
f(
`bad input`,
`cannot decode jwt body as b64: cannot decode jwt body as b64: illegal base64 data at input byte 3`,
`illegal base64 data at input byte 3`,
false,
)
// invalid b644
f(
`YmFk`,
`cannot parse jwt body: invalid character 'b' looking for beginning of value`,
`cannot parse JSON: cannot parse number: unexpected char: "b"; unparsed tail: "bad"`,
false,
)
// invalid body json
f(
`{]`,
`cannot parse jwt body: invalid character ']' looking for beginning of object key string`,
`cannot parse JSON: cannot parse object: cannot find opening '"" for object key; unparsed tail: "]"`,
true,
)
// invalid body type json
f(
`[]`,
`cannot parse jwt body: json: cannot unmarshal array into Go value of type jwt.tbody`,
"missing `vm_access` claim",
true,
)
@@ -141,7 +185,7 @@ func TestParseJWTBody_Failure(t *testing.T) {
// vm_access claim invalid type
f(
`{"vm_access": 123}`,
"cannot parse jwt body vm_access: json: cannot unmarshal number into Go value of type string",
"unexpected type for `vm_access` field; got: \"number\", want object {}",
true,
)
@@ -155,14 +199,14 @@ func TestParseJWTBody_Failure(t *testing.T) {
// invalid vm_access: account_id type mismatch
f(
`{"vm_access": {"tenant_id": {"account_id": "1", "project_id": 5}}}`,
`cannot parse jwt body vm_access: json: cannot unmarshal object into Go value of type string`,
`unexpected non-int32 value for key="account_id": value doesn't contain number; it contains string`,
true,
)
// invalid vm_access: project_id type mismatch
f(
`{"vm_access": {"tenant_id": {"account_id": 1, "project_id": "5"}}}`,
`cannot parse jwt body vm_access: json: cannot unmarshal object into Go value of type string`,
`unexpected non-int32 value for key="project_id": value doesn't contain number; it contains string`,
true,
)
@@ -180,7 +224,7 @@ func TestParseJWTBody_Failure(t *testing.T) {
}
}
}`,
`cannot parse jwt body vm_access: json: cannot unmarshal object into Go value of type string`,
"cannot parse `extra_labels` field: value doesn't contain object; it contains array",
true,
)
@@ -195,70 +239,70 @@ func TestParseJWTBody_Failure(t *testing.T) {
}
}
}`,
`cannot parse jwt body vm_access: json: cannot unmarshal object into Go value of type string`,
`unexpected non string array[] type for key="extra_filters": value doesn't contain string; it contains object`,
true,
)
// invalid exp claim value type
f(
`{"exp": "1610976189", "vm_access": {}}`,
`cannot parse jwt body: json: cannot unmarshal string into Go struct field tbody.exp of type int64`,
"cannot parse `exp` field: value doesn't contain number; it contains string",
true,
)
// invalid metrics metrics_account_id claim value type
f(
`{"vm_access": {"metrics_account_id": "1"}}`,
`cannot parse jwt body vm_access: json: cannot unmarshal object into Go value of type string`,
`unexpected non-uint32 value for key="metrics_account_id": value doesn't contain number; it contains string`,
true,
)
// invalid metrics metrics_project_id claim value type
f(
`{"vm_access": {"metrics_project_id": "1"}}`,
`cannot parse jwt body vm_access: json: cannot unmarshal object into Go value of type string`,
`unexpected non-uint32 value for key="metrics_project_id": value doesn't contain number; it contains string`,
true,
)
// invalid metrics metrics_extra_labels claim value type
f(
`{"vm_access": {"metrics_extra_labels": "aString"}}`,
`cannot parse jwt body vm_access: json: cannot unmarshal object into Go value of type string`,
`unexpected type for key="metrics_extra_labels", got: string, want: array string`,
true,
)
// invalid metrics metrics_extra_filters claim value type
f(
`{"vm_access": {"metrics_extra_filters": "aString"}}`,
`cannot parse jwt body vm_access: json: cannot unmarshal object into Go value of type string`,
`unexpected type for key="metrics_extra_filters", got: string, want: array string`,
true,
)
// invalid metrics logs_account_id claim value type
f(
`{"vm_access": {"logs_account_id": "1"}}`,
`cannot parse jwt body vm_access: json: cannot unmarshal object into Go value of type string`,
`unexpected non-uint32 value for key="logs_account_id": value doesn't contain number; it contains string`,
true,
)
// invalid metrics logs_project_id claim value type
f(
`{"vm_access": {"logs_project_id": "1"}}`,
`cannot parse jwt body vm_access: json: cannot unmarshal object into Go value of type string`,
`unexpected non-uint32 value for key="logs_project_id": value doesn't contain number; it contains string`,
true,
)
// invalid metrics logs_extra_filters claim value type
f(
`{"vm_access": {"logs_extra_filters": "aString"}}`,
`cannot parse jwt body vm_access: json: cannot unmarshal object into Go value of type string`,
`unexpected type for key="logs_extra_filters", got: string, want: array string`,
true,
)
// invalid metrics logs_extra_stream_filters claim value type
f(
`{"vm_access": {"logs_extra_stream_filters": "aString"}}`,
`cannot parse jwt body vm_access: json: cannot unmarshal object into Go value of type string`,
`unexpected type for key="logs_extra_stream_filters", got: string, want: array string`,
true,
)
}
@@ -271,7 +315,8 @@ func TestParseJWTBody_Success(t *testing.T) {
encoded := make([]byte, encodedLen)
base64.RawURLEncoding.Encode(encoded, []byte(data))
result, err := parseJWTBody(string(encoded))
var result body
err := result.parse(string(encoded))
if err != nil {
t.Fatalf("parseJWTBody() error: %s", err)
}
@@ -287,22 +332,22 @@ func TestParseJWTBody_Success(t *testing.T) {
if result.Jti != resultExpected.Jti {
t.Fatalf("unexpected jti; got %q; want %q", result.Jti, resultExpected.Jti)
}
if !reflect.DeepEqual(result.VMAccess.Tenant, resultExpected.VMAccess.Tenant) {
t.Fatalf("unexpected tenant; got %v; want %v", result.VMAccess.Tenant, resultExpected.VMAccess.Tenant)
if !reflect.DeepEqual(result.vmAccessClaim.Tenant, resultExpected.vmAccessClaim.Tenant) {
t.Fatalf("unexpected tenant; got %v; want %v", result.vmAccessClaim.Tenant, resultExpected.vmAccessClaim.Tenant)
}
if !reflect.DeepEqual(result.VMAccess.Labels, resultExpected.VMAccess.Labels) {
t.Fatalf("unexpected labels; got %v; want %v", result.VMAccess.Labels, resultExpected.VMAccess.Labels)
if !reflect.DeepEqual(result.vmAccessClaim.Labels, resultExpected.vmAccessClaim.Labels) {
t.Fatalf("unexpected labels; got %v; want %v", result.vmAccessClaim.Labels, resultExpected.vmAccessClaim.Labels)
}
if !reflect.DeepEqual(result.VMAccess.ExtraFilters, resultExpected.VMAccess.ExtraFilters) {
t.Fatalf("unexpected extra_filters; got %v; want %v", result.VMAccess.ExtraFilters, resultExpected.VMAccess.ExtraFilters)
if !reflect.DeepEqual(result.vmAccessClaim.ExtraFilters, resultExpected.vmAccessClaim.ExtraFilters) {
t.Fatalf("unexpected extra_filters; got %v; want %v", result.vmAccessClaim.ExtraFilters, resultExpected.vmAccessClaim.ExtraFilters)
}
}
f(`{"vm_access": {}}`, &body{
VMAccess: &VMAccessClaim{},
vmAccessClaim: VMAccessClaim{},
})
f(`{"vm_access": {"tenant_id": {}}}`, &body{
VMAccess: &VMAccessClaim{},
vmAccessClaim: VMAccessClaim{},
})
f(
@@ -316,7 +361,7 @@ func TestParseJWTBody_Success(t *testing.T) {
}
}`,
&body{
VMAccess: &VMAccessClaim{
vmAccessClaim: VMAccessClaim{
Tenant: TenantID{
ProjectID: 5,
AccountID: 1,
@@ -336,10 +381,10 @@ func TestParseJWTBody_Success(t *testing.T) {
}
}`,
&body{
VMAccess: &VMAccessClaim{
Labels: Labels{
"project": "dev",
"team": "mobile",
vmAccessClaim: VMAccessClaim{
Labels: []string{
"project=dev",
"team=mobile",
},
},
},
@@ -356,7 +401,7 @@ func TestParseJWTBody_Success(t *testing.T) {
}
}`,
&body{
VMAccess: &VMAccessClaim{
vmAccessClaim: VMAccessClaim{
ExtraFilters: []string{
`{project="dev"}`,
`{team=~"mobile"}`,
@@ -384,14 +429,14 @@ func TestParseJWTBody_Success(t *testing.T) {
}
}`,
&body{
VMAccess: &VMAccessClaim{
vmAccessClaim: VMAccessClaim{
Tenant: TenantID{
ProjectID: 5,
AccountID: 1,
},
Labels: Labels{
"project": "dev",
"team": "mobile",
Labels: []string{
"project=dev",
"team=mobile",
},
ExtraFilters: []string{
`{project="dev"}`,
@@ -411,11 +456,27 @@ func TestParseJWTBody_Success(t *testing.T) {
"vm_access": {}
}`,
&body{
Exp: 1610976189,
Iat: 1610975889,
Jti: "9b194187-6bb7-4244-9d1b-559eab2ef7f3",
Scope: "openid email profile",
VMAccess: &VMAccessClaim{},
Exp: 1610976189,
Iat: 1610975889,
Jti: "9b194187-6bb7-4244-9d1b-559eab2ef7f3",
Scope: "openid email profile",
},
)
// scope as []string
f(
`
{
"exp": 1610976189,
"iat": 1610975889,
"jti": "9b194187-6bb7-4244-9d1b-559eab2ef7f3",
"scope": ["openid","email","profile"],
"vm_access": {}
}`,
&body{
Exp: 1610976189,
Iat: 1610975889,
Jti: "9b194187-6bb7-4244-9d1b-559eab2ef7f3",
Scope: "openid email profile",
},
)
@@ -436,7 +497,7 @@ func TestParseJWTBody_Success(t *testing.T) {
}
}`,
&body{
VMAccess: &VMAccessClaim{
vmAccessClaim: VMAccessClaim{
MetricsAccountID: 1,
MetricsProjectID: 5,
MetricsExtraLabels: []string{
@@ -466,7 +527,7 @@ func TestParseJWTBody_Success(t *testing.T) {
}
}`,
&body{
VMAccess: &VMAccessClaim{
vmAccessClaim: VMAccessClaim{
LogsAccountID: 1,
LogsProjectID: 5,
LogsExtraFilters: []string{
@@ -521,8 +582,18 @@ func TestNewTokenFromRequest_Success(t *testing.T) {
if err != nil {
t.Fatalf("NewTokenFromRequest() error: %s", err)
}
if !reflect.DeepEqual(result.body.VMAccess, resultExpected.body.VMAccess) {
t.Fatalf("unexpected token body VMAccess;\ngot\n%v\nwant\n%v", result.body.VMAccess, resultExpected.body.VMAccess)
// assign nil values to simplify equal check below
result.header.buf = nil
result.header.p = nil
result.body.vmAccessClaim.labelsBuf = nil
if result.body.Iat != resultExpected.body.Iat {
t.Fatalf("unexpected iat: %d;%d", result.body.Iat, resultExpected.body.Iat)
}
if result.body.Exp != resultExpected.body.Exp {
t.Fatalf("unexpected exp: %d;%d", result.body.Exp, resultExpected.body.Exp)
}
if !reflect.DeepEqual(result.body.vmAccessClaim, resultExpected.body.vmAccessClaim) {
t.Fatalf("unexpected token body VMAccess;\ngot\n%v\nwant\n%v", result.body.vmAccessClaim, resultExpected.body.vmAccessClaim)
}
if !reflect.DeepEqual(result.header, resultExpected.header) {
t.Fatalf("unexpected token header\ngot\n%v\nwant\n%v", result.header, resultExpected.header)
@@ -538,23 +609,23 @@ func TestNewTokenFromRequest_Success(t *testing.T) {
},
}
resultExpected := &Token{
body: &body{
Exp: 1610889266,
Iat: 1610888966,
body: body{
Exp: 1610976189,
Iat: 1610975889,
Jti: "09a058a2-0752-4ecd-a4e9-b65e85af423f",
Scope: "openid email profile",
VMAccess: &VMAccessClaim{
vmAccessClaim: VMAccessClaim{
Tenant: TenantID{
ProjectID: 5,
AccountID: 1,
},
Labels: map[string]string{
"project": "dev",
"team": "mobile",
Labels: []string{
"project=dev",
"team=mobile",
},
},
},
header: &header{
header: header{
Alg: "RS256",
Kid: "aAZoCGvuGbFoftWHxQZyRSQen3yX4U0GPlP5oZOQSwc",
Typ: "JWT",
@@ -571,23 +642,23 @@ func TestNewTokenFromRequest_Success(t *testing.T) {
},
}
resultExpected = &Token{
body: &body{
Exp: 1610889266,
Iat: 1610888966,
body: body{
Exp: 1610976189,
Iat: 1610975889,
Jti: "09a058a2-0752-4ecd-a4e9-b65e85af423f",
Scope: "openid email profile",
VMAccess: &VMAccessClaim{
vmAccessClaim: VMAccessClaim{
Tenant: TenantID{
ProjectID: 5,
AccountID: 1,
},
Labels: map[string]string{
"project": "dev",
"team": "mobile",
Labels: []string{
"project=dev",
"team=mobile",
},
},
},
header: &header{
header: header{
Alg: "RS256",
Kid: "aAZoCGvuGbFoftWHxQZyRSQen3yX4U0GPlP5oZOQSwc",
Typ: "JWT",
@@ -604,8 +675,10 @@ func TestNewTokenFromRequest_Success(t *testing.T) {
},
}
resultExpected = &Token{
body: &body{
VMAccess: &VMAccessClaim{
body: body{
Iat: 1645536638,
Exp: 1645536758,
vmAccessClaim: VMAccessClaim{
Tenant: TenantID{
ProjectID: 0,
AccountID: 1,
@@ -616,7 +689,7 @@ func TestNewTokenFromRequest_Success(t *testing.T) {
Mode: 1,
},
},
header: &header{
header: header{
Alg: "HS256",
Typ: "JWT",
},
@@ -632,8 +705,10 @@ func TestNewTokenFromRequest_Success(t *testing.T) {
},
}
resultExpected = &Token{
body: &body{
VMAccess: &VMAccessClaim{
body: body{
Iat: 1645606878,
Exp: 1645606998,
vmAccessClaim: VMAccessClaim{
Tenant: TenantID{
ProjectID: 0,
AccountID: 1,
@@ -644,7 +719,7 @@ func TestNewTokenFromRequest_Success(t *testing.T) {
Mode: 1,
},
},
header: &header{
header: header{
Alg: "HS256",
Typ: "JWT",
},
@@ -660,24 +735,24 @@ func TestNewTokenFromRequest_Success(t *testing.T) {
},
}
resultExpected = &Token{
body: &body{
Exp: 1610889266,
Iat: 1610888966,
body: body{
Exp: 1610976189,
Iat: 1610975889,
Jti: "09a058a2-0752-4ecd-a4e9-b65e85af423f",
Scope: "openid email profile",
VMAccess: &VMAccessClaim{
vmAccessClaim: VMAccessClaim{
Tenant: TenantID{
ProjectID: 5,
AccountID: 1,
},
Labels: map[string]string{
"project": "dev",
"team": "mobile",
Labels: []string{
"project=dev",
"team=mobile",
},
ExtraFilters: []string{`{env=~"prod|dev"}`, `{team!="test"}`},
},
},
header: &header{
header: header{
Alg: "HS256",
Kid: "aAZoCGvuGbFoftWHxQZyRSQen3yX4U0GPlP5oZOQSwc",
Typ: "JWT",
@@ -694,24 +769,24 @@ func TestNewTokenFromRequest_Success(t *testing.T) {
},
}
resultExpected = &Token{
body: &body{
Exp: 1610889266,
Iat: 1610888966,
body: body{
Exp: 1610976189,
Iat: 1610975889,
Jti: "09a058a2-0752-4ecd-a4e9-b65e85af423f",
Scope: "openid email profile",
VMAccess: &VMAccessClaim{
vmAccessClaim: VMAccessClaim{
Tenant: TenantID{
ProjectID: 5,
AccountID: 1,
},
Labels: map[string]string{
"project": "dev",
"team": "mobile",
Labels: []string{
"project=dev",
"team=mobile",
},
ExtraFilters: []string{`{env=~"prod|dev"}`, `{team!="test"}`},
},
},
header: &header{
header: header{
Alg: "HS256",
Kid: "aAZoCGvuGbFoftWHxQZyRSQen3yX4U0GPlP5oZOQSwc",
Typ: "JWT",
@@ -729,17 +804,17 @@ func TestNewTokenFromRequest_Success(t *testing.T) {
}
resultExpected = &Token{
body: &body{
body: body{
Exp: 1725629232,
Iat: 1725625332,
VMAccess: &VMAccessClaim{
vmAccessClaim: VMAccessClaim{
Tenant: TenantID{
ProjectID: 5,
AccountID: 1,
},
},
},
header: &header{
header: header{
Alg: "RS256",
Kid: "H9nj5AOSswMphg1SFx7jaV-lB9w",
Typ: "JWT",
@@ -757,17 +832,17 @@ func TestNewTokenFromRequest_Success(t *testing.T) {
}
resultExpected = &Token{
body: &body{
body: body{
Exp: 1725629232,
Iat: 1725625332,
VMAccess: &VMAccessClaim{
vmAccessClaim: VMAccessClaim{
Tenant: TenantID{
ProjectID: 5,
AccountID: 1,
},
},
},
header: &header{
header: header{
Alg: "RS256",
Kid: "H9nj5AOSswMphg1SFx7jaV-lB9w",
Typ: "JWT",

View File

@@ -0,0 +1,38 @@
package jwt
import "testing"
func BenchmarkTokenParse(t *testing.B) {
f := func(name string, rawToken string) {
t.Helper()
t.Run(name, func(t *testing.B) {
t.ReportAllocs()
t.RunParallel(func(pb *testing.PB) {
var jt Token
for pb.Next() {
jt.Reset()
if err := jt.Parse(rawToken, true); err != nil {
t.Fatalf("unexpected parsing error: %s", err)
}
}
})
})
}
// simple token with only tenant_id
f("simple", `Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFBWm9DR3Z1R2JGb2Z0V0h4UVp5UlNRZW4zeVg0VTBHUGxQNW9aT1FTd2MifQ.eyJleHAiOjE2MTA5NzYxOTAsImlhdCI6MTYxMDk3NTg4OSwiYXV0aF90aW1lIjoxNjEwOTc1ODg5LCJqdGkiOiI5YjE5NDE4Ny02YmI3LTQyNDQtOWQxYi01NTllYWIyZWY3ZjMiLCJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo4NDQzL2F1dGgvcmVhbG1zL3Rlc3QiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiNDYwODU5NDEtYjkyYi00NzFhLWIwNWEtOTU5OWNhMjlkYTFlIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiZ3JhZmFuYSIsInNlc3Npb25fc3RhdGUiOiIxMzc3ZDEwMi03NTJiLTQ0ODYtOTlkYS1jMjA4MjRiODJkMzEiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJ2bV9hY2Nlc3MiOnsidGVuYW50X2lkIjp7ImFjY291bnRfaWQiOjEsInByb2plY3RfaWQiOjV9fSwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoidGcgdGciLCJwcm9qZWN0IjoibW9iaWxlIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidGciLCJ0ZWFtIjoiZGV2IiwiZ2l2ZW5fbmFtZSI6InRnIiwiZmFtaWx5X25hbWUiOiJ0ZyIsImVtYWlsIjoidGdAZmdodC5uZXQifQ.mpT7_kGOIZtoRv2Tn-_80YXmy7_3Qc4_xeaQr1Nhk4UyXSeWh6HB96wWkBS8Jhj3NksGj7bqxezOEbOBBaqlYn6cdGV2hVZ8GKT2zt6oRLCuuUORiRU1joBeIhVRMNtXvPXLFTs4e1VIKejncWbeKmXSneYCjJityixQza0mVyO7ldiXHc6J2f_wQJDPkwkFJJvfwwTbyu4maUzv5gNIvVSUfWnjPq3skFmnjwpsfD9KZnZg-pPTKUmri6kdK0YrFTGA5HT_DM77UkXzsDSMdHPP5tgiPD3LeK75djTZdMAidX53ai85BDn9d5vzi9nfoVezyN3dh0xqqaaQJGqjng`)
// gateway extra labels and extra filters
f("gateway labels and filters", `Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFBWm9DR3Z1R2JGb2Z0V0h4UVp5UlNRZW4zeVg0VTBHUGxQNW9aT1FTd2MifQ.eyJleHAiOjE2MTA5NzYxOTAsImlhdCI6MTYxMDk3NTg4OSwiYXV0aF90aW1lIjoxNjEwOTc1ODg5LCJqdGkiOiI5YjE5NDE4Ny02YmI3LTQyNDQtOWQxYi01NTllYWIyZWY3ZjMiLCJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo4NDQzL2F1dGgvcmVhbG1zL3Rlc3QiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiNDYwODU5NDEtYjkyYi00NzFhLWIwNWEtOTU5OWNhMjlkYTFlIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiZ3JhZmFuYSIsInNlc3Npb25fc3RhdGUiOiIxMzc3ZDEwMi03NTJiLTQ0ODYtOTlkYS1jMjA4MjRiODJkMzEiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJ2bV9hY2Nlc3MiOnsidGVuYW50X2lkIjp7ImFjY291bnRfaWQiOjEsInByb2plY3RfaWQiOjV9LCJleHRyYV9sYWJlbHMiOnsiZW52IjoicHJvZCIsInRlYW0iOiJvcHMifSwiZXh0cmFfZmlsdGVycyI6WyJtZXRyaWMiLCJ7c2VsZWN0b3I9XCJ2YWx1ZVwiIl19LCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJ0ZyB0ZyIsInByb2plY3QiOiJtb2JpbGUiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZyIsInRlYW0iOiJkZXYiLCJnaXZlbl9uYW1lIjoidGciLCJmYW1pbHlfbmFtZSI6InRnIiwiZW1haWwiOiJ0Z0BmZ2h0Lm5ldCJ9.lUEn5nVQ6Trra-9YkbMKyhL0eiWmL2VSIKj2HDQSH43ZkeagLPQbPTnLYfkbuc1sI9tPcyFOPuwgdEAkckEgQ7szvw9g5bLrtT4etWoOnPJ1GaQpcn0z16w7bAgbMf8rpb0i4JMOXicRd7ARlkjyJZDjehaVUX726052qv2NG7npShafK0wei1QBpD3N34TJlqixbOnD1DCfsorwxzba8OuwgQI8lfTHWmgFO0611DGKZb1a-srTPZ5ziZ29NhtDAkbx6bZnYHMp_8CTLD6p0z34RM2wPWyI_2_AdKDbqkdDSZoapJneQDdoNsmMA0IUFETqBgfRTavnApkgeu12HA`)
// scope as []string
f("scope as slice string", `Bearer ewogICJ0eXAiOiJKV1QiLAogICJhbGciOiJSUzI1NiIsCiAgImtpZCI6Ikg5bmo1QU9Tc3dNcGhnMVNGeDdqYVYtbEI5dyIKfQ.ewogICJhdWQiOiI3YTczMTFlNy1iYTdlLTQ5NWUtOTk1ZS1hZjUzNGU3M2MxMTAiLAogICJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vMjVkYTFlY2UtNjY5MS00ODY4LWE3N2ItMWIwZjliYmU1ZjQzL3YyLjAiLAogICJpYXQiOjE3MjU2MjUzMzIsCiAgIm5iZiI6MTcyNTYyNTMzMiwKICAiZXhwIjoxNzI1NjI5MjMyLAogICJuYW1lIjoiWmFraGFyIEJlc3NhcmFiIiwKICAib2lkIjoiOGI5ZWY2YjMtMWMwMS00YjczLTg0ODItMjRkNmI2NTE1Y2U0IiwKICAicHJlZmVycmVkX3VzZXJuYW1lIjoiei5iZXNzYXJhYkB2aWN0b3JpYW1ldHJpY3MuY29tIiwKICAicmgiOiIwLkFXTUJ6aDdhSlpGbWFFaW5leHNQbTc1ZlEtY1JjM3AtdWw1Sm1WNnZVMDV6d1JCakFaby4iLAogICJzdWIiOiJXRld3QTlYZjZpZXUxLUgwNDBuU0QxRVo3UWxOLTVHbWxob2p4czdMUFJRIiwKICAidGlkIjoiMjVkYTFlY2UtNjY5MS00ODY4LWE3N2ItMWIwZjliYmU1ZjQzIiwKICAidXRpIjoidlo1MjQySmhNVWFUUktaYVFCRjhBQSIsCiAgInZlciI6IjIuMCIsCiAgInZtX2FjY2VzcyI6IntcInRlbmFudF9pZFwiOntcInByb2plY3RfaWRcIjogNSwgXCJhY2NvdW50X2lkXCI6IDF9fSIsCiAgInNjb3BlIjogWyJvcGVuaWQiLCAidm0iXQp9.ZXdvZ0lDSjBlWEFpT2lKS1YxUWlMQW9nSUNKaGJHY2lPaUpTVXpJMU5pSXNDaUFnSW10cFpDSTZJa2c1Ym1vMVFVOVRjM2ROY0dobk1WTkdlRGRxWVZZdGJFSTVkeUlLZlEuLktrUG9qNWJoaDNWcnRyY3RVb0lHaE5vN2hNc2VGT3hESGVEQ2g3MFViV2l2LU5pb1Zia2duZk1CMkhacHN6WGU5WmNmX2FIaURJSVNTYkNTaDlvQnF1aS02OEJDcmplNFJWRkpGZFV6R3V1SmdOTS11YVpBcFJqSFNNZDUxb2RvbHFoUGFHS09URnJXVmlIWlpfVDdXaVNUcV84U3Y1a2x1Y2xMb0hEcU82MU5Na2w0TmRCVnQxM1hjRTBfM243U3VxTDdpaks2dGMwZ2NzcmJ5c3JNdl9jd2VRamZsLU5fV0N0SG40NnhadEhvX0RpZERabzc2TjV1NE52Uk1OZUxNcXZ0YTgzUzhPdzNyUUlhaUFjUUNHYjBqUU5hV2VEQlFzZUZ6SjRyR0h6RjAwZDlqVkNCSHVWRmI5eHNnSnJVUDZ0S05iT2hTeEY1RzBocElVYk5OUQ`)
// vm_access string
f("access claim string", `Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ikg5bmo1QU9Tc3dNcGhnMVNGeDdqYVYtbEI5dyJ9.eyJhdWQiOiI3YTczMTFlNy1iYTdlLTQ5NWUtOTk1ZS1hZjUzNGU3M2MxMTAiLCJpc3MiOiJodHRwczovLzFlY2UtNjY5MS00ODY4LWE3N2ItMWIwZjliYmU1ZjQzL3YyLjAiLCJpYXQiOjE3MjU2MjUzMzIsIm5iZiI6MTcyNTYyNTMzMiwiZXhwIjoxNzI1NjI5MjMyLCJvaWQiOiIwMDAwMC0xYzAxLTRiNzMtODQ4Mi0yNGQ2YjY1MTVjZTQiLCJ0aWQiOiIwMC02NjkxLTQ4NjgtYTc3Yi0xYjBmOWJiZTVmNDMiLCJ1dGkiOiJ2WjUyNDJKaE1VYVRSS1phUUJGOEFBIiwidmVyIjoiMi4wIiwidm1fYWNjZXNzIjoie1widGVuYW50X2lkXCI6e1wicHJvamVjdF9pZFwiOiA1LCBcImFjY291bnRfaWRcIjogMX19In0.RYYL-Ct-a3dlToRCemUCDbnY_HIFeJ1Feqzj6yXcchy_VtE0DjGu-qGspwPHsJe_JlgHSegN_wSlCLAuorO4vQxIVYansL-6AOQ8fiAh_HRA1dID6lvmxYIkCxNFIEyc7ufp7QJYZiyT_lKJkDOrXqWuJ5l_ajLVRSGK1kWRL0V_e6BsU8-2NF_f1gkPEpULooHmQfpdNszZwPpN_Hyd24gQmSbTZk1MA1jkuo6LLuMDyZK2UDnRQA3Xx480LYnl-VzlBLwv5fwEGFwOJC_E9olvAJxr8eYJEQA4lwsdpwmfJkWBlrdcOZNHzmNaTWMFxmDIBOirH-CUm9ndF2r-Og`)
// vmauth related claim fields
f("vmauth related fields", `Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ikg5bmo1QU9Tc3dNcGhnMVNGeDdqYVYtbEI5dyJ9.eyJhdWQiOiIwMDAwMC1iYTdlLTQ5NWUtOTk1ZS1hZjUzNGU3M2MxMTAiLCJpc3MiOiJodHRwczovL2xvY2FsaG9zdC92Mi4wIiwiaWF0IjoxNzI1NjI1MzMyLCJuYmYiOjE3MjU2MjUzMzIsImV4cCI6MTcyNTYyOTIzMiwidmVyIjoiMi4wIiwidm1fYWNjZXNzIjp7Im1ldHJpY3NfYWNjb3VudF9pZCI6MTAwLCJtZXRyaWNzX3Byb2plY3RfaWQiOjEwMDA1LCJtZXRyaWNzX2V4dHJhX2ZpbHRlcnMiOlsie2ZpbHRlcj1cIjFcIn0iLCJ7ZmlsdGVyMj1cIjJcIn0iXSwibWV0cmljc19leHRyYV9sYWJlbHMiOlsia2V5PXZhbHVlIiwib3RoZXJfbGFiZWw9dmFsdWUiXSwibG9nc19hY2NvdW50X2lkIjo1MDAsImxvZ3NfcHJvamVjdF9pZCI6NTU1NSwibG9nc19leHRyYV9maWx0ZXJzIjpbImZpbHRlcj12YWx1ZSIsIm90aGVyX2ZpbGVyIl0sImxvZ3NfZXh0cmFfc3RyZWFtX2ZpbHRlcnMiOlsic3RyZWFtIGZpbHRlciIsIm90aGVyIHN0cmVhbSBmaWx0ZXIiLCJsYXN0IHN0cmVhbSBmaWx0ZXIiXX0sInNjb3BlIjoib3BlbmlkIn0.SVRbfypXpzJ1FL2ALu9_iO_J_UXTS0MiUX4SJ8ZqmN-JAsR8SudJAe1Lk8uubTsRtb234a8QYuzR1XhMLwM6SDkuioKC2VAGPV2YPb5Z7axv0juShJfZkaBaqf-zz_bx51-Bop6Xlpg5zySymYs9mLRwGKfIKMiIZVF5d0mDnG-BUawstQZX3RvVWODrLucIPiuJy9ry_tQz1uYbL8eadeqezfAPprB-bxGSScZ4SKeSW9j3wksB2zvAidlj5ZMnmkDRcXCkBgBxazQ0KeHXPly8kkC4yREtZiBCVz1HKsCncO-iWR2DFCf5jLwHiJVuwsTIjj7jdb9Hxgiu_CS3eA`)
}