forked from mirror/jwt
Compare commits
56 Commits
Author | SHA1 | Date |
---|---|---|
re | 29918af7f7 | |
Alexander Yastrebov | 9358574a7a | |
Christian Banse | 2f0984a28b | |
Christian Banse | 2101c1f4bc | |
Krouton | 35053d4e20 | |
Jacob Kopczynski | 0c4e387985 | |
Christian Banse | bfea432b1a | |
Michael Fridman | d81acbf7f3 | |
Hugo | fdaf0eb0e0 | |
KroKite | f2878bb94b | |
George Kechagias | 9294af54b5 | |
Qian Qiao | 2da0bf7566 | |
Christian Banse | 8fb42696ff | |
Michael Fridman | cf43decf7c | |
Michael Fridman | 4426925f0c | |
Håvard Anda Estensen | f6c6299f67 | |
Luigi Morel | 89a6400b7f | |
Vladislav Polyakov | 6e2ab4291f | |
Christian Banse | 83478b3c8f | |
Michael Fridman | 0972257eba | |
Michael Fridman | 1096e506e6 | |
ksegun | d489c99d3e | |
ydylla | 6de17d3b3e | |
Michael Fridman | 279dd19720 | |
Giau. Tran Minh | 863d23d08a | |
Michael Fridman | 2387103809 | |
Máté Lang | d0c0939ff8 | |
hyeonjae | e01ed05a31 | |
Christian Banse | 78a18c0808 | |
Stefan Tudose | 0fb40d3824 | |
tfonfara | c435f38291 | |
Alexander Yastrebov | a725c1f60c | |
Kevin de Berk | 823c014036 | |
Alexander Yastrebov | 1275a5b909 | |
ajermaky | f4865cddea | |
Alexander Yastrebov | 9c3665f0fc | |
PiotrKozimor | a2aa655627 | |
Sebastien Rosset | c0ffb890f3 | |
Christian Banse | 65357b9e5b | |
Ichinose Shogo | cac353cdc2 | |
Sebastien Rosset | fd8cd69d8e | |
Hinagiku Soranoba | 02bc1ac506 | |
Hyun | d2c5d5ab01 | |
Yoan Blanc | 205b3dc4bb | |
Michael Fridman | 93130d3c71 | |
yoogo | 3f50a786ff | |
Mark Karpelès | 2bd8ee77fc | |
Christian Banse | 80625fb516 | |
Luis Gabriel Gomez | c9ab96ba53 | |
Alexander F. Rødseth | eac9e9edf2 | |
Michael Fridman | a06361ba65 | |
Zach Wasserman | bac80eaac8 | |
Francois Lebel | 85f0a979dd | |
Luis Gabriel Gomez | 3258b3fca0 | |
Christian Banse | bd2db2d4a2 | |
Michael Fridman | 2ebb50f957 |
|
@ -8,26 +8,43 @@ on:
|
||||||
types: [opened, synchronize, reopened]
|
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 }}
|
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
# pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
# branches: [ main ]
|
||||||
|
schedule:
|
||||||
|
- cron: '31 10 * * 5'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'go' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
|
# Learn more:
|
||||||
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
|
@ -1,22 +1,22 @@
|
||||||
## Migration Guide (v3.2.1)
|
## Migration Guide (v4.0.0)
|
||||||
|
|
||||||
Starting from [v3.2.1](https://github.com/golang-jwt/jwt/releases/tag/v3.2.1]), the import path has changed from `github.com/dgrijalva/jwt-go` to `github.com/golang-jwt/jwt`. Future releases will be using the `github.com/golang-jwt/jwt` import path and continue the existing versioning scheme of `v3.x.x+incompatible`. Backwards-compatible patches and fixes will be done on the `v3` release branch, where as new build-breaking features will be developed in a `v4` release, possibly including a SIV-style import path.
|
Starting from [v4.0.0](https://github.com/golang-jwt/jwt/releases/tag/v4.0.0), the import path will be:
|
||||||
|
|
||||||
### go.mod replacement
|
"git.internal/re/jwt/v4"
|
||||||
|
|
||||||
In a first step, the easiest way is to use `go mod edit` to issue a replacement.
|
The `/v4` version will be backwards compatible with existing `v3.x.y` tags in this repo, as well as
|
||||||
|
`github.com/dgrijalva/jwt-go`. For most users this should be a drop-in replacement, if you're having
|
||||||
|
troubles migrating, please open an issue.
|
||||||
|
|
||||||
|
You can replace all occurrences of `github.com/dgrijalva/jwt-go` or `github.com/golang-jwt/jwt` with `git.internal/re/jwt/v4`, either manually or by using tools such as `sed` or `gofmt`.
|
||||||
|
|
||||||
|
And then you'd typically run:
|
||||||
|
|
||||||
```
|
```
|
||||||
go mod edit -replace github.com/dgrijalva/jwt-go=github.com/golang-jwt/jwt@v3.2.1+incompatible
|
go get git.internal/re/jwt/v4
|
||||||
go mod tidy
|
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.
|
61
README.md
61
README.md
|
@ -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).
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
As of February 2022 (and until this document is updated), the latest version `v4` is supported.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you think you found a vulnerability, and even if you are not sure, please report it to jwt-go-security@googlegroups.com or one of the other [golang-jwt maintainers](https://github.com/orgs/golang-jwt/people). Please try be explicit, describe steps to reproduce the security issue with code example(s).
|
||||||
|
|
||||||
|
You will receive a response within a timely manner. If the issue is confirmed, we will do our best to release a patch as soon as possible given the complexity of the problem.
|
||||||
|
|
||||||
|
## Public Discussions
|
||||||
|
|
||||||
|
Please avoid publicly discussing a potential security vulnerability.
|
||||||
|
|
||||||
|
Let's take this offline and find a solution first, this limits the potential impact as much as possible.
|
||||||
|
|
||||||
|
We appreciate your help!
|
|
@ -1,5 +1,9 @@
|
||||||
## `jwt-go` Version History
|
## `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)).
|
||||||
|
|
221
claims.go
221
claims.go
|
@ -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"`
|
//
|
||||||
|
// See examples for how to use this with your own claim types.
|
||||||
|
type RegisteredClaims struct {
|
||||||
|
// the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
|
||||||
Issuer string `json:"iss,omitempty"`
|
Issuer string `json:"iss,omitempty"`
|
||||||
NotBefore int64 `json:"nbf,omitempty"`
|
|
||||||
|
// the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
|
||||||
Subject string `json:"sub,omitempty"`
|
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ArgList map[string]string
|
|
||||||
|
|
||||||
func (l ArgList) String() string {
|
|
||||||
data, _ := json.Marshal(l)
|
|
||||||
return string(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l ArgList) Set(arg string) error {
|
|
||||||
parts := strings.SplitN(arg, "=", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return fmt.Errorf("Invalid argument '%v'. Must use format 'key=value'. %v", arg, parts)
|
|
||||||
}
|
|
||||||
l[parts[0]] = parts[1]
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -3,6 +3,7 @@
|
||||||
//
|
//
|
||||||
// Example usage:
|
// 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
|
||||||
|
|
||||||
|
@ -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{}
|
||||||
|
if isNone() {
|
||||||
|
key = jwt.UnsafeAllowNoneSignatureType
|
||||||
|
} else {
|
||||||
key, err = loadData(*flagKey)
|
key, err = loadData(*flagKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Couldn't read key: %v", err)
|
return fmt.Errorf("couldn't read key: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the signing alg
|
// 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
|
||||||
}
|
}
|
6
ecdsa.go
6
ecdsa.go
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
22
ed25519.go
22
ed25519.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
59
errors.go
59
errors.go
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
at(time.Unix(0, 0), func() {
|
|
||||||
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
return []byte("AllYourBase"), nil
|
return []byte("AllYourBase"), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {
|
if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {
|
||||||
fmt.Printf("%v %v", claims.Foo, claims.StandardClaims.ExpiresAt)
|
fmt.Printf("%v %v", claims.Foo, claims.RegisteredClaims.Issuer)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
// Output: bar 15000
|
// Output: bar test
|
||||||
}
|
|
||||||
|
|
||||||
// Override time value for tests. Restore default value after.
|
|
||||||
func at(t time.Time, f func()) {
|
|
||||||
jwt.TimeFunc = func() time.Time {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
f()
|
|
||||||
jwt.TimeFunc = time.Now
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,18 +104,14 @@ 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 ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
|
} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
|
||||||
// Token is either expired or not active yet
|
// Token is either expired or not active yet
|
||||||
fmt.Println("Timing is everything")
|
fmt.Println("Timing is everything")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Couldn't handle this token:", err)
|
fmt.Println("Couldn't handle this token:", err)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
fmt.Println("Couldn't handle this token:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output: Timing is everything
|
// Output: Timing is everything
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
module git.internal/re/jwt/v4
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
retract (
|
||||||
|
v4.4.0 // Contains a backwards incompatible change to the Claims interface.
|
||||||
|
)
|
6
hmac.go
6
hmac.go
|
@ -6,7 +6,7 @@ import (
|
||||||
"errors"
|
"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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
103
map_claims.go
103
map_claims.go
|
@ -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 {
|
||||||
case json.Number:
|
return verifyExp(nil, cmpTime, req)
|
||||||
v, _ := expType.Int64()
|
|
||||||
return verifyExp(v, cmp, req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req)
|
||||||
|
case json.Number:
|
||||||
|
v, _ := exp.Float64()
|
||||||
|
|
||||||
|
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 {
|
||||||
case json.Number:
|
return verifyIat(nil, cmpTime, req)
|
||||||
v, _ := iatType.Int64()
|
|
||||||
return verifyIat(v, cmp, req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return verifyIat(&newNumericDateFromSeconds(iat).Time, cmpTime, req)
|
||||||
|
case json.Number:
|
||||||
|
v, _ := iat.Float64()
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,14 @@ 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
|
||||||
|
@ -18,44 +19,41 @@ func TestVerifyAud(t *testing.T) {
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
2
none.go
2
none.go
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
49
parser.go
49
parser.go
|
@ -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 {
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
// ParserOption is used to implement functional-style options that modify the behavior of the parser. To add
|
||||||
|
// new options, just create a function (ideally beginning with With or Without) that returns an anonymous function that
|
||||||
|
// takes a *Parser type as input and manipulates its configuration accordingly.
|
||||||
|
type ParserOption func(*Parser)
|
||||||
|
|
||||||
|
// WithValidMethods is an option to supply algorithm methods that the parser will check. Only those methods will be considered valid.
|
||||||
|
// It is heavily encouraged to use this option in order to prevent attacks such as https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/.
|
||||||
|
func WithValidMethods(methods []string) ParserOption {
|
||||||
|
return func(p *Parser) {
|
||||||
|
p.ValidMethods = methods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithJSONNumber is an option to configure the underlying JSON parser with UseNumber
|
||||||
|
func WithJSONNumber() ParserOption {
|
||||||
|
return func(p *Parser) {
|
||||||
|
p.UseJSONNumber = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithoutClaimsValidation is an option to disable claims validation. This option should only be used if you exactly know
|
||||||
|
// what you are doing.
|
||||||
|
func WithoutClaimsValidation() ParserOption {
|
||||||
|
return func(p *Parser) {
|
||||||
|
p.SkipClaimsValidation = true
|
||||||
|
}
|
||||||
|
}
|
465
parser_test.go
465
parser_test.go
|
@ -1,29 +1,47 @@
|
||||||
package jwt_test
|
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
|
||||||
|
jwtTestRSAPrivateKey *rsa.PrivateKey
|
||||||
|
jwtTestEC256PublicKey crypto.PublicKey
|
||||||
|
jwtTestEC256PrivateKey crypto.PrivateKey
|
||||||
|
paddedKey crypto.PublicKey
|
||||||
defaultKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return jwtTestDefaultKey, nil }
|
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 }
|
emptyKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, nil }
|
||||||
errorKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, keyFuncError }
|
errorKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, errKeyFuncError }
|
||||||
nilKeyFunc jwt.Keyfunc = nil
|
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 {
|
||||||
|
@ -33,7 +51,9 @@ var jwtTestData = []struct {
|
||||||
claims jwt.Claims
|
claims jwt.Claims
|
||||||
valid bool
|
valid bool
|
||||||
errors uint32
|
errors uint32
|
||||||
|
err []error
|
||||||
parser *jwt.Parser
|
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,24 +251,105 @@ 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 {
|
||||||
|
t.Run(data.name, func(t *testing.T) {
|
||||||
// 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
|
var token *jwt.Token
|
||||||
|
var ve *jwt.ValidationError
|
||||||
var err error
|
var err error
|
||||||
var parser = data.parser
|
parser := data.parser
|
||||||
if parser == nil {
|
if parser == nil {
|
||||||
parser = new(jwt.Parser)
|
parser = new(jwt.Parser)
|
||||||
}
|
}
|
||||||
|
@ -206,6 +359,8 @@ func TestParser_Parse(t *testing.T) {
|
||||||
token, err = parser.ParseWithClaims(data.tokenString, jwt.MapClaims{}, data.keyfunc)
|
token, err = parser.ParseWithClaims(data.tokenString, jwt.MapClaims{}, data.keyfunc)
|
||||||
case *jwt.StandardClaims:
|
case *jwt.StandardClaims:
|
||||||
token, err = parser.ParseWithClaims(data.tokenString, &jwt.StandardClaims{}, data.keyfunc)
|
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
|
||||||
|
@ -229,38 +384,65 @@ func TestParser_Parse(t *testing.T) {
|
||||||
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) {
|
||||||
ve := err.(*jwt.ValidationError)
|
|
||||||
// compare the bitfield part of the error
|
// compare the bitfield part of the error
|
||||||
if e := ve.Errors; e != data.errors {
|
if e := ve.Errors; e != data.errors {
|
||||||
t.Errorf("[%v] Errors don't match expectation. %v != %v", data.name, 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 {
|
if err.Error() == errKeyFuncError.Error() && ve.Inner != errKeyFuncError {
|
||||||
t.Errorf("[%v] Inner error does not match expectation. %v != %v", data.name, ve.Inner, keyFuncError)
|
t.Errorf("[%v] Inner error does not match expectation. %v != %v", data.name, ve.Inner, errKeyFuncError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if data.valid && token.Signature == "" {
|
}
|
||||||
|
|
||||||
|
if data.err != nil {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("[%v] Expecting error(s). Didn't get one.", data.name)
|
||||||
|
} else {
|
||||||
|
all := false
|
||||||
|
for _, e := range data.err {
|
||||||
|
all = errors.Is(err, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !all {
|
||||||
|
t.Errorf("[%v] Errors don't match expectation. %v should contain all of %v", data.name, err, data.err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.valid {
|
||||||
|
if token.Signature == "" {
|
||||||
t.Errorf("[%v] Signature is left unpopulated after parsing", data.name)
|
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
|
// Iterate over test data set and run tests
|
||||||
for _, data := range jwtTestData {
|
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 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
|
var token *jwt.Token
|
||||||
var err error
|
var err error
|
||||||
var parser = data.parser
|
parser := data.parser
|
||||||
if parser == nil {
|
if parser == nil {
|
||||||
parser = new(jwt.Parser)
|
parser = new(jwt.Parser)
|
||||||
}
|
}
|
||||||
|
@ -270,6 +452,8 @@ func TestParser_ParseUnverified(t *testing.T) {
|
||||||
token, _, err = parser.ParseUnverified(data.tokenString, jwt.MapClaims{})
|
token, _, err = parser.ParseUnverified(data.tokenString, jwt.MapClaims{})
|
||||||
case *jwt.StandardClaims:
|
case *jwt.StandardClaims:
|
||||||
token, _, err = parser.ParseUnverified(data.tokenString, &jwt.StandardClaims{})
|
token, _, err = parser.ParseUnverified(data.tokenString, &jwt.StandardClaims{})
|
||||||
|
case *jwt.RegisteredClaims:
|
||||||
|
token, _, err = parser.ParseUnverified(data.tokenString, &jwt.RegisteredClaims{})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -284,11 +468,253 @@ func TestParser_ParseUnverified(t *testing.T) {
|
||||||
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 token.Valid {
|
||||||
|
// The 'Valid' field should not be set to true when invoking ParseUnverified()
|
||||||
|
t.Errorf("[%v] Token.Valid field mismatch. Expecting false, got %v", data.name, token.Valid)
|
||||||
|
}
|
||||||
|
if token.Signature != "" {
|
||||||
|
// The signature was not validated, hence the 'Signature' field is not populated.
|
||||||
|
t.Errorf("[%v] Token.Signature field mismatch. Expecting '', got %v", data.name, token.Signature)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper method for benchmarking various methods
|
var setPaddingTestData = []struct {
|
||||||
|
name string
|
||||||
|
tokenString string
|
||||||
|
claims jwt.Claims
|
||||||
|
paddedDecode bool
|
||||||
|
strictDecode bool
|
||||||
|
signingMethod jwt.SigningMethod
|
||||||
|
keyfunc jwt.Keyfunc
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Validated non-padded token with padding disabled",
|
||||||
|
tokenString: "",
|
||||||
|
claims: jwt.MapClaims{"foo": "paddedbar"},
|
||||||
|
paddedDecode: false,
|
||||||
|
signingMethod: jwt.SigningMethodRS256,
|
||||||
|
keyfunc: defaultKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Validated non-padded token with padding enabled",
|
||||||
|
tokenString: "",
|
||||||
|
claims: jwt.MapClaims{"foo": "paddedbar"},
|
||||||
|
paddedDecode: true,
|
||||||
|
signingMethod: jwt.SigningMethodRS256,
|
||||||
|
keyfunc: defaultKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error for padded token with padding disabled",
|
||||||
|
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ==.20kGGJaYekGTRFf8b0TwhuETcR8lv5z2363X5jf7G1yTWVTwOmte5Ii8L8_OQbYwPoiVHmZY6iJPbt_DhCN42AeFY74BcsUhR-BVrYUVhKK0RppuzEcSlILDNeQsJDLEL035CPm1VO6Jrgk7enQPIctVxUesRgswP71OpGvJxy3j1k_J8p0WzZvRZTe1D_2Misa0UDGwnEIHhmr97fIpMSZjFxlcygQw8QN34IHLHIXMaTY1eiCf4CCr6rOS9wUeu7P3CPkmFq9XhxBT_LLCmIMhHnxP5x27FUJE_JZlfek0MmARcrhpsZS2sFhHAiWrjxjOE27jkDtv1nEwn65wMw==",
|
||||||
|
claims: jwt.MapClaims{"foo": "paddedbar"},
|
||||||
|
paddedDecode: false,
|
||||||
|
signingMethod: jwt.SigningMethodRS256,
|
||||||
|
keyfunc: defaultKeyFunc,
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Validated padded token with padding enabled",
|
||||||
|
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ==.20kGGJaYekGTRFf8b0TwhuETcR8lv5z2363X5jf7G1yTWVTwOmte5Ii8L8_OQbYwPoiVHmZY6iJPbt_DhCN42AeFY74BcsUhR-BVrYUVhKK0RppuzEcSlILDNeQsJDLEL035CPm1VO6Jrgk7enQPIctVxUesRgswP71OpGvJxy3j1k_J8p0WzZvRZTe1D_2Misa0UDGwnEIHhmr97fIpMSZjFxlcygQw8QN34IHLHIXMaTY1eiCf4CCr6rOS9wUeu7P3CPkmFq9XhxBT_LLCmIMhHnxP5x27FUJE_JZlfek0MmARcrhpsZS2sFhHAiWrjxjOE27jkDtv1nEwn65wMw==",
|
||||||
|
claims: jwt.MapClaims{"foo": "paddedbar"},
|
||||||
|
paddedDecode: true,
|
||||||
|
signingMethod: jwt.SigningMethodRS256,
|
||||||
|
keyfunc: defaultKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error for example padded token with padding disabled",
|
||||||
|
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3grw==",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: false,
|
||||||
|
signingMethod: jwt.SigningMethodES256,
|
||||||
|
keyfunc: paddedKeyFunc,
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Validated example padded token with padding enabled",
|
||||||
|
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3grw==",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: true,
|
||||||
|
signingMethod: jwt.SigningMethodES256,
|
||||||
|
keyfunc: paddedKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
// DecodeStrict tests, DecodePaddingAllowed=false
|
||||||
|
{
|
||||||
|
name: "Validated non-padded token with padding disabled, non-strict decode, non-tweaked signature",
|
||||||
|
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
|
||||||
|
"g",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: false,
|
||||||
|
strictDecode: false,
|
||||||
|
signingMethod: jwt.SigningMethodRS256,
|
||||||
|
keyfunc: defaultKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Validated non-padded token with padding disabled, non-strict decode, tweaked signature",
|
||||||
|
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
|
||||||
|
"h",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: false,
|
||||||
|
strictDecode: false,
|
||||||
|
signingMethod: jwt.SigningMethodRS256,
|
||||||
|
keyfunc: defaultKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Validated non-padded token with padding disabled, strict decode, non-tweaked signature",
|
||||||
|
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
|
||||||
|
"g",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: false,
|
||||||
|
strictDecode: true,
|
||||||
|
signingMethod: jwt.SigningMethodRS256,
|
||||||
|
keyfunc: defaultKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error for non-padded token with padding disabled, strict decode, tweaked signature",
|
||||||
|
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
|
||||||
|
"h",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: false,
|
||||||
|
strictDecode: true,
|
||||||
|
signingMethod: jwt.SigningMethodRS256,
|
||||||
|
keyfunc: defaultKeyFunc,
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
// DecodeStrict tests, DecodePaddingAllowed=true
|
||||||
|
{
|
||||||
|
name: "Validated padded token with padding enabled, non-strict decode, non-tweaked signature",
|
||||||
|
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
|
||||||
|
"w==",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: true,
|
||||||
|
strictDecode: false,
|
||||||
|
signingMethod: jwt.SigningMethodES256,
|
||||||
|
keyfunc: paddedKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Validated padded token with padding enabled, non-strict decode, tweaked signature",
|
||||||
|
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
|
||||||
|
"x==",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: true,
|
||||||
|
strictDecode: false,
|
||||||
|
signingMethod: jwt.SigningMethodES256,
|
||||||
|
keyfunc: paddedKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Validated padded token with padding enabled, strict decode, non-tweaked signature",
|
||||||
|
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
|
||||||
|
"w==",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: true,
|
||||||
|
strictDecode: true,
|
||||||
|
signingMethod: jwt.SigningMethodES256,
|
||||||
|
keyfunc: paddedKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error for padded token with padding enabled, strict decode, tweaked signature",
|
||||||
|
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
|
||||||
|
"x==",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: true,
|
||||||
|
strictDecode: true,
|
||||||
|
signingMethod: jwt.SigningMethodES256,
|
||||||
|
keyfunc: paddedKeyFunc,
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension of Parsing, this is to test out functionality specific to switching codecs with padding.
|
||||||
|
func TestSetPadding(t *testing.T) {
|
||||||
|
for _, data := range setPaddingTestData {
|
||||||
|
t.Run(data.name, func(t *testing.T) {
|
||||||
|
jwt.DecodePaddingAllowed = data.paddedDecode
|
||||||
|
jwt.DecodeStrict = data.strictDecode
|
||||||
|
|
||||||
|
// If the token string is blank, use helper function to generate string
|
||||||
|
if data.tokenString == "" {
|
||||||
|
data.tokenString = signToken(data.claims, data.signingMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the token
|
||||||
|
var token *jwt.Token
|
||||||
|
var err error
|
||||||
|
parser := new(jwt.Parser)
|
||||||
|
parser.SkipClaimsValidation = true
|
||||||
|
|
||||||
|
// Figure out correct claims type
|
||||||
|
token, err = parser.ParseWithClaims(data.tokenString, jwt.MapClaims{}, data.keyfunc)
|
||||||
|
|
||||||
|
if (err == nil) != data.valid || token.Valid != data.valid {
|
||||||
|
t.Errorf("[%v] Error Parsing Token with decoding padding set to %v: %v",
|
||||||
|
data.name,
|
||||||
|
data.paddedDecode,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
jwt.DecodePaddingAllowed = false
|
||||||
|
jwt.DecodeStrict = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkParseUnverified(b *testing.B) {
|
||||||
|
// Iterate over test data set and run tests
|
||||||
|
for _, data := range jwtTestData {
|
||||||
|
// If the token string is blank, use helper function to generate string
|
||||||
|
if data.tokenString == "" {
|
||||||
|
data.tokenString = signToken(data.claims, data.signingMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the token
|
||||||
|
parser := data.parser
|
||||||
|
if parser == nil {
|
||||||
|
parser = new(jwt.Parser)
|
||||||
|
}
|
||||||
|
// Figure out correct claims type
|
||||||
|
switch data.claims.(type) {
|
||||||
|
case jwt.MapClaims:
|
||||||
|
b.Run("map_claims", func(b *testing.B) {
|
||||||
|
benchmarkParsing(b, parser, data.tokenString, jwt.MapClaims{})
|
||||||
|
})
|
||||||
|
case *jwt.StandardClaims:
|
||||||
|
b.Run("standard_claims", func(b *testing.B) {
|
||||||
|
benchmarkParsing(b, parser, data.tokenString, &jwt.StandardClaims{})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method for benchmarking various parsing methods
|
||||||
|
func benchmarkParsing(b *testing.B, parser *jwt.Parser, tokenString string, claims jwt.Claims) {
|
||||||
|
b.Helper()
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
_, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method for benchmarking various signing methods
|
||||||
func benchmarkSigning(b *testing.B, method jwt.SigningMethod, key interface{}) {
|
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{}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
6
rsa.go
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
42
rsa_test.go
42
rsa_test.go
|
@ -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)
|
||||||
|
|
16
rsa_utils.go
16
rsa_utils.go
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
checks = ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1023"]
|
|
@ -0,0 +1,4 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIcaUjXhC7Mn2OonyfHF+zjblKkns
|
||||||
|
4GLbILnHrZr+aQwddiff5urCDAZ177t81Mn39CDs3uhlNDxfRIRheGnK/Q==
|
||||||
|
-----END PUBLIC KEY-----
|
|
@ -1,14 +1,15 @@
|
||||||
package test
|
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
|
||||||
|
}
|
||||||
|
|
85
token.go
85
token.go
|
@ -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)
|
|
||||||
for i := range parts {
|
|
||||||
var jsonValue []byte
|
var jsonValue []byte
|
||||||
if i == 0 {
|
|
||||||
if jsonValue, err = json.Marshal(t.Header); err != nil {
|
if jsonValue, err = json.Marshal(t.Header); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
} else {
|
header := EncodeSegment(jsonValue)
|
||||||
|
|
||||||
if jsonValue, err = json.Marshal(t.Claims); err != nil {
|
if jsonValue, err = json.Marshal(t.Claims); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
claim := EncodeSegment(jsonValue)
|
||||||
|
|
||||||
parts[i] = EncodeSegment(jsonValue)
|
return strings.Join([]string{header, claim}, "."), nil
|
||||||
}
|
|
||||||
return strings.Join(parts, "."), 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
package jwt_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.internal/re/jwt/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToken_SigningString(t1 *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Raw string
|
||||||
|
Method jwt.SigningMethod
|
||||||
|
Header map[string]interface{}
|
||||||
|
Claims jwt.Claims
|
||||||
|
Signature string
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "",
|
||||||
|
fields: fields{
|
||||||
|
Raw: "",
|
||||||
|
Method: jwt.SigningMethodHS256,
|
||||||
|
Header: map[string]interface{}{
|
||||||
|
"typ": "JWT",
|
||||||
|
"alg": jwt.SigningMethodHS256.Alg(),
|
||||||
|
},
|
||||||
|
Claims: jwt.StandardClaims{},
|
||||||
|
Signature: "",
|
||||||
|
Valid: false,
|
||||||
|
},
|
||||||
|
want: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t1.Run(tt.name, func(t1 *testing.T) {
|
||||||
|
t := &jwt.Token{
|
||||||
|
Raw: tt.fields.Raw,
|
||||||
|
Method: tt.fields.Method,
|
||||||
|
Header: tt.fields.Header,
|
||||||
|
Claims: tt.fields.Claims,
|
||||||
|
Signature: tt.fields.Signature,
|
||||||
|
Valid: tt.fields.Valid,
|
||||||
|
}
|
||||||
|
got, err := t.SigningString()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t1.Errorf("SigningString() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t1.Errorf("SigningString() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkToken_SigningString(b *testing.B) {
|
||||||
|
t := &jwt.Token{
|
||||||
|
Method: jwt.SigningMethodHS256,
|
||||||
|
Header: map[string]interface{}{
|
||||||
|
"typ": "JWT",
|
||||||
|
"alg": jwt.SigningMethodHS256.Alg(),
|
||||||
|
},
|
||||||
|
Claims: jwt.StandardClaims{},
|
||||||
|
}
|
||||||
|
b.Run("BenchmarkToken_SigningString", func(b *testing.B) {
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
t.SigningString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimePrecision sets the precision of times and dates within this library.
|
||||||
|
// This has an influence on the precision of times when comparing expiry or
|
||||||
|
// other related time fields. Furthermore, it is also the precision of times
|
||||||
|
// when serializing.
|
||||||
|
//
|
||||||
|
// For backwards compatibility the default precision is set to seconds, so that
|
||||||
|
// no fractional timestamps are generated.
|
||||||
|
var TimePrecision = time.Second
|
||||||
|
|
||||||
|
// MarshalSingleStringAsArray modifies the behaviour of the ClaimStrings type, especially
|
||||||
|
// its MarshalJSON function.
|
||||||
|
//
|
||||||
|
// If it is set to true (the default), it will always serialize the type as an
|
||||||
|
// array of strings, even if it just contains one element, defaulting to the behaviour
|
||||||
|
// of the underlying []string. If it is set to false, it will serialize to a single
|
||||||
|
// string, if it contains one element. Otherwise, it will serialize to an array of strings.
|
||||||
|
var MarshalSingleStringAsArray = true
|
||||||
|
|
||||||
|
// NumericDate represents a JSON numeric date value, as referenced at
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7519#section-2.
|
||||||
|
type NumericDate struct {
|
||||||
|
time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNumericDate constructs a new *NumericDate from a standard library time.Time struct.
|
||||||
|
// It will truncate the timestamp according to the precision specified in TimePrecision.
|
||||||
|
func NewNumericDate(t time.Time) *NumericDate {
|
||||||
|
return &NumericDate{t.Truncate(TimePrecision)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newNumericDateFromSeconds creates a new *NumericDate out of a float64 representing a
|
||||||
|
// UNIX epoch with the float fraction representing non-integer seconds.
|
||||||
|
func newNumericDateFromSeconds(f float64) *NumericDate {
|
||||||
|
round, frac := math.Modf(f)
|
||||||
|
return NewNumericDate(time.Unix(int64(round), int64(frac*1e9)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON is an implementation of the json.RawMessage interface and serializes the UNIX epoch
|
||||||
|
// represented in NumericDate to a byte array, using the precision specified in TimePrecision.
|
||||||
|
func (date NumericDate) MarshalJSON() (b []byte, err error) {
|
||||||
|
var prec int
|
||||||
|
if TimePrecision < time.Second {
|
||||||
|
prec = int(math.Log10(float64(time.Second) / float64(TimePrecision)))
|
||||||
|
}
|
||||||
|
truncatedDate := date.Truncate(TimePrecision)
|
||||||
|
|
||||||
|
// For very large timestamps, UnixNano would overflow an int64, but this
|
||||||
|
// function requires nanosecond level precision, so we have to use the
|
||||||
|
// following technique to get round the issue:
|
||||||
|
// 1. Take the normal unix timestamp to form the whole number part of the
|
||||||
|
// output,
|
||||||
|
// 2. Take the result of the Nanosecond function, which retuns the offset
|
||||||
|
// within the second of the particular unix time instance, to form the
|
||||||
|
// decimal part of the output
|
||||||
|
// 3. Concatenate them to produce the final result
|
||||||
|
seconds := strconv.FormatInt(truncatedDate.Unix(), 10)
|
||||||
|
nanosecondsOffset := strconv.FormatFloat(float64(truncatedDate.Nanosecond())/float64(time.Second), 'f', prec, 64)
|
||||||
|
|
||||||
|
output := append([]byte(seconds), []byte(nanosecondsOffset)[1:]...)
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON is an implementation of the json.RawMessage interface and deserializses a
|
||||||
|
// NumericDate from a JSON representation, i.e. a json.Number. This number represents an UNIX epoch
|
||||||
|
// with either integer or non-integer seconds.
|
||||||
|
func (date *NumericDate) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
var (
|
||||||
|
number json.Number
|
||||||
|
f float64
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = json.Unmarshal(b, &number); err != nil {
|
||||||
|
return fmt.Errorf("could not parse NumericData: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f, err = number.Float64(); err != nil {
|
||||||
|
return fmt.Errorf("could not convert json number value to float: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n := newNumericDateFromSeconds(f)
|
||||||
|
*date = *n
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClaimStrings is basically just a slice of strings, but it can be either serialized from a string array or just a string.
|
||||||
|
// This type is necessary, since the "aud" claim can either be a single string or an array.
|
||||||
|
type ClaimStrings []string
|
||||||
|
|
||||||
|
func (s *ClaimStrings) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
var value interface{}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(data, &value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var aud []string
|
||||||
|
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
aud = append(aud, v)
|
||||||
|
case []string:
|
||||||
|
aud = ClaimStrings(v)
|
||||||
|
case []interface{}:
|
||||||
|
for _, vv := range v {
|
||||||
|
vs, ok := vv.(string)
|
||||||
|
if !ok {
|
||||||
|
return &json.UnsupportedTypeError{Type: reflect.TypeOf(vv)}
|
||||||
|
}
|
||||||
|
aud = append(aud, vs)
|
||||||
|
}
|
||||||
|
case nil:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return &json.UnsupportedTypeError{Type: reflect.TypeOf(v)}
|
||||||
|
}
|
||||||
|
|
||||||
|
*s = aud
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ClaimStrings) MarshalJSON() (b []byte, err error) {
|
||||||
|
// This handles a special case in the JWT RFC. If the string array, e.g. used by the "aud" field,
|
||||||
|
// only contains one element, it MAY be serialized as a single string. This may or may not be
|
||||||
|
// desired based on the ecosystem of other JWT library used, so we make it configurable by the
|
||||||
|
// variable MarshalSingleStringAsArray.
|
||||||
|
if len(s) == 1 && !MarshalSingleStringAsArray {
|
||||||
|
return json.Marshal(s[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal([]string(s))
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
package jwt_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.internal/re/jwt/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNumericDate(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Iat jwt.NumericDate `json:"iat"`
|
||||||
|
Exp jwt.NumericDate `json:"exp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
oldPrecision := jwt.TimePrecision
|
||||||
|
|
||||||
|
jwt.TimePrecision = time.Microsecond
|
||||||
|
|
||||||
|
raw := `{"iat":1516239022.000000,"exp":1516239022.123450}`
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(raw), &s); err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := json.Marshal(s)
|
||||||
|
|
||||||
|
if raw != string(b) {
|
||||||
|
t.Errorf("Serialized format of numeric date mismatch. Expecting: %s Got: %s", string(raw), string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt.TimePrecision = oldPrecision
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSingleArrayMarshal(t *testing.T) {
|
||||||
|
jwt.MarshalSingleStringAsArray = false
|
||||||
|
|
||||||
|
s := jwt.ClaimStrings{"test"}
|
||||||
|
expected := `"test"`
|
||||||
|
|
||||||
|
b, err := json.Marshal(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected != string(b) {
|
||||||
|
t.Errorf("Serialized format of string array mismatch. Expecting: %s Got: %s", string(expected), string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt.MarshalSingleStringAsArray = true
|
||||||
|
|
||||||
|
expected = `["test"]`
|
||||||
|
|
||||||
|
b, err = json.Marshal(s)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected != string(b) {
|
||||||
|
t.Errorf("Serialized format of string array mismatch. Expecting: %s Got: %s", string(expected), string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNumericDate_MarshalJSON(t *testing.T) {
|
||||||
|
// Do not run this test in parallel because it's changing
|
||||||
|
// global state.
|
||||||
|
oldPrecision := jwt.TimePrecision
|
||||||
|
t.Cleanup(func() {
|
||||||
|
jwt.TimePrecision = oldPrecision
|
||||||
|
})
|
||||||
|
|
||||||
|
tt := []struct {
|
||||||
|
in time.Time
|
||||||
|
want string
|
||||||
|
precision time.Duration
|
||||||
|
}{
|
||||||
|
{time.Unix(5243700879, 0), "5243700879", time.Second},
|
||||||
|
{time.Unix(5243700879, 0), "5243700879.000", time.Millisecond},
|
||||||
|
{time.Unix(5243700879, 0), "5243700879.000000", time.Microsecond},
|
||||||
|
{time.Unix(5243700879, 0), "5243700879.000000000", time.Nanosecond},
|
||||||
|
//
|
||||||
|
{time.Unix(4239425898, 0), "4239425898", time.Second},
|
||||||
|
{time.Unix(4239425898, 0), "4239425898.000", time.Millisecond},
|
||||||
|
{time.Unix(4239425898, 0), "4239425898.000000", time.Microsecond},
|
||||||
|
{time.Unix(4239425898, 0), "4239425898.000000000", time.Nanosecond},
|
||||||
|
//
|
||||||
|
{time.Unix(253402271999, 0), "253402271999", time.Second},
|
||||||
|
{time.Unix(253402271999, 0), "253402271999.000", time.Millisecond},
|
||||||
|
{time.Unix(253402271999, 0), "253402271999.000000", time.Microsecond},
|
||||||
|
{time.Unix(253402271999, 0), "253402271999.000000000", time.Nanosecond},
|
||||||
|
//
|
||||||
|
{time.Unix(0, 1644285000210402000), "1644285000", time.Second},
|
||||||
|
{time.Unix(0, 1644285000210402000), "1644285000.210", time.Millisecond},
|
||||||
|
{time.Unix(0, 1644285000210402000), "1644285000.210402", time.Microsecond},
|
||||||
|
{time.Unix(0, 1644285000210402000), "1644285000.210402000", time.Nanosecond},
|
||||||
|
//
|
||||||
|
{time.Unix(0, 1644285315063096000), "1644285315", time.Second},
|
||||||
|
{time.Unix(0, 1644285315063096000), "1644285315.063", time.Millisecond},
|
||||||
|
{time.Unix(0, 1644285315063096000), "1644285315.063096", time.Microsecond},
|
||||||
|
{time.Unix(0, 1644285315063096000), "1644285315.063096000", time.Nanosecond},
|
||||||
|
// Maximum time that a go time.Time can represent
|
||||||
|
{time.Unix(math.MaxInt64, 999999999), "9223372036854775807", time.Second},
|
||||||
|
{time.Unix(math.MaxInt64, 999999999), "9223372036854775807.999", time.Millisecond},
|
||||||
|
{time.Unix(math.MaxInt64, 999999999), "9223372036854775807.999999", time.Microsecond},
|
||||||
|
{time.Unix(math.MaxInt64, 999999999), "9223372036854775807.999999999", time.Nanosecond},
|
||||||
|
// Strange precisions
|
||||||
|
{time.Unix(math.MaxInt64, 999999999), "9223372036854775807", time.Second},
|
||||||
|
{time.Unix(math.MaxInt64, 999999999), "9223372036854775756", time.Minute},
|
||||||
|
{time.Unix(math.MaxInt64, 999999999), "9223372036854774016", time.Hour},
|
||||||
|
{time.Unix(math.MaxInt64, 999999999), "9223372036854745216", 24 * time.Hour},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range tt {
|
||||||
|
jwt.TimePrecision = tc.precision
|
||||||
|
by, err := jwt.NewNumericDate(tc.in).MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := string(by); got != tc.want {
|
||||||
|
t.Errorf("[%d]: failed encoding: got %q want %q", i, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue