forked from mirror/jwt
Compare commits
56 Commits
Author | SHA1 | Date |
---|---|---|
re | 29918af7f7 | |
Alexander Yastrebov | 9358574a7a | |
Christian Banse | 2f0984a28b | |
Christian Banse | 2101c1f4bc | |
Krouton | 35053d4e20 | |
Jacob Kopczynski | 0c4e387985 | |
Christian Banse | bfea432b1a | |
Michael Fridman | d81acbf7f3 | |
Hugo | fdaf0eb0e0 | |
KroKite | f2878bb94b | |
George Kechagias | 9294af54b5 | |
Qian Qiao | 2da0bf7566 | |
Christian Banse | 8fb42696ff | |
Michael Fridman | cf43decf7c | |
Michael Fridman | 4426925f0c | |
Håvard Anda Estensen | f6c6299f67 | |
Luigi Morel | 89a6400b7f | |
Vladislav Polyakov | 6e2ab4291f | |
Christian Banse | 83478b3c8f | |
Michael Fridman | 0972257eba | |
Michael Fridman | 1096e506e6 | |
ksegun | d489c99d3e | |
ydylla | 6de17d3b3e | |
Michael Fridman | 279dd19720 | |
Giau. Tran Minh | 863d23d08a | |
Michael Fridman | 2387103809 | |
Máté Lang | d0c0939ff8 | |
hyeonjae | e01ed05a31 | |
Christian Banse | 78a18c0808 | |
Stefan Tudose | 0fb40d3824 | |
tfonfara | c435f38291 | |
Alexander Yastrebov | a725c1f60c | |
Kevin de Berk | 823c014036 | |
Alexander Yastrebov | 1275a5b909 | |
ajermaky | f4865cddea | |
Alexander Yastrebov | 9c3665f0fc | |
PiotrKozimor | a2aa655627 | |
Sebastien Rosset | c0ffb890f3 | |
Christian Banse | 65357b9e5b | |
Ichinose Shogo | cac353cdc2 | |
Sebastien Rosset | fd8cd69d8e | |
Hinagiku Soranoba | 02bc1ac506 | |
Hyun | d2c5d5ab01 | |
Yoan Blanc | 205b3dc4bb | |
Michael Fridman | 93130d3c71 | |
yoogo | 3f50a786ff | |
Mark Karpelès | 2bd8ee77fc | |
Christian Banse | 80625fb516 | |
Luis Gabriel Gomez | c9ab96ba53 | |
Alexander F. Rødseth | eac9e9edf2 | |
Michael Fridman | a06361ba65 | |
Zach Wasserman | bac80eaac8 | |
Francois Lebel | 85f0a979dd | |
Luis Gabriel Gomez | 3258b3fca0 | |
Christian Banse | bd2db2d4a2 | |
Michael Fridman | 2ebb50f957 |
|
@ -8,26 +8,43 @@ on:
|
|||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: reviewdog/action-staticcheck@v1
|
||||
with:
|
||||
github_token: ${{ secrets.github_token }}
|
||||
reporter: github-pr-review
|
||||
filter_mode: nofilter
|
||||
fail_on_error: true
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: [1.15, 1.16]
|
||||
go: [1.17, 1.18, 1.19]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: src/github.com/golang-jwt/jwt
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "${{ matrix.go }}"
|
||||
check-latest: true
|
||||
cache: true
|
||||
- name: Check Go code formatting
|
||||
run: |
|
||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||
gofmt -s -l .
|
||||
echo "Please format Go code by running: go fmt ./..."
|
||||
exit 1
|
||||
fi
|
||||
- name: Build
|
||||
run: |
|
||||
go install github.com/mfridman/tparse@latest
|
||||
go vet ./...
|
||||
go test -v ./...
|
||||
go test -v -race -count=1 -json -coverpkg=$(go list ./...) ./... | tparse -follow -notests
|
||||
go build ./...
|
||||
env:
|
||||
GO111MODULE: auto
|
||||
GOPATH: ${{ github.workspace }}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
# pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
# branches: [ main ]
|
||||
schedule:
|
||||
- cron: '31 10 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
|
@ -1,22 +1,22 @@
|
|||
## Migration Guide (v3.2.1)
|
||||
## Migration Guide (v4.0.0)
|
||||
|
||||
Starting from [v3.2.1](https://github.com/golang-jwt/jwt/releases/tag/v3.2.1]), the import path has changed from `github.com/dgrijalva/jwt-go` to `github.com/golang-jwt/jwt`. Future releases will be using the `github.com/golang-jwt/jwt` import path and continue the existing versioning scheme of `v3.x.x+incompatible`. Backwards-compatible patches and fixes will be done on the `v3` release branch, where as new build-breaking features will be developed in a `v4` release, possibly including a SIV-style import path.
|
||||
Starting from [v4.0.0](https://github.com/golang-jwt/jwt/releases/tag/v4.0.0), the import path will be:
|
||||
|
||||
### go.mod replacement
|
||||
"git.internal/re/jwt/v4"
|
||||
|
||||
In a first step, the easiest way is to use `go mod edit` to issue a replacement.
|
||||
The `/v4` version will be backwards compatible with existing `v3.x.y` tags in this repo, as well as
|
||||
`github.com/dgrijalva/jwt-go`. For most users this should be a drop-in replacement, if you're having
|
||||
troubles migrating, please open an issue.
|
||||
|
||||
You can replace all occurrences of `github.com/dgrijalva/jwt-go` or `github.com/golang-jwt/jwt` with `git.internal/re/jwt/v4`, either manually or by using tools such as `sed` or `gofmt`.
|
||||
|
||||
And then you'd typically run:
|
||||
|
||||
```
|
||||
go mod edit -replace github.com/dgrijalva/jwt-go=github.com/golang-jwt/jwt@v3.2.1+incompatible
|
||||
go get git.internal/re/jwt/v4
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
This will still keep the old import path in your code but replace it with the new package and also introduce a new indirect dependency to `github.com/golang-jwt/jwt`. Try to compile your project; it should still work.
|
||||
|
||||
### Cleanup
|
||||
|
||||
If your code still consistently builds, you can replace all occurences of `github.com/dgrijalva/jwt-go` with `github.com/golang-jwt/jwt`, either manually or by using tools such as `sed`. Finally, the `replace` directive in the `go.mod` file can be removed.
|
||||
|
||||
## Older releases (before v3.2.0)
|
||||
|
||||
The original migration guide for older releases can be found at https://github.com/dgrijalva/jwt-go/blob/master/MIGRATION_GUIDE.md.
|
61
README.md
61
README.md
|
@ -1,13 +1,15 @@
|
|||
# jwt-go
|
||||
|
||||
[![build](https://github.com/golang-jwt/jwt/actions/workflows/build.yml/badge.svg)](https://github.com/golang-jwt/jwt/actions/workflows/build.yml)
|
||||
[![Go Reference](https://pkg.go.dev/badge/github.com/golang-jwt/jwt.svg)](https://pkg.go.dev/github.com/golang-jwt/jwt)
|
||||
[![Go Reference](https://pkg.go.dev/badge/git.internal/re/jwt/v4.svg)](https://pkg.go.dev/git.internal/re/jwt/v4)
|
||||
|
||||
A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](https://datatracker.ietf.org/doc/html/rfc7519).
|
||||
|
||||
**IMPORT PATH CHANGE:** Starting from [v3.2.1](https://github.com/golang-jwt/jwt/releases/tag/v3.2.1), the import path has changed from `github.com/dgrijalva/jwt-go` to `github.com/golang-jwt/jwt`. After the original author of the library suggested migrating the maintenance of `jwt-go`, a dedicated team of open source maintainers decided to clone the existing library into this repository. See [dgrijalva/jwt-go#462](https://github.com/dgrijalva/jwt-go/issues/462) for a detailed discussion on this topic.
|
||||
Starting with [v4.0.0](https://github.com/golang-jwt/jwt/releases/tag/v4.0.0) this project adds Go module support, but maintains backwards compatibility with older `v3.x.y` tags and upstream `github.com/dgrijalva/jwt-go`.
|
||||
See the [`MIGRATION_GUIDE.md`](./MIGRATION_GUIDE.md) for more information.
|
||||
|
||||
> After the original author of the library suggested migrating the maintenance of `jwt-go`, a dedicated team of open source maintainers decided to clone the existing library into this repository. See [dgrijalva/jwt-go#462](https://github.com/dgrijalva/jwt-go/issues/462) for a detailed discussion on this topic.
|
||||
|
||||
Future releases will be using the `github.com/golang-jwt/jwt` import path and continue the existing versioning scheme of `v3.x.x+incompatible`. Backwards-compatible patches and fixes will be done on the `v3` release branch, where as new build-breaking features will be developed in a `v4` release, possibly including a SIV-style import path.
|
||||
|
||||
**SECURITY NOTICE:** Some older versions of Go have a security issue in the crypto/elliptic. Recommendation is to upgrade to at least 1.15 See issue [dgrijalva/jwt-go#216](https://github.com/dgrijalva/jwt-go/issues/216) for more detail.
|
||||
|
||||
|
@ -34,23 +36,45 @@ The part in the middle is the interesting bit. It's called the Claims and conta
|
|||
|
||||
This library supports the parsing and verification as well as the generation and signing of JWTs. Current supported signing algorithms are HMAC SHA, RSA, RSA-PSS, and ECDSA, though hooks are present for adding your own.
|
||||
|
||||
## Installation Guidelines
|
||||
|
||||
1. To install the jwt package, you first need to have [Go](https://go.dev/doc/install) installed, then you can use the command below to add `jwt-go` as a dependency in your Go program.
|
||||
|
||||
```sh
|
||||
go get -u git.internal/re/jwt/v4
|
||||
```
|
||||
|
||||
2. Import it in your code:
|
||||
|
||||
```go
|
||||
import "git.internal/re/jwt/v4"
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See [the project documentation](https://pkg.go.dev/github.com/golang-jwt/jwt) for examples of usage:
|
||||
See [the project documentation](https://pkg.go.dev/git.internal/re/jwt/v4) for examples of usage:
|
||||
|
||||
* [Simple example of parsing and validating a token](https://pkg.go.dev/github.com/golang-jwt/jwt#example-Parse-Hmac)
|
||||
* [Simple example of building and signing a token](https://pkg.go.dev/github.com/golang-jwt/jwt#example-New-Hmac)
|
||||
* [Directory of Examples](https://pkg.go.dev/github.com/golang-jwt/jwt#pkg-examples)
|
||||
* [Simple example of parsing and validating a token](https://pkg.go.dev/git.internal/re/jwt/v4#example-Parse-Hmac)
|
||||
* [Simple example of building and signing a token](https://pkg.go.dev/git.internal/re/jwt/v4#example-New-Hmac)
|
||||
* [Directory of Examples](https://pkg.go.dev/git.internal/re/jwt/v4#pkg-examples)
|
||||
|
||||
## Extensions
|
||||
|
||||
This library publishes all the necessary components for adding your own signing methods. Simply implement the `SigningMethod` interface and register a factory method using `RegisterSigningMethod`.
|
||||
This library publishes all the necessary components for adding your own signing methods or key functions. Simply implement the `SigningMethod` interface and register a factory method using `RegisterSigningMethod` or provide a `jwt.Keyfunc`.
|
||||
|
||||
Here's an example of an extension that integrates with multiple Google Cloud Platform signing tools (AppEngine, IAM API, Cloud KMS): https://github.com/someone1/gcp-jwt-go
|
||||
A common use case would be integrating with different 3rd party signature providers, like key management services from various cloud providers or Hardware Security Modules (HSMs) or to implement additional standards.
|
||||
|
||||
| Extension | Purpose | Repo |
|
||||
| --------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
|
||||
| GCP | Integrates with multiple Google Cloud Platform signing tools (AppEngine, IAM API, Cloud KMS) | https://github.com/someone1/gcp-jwt-go |
|
||||
| AWS | Integrates with AWS Key Management Service, KMS | https://github.com/matelang/jwt-go-aws-kms |
|
||||
| JWKS | Provides support for JWKS ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) as a `jwt.Keyfunc` | https://github.com/MicahParks/keyfunc |
|
||||
|
||||
*Disclaimer*: Unless otherwise specified, these integrations are maintained by third parties and should not be considered as a primary offer by any of the mentioned cloud providers
|
||||
|
||||
## Compliance
|
||||
|
||||
This library was last reviewed to comply with [RTF 7519](https://datatracker.ietf.org/doc/html/rfc7519) dated May 2015 with a few notable differences:
|
||||
This library was last reviewed to comply with [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519) dated May 2015 with a few notable differences:
|
||||
|
||||
* In order to protect against accidental use of [Unsecured JWTs](https://datatracker.ietf.org/doc/html/rfc7519#section-6), tokens using `alg=none` will only be accepted if the constant `jwt.UnsafeAllowNoneSignatureType` is provided as the key.
|
||||
|
||||
|
@ -60,10 +84,8 @@ This library is considered production ready. Feedback and feature requests are
|
|||
|
||||
This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `main`. Periodically, versions will be tagged from `main`. You can find all the releases on [the project releases page](https://github.com/golang-jwt/jwt/releases).
|
||||
|
||||
While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/golang-jwt/jwt.v3`. It will do the right thing WRT semantic versioning.
|
||||
|
||||
**BREAKING CHANGES:***
|
||||
* Version 3.0.0 includes _a lot_ of changes from the 2.x line, including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code.
|
||||
A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code.
|
||||
|
||||
## Usage Tips
|
||||
|
||||
|
@ -74,7 +96,7 @@ A token is simply a JSON object that is signed by its author. this tells you exa
|
|||
* The author of the token was in the possession of the signing secret
|
||||
* The data has not been modified since it was signed
|
||||
|
||||
It's important to know that JWT does not provide encryption, which means anyone who has access to the token can read its contents. If you need to protect (encrypt) the data, there is a companion spec, `JWE`, that provides this functionality. JWE is currently outside the scope of this library.
|
||||
It's important to know that JWT does not provide encryption, which means anyone who has access to the token can read its contents. If you need to protect (encrypt) the data, there is a companion spec, `JWE`, that provides this functionality. The companion project https://github.com/golang-jwt/jwe aims at a (very) experimental implementation of the JWE standard.
|
||||
|
||||
### Choosing a Signing Method
|
||||
|
||||
|
@ -88,9 +110,10 @@ Asymmetric signing methods, such as RSA, use different keys for signing and veri
|
|||
|
||||
Each signing method expects a different object type for its signing keys. See the package documentation for details. Here are the most common ones:
|
||||
|
||||
* The [HMAC signing method](https://pkg.go.dev/github.com/golang-jwt/jwt#SigningMethodHMAC) (`HS256`,`HS384`,`HS512`) expect `[]byte` values for signing and validation
|
||||
* The [RSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for validation
|
||||
* The [ECDSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.PublicKey` for validation
|
||||
* The [HMAC signing method](https://pkg.go.dev/git.internal/re/jwt/v4#SigningMethodHMAC) (`HS256`,`HS384`,`HS512`) expect `[]byte` values for signing and validation
|
||||
* The [RSA signing method](https://pkg.go.dev/git.internal/re/jwt/v4#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for validation
|
||||
* The [ECDSA signing method](https://pkg.go.dev/git.internal/re/jwt/v4#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.PublicKey` for validation
|
||||
* The [EdDSA signing method](https://pkg.go.dev/git.internal/re/jwt/v4#SigningMethodEd25519) (`Ed25519`) expect `ed25519.PrivateKey` for signing and `ed25519.PublicKey` for validation
|
||||
|
||||
### JWT and OAuth
|
||||
|
||||
|
@ -108,6 +131,8 @@ This library uses descriptive error messages whenever possible. If you are not g
|
|||
|
||||
## More
|
||||
|
||||
Documentation can be found [on pkg.go.dev](https://pkg.go.dev/github.com/golang-jwt/jwt).
|
||||
Documentation can be found [on pkg.go.dev](https://pkg.go.dev/git.internal/re/jwt/v4).
|
||||
|
||||
The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. You'll also find several implementation examples in the documentation.
|
||||
|
||||
[golang-jwt](https://github.com/orgs/golang-jwt) incorporates a modified version of the JWT logo, which is distributed under the terms of the [MIT License](https://github.com/jsonwebtoken/jsonwebtoken.github.io/blob/master/LICENSE.txt).
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
As of February 2022 (and until this document is updated), the latest version `v4` is supported.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you think you found a vulnerability, and even if you are not sure, please report it to jwt-go-security@googlegroups.com or one of the other [golang-jwt maintainers](https://github.com/orgs/golang-jwt/people). Please try be explicit, describe steps to reproduce the security issue with code example(s).
|
||||
|
||||
You will receive a response within a timely manner. If the issue is confirmed, we will do our best to release a patch as soon as possible given the complexity of the problem.
|
||||
|
||||
## Public Discussions
|
||||
|
||||
Please avoid publicly discussing a potential security vulnerability.
|
||||
|
||||
Let's take this offline and find a solution first, this limits the potential impact as much as possible.
|
||||
|
||||
We appreciate your help!
|
|
@ -1,5 +1,9 @@
|
|||
## `jwt-go` Version History
|
||||
|
||||
#### 4.0.0
|
||||
|
||||
* Introduces support for Go modules. The `v4` version will be backwards compatible with `v3.x.y`.
|
||||
|
||||
#### 3.2.2
|
||||
|
||||
* Starting from this release, we are adopting the policy to support the most 2 recent versions of Go currently available. By the time of this release, this is Go 1.15 and 1.16 ([#28](https://github.com/golang-jwt/jwt/pull/28)).
|
||||
|
|
221
claims.go
221
claims.go
|
@ -6,48 +6,67 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// For a type to be a Claims object, it must just have a Valid method that determines
|
||||
// Claims must just have a Valid method that determines
|
||||
// if the token is invalid for any supported reason
|
||||
type Claims interface {
|
||||
Valid() error
|
||||
}
|
||||
|
||||
// Structured version of Claims Section, as referenced at
|
||||
// https://tools.ietf.org/html/rfc7519#section-4.1
|
||||
// See examples for how to use this with your own claim types
|
||||
type StandardClaims struct {
|
||||
Audience string `json:"aud,omitempty"`
|
||||
ExpiresAt int64 `json:"exp,omitempty"`
|
||||
Id string `json:"jti,omitempty"`
|
||||
IssuedAt int64 `json:"iat,omitempty"`
|
||||
// RegisteredClaims are a structured version of the JWT Claims Set,
|
||||
// restricted to Registered Claim Names, as referenced at
|
||||
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
|
||||
//
|
||||
// This type can be used on its own, but then additional private and
|
||||
// public claims embedded in the JWT will not be parsed. The typical usecase
|
||||
// therefore is to embedded this in a user-defined claim type.
|
||||
//
|
||||
// See examples for how to use this with your own claim types.
|
||||
type RegisteredClaims struct {
|
||||
// the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
|
||||
Issuer string `json:"iss,omitempty"`
|
||||
NotBefore int64 `json:"nbf,omitempty"`
|
||||
|
||||
// the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
|
||||
Subject string `json:"sub,omitempty"`
|
||||
|
||||
// the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
|
||||
Audience ClaimStrings `json:"aud,omitempty"`
|
||||
|
||||
// the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
|
||||
ExpiresAt *NumericDate `json:"exp,omitempty"`
|
||||
|
||||
// the `nbf` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
|
||||
NotBefore *NumericDate `json:"nbf,omitempty"`
|
||||
|
||||
// the `iat` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
|
||||
IssuedAt *NumericDate `json:"iat,omitempty"`
|
||||
|
||||
// the `jti` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
|
||||
ID string `json:"jti,omitempty"`
|
||||
}
|
||||
|
||||
// Validates time based claims "exp, iat, nbf".
|
||||
// Valid validates time based claims "exp, iat, nbf".
|
||||
// There is no accounting for clock skew.
|
||||
// As well, if any of the above claims are not in the token, it will still
|
||||
// be considered a valid claim.
|
||||
func (c StandardClaims) Valid() error {
|
||||
func (c RegisteredClaims) Valid() error {
|
||||
vErr := new(ValidationError)
|
||||
now := TimeFunc().Unix()
|
||||
now := TimeFunc()
|
||||
|
||||
// The claims below are optional, by default, so if they are set to the
|
||||
// default value in Go, let's not fail the verification for them.
|
||||
if !c.VerifyExpiresAt(now, false) {
|
||||
delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0))
|
||||
vErr.Inner = fmt.Errorf("token is expired by %v", delta)
|
||||
delta := now.Sub(c.ExpiresAt.Time)
|
||||
vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta)
|
||||
vErr.Errors |= ValidationErrorExpired
|
||||
}
|
||||
|
||||
if !c.VerifyIssuedAt(now, false) {
|
||||
vErr.Inner = fmt.Errorf("Token used before issued")
|
||||
vErr.Inner = ErrTokenUsedBeforeIssued
|
||||
vErr.Errors |= ValidationErrorIssuedAt
|
||||
}
|
||||
|
||||
if !c.VerifyNotBefore(now, false) {
|
||||
vErr.Inner = fmt.Errorf("token is not valid yet")
|
||||
vErr.Inner = ErrTokenNotValidYet
|
||||
vErr.Errors |= ValidationErrorNotValidYet
|
||||
}
|
||||
|
||||
|
@ -58,36 +77,144 @@ func (c StandardClaims) Valid() error {
|
|||
return vErr
|
||||
}
|
||||
|
||||
// Compares the aud claim against cmp.
|
||||
// VerifyAudience compares the aud claim against cmp.
|
||||
// If required is false, this method will return true if the value matches or is unset
|
||||
func (c *RegisteredClaims) VerifyAudience(cmp string, req bool) bool {
|
||||
return verifyAud(c.Audience, cmp, req)
|
||||
}
|
||||
|
||||
// VerifyExpiresAt compares the exp claim against cmp (cmp < exp).
|
||||
// If req is false, it will return true, if exp is unset.
|
||||
func (c *RegisteredClaims) VerifyExpiresAt(cmp time.Time, req bool) bool {
|
||||
if c.ExpiresAt == nil {
|
||||
return verifyExp(nil, cmp, req)
|
||||
}
|
||||
|
||||
return verifyExp(&c.ExpiresAt.Time, cmp, req)
|
||||
}
|
||||
|
||||
// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
|
||||
// If req is false, it will return true, if iat is unset.
|
||||
func (c *RegisteredClaims) VerifyIssuedAt(cmp time.Time, req bool) bool {
|
||||
if c.IssuedAt == nil {
|
||||
return verifyIat(nil, cmp, req)
|
||||
}
|
||||
|
||||
return verifyIat(&c.IssuedAt.Time, cmp, req)
|
||||
}
|
||||
|
||||
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
|
||||
// If req is false, it will return true, if nbf is unset.
|
||||
func (c *RegisteredClaims) VerifyNotBefore(cmp time.Time, req bool) bool {
|
||||
if c.NotBefore == nil {
|
||||
return verifyNbf(nil, cmp, req)
|
||||
}
|
||||
|
||||
return verifyNbf(&c.NotBefore.Time, cmp, req)
|
||||
}
|
||||
|
||||
// VerifyIssuer compares the iss claim against cmp.
|
||||
// If required is false, this method will return true if the value matches or is unset
|
||||
func (c *RegisteredClaims) VerifyIssuer(cmp string, req bool) bool {
|
||||
return verifyIss(c.Issuer, cmp, req)
|
||||
}
|
||||
|
||||
// StandardClaims are a structured version of the JWT Claims Set, as referenced at
|
||||
// https://datatracker.ietf.org/doc/html/rfc7519#section-4. They do not follow the
|
||||
// specification exactly, since they were based on an earlier draft of the
|
||||
// specification and not updated. The main difference is that they only
|
||||
// support integer-based date fields and singular audiences. This might lead to
|
||||
// incompatibilities with other JWT implementations. The use of this is discouraged, instead
|
||||
// the newer RegisteredClaims struct should be used.
|
||||
//
|
||||
// Deprecated: Use RegisteredClaims instead for a forward-compatible way to access registered claims in a struct.
|
||||
type StandardClaims struct {
|
||||
Audience string `json:"aud,omitempty"`
|
||||
ExpiresAt int64 `json:"exp,omitempty"`
|
||||
Id string `json:"jti,omitempty"`
|
||||
IssuedAt int64 `json:"iat,omitempty"`
|
||||
Issuer string `json:"iss,omitempty"`
|
||||
NotBefore int64 `json:"nbf,omitempty"`
|
||||
Subject string `json:"sub,omitempty"`
|
||||
}
|
||||
|
||||
// Valid validates time based claims "exp, iat, nbf". There is no accounting for clock skew.
|
||||
// As well, if any of the above claims are not in the token, it will still
|
||||
// be considered a valid claim.
|
||||
func (c StandardClaims) Valid() error {
|
||||
vErr := new(ValidationError)
|
||||
now := TimeFunc().Unix()
|
||||
|
||||
// The claims below are optional, by default, so if they are set to the
|
||||
// default value in Go, let's not fail the verification for them.
|
||||
if !c.VerifyExpiresAt(now, false) {
|
||||
delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0))
|
||||
vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta)
|
||||
vErr.Errors |= ValidationErrorExpired
|
||||
}
|
||||
|
||||
if !c.VerifyIssuedAt(now, false) {
|
||||
vErr.Inner = ErrTokenUsedBeforeIssued
|
||||
vErr.Errors |= ValidationErrorIssuedAt
|
||||
}
|
||||
|
||||
if !c.VerifyNotBefore(now, false) {
|
||||
vErr.Inner = ErrTokenNotValidYet
|
||||
vErr.Errors |= ValidationErrorNotValidYet
|
||||
}
|
||||
|
||||
if vErr.valid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return vErr
|
||||
}
|
||||
|
||||
// VerifyAudience compares the aud claim against cmp.
|
||||
// If required is false, this method will return true if the value matches or is unset
|
||||
func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool {
|
||||
return verifyAud([]string{c.Audience}, cmp, req)
|
||||
}
|
||||
|
||||
// Compares the exp claim against cmp.
|
||||
// If required is false, this method will return true if the value matches or is unset
|
||||
// VerifyExpiresAt compares the exp claim against cmp (cmp < exp).
|
||||
// If req is false, it will return true, if exp is unset.
|
||||
func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool {
|
||||
return verifyExp(c.ExpiresAt, cmp, req)
|
||||
if c.ExpiresAt == 0 {
|
||||
return verifyExp(nil, time.Unix(cmp, 0), req)
|
||||
}
|
||||
|
||||
t := time.Unix(c.ExpiresAt, 0)
|
||||
return verifyExp(&t, time.Unix(cmp, 0), req)
|
||||
}
|
||||
|
||||
// Compares the iat claim against cmp.
|
||||
// If required is false, this method will return true if the value matches or is unset
|
||||
// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
|
||||
// If req is false, it will return true, if iat is unset.
|
||||
func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool {
|
||||
return verifyIat(c.IssuedAt, cmp, req)
|
||||
if c.IssuedAt == 0 {
|
||||
return verifyIat(nil, time.Unix(cmp, 0), req)
|
||||
}
|
||||
|
||||
t := time.Unix(c.IssuedAt, 0)
|
||||
return verifyIat(&t, time.Unix(cmp, 0), req)
|
||||
}
|
||||
|
||||
// Compares the iss claim against cmp.
|
||||
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
|
||||
// If req is false, it will return true, if nbf is unset.
|
||||
func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool {
|
||||
if c.NotBefore == 0 {
|
||||
return verifyNbf(nil, time.Unix(cmp, 0), req)
|
||||
}
|
||||
|
||||
t := time.Unix(c.NotBefore, 0)
|
||||
return verifyNbf(&t, time.Unix(cmp, 0), req)
|
||||
}
|
||||
|
||||
// VerifyIssuer compares the iss claim against cmp.
|
||||
// If required is false, this method will return true if the value matches or is unset
|
||||
func (c *StandardClaims) VerifyIssuer(cmp string, req bool) bool {
|
||||
return verifyIss(c.Issuer, cmp, req)
|
||||
}
|
||||
|
||||
// Compares the nbf claim against cmp.
|
||||
// If required is false, this method will return true if the value matches or is unset
|
||||
func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool {
|
||||
return verifyNbf(c.NotBefore, cmp, req)
|
||||
}
|
||||
|
||||
// ----- helpers
|
||||
|
||||
func verifyAud(aud []string, cmp string, required bool) bool {
|
||||
|
@ -113,34 +240,30 @@ func verifyAud(aud []string, cmp string, required bool) bool {
|
|||
return result
|
||||
}
|
||||
|
||||
func verifyExp(exp int64, now int64, required bool) bool {
|
||||
if exp == 0 {
|
||||
func verifyExp(exp *time.Time, now time.Time, required bool) bool {
|
||||
if exp == nil {
|
||||
return !required
|
||||
}
|
||||
return now <= exp
|
||||
return now.Before(*exp)
|
||||
}
|
||||
|
||||
func verifyIat(iat int64, now int64, required bool) bool {
|
||||
if iat == 0 {
|
||||
func verifyIat(iat *time.Time, now time.Time, required bool) bool {
|
||||
if iat == nil {
|
||||
return !required
|
||||
}
|
||||
return now >= iat
|
||||
return now.After(*iat) || now.Equal(*iat)
|
||||
}
|
||||
|
||||
func verifyNbf(nbf *time.Time, now time.Time, required bool) bool {
|
||||
if nbf == nil {
|
||||
return !required
|
||||
}
|
||||
return now.After(*nbf) || now.Equal(*nbf)
|
||||
}
|
||||
|
||||
func verifyIss(iss string, cmp string, required bool) bool {
|
||||
if iss == "" {
|
||||
return !required
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func verifyNbf(nbf int64, now int64, required bool) bool {
|
||||
if nbf == 0 {
|
||||
return !required
|
||||
}
|
||||
return now >= nbf
|
||||
return subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0
|
||||
}
|
||||
|
|
|
@ -16,5 +16,4 @@ To simply display a token, use:
|
|||
|
||||
You can install this tool with the following command:
|
||||
|
||||
go install github.com/golang-jwt/jwt/cmd/jwt
|
||||
|
||||
go install git.internal/re/jwt/v4/cmd/jwt
|
|
@ -1,23 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ArgList map[string]string
|
||||
|
||||
func (l ArgList) String() string {
|
||||
data, _ := json.Marshal(l)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (l ArgList) Set(arg string) error {
|
||||
parts := strings.SplitN(arg, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("Invalid argument '%v'. Must use format 'key=value'. %v", arg, parts)
|
||||
}
|
||||
l[parts[0]] = parts[1]
|
||||
return nil
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
//
|
||||
// Example usage:
|
||||
// The following will create and sign a token, then verify it and output the original claims.
|
||||
//
|
||||
// echo {\"foo\":\"bar\"} | bin/jwt -key test/sample_key -alg RS256 -sign - | bin/jwt -key test/sample_key.pub -verify -
|
||||
package main
|
||||
|
||||
|
@ -11,17 +12,17 @@ import (
|
|||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"git.internal/re/jwt/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
// Options
|
||||
flagAlg = flag.String("alg", "", "signing algorithm identifier")
|
||||
flagAlg = flag.String("alg", "", algHelp())
|
||||
flagKey = flag.String("key", "", "path to key file or '-' to read from stdin")
|
||||
flagCompact = flag.Bool("compact", false, "output compact JSON")
|
||||
flagDebug = flag.Bool("debug", false, "print out all kinds of debug data")
|
||||
|
@ -67,14 +68,14 @@ func start() error {
|
|||
return showToken()
|
||||
} else {
|
||||
flag.Usage()
|
||||
return fmt.Errorf("None of the required flags are present. What do you want me to do?")
|
||||
return fmt.Errorf("none of the required flags are present. What do you want me to do?")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper func: Read input from specified file or stdin
|
||||
func loadData(p string) ([]byte, error) {
|
||||
if p == "" {
|
||||
return nil, fmt.Errorf("No path specified")
|
||||
return nil, fmt.Errorf("no path specified")
|
||||
}
|
||||
|
||||
var rdr io.Reader
|
||||
|
@ -90,7 +91,7 @@ func loadData(p string) ([]byte, error) {
|
|||
return nil, err
|
||||
}
|
||||
}
|
||||
return ioutil.ReadAll(rdr)
|
||||
return io.ReadAll(rdr)
|
||||
}
|
||||
|
||||
// Print a json object in accordance with the prophecy (or the command line options)
|
||||
|
@ -117,7 +118,7 @@ func verifyToken() error {
|
|||
// get the token
|
||||
tokData, err := loadData(*flagVerify)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't read token: %v", err)
|
||||
return fmt.Errorf("couldn't read token: %w", err)
|
||||
}
|
||||
|
||||
// trim possible whitespace from token
|
||||
|
@ -128,6 +129,9 @@ func verifyToken() error {
|
|||
|
||||
// Parse the token. Load the key from command line option
|
||||
token, err := jwt.Parse(string(tokData), func(t *jwt.Token) (interface{}, error) {
|
||||
if isNone() {
|
||||
return jwt.UnsafeAllowNoneSignatureType, nil
|
||||
}
|
||||
data, err := loadData(*flagKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -150,17 +154,17 @@ func verifyToken() error {
|
|||
|
||||
// Print an error if we can't parse for some reason
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't parse token: %v", err)
|
||||
return fmt.Errorf("couldn't parse token: %w", err)
|
||||
}
|
||||
|
||||
// Is token invalid?
|
||||
if !token.Valid {
|
||||
return fmt.Errorf("Token is invalid")
|
||||
return fmt.Errorf("token is invalid")
|
||||
}
|
||||
|
||||
// Print the token details
|
||||
if err := printJSON(token.Claims); err != nil {
|
||||
return fmt.Errorf("Failed to output claims: %v", err)
|
||||
return fmt.Errorf("failed to output claims: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -172,7 +176,7 @@ func signToken() error {
|
|||
// get the token data from command line arguments
|
||||
tokData, err := loadData(*flagSign)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't read token: %v", err)
|
||||
return fmt.Errorf("couldn't read token: %w", err)
|
||||
} else if *flagDebug {
|
||||
fmt.Fprintf(os.Stderr, "Token: %v bytes", len(tokData))
|
||||
}
|
||||
|
@ -180,7 +184,7 @@ func signToken() error {
|
|||
// parse the JSON of the claims
|
||||
var claims jwt.MapClaims
|
||||
if err := json.Unmarshal(tokData, &claims); err != nil {
|
||||
return fmt.Errorf("Couldn't parse claims JSON: %v", err)
|
||||
return fmt.Errorf("couldn't parse claims JSON: %w", err)
|
||||
}
|
||||
|
||||
// add command line claims
|
||||
|
@ -192,15 +196,19 @@ func signToken() error {
|
|||
|
||||
// get the key
|
||||
var key interface{}
|
||||
if isNone() {
|
||||
key = jwt.UnsafeAllowNoneSignatureType
|
||||
} else {
|
||||
key, err = loadData(*flagKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't read key: %v", err)
|
||||
return fmt.Errorf("couldn't read key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// get the signing alg
|
||||
alg := jwt.GetSigningMethod(*flagAlg)
|
||||
if alg == nil {
|
||||
return fmt.Errorf("Couldn't find signing method: %v", *flagAlg)
|
||||
return fmt.Errorf("couldn't find signing method: %v", *flagAlg)
|
||||
}
|
||||
|
||||
// create a new token
|
||||
|
@ -215,7 +223,7 @@ func signToken() error {
|
|||
|
||||
if isEs() {
|
||||
if k, ok := key.([]byte); !ok {
|
||||
return fmt.Errorf("Couldn't convert key data to key")
|
||||
return fmt.Errorf("couldn't convert key data to key")
|
||||
} else {
|
||||
key, err = jwt.ParseECPrivateKeyFromPEM(k)
|
||||
if err != nil {
|
||||
|
@ -224,7 +232,7 @@ func signToken() error {
|
|||
}
|
||||
} else if isRs() {
|
||||
if k, ok := key.([]byte); !ok {
|
||||
return fmt.Errorf("Couldn't convert key data to key")
|
||||
return fmt.Errorf("couldn't convert key data to key")
|
||||
} else {
|
||||
key, err = jwt.ParseRSAPrivateKeyFromPEM(k)
|
||||
if err != nil {
|
||||
|
@ -233,7 +241,7 @@ func signToken() error {
|
|||
}
|
||||
} else if isEd() {
|
||||
if k, ok := key.([]byte); !ok {
|
||||
return fmt.Errorf("Couldn't convert key data to key")
|
||||
return fmt.Errorf("couldn't convert key data to key")
|
||||
} else {
|
||||
key, err = jwt.ParseEdPrivateKeyFromPEM(k)
|
||||
if err != nil {
|
||||
|
@ -245,7 +253,7 @@ func signToken() error {
|
|||
if out, err := token.SignedString(key); err == nil {
|
||||
fmt.Println(out)
|
||||
} else {
|
||||
return fmt.Errorf("Error signing token: %v", err)
|
||||
return fmt.Errorf("error signing token: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -256,7 +264,7 @@ func showToken() error {
|
|||
// get the token
|
||||
tokData, err := loadData(*flagShow)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't read token: %v", err)
|
||||
return fmt.Errorf("couldn't read token: %w", err)
|
||||
}
|
||||
|
||||
// trim possible whitespace from token
|
||||
|
@ -267,18 +275,18 @@ func showToken() error {
|
|||
|
||||
token, err := jwt.Parse(string(tokData), nil)
|
||||
if token == nil {
|
||||
return fmt.Errorf("malformed token: %v", err)
|
||||
return fmt.Errorf("malformed token: %w", err)
|
||||
}
|
||||
|
||||
// Print the token details
|
||||
fmt.Println("Header:")
|
||||
if err := printJSON(token.Header); err != nil {
|
||||
return fmt.Errorf("Failed to output header: %v", err)
|
||||
return fmt.Errorf("failed to output header: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Claims:")
|
||||
if err := printJSON(token.Claims); err != nil {
|
||||
return fmt.Errorf("Failed to output claims: %v", err)
|
||||
return fmt.Errorf("failed to output claims: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -293,5 +301,44 @@ func isRs() bool {
|
|||
}
|
||||
|
||||
func isEd() bool {
|
||||
return strings.HasPrefix(strings.ToUpper(*flagAlg), "Ed")
|
||||
return *flagAlg == "EdDSA"
|
||||
}
|
||||
|
||||
func isNone() bool {
|
||||
return *flagAlg == "none"
|
||||
}
|
||||
|
||||
func algHelp() string {
|
||||
algs := jwt.GetAlgorithms()
|
||||
sort.Strings(algs)
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("signing algorithm identifier, one of\n")
|
||||
for i, alg := range algs {
|
||||
if i > 0 {
|
||||
if i%7 == 0 {
|
||||
b.WriteString(",\n")
|
||||
} else {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
}
|
||||
b.WriteString(alg)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type ArgList map[string]string
|
||||
|
||||
func (l ArgList) String() string {
|
||||
data, _ := json.Marshal(l)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (l ArgList) Set(arg string) error {
|
||||
parts := strings.SplitN(arg, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid argument '%v'. Must use format 'key=value'. %v", arg, parts)
|
||||
}
|
||||
l[parts[0]] = parts[1]
|
||||
return nil
|
||||
}
|
6
ecdsa.go
6
ecdsa.go
|
@ -13,7 +13,7 @@ var (
|
|||
ErrECDSAVerification = errors.New("crypto/ecdsa: verification error")
|
||||
)
|
||||
|
||||
// Implements the ECDSA family of signing methods signing methods
|
||||
// SigningMethodECDSA implements the ECDSA family of signing methods.
|
||||
// Expects *ecdsa.PrivateKey for signing and *ecdsa.PublicKey for verification
|
||||
type SigningMethodECDSA struct {
|
||||
Name string
|
||||
|
@ -53,7 +53,7 @@ func (m *SigningMethodECDSA) Alg() string {
|
|||
return m.Name
|
||||
}
|
||||
|
||||
// Implements the Verify method from SigningMethod
|
||||
// Verify implements token verification for the SigningMethod.
|
||||
// For this verify method, key must be an ecdsa.PublicKey struct
|
||||
func (m *SigningMethodECDSA) Verify(signingString, signature string, key interface{}) error {
|
||||
var err error
|
||||
|
@ -95,7 +95,7 @@ func (m *SigningMethodECDSA) Verify(signingString, signature string, key interfa
|
|||
return ErrECDSAVerification
|
||||
}
|
||||
|
||||
// Implements the Sign method from SigningMethod
|
||||
// Sign implements token signing for the SigningMethod.
|
||||
// For this signing method, key must be an ecdsa.PrivateKey struct
|
||||
func (m *SigningMethodECDSA) Sign(signingString string, key interface{}) (string, error) {
|
||||
// Get the key
|
||||
|
|
|
@ -2,11 +2,11 @@ package jwt_test
|
|||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"git.internal/re/jwt/v4"
|
||||
)
|
||||
|
||||
var ecdsaTestData = []struct {
|
||||
|
@ -55,7 +55,7 @@ func TestECDSAVerify(t *testing.T) {
|
|||
for _, data := range ecdsaTestData {
|
||||
var err error
|
||||
|
||||
key, _ := ioutil.ReadFile(data.keys["public"])
|
||||
key, _ := os.ReadFile(data.keys["public"])
|
||||
|
||||
var ecdsaKey *ecdsa.PublicKey
|
||||
if ecdsaKey, err = jwt.ParseECPublicKeyFromPEM(key); err != nil {
|
||||
|
@ -78,7 +78,7 @@ func TestECDSAVerify(t *testing.T) {
|
|||
func TestECDSASign(t *testing.T) {
|
||||
for _, data := range ecdsaTestData {
|
||||
var err error
|
||||
key, _ := ioutil.ReadFile(data.keys["private"])
|
||||
key, _ := os.ReadFile(data.keys["private"])
|
||||
|
||||
var ecdsaKey *ecdsa.PrivateKey
|
||||
if ecdsaKey, err = jwt.ParseECPrivateKeyFromPEM(key); err != nil {
|
||||
|
@ -90,7 +90,6 @@ func TestECDSASign(t *testing.T) {
|
|||
toSign := strings.Join(parts[0:2], ".")
|
||||
method := jwt.GetSigningMethod(data.alg)
|
||||
sig, err := method.Sign(toSign, ecdsaKey)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("[%v] Error signing token: %v", data.name, err)
|
||||
}
|
||||
|
@ -106,9 +105,27 @@ func TestECDSASign(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func BenchmarkECDSAParsing(b *testing.B) {
|
||||
for _, data := range ecdsaTestData {
|
||||
key, _ := os.ReadFile(data.keys["private"])
|
||||
|
||||
b.Run(data.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
if _, err := jwt.ParseECPrivateKeyFromPEM(key); err != nil {
|
||||
b.Fatalf("Unable to parse ECDSA private key: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkECDSASigning(b *testing.B) {
|
||||
for _, data := range ecdsaTestData {
|
||||
key, _ := ioutil.ReadFile(data.keys["private"])
|
||||
key, _ := os.ReadFile(data.keys["private"])
|
||||
|
||||
ecdsaKey, err := jwt.ParseECPrivateKeyFromPEM(key)
|
||||
if err != nil {
|
||||
|
|
|
@ -8,11 +8,11 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ErrNotECPublicKey = errors.New("Key is not a valid ECDSA public key")
|
||||
ErrNotECPrivateKey = errors.New("Key is not a valid ECDSA private key")
|
||||
ErrNotECPublicKey = errors.New("key is not a valid ECDSA public key")
|
||||
ErrNotECPrivateKey = errors.New("key is not a valid ECDSA private key")
|
||||
)
|
||||
|
||||
// Parse PEM encoded Elliptic Curve Private Key Structure
|
||||
// ParseECPrivateKeyFromPEM parses a PEM encoded Elliptic Curve Private Key Structure
|
||||
func ParseECPrivateKeyFromPEM(key []byte) (*ecdsa.PrivateKey, error) {
|
||||
var err error
|
||||
|
||||
|
@ -39,7 +39,7 @@ func ParseECPrivateKeyFromPEM(key []byte) (*ecdsa.PrivateKey, error) {
|
|||
return pkey, nil
|
||||
}
|
||||
|
||||
// Parse PEM encoded PKCS1 or PKCS8 public key
|
||||
// ParseECPublicKeyFromPEM parses a PEM encoded PKCS1 or PKCS8 public key
|
||||
func ParseECPublicKeyFromPEM(key []byte) (*ecdsa.PublicKey, error) {
|
||||
var err error
|
||||
|
||||
|
|
22
ed25519.go
22
ed25519.go
|
@ -3,14 +3,16 @@ package jwt
|
|||
import (
|
||||
"errors"
|
||||
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEd25519Verification = errors.New("ed25519: verification error")
|
||||
)
|
||||
|
||||
// Implements the EdDSA family
|
||||
// SigningMethodEd25519 implements the EdDSA family.
|
||||
// Expects ed25519.PrivateKey for signing and ed25519.PublicKey for verification
|
||||
type SigningMethodEd25519 struct{}
|
||||
|
||||
|
@ -30,7 +32,7 @@ func (m *SigningMethodEd25519) Alg() string {
|
|||
return "EdDSA"
|
||||
}
|
||||
|
||||
// Implements the Verify method from SigningMethod
|
||||
// Verify implements token verification for the SigningMethod.
|
||||
// For this verify method, key must be an ed25519.PublicKey
|
||||
func (m *SigningMethodEd25519) Verify(signingString, signature string, key interface{}) error {
|
||||
var err error
|
||||
|
@ -59,23 +61,25 @@ func (m *SigningMethodEd25519) Verify(signingString, signature string, key inter
|
|||
return nil
|
||||
}
|
||||
|
||||
// Implements the Sign method from SigningMethod
|
||||
// Sign implements token signing for the SigningMethod.
|
||||
// For this signing method, key must be an ed25519.PrivateKey
|
||||
func (m *SigningMethodEd25519) Sign(signingString string, key interface{}) (string, error) {
|
||||
var ed25519Key ed25519.PrivateKey
|
||||
var ed25519Key crypto.Signer
|
||||
var ok bool
|
||||
|
||||
if ed25519Key, ok = key.(ed25519.PrivateKey); !ok {
|
||||
if ed25519Key, ok = key.(crypto.Signer); !ok {
|
||||
return "", ErrInvalidKeyType
|
||||
}
|
||||
|
||||
// ed25519.Sign panics if private key not equal to ed25519.PrivateKeySize
|
||||
// this allows to avoid recover usage
|
||||
if len(ed25519Key) != ed25519.PrivateKeySize {
|
||||
if _, ok := ed25519Key.Public().(ed25519.PublicKey); !ok {
|
||||
return "", ErrInvalidKey
|
||||
}
|
||||
|
||||
// Sign the string and return the encoded result
|
||||
sig := ed25519.Sign(ed25519Key, []byte(signingString))
|
||||
// ed25519 performs a two-pass hash as part of its algorithm. Therefore, we need to pass a non-prehashed message into the Sign function, as indicated by crypto.Hash(0)
|
||||
sig, err := ed25519Key.Sign(rand.Reader, []byte(signingString), crypto.Hash(0))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return EncodeSegment(sig), nil
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package jwt_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"git.internal/re/jwt/v4"
|
||||
)
|
||||
|
||||
var ed25519TestData = []struct {
|
||||
|
@ -38,7 +38,7 @@ func TestEd25519Verify(t *testing.T) {
|
|||
for _, data := range ed25519TestData {
|
||||
var err error
|
||||
|
||||
key, _ := ioutil.ReadFile(data.keys["public"])
|
||||
key, _ := os.ReadFile(data.keys["public"])
|
||||
|
||||
ed25519Key, err := jwt.ParseEdPublicKeyFromPEM(key)
|
||||
if err != nil {
|
||||
|
@ -62,7 +62,7 @@ func TestEd25519Verify(t *testing.T) {
|
|||
func TestEd25519Sign(t *testing.T) {
|
||||
for _, data := range ed25519TestData {
|
||||
var err error
|
||||
key, _ := ioutil.ReadFile(data.keys["private"])
|
||||
key, _ := os.ReadFile(data.keys["private"])
|
||||
|
||||
ed25519Key, err := jwt.ParseEdPrivateKeyFromPEM(key)
|
||||
if err != nil {
|
||||
|
|
|
@ -9,11 +9,11 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ErrNotEdPrivateKey = errors.New("Key is not a valid Ed25519 private key")
|
||||
ErrNotEdPublicKey = errors.New("Key is not a valid Ed25519 public key")
|
||||
ErrNotEdPrivateKey = errors.New("key is not a valid Ed25519 private key")
|
||||
ErrNotEdPublicKey = errors.New("key is not a valid Ed25519 public key")
|
||||
)
|
||||
|
||||
// Parse PEM-encoded Edwards curve private key
|
||||
// ParseEdPrivateKeyFromPEM parses a PEM-encoded Edwards curve private key
|
||||
func ParseEdPrivateKeyFromPEM(key []byte) (crypto.PrivateKey, error) {
|
||||
var err error
|
||||
|
||||
|
@ -38,7 +38,7 @@ func ParseEdPrivateKeyFromPEM(key []byte) (crypto.PrivateKey, error) {
|
|||
return pkey, nil
|
||||
}
|
||||
|
||||
// Parse PEM-encoded Edwards curve public key
|
||||
// ParseEdPublicKeyFromPEM parses a PEM-encoded Edwards curve public key
|
||||
func ParseEdPublicKeyFromPEM(key []byte) (crypto.PublicKey, error) {
|
||||
var err error
|
||||
|
||||
|
|
59
errors.go
59
errors.go
|
@ -9,6 +9,18 @@ var (
|
|||
ErrInvalidKey = errors.New("key is invalid")
|
||||
ErrInvalidKeyType = errors.New("key is of invalid type")
|
||||
ErrHashUnavailable = errors.New("the requested hash function is unavailable")
|
||||
|
||||
ErrTokenMalformed = errors.New("token is malformed")
|
||||
ErrTokenUnverifiable = errors.New("token is unverifiable")
|
||||
ErrTokenSignatureInvalid = errors.New("token signature is invalid")
|
||||
|
||||
ErrTokenInvalidAudience = errors.New("token has invalid audience")
|
||||
ErrTokenExpired = errors.New("token is expired")
|
||||
ErrTokenUsedBeforeIssued = errors.New("token used before issued")
|
||||
ErrTokenInvalidIssuer = errors.New("token has invalid issuer")
|
||||
ErrTokenNotValidYet = errors.New("token is not valid yet")
|
||||
ErrTokenInvalidId = errors.New("token has invalid id")
|
||||
ErrTokenInvalidClaims = errors.New("token has invalid claims")
|
||||
)
|
||||
|
||||
// The errors that might occur when parsing and validating a token
|
||||
|
@ -27,7 +39,7 @@ const (
|
|||
ValidationErrorClaimsInvalid // Generic claims validation error
|
||||
)
|
||||
|
||||
// Helper for constructing a ValidationError with a string error message
|
||||
// NewValidationError is a helper for constructing a ValidationError with a string error message
|
||||
func NewValidationError(errorText string, errorFlags uint32) *ValidationError {
|
||||
return &ValidationError{
|
||||
text: errorText,
|
||||
|
@ -35,14 +47,14 @@ func NewValidationError(errorText string, errorFlags uint32) *ValidationError {
|
|||
}
|
||||
}
|
||||
|
||||
// The error from Parse if token is not valid
|
||||
// ValidationError represents an error from Parse if token is not valid
|
||||
type ValidationError struct {
|
||||
Inner error // stores the error returned by external dependencies, i.e.: KeyFunc
|
||||
Errors uint32 // bitfield. see ValidationError... constants
|
||||
text string // errors that do not have a valid error just have text
|
||||
}
|
||||
|
||||
// Validation error is an error type
|
||||
// Error is the implementation of the err interface.
|
||||
func (e ValidationError) Error() string {
|
||||
if e.Inner != nil {
|
||||
return e.Inner.Error()
|
||||
|
@ -53,7 +65,48 @@ func (e ValidationError) Error() string {
|
|||
}
|
||||
}
|
||||
|
||||
// Unwrap gives errors.Is and errors.As access to the inner error.
|
||||
func (e *ValidationError) Unwrap() error {
|
||||
return e.Inner
|
||||
}
|
||||
|
||||
// No errors
|
||||
func (e *ValidationError) valid() bool {
|
||||
return e.Errors == 0
|
||||
}
|
||||
|
||||
// Is checks if this ValidationError is of the supplied error. We are first checking for the exact error message
|
||||
// by comparing the inner error message. If that fails, we compare using the error flags. This way we can use
|
||||
// custom error messages (mainly for backwards compatability) and still leverage errors.Is using the global error variables.
|
||||
func (e *ValidationError) Is(err error) bool {
|
||||
// Check, if our inner error is a direct match
|
||||
if errors.Is(errors.Unwrap(e), err) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Otherwise, we need to match using our error flags
|
||||
switch err {
|
||||
case ErrTokenMalformed:
|
||||
return e.Errors&ValidationErrorMalformed != 0
|
||||
case ErrTokenUnverifiable:
|
||||
return e.Errors&ValidationErrorUnverifiable != 0
|
||||
case ErrTokenSignatureInvalid:
|
||||
return e.Errors&ValidationErrorSignatureInvalid != 0
|
||||
case ErrTokenInvalidAudience:
|
||||
return e.Errors&ValidationErrorAudience != 0
|
||||
case ErrTokenExpired:
|
||||
return e.Errors&ValidationErrorExpired != 0
|
||||
case ErrTokenUsedBeforeIssued:
|
||||
return e.Errors&ValidationErrorIssuedAt != 0
|
||||
case ErrTokenInvalidIssuer:
|
||||
return e.Errors&ValidationErrorIssuer != 0
|
||||
case ErrTokenNotValidYet:
|
||||
return e.Errors&ValidationErrorNotValidYet != 0
|
||||
case ErrTokenInvalidId:
|
||||
return e.Errors&ValidationErrorId != 0
|
||||
case ErrTokenInvalidClaims:
|
||||
return e.Errors&ValidationErrorClaimsInvalid != 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,47 +1,64 @@
|
|||
package jwt_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"git.internal/re/jwt/v4"
|
||||
)
|
||||
|
||||
// Example (atypical) using the StandardClaims type by itself to parse a token.
|
||||
// The StandardClaims type is designed to be embedded into your custom types
|
||||
// Example (atypical) using the RegisteredClaims type by itself to parse a token.
|
||||
// The RegisteredClaims type is designed to be embedded into your custom types
|
||||
// to provide standard validation features. You can use it alone, but there's
|
||||
// no way to retrieve other fields after parsing.
|
||||
// See the CustomClaimsType example for intended usage.
|
||||
func ExampleNewWithClaims_standardClaims() {
|
||||
func ExampleNewWithClaims_registeredClaims() {
|
||||
mySigningKey := []byte("AllYourBase")
|
||||
|
||||
// Create the Claims
|
||||
claims := &jwt.StandardClaims{
|
||||
ExpiresAt: 15000,
|
||||
claims := &jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)),
|
||||
Issuer: "test",
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
ss, err := token.SignedString(mySigningKey)
|
||||
fmt.Printf("%v %v", ss, err)
|
||||
//Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.QsODzZu3lUZMVdhbO76u3Jv02iYCvEHcYVUI1kOWEU0 <nil>
|
||||
// Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.0XN_1Tpp9FszFOonIBpwha0c_SfnNI22DhTnjMshPg8 <nil>
|
||||
}
|
||||
|
||||
// Example creating a token using a custom claims type. The StandardClaim is embedded
|
||||
// in the custom type to allow for easy encoding, parsing and validation of standard claims.
|
||||
// Example creating a token using a custom claims type. The RegisteredClaims is embedded
|
||||
// in the custom type to allow for easy encoding, parsing and validation of registered claims.
|
||||
func ExampleNewWithClaims_customClaimsType() {
|
||||
mySigningKey := []byte("AllYourBase")
|
||||
|
||||
type MyCustomClaims struct {
|
||||
Foo string `json:"foo"`
|
||||
jwt.StandardClaims
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// Create the Claims
|
||||
// Create the claims
|
||||
claims := MyCustomClaims{
|
||||
"bar",
|
||||
jwt.StandardClaims{
|
||||
ExpiresAt: 15000,
|
||||
jwt.RegisteredClaims{
|
||||
// A usual scenario is to set the expiration time relative to the current time
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "test",
|
||||
Subject: "somebody",
|
||||
ID: "1",
|
||||
Audience: []string{"somebody_else"},
|
||||
},
|
||||
}
|
||||
|
||||
// Create claims while leaving out some of the optional fields
|
||||
claims = MyCustomClaims{
|
||||
"bar",
|
||||
jwt.RegisteredClaims{
|
||||
// Also fixed dates can be used for the NumericDate
|
||||
ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)),
|
||||
Issuer: "test",
|
||||
},
|
||||
}
|
||||
|
@ -49,48 +66,37 @@ func ExampleNewWithClaims_customClaimsType() {
|
|||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
ss, err := token.SignedString(mySigningKey)
|
||||
fmt.Printf("%v %v", ss, err)
|
||||
//Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c <nil>
|
||||
|
||||
// Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.xVuY2FZ_MRXMIEgVQ7J-TFtaucVFRXUzHm9LmV41goM <nil>
|
||||
}
|
||||
|
||||
// Example creating a token using a custom claims type. The StandardClaim is embedded
|
||||
// in the custom type to allow for easy encoding, parsing and validation of standard claims.
|
||||
func ExampleParseWithClaims_customClaimsType() {
|
||||
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c"
|
||||
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA"
|
||||
|
||||
type MyCustomClaims struct {
|
||||
Foo string `json:"foo"`
|
||||
jwt.StandardClaims
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// sample token is expired. override time so it parses as valid
|
||||
at(time.Unix(0, 0), func() {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte("AllYourBase"), nil
|
||||
})
|
||||
|
||||
if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {
|
||||
fmt.Printf("%v %v", claims.Foo, claims.StandardClaims.ExpiresAt)
|
||||
fmt.Printf("%v %v", claims.Foo, claims.RegisteredClaims.Issuer)
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
})
|
||||
|
||||
// Output: bar 15000
|
||||
}
|
||||
|
||||
// Override time value for tests. Restore default value after.
|
||||
func at(t time.Time, f func()) {
|
||||
jwt.TimeFunc = func() time.Time {
|
||||
return t
|
||||
}
|
||||
f()
|
||||
jwt.TimeFunc = time.Now
|
||||
// Output: bar test
|
||||
}
|
||||
|
||||
// An example of parsing the error types using bitfield checks
|
||||
func ExampleParse_errorChecking() {
|
||||
// Token from another example. This token is expired
|
||||
var tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c"
|
||||
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c"
|
||||
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte("AllYourBase"), nil
|
||||
|
@ -98,18 +104,14 @@ func ExampleParse_errorChecking() {
|
|||
|
||||
if token.Valid {
|
||||
fmt.Println("You look nice today")
|
||||
} else if ve, ok := err.(*jwt.ValidationError); ok {
|
||||
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
|
||||
} else if errors.Is(err, jwt.ErrTokenMalformed) {
|
||||
fmt.Println("That's not even a token")
|
||||
} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
|
||||
} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
|
||||
// Token is either expired or not active yet
|
||||
fmt.Println("Timing is everything")
|
||||
} else {
|
||||
fmt.Println("Couldn't handle this token:", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Couldn't handle this token:", err)
|
||||
}
|
||||
|
||||
// Output: Timing is everything
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
module git.internal/re/jwt/v4
|
||||
|
||||
go 1.16
|
||||
|
||||
retract (
|
||||
v4.4.0 // Contains a backwards incompatible change to the Claims interface.
|
||||
)
|
6
hmac.go
6
hmac.go
|
@ -6,7 +6,7 @@ import (
|
|||
"errors"
|
||||
)
|
||||
|
||||
// Implements the HMAC-SHA family of signing methods signing methods
|
||||
// SigningMethodHMAC implements the HMAC-SHA family of signing methods.
|
||||
// Expects key type of []byte for both signing and validation
|
||||
type SigningMethodHMAC struct {
|
||||
Name string
|
||||
|
@ -45,7 +45,7 @@ func (m *SigningMethodHMAC) Alg() string {
|
|||
return m.Name
|
||||
}
|
||||
|
||||
// Verify the signature of HSXXX tokens. Returns nil if the signature is valid.
|
||||
// Verify implements token verification for the SigningMethod. Returns nil if the signature is valid.
|
||||
func (m *SigningMethodHMAC) Verify(signingString, signature string, key interface{}) error {
|
||||
// Verify the key is the right type
|
||||
keyBytes, ok := key.([]byte)
|
||||
|
@ -77,7 +77,7 @@ func (m *SigningMethodHMAC) Verify(signingString, signature string, key interfac
|
|||
return nil
|
||||
}
|
||||
|
||||
// Implements the Sign method from SigningMethod for this signing method.
|
||||
// Sign implements token signing for the SigningMethod.
|
||||
// Key must be []byte
|
||||
func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) {
|
||||
if keyBytes, ok := key.([]byte); ok {
|
||||
|
|
|
@ -2,10 +2,10 @@ package jwt_test
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"git.internal/re/jwt/v4"
|
||||
)
|
||||
|
||||
// For HMAC signing method, the key can be any []byte. It is recommended to generate
|
||||
|
@ -15,7 +15,7 @@ var hmacSampleSecret []byte
|
|||
|
||||
func init() {
|
||||
// Load sample key data
|
||||
if keyData, e := ioutil.ReadFile("test/hmacTestKey"); e == nil {
|
||||
if keyData, e := os.ReadFile("test/hmacTestKey"); e == nil {
|
||||
hmacSampleSecret = keyData
|
||||
} else {
|
||||
panic(e)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package jwt_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"git.internal/re/jwt/v4"
|
||||
)
|
||||
|
||||
var hmacTestData = []struct {
|
||||
|
@ -46,7 +46,7 @@ var hmacTestData = []struct {
|
|||
}
|
||||
|
||||
// Sample data from http://tools.ietf.org/html/draft-jones-json-web-signature-04#appendix-A.1
|
||||
var hmacTestKey, _ = ioutil.ReadFile("test/hmacTestKey")
|
||||
var hmacTestKey, _ = os.ReadFile("test/hmacTestKey")
|
||||
|
||||
func TestHMACVerify(t *testing.T) {
|
||||
for _, data := range hmacTestData {
|
||||
|
|
|
@ -8,16 +8,16 @@ import (
|
|||
"crypto/rsa"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/golang-jwt/jwt/request"
|
||||
"git.internal/re/jwt/v4"
|
||||
"git.internal/re/jwt/v4/request"
|
||||
)
|
||||
|
||||
// location of the files used for signing and verification
|
||||
|
@ -30,22 +30,17 @@ var (
|
|||
verifyKey *rsa.PublicKey
|
||||
signKey *rsa.PrivateKey
|
||||
serverPort int
|
||||
// storing sample username/password pairs
|
||||
// don't do this on a real server
|
||||
users = map[string]string{
|
||||
"test": "known",
|
||||
}
|
||||
)
|
||||
|
||||
// read the key files before starting http handlers
|
||||
func init() {
|
||||
signBytes, err := ioutil.ReadFile(privKeyPath)
|
||||
signBytes, err := os.ReadFile(privKeyPath)
|
||||
fatal(err)
|
||||
|
||||
signKey, err = jwt.ParseRSAPrivateKeyFromPEM(signBytes)
|
||||
fatal(err)
|
||||
|
||||
verifyBytes, err := ioutil.ReadFile(pubKeyPath)
|
||||
verifyBytes, err := os.ReadFile(pubKeyPath)
|
||||
fatal(err)
|
||||
|
||||
verifyKey, err = jwt.ParseRSAPublicKeyFromPEM(verifyBytes)
|
||||
|
@ -65,8 +60,6 @@ func init() {
|
|||
}()
|
||||
}
|
||||
|
||||
var start func()
|
||||
|
||||
func fatal(err error) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -80,7 +73,7 @@ type CustomerInfo struct {
|
|||
}
|
||||
|
||||
type CustomClaimsExample struct {
|
||||
*jwt.StandardClaims
|
||||
jwt.RegisteredClaims
|
||||
TokenType string
|
||||
CustomerInfo
|
||||
}
|
||||
|
@ -116,11 +109,10 @@ func Example_getTokenViaHTTP() {
|
|||
claims := token.Claims.(*CustomClaimsExample)
|
||||
fmt.Println(claims.CustomerInfo.Name)
|
||||
|
||||
//Output: test
|
||||
// Output: test
|
||||
}
|
||||
|
||||
func Example_useTokenViaHTTP() {
|
||||
|
||||
// Make a sample token
|
||||
// In a real world situation, this token will have been acquired from
|
||||
// some other API call (see Example_getTokenViaHTTP)
|
||||
|
@ -149,10 +141,10 @@ func createToken(user string) (string, error) {
|
|||
|
||||
// set our claims
|
||||
t.Claims = &CustomClaimsExample{
|
||||
&jwt.StandardClaims{
|
||||
jwt.RegisteredClaims{
|
||||
// set the expire time
|
||||
// see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-20#section-4.1.4
|
||||
ExpiresAt: time.Now().Add(time.Minute * 1).Unix(),
|
||||
// see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 1)),
|
||||
},
|
||||
"level1",
|
||||
CustomerInfo{user, "human"},
|
||||
|
@ -199,12 +191,11 @@ func authHandler(w http.ResponseWriter, r *http.Request) {
|
|||
// only accessible with a valid token
|
||||
func restrictedHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Get token from request
|
||||
token, err := request.ParseFromRequestWithClaims(r, request.OAuth2Extractor, &CustomClaimsExample{}, func(token *jwt.Token) (interface{}, error) {
|
||||
token, err := request.ParseFromRequest(r, request.OAuth2Extractor, func(token *jwt.Token) (interface{}, error) {
|
||||
// since we only use the one private key to sign the tokens,
|
||||
// we also only use its public counter part to verify
|
||||
return verifyKey, nil
|
||||
})
|
||||
|
||||
}, request.WithClaims(&CustomClaimsExample{}))
|
||||
// If the token is missing or invalid, return error
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
|
@ -214,5 +205,4 @@ func restrictedHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// Token is valid
|
||||
fmt.Fprintln(w, "Welcome,", token.Claims.(*CustomClaimsExample).Name)
|
||||
return
|
||||
}
|
||||
|
|
103
map_claims.go
103
map_claims.go
|
@ -3,10 +3,11 @@ package jwt
|
|||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
// "fmt"
|
||||
)
|
||||
|
||||
// Claims type that uses the map[string]interface{} for JSON decoding
|
||||
// MapClaims is a claims type that uses the map[string]interface{} for JSON decoding.
|
||||
// This is the default claims type if you don't supply one
|
||||
type MapClaims map[string]interface{}
|
||||
|
||||
|
@ -31,65 +32,92 @@ func (m MapClaims) VerifyAudience(cmp string, req bool) bool {
|
|||
return verifyAud(aud, cmp, req)
|
||||
}
|
||||
|
||||
// Compares the exp claim against cmp.
|
||||
// If required is false, this method will return true if the value matches or is unset
|
||||
// VerifyExpiresAt compares the exp claim against cmp (cmp <= exp).
|
||||
// If req is false, it will return true, if exp is unset.
|
||||
func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool {
|
||||
exp, ok := m["exp"]
|
||||
cmpTime := time.Unix(cmp, 0)
|
||||
|
||||
v, ok := m["exp"]
|
||||
if !ok {
|
||||
return !req
|
||||
}
|
||||
switch expType := exp.(type) {
|
||||
|
||||
switch exp := v.(type) {
|
||||
case float64:
|
||||
return verifyExp(int64(expType), cmp, req)
|
||||
case json.Number:
|
||||
v, _ := expType.Int64()
|
||||
return verifyExp(v, cmp, req)
|
||||
if exp == 0 {
|
||||
return verifyExp(nil, cmpTime, req)
|
||||
}
|
||||
|
||||
return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req)
|
||||
case json.Number:
|
||||
v, _ := exp.Float64()
|
||||
|
||||
return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Compares the iat claim against cmp.
|
||||
// If required is false, this method will return true if the value matches or is unset
|
||||
// VerifyIssuedAt compares the exp claim against cmp (cmp >= iat).
|
||||
// If req is false, it will return true, if iat is unset.
|
||||
func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool {
|
||||
iat, ok := m["iat"]
|
||||
cmpTime := time.Unix(cmp, 0)
|
||||
|
||||
v, ok := m["iat"]
|
||||
if !ok {
|
||||
return !req
|
||||
}
|
||||
switch iatType := iat.(type) {
|
||||
|
||||
switch iat := v.(type) {
|
||||
case float64:
|
||||
return verifyIat(int64(iatType), cmp, req)
|
||||
case json.Number:
|
||||
v, _ := iatType.Int64()
|
||||
return verifyIat(v, cmp, req)
|
||||
if iat == 0 {
|
||||
return verifyIat(nil, cmpTime, req)
|
||||
}
|
||||
|
||||
return verifyIat(&newNumericDateFromSeconds(iat).Time, cmpTime, req)
|
||||
case json.Number:
|
||||
v, _ := iat.Float64()
|
||||
|
||||
return verifyIat(&newNumericDateFromSeconds(v).Time, cmpTime, req)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Compares the iss claim against cmp.
|
||||
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
|
||||
// If req is false, it will return true, if nbf is unset.
|
||||
func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool {
|
||||
cmpTime := time.Unix(cmp, 0)
|
||||
|
||||
v, ok := m["nbf"]
|
||||
if !ok {
|
||||
return !req
|
||||
}
|
||||
|
||||
switch nbf := v.(type) {
|
||||
case float64:
|
||||
if nbf == 0 {
|
||||
return verifyNbf(nil, cmpTime, req)
|
||||
}
|
||||
|
||||
return verifyNbf(&newNumericDateFromSeconds(nbf).Time, cmpTime, req)
|
||||
case json.Number:
|
||||
v, _ := nbf.Float64()
|
||||
|
||||
return verifyNbf(&newNumericDateFromSeconds(v).Time, cmpTime, req)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// VerifyIssuer compares the iss claim against cmp.
|
||||
// If required is false, this method will return true if the value matches or is unset
|
||||
func (m MapClaims) VerifyIssuer(cmp string, req bool) bool {
|
||||
iss, _ := m["iss"].(string)
|
||||
return verifyIss(iss, cmp, req)
|
||||
}
|
||||
|
||||
// Compares the nbf claim against cmp.
|
||||
// If required is false, this method will return true if the value matches or is unset
|
||||
func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool {
|
||||
nbf, ok := m["nbf"]
|
||||
if !ok {
|
||||
return !req
|
||||
}
|
||||
switch nbfType := nbf.(type) {
|
||||
case float64:
|
||||
return verifyNbf(int64(nbfType), cmp, req)
|
||||
case json.Number:
|
||||
v, _ := nbfType.Int64()
|
||||
return verifyNbf(v, cmp, req)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Validates time based claims "exp, iat, nbf".
|
||||
// Valid validates time based claims "exp, iat, nbf".
|
||||
// There is no accounting for clock skew.
|
||||
// As well, if any of the above claims are not in the token, it will still
|
||||
// be considered a valid claim.
|
||||
|
@ -98,16 +126,19 @@ func (m MapClaims) Valid() error {
|
|||
now := TimeFunc().Unix()
|
||||
|
||||
if !m.VerifyExpiresAt(now, false) {
|
||||
// TODO(oxisto): this should be replaced with ErrTokenExpired
|
||||
vErr.Inner = errors.New("Token is expired")
|
||||
vErr.Errors |= ValidationErrorExpired
|
||||
}
|
||||
|
||||
if !m.VerifyIssuedAt(now, false) {
|
||||
// TODO(oxisto): this should be replaced with ErrTokenUsedBeforeIssued
|
||||
vErr.Inner = errors.New("Token used before issued")
|
||||
vErr.Errors |= ValidationErrorIssuedAt
|
||||
}
|
||||
|
||||
if !m.VerifyNotBefore(now, false) {
|
||||
// TODO(oxisto): this should be replaced with ErrTokenNotValidYet
|
||||
vErr.Inner = errors.New("Token is not valid yet")
|
||||
vErr.Errors |= ValidationErrorNotValidYet
|
||||
}
|
||||
|
|
|
@ -2,13 +2,14 @@ package jwt
|
|||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestVerifyAud(t *testing.T) {
|
||||
var nilInterface interface{}
|
||||
var nilListInterface []interface{}
|
||||
var intListInterface interface{} = []int{1,2,3}
|
||||
type test struct{
|
||||
var intListInterface interface{} = []int{1, 2, 3}
|
||||
type test struct {
|
||||
Name string
|
||||
MapClaims MapClaims
|
||||
Expected bool
|
||||
|
@ -18,44 +19,41 @@ func TestVerifyAud(t *testing.T) {
|
|||
tests := []test{
|
||||
// Matching Claim in aud
|
||||
// Required = true
|
||||
{ Name: "String Aud matching required", MapClaims: MapClaims{"aud": "example.com"}, Expected: true , Required: true, Comparison: "example.com"},
|
||||
{ Name: "[]String Aud with match required", MapClaims: MapClaims{"aud": []string{"example.com", "example.example.com"}}, Expected: true, Required: true, Comparison: "example.com"},
|
||||
{Name: "String Aud matching required", MapClaims: MapClaims{"aud": "example.com"}, Expected: true, Required: true, Comparison: "example.com"},
|
||||
{Name: "[]String Aud with match required", MapClaims: MapClaims{"aud": []string{"example.com", "example.example.com"}}, Expected: true, Required: true, Comparison: "example.com"},
|
||||
|
||||
// Required = false
|
||||
{ Name: "String Aud with match not required", MapClaims: MapClaims{"aud": "example.com"}, Expected: true , Required: false, Comparison: "example.com"},
|
||||
{ Name: "Empty String Aud with match not required", MapClaims: MapClaims{}, Expected: true , Required: false, Comparison: "example.com"},
|
||||
{ Name: "Empty String Aud with match not required", MapClaims: MapClaims{"aud": ""}, Expected: true , Required: false, Comparison: "example.com"},
|
||||
{ Name: "Nil String Aud with match not required", MapClaims: MapClaims{"aud": nil}, Expected: true , Required: false, Comparison: "example.com"},
|
||||
{Name: "String Aud with match not required", MapClaims: MapClaims{"aud": "example.com"}, Expected: true, Required: false, Comparison: "example.com"},
|
||||
{Name: "Empty String Aud with match not required", MapClaims: MapClaims{}, Expected: true, Required: false, Comparison: "example.com"},
|
||||
{Name: "Empty String Aud with match not required", MapClaims: MapClaims{"aud": ""}, Expected: true, Required: false, Comparison: "example.com"},
|
||||
{Name: "Nil String Aud with match not required", MapClaims: MapClaims{"aud": nil}, Expected: true, Required: false, Comparison: "example.com"},
|
||||
|
||||
{ Name: "[]String Aud with match not required", MapClaims: MapClaims{"aud": []string{"example.com", "example.example.com"}}, Expected: true, Required: false, Comparison: "example.com"},
|
||||
{ Name: "Empty []String Aud with match not required", MapClaims: MapClaims{"aud": []string{}}, Expected: true, Required: false, Comparison: "example.com"},
|
||||
{Name: "[]String Aud with match not required", MapClaims: MapClaims{"aud": []string{"example.com", "example.example.com"}}, Expected: true, Required: false, Comparison: "example.com"},
|
||||
{Name: "Empty []String Aud with match not required", MapClaims: MapClaims{"aud": []string{}}, Expected: true, Required: false, Comparison: "example.com"},
|
||||
|
||||
// Non-Matching Claim in aud
|
||||
// Required = true
|
||||
{ Name: "String Aud without match required", MapClaims: MapClaims{"aud": "not.example.com"}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{ Name: "Empty String Aud without match required", MapClaims: MapClaims{"aud": ""}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{ Name: "[]String Aud without match required", MapClaims: MapClaims{"aud": []string{"not.example.com", "example.example.com"}}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{ Name: "Empty []String Aud without match required", MapClaims: MapClaims{"aud": []string{""}}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{ Name: "String Aud without match not required", MapClaims: MapClaims{"aud": "not.example.com"}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{ Name: "Empty String Aud without match not required", MapClaims: MapClaims{"aud": ""}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{ Name: "[]String Aud without match not required", MapClaims: MapClaims{"aud": []string{"not.example.com", "example.example.com"}}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{Name: "String Aud without match required", MapClaims: MapClaims{"aud": "not.example.com"}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{Name: "Empty String Aud without match required", MapClaims: MapClaims{"aud": ""}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{Name: "[]String Aud without match required", MapClaims: MapClaims{"aud": []string{"not.example.com", "example.example.com"}}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{Name: "Empty []String Aud without match required", MapClaims: MapClaims{"aud": []string{""}}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{Name: "String Aud without match not required", MapClaims: MapClaims{"aud": "not.example.com"}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{Name: "Empty String Aud without match not required", MapClaims: MapClaims{"aud": ""}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{Name: "[]String Aud without match not required", MapClaims: MapClaims{"aud": []string{"not.example.com", "example.example.com"}}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
|
||||
// Required = false
|
||||
{ Name: "Empty []String Aud without match required", MapClaims: MapClaims{"aud": []string{""}}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{Name: "Empty []String Aud without match required", MapClaims: MapClaims{"aud": []string{""}}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
|
||||
// []interface{}
|
||||
{ Name: "Empty []interface{} Aud without match required", MapClaims: MapClaims{"aud": nilListInterface}, Expected: true, Required: false, Comparison: "example.com"},
|
||||
{ Name: "[]interface{} Aud wit match required", MapClaims: MapClaims{"aud": []interface{}{"a", "foo", "example.com"}}, Expected: true, Required: true, Comparison: "example.com"},
|
||||
{ Name: "[]interface{} Aud wit match but invalid types", MapClaims: MapClaims{"aud": []interface{}{"a", 5, "example.com"}}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{ Name: "[]interface{} Aud int wit match required", MapClaims: MapClaims{"aud": intListInterface}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
|
||||
{Name: "Empty []interface{} Aud without match required", MapClaims: MapClaims{"aud": nilListInterface}, Expected: true, Required: false, Comparison: "example.com"},
|
||||
{Name: "[]interface{} Aud wit match required", MapClaims: MapClaims{"aud": []interface{}{"a", "foo", "example.com"}}, Expected: true, Required: true, Comparison: "example.com"},
|
||||
{Name: "[]interface{} Aud wit match but invalid types", MapClaims: MapClaims{"aud": []interface{}{"a", 5, "example.com"}}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{Name: "[]interface{} Aud int wit match required", MapClaims: MapClaims{"aud": intListInterface}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
|
||||
// interface{}
|
||||
{ Name: "Empty interface{} Aud without match not required", MapClaims: MapClaims{"aud": nilInterface}, Expected: true, Required: false, Comparison: "example.com"},
|
||||
|
||||
{Name: "Empty interface{} Aud without match not required", MapClaims: MapClaims{"aud": nilInterface}, Expected: true, Required: false, Comparison: "example.com"},
|
||||
}
|
||||
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
got := test.MapClaims.VerifyAudience(test.Comparison, test.Required)
|
||||
|
@ -100,3 +98,26 @@ func TestMapclaimsVerifyExpiresAtInvalidTypeString(t *testing.T) {
|
|||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapClaimsVerifyExpiresAtExpire(t *testing.T) {
|
||||
exp := time.Now().Unix()
|
||||
mapClaims := MapClaims{
|
||||
"exp": float64(exp),
|
||||
}
|
||||
want := false
|
||||
got := mapClaims.VerifyExpiresAt(exp, true)
|
||||
if want != got {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
||||
}
|
||||
|
||||
got = mapClaims.VerifyExpiresAt(exp+1, true)
|
||||
if want != got {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
||||
}
|
||||
|
||||
want = true
|
||||
got = mapClaims.VerifyExpiresAt(exp-1, true)
|
||||
if want != got {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
|
2
none.go
2
none.go
|
@ -1,6 +1,6 @@
|
|||
package jwt
|
||||
|
||||
// Implements the none signing method. This is required by the spec
|
||||
// SigningMethodNone implements the none signing method. This is required by the spec
|
||||
// but you probably should never use it.
|
||||
var SigningMethodNone *signingMethodNone
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"git.internal/re/jwt/v4"
|
||||
)
|
||||
|
||||
var noneTestData = []struct {
|
||||
|
|
49
parser.go
49
parser.go
|
@ -8,18 +8,47 @@ import (
|
|||
)
|
||||
|
||||
type Parser struct {
|
||||
ValidMethods []string // If populated, only these methods will be considered valid
|
||||
UseJSONNumber bool // Use JSON Number format in JSON decoder
|
||||
SkipClaimsValidation bool // Skip claims validation during token parsing
|
||||
// If populated, only these methods will be considered valid.
|
||||
//
|
||||
// Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead.
|
||||
ValidMethods []string
|
||||
|
||||
// Use JSON Number format in JSON decoder.
|
||||
//
|
||||
// Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead.
|
||||
UseJSONNumber bool
|
||||
|
||||
// Skip claims validation during token parsing.
|
||||
//
|
||||
// Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead.
|
||||
SkipClaimsValidation bool
|
||||
}
|
||||
|
||||
// Parse, validate, and return a token.
|
||||
// NewParser creates a new Parser with the specified options
|
||||
func NewParser(options ...ParserOption) *Parser {
|
||||
p := &Parser{}
|
||||
|
||||
// loop through our parsing options and apply them
|
||||
for _, option := range options {
|
||||
option(p)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Parse parses, validates, verifies the signature and returns the parsed token.
|
||||
// keyFunc will receive the parsed token and should return the key for validating.
|
||||
// If everything is kosher, err will be nil
|
||||
func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
|
||||
return p.ParseWithClaims(tokenString, MapClaims{}, keyFunc)
|
||||
}
|
||||
|
||||
// ParseWithClaims parses, validates, and verifies like Parse, but supplies a default object implementing the Claims
|
||||
// interface. This provides default values which can be overridden and allows a caller to use their own type, rather
|
||||
// than the default MapClaims implementation of Claims.
|
||||
//
|
||||
// Note: If you provide a custom claim implementation that embeds one of the standard claims (such as RegisteredClaims),
|
||||
// make sure that a) you either embed a non-pointer version of the claims or b) if you are using a pointer, allocate the
|
||||
// proper memory for it before passing in the overall claims, otherwise you might run into a panic.
|
||||
func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
|
||||
token, parts, err := p.ParseUnverified(tokenString, claims)
|
||||
if err != nil {
|
||||
|
@ -87,12 +116,12 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
|
|||
return token, vErr
|
||||
}
|
||||
|
||||
// WARNING: Don't use this method unless you know what you're doing
|
||||
// ParseUnverified parses the token but doesn't validate the signature.
|
||||
//
|
||||
// This method parses the token but doesn't validate the signature. It's only
|
||||
// ever useful in cases where you know the signature is valid (because it has
|
||||
// been checked previously in the stack) and you want to extract values from
|
||||
// it.
|
||||
// WARNING: Don't use this method unless you know what you're doing.
|
||||
//
|
||||
// It's only ever useful in cases where you know the signature is valid (because it has
|
||||
// been checked previously in the stack) and you want to extract values from it.
|
||||
func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) {
|
||||
parts = strings.Split(tokenString, ".")
|
||||
if len(parts) != 3 {
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package jwt
|
||||
|
||||
// ParserOption is used to implement functional-style options that modify the behavior of the parser. To add
|
||||
// new options, just create a function (ideally beginning with With or Without) that returns an anonymous function that
|
||||
// takes a *Parser type as input and manipulates its configuration accordingly.
|
||||
type ParserOption func(*Parser)
|
||||
|
||||
// WithValidMethods is an option to supply algorithm methods that the parser will check. Only those methods will be considered valid.
|
||||
// It is heavily encouraged to use this option in order to prevent attacks such as https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/.
|
||||
func WithValidMethods(methods []string) ParserOption {
|
||||
return func(p *Parser) {
|
||||
p.ValidMethods = methods
|
||||
}
|
||||
}
|
||||
|
||||
// WithJSONNumber is an option to configure the underlying JSON parser with UseNumber
|
||||
func WithJSONNumber() ParserOption {
|
||||
return func(p *Parser) {
|
||||
p.UseJSONNumber = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithoutClaimsValidation is an option to disable claims validation. This option should only be used if you exactly know
|
||||
// what you are doing.
|
||||
func WithoutClaimsValidation() ParserOption {
|
||||
return func(p *Parser) {
|
||||
p.SkipClaimsValidation = true
|
||||
}
|
||||
}
|
465
parser_test.go
465
parser_test.go
|
@ -1,29 +1,47 @@
|
|||
package jwt_test
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/golang-jwt/jwt/test"
|
||||
"git.internal/re/jwt/v4"
|
||||
"git.internal/re/jwt/v4/test"
|
||||
)
|
||||
|
||||
var keyFuncError error = fmt.Errorf("error loading key")
|
||||
var errKeyFuncError error = fmt.Errorf("error loading key")
|
||||
|
||||
var (
|
||||
jwtTestDefaultKey *rsa.PublicKey
|
||||
jwtTestRSAPrivateKey *rsa.PrivateKey
|
||||
jwtTestEC256PublicKey crypto.PublicKey
|
||||
jwtTestEC256PrivateKey crypto.PrivateKey
|
||||
paddedKey crypto.PublicKey
|
||||
defaultKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return jwtTestDefaultKey, nil }
|
||||
ecdsaKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return jwtTestEC256PublicKey, nil }
|
||||
paddedKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return paddedKey, nil }
|
||||
emptyKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, nil }
|
||||
errorKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, keyFuncError }
|
||||
errorKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, errKeyFuncError }
|
||||
nilKeyFunc jwt.Keyfunc = nil
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Load public keys
|
||||
jwtTestDefaultKey = test.LoadRSAPublicKeyFromDisk("test/sample_key.pub")
|
||||
jwtTestEC256PublicKey = test.LoadECPublicKeyFromDisk("test/ec256-public.pem")
|
||||
|
||||
// Load padded public key - note there is only a public key for this key pair and should only be used for the
|
||||
// two test cases below.
|
||||
paddedKey = test.LoadECPublicKeyFromDisk("test/examplePaddedKey-public.pem")
|
||||
|
||||
// Load private keys
|
||||
jwtTestRSAPrivateKey = test.LoadRSAPrivateKeyFromDisk("test/sample_key")
|
||||
jwtTestEC256PrivateKey = test.LoadECPrivateKeyFromDisk("test/ec256-private.pem")
|
||||
}
|
||||
|
||||
var jwtTestData = []struct {
|
||||
|
@ -33,7 +51,9 @@ var jwtTestData = []struct {
|
|||
claims jwt.Claims
|
||||
valid bool
|
||||
errors uint32
|
||||
err []error
|
||||
parser *jwt.Parser
|
||||
signingMethod jwt.SigningMethod // The method to sign the JWT token for test purpose
|
||||
}{
|
||||
{
|
||||
"basic",
|
||||
|
@ -43,6 +63,8 @@ var jwtTestData = []struct {
|
|||
true,
|
||||
0,
|
||||
nil,
|
||||
nil,
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"basic expired",
|
||||
|
@ -51,7 +73,9 @@ var jwtTestData = []struct {
|
|||
jwt.MapClaims{"foo": "bar", "exp": float64(time.Now().Unix() - 100)},
|
||||
false,
|
||||
jwt.ValidationErrorExpired,
|
||||
[]error{jwt.ErrTokenExpired},
|
||||
nil,
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"basic nbf",
|
||||
|
@ -60,7 +84,9 @@ var jwtTestData = []struct {
|
|||
jwt.MapClaims{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)},
|
||||
false,
|
||||
jwt.ValidationErrorNotValidYet,
|
||||
[]error{jwt.ErrTokenNotValidYet},
|
||||
nil,
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"expired and nbf",
|
||||
|
@ -69,7 +95,9 @@ var jwtTestData = []struct {
|
|||
jwt.MapClaims{"foo": "bar", "nbf": float64(time.Now().Unix() + 100), "exp": float64(time.Now().Unix() - 100)},
|
||||
false,
|
||||
jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired,
|
||||
[]error{jwt.ErrTokenNotValidYet},
|
||||
nil,
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"basic invalid",
|
||||
|
@ -78,7 +106,9 @@ var jwtTestData = []struct {
|
|||
jwt.MapClaims{"foo": "bar"},
|
||||
false,
|
||||
jwt.ValidationErrorSignatureInvalid,
|
||||
[]error{jwt.ErrTokenSignatureInvalid, rsa.ErrVerification},
|
||||
nil,
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"basic nokeyfunc",
|
||||
|
@ -87,7 +117,9 @@ var jwtTestData = []struct {
|
|||
jwt.MapClaims{"foo": "bar"},
|
||||
false,
|
||||
jwt.ValidationErrorUnverifiable,
|
||||
[]error{jwt.ErrTokenUnverifiable},
|
||||
nil,
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"basic nokey",
|
||||
|
@ -96,7 +128,9 @@ var jwtTestData = []struct {
|
|||
jwt.MapClaims{"foo": "bar"},
|
||||
false,
|
||||
jwt.ValidationErrorSignatureInvalid,
|
||||
[]error{jwt.ErrTokenSignatureInvalid},
|
||||
nil,
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"basic errorkey",
|
||||
|
@ -105,7 +139,9 @@ var jwtTestData = []struct {
|
|||
jwt.MapClaims{"foo": "bar"},
|
||||
false,
|
||||
jwt.ValidationErrorUnverifiable,
|
||||
[]error{jwt.ErrTokenUnverifiable, errKeyFuncError},
|
||||
nil,
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"invalid signing method",
|
||||
|
@ -114,16 +150,42 @@ var jwtTestData = []struct {
|
|||
jwt.MapClaims{"foo": "bar"},
|
||||
false,
|
||||
jwt.ValidationErrorSignatureInvalid,
|
||||
[]error{jwt.ErrTokenSignatureInvalid},
|
||||
&jwt.Parser{ValidMethods: []string{"HS256"}},
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"valid signing method",
|
||||
"valid RSA signing method",
|
||||
"",
|
||||
defaultKeyFunc,
|
||||
jwt.MapClaims{"foo": "bar"},
|
||||
true,
|
||||
0,
|
||||
nil,
|
||||
&jwt.Parser{ValidMethods: []string{"RS256", "HS256"}},
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"ECDSA signing method not accepted",
|
||||
"",
|
||||
ecdsaKeyFunc,
|
||||
jwt.MapClaims{"foo": "bar"},
|
||||
false,
|
||||
jwt.ValidationErrorSignatureInvalid,
|
||||
[]error{jwt.ErrTokenSignatureInvalid},
|
||||
&jwt.Parser{ValidMethods: []string{"RS256", "HS256"}},
|
||||
jwt.SigningMethodES256,
|
||||
},
|
||||
{
|
||||
"valid ECDSA signing method",
|
||||
"",
|
||||
ecdsaKeyFunc,
|
||||
jwt.MapClaims{"foo": "bar"},
|
||||
true,
|
||||
0,
|
||||
nil,
|
||||
&jwt.Parser{ValidMethods: []string{"HS256", "ES256"}},
|
||||
jwt.SigningMethodES256,
|
||||
},
|
||||
{
|
||||
"JSON Number",
|
||||
|
@ -132,7 +194,9 @@ var jwtTestData = []struct {
|
|||
jwt.MapClaims{"foo": json.Number("123.4")},
|
||||
true,
|
||||
0,
|
||||
nil,
|
||||
&jwt.Parser{UseJSONNumber: true},
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"Standard Claims",
|
||||
|
@ -143,7 +207,9 @@ var jwtTestData = []struct {
|
|||
},
|
||||
true,
|
||||
0,
|
||||
nil,
|
||||
&jwt.Parser{UseJSONNumber: true},
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"JSON Number - basic expired",
|
||||
|
@ -152,7 +218,9 @@ var jwtTestData = []struct {
|
|||
jwt.MapClaims{"foo": "bar", "exp": json.Number(fmt.Sprintf("%v", time.Now().Unix()-100))},
|
||||
false,
|
||||
jwt.ValidationErrorExpired,
|
||||
[]error{jwt.ErrTokenExpired},
|
||||
&jwt.Parser{UseJSONNumber: true},
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"JSON Number - basic nbf",
|
||||
|
@ -161,7 +229,9 @@ var jwtTestData = []struct {
|
|||
jwt.MapClaims{"foo": "bar", "nbf": json.Number(fmt.Sprintf("%v", time.Now().Unix()+100))},
|
||||
false,
|
||||
jwt.ValidationErrorNotValidYet,
|
||||
[]error{jwt.ErrTokenNotValidYet},
|
||||
&jwt.Parser{UseJSONNumber: true},
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"JSON Number - expired and nbf",
|
||||
|
@ -170,7 +240,9 @@ var jwtTestData = []struct {
|
|||
jwt.MapClaims{"foo": "bar", "nbf": json.Number(fmt.Sprintf("%v", time.Now().Unix()+100)), "exp": json.Number(fmt.Sprintf("%v", time.Now().Unix()-100))},
|
||||
false,
|
||||
jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired,
|
||||
[]error{jwt.ErrTokenNotValidYet},
|
||||
&jwt.Parser{UseJSONNumber: true},
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"SkipClaimsValidation during token parsing",
|
||||
|
@ -179,24 +251,105 @@ var jwtTestData = []struct {
|
|||
jwt.MapClaims{"foo": "bar", "nbf": json.Number(fmt.Sprintf("%v", time.Now().Unix()+100))},
|
||||
true,
|
||||
0,
|
||||
nil,
|
||||
&jwt.Parser{UseJSONNumber: true, SkipClaimsValidation: true},
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"RFC7519 Claims",
|
||||
"",
|
||||
defaultKeyFunc,
|
||||
&jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * 10)),
|
||||
},
|
||||
true,
|
||||
0,
|
||||
nil,
|
||||
&jwt.Parser{UseJSONNumber: true},
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"RFC7519 Claims - single aud",
|
||||
"",
|
||||
defaultKeyFunc,
|
||||
&jwt.RegisteredClaims{
|
||||
Audience: jwt.ClaimStrings{"test"},
|
||||
},
|
||||
true,
|
||||
0,
|
||||
nil,
|
||||
&jwt.Parser{UseJSONNumber: true},
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"RFC7519 Claims - multiple aud",
|
||||
"",
|
||||
defaultKeyFunc,
|
||||
&jwt.RegisteredClaims{
|
||||
Audience: jwt.ClaimStrings{"test", "test"},
|
||||
},
|
||||
true,
|
||||
0,
|
||||
nil,
|
||||
&jwt.Parser{UseJSONNumber: true},
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"RFC7519 Claims - single aud with wrong type",
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOjF9.8mAIDUfZNQT3TGm1QFIQp91OCpJpQpbB1-m9pA2mkHc", // { "aud": 1 }
|
||||
defaultKeyFunc,
|
||||
&jwt.RegisteredClaims{
|
||||
Audience: nil, // because of the unmarshal error, this will be empty
|
||||
},
|
||||
false,
|
||||
jwt.ValidationErrorMalformed,
|
||||
[]error{jwt.ErrTokenMalformed},
|
||||
&jwt.Parser{UseJSONNumber: true},
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
{
|
||||
"RFC7519 Claims - multiple aud with wrong types",
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidGVzdCIsMV19.htEBUf7BVbfSmVoTFjXf3y6DLmDUuLy1vTJ14_EX7Ws", // { "aud": ["test", 1] }
|
||||
defaultKeyFunc,
|
||||
&jwt.RegisteredClaims{
|
||||
Audience: nil, // because of the unmarshal error, this will be empty
|
||||
},
|
||||
false,
|
||||
jwt.ValidationErrorMalformed,
|
||||
[]error{jwt.ErrTokenMalformed},
|
||||
&jwt.Parser{UseJSONNumber: true},
|
||||
jwt.SigningMethodRS256,
|
||||
},
|
||||
}
|
||||
|
||||
func TestParser_Parse(t *testing.T) {
|
||||
privateKey := test.LoadRSAPrivateKeyFromDisk("test/sample_key")
|
||||
// signToken creates and returns a signed JWT token using signingMethod.
|
||||
func signToken(claims jwt.Claims, signingMethod jwt.SigningMethod) string {
|
||||
var privateKey interface{}
|
||||
switch signingMethod {
|
||||
case jwt.SigningMethodRS256:
|
||||
privateKey = jwtTestRSAPrivateKey
|
||||
case jwt.SigningMethodES256:
|
||||
privateKey = jwtTestEC256PrivateKey
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
return test.MakeSampleToken(claims, signingMethod, privateKey)
|
||||
}
|
||||
|
||||
func TestParser_Parse(t *testing.T) {
|
||||
// Iterate over test data set and run tests
|
||||
for _, data := range jwtTestData {
|
||||
t.Run(data.name, func(t *testing.T) {
|
||||
// If the token string is blank, use helper function to generate string
|
||||
if data.tokenString == "" {
|
||||
data.tokenString = test.MakeSampleToken(data.claims, privateKey)
|
||||
data.tokenString = signToken(data.claims, data.signingMethod)
|
||||
}
|
||||
|
||||
// Parse the token
|
||||
var token *jwt.Token
|
||||
var ve *jwt.ValidationError
|
||||
var err error
|
||||
var parser = data.parser
|
||||
parser := data.parser
|
||||
if parser == nil {
|
||||
parser = new(jwt.Parser)
|
||||
}
|
||||
|
@ -206,6 +359,8 @@ func TestParser_Parse(t *testing.T) {
|
|||
token, err = parser.ParseWithClaims(data.tokenString, jwt.MapClaims{}, data.keyfunc)
|
||||
case *jwt.StandardClaims:
|
||||
token, err = parser.ParseWithClaims(data.tokenString, &jwt.StandardClaims{}, data.keyfunc)
|
||||
case *jwt.RegisteredClaims:
|
||||
token, err = parser.ParseWithClaims(data.tokenString, &jwt.RegisteredClaims{}, data.keyfunc)
|
||||
}
|
||||
|
||||
// Verify result matches expectation
|
||||
|
@ -229,38 +384,65 @@ func TestParser_Parse(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Errorf("[%v] Expecting error. Didn't get one.", data.name)
|
||||
} else {
|
||||
|
||||
ve := err.(*jwt.ValidationError)
|
||||
if errors.As(err, &ve) {
|
||||
// compare the bitfield part of the error
|
||||
if e := ve.Errors; e != data.errors {
|
||||
t.Errorf("[%v] Errors don't match expectation. %v != %v", data.name, e, data.errors)
|
||||
}
|
||||
|
||||
if err.Error() == keyFuncError.Error() && ve.Inner != keyFuncError {
|
||||
t.Errorf("[%v] Inner error does not match expectation. %v != %v", data.name, ve.Inner, keyFuncError)
|
||||
if err.Error() == errKeyFuncError.Error() && ve.Inner != errKeyFuncError {
|
||||
t.Errorf("[%v] Inner error does not match expectation. %v != %v", data.name, ve.Inner, errKeyFuncError)
|
||||
}
|
||||
}
|
||||
}
|
||||
if data.valid && token.Signature == "" {
|
||||
}
|
||||
|
||||
if data.err != nil {
|
||||
if err == nil {
|
||||
t.Errorf("[%v] Expecting error(s). Didn't get one.", data.name)
|
||||
} else {
|
||||
all := false
|
||||
for _, e := range data.err {
|
||||
all = errors.Is(err, e)
|
||||
}
|
||||
|
||||
if !all {
|
||||
t.Errorf("[%v] Errors don't match expectation. %v should contain all of %v", data.name, err, data.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if data.valid {
|
||||
if token.Signature == "" {
|
||||
t.Errorf("[%v] Signature is left unpopulated after parsing", data.name)
|
||||
}
|
||||
if !token.Valid {
|
||||
// The 'Valid' field should be set to true when invoking Parse()
|
||||
t.Errorf("[%v] Token.Valid field mismatch. Expecting true, got %v", data.name, token.Valid)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseUnverified(t *testing.T) {
|
||||
privateKey := test.LoadRSAPrivateKeyFromDisk("test/sample_key")
|
||||
|
||||
// Iterate over test data set and run tests
|
||||
for _, data := range jwtTestData {
|
||||
// Skip test data, that intentionally contains malformed tokens, as they would lead to an error
|
||||
if data.errors&jwt.ValidationErrorMalformed != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
t.Run(data.name, func(t *testing.T) {
|
||||
// If the token string is blank, use helper function to generate string
|
||||
if data.tokenString == "" {
|
||||
data.tokenString = test.MakeSampleToken(data.claims, privateKey)
|
||||
data.tokenString = signToken(data.claims, data.signingMethod)
|
||||
}
|
||||
|
||||
// Parse the token
|
||||
var token *jwt.Token
|
||||
var err error
|
||||
var parser = data.parser
|
||||
parser := data.parser
|
||||
if parser == nil {
|
||||
parser = new(jwt.Parser)
|
||||
}
|
||||
|
@ -270,6 +452,8 @@ func TestParser_ParseUnverified(t *testing.T) {
|
|||
token, _, err = parser.ParseUnverified(data.tokenString, jwt.MapClaims{})
|
||||
case *jwt.StandardClaims:
|
||||
token, _, err = parser.ParseUnverified(data.tokenString, &jwt.StandardClaims{})
|
||||
case *jwt.RegisteredClaims:
|
||||
token, _, err = parser.ParseUnverified(data.tokenString, &jwt.RegisteredClaims{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -284,11 +468,253 @@ func TestParser_ParseUnverified(t *testing.T) {
|
|||
if data.valid && err != nil {
|
||||
t.Errorf("[%v] Error while verifying token: %T:%v", data.name, err, err)
|
||||
}
|
||||
if token.Valid {
|
||||
// The 'Valid' field should not be set to true when invoking ParseUnverified()
|
||||
t.Errorf("[%v] Token.Valid field mismatch. Expecting false, got %v", data.name, token.Valid)
|
||||
}
|
||||
if token.Signature != "" {
|
||||
// The signature was not validated, hence the 'Signature' field is not populated.
|
||||
t.Errorf("[%v] Token.Signature field mismatch. Expecting '', got %v", data.name, token.Signature)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method for benchmarking various methods
|
||||
var setPaddingTestData = []struct {
|
||||
name string
|
||||
tokenString string
|
||||
claims jwt.Claims
|
||||
paddedDecode bool
|
||||
strictDecode bool
|
||||
signingMethod jwt.SigningMethod
|
||||
keyfunc jwt.Keyfunc
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
name: "Validated non-padded token with padding disabled",
|
||||
tokenString: "",
|
||||
claims: jwt.MapClaims{"foo": "paddedbar"},
|
||||
paddedDecode: false,
|
||||
signingMethod: jwt.SigningMethodRS256,
|
||||
keyfunc: defaultKeyFunc,
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Validated non-padded token with padding enabled",
|
||||
tokenString: "",
|
||||
claims: jwt.MapClaims{"foo": "paddedbar"},
|
||||
paddedDecode: true,
|
||||
signingMethod: jwt.SigningMethodRS256,
|
||||
keyfunc: defaultKeyFunc,
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Error for padded token with padding disabled",
|
||||
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ==.20kGGJaYekGTRFf8b0TwhuETcR8lv5z2363X5jf7G1yTWVTwOmte5Ii8L8_OQbYwPoiVHmZY6iJPbt_DhCN42AeFY74BcsUhR-BVrYUVhKK0RppuzEcSlILDNeQsJDLEL035CPm1VO6Jrgk7enQPIctVxUesRgswP71OpGvJxy3j1k_J8p0WzZvRZTe1D_2Misa0UDGwnEIHhmr97fIpMSZjFxlcygQw8QN34IHLHIXMaTY1eiCf4CCr6rOS9wUeu7P3CPkmFq9XhxBT_LLCmIMhHnxP5x27FUJE_JZlfek0MmARcrhpsZS2sFhHAiWrjxjOE27jkDtv1nEwn65wMw==",
|
||||
claims: jwt.MapClaims{"foo": "paddedbar"},
|
||||
paddedDecode: false,
|
||||
signingMethod: jwt.SigningMethodRS256,
|
||||
keyfunc: defaultKeyFunc,
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "Validated padded token with padding enabled",
|
||||
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ==.20kGGJaYekGTRFf8b0TwhuETcR8lv5z2363X5jf7G1yTWVTwOmte5Ii8L8_OQbYwPoiVHmZY6iJPbt_DhCN42AeFY74BcsUhR-BVrYUVhKK0RppuzEcSlILDNeQsJDLEL035CPm1VO6Jrgk7enQPIctVxUesRgswP71OpGvJxy3j1k_J8p0WzZvRZTe1D_2Misa0UDGwnEIHhmr97fIpMSZjFxlcygQw8QN34IHLHIXMaTY1eiCf4CCr6rOS9wUeu7P3CPkmFq9XhxBT_LLCmIMhHnxP5x27FUJE_JZlfek0MmARcrhpsZS2sFhHAiWrjxjOE27jkDtv1nEwn65wMw==",
|
||||
claims: jwt.MapClaims{"foo": "paddedbar"},
|
||||
paddedDecode: true,
|
||||
signingMethod: jwt.SigningMethodRS256,
|
||||
keyfunc: defaultKeyFunc,
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Error for example padded token with padding disabled",
|
||||
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3grw==",
|
||||
claims: nil,
|
||||
paddedDecode: false,
|
||||
signingMethod: jwt.SigningMethodES256,
|
||||
keyfunc: paddedKeyFunc,
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "Validated example padded token with padding enabled",
|
||||
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3grw==",
|
||||
claims: nil,
|
||||
paddedDecode: true,
|
||||
signingMethod: jwt.SigningMethodES256,
|
||||
keyfunc: paddedKeyFunc,
|
||||
valid: true,
|
||||
},
|
||||
// DecodeStrict tests, DecodePaddingAllowed=false
|
||||
{
|
||||
name: "Validated non-padded token with padding disabled, non-strict decode, non-tweaked signature",
|
||||
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
|
||||
"g",
|
||||
claims: nil,
|
||||
paddedDecode: false,
|
||||
strictDecode: false,
|
||||
signingMethod: jwt.SigningMethodRS256,
|
||||
keyfunc: defaultKeyFunc,
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Validated non-padded token with padding disabled, non-strict decode, tweaked signature",
|
||||
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
|
||||
"h",
|
||||
claims: nil,
|
||||
paddedDecode: false,
|
||||
strictDecode: false,
|
||||
signingMethod: jwt.SigningMethodRS256,
|
||||
keyfunc: defaultKeyFunc,
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Validated non-padded token with padding disabled, strict decode, non-tweaked signature",
|
||||
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
|
||||
"g",
|
||||
claims: nil,
|
||||
paddedDecode: false,
|
||||
strictDecode: true,
|
||||
signingMethod: jwt.SigningMethodRS256,
|
||||
keyfunc: defaultKeyFunc,
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Error for non-padded token with padding disabled, strict decode, tweaked signature",
|
||||
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
|
||||
"h",
|
||||
claims: nil,
|
||||
paddedDecode: false,
|
||||
strictDecode: true,
|
||||
signingMethod: jwt.SigningMethodRS256,
|
||||
keyfunc: defaultKeyFunc,
|
||||
valid: false,
|
||||
},
|
||||
// DecodeStrict tests, DecodePaddingAllowed=true
|
||||
{
|
||||
name: "Validated padded token with padding enabled, non-strict decode, non-tweaked signature",
|
||||
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
|
||||
"w==",
|
||||
claims: nil,
|
||||
paddedDecode: true,
|
||||
strictDecode: false,
|
||||
signingMethod: jwt.SigningMethodES256,
|
||||
keyfunc: paddedKeyFunc,
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Validated padded token with padding enabled, non-strict decode, tweaked signature",
|
||||
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
|
||||
"x==",
|
||||
claims: nil,
|
||||
paddedDecode: true,
|
||||
strictDecode: false,
|
||||
signingMethod: jwt.SigningMethodES256,
|
||||
keyfunc: paddedKeyFunc,
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Validated padded token with padding enabled, strict decode, non-tweaked signature",
|
||||
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
|
||||
"w==",
|
||||
claims: nil,
|
||||
paddedDecode: true,
|
||||
strictDecode: true,
|
||||
signingMethod: jwt.SigningMethodES256,
|
||||
keyfunc: paddedKeyFunc,
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Error for padded token with padding enabled, strict decode, tweaked signature",
|
||||
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
|
||||
"x==",
|
||||
claims: nil,
|
||||
paddedDecode: true,
|
||||
strictDecode: true,
|
||||
signingMethod: jwt.SigningMethodES256,
|
||||
keyfunc: paddedKeyFunc,
|
||||
valid: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Extension of Parsing, this is to test out functionality specific to switching codecs with padding.
|
||||
func TestSetPadding(t *testing.T) {
|
||||
for _, data := range setPaddingTestData {
|
||||
t.Run(data.name, func(t *testing.T) {
|
||||
jwt.DecodePaddingAllowed = data.paddedDecode
|
||||
jwt.DecodeStrict = data.strictDecode
|
||||
|
||||
// If the token string is blank, use helper function to generate string
|
||||
if data.tokenString == "" {
|
||||
data.tokenString = signToken(data.claims, data.signingMethod)
|
||||
}
|
||||
|
||||
// Parse the token
|
||||
var token *jwt.Token
|
||||
var err error
|
||||
parser := new(jwt.Parser)
|
||||
parser.SkipClaimsValidation = true
|
||||
|
||||
// Figure out correct claims type
|
||||
token, err = parser.ParseWithClaims(data.tokenString, jwt.MapClaims{}, data.keyfunc)
|
||||
|
||||
if (err == nil) != data.valid || token.Valid != data.valid {
|
||||
t.Errorf("[%v] Error Parsing Token with decoding padding set to %v: %v",
|
||||
data.name,
|
||||
data.paddedDecode,
|
||||
err,
|
||||
)
|
||||
}
|
||||
})
|
||||
jwt.DecodePaddingAllowed = false
|
||||
jwt.DecodeStrict = false
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseUnverified(b *testing.B) {
|
||||
// Iterate over test data set and run tests
|
||||
for _, data := range jwtTestData {
|
||||
// If the token string is blank, use helper function to generate string
|
||||
if data.tokenString == "" {
|
||||
data.tokenString = signToken(data.claims, data.signingMethod)
|
||||
}
|
||||
|
||||
// Parse the token
|
||||
parser := data.parser
|
||||
if parser == nil {
|
||||
parser = new(jwt.Parser)
|
||||
}
|
||||
// Figure out correct claims type
|
||||
switch data.claims.(type) {
|
||||
case jwt.MapClaims:
|
||||
b.Run("map_claims", func(b *testing.B) {
|
||||
benchmarkParsing(b, parser, data.tokenString, jwt.MapClaims{})
|
||||
})
|
||||
case *jwt.StandardClaims:
|
||||
b.Run("standard_claims", func(b *testing.B) {
|
||||
benchmarkParsing(b, parser, data.tokenString, &jwt.StandardClaims{})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method for benchmarking various parsing methods
|
||||
func benchmarkParsing(b *testing.B, parser *jwt.Parser, tokenString string, claims jwt.Claims) {
|
||||
b.Helper()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Helper method for benchmarking various signing methods
|
||||
func benchmarkSigning(b *testing.B, method jwt.SigningMethod, key interface{}) {
|
||||
b.Helper()
|
||||
t := jwt.New(method)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
@ -299,5 +725,4 @@ func benchmarkSigning(b *testing.B, method jwt.SigningMethod, key interface{}) {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package request
|
|||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Errors
|
||||
|
@ -10,15 +11,15 @@ var (
|
|||
ErrNoTokenInRequest = errors.New("no token present in request")
|
||||
)
|
||||
|
||||
// Interface for extracting a token from an HTTP request.
|
||||
// Extractor is an interface for extracting a token from an HTTP request.
|
||||
// The ExtractToken method should return a token string or an error.
|
||||
// If no token is present, you must return ErrNoTokenInRequest.
|
||||
type Extractor interface {
|
||||
ExtractToken(*http.Request) (string, error)
|
||||
}
|
||||
|
||||
// Extractor for finding a token in a header. Looks at each specified
|
||||
// header in order until there's a match
|
||||
// HeaderExtractor is an extractor for finding a token in a header.
|
||||
// Looks at each specified header in order until there's a match
|
||||
type HeaderExtractor []string
|
||||
|
||||
func (e HeaderExtractor) ExtractToken(req *http.Request) (string, error) {
|
||||
|
@ -31,7 +32,7 @@ func (e HeaderExtractor) ExtractToken(req *http.Request) (string, error) {
|
|||
return "", ErrNoTokenInRequest
|
||||
}
|
||||
|
||||
// Extract token from request arguments. This includes a POSTed form or
|
||||
// ArgumentExtractor extracts a token from request arguments. This includes a POSTed form or
|
||||
// GET URL arguments. Argument names are tried in order until there's a match.
|
||||
// This extractor calls `ParseMultipartForm` on the request
|
||||
type ArgumentExtractor []string
|
||||
|
@ -50,7 +51,7 @@ func (e ArgumentExtractor) ExtractToken(req *http.Request) (string, error) {
|
|||
return "", ErrNoTokenInRequest
|
||||
}
|
||||
|
||||
// Tries Extractors in order until one returns a token string or an error occurs
|
||||
// MultiExtractor tries Extractors in order until one returns a token string or an error occurs
|
||||
type MultiExtractor []Extractor
|
||||
|
||||
func (e MultiExtractor) ExtractToken(req *http.Request) (string, error) {
|
||||
|
@ -58,14 +59,14 @@ func (e MultiExtractor) ExtractToken(req *http.Request) (string, error) {
|
|||
for _, extractor := range e {
|
||||
if tok, err := extractor.ExtractToken(req); tok != "" {
|
||||
return tok, nil
|
||||
} else if err != ErrNoTokenInRequest {
|
||||
} else if !errors.Is(err, ErrNoTokenInRequest) {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return "", ErrNoTokenInRequest
|
||||
}
|
||||
|
||||
// Wrap an Extractor in this to post-process the value before it's handed off.
|
||||
// PostExtractionFilter wraps an Extractor in this to post-process the value before it's handed off.
|
||||
// See AuthorizationHeaderExtractor for an example
|
||||
type PostExtractionFilter struct {
|
||||
Extractor
|
||||
|
@ -79,3 +80,18 @@ func (e *PostExtractionFilter) ExtractToken(req *http.Request) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// BearerExtractor extracts a token from the Authorization header.
|
||||
// The header is expected to match the format "Bearer XX", where "XX" is the
|
||||
// JWT token.
|
||||
type BearerExtractor struct{}
|
||||
|
||||
func (e BearerExtractor) ExtractToken(req *http.Request) (string, error) {
|
||||
tokenHeader := req.Header.Get("Authorization")
|
||||
// The usual convention is for "Bearer" to be title-cased. However, there's no
|
||||
// strict rule around this, and it's best to follow the robustness principle here.
|
||||
if tokenHeader == "" || !strings.HasPrefix(strings.ToLower(tokenHeader), "bearer ") {
|
||||
return "", ErrNoTokenInRequest
|
||||
}
|
||||
return tokenHeader[7:], nil
|
||||
}
|
||||
|
|
|
@ -89,3 +89,23 @@ func makeExampleRequest(method, path string, headers map[string]string, urlArgs
|
|||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func TestBearerExtractor(t *testing.T) {
|
||||
request := makeExampleRequest("POST", "https://example.com/", map[string]string{"Authorization": "Bearer ToKen"}, nil)
|
||||
token, err := BearerExtractor{}.ExtractToken(request)
|
||||
if err != nil || token != "ToKen" {
|
||||
t.Errorf("ExtractToken did not return token, returned: %v, %v", token, err)
|
||||
}
|
||||
|
||||
request = makeExampleRequest("POST", "https://example.com/", map[string]string{"Authorization": "Bearo ToKen"}, nil)
|
||||
token, err = BearerExtractor{}.ExtractToken(request)
|
||||
if err == nil || token != "" {
|
||||
t.Errorf("ExtractToken did not return error, returned: %v, %v", token, err)
|
||||
}
|
||||
|
||||
request = makeExampleRequest("POST", "https://example.com/", map[string]string{"Authorization": "BeArEr HeLO"}, nil)
|
||||
token, err = BearerExtractor{}.ExtractToken(request)
|
||||
if err != nil || token != "HeLO" {
|
||||
t.Errorf("ExtractToken did not return token, returned: %v, %v", token, err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,14 +13,14 @@ func stripBearerPrefixFromTokenString(tok string) (string, error) {
|
|||
return tok, nil
|
||||
}
|
||||
|
||||
// Extract bearer token from Authorization header
|
||||
// AuthorizationHeaderExtractor extracts a bearer token from Authorization header
|
||||
// Uses PostExtractionFilter to strip "Bearer " prefix from header
|
||||
var AuthorizationHeaderExtractor = &PostExtractionFilter{
|
||||
HeaderExtractor{"Authorization"},
|
||||
stripBearerPrefixFromTokenString,
|
||||
}
|
||||
|
||||
// Extractor for OAuth2 access tokens. Looks in 'Authorization'
|
||||
// OAuth2Extractor is an Extractor for OAuth2 access tokens. Looks in 'Authorization'
|
||||
// header then 'access_token' argument for a token.
|
||||
var OAuth2Extractor = &MultiExtractor{
|
||||
AuthorizationHeaderExtractor,
|
||||
|
|
|
@ -3,10 +3,10 @@ package request
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"git.internal/re/jwt/v4"
|
||||
)
|
||||
|
||||
// Extract and parse a JWT token from an HTTP request.
|
||||
// ParseFromRequest extracts and parses a JWT token from an HTTP request.
|
||||
// This behaves the same as Parse, but accepts a request and an extractor
|
||||
// instead of a token string. The Extractor interface allows you to define
|
||||
// the logic for extracting a token. Several useful implementations are provided.
|
||||
|
@ -39,8 +39,9 @@ func ParseFromRequest(req *http.Request, extractor Extractor, keyFunc jwt.Keyfun
|
|||
return p.parser.ParseWithClaims(tokenString, p.claims, keyFunc)
|
||||
}
|
||||
|
||||
// ParseFromRequest but with custom Claims type
|
||||
// DEPRECATED: use ParseFromRequest and the WithClaims option
|
||||
// ParseFromRequestWithClaims is an alias for ParseFromRequest but with custom Claims type.
|
||||
//
|
||||
// Deprecated: use ParseFromRequest and the WithClaims option
|
||||
func ParseFromRequestWithClaims(req *http.Request, extractor Extractor, claims jwt.Claims, keyFunc jwt.Keyfunc) (token *jwt.Token, err error) {
|
||||
return ParseFromRequest(req, extractor, keyFunc, WithClaims(claims))
|
||||
}
|
||||
|
@ -54,14 +55,14 @@ type fromRequestParser struct {
|
|||
|
||||
type ParseFromRequestOption func(*fromRequestParser)
|
||||
|
||||
// Parse with custom claims
|
||||
// WithClaims parses with custom claims
|
||||
func WithClaims(claims jwt.Claims) ParseFromRequestOption {
|
||||
return func(p *fromRequestParser) {
|
||||
p.claims = claims
|
||||
}
|
||||
}
|
||||
|
||||
// Parse using a custom parser
|
||||
// WithParser parses using a custom parser
|
||||
func WithParser(parser *jwt.Parser) ParseFromRequestOption {
|
||||
return func(p *fromRequestParser) {
|
||||
p.parser = parser
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/golang-jwt/jwt/test"
|
||||
"git.internal/re/jwt/v4"
|
||||
"git.internal/re/jwt/v4/test"
|
||||
)
|
||||
|
||||
var requestTestData = []struct {
|
||||
|
@ -65,7 +65,7 @@ func TestParseRequest(t *testing.T) {
|
|||
// Bearer token request
|
||||
for _, data := range requestTestData {
|
||||
// Make token from claims
|
||||
tokenString := test.MakeSampleToken(data.claims, privateKey)
|
||||
tokenString := test.MakeSampleToken(data.claims, jwt.SigningMethodRS256, privateKey)
|
||||
|
||||
// Make query string
|
||||
for k, vv := range data.query {
|
||||
|
|
6
rsa.go
6
rsa.go
|
@ -6,7 +6,7 @@ import (
|
|||
"crypto/rsa"
|
||||
)
|
||||
|
||||
// Implements the RSA family of signing methods signing methods
|
||||
// SigningMethodRSA implements the RSA family of signing methods.
|
||||
// Expects *rsa.PrivateKey for signing and *rsa.PublicKey for validation
|
||||
type SigningMethodRSA struct {
|
||||
Name string
|
||||
|
@ -44,7 +44,7 @@ func (m *SigningMethodRSA) Alg() string {
|
|||
return m.Name
|
||||
}
|
||||
|
||||
// Implements the Verify method from SigningMethod
|
||||
// Verify implements token verification for the SigningMethod
|
||||
// For this signing method, must be an *rsa.PublicKey structure.
|
||||
func (m *SigningMethodRSA) Verify(signingString, signature string, key interface{}) error {
|
||||
var err error
|
||||
|
@ -73,7 +73,7 @@ func (m *SigningMethodRSA) Verify(signingString, signature string, key interface
|
|||
return rsa.VerifyPKCS1v15(rsaKey, m.Hash, hasher.Sum(nil), sig)
|
||||
}
|
||||
|
||||
// Implements the Sign method from SigningMethod
|
||||
// Sign implements token signing for the SigningMethod
|
||||
// For this signing method, must be an *rsa.PrivateKey structure.
|
||||
func (m *SigningMethodRSA) Sign(signingString string, key interface{}) (string, error) {
|
||||
var rsaKey *rsa.PrivateKey
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//go:build go1.4
|
||||
// +build go1.4
|
||||
|
||||
package jwt
|
||||
|
@ -8,7 +9,7 @@ import (
|
|||
"crypto/rsa"
|
||||
)
|
||||
|
||||
// Implements the RSAPSS family of signing methods signing methods
|
||||
// SigningMethodRSAPSS implements the RSAPSS family of signing methods signing methods
|
||||
type SigningMethodRSAPSS struct {
|
||||
*SigningMethodRSA
|
||||
Options *rsa.PSSOptions
|
||||
|
@ -79,7 +80,7 @@ func init() {
|
|||
})
|
||||
}
|
||||
|
||||
// Implements the Verify method from SigningMethod
|
||||
// Verify implements token verification for the SigningMethod.
|
||||
// For this verify method, key must be an rsa.PublicKey struct
|
||||
func (m *SigningMethodRSAPSS) Verify(signingString, signature string, key interface{}) error {
|
||||
var err error
|
||||
|
@ -113,7 +114,7 @@ func (m *SigningMethodRSAPSS) Verify(signingString, signature string, key interf
|
|||
return rsa.VerifyPSS(rsaKey, m.Hash, hasher.Sum(nil), sig, opts)
|
||||
}
|
||||
|
||||
// Implements the Sign method from SigningMethod
|
||||
// Sign implements token signing for the SigningMethod.
|
||||
// For this signing method, key must be an rsa.PrivateKey struct
|
||||
func (m *SigningMethodRSAPSS) Sign(signingString string, key interface{}) (string, error) {
|
||||
var rsaKey *rsa.PrivateKey
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
//go:build go1.4
|
||||
// +build go1.4
|
||||
|
||||
package jwt_test
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/golang-jwt/jwt/test"
|
||||
"git.internal/re/jwt/v4"
|
||||
"git.internal/re/jwt/v4/test"
|
||||
)
|
||||
|
||||
var rsaPSSTestData = []struct {
|
||||
|
@ -53,7 +54,7 @@ var rsaPSSTestData = []struct {
|
|||
func TestRSAPSSVerify(t *testing.T) {
|
||||
var err error
|
||||
|
||||
key, _ := ioutil.ReadFile("test/sample_key.pub")
|
||||
key, _ := os.ReadFile("test/sample_key.pub")
|
||||
var rsaPSSKey *rsa.PublicKey
|
||||
if rsaPSSKey, err = jwt.ParseRSAPublicKeyFromPEM(key); err != nil {
|
||||
t.Errorf("Unable to parse RSA public key: %v", err)
|
||||
|
@ -76,7 +77,7 @@ func TestRSAPSSVerify(t *testing.T) {
|
|||
func TestRSAPSSSign(t *testing.T) {
|
||||
var err error
|
||||
|
||||
key, _ := ioutil.ReadFile("test/sample_key")
|
||||
key, _ := os.ReadFile("test/sample_key")
|
||||
var rsaPSSKey *rsa.PrivateKey
|
||||
if rsaPSSKey, err = jwt.ParseRSAPrivateKeyFromPEM(key); err != nil {
|
||||
t.Errorf("Unable to parse RSA private key: %v", err)
|
||||
|
@ -131,9 +132,9 @@ func TestRSAPSSSaltLengthCompatibility(t *testing.T) {
|
|||
}
|
||||
|
||||
func makeToken(method jwt.SigningMethod) string {
|
||||
token := jwt.NewWithClaims(method, jwt.StandardClaims{
|
||||
token := jwt.NewWithClaims(method, jwt.RegisteredClaims{
|
||||
Issuer: "example",
|
||||
IssuedAt: time.Now().Unix(),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
})
|
||||
privateKey := test.LoadRSAPrivateKeyFromDisk("test/sample_key")
|
||||
signed, err := token.SignedString(privateKey)
|
||||
|
|
42
rsa_test.go
42
rsa_test.go
|
@ -1,52 +1,47 @@
|
|||
package jwt_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"git.internal/re/jwt/v4"
|
||||
)
|
||||
|
||||
var rsaTestData = []struct {
|
||||
name string
|
||||
tokenString string
|
||||
alg string
|
||||
claims map[string]interface{}
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
"Basic RS256",
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
|
||||
"RS256",
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Basic RS384",
|
||||
"eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.W-jEzRfBigtCWsinvVVuldiuilzVdU5ty0MvpLaSaqK9PlAWWlDQ1VIQ_qSKzwL5IXaZkvZFJXT3yL3n7OUVu7zCNJzdwznbC8Z-b0z2lYvcklJYi2VOFRcGbJtXUqgjk2oGsiqUMUMOLP70TTefkpsgqDxbRh9CDUfpOJgW-dU7cmgaoswe3wjUAUi6B6G2YEaiuXC0XScQYSYVKIzgKXJV8Zw-7AN_DBUI4GkTpsvQ9fVVjZM9csQiEXhYekyrKu1nu_POpQonGd8yqkIyXPECNmmqH5jH4sFiF67XhD7_JpkvLziBpI-uh86evBUadmHhb9Otqw3uV3NTaXLzJw",
|
||||
"RS384",
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Basic RS512",
|
||||
"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.zBlLlmRrUxx4SJPUbV37Q1joRcI9EW13grnKduK3wtYKmDXbgDpF1cZ6B-2Jsm5RB8REmMiLpGms-EjXhgnyh2TSHE-9W2gA_jvshegLWtwRVDX40ODSkTb7OVuaWgiy9y7llvcknFBTIg-FnVPVpXMmeV_pvwQyhaz1SSwSPrDyxEmksz1hq7YONXhXPpGaNbMMeDTNP_1oj8DZaqTIL9TwV8_1wb2Odt_Fy58Ke2RVFijsOLdnyEAjt2n9Mxihu9i3PhNBkkxa2GbnXBfq3kzvZ_xxGGopLdHhJjcGWXO-NiwI9_tiu14NRv4L2xC0ItD9Yz68v2ZIZEp_DuzwRQ",
|
||||
"RS512",
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"basic invalid: foo => bar",
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.EhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
|
||||
"RS256",
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestRSAVerify(t *testing.T) {
|
||||
keyData, _ := ioutil.ReadFile("test/sample_key.pub")
|
||||
keyData, _ := os.ReadFile("test/sample_key.pub")
|
||||
key, _ := jwt.ParseRSAPublicKeyFromPEM(keyData)
|
||||
|
||||
for _, data := range rsaTestData {
|
||||
|
@ -64,7 +59,7 @@ func TestRSAVerify(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRSASign(t *testing.T) {
|
||||
keyData, _ := ioutil.ReadFile("test/sample_key")
|
||||
keyData, _ := os.ReadFile("test/sample_key")
|
||||
key, _ := jwt.ParseRSAPrivateKeyFromPEM(keyData)
|
||||
|
||||
for _, data := range rsaTestData {
|
||||
|
@ -83,7 +78,7 @@ func TestRSASign(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRSAVerifyWithPreParsedPrivateKey(t *testing.T) {
|
||||
key, _ := ioutil.ReadFile("test/sample_key.pub")
|
||||
key, _ := os.ReadFile("test/sample_key.pub")
|
||||
parsedKey, err := jwt.ParseRSAPublicKeyFromPEM(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -97,7 +92,7 @@ func TestRSAVerifyWithPreParsedPrivateKey(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRSAWithPreParsedPrivateKey(t *testing.T) {
|
||||
key, _ := ioutil.ReadFile("test/sample_key")
|
||||
key, _ := os.ReadFile("test/sample_key")
|
||||
parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -114,9 +109,9 @@ func TestRSAWithPreParsedPrivateKey(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRSAKeyParsing(t *testing.T) {
|
||||
key, _ := ioutil.ReadFile("test/sample_key")
|
||||
secureKey, _ := ioutil.ReadFile("test/privateSecure.pem")
|
||||
pubKey, _ := ioutil.ReadFile("test/sample_key.pub")
|
||||
key, _ := os.ReadFile("test/sample_key")
|
||||
secureKey, _ := os.ReadFile("test/privateSecure.pem")
|
||||
pubKey, _ := os.ReadFile("test/sample_key.pub")
|
||||
badKey := []byte("All your base are belong to key")
|
||||
|
||||
// Test parsePrivateKey
|
||||
|
@ -152,11 +147,24 @@ func TestRSAKeyParsing(t *testing.T) {
|
|||
if k, e := jwt.ParseRSAPublicKeyFromPEM(badKey); e == nil {
|
||||
t.Errorf("Parsed invalid key as valid private key: %v", k)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRSAParsing(b *testing.B) {
|
||||
key, _ := os.ReadFile("test/sample_key")
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
if _, err := jwt.ParseRSAPrivateKeyFromPEM(key); err != nil {
|
||||
b.Fatalf("Unable to parse RSA private key: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkRS256Signing(b *testing.B) {
|
||||
key, _ := ioutil.ReadFile("test/sample_key")
|
||||
key, _ := os.ReadFile("test/sample_key")
|
||||
parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
|
@ -166,7 +174,7 @@ func BenchmarkRS256Signing(b *testing.B) {
|
|||
}
|
||||
|
||||
func BenchmarkRS384Signing(b *testing.B) {
|
||||
key, _ := ioutil.ReadFile("test/sample_key")
|
||||
key, _ := os.ReadFile("test/sample_key")
|
||||
parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
|
@ -176,7 +184,7 @@ func BenchmarkRS384Signing(b *testing.B) {
|
|||
}
|
||||
|
||||
func BenchmarkRS512Signing(b *testing.B) {
|
||||
key, _ := ioutil.ReadFile("test/sample_key")
|
||||
key, _ := os.ReadFile("test/sample_key")
|
||||
parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
|
|
16
rsa_utils.go
16
rsa_utils.go
|
@ -8,12 +8,12 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ErrKeyMustBePEMEncoded = errors.New("Invalid Key: Key must be a PEM encoded PKCS1 or PKCS8 key")
|
||||
ErrNotRSAPrivateKey = errors.New("Key is not a valid RSA private key")
|
||||
ErrNotRSAPublicKey = errors.New("Key is not a valid RSA public key")
|
||||
ErrKeyMustBePEMEncoded = errors.New("invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key")
|
||||
ErrNotRSAPrivateKey = errors.New("key is not a valid RSA private key")
|
||||
ErrNotRSAPublicKey = errors.New("key is not a valid RSA public key")
|
||||
)
|
||||
|
||||
// Parse PEM encoded PKCS1 or PKCS8 private key
|
||||
// ParseRSAPrivateKeyFromPEM parses a PEM encoded PKCS1 or PKCS8 private key
|
||||
func ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error) {
|
||||
var err error
|
||||
|
||||
|
@ -39,7 +39,11 @@ func ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error) {
|
|||
return pkey, nil
|
||||
}
|
||||
|
||||
// Parse PEM encoded PKCS1 or PKCS8 private key protected with password
|
||||
// ParseRSAPrivateKeyFromPEMWithPassword parses a PEM encoded PKCS1 or PKCS8 private key protected with password
|
||||
//
|
||||
// Deprecated: This function is deprecated and should not be used anymore. It uses the deprecated x509.DecryptPEMBlock
|
||||
// function, which was deprecated since RFC 1423 is regarded insecure by design. Unfortunately, there is no alternative
|
||||
// in the Go standard library for now. See https://github.com/golang/go/issues/8860.
|
||||
func ParseRSAPrivateKeyFromPEMWithPassword(key []byte, password string) (*rsa.PrivateKey, error) {
|
||||
var err error
|
||||
|
||||
|
@ -71,7 +75,7 @@ func ParseRSAPrivateKeyFromPEMWithPassword(key []byte, password string) (*rsa.Pr
|
|||
return pkey, nil
|
||||
}
|
||||
|
||||
// Parse PEM encoded PKCS1 or PKCS8 public key
|
||||
// ParseRSAPublicKeyFromPEM parses a PEM encoded PKCS1 or PKCS8 public key
|
||||
func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) {
|
||||
var err error
|
||||
|
||||
|
|
|
@ -7,14 +7,14 @@ import (
|
|||
var signingMethods = map[string]func() SigningMethod{}
|
||||
var signingMethodLock = new(sync.RWMutex)
|
||||
|
||||
// Implement SigningMethod to add new methods for signing or verifying tokens.
|
||||
// SigningMethod can be used add new methods for signing or verifying tokens.
|
||||
type SigningMethod interface {
|
||||
Verify(signingString, signature string, key interface{}) error // Returns nil if signature is valid
|
||||
Sign(signingString string, key interface{}) (string, error) // Returns encoded signature or error
|
||||
Alg() string // returns the alg identifier for this method (example: 'HS256')
|
||||
}
|
||||
|
||||
// Register the "alg" name and a factory function for signing method.
|
||||
// RegisterSigningMethod registers the "alg" name and a factory function for signing method.
|
||||
// This is typically done during init() in the method's implementation
|
||||
func RegisterSigningMethod(alg string, f func() SigningMethod) {
|
||||
signingMethodLock.Lock()
|
||||
|
@ -23,7 +23,7 @@ func RegisterSigningMethod(alg string, f func() SigningMethod) {
|
|||
signingMethods[alg] = f
|
||||
}
|
||||
|
||||
// Get a signing method from an "alg" string
|
||||
// GetSigningMethod retrieves a signing method from an "alg" string
|
||||
func GetSigningMethod(alg string) (method SigningMethod) {
|
||||
signingMethodLock.RLock()
|
||||
defer signingMethodLock.RUnlock()
|
||||
|
@ -33,3 +33,14 @@ func GetSigningMethod(alg string) (method SigningMethod) {
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetAlgorithms returns a list of registered "alg" names
|
||||
func GetAlgorithms() (algs []string) {
|
||||
signingMethodLock.RLock()
|
||||
defer signingMethodLock.RUnlock()
|
||||
|
||||
for alg := range signingMethods {
|
||||
algs = append(algs, alg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
checks = ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1023"]
|
|
@ -0,0 +1,4 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIcaUjXhC7Mn2OonyfHF+zjblKkns
|
||||
4GLbILnHrZr+aQwddiff5urCDAZ177t81Mn39CDs3uhlNDxfRIRheGnK/Q==
|
||||
-----END PUBLIC KEY-----
|
|
@ -1,14 +1,15 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"git.internal/re/jwt/v4"
|
||||
)
|
||||
|
||||
func LoadRSAPrivateKeyFromDisk(location string) *rsa.PrivateKey {
|
||||
keyData, e := ioutil.ReadFile(location)
|
||||
keyData, e := os.ReadFile(location)
|
||||
if e != nil {
|
||||
panic(e.Error())
|
||||
}
|
||||
|
@ -20,7 +21,7 @@ func LoadRSAPrivateKeyFromDisk(location string) *rsa.PrivateKey {
|
|||
}
|
||||
|
||||
func LoadRSAPublicKeyFromDisk(location string) *rsa.PublicKey {
|
||||
keyData, e := ioutil.ReadFile(location)
|
||||
keyData, e := os.ReadFile(location)
|
||||
if e != nil {
|
||||
panic(e.Error())
|
||||
}
|
||||
|
@ -31,8 +32,9 @@ func LoadRSAPublicKeyFromDisk(location string) *rsa.PublicKey {
|
|||
return key
|
||||
}
|
||||
|
||||
func MakeSampleToken(c jwt.Claims, key interface{}) string {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, c)
|
||||
// MakeSampleToken creates and returns a encoded JWT token that has been signed with the specified cryptographic key.
|
||||
func MakeSampleToken(c jwt.Claims, method jwt.SigningMethod, key interface{}) string {
|
||||
token := jwt.NewWithClaims(method, c)
|
||||
s, e := token.SignedString(key)
|
||||
|
||||
if e != nil {
|
||||
|
@ -41,3 +43,27 @@ func MakeSampleToken(c jwt.Claims, key interface{}) string {
|
|||
|
||||
return s
|
||||
}
|
||||
|
||||
func LoadECPrivateKeyFromDisk(location string) crypto.PrivateKey {
|
||||
keyData, e := os.ReadFile(location)
|
||||
if e != nil {
|
||||
panic(e.Error())
|
||||
}
|
||||
key, e := jwt.ParseECPrivateKeyFromPEM(keyData)
|
||||
if e != nil {
|
||||
panic(e.Error())
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func LoadECPublicKeyFromDisk(location string) crypto.PublicKey {
|
||||
keyData, e := os.ReadFile(location)
|
||||
if e != nil {
|
||||
panic(e.Error())
|
||||
}
|
||||
key, e := jwt.ParseECPublicKeyFromPEM(keyData)
|
||||
if e != nil {
|
||||
panic(e.Error())
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
|
85
token.go
85
token.go
|
@ -7,18 +7,31 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// DecodePaddingAllowed will switch the codec used for decoding JWTs respectively. Note that the JWS RFC7515
|
||||
// states that the tokens will utilize a Base64url encoding with no padding. Unfortunately, some implementations
|
||||
// of JWT are producing non-standard tokens, and thus require support for decoding. Note that this is a global
|
||||
// variable, and updating it will change the behavior on a package level, and is also NOT go-routine safe.
|
||||
// To use the non-recommended decoding, set this boolean to `true` prior to using this package.
|
||||
var DecodePaddingAllowed bool
|
||||
|
||||
// DecodeStrict will switch the codec used for decoding JWTs into strict mode.
|
||||
// In this mode, the decoder requires that trailing padding bits are zero, as described in RFC 4648 section 3.5.
|
||||
// Note that this is a global variable, and updating it will change the behavior on a package level, and is also NOT go-routine safe.
|
||||
// To use strict decoding, set this boolean to `true` prior to using this package.
|
||||
var DecodeStrict bool
|
||||
|
||||
// TimeFunc provides the current time when parsing token to validate "exp" claim (expiration time).
|
||||
// You can override it to use another time value. This is useful for testing or if your
|
||||
// server uses a different time zone than your tokens.
|
||||
var TimeFunc = time.Now
|
||||
|
||||
// Parse methods use this callback function to supply
|
||||
// Keyfunc will be used by the Parse methods as a callback function to supply
|
||||
// the key for verification. The function receives the parsed,
|
||||
// but unverified Token. This allows you to use properties in the
|
||||
// Header of the token (such as `kid`) to identify which key to use.
|
||||
type Keyfunc func(*Token) (interface{}, error)
|
||||
|
||||
// A JWT Token. Different fields will be used depending on whether you're
|
||||
// Token represents a JWT Token. Different fields will be used depending on whether you're
|
||||
// creating or parsing/verifying a token.
|
||||
type Token struct {
|
||||
Raw string // The raw token. Populated when you Parse a token
|
||||
|
@ -29,11 +42,12 @@ type Token struct {
|
|||
Valid bool // Is the token valid? Populated when you Parse/Verify a token
|
||||
}
|
||||
|
||||
// Create a new Token. Takes a signing method
|
||||
// New creates a new Token with the specified signing method and an empty map of claims.
|
||||
func New(method SigningMethod) *Token {
|
||||
return NewWithClaims(method, MapClaims{})
|
||||
}
|
||||
|
||||
// NewWithClaims creates a new Token with the specified signing method and claims.
|
||||
func NewWithClaims(method SigningMethod, claims Claims) *Token {
|
||||
return &Token{
|
||||
Header: map[string]interface{}{
|
||||
|
@ -45,7 +59,8 @@ func NewWithClaims(method SigningMethod, claims Claims) *Token {
|
|||
}
|
||||
}
|
||||
|
||||
// Get the complete, signed token
|
||||
// SignedString creates and returns a complete, signed JWT.
|
||||
// The token is signed using the SigningMethod specified in the token.
|
||||
func (t *Token) SignedString(key interface{}) (string, error) {
|
||||
var sig, sstr string
|
||||
var err error
|
||||
|
@ -58,47 +73,71 @@ func (t *Token) SignedString(key interface{}) (string, error) {
|
|||
return strings.Join([]string{sstr, sig}, "."), nil
|
||||
}
|
||||
|
||||
// Generate the signing string. This is the
|
||||
// SigningString generates the signing string. This is the
|
||||
// most expensive part of the whole deal. Unless you
|
||||
// need this for something special, just go straight for
|
||||
// the SignedString.
|
||||
func (t *Token) SigningString() (string, error) {
|
||||
var err error
|
||||
parts := make([]string, 2)
|
||||
for i := range parts {
|
||||
var jsonValue []byte
|
||||
if i == 0 {
|
||||
|
||||
if jsonValue, err = json.Marshal(t.Header); err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
header := EncodeSegment(jsonValue)
|
||||
|
||||
if jsonValue, err = json.Marshal(t.Claims); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
claim := EncodeSegment(jsonValue)
|
||||
|
||||
parts[i] = EncodeSegment(jsonValue)
|
||||
}
|
||||
return strings.Join(parts, "."), nil
|
||||
return strings.Join([]string{header, claim}, "."), nil
|
||||
}
|
||||
|
||||
// Parse, validate, and return a token.
|
||||
// keyFunc will receive the parsed token and should return the key for validating.
|
||||
// If everything is kosher, err will be nil
|
||||
func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
|
||||
return new(Parser).Parse(tokenString, keyFunc)
|
||||
// Parse parses, validates, verifies the signature and returns the parsed token.
|
||||
// keyFunc will receive the parsed token and should return the cryptographic key
|
||||
// for verifying the signature.
|
||||
// The caller is strongly encouraged to set the WithValidMethods option to
|
||||
// validate the 'alg' claim in the token matches the expected algorithm.
|
||||
// For more details about the importance of validating the 'alg' claim,
|
||||
// see https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
|
||||
func Parse(tokenString string, keyFunc Keyfunc, options ...ParserOption) (*Token, error) {
|
||||
return NewParser(options...).Parse(tokenString, keyFunc)
|
||||
}
|
||||
|
||||
func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
|
||||
return new(Parser).ParseWithClaims(tokenString, claims, keyFunc)
|
||||
// ParseWithClaims is a shortcut for NewParser().ParseWithClaims().
|
||||
//
|
||||
// Note: If you provide a custom claim implementation that embeds one of the standard claims (such as RegisteredClaims),
|
||||
// make sure that a) you either embed a non-pointer version of the claims or b) if you are using a pointer, allocate the
|
||||
// proper memory for it before passing in the overall claims, otherwise you might run into a panic.
|
||||
func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc, options ...ParserOption) (*Token, error) {
|
||||
return NewParser(options...).ParseWithClaims(tokenString, claims, keyFunc)
|
||||
}
|
||||
|
||||
// Encode JWT specific base64url encoding with padding stripped
|
||||
// EncodeSegment encodes a JWT specific base64url encoding with padding stripped
|
||||
//
|
||||
// Deprecated: In a future release, we will demote this function to a non-exported function, since it
|
||||
// should only be used internally
|
||||
func EncodeSegment(seg []byte) string {
|
||||
return base64.RawURLEncoding.EncodeToString(seg)
|
||||
}
|
||||
|
||||
// Decode JWT specific base64url encoding with padding stripped
|
||||
// DecodeSegment decodes a JWT specific base64url encoding with padding stripped
|
||||
//
|
||||
// Deprecated: In a future release, we will demote this function to a non-exported function, since it
|
||||
// should only be used internally
|
||||
func DecodeSegment(seg string) ([]byte, error) {
|
||||
return base64.RawURLEncoding.DecodeString(seg)
|
||||
encoding := base64.RawURLEncoding
|
||||
|
||||
if DecodePaddingAllowed {
|
||||
if l := len(seg) % 4; l > 0 {
|
||||
seg += strings.Repeat("=", 4-l)
|
||||
}
|
||||
encoding = base64.URLEncoding
|
||||
}
|
||||
|
||||
if DecodeStrict {
|
||||
encoding = encoding.Strict()
|
||||
}
|
||||
return encoding.DecodeString(seg)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
package jwt_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.internal/re/jwt/v4"
|
||||
)
|
||||
|
||||
func TestToken_SigningString(t1 *testing.T) {
|
||||
type fields struct {
|
||||
Raw string
|
||||
Method jwt.SigningMethod
|
||||
Header map[string]interface{}
|
||||
Claims jwt.Claims
|
||||
Signature string
|
||||
Valid bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "",
|
||||
fields: fields{
|
||||
Raw: "",
|
||||
Method: jwt.SigningMethodHS256,
|
||||
Header: map[string]interface{}{
|
||||
"typ": "JWT",
|
||||
"alg": jwt.SigningMethodHS256.Alg(),
|
||||
},
|
||||
Claims: jwt.StandardClaims{},
|
||||
Signature: "",
|
||||
Valid: false,
|
||||
},
|
||||
want: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t1.Run(tt.name, func(t1 *testing.T) {
|
||||
t := &jwt.Token{
|
||||
Raw: tt.fields.Raw,
|
||||
Method: tt.fields.Method,
|
||||
Header: tt.fields.Header,
|
||||
Claims: tt.fields.Claims,
|
||||
Signature: tt.fields.Signature,
|
||||
Valid: tt.fields.Valid,
|
||||
}
|
||||
got, err := t.SigningString()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t1.Errorf("SigningString() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t1.Errorf("SigningString() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkToken_SigningString(b *testing.B) {
|
||||
t := &jwt.Token{
|
||||
Method: jwt.SigningMethodHS256,
|
||||
Header: map[string]interface{}{
|
||||
"typ": "JWT",
|
||||
"alg": jwt.SigningMethodHS256.Alg(),
|
||||
},
|
||||
Claims: jwt.StandardClaims{},
|
||||
}
|
||||
b.Run("BenchmarkToken_SigningString", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
t.SigningString()
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
package jwt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TimePrecision sets the precision of times and dates within this library.
|
||||
// This has an influence on the precision of times when comparing expiry or
|
||||
// other related time fields. Furthermore, it is also the precision of times
|
||||
// when serializing.
|
||||
//
|
||||
// For backwards compatibility the default precision is set to seconds, so that
|
||||
// no fractional timestamps are generated.
|
||||
var TimePrecision = time.Second
|
||||
|
||||
// MarshalSingleStringAsArray modifies the behaviour of the ClaimStrings type, especially
|
||||
// its MarshalJSON function.
|
||||
//
|
||||
// If it is set to true (the default), it will always serialize the type as an
|
||||
// array of strings, even if it just contains one element, defaulting to the behaviour
|
||||
// of the underlying []string. If it is set to false, it will serialize to a single
|
||||
// string, if it contains one element. Otherwise, it will serialize to an array of strings.
|
||||
var MarshalSingleStringAsArray = true
|
||||
|
||||
// NumericDate represents a JSON numeric date value, as referenced at
|
||||
// https://datatracker.ietf.org/doc/html/rfc7519#section-2.
|
||||
type NumericDate struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// NewNumericDate constructs a new *NumericDate from a standard library time.Time struct.
|
||||
// It will truncate the timestamp according to the precision specified in TimePrecision.
|
||||
func NewNumericDate(t time.Time) *NumericDate {
|
||||
return &NumericDate{t.Truncate(TimePrecision)}
|
||||
}
|
||||
|
||||
// newNumericDateFromSeconds creates a new *NumericDate out of a float64 representing a
|
||||
// UNIX epoch with the float fraction representing non-integer seconds.
|
||||
func newNumericDateFromSeconds(f float64) *NumericDate {
|
||||
round, frac := math.Modf(f)
|
||||
return NewNumericDate(time.Unix(int64(round), int64(frac*1e9)))
|
||||
}
|
||||
|
||||
// MarshalJSON is an implementation of the json.RawMessage interface and serializes the UNIX epoch
|
||||
// represented in NumericDate to a byte array, using the precision specified in TimePrecision.
|
||||
func (date NumericDate) MarshalJSON() (b []byte, err error) {
|
||||
var prec int
|
||||
if TimePrecision < time.Second {
|
||||
prec = int(math.Log10(float64(time.Second) / float64(TimePrecision)))
|
||||
}
|
||||
truncatedDate := date.Truncate(TimePrecision)
|
||||
|
||||
// For very large timestamps, UnixNano would overflow an int64, but this
|
||||
// function requires nanosecond level precision, so we have to use the
|
||||
// following technique to get round the issue:
|
||||
// 1. Take the normal unix timestamp to form the whole number part of the
|
||||
// output,
|
||||
// 2. Take the result of the Nanosecond function, which retuns the offset
|
||||
// within the second of the particular unix time instance, to form the
|
||||
// decimal part of the output
|
||||
// 3. Concatenate them to produce the final result
|
||||
seconds := strconv.FormatInt(truncatedDate.Unix(), 10)
|
||||
nanosecondsOffset := strconv.FormatFloat(float64(truncatedDate.Nanosecond())/float64(time.Second), 'f', prec, 64)
|
||||
|
||||
output := append([]byte(seconds), []byte(nanosecondsOffset)[1:]...)
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON is an implementation of the json.RawMessage interface and deserializses a
|
||||
// NumericDate from a JSON representation, i.e. a json.Number. This number represents an UNIX epoch
|
||||
// with either integer or non-integer seconds.
|
||||
func (date *NumericDate) UnmarshalJSON(b []byte) (err error) {
|
||||
var (
|
||||
number json.Number
|
||||
f float64
|
||||
)
|
||||
|
||||
if err = json.Unmarshal(b, &number); err != nil {
|
||||
return fmt.Errorf("could not parse NumericData: %w", err)
|
||||
}
|
||||
|
||||
if f, err = number.Float64(); err != nil {
|
||||
return fmt.Errorf("could not convert json number value to float: %w", err)
|
||||
}
|
||||
|
||||
n := newNumericDateFromSeconds(f)
|
||||
*date = *n
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClaimStrings is basically just a slice of strings, but it can be either serialized from a string array or just a string.
|
||||
// This type is necessary, since the "aud" claim can either be a single string or an array.
|
||||
type ClaimStrings []string
|
||||
|
||||
func (s *ClaimStrings) UnmarshalJSON(data []byte) (err error) {
|
||||
var value interface{}
|
||||
|
||||
if err = json.Unmarshal(data, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var aud []string
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
aud = append(aud, v)
|
||||
case []string:
|
||||
aud = ClaimStrings(v)
|
||||
case []interface{}:
|
||||
for _, vv := range v {
|
||||
vs, ok := vv.(string)
|
||||
if !ok {
|
||||
return &json.UnsupportedTypeError{Type: reflect.TypeOf(vv)}
|
||||
}
|
||||
aud = append(aud, vs)
|
||||
}
|
||||
case nil:
|
||||
return nil
|
||||
default:
|
||||
return &json.UnsupportedTypeError{Type: reflect.TypeOf(v)}
|
||||
}
|
||||
|
||||
*s = aud
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s ClaimStrings) MarshalJSON() (b []byte, err error) {
|
||||
// This handles a special case in the JWT RFC. If the string array, e.g. used by the "aud" field,
|
||||
// only contains one element, it MAY be serialized as a single string. This may or may not be
|
||||
// desired based on the ecosystem of other JWT library used, so we make it configurable by the
|
||||
// variable MarshalSingleStringAsArray.
|
||||
if len(s) == 1 && !MarshalSingleStringAsArray {
|
||||
return json.Marshal(s[0])
|
||||
}
|
||||
|
||||
return json.Marshal([]string(s))
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
package jwt_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.internal/re/jwt/v4"
|
||||
)
|
||||
|
||||
func TestNumericDate(t *testing.T) {
|
||||
var s struct {
|
||||
Iat jwt.NumericDate `json:"iat"`
|
||||
Exp jwt.NumericDate `json:"exp"`
|
||||
}
|
||||
|
||||
oldPrecision := jwt.TimePrecision
|
||||
|
||||
jwt.TimePrecision = time.Microsecond
|
||||
|
||||
raw := `{"iat":1516239022.000000,"exp":1516239022.123450}`
|
||||
|
||||
if err := json.Unmarshal([]byte(raw), &s); err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(s)
|
||||
|
||||
if raw != string(b) {
|
||||
t.Errorf("Serialized format of numeric date mismatch. Expecting: %s Got: %s", string(raw), string(b))
|
||||
}
|
||||
|
||||
jwt.TimePrecision = oldPrecision
|
||||
}
|
||||
|
||||
func TestSingleArrayMarshal(t *testing.T) {
|
||||
jwt.MarshalSingleStringAsArray = false
|
||||
|
||||
s := jwt.ClaimStrings{"test"}
|
||||
expected := `"test"`
|
||||
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if expected != string(b) {
|
||||
t.Errorf("Serialized format of string array mismatch. Expecting: %s Got: %s", string(expected), string(b))
|
||||
}
|
||||
|
||||
jwt.MarshalSingleStringAsArray = true
|
||||
|
||||
expected = `["test"]`
|
||||
|
||||
b, err = json.Marshal(s)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if expected != string(b) {
|
||||
t.Errorf("Serialized format of string array mismatch. Expecting: %s Got: %s", string(expected), string(b))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumericDate_MarshalJSON(t *testing.T) {
|
||||
// Do not run this test in parallel because it's changing
|
||||
// global state.
|
||||
oldPrecision := jwt.TimePrecision
|
||||
t.Cleanup(func() {
|
||||
jwt.TimePrecision = oldPrecision
|
||||
})
|
||||
|
||||
tt := []struct {
|
||||
in time.Time
|
||||
want string
|
||||
precision time.Duration
|
||||
}{
|
||||
{time.Unix(5243700879, 0), "5243700879", time.Second},
|
||||
{time.Unix(5243700879, 0), "5243700879.000", time.Millisecond},
|
||||
{time.Unix(5243700879, 0), "5243700879.000000", time.Microsecond},
|
||||
{time.Unix(5243700879, 0), "5243700879.000000000", time.Nanosecond},
|
||||
//
|
||||
{time.Unix(4239425898, 0), "4239425898", time.Second},
|
||||
{time.Unix(4239425898, 0), "4239425898.000", time.Millisecond},
|
||||
{time.Unix(4239425898, 0), "4239425898.000000", time.Microsecond},
|
||||
{time.Unix(4239425898, 0), "4239425898.000000000", time.Nanosecond},
|
||||
//
|
||||
{time.Unix(253402271999, 0), "253402271999", time.Second},
|
||||
{time.Unix(253402271999, 0), "253402271999.000", time.Millisecond},
|
||||
{time.Unix(253402271999, 0), "253402271999.000000", time.Microsecond},
|
||||
{time.Unix(253402271999, 0), "253402271999.000000000", time.Nanosecond},
|
||||
//
|
||||
{time.Unix(0, 1644285000210402000), "1644285000", time.Second},
|
||||
{time.Unix(0, 1644285000210402000), "1644285000.210", time.Millisecond},
|
||||
{time.Unix(0, 1644285000210402000), "1644285000.210402", time.Microsecond},
|
||||
{time.Unix(0, 1644285000210402000), "1644285000.210402000", time.Nanosecond},
|
||||
//
|
||||
{time.Unix(0, 1644285315063096000), "1644285315", time.Second},
|
||||
{time.Unix(0, 1644285315063096000), "1644285315.063", time.Millisecond},
|
||||
{time.Unix(0, 1644285315063096000), "1644285315.063096", time.Microsecond},
|
||||
{time.Unix(0, 1644285315063096000), "1644285315.063096000", time.Nanosecond},
|
||||
// Maximum time that a go time.Time can represent
|
||||
{time.Unix(math.MaxInt64, 999999999), "9223372036854775807", time.Second},
|
||||
{time.Unix(math.MaxInt64, 999999999), "9223372036854775807.999", time.Millisecond},
|
||||
{time.Unix(math.MaxInt64, 999999999), "9223372036854775807.999999", time.Microsecond},
|
||||
{time.Unix(math.MaxInt64, 999999999), "9223372036854775807.999999999", time.Nanosecond},
|
||||
// Strange precisions
|
||||
{time.Unix(math.MaxInt64, 999999999), "9223372036854775807", time.Second},
|
||||
{time.Unix(math.MaxInt64, 999999999), "9223372036854775756", time.Minute},
|
||||
{time.Unix(math.MaxInt64, 999999999), "9223372036854774016", time.Hour},
|
||||
{time.Unix(math.MaxInt64, 999999999), "9223372036854745216", 24 * time.Hour},
|
||||
}
|
||||
|
||||
for i, tc := range tt {
|
||||
jwt.TimePrecision = tc.precision
|
||||
by, err := jwt.NewNumericDate(tc.in).MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := string(by); got != tc.want {
|
||||
t.Errorf("[%d]: failed encoding: got %q want %q", i, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue