Compare commits

...

56 Commits
v3 ... main

Author SHA1 Message Date
re 29918af7f7 fix repos 2022-12-12 17:07:02 +03:00
Alexander Yastrebov 9358574a7a
Allow strict base64 decoding (#259)
By default base64 decoder works in non-strict mode which
allows tweaking signatures having padding without failing validation.

This creates a potential problem if application treats token value as an identifier.

For example ES256 signature has length of 64 bytes and two padding symbols (stripped by default).
Therefore its base64-encoded value can only end with A, Q, g and w.
In non-strict mode last symbol could be tweaked resulting in 16 distinct
token values having the same signature and passing validation.

This change adds backward-compatible global config variable DecodeStrict
(similar to existing DecodePaddingAllowed) that enables strict base64 decoder mode.

See also https://github.com/golang/go/issues/15656.

Signed-off-by: Alexander Yastrebov <yastrebov.alex@gmail.com>
2022-12-09 18:04:03 +01:00
Christian Banse 2f0984a28b
Using `tparse` for nicer CI test display (#251) 2022-11-29 10:00:41 -05:00
Christian Banse 2101c1f4bc
No pointer embedding in the example (#255)
Fixes #223
2022-11-08 15:43:45 +01:00
Krouton 35053d4e20
Removed unneeded if statement (#241) 2022-10-15 14:38:07 +02:00
Jacob Kopczynski 0c4e387985
Add doc comment to ParseWithClaims (#232) 2022-09-26 10:01:52 -04:00
Christian Banse bfea432b1a
Include https://github.com/golang-jwt/jwe in README (#229) 2022-08-20 17:04:58 +02:00
Michael Fridman d81acbf7f3
Bump matrix to support latest go version (go1.19) (#231)
* Bump matrix to support latest go version (go1.19)

* Fix comment
2022-08-20 16:53:04 +02:00
Hugo fdaf0eb0e0
Implement a BearerExtractor (#226)
* Implement a BearerExtractor

This is a rather common extractor; it extracts the JWT from the HTTP
Authorization header, expecting it to include the "Bearer " prefix.

This patterns is rather common and this snippet is repeated in enough
applications that it's probably best to just include it upstream and
allow reusing it.

* Ignore case-sensitivity for "Bearer"
2022-08-19 13:59:36 +02:00
KroKite f2878bb94b
fix: link update for README.md for v4 (#217)
Co-authored-by: Christian Banse <oxisto@aybaze.com>
2022-08-15 12:45:52 +02:00
George Kechagias 9294af54b5
chore: remove unused claims in RSA table driven test (#212) 2022-06-04 08:03:41 -04:00
Qian Qiao 2da0bf7566
Fixed integer overflow in NumericDate.MarshalJSON (#200) 2022-06-03 22:13:34 -04:00
Christian Banse 8fb42696ff
Update SECURITY.md (#207) 2022-05-28 21:53:11 +02:00
Michael Fridman cf43decf7c
Create SECURITY.md (#171) 2022-05-28 12:40:34 -04:00
Michael Fridman 4426925f0c
CI check for Go code formatting (#206)
Signed-off-by: jay-dee7 <jasdeepsingh.uppal@gmail.com>
Co-authored-by: jay-dee7 <jasdeepsingh.uppal@gmail.com>
2022-05-28 16:03:15 +02:00
Håvard Anda Estensen f6c6299f67
chore: replace ioutil with io and os (#198) 2022-05-27 19:11:16 -04:00
Luigi Morel 89a6400b7f
add installation guidelines to the README file (#204) 2022-05-27 19:07:25 -04:00
Vladislav Polyakov 6e2ab4291f
docs: update link to pkg.go.dev page (#195) 2022-04-19 17:45:50 +02:00
Christian Banse 83478b3c8f
Added MicahParks/keyfunc to extensions (#194) 2022-04-18 22:01:59 +02:00
Michael Fridman 0972257eba
Revert "feat: port clockskew support (#139)" (#184)
This reverts commit d489c99d3e.
2022-03-26 10:13:03 -04:00
Michael Fridman 1096e506e6
Add go1.18 to ci pipeline (#173) 2022-03-18 07:15:45 -04:00
ksegun d489c99d3e
feat: port clockskew support (#139)
Co-authored-by: Kolawole Segun <Kolawole.Segun@kyndryl.com>
Co-authored-by: Christian Banse <oxisto@aybaze.com>
2022-03-08 08:43:46 +01:00
ydylla 6de17d3b3e
fix: expired token error message (#165) 2022-02-15 08:31:33 -05:00
Michael Fridman 279dd19720
Set json encoding precision (#162) 2022-02-09 21:54:31 -05:00
Giau. Tran Minh 863d23d08a
fix: fixed typo detect by cSpell (#164) 2022-02-09 13:14:42 -03:00
Michael Fridman 2387103809
Add JWT logo image attribution (#161) 2022-02-08 22:35:49 -05:00
Máté Lang d0c0939ff8
updated README.md to contain more extensions (#155)
* updated README.md to contain more extensions

* Update README.md

Co-authored-by: Luis Gabriel Gomez <lggomez@users.noreply.github.com>

Co-authored-by: Luis Gabriel Gomez <lggomez@users.noreply.github.com>
2022-02-03 08:49:22 -03:00
hyeonjae e01ed05a31
remove unnecessary for loop in token signing string for readability (#34)
* remove unnecessary for loop in token signing string for readability

 - add testcase
 - add benchmark
 - improve performance slightly

* Fix benchtests on token_test.go

* Update token_test.go to v4

Co-authored-by: hyeonjae <hyeonjae@ip-192-168-1-3.ap-northeast-2.compute.internal>
Co-authored-by: Luis Gabriel Gomez <lggomez@users.noreply.github.com>
2022-02-03 08:47:58 -03:00
Christian Banse 78a18c0808
Implementing `Is(err) bool` to support Go 1.13 style error checking (#136) 2022-01-19 22:55:19 +01:00
Stefan Tudose 0fb40d3824
use errors.Is for extractor errors (#141) 2021-12-15 12:50:05 +01:00
tfonfara c435f38291
#129: Added VerifyIssuer method to RegisteredClaims (#130) 2021-11-24 14:27:41 +01:00
Alexander Yastrebov a725c1f60c
cmd: list supported algorithms (-alg flag) (#123) 2021-11-16 09:00:45 -05:00
Kevin de Berk 823c014036
Unwrap for ValidationError (#125) 2021-11-15 09:25:32 -05:00
Alexander Yastrebov 1275a5b909
Allow `none` algorithm in jwt command (#121) 2021-11-10 07:33:04 +01:00
ajermaky f4865cddea
Revert Encoding/Decoding changes for better compatibility (#117) 2021-11-06 07:21:20 -04:00
Alexander Yastrebov 9c3665f0fc
Fixes jwt command to support EdDSA algorithm (#118)
Fixes
```
$ echo '{"foo":"bar"}' | jwt -key test/ed25519-private.pem -alg EdDSA -sign -
Error: error signing token: key is of invalid type
```

Signed-off-by: Alexander Yastrebov <yastrebov.alex@gmail.com>
2021-11-03 09:14:30 -04:00
PiotrKozimor a2aa655627
Fix int64 overflow in newNumericDateFromSeconds (#112) 2021-10-26 21:14:01 -04:00
Sebastien Rosset c0ffb890f3
Improve code comments, including security consideration (#107)
* improve code comments, including security consideration

* Add link to URL with details about security vulnerabilities.

* Update token.go

Co-authored-by: Christian Banse <oxisto@aybaze.com>

* Update token.go

Co-authored-by: Christian Banse <oxisto@aybaze.com>

* update code comments

Co-authored-by: Christian Banse <oxisto@aybaze.com>
2021-10-15 09:48:31 -03:00
Christian Banse 65357b9e5b
Introducing functional-style options for the Parser type (#108) 2021-10-13 19:36:33 +02:00
Ichinose Shogo cac353cdc2
fix the comment of VerifyExpiresAt (#109) 2021-10-09 18:17:39 -03:00
Sebastien Rosset fd8cd69d8e
Adjusted `parser_test.go` to include RSA and ECDSA tokens (#106) 2021-09-24 21:32:29 +02:00
Hinagiku Soranoba 02bc1ac506
When exp indicates the present, make it invalid. (#86)
* When exp indicates the present, make it invalid.

* Update map_claims_test.go

Co-authored-by: Christian Banse <oxisto@aybaze.com>
2021-09-10 17:44:55 -04:00
Hyun d2c5d5ab01
Add EdDSA to "Signing methods and Key types" in README.md (#103) 2021-09-10 20:30:13 +02:00
Yoan Blanc 205b3dc4bb
fix link (#102) 2021-09-10 08:27:13 -04:00
Michael Fridman 93130d3c71
Create codeql-analysis.yml (#101) 2021-09-09 10:42:26 -04:00
yoogo 3f50a786ff
Harmonising capitalisation of "token" in error strings (#97) 2021-08-29 20:45:24 +02:00
Mark Karpelès 2bd8ee77fc
Accept `crypto.Signer` that contains a `ed25519.PublicKey` in ed25519 (#95)
* accept generic crypto.Signer in ed25519 in order to allow usage of other ed25519 providers than crypto/ed25519

* add check to ensure the key is indeed of type ed25519

* adding comment clarifying crypto.Hash(0)

* Update ed25519.go

Co-authored-by: Christian Banse <oxisto@aybaze.com>
2021-08-23 22:56:11 -03:00
Christian Banse 80625fb516
Backwards-compatible implementation of RFC7519's registered claim's structure (#15)
This PR aims at implementing compliance to RFC7519, as documented in #11 without breaking the public API. It creates a new struct `RegisteredClaims` and deprecates (but not removes) the `StandardClaims`. It introduces a new type `NumericDate`, which represents a JSON numeric date value as specified in the RFC. This allows us to handle float as well as int-based time fields in `aud`, `exp` and `nbf`. Additionally, it introduces the type `StringArray`, which is basically a wrapper around `[]string` to deal with the oddities of the JWT `aud` field.
2021-08-22 19:23:13 +02:00
Luis Gabriel Gomez c9ab96ba53
jwt: Fix Verify methods documentation (#83) 2021-08-22 10:18:33 +02:00
Alexander F. Rødseth eac9e9edf2
Format code with "go fmt" (#53) 2021-08-20 20:43:08 -03:00
Michael Fridman a06361ba65
ci: add support for go1.17 (#89) 2021-08-17 10:05:04 +02:00
Zach Wasserman bac80eaac8
Link to migration guide in README.md (#87) 2021-08-11 16:19:58 -03:00
Francois Lebel 85f0a979dd
Fix typo in note (#82) 2021-08-03 17:59:46 -03:00
Luis Gabriel Gomez 3258b3fca0
jwt: Add parser benchmarks (#70) 2021-08-03 17:57:36 -03:00
Christian Banse bd2db2d4a2
Changing pkg.go.dev URL to github.com/golang-jwt/jwt/v4 (#77)
* Changing pkg.go.dev URL to https://pkg.go.dev/github.com/golang-jwt/jwt/v4

Otherwise, people will end up at the v3 release and might miss on clicking the small "there is a v4 hint" on pkg.go.dev
2021-08-03 19:41:00 +02:00
Michael Fridman 2ebb50f957
Adds go module support /v4 (#41)
Additionally, added `staticcheck` for basic static code analysis (#44)

Co-authored-by: Christian Banse <oxisto@aybaze.com>
2021-08-03 15:51:01 +02:00
49 changed files with 1878 additions and 506 deletions

View File

@ -8,26 +8,43 @@ on:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
jobs: 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: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
go: [1.15, 1.16] go: [1.17, 1.18, 1.19]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with:
path: src/github.com/golang-jwt/jwt
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: "${{ matrix.go }}" 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 - name: Build
run: | run: |
go install github.com/mfridman/tparse@latest
go vet ./... go vet ./...
go test -v ./... go test -v -race -count=1 -json -coverpkg=$(go list ./...) ./... | tparse -follow -notests
go build ./... go build ./...
env:
GO111MODULE: auto
GOPATH: ${{ github.workspace }}

71
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -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

View File

@ -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 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) ## 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. The original migration guide for older releases can be found at https://github.com/dgrijalva/jwt-go/blob/master/MIGRATION_GUIDE.md.

View File

@ -1,13 +1,15 @@
# jwt-go # 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) [![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). 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. **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. 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 ## 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 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/github.com/golang-jwt/jwt#example-New-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/github.com/golang-jwt/jwt#pkg-examples) * [Directory of Examples](https://pkg.go.dev/git.internal/re/jwt/v4#pkg-examples)
## Extensions ## 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 ## 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. * 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). 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:*** **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 ## 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 author of the token was in the possession of the signing secret
* The data has not been modified since it was signed * 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 ### 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: 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 [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/github.com/golang-jwt/jwt#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for 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/github.com/golang-jwt/jwt#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.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 ### JWT and OAuth
@ -108,6 +131,8 @@ This library uses descriptive error messages whenever possible. If you are not g
## More ## 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. 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).

19
SECURITY.md Normal file
View File

@ -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!

View File

@ -1,5 +1,9 @@
## `jwt-go` Version History ## `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 #### 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)). * 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)).

225
claims.go
View File

@ -6,48 +6,67 @@ import (
"time" "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 // if the token is invalid for any supported reason
type Claims interface { type Claims interface {
Valid() error Valid() error
} }
// Structured version of Claims Section, as referenced at // RegisteredClaims are a structured version of the JWT Claims Set,
// https://tools.ietf.org/html/rfc7519#section-4.1 // restricted to Registered Claim Names, as referenced at
// See examples for how to use this with your own claim types // https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
type StandardClaims struct { //
Audience string `json:"aud,omitempty"` // This type can be used on its own, but then additional private and
ExpiresAt int64 `json:"exp,omitempty"` // public claims embedded in the JWT will not be parsed. The typical usecase
Id string `json:"jti,omitempty"` // therefore is to embedded this in a user-defined claim type.
IssuedAt int64 `json:"iat,omitempty"` //
Issuer string `json:"iss,omitempty"` // See examples for how to use this with your own claim types.
NotBefore int64 `json:"nbf,omitempty"` type RegisteredClaims struct {
Subject string `json:"sub,omitempty"` // the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
Issuer string `json:"iss,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. // There is no accounting for clock skew.
// As well, if any of the above claims are not in the token, it will still // As well, if any of the above claims are not in the token, it will still
// be considered a valid claim. // be considered a valid claim.
func (c StandardClaims) Valid() error { func (c RegisteredClaims) Valid() error {
vErr := new(ValidationError) vErr := new(ValidationError)
now := TimeFunc().Unix() now := TimeFunc()
// The claims below are optional, by default, so if they are set to the // 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. // default value in Go, let's not fail the verification for them.
if !c.VerifyExpiresAt(now, false) { if !c.VerifyExpiresAt(now, false) {
delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0)) delta := now.Sub(c.ExpiresAt.Time)
vErr.Inner = fmt.Errorf("token is expired by %v", delta) vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta)
vErr.Errors |= ValidationErrorExpired vErr.Errors |= ValidationErrorExpired
} }
if !c.VerifyIssuedAt(now, false) { if !c.VerifyIssuedAt(now, false) {
vErr.Inner = fmt.Errorf("Token used before issued") vErr.Inner = ErrTokenUsedBeforeIssued
vErr.Errors |= ValidationErrorIssuedAt vErr.Errors |= ValidationErrorIssuedAt
} }
if !c.VerifyNotBefore(now, false) { if !c.VerifyNotBefore(now, false) {
vErr.Inner = fmt.Errorf("token is not valid yet") vErr.Inner = ErrTokenNotValidYet
vErr.Errors |= ValidationErrorNotValidYet vErr.Errors |= ValidationErrorNotValidYet
} }
@ -58,36 +77,144 @@ func (c StandardClaims) Valid() error {
return vErr 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 // 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 { func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool {
return verifyAud([]string{c.Audience}, cmp, req) return verifyAud([]string{c.Audience}, cmp, req)
} }
// Compares the exp claim against cmp. // VerifyExpiresAt compares the exp claim against cmp (cmp < exp).
// If required is false, this method will return true if the value matches or is unset // If req is false, it will return true, if exp is unset.
func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { 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. // VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
// If required is false, this method will return true if the value matches or is unset // If req is false, it will return true, if iat is unset.
func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool { 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 // 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 { func (c *StandardClaims) VerifyIssuer(cmp string, req bool) bool {
return verifyIss(c.Issuer, cmp, req) 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 // ----- helpers
func verifyAud(aud []string, cmp string, required bool) bool { func verifyAud(aud []string, cmp string, required bool) bool {
@ -113,34 +240,30 @@ func verifyAud(aud []string, cmp string, required bool) bool {
return result return result
} }
func verifyExp(exp int64, now int64, required bool) bool { func verifyExp(exp *time.Time, now time.Time, required bool) bool {
if exp == 0 { if exp == nil {
return !required return !required
} }
return now <= exp return now.Before(*exp)
} }
func verifyIat(iat int64, now int64, required bool) bool { func verifyIat(iat *time.Time, now time.Time, required bool) bool {
if iat == 0 { if iat == nil {
return !required 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 { func verifyIss(iss string, cmp string, required bool) bool {
if iss == "" { if iss == "" {
return !required return !required
} }
if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 { return 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
} }

View File

@ -16,5 +16,4 @@ To simply display a token, use:
You can install this tool with the following command: 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

View File

@ -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
}

View File

@ -3,7 +3,8 @@
// //
// Example usage: // Example usage:
// The following will create and sign a token, then verify it and output the original claims. // 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 - //
// echo {\"foo\":\"bar\"} | bin/jwt -key test/sample_key -alg RS256 -sign - | bin/jwt -key test/sample_key.pub -verify -
package main package main
import ( import (
@ -11,17 +12,17 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"regexp" "regexp"
"sort"
"strings" "strings"
"github.com/golang-jwt/jwt" "git.internal/re/jwt/v4"
) )
var ( var (
// Options // 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") flagKey = flag.String("key", "", "path to key file or '-' to read from stdin")
flagCompact = flag.Bool("compact", false, "output compact JSON") flagCompact = flag.Bool("compact", false, "output compact JSON")
flagDebug = flag.Bool("debug", false, "print out all kinds of debug data") flagDebug = flag.Bool("debug", false, "print out all kinds of debug data")
@ -67,14 +68,14 @@ func start() error {
return showToken() return showToken()
} else { } else {
flag.Usage() 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 // Helper func: Read input from specified file or stdin
func loadData(p string) ([]byte, error) { func loadData(p string) ([]byte, error) {
if p == "" { if p == "" {
return nil, fmt.Errorf("No path specified") return nil, fmt.Errorf("no path specified")
} }
var rdr io.Reader var rdr io.Reader
@ -90,7 +91,7 @@ func loadData(p string) ([]byte, error) {
return nil, err 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) // Print a json object in accordance with the prophecy (or the command line options)
@ -117,7 +118,7 @@ func verifyToken() error {
// get the token // get the token
tokData, err := loadData(*flagVerify) tokData, err := loadData(*flagVerify)
if err != nil { 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 // trim possible whitespace from token
@ -128,6 +129,9 @@ func verifyToken() error {
// Parse the token. Load the key from command line option // Parse the token. Load the key from command line option
token, err := jwt.Parse(string(tokData), func(t *jwt.Token) (interface{}, error) { token, err := jwt.Parse(string(tokData), func(t *jwt.Token) (interface{}, error) {
if isNone() {
return jwt.UnsafeAllowNoneSignatureType, nil
}
data, err := loadData(*flagKey) data, err := loadData(*flagKey)
if err != nil { if err != nil {
return nil, err return nil, err
@ -150,17 +154,17 @@ func verifyToken() error {
// Print an error if we can't parse for some reason // Print an error if we can't parse for some reason
if err != nil { if err != nil {
return fmt.Errorf("Couldn't parse token: %v", err) return fmt.Errorf("couldn't parse token: %w", err)
} }
// Is token invalid? // Is token invalid?
if !token.Valid { if !token.Valid {
return fmt.Errorf("Token is invalid") return fmt.Errorf("token is invalid")
} }
// Print the token details // Print the token details
if err := printJSON(token.Claims); err != nil { 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 return nil
@ -172,7 +176,7 @@ func signToken() error {
// get the token data from command line arguments // get the token data from command line arguments
tokData, err := loadData(*flagSign) tokData, err := loadData(*flagSign)
if err != nil { if err != nil {
return fmt.Errorf("Couldn't read token: %v", err) return fmt.Errorf("couldn't read token: %w", err)
} else if *flagDebug { } else if *flagDebug {
fmt.Fprintf(os.Stderr, "Token: %v bytes", len(tokData)) fmt.Fprintf(os.Stderr, "Token: %v bytes", len(tokData))
} }
@ -180,7 +184,7 @@ func signToken() error {
// parse the JSON of the claims // parse the JSON of the claims
var claims jwt.MapClaims var claims jwt.MapClaims
if err := json.Unmarshal(tokData, &claims); err != nil { 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 // add command line claims
@ -192,15 +196,19 @@ func signToken() error {
// get the key // get the key
var key interface{} var key interface{}
key, err = loadData(*flagKey) if isNone() {
if err != nil { key = jwt.UnsafeAllowNoneSignatureType
return fmt.Errorf("Couldn't read key: %v", err) } else {
key, err = loadData(*flagKey)
if err != nil {
return fmt.Errorf("couldn't read key: %w", err)
}
} }
// get the signing alg // get the signing alg
alg := jwt.GetSigningMethod(*flagAlg) alg := jwt.GetSigningMethod(*flagAlg)
if alg == nil { 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 // create a new token
@ -215,7 +223,7 @@ func signToken() error {
if isEs() { if isEs() {
if k, ok := key.([]byte); !ok { 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 { } else {
key, err = jwt.ParseECPrivateKeyFromPEM(k) key, err = jwt.ParseECPrivateKeyFromPEM(k)
if err != nil { if err != nil {
@ -224,7 +232,7 @@ func signToken() error {
} }
} else if isRs() { } else if isRs() {
if k, ok := key.([]byte); !ok { 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 { } else {
key, err = jwt.ParseRSAPrivateKeyFromPEM(k) key, err = jwt.ParseRSAPrivateKeyFromPEM(k)
if err != nil { if err != nil {
@ -233,7 +241,7 @@ func signToken() error {
} }
} else if isEd() { } else if isEd() {
if k, ok := key.([]byte); !ok { 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 { } else {
key, err = jwt.ParseEdPrivateKeyFromPEM(k) key, err = jwt.ParseEdPrivateKeyFromPEM(k)
if err != nil { if err != nil {
@ -245,7 +253,7 @@ func signToken() error {
if out, err := token.SignedString(key); err == nil { if out, err := token.SignedString(key); err == nil {
fmt.Println(out) fmt.Println(out)
} else { } else {
return fmt.Errorf("Error signing token: %v", err) return fmt.Errorf("error signing token: %w", err)
} }
return nil return nil
@ -256,7 +264,7 @@ func showToken() error {
// get the token // get the token
tokData, err := loadData(*flagShow) tokData, err := loadData(*flagShow)
if err != nil { 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 // trim possible whitespace from token
@ -267,18 +275,18 @@ func showToken() error {
token, err := jwt.Parse(string(tokData), nil) token, err := jwt.Parse(string(tokData), nil)
if token == nil { if token == nil {
return fmt.Errorf("malformed token: %v", err) return fmt.Errorf("malformed token: %w", err)
} }
// Print the token details // Print the token details
fmt.Println("Header:") fmt.Println("Header:")
if err := printJSON(token.Header); err != nil { 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:") fmt.Println("Claims:")
if err := printJSON(token.Claims); err != nil { 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 return nil
@ -293,5 +301,44 @@ func isRs() bool {
} }
func isEd() 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
} }

View File

@ -13,7 +13,7 @@ var (
ErrECDSAVerification = errors.New("crypto/ecdsa: verification error") 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 // Expects *ecdsa.PrivateKey for signing and *ecdsa.PublicKey for verification
type SigningMethodECDSA struct { type SigningMethodECDSA struct {
Name string Name string
@ -53,7 +53,7 @@ func (m *SigningMethodECDSA) Alg() string {
return m.Name 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 // For this verify method, key must be an ecdsa.PublicKey struct
func (m *SigningMethodECDSA) Verify(signingString, signature string, key interface{}) error { func (m *SigningMethodECDSA) Verify(signingString, signature string, key interface{}) error {
var err error var err error
@ -95,7 +95,7 @@ func (m *SigningMethodECDSA) Verify(signingString, signature string, key interfa
return ErrECDSAVerification 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 // For this signing method, key must be an ecdsa.PrivateKey struct
func (m *SigningMethodECDSA) Sign(signingString string, key interface{}) (string, error) { func (m *SigningMethodECDSA) Sign(signingString string, key interface{}) (string, error) {
// Get the key // Get the key

View File

@ -2,11 +2,11 @@ package jwt_test
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"io/ioutil" "os"
"strings" "strings"
"testing" "testing"
"github.com/golang-jwt/jwt" "git.internal/re/jwt/v4"
) )
var ecdsaTestData = []struct { var ecdsaTestData = []struct {
@ -55,7 +55,7 @@ func TestECDSAVerify(t *testing.T) {
for _, data := range ecdsaTestData { for _, data := range ecdsaTestData {
var err error var err error
key, _ := ioutil.ReadFile(data.keys["public"]) key, _ := os.ReadFile(data.keys["public"])
var ecdsaKey *ecdsa.PublicKey var ecdsaKey *ecdsa.PublicKey
if ecdsaKey, err = jwt.ParseECPublicKeyFromPEM(key); err != nil { if ecdsaKey, err = jwt.ParseECPublicKeyFromPEM(key); err != nil {
@ -78,7 +78,7 @@ func TestECDSAVerify(t *testing.T) {
func TestECDSASign(t *testing.T) { func TestECDSASign(t *testing.T) {
for _, data := range ecdsaTestData { for _, data := range ecdsaTestData {
var err error var err error
key, _ := ioutil.ReadFile(data.keys["private"]) key, _ := os.ReadFile(data.keys["private"])
var ecdsaKey *ecdsa.PrivateKey var ecdsaKey *ecdsa.PrivateKey
if ecdsaKey, err = jwt.ParseECPrivateKeyFromPEM(key); err != nil { if ecdsaKey, err = jwt.ParseECPrivateKeyFromPEM(key); err != nil {
@ -90,7 +90,6 @@ func TestECDSASign(t *testing.T) {
toSign := strings.Join(parts[0:2], ".") toSign := strings.Join(parts[0:2], ".")
method := jwt.GetSigningMethod(data.alg) method := jwt.GetSigningMethod(data.alg)
sig, err := method.Sign(toSign, ecdsaKey) sig, err := method.Sign(toSign, ecdsaKey)
if err != nil { if err != nil {
t.Errorf("[%v] Error signing token: %v", data.name, err) 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) { func BenchmarkECDSASigning(b *testing.B) {
for _, data := range ecdsaTestData { for _, data := range ecdsaTestData {
key, _ := ioutil.ReadFile(data.keys["private"]) key, _ := os.ReadFile(data.keys["private"])
ecdsaKey, err := jwt.ParseECPrivateKeyFromPEM(key) ecdsaKey, err := jwt.ParseECPrivateKeyFromPEM(key)
if err != nil { if err != nil {

View File

@ -8,11 +8,11 @@ import (
) )
var ( var (
ErrNotECPublicKey = errors.New("Key is not a valid ECDSA public key") ErrNotECPublicKey = errors.New("key is not a valid ECDSA public key")
ErrNotECPrivateKey = errors.New("Key is not a valid ECDSA private 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) { func ParseECPrivateKeyFromPEM(key []byte) (*ecdsa.PrivateKey, error) {
var err error var err error
@ -39,7 +39,7 @@ func ParseECPrivateKeyFromPEM(key []byte) (*ecdsa.PrivateKey, error) {
return pkey, nil 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) { func ParseECPublicKeyFromPEM(key []byte) (*ecdsa.PublicKey, error) {
var err error var err error

View File

@ -3,14 +3,16 @@ package jwt
import ( import (
"errors" "errors"
"crypto"
"crypto/ed25519" "crypto/ed25519"
"crypto/rand"
) )
var ( var (
ErrEd25519Verification = errors.New("ed25519: verification error") 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 // Expects ed25519.PrivateKey for signing and ed25519.PublicKey for verification
type SigningMethodEd25519 struct{} type SigningMethodEd25519 struct{}
@ -30,7 +32,7 @@ func (m *SigningMethodEd25519) Alg() string {
return "EdDSA" 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 // For this verify method, key must be an ed25519.PublicKey
func (m *SigningMethodEd25519) Verify(signingString, signature string, key interface{}) error { func (m *SigningMethodEd25519) Verify(signingString, signature string, key interface{}) error {
var err error var err error
@ -59,23 +61,25 @@ func (m *SigningMethodEd25519) Verify(signingString, signature string, key inter
return nil 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 // For this signing method, key must be an ed25519.PrivateKey
func (m *SigningMethodEd25519) Sign(signingString string, key interface{}) (string, error) { func (m *SigningMethodEd25519) Sign(signingString string, key interface{}) (string, error) {
var ed25519Key ed25519.PrivateKey var ed25519Key crypto.Signer
var ok bool var ok bool
if ed25519Key, ok = key.(ed25519.PrivateKey); !ok { if ed25519Key, ok = key.(crypto.Signer); !ok {
return "", ErrInvalidKeyType return "", ErrInvalidKeyType
} }
// ed25519.Sign panics if private key not equal to ed25519.PrivateKeySize if _, ok := ed25519Key.Public().(ed25519.PublicKey); !ok {
// this allows to avoid recover usage
if len(ed25519Key) != ed25519.PrivateKeySize {
return "", ErrInvalidKey return "", ErrInvalidKey
} }
// Sign the string and return the encoded result // 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 return EncodeSegment(sig), nil
} }

View File

@ -1,11 +1,11 @@
package jwt_test package jwt_test
import ( import (
"io/ioutil" "os"
"strings" "strings"
"testing" "testing"
"github.com/golang-jwt/jwt" "git.internal/re/jwt/v4"
) )
var ed25519TestData = []struct { var ed25519TestData = []struct {
@ -38,7 +38,7 @@ func TestEd25519Verify(t *testing.T) {
for _, data := range ed25519TestData { for _, data := range ed25519TestData {
var err error var err error
key, _ := ioutil.ReadFile(data.keys["public"]) key, _ := os.ReadFile(data.keys["public"])
ed25519Key, err := jwt.ParseEdPublicKeyFromPEM(key) ed25519Key, err := jwt.ParseEdPublicKeyFromPEM(key)
if err != nil { if err != nil {
@ -62,7 +62,7 @@ func TestEd25519Verify(t *testing.T) {
func TestEd25519Sign(t *testing.T) { func TestEd25519Sign(t *testing.T) {
for _, data := range ed25519TestData { for _, data := range ed25519TestData {
var err error var err error
key, _ := ioutil.ReadFile(data.keys["private"]) key, _ := os.ReadFile(data.keys["private"])
ed25519Key, err := jwt.ParseEdPrivateKeyFromPEM(key) ed25519Key, err := jwt.ParseEdPrivateKeyFromPEM(key)
if err != nil { if err != nil {

View File

@ -9,11 +9,11 @@ import (
) )
var ( var (
ErrNotEdPrivateKey = errors.New("Key is not a valid Ed25519 private key") ErrNotEdPrivateKey = errors.New("key is not a valid Ed25519 private key")
ErrNotEdPublicKey = errors.New("Key is not a valid Ed25519 public 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) { func ParseEdPrivateKeyFromPEM(key []byte) (crypto.PrivateKey, error) {
var err error var err error
@ -38,7 +38,7 @@ func ParseEdPrivateKeyFromPEM(key []byte) (crypto.PrivateKey, error) {
return pkey, nil 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) { func ParseEdPublicKeyFromPEM(key []byte) (crypto.PublicKey, error) {
var err error var err error

View File

@ -9,6 +9,18 @@ var (
ErrInvalidKey = errors.New("key is invalid") ErrInvalidKey = errors.New("key is invalid")
ErrInvalidKeyType = errors.New("key is of invalid type") ErrInvalidKeyType = errors.New("key is of invalid type")
ErrHashUnavailable = errors.New("the requested hash function is unavailable") 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 // The errors that might occur when parsing and validating a token
@ -27,7 +39,7 @@ const (
ValidationErrorClaimsInvalid // Generic claims validation error 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 { func NewValidationError(errorText string, errorFlags uint32) *ValidationError {
return &ValidationError{ return &ValidationError{
text: errorText, 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 { type ValidationError struct {
Inner error // stores the error returned by external dependencies, i.e.: KeyFunc Inner error // stores the error returned by external dependencies, i.e.: KeyFunc
Errors uint32 // bitfield. see ValidationError... constants Errors uint32 // bitfield. see ValidationError... constants
text string // errors that do not have a valid error just have text 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 { func (e ValidationError) Error() string {
if e.Inner != nil { if e.Inner != nil {
return e.Inner.Error() 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 // No errors
func (e *ValidationError) valid() bool { func (e *ValidationError) valid() bool {
return e.Errors == 0 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
}

View File

@ -1,47 +1,64 @@
package jwt_test package jwt_test
import ( import (
"errors"
"fmt" "fmt"
"time" "time"
"github.com/golang-jwt/jwt" "git.internal/re/jwt/v4"
) )
// Example (atypical) using the StandardClaims type by itself to parse a token. // Example (atypical) using the RegisteredClaims type by itself to parse a token.
// The StandardClaims type is designed to be embedded into your custom types // 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 // to provide standard validation features. You can use it alone, but there's
// no way to retrieve other fields after parsing. // no way to retrieve other fields after parsing.
// See the CustomClaimsType example for intended usage. // See the CustomClaimsType example for intended usage.
func ExampleNewWithClaims_standardClaims() { func ExampleNewWithClaims_registeredClaims() {
mySigningKey := []byte("AllYourBase") mySigningKey := []byte("AllYourBase")
// Create the Claims // Create the Claims
claims := &jwt.StandardClaims{ claims := &jwt.RegisteredClaims{
ExpiresAt: 15000, ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)),
Issuer: "test", Issuer: "test",
} }
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(mySigningKey) ss, err := token.SignedString(mySigningKey)
fmt.Printf("%v %v", ss, err) 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 // 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 standard claims. // in the custom type to allow for easy encoding, parsing and validation of registered claims.
func ExampleNewWithClaims_customClaimsType() { func ExampleNewWithClaims_customClaimsType() {
mySigningKey := []byte("AllYourBase") mySigningKey := []byte("AllYourBase")
type MyCustomClaims struct { type MyCustomClaims struct {
Foo string `json:"foo"` Foo string `json:"foo"`
jwt.StandardClaims jwt.RegisteredClaims
} }
// Create the Claims // Create the claims
claims := MyCustomClaims{ claims := MyCustomClaims{
"bar", "bar",
jwt.StandardClaims{ jwt.RegisteredClaims{
ExpiresAt: 15000, // 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", Issuer: "test",
}, },
} }
@ -49,48 +66,37 @@ func ExampleNewWithClaims_customClaimsType() {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(mySigningKey) ss, err := token.SignedString(mySigningKey)
fmt.Printf("%v %v", ss, err) 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 // 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. // in the custom type to allow for easy encoding, parsing and validation of standard claims.
func ExampleParseWithClaims_customClaimsType() { func ExampleParseWithClaims_customClaimsType() {
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c" tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA"
type MyCustomClaims struct { type MyCustomClaims struct {
Foo string `json:"foo"` Foo string `json:"foo"`
jwt.StandardClaims jwt.RegisteredClaims
} }
// sample token is expired. override time so it parses as valid token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
at(time.Unix(0, 0), func() { return []byte("AllYourBase"), nil
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)
} else {
fmt.Println(err)
}
}) })
// Output: bar 15000 if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {
} fmt.Printf("%v %v", claims.Foo, claims.RegisteredClaims.Issuer)
} else {
// Override time value for tests. Restore default value after. fmt.Println(err)
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 // An example of parsing the error types using bitfield checks
func ExampleParse_errorChecking() { func ExampleParse_errorChecking() {
// Token from another example. This token is expired // 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) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte("AllYourBase"), nil return []byte("AllYourBase"), nil
@ -98,15 +104,11 @@ func ExampleParse_errorChecking() {
if token.Valid { if token.Valid {
fmt.Println("You look nice today") fmt.Println("You look nice today")
} else if ve, ok := err.(*jwt.ValidationError); ok { } else if errors.Is(err, jwt.ErrTokenMalformed) {
if ve.Errors&jwt.ValidationErrorMalformed != 0 { fmt.Println("That's not even a token")
fmt.Println("That's not even a token") } else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 { // Token is either expired or not active yet
// Token is either expired or not active yet fmt.Println("Timing is everything")
fmt.Println("Timing is everything")
} else {
fmt.Println("Couldn't handle this token:", err)
}
} else { } else {
fmt.Println("Couldn't handle this token:", err) fmt.Println("Couldn't handle this token:", err)
} }

7
go.mod Normal file
View File

@ -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.
)

0
go.sum Normal file
View File

View File

@ -6,7 +6,7 @@ import (
"errors" "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 // Expects key type of []byte for both signing and validation
type SigningMethodHMAC struct { type SigningMethodHMAC struct {
Name string Name string
@ -45,7 +45,7 @@ func (m *SigningMethodHMAC) Alg() string {
return m.Name 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 { func (m *SigningMethodHMAC) Verify(signingString, signature string, key interface{}) error {
// Verify the key is the right type // Verify the key is the right type
keyBytes, ok := key.([]byte) keyBytes, ok := key.([]byte)
@ -77,7 +77,7 @@ func (m *SigningMethodHMAC) Verify(signingString, signature string, key interfac
return nil return nil
} }
// Implements the Sign method from SigningMethod for this signing method. // Sign implements token signing for the SigningMethod.
// Key must be []byte // Key must be []byte
func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) { func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) {
if keyBytes, ok := key.([]byte); ok { if keyBytes, ok := key.([]byte); ok {

View File

@ -2,10 +2,10 @@ package jwt_test
import ( import (
"fmt" "fmt"
"io/ioutil" "os"
"time" "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 // For HMAC signing method, the key can be any []byte. It is recommended to generate
@ -15,7 +15,7 @@ var hmacSampleSecret []byte
func init() { func init() {
// Load sample key data // Load sample key data
if keyData, e := ioutil.ReadFile("test/hmacTestKey"); e == nil { if keyData, e := os.ReadFile("test/hmacTestKey"); e == nil {
hmacSampleSecret = keyData hmacSampleSecret = keyData
} else { } else {
panic(e) panic(e)

View File

@ -1,11 +1,11 @@
package jwt_test package jwt_test
import ( import (
"io/ioutil" "os"
"strings" "strings"
"testing" "testing"
"github.com/golang-jwt/jwt" "git.internal/re/jwt/v4"
) )
var hmacTestData = []struct { 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 // 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) { func TestHMACVerify(t *testing.T) {
for _, data := range hmacTestData { for _, data := range hmacTestData {

View File

@ -8,16 +8,16 @@ import (
"crypto/rsa" "crypto/rsa"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strings" "strings"
"time" "time"
"github.com/golang-jwt/jwt" "git.internal/re/jwt/v4"
"github.com/golang-jwt/jwt/request" "git.internal/re/jwt/v4/request"
) )
// location of the files used for signing and verification // location of the files used for signing and verification
@ -30,22 +30,17 @@ var (
verifyKey *rsa.PublicKey verifyKey *rsa.PublicKey
signKey *rsa.PrivateKey signKey *rsa.PrivateKey
serverPort int 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 // read the key files before starting http handlers
func init() { func init() {
signBytes, err := ioutil.ReadFile(privKeyPath) signBytes, err := os.ReadFile(privKeyPath)
fatal(err) fatal(err)
signKey, err = jwt.ParseRSAPrivateKeyFromPEM(signBytes) signKey, err = jwt.ParseRSAPrivateKeyFromPEM(signBytes)
fatal(err) fatal(err)
verifyBytes, err := ioutil.ReadFile(pubKeyPath) verifyBytes, err := os.ReadFile(pubKeyPath)
fatal(err) fatal(err)
verifyKey, err = jwt.ParseRSAPublicKeyFromPEM(verifyBytes) verifyKey, err = jwt.ParseRSAPublicKeyFromPEM(verifyBytes)
@ -65,8 +60,6 @@ func init() {
}() }()
} }
var start func()
func fatal(err error) { func fatal(err error) {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -80,7 +73,7 @@ type CustomerInfo struct {
} }
type CustomClaimsExample struct { type CustomClaimsExample struct {
*jwt.StandardClaims jwt.RegisteredClaims
TokenType string TokenType string
CustomerInfo CustomerInfo
} }
@ -116,11 +109,10 @@ func Example_getTokenViaHTTP() {
claims := token.Claims.(*CustomClaimsExample) claims := token.Claims.(*CustomClaimsExample)
fmt.Println(claims.CustomerInfo.Name) fmt.Println(claims.CustomerInfo.Name)
//Output: test // Output: test
} }
func Example_useTokenViaHTTP() { func Example_useTokenViaHTTP() {
// Make a sample token // Make a sample token
// In a real world situation, this token will have been acquired from // In a real world situation, this token will have been acquired from
// some other API call (see Example_getTokenViaHTTP) // some other API call (see Example_getTokenViaHTTP)
@ -149,10 +141,10 @@ func createToken(user string) (string, error) {
// set our claims // set our claims
t.Claims = &CustomClaimsExample{ t.Claims = &CustomClaimsExample{
&jwt.StandardClaims{ jwt.RegisteredClaims{
// set the expire time // set the expire time
// see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-20#section-4.1.4 // see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
ExpiresAt: time.Now().Add(time.Minute * 1).Unix(), ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 1)),
}, },
"level1", "level1",
CustomerInfo{user, "human"}, CustomerInfo{user, "human"},
@ -199,12 +191,11 @@ func authHandler(w http.ResponseWriter, r *http.Request) {
// only accessible with a valid token // only accessible with a valid token
func restrictedHandler(w http.ResponseWriter, r *http.Request) { func restrictedHandler(w http.ResponseWriter, r *http.Request) {
// Get token from 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, // since we only use the one private key to sign the tokens,
// we also only use its public counter part to verify // we also only use its public counter part to verify
return verifyKey, nil return verifyKey, nil
}) }, request.WithClaims(&CustomClaimsExample{}))
// If the token is missing or invalid, return error // If the token is missing or invalid, return error
if err != nil { if err != nil {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
@ -214,5 +205,4 @@ func restrictedHandler(w http.ResponseWriter, r *http.Request) {
// Token is valid // Token is valid
fmt.Fprintln(w, "Welcome,", token.Claims.(*CustomClaimsExample).Name) fmt.Fprintln(w, "Welcome,", token.Claims.(*CustomClaimsExample).Name)
return
} }

View File

@ -3,10 +3,11 @@ package jwt
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"time"
// "fmt" // "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 // This is the default claims type if you don't supply one
type MapClaims map[string]interface{} type MapClaims map[string]interface{}
@ -31,65 +32,92 @@ func (m MapClaims) VerifyAudience(cmp string, req bool) bool {
return verifyAud(aud, cmp, req) return verifyAud(aud, cmp, req)
} }
// Compares the exp claim against cmp. // VerifyExpiresAt compares the exp claim against cmp (cmp <= exp).
// If required is false, this method will return true if the value matches or is unset // If req is false, it will return true, if exp is unset.
func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool {
exp, ok := m["exp"] cmpTime := time.Unix(cmp, 0)
v, ok := m["exp"]
if !ok { if !ok {
return !req return !req
} }
switch expType := exp.(type) {
switch exp := v.(type) {
case float64: case float64:
return verifyExp(int64(expType), cmp, req) if exp == 0 {
return verifyExp(nil, cmpTime, req)
}
return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req)
case json.Number: case json.Number:
v, _ := expType.Int64() v, _ := exp.Float64()
return verifyExp(v, cmp, req)
return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req)
} }
return false return false
} }
// Compares the iat claim against cmp. // VerifyIssuedAt compares the exp claim against cmp (cmp >= iat).
// If required is false, this method will return true if the value matches or is unset // If req is false, it will return true, if iat is unset.
func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool { func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool {
iat, ok := m["iat"] cmpTime := time.Unix(cmp, 0)
v, ok := m["iat"]
if !ok { if !ok {
return !req return !req
} }
switch iatType := iat.(type) {
switch iat := v.(type) {
case float64: case float64:
return verifyIat(int64(iatType), cmp, req) if iat == 0 {
return verifyIat(nil, cmpTime, req)
}
return verifyIat(&newNumericDateFromSeconds(iat).Time, cmpTime, req)
case json.Number: case json.Number:
v, _ := iatType.Int64() v, _ := iat.Float64()
return verifyIat(v, cmp, req)
return verifyIat(&newNumericDateFromSeconds(v).Time, cmpTime, req)
} }
return false 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 // 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 { func (m MapClaims) VerifyIssuer(cmp string, req bool) bool {
iss, _ := m["iss"].(string) iss, _ := m["iss"].(string)
return verifyIss(iss, cmp, req) return verifyIss(iss, cmp, req)
} }
// Compares the nbf claim against cmp. // Valid validates time based claims "exp, iat, nbf".
// 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".
// There is no accounting for clock skew. // There is no accounting for clock skew.
// As well, if any of the above claims are not in the token, it will still // As well, if any of the above claims are not in the token, it will still
// be considered a valid claim. // be considered a valid claim.
@ -98,16 +126,19 @@ func (m MapClaims) Valid() error {
now := TimeFunc().Unix() now := TimeFunc().Unix()
if !m.VerifyExpiresAt(now, false) { if !m.VerifyExpiresAt(now, false) {
// TODO(oxisto): this should be replaced with ErrTokenExpired
vErr.Inner = errors.New("Token is expired") vErr.Inner = errors.New("Token is expired")
vErr.Errors |= ValidationErrorExpired vErr.Errors |= ValidationErrorExpired
} }
if !m.VerifyIssuedAt(now, false) { if !m.VerifyIssuedAt(now, false) {
// TODO(oxisto): this should be replaced with ErrTokenUsedBeforeIssued
vErr.Inner = errors.New("Token used before issued") vErr.Inner = errors.New("Token used before issued")
vErr.Errors |= ValidationErrorIssuedAt vErr.Errors |= ValidationErrorIssuedAt
} }
if !m.VerifyNotBefore(now, false) { if !m.VerifyNotBefore(now, false) {
// TODO(oxisto): this should be replaced with ErrTokenNotValidYet
vErr.Inner = errors.New("Token is not valid yet") vErr.Inner = errors.New("Token is not valid yet")
vErr.Errors |= ValidationErrorNotValidYet vErr.Errors |= ValidationErrorNotValidYet
} }

View File

@ -2,60 +2,58 @@ package jwt
import ( import (
"testing" "testing"
"time"
) )
func TestVerifyAud(t *testing.T) { func TestVerifyAud(t *testing.T) {
var nilInterface interface{} var nilInterface interface{}
var nilListInterface []interface{} var nilListInterface []interface{}
var intListInterface interface{} = []int{1,2,3} var intListInterface interface{} = []int{1, 2, 3}
type test struct{ type test struct {
Name string Name string
MapClaims MapClaims MapClaims MapClaims
Expected bool Expected bool
Comparison string Comparison string
Required bool Required bool
} }
tests := []test{ tests := []test{
// Matching Claim in aud // Matching Claim in aud
// Required = true // Required = true
{ Name: "String Aud matching required", MapClaims: MapClaims{"aud": "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"}, {Name: "[]String Aud with match required", MapClaims: MapClaims{"aud": []string{"example.com", "example.example.com"}}, Expected: true, Required: true, Comparison: "example.com"},
// Required = false // Required = false
{ Name: "String Aud with match not required", MapClaims: MapClaims{"aud": "example.com"}, 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{}, 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: "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: "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: "[]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: "Empty []String Aud with match not required", MapClaims: MapClaims{"aud": []string{}}, Expected: true, Required: false, Comparison: "example.com"},
// Non-Matching Claim in aud // Non-Matching Claim in aud
// Required = true // Required = true
{ Name: "String Aud without match required", MapClaims: MapClaims{"aud": "not.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: "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: "[]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: "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: "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: "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 not required", MapClaims: MapClaims{"aud": []string{"not.example.com", "example.example.com"}}, Expected: false, Required: true, Comparison: "example.com"},
// Required = false // 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{} // []interface{}
{ Name: "Empty []interface{} Aud without match required", MapClaims: MapClaims{"aud": nilListInterface}, Expected: true, Required: false, 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 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 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: "[]interface{} Aud int wit match required", MapClaims: MapClaims{"aud": intListInterface}, Expected: false, Required: true, Comparison: "example.com"},
// interface{} // 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 { for _, test := range tests {
t.Run(test.Name, func(t *testing.T) { t.Run(test.Name, func(t *testing.T) {
got := test.MapClaims.VerifyAudience(test.Comparison, test.Required) 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) 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)
}
}

View File

@ -1,6 +1,6 @@
package jwt 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. // but you probably should never use it.
var SigningMethodNone *signingMethodNone var SigningMethodNone *signingMethodNone

View File

@ -4,7 +4,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/golang-jwt/jwt" "git.internal/re/jwt/v4"
) )
var noneTestData = []struct { var noneTestData = []struct {

View File

@ -8,18 +8,47 @@ import (
) )
type Parser struct { type Parser struct {
ValidMethods []string // If populated, only these methods will be considered valid // 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 // 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. // 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) { func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
return p.ParseWithClaims(tokenString, MapClaims{}, keyFunc) 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) { func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
token, parts, err := p.ParseUnverified(tokenString, claims) token, parts, err := p.ParseUnverified(tokenString, claims)
if err != nil { if err != nil {
@ -87,12 +116,12 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
return token, vErr 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 // WARNING: Don't use this method unless you know what you're doing.
// 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's only ever useful in cases where you know the signature is valid (because it has
// it. // 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) { func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) {
parts = strings.Split(tokenString, ".") parts = strings.Split(tokenString, ".")
if len(parts) != 3 { if len(parts) != 3 {

29
parser_option.go Normal file
View File

@ -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
}
}

View File

@ -1,39 +1,59 @@
package jwt_test package jwt_test
import ( import (
"crypto"
"crypto/rsa" "crypto/rsa"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"reflect" "reflect"
"testing" "testing"
"time" "time"
"github.com/golang-jwt/jwt" "git.internal/re/jwt/v4"
"github.com/golang-jwt/jwt/test" "git.internal/re/jwt/v4/test"
) )
var keyFuncError error = fmt.Errorf("error loading key") var errKeyFuncError error = fmt.Errorf("error loading key")
var ( var (
jwtTestDefaultKey *rsa.PublicKey jwtTestDefaultKey *rsa.PublicKey
defaultKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return jwtTestDefaultKey, nil } jwtTestRSAPrivateKey *rsa.PrivateKey
emptyKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, nil } jwtTestEC256PublicKey crypto.PublicKey
errorKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, keyFuncError } jwtTestEC256PrivateKey crypto.PrivateKey
nilKeyFunc jwt.Keyfunc = nil 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, errKeyFuncError }
nilKeyFunc jwt.Keyfunc = nil
) )
func init() { func init() {
// Load public keys
jwtTestDefaultKey = test.LoadRSAPublicKeyFromDisk("test/sample_key.pub") 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 { var jwtTestData = []struct {
name string name string
tokenString string tokenString string
keyfunc jwt.Keyfunc keyfunc jwt.Keyfunc
claims jwt.Claims claims jwt.Claims
valid bool valid bool
errors uint32 errors uint32
parser *jwt.Parser err []error
parser *jwt.Parser
signingMethod jwt.SigningMethod // The method to sign the JWT token for test purpose
}{ }{
{ {
"basic", "basic",
@ -43,6 +63,8 @@ var jwtTestData = []struct {
true, true,
0, 0,
nil, nil,
nil,
jwt.SigningMethodRS256,
}, },
{ {
"basic expired", "basic expired",
@ -51,7 +73,9 @@ var jwtTestData = []struct {
jwt.MapClaims{"foo": "bar", "exp": float64(time.Now().Unix() - 100)}, jwt.MapClaims{"foo": "bar", "exp": float64(time.Now().Unix() - 100)},
false, false,
jwt.ValidationErrorExpired, jwt.ValidationErrorExpired,
[]error{jwt.ErrTokenExpired},
nil, nil,
jwt.SigningMethodRS256,
}, },
{ {
"basic nbf", "basic nbf",
@ -60,7 +84,9 @@ var jwtTestData = []struct {
jwt.MapClaims{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)}, jwt.MapClaims{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)},
false, false,
jwt.ValidationErrorNotValidYet, jwt.ValidationErrorNotValidYet,
[]error{jwt.ErrTokenNotValidYet},
nil, nil,
jwt.SigningMethodRS256,
}, },
{ {
"expired and nbf", "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)}, jwt.MapClaims{"foo": "bar", "nbf": float64(time.Now().Unix() + 100), "exp": float64(time.Now().Unix() - 100)},
false, false,
jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired, jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired,
[]error{jwt.ErrTokenNotValidYet},
nil, nil,
jwt.SigningMethodRS256,
}, },
{ {
"basic invalid", "basic invalid",
@ -78,7 +106,9 @@ var jwtTestData = []struct {
jwt.MapClaims{"foo": "bar"}, jwt.MapClaims{"foo": "bar"},
false, false,
jwt.ValidationErrorSignatureInvalid, jwt.ValidationErrorSignatureInvalid,
[]error{jwt.ErrTokenSignatureInvalid, rsa.ErrVerification},
nil, nil,
jwt.SigningMethodRS256,
}, },
{ {
"basic nokeyfunc", "basic nokeyfunc",
@ -87,7 +117,9 @@ var jwtTestData = []struct {
jwt.MapClaims{"foo": "bar"}, jwt.MapClaims{"foo": "bar"},
false, false,
jwt.ValidationErrorUnverifiable, jwt.ValidationErrorUnverifiable,
[]error{jwt.ErrTokenUnverifiable},
nil, nil,
jwt.SigningMethodRS256,
}, },
{ {
"basic nokey", "basic nokey",
@ -96,7 +128,9 @@ var jwtTestData = []struct {
jwt.MapClaims{"foo": "bar"}, jwt.MapClaims{"foo": "bar"},
false, false,
jwt.ValidationErrorSignatureInvalid, jwt.ValidationErrorSignatureInvalid,
[]error{jwt.ErrTokenSignatureInvalid},
nil, nil,
jwt.SigningMethodRS256,
}, },
{ {
"basic errorkey", "basic errorkey",
@ -105,7 +139,9 @@ var jwtTestData = []struct {
jwt.MapClaims{"foo": "bar"}, jwt.MapClaims{"foo": "bar"},
false, false,
jwt.ValidationErrorUnverifiable, jwt.ValidationErrorUnverifiable,
[]error{jwt.ErrTokenUnverifiable, errKeyFuncError},
nil, nil,
jwt.SigningMethodRS256,
}, },
{ {
"invalid signing method", "invalid signing method",
@ -114,16 +150,42 @@ var jwtTestData = []struct {
jwt.MapClaims{"foo": "bar"}, jwt.MapClaims{"foo": "bar"},
false, false,
jwt.ValidationErrorSignatureInvalid, jwt.ValidationErrorSignatureInvalid,
[]error{jwt.ErrTokenSignatureInvalid},
&jwt.Parser{ValidMethods: []string{"HS256"}}, &jwt.Parser{ValidMethods: []string{"HS256"}},
jwt.SigningMethodRS256,
}, },
{ {
"valid signing method", "valid RSA signing method",
"", "",
defaultKeyFunc, defaultKeyFunc,
jwt.MapClaims{"foo": "bar"}, jwt.MapClaims{"foo": "bar"},
true, true,
0, 0,
nil,
&jwt.Parser{ValidMethods: []string{"RS256", "HS256"}}, &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", "JSON Number",
@ -132,7 +194,9 @@ var jwtTestData = []struct {
jwt.MapClaims{"foo": json.Number("123.4")}, jwt.MapClaims{"foo": json.Number("123.4")},
true, true,
0, 0,
nil,
&jwt.Parser{UseJSONNumber: true}, &jwt.Parser{UseJSONNumber: true},
jwt.SigningMethodRS256,
}, },
{ {
"Standard Claims", "Standard Claims",
@ -143,7 +207,9 @@ var jwtTestData = []struct {
}, },
true, true,
0, 0,
nil,
&jwt.Parser{UseJSONNumber: true}, &jwt.Parser{UseJSONNumber: true},
jwt.SigningMethodRS256,
}, },
{ {
"JSON Number - basic expired", "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))}, jwt.MapClaims{"foo": "bar", "exp": json.Number(fmt.Sprintf("%v", time.Now().Unix()-100))},
false, false,
jwt.ValidationErrorExpired, jwt.ValidationErrorExpired,
[]error{jwt.ErrTokenExpired},
&jwt.Parser{UseJSONNumber: true}, &jwt.Parser{UseJSONNumber: true},
jwt.SigningMethodRS256,
}, },
{ {
"JSON Number - basic nbf", "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))}, jwt.MapClaims{"foo": "bar", "nbf": json.Number(fmt.Sprintf("%v", time.Now().Unix()+100))},
false, false,
jwt.ValidationErrorNotValidYet, jwt.ValidationErrorNotValidYet,
[]error{jwt.ErrTokenNotValidYet},
&jwt.Parser{UseJSONNumber: true}, &jwt.Parser{UseJSONNumber: true},
jwt.SigningMethodRS256,
}, },
{ {
"JSON Number - expired and nbf", "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))}, 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, false,
jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired, jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired,
[]error{jwt.ErrTokenNotValidYet},
&jwt.Parser{UseJSONNumber: true}, &jwt.Parser{UseJSONNumber: true},
jwt.SigningMethodRS256,
}, },
{ {
"SkipClaimsValidation during token parsing", "SkipClaimsValidation during token parsing",
@ -179,116 +251,470 @@ var jwtTestData = []struct {
jwt.MapClaims{"foo": "bar", "nbf": json.Number(fmt.Sprintf("%v", time.Now().Unix()+100))}, jwt.MapClaims{"foo": "bar", "nbf": json.Number(fmt.Sprintf("%v", time.Now().Unix()+100))},
true, true,
0, 0,
nil,
&jwt.Parser{UseJSONNumber: true, SkipClaimsValidation: true}, &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) { // signToken creates and returns a signed JWT token using signingMethod.
privateKey := test.LoadRSAPrivateKeyFromDisk("test/sample_key") 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 // Iterate over test data set and run tests
for _, data := range jwtTestData { for _, data := range jwtTestData {
// If the token string is blank, use helper function to generate string t.Run(data.name, func(t *testing.T) {
if data.tokenString == "" { // If the token string is blank, use helper function to generate string
data.tokenString = test.MakeSampleToken(data.claims, privateKey) if data.tokenString == "" {
} data.tokenString = signToken(data.claims, data.signingMethod)
}
// Parse the token // Parse the token
var token *jwt.Token var token *jwt.Token
var err error var ve *jwt.ValidationError
var parser = data.parser var err error
if parser == nil { parser := data.parser
parser = new(jwt.Parser) if parser == nil {
} parser = new(jwt.Parser)
// Figure out correct claims type }
switch data.claims.(type) { // Figure out correct claims type
case jwt.MapClaims: switch data.claims.(type) {
token, err = parser.ParseWithClaims(data.tokenString, jwt.MapClaims{}, data.keyfunc) case jwt.MapClaims:
case *jwt.StandardClaims: token, err = parser.ParseWithClaims(data.tokenString, jwt.MapClaims{}, data.keyfunc)
token, err = parser.ParseWithClaims(data.tokenString, &jwt.StandardClaims{}, 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 // Verify result matches expectation
if !reflect.DeepEqual(data.claims, token.Claims) { if !reflect.DeepEqual(data.claims, token.Claims) {
t.Errorf("[%v] Claims mismatch. Expecting: %v Got: %v", data.name, data.claims, token.Claims) t.Errorf("[%v] Claims mismatch. Expecting: %v Got: %v", data.name, data.claims, token.Claims)
} }
if data.valid && err != nil { if data.valid && err != nil {
t.Errorf("[%v] Error while verifying token: %T:%v", data.name, err, err) t.Errorf("[%v] Error while verifying token: %T:%v", data.name, err, err)
} }
if !data.valid && err == nil { if !data.valid && err == nil {
t.Errorf("[%v] Invalid token passed validation", data.name) t.Errorf("[%v] Invalid token passed validation", data.name)
} }
if (err == nil && !token.Valid) || (err != nil && token.Valid) { if (err == nil && !token.Valid) || (err != nil && token.Valid) {
t.Errorf("[%v] Inconsistent behavior between returned error and token.Valid", data.name) t.Errorf("[%v] Inconsistent behavior between returned error and token.Valid", data.name)
} }
if data.errors != 0 { if data.errors != 0 {
if err == nil { if err == nil {
t.Errorf("[%v] Expecting error. Didn't get one.", data.name) t.Errorf("[%v] Expecting error. Didn't get one.", data.name)
} else { } else {
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)
}
ve := err.(*jwt.ValidationError) if err.Error() == errKeyFuncError.Error() && ve.Inner != errKeyFuncError {
// compare the bitfield part of the error t.Errorf("[%v] Inner error does not match expectation. %v != %v", data.name, ve.Inner, errKeyFuncError)
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 data.valid && token.Signature == "" { if data.err != nil {
t.Errorf("[%v] Signature is left unpopulated after parsing", data.name) 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) { 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 = signToken(data.claims, data.signingMethod)
}
// Parse the token
var token *jwt.Token
var err error
parser := data.parser
if parser == nil {
parser = new(jwt.Parser)
}
// Figure out correct claims type
switch data.claims.(type) {
case jwt.MapClaims:
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 {
t.Errorf("[%v] Invalid token", data.name)
}
// Verify result matches expectation
if !reflect.DeepEqual(data.claims, token.Claims) {
t.Errorf("[%v] Claims mismatch. Expecting: %v Got: %v", data.name, data.claims, token.Claims)
}
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)
}
})
}
}
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 // Iterate over test data set and run tests
for _, data := range jwtTestData { for _, data := range jwtTestData {
// If the token string is blank, use helper function to generate string // If the token string is blank, use helper function to generate string
if data.tokenString == "" { if data.tokenString == "" {
data.tokenString = test.MakeSampleToken(data.claims, privateKey) data.tokenString = signToken(data.claims, data.signingMethod)
} }
// Parse the token // Parse the token
var token *jwt.Token parser := data.parser
var err error
var parser = data.parser
if parser == nil { if parser == nil {
parser = new(jwt.Parser) parser = new(jwt.Parser)
} }
// Figure out correct claims type // Figure out correct claims type
switch data.claims.(type) { switch data.claims.(type) {
case jwt.MapClaims: case jwt.MapClaims:
token, _, err = parser.ParseUnverified(data.tokenString, jwt.MapClaims{}) b.Run("map_claims", func(b *testing.B) {
benchmarkParsing(b, parser, data.tokenString, jwt.MapClaims{})
})
case *jwt.StandardClaims: case *jwt.StandardClaims:
token, _, err = parser.ParseUnverified(data.tokenString, &jwt.StandardClaims{}) b.Run("standard_claims", func(b *testing.B) {
} benchmarkParsing(b, parser, data.tokenString, &jwt.StandardClaims{})
})
if err != nil {
t.Errorf("[%v] Invalid token", data.name)
}
// Verify result matches expectation
if !reflect.DeepEqual(data.claims, token.Claims) {
t.Errorf("[%v] Claims mismatch. Expecting: %v Got: %v", data.name, data.claims, token.Claims)
}
if data.valid && err != nil {
t.Errorf("[%v] Error while verifying token: %T:%v", data.name, err, err)
} }
} }
} }
// Helper method for benchmarking various methods // 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{}) { func benchmarkSigning(b *testing.B, method jwt.SigningMethod, key interface{}) {
b.Helper()
t := jwt.New(method) t := jwt.New(method)
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
@ -299,5 +725,4 @@ func benchmarkSigning(b *testing.B, method jwt.SigningMethod, key interface{}) {
} }
} }
}) })
} }

View File

@ -3,6 +3,7 @@ package request
import ( import (
"errors" "errors"
"net/http" "net/http"
"strings"
) )
// Errors // Errors
@ -10,15 +11,15 @@ var (
ErrNoTokenInRequest = errors.New("no token present in request") 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. // The ExtractToken method should return a token string or an error.
// If no token is present, you must return ErrNoTokenInRequest. // If no token is present, you must return ErrNoTokenInRequest.
type Extractor interface { type Extractor interface {
ExtractToken(*http.Request) (string, error) ExtractToken(*http.Request) (string, error)
} }
// Extractor for finding a token in a header. Looks at each specified // HeaderExtractor is an extractor for finding a token in a header.
// header in order until there's a match // Looks at each specified header in order until there's a match
type HeaderExtractor []string type HeaderExtractor []string
func (e HeaderExtractor) ExtractToken(req *http.Request) (string, error) { func (e HeaderExtractor) ExtractToken(req *http.Request) (string, error) {
@ -31,7 +32,7 @@ func (e HeaderExtractor) ExtractToken(req *http.Request) (string, error) {
return "", ErrNoTokenInRequest 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. // GET URL arguments. Argument names are tried in order until there's a match.
// This extractor calls `ParseMultipartForm` on the request // This extractor calls `ParseMultipartForm` on the request
type ArgumentExtractor []string type ArgumentExtractor []string
@ -50,7 +51,7 @@ func (e ArgumentExtractor) ExtractToken(req *http.Request) (string, error) {
return "", ErrNoTokenInRequest 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 type MultiExtractor []Extractor
func (e MultiExtractor) ExtractToken(req *http.Request) (string, error) { 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 { for _, extractor := range e {
if tok, err := extractor.ExtractToken(req); tok != "" { if tok, err := extractor.ExtractToken(req); tok != "" {
return tok, nil return tok, nil
} else if err != ErrNoTokenInRequest { } else if !errors.Is(err, ErrNoTokenInRequest) {
return "", err return "", err
} }
} }
return "", ErrNoTokenInRequest 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 // See AuthorizationHeaderExtractor for an example
type PostExtractionFilter struct { type PostExtractionFilter struct {
Extractor Extractor
@ -79,3 +80,18 @@ func (e *PostExtractionFilter) ExtractToken(req *http.Request) (string, error) {
return "", err 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
}

View File

@ -89,3 +89,23 @@ func makeExampleRequest(method, path string, headers map[string]string, urlArgs
} }
return r 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)
}
}

View File

@ -13,14 +13,14 @@ func stripBearerPrefixFromTokenString(tok string) (string, error) {
return tok, nil 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 // Uses PostExtractionFilter to strip "Bearer " prefix from header
var AuthorizationHeaderExtractor = &PostExtractionFilter{ var AuthorizationHeaderExtractor = &PostExtractionFilter{
HeaderExtractor{"Authorization"}, HeaderExtractor{"Authorization"},
stripBearerPrefixFromTokenString, 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. // header then 'access_token' argument for a token.
var OAuth2Extractor = &MultiExtractor{ var OAuth2Extractor = &MultiExtractor{
AuthorizationHeaderExtractor, AuthorizationHeaderExtractor,

View File

@ -3,10 +3,10 @@ package request
import ( import (
"net/http" "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 // 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 // instead of a token string. The Extractor interface allows you to define
// the logic for extracting a token. Several useful implementations are provided. // 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) return p.parser.ParseWithClaims(tokenString, p.claims, keyFunc)
} }
// ParseFromRequest but with custom Claims type // ParseFromRequestWithClaims is an alias for ParseFromRequest but with custom Claims type.
// DEPRECATED: use ParseFromRequest and the WithClaims option //
// 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) { 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)) return ParseFromRequest(req, extractor, keyFunc, WithClaims(claims))
} }
@ -54,14 +55,14 @@ type fromRequestParser struct {
type ParseFromRequestOption func(*fromRequestParser) type ParseFromRequestOption func(*fromRequestParser)
// Parse with custom claims // WithClaims parses with custom claims
func WithClaims(claims jwt.Claims) ParseFromRequestOption { func WithClaims(claims jwt.Claims) ParseFromRequestOption {
return func(p *fromRequestParser) { return func(p *fromRequestParser) {
p.claims = claims p.claims = claims
} }
} }
// Parse using a custom parser // WithParser parses using a custom parser
func WithParser(parser *jwt.Parser) ParseFromRequestOption { func WithParser(parser *jwt.Parser) ParseFromRequestOption {
return func(p *fromRequestParser) { return func(p *fromRequestParser) {
p.parser = parser p.parser = parser

View File

@ -8,8 +8,8 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/golang-jwt/jwt" "git.internal/re/jwt/v4"
"github.com/golang-jwt/jwt/test" "git.internal/re/jwt/v4/test"
) )
var requestTestData = []struct { var requestTestData = []struct {
@ -65,7 +65,7 @@ func TestParseRequest(t *testing.T) {
// Bearer token request // Bearer token request
for _, data := range requestTestData { for _, data := range requestTestData {
// Make token from claims // Make token from claims
tokenString := test.MakeSampleToken(data.claims, privateKey) tokenString := test.MakeSampleToken(data.claims, jwt.SigningMethodRS256, privateKey)
// Make query string // Make query string
for k, vv := range data.query { for k, vv := range data.query {

6
rsa.go
View File

@ -6,7 +6,7 @@ import (
"crypto/rsa" "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 // Expects *rsa.PrivateKey for signing and *rsa.PublicKey for validation
type SigningMethodRSA struct { type SigningMethodRSA struct {
Name string Name string
@ -44,7 +44,7 @@ func (m *SigningMethodRSA) Alg() string {
return m.Name 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. // For this signing method, must be an *rsa.PublicKey structure.
func (m *SigningMethodRSA) Verify(signingString, signature string, key interface{}) error { func (m *SigningMethodRSA) Verify(signingString, signature string, key interface{}) error {
var err 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) 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. // For this signing method, must be an *rsa.PrivateKey structure.
func (m *SigningMethodRSA) Sign(signingString string, key interface{}) (string, error) { func (m *SigningMethodRSA) Sign(signingString string, key interface{}) (string, error) {
var rsaKey *rsa.PrivateKey var rsaKey *rsa.PrivateKey

View File

@ -1,3 +1,4 @@
//go:build go1.4
// +build go1.4 // +build go1.4
package jwt package jwt
@ -8,7 +9,7 @@ import (
"crypto/rsa" "crypto/rsa"
) )
// Implements the RSAPSS family of signing methods signing methods // SigningMethodRSAPSS implements the RSAPSS family of signing methods signing methods
type SigningMethodRSAPSS struct { type SigningMethodRSAPSS struct {
*SigningMethodRSA *SigningMethodRSA
Options *rsa.PSSOptions 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 // For this verify method, key must be an rsa.PublicKey struct
func (m *SigningMethodRSAPSS) Verify(signingString, signature string, key interface{}) error { func (m *SigningMethodRSAPSS) Verify(signingString, signature string, key interface{}) error {
var err 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) 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 // For this signing method, key must be an rsa.PrivateKey struct
func (m *SigningMethodRSAPSS) Sign(signingString string, key interface{}) (string, error) { func (m *SigningMethodRSAPSS) Sign(signingString string, key interface{}) (string, error) {
var rsaKey *rsa.PrivateKey var rsaKey *rsa.PrivateKey

View File

@ -1,16 +1,17 @@
//go:build go1.4
// +build go1.4 // +build go1.4
package jwt_test package jwt_test
import ( import (
"crypto/rsa" "crypto/rsa"
"io/ioutil" "os"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/golang-jwt/jwt" "git.internal/re/jwt/v4"
"github.com/golang-jwt/jwt/test" "git.internal/re/jwt/v4/test"
) )
var rsaPSSTestData = []struct { var rsaPSSTestData = []struct {
@ -53,7 +54,7 @@ var rsaPSSTestData = []struct {
func TestRSAPSSVerify(t *testing.T) { func TestRSAPSSVerify(t *testing.T) {
var err error var err error
key, _ := ioutil.ReadFile("test/sample_key.pub") key, _ := os.ReadFile("test/sample_key.pub")
var rsaPSSKey *rsa.PublicKey var rsaPSSKey *rsa.PublicKey
if rsaPSSKey, err = jwt.ParseRSAPublicKeyFromPEM(key); err != nil { if rsaPSSKey, err = jwt.ParseRSAPublicKeyFromPEM(key); err != nil {
t.Errorf("Unable to parse RSA public key: %v", err) t.Errorf("Unable to parse RSA public key: %v", err)
@ -76,7 +77,7 @@ func TestRSAPSSVerify(t *testing.T) {
func TestRSAPSSSign(t *testing.T) { func TestRSAPSSSign(t *testing.T) {
var err error var err error
key, _ := ioutil.ReadFile("test/sample_key") key, _ := os.ReadFile("test/sample_key")
var rsaPSSKey *rsa.PrivateKey var rsaPSSKey *rsa.PrivateKey
if rsaPSSKey, err = jwt.ParseRSAPrivateKeyFromPEM(key); err != nil { if rsaPSSKey, err = jwt.ParseRSAPrivateKeyFromPEM(key); err != nil {
t.Errorf("Unable to parse RSA private key: %v", err) 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 { func makeToken(method jwt.SigningMethod) string {
token := jwt.NewWithClaims(method, jwt.StandardClaims{ token := jwt.NewWithClaims(method, jwt.RegisteredClaims{
Issuer: "example", Issuer: "example",
IssuedAt: time.Now().Unix(), IssuedAt: jwt.NewNumericDate(time.Now()),
}) })
privateKey := test.LoadRSAPrivateKeyFromDisk("test/sample_key") privateKey := test.LoadRSAPrivateKeyFromDisk("test/sample_key")
signed, err := token.SignedString(privateKey) signed, err := token.SignedString(privateKey)

View File

@ -1,52 +1,47 @@
package jwt_test package jwt_test
import ( import (
"io/ioutil" "os"
"strings" "strings"
"testing" "testing"
"github.com/golang-jwt/jwt" "git.internal/re/jwt/v4"
) )
var rsaTestData = []struct { var rsaTestData = []struct {
name string name string
tokenString string tokenString string
alg string alg string
claims map[string]interface{}
valid bool valid bool
}{ }{
{ {
"Basic RS256", "Basic RS256",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
"RS256", "RS256",
map[string]interface{}{"foo": "bar"},
true, true,
}, },
{ {
"Basic RS384", "Basic RS384",
"eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.W-jEzRfBigtCWsinvVVuldiuilzVdU5ty0MvpLaSaqK9PlAWWlDQ1VIQ_qSKzwL5IXaZkvZFJXT3yL3n7OUVu7zCNJzdwznbC8Z-b0z2lYvcklJYi2VOFRcGbJtXUqgjk2oGsiqUMUMOLP70TTefkpsgqDxbRh9CDUfpOJgW-dU7cmgaoswe3wjUAUi6B6G2YEaiuXC0XScQYSYVKIzgKXJV8Zw-7AN_DBUI4GkTpsvQ9fVVjZM9csQiEXhYekyrKu1nu_POpQonGd8yqkIyXPECNmmqH5jH4sFiF67XhD7_JpkvLziBpI-uh86evBUadmHhb9Otqw3uV3NTaXLzJw", "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.W-jEzRfBigtCWsinvVVuldiuilzVdU5ty0MvpLaSaqK9PlAWWlDQ1VIQ_qSKzwL5IXaZkvZFJXT3yL3n7OUVu7zCNJzdwznbC8Z-b0z2lYvcklJYi2VOFRcGbJtXUqgjk2oGsiqUMUMOLP70TTefkpsgqDxbRh9CDUfpOJgW-dU7cmgaoswe3wjUAUi6B6G2YEaiuXC0XScQYSYVKIzgKXJV8Zw-7AN_DBUI4GkTpsvQ9fVVjZM9csQiEXhYekyrKu1nu_POpQonGd8yqkIyXPECNmmqH5jH4sFiF67XhD7_JpkvLziBpI-uh86evBUadmHhb9Otqw3uV3NTaXLzJw",
"RS384", "RS384",
map[string]interface{}{"foo": "bar"},
true, true,
}, },
{ {
"Basic RS512", "Basic RS512",
"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.zBlLlmRrUxx4SJPUbV37Q1joRcI9EW13grnKduK3wtYKmDXbgDpF1cZ6B-2Jsm5RB8REmMiLpGms-EjXhgnyh2TSHE-9W2gA_jvshegLWtwRVDX40ODSkTb7OVuaWgiy9y7llvcknFBTIg-FnVPVpXMmeV_pvwQyhaz1SSwSPrDyxEmksz1hq7YONXhXPpGaNbMMeDTNP_1oj8DZaqTIL9TwV8_1wb2Odt_Fy58Ke2RVFijsOLdnyEAjt2n9Mxihu9i3PhNBkkxa2GbnXBfq3kzvZ_xxGGopLdHhJjcGWXO-NiwI9_tiu14NRv4L2xC0ItD9Yz68v2ZIZEp_DuzwRQ", "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.zBlLlmRrUxx4SJPUbV37Q1joRcI9EW13grnKduK3wtYKmDXbgDpF1cZ6B-2Jsm5RB8REmMiLpGms-EjXhgnyh2TSHE-9W2gA_jvshegLWtwRVDX40ODSkTb7OVuaWgiy9y7llvcknFBTIg-FnVPVpXMmeV_pvwQyhaz1SSwSPrDyxEmksz1hq7YONXhXPpGaNbMMeDTNP_1oj8DZaqTIL9TwV8_1wb2Odt_Fy58Ke2RVFijsOLdnyEAjt2n9Mxihu9i3PhNBkkxa2GbnXBfq3kzvZ_xxGGopLdHhJjcGWXO-NiwI9_tiu14NRv4L2xC0ItD9Yz68v2ZIZEp_DuzwRQ",
"RS512", "RS512",
map[string]interface{}{"foo": "bar"},
true, true,
}, },
{ {
"basic invalid: foo => bar", "basic invalid: foo => bar",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.EhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.EhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
"RS256", "RS256",
map[string]interface{}{"foo": "bar"},
false, false,
}, },
} }
func TestRSAVerify(t *testing.T) { func TestRSAVerify(t *testing.T) {
keyData, _ := ioutil.ReadFile("test/sample_key.pub") keyData, _ := os.ReadFile("test/sample_key.pub")
key, _ := jwt.ParseRSAPublicKeyFromPEM(keyData) key, _ := jwt.ParseRSAPublicKeyFromPEM(keyData)
for _, data := range rsaTestData { for _, data := range rsaTestData {
@ -64,7 +59,7 @@ func TestRSAVerify(t *testing.T) {
} }
func TestRSASign(t *testing.T) { func TestRSASign(t *testing.T) {
keyData, _ := ioutil.ReadFile("test/sample_key") keyData, _ := os.ReadFile("test/sample_key")
key, _ := jwt.ParseRSAPrivateKeyFromPEM(keyData) key, _ := jwt.ParseRSAPrivateKeyFromPEM(keyData)
for _, data := range rsaTestData { for _, data := range rsaTestData {
@ -83,7 +78,7 @@ func TestRSASign(t *testing.T) {
} }
func TestRSAVerifyWithPreParsedPrivateKey(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) parsedKey, err := jwt.ParseRSAPublicKeyFromPEM(key)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -97,7 +92,7 @@ func TestRSAVerifyWithPreParsedPrivateKey(t *testing.T) {
} }
func TestRSAWithPreParsedPrivateKey(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) parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -114,9 +109,9 @@ func TestRSAWithPreParsedPrivateKey(t *testing.T) {
} }
func TestRSAKeyParsing(t *testing.T) { func TestRSAKeyParsing(t *testing.T) {
key, _ := ioutil.ReadFile("test/sample_key") key, _ := os.ReadFile("test/sample_key")
secureKey, _ := ioutil.ReadFile("test/privateSecure.pem") secureKey, _ := os.ReadFile("test/privateSecure.pem")
pubKey, _ := ioutil.ReadFile("test/sample_key.pub") pubKey, _ := os.ReadFile("test/sample_key.pub")
badKey := []byte("All your base are belong to key") badKey := []byte("All your base are belong to key")
// Test parsePrivateKey // Test parsePrivateKey
@ -152,11 +147,24 @@ func TestRSAKeyParsing(t *testing.T) {
if k, e := jwt.ParseRSAPublicKeyFromPEM(badKey); e == nil { if k, e := jwt.ParseRSAPublicKeyFromPEM(badKey); e == nil {
t.Errorf("Parsed invalid key as valid private key: %v", k) 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) { func BenchmarkRS256Signing(b *testing.B) {
key, _ := ioutil.ReadFile("test/sample_key") key, _ := os.ReadFile("test/sample_key")
parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key) parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
@ -166,7 +174,7 @@ func BenchmarkRS256Signing(b *testing.B) {
} }
func BenchmarkRS384Signing(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) parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
@ -176,7 +184,7 @@ func BenchmarkRS384Signing(b *testing.B) {
} }
func BenchmarkRS512Signing(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) parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)

View File

@ -8,12 +8,12 @@ import (
) )
var ( var (
ErrKeyMustBePEMEncoded = errors.New("Invalid Key: Key must be a PEM encoded PKCS1 or PKCS8 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") ErrNotRSAPrivateKey = errors.New("key is not a valid RSA private key")
ErrNotRSAPublicKey = errors.New("Key is not a valid RSA public 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) { func ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error) {
var err error var err error
@ -39,7 +39,11 @@ func ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error) {
return pkey, nil 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) { func ParseRSAPrivateKeyFromPEMWithPassword(key []byte, password string) (*rsa.PrivateKey, error) {
var err error var err error
@ -71,7 +75,7 @@ func ParseRSAPrivateKeyFromPEMWithPassword(key []byte, password string) (*rsa.Pr
return pkey, nil 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) { func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) {
var err error var err error

View File

@ -7,14 +7,14 @@ import (
var signingMethods = map[string]func() SigningMethod{} var signingMethods = map[string]func() SigningMethod{}
var signingMethodLock = new(sync.RWMutex) 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 { type SigningMethod interface {
Verify(signingString, signature string, key interface{}) error // Returns nil if signature is valid 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 Sign(signingString string, key interface{}) (string, error) // Returns encoded signature or error
Alg() string // returns the alg identifier for this method (example: 'HS256') 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 // This is typically done during init() in the method's implementation
func RegisterSigningMethod(alg string, f func() SigningMethod) { func RegisterSigningMethod(alg string, f func() SigningMethod) {
signingMethodLock.Lock() signingMethodLock.Lock()
@ -23,7 +23,7 @@ func RegisterSigningMethod(alg string, f func() SigningMethod) {
signingMethods[alg] = f 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) { func GetSigningMethod(alg string) (method SigningMethod) {
signingMethodLock.RLock() signingMethodLock.RLock()
defer signingMethodLock.RUnlock() defer signingMethodLock.RUnlock()
@ -33,3 +33,14 @@ func GetSigningMethod(alg string) (method SigningMethod) {
} }
return 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
}

1
staticcheck.conf Normal file
View File

@ -0,0 +1 @@
checks = ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1023"]

View File

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIcaUjXhC7Mn2OonyfHF+zjblKkns
4GLbILnHrZr+aQwddiff5urCDAZ177t81Mn39CDs3uhlNDxfRIRheGnK/Q==
-----END PUBLIC KEY-----

View File

@ -1,14 +1,15 @@
package test package test
import ( import (
"crypto"
"crypto/rsa" "crypto/rsa"
"io/ioutil" "os"
"github.com/golang-jwt/jwt" "git.internal/re/jwt/v4"
) )
func LoadRSAPrivateKeyFromDisk(location string) *rsa.PrivateKey { func LoadRSAPrivateKeyFromDisk(location string) *rsa.PrivateKey {
keyData, e := ioutil.ReadFile(location) keyData, e := os.ReadFile(location)
if e != nil { if e != nil {
panic(e.Error()) panic(e.Error())
} }
@ -20,7 +21,7 @@ func LoadRSAPrivateKeyFromDisk(location string) *rsa.PrivateKey {
} }
func LoadRSAPublicKeyFromDisk(location string) *rsa.PublicKey { func LoadRSAPublicKeyFromDisk(location string) *rsa.PublicKey {
keyData, e := ioutil.ReadFile(location) keyData, e := os.ReadFile(location)
if e != nil { if e != nil {
panic(e.Error()) panic(e.Error())
} }
@ -31,8 +32,9 @@ func LoadRSAPublicKeyFromDisk(location string) *rsa.PublicKey {
return key return key
} }
func MakeSampleToken(c jwt.Claims, key interface{}) string { // MakeSampleToken creates and returns a encoded JWT token that has been signed with the specified cryptographic key.
token := jwt.NewWithClaims(jwt.SigningMethodRS256, c) func MakeSampleToken(c jwt.Claims, method jwt.SigningMethod, key interface{}) string {
token := jwt.NewWithClaims(method, c)
s, e := token.SignedString(key) s, e := token.SignedString(key)
if e != nil { if e != nil {
@ -41,3 +43,27 @@ func MakeSampleToken(c jwt.Claims, key interface{}) string {
return s 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
}

View File

@ -7,18 +7,31 @@ import (
"time" "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). // 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 // 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. // server uses a different time zone than your tokens.
var TimeFunc = time.Now 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, // the key for verification. The function receives the parsed,
// but unverified Token. This allows you to use properties in the // but unverified Token. This allows you to use properties in the
// Header of the token (such as `kid`) to identify which key to use. // Header of the token (such as `kid`) to identify which key to use.
type Keyfunc func(*Token) (interface{}, error) 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. // creating or parsing/verifying a token.
type Token struct { type Token struct {
Raw string // The raw token. Populated when you Parse a token 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 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 { func New(method SigningMethod) *Token {
return NewWithClaims(method, MapClaims{}) return NewWithClaims(method, MapClaims{})
} }
// NewWithClaims creates a new Token with the specified signing method and claims.
func NewWithClaims(method SigningMethod, claims Claims) *Token { func NewWithClaims(method SigningMethod, claims Claims) *Token {
return &Token{ return &Token{
Header: map[string]interface{}{ 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) { func (t *Token) SignedString(key interface{}) (string, error) {
var sig, sstr string var sig, sstr string
var err error var err error
@ -58,47 +73,71 @@ func (t *Token) SignedString(key interface{}) (string, error) {
return strings.Join([]string{sstr, sig}, "."), nil 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 // most expensive part of the whole deal. Unless you
// need this for something special, just go straight for // need this for something special, just go straight for
// the SignedString. // the SignedString.
func (t *Token) SigningString() (string, error) { func (t *Token) SigningString() (string, error) {
var err error var err error
parts := make([]string, 2) var jsonValue []byte
for i := range parts {
var jsonValue []byte
if i == 0 {
if jsonValue, err = json.Marshal(t.Header); err != nil {
return "", err
}
} else {
if jsonValue, err = json.Marshal(t.Claims); err != nil {
return "", err
}
}
parts[i] = EncodeSegment(jsonValue) if jsonValue, err = json.Marshal(t.Header); err != nil {
return "", err
} }
return strings.Join(parts, "."), nil header := EncodeSegment(jsonValue)
if jsonValue, err = json.Marshal(t.Claims); err != nil {
return "", err
}
claim := EncodeSegment(jsonValue)
return strings.Join([]string{header, claim}, "."), nil
} }
// Parse, validate, and return a token. // Parse parses, validates, verifies the signature and returns the parsed token.
// keyFunc will receive the parsed token and should return the key for validating. // keyFunc will receive the parsed token and should return the cryptographic key
// If everything is kosher, err will be nil // for verifying the signature.
func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { // The caller is strongly encouraged to set the WithValidMethods option to
return new(Parser).Parse(tokenString, keyFunc) // 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) { // ParseWithClaims is a shortcut for NewParser().ParseWithClaims().
return new(Parser).ParseWithClaims(tokenString, claims, keyFunc) //
// 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 { func EncodeSegment(seg []byte) string {
return base64.RawURLEncoding.EncodeToString(seg) 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) { 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)
} }

79
token_test.go Normal file
View File

@ -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()
}
})
}

145
types.go Normal file
View File

@ -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))
}

126
types_test.go Normal file
View File

@ -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)
}
}
}