Skip to content

Commit

Permalink
add key ids helper and replace travis with github actions
Browse files Browse the repository at this point in the history
  • Loading branch information
kataras committed Mar 3, 2021
1 parent eb8757e commit 1639fcf
Show file tree
Hide file tree
Showing 17 changed files with 193 additions and 42 deletions.
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# These owners will be the default owners for everything in the repo.
* @kataras
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github: kataras
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:

test:
name: Test
runs-on: ubuntu-latest

strategy:
matrix:
go_version: [1.16]
steps:

- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go_version }}

- name: Check out code into the Go module directory
uses: actions/checkout@v2

- name: Test
run: go test -v --race ./...
17 changes: 0 additions & 17 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# JWT

[![build status](https://img.shields.io/travis/kataras/jwt/main.svg?style=for-the-badge&logo=travis)](https://travis-ci.org/github/kataras/jwt) [![gocov](https://img.shields.io/badge/Go%20Coverage-92%25-brightgreen.svg?style=for-the-badge)](https://travis-ci.org/github/kataras/jwt/jobs/740739405#L322) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=for-the-badge)](https://goreportcard.com/report/github.com/kataras/jwt) [![godocs](https://img.shields.io/badge/go-%20docs-488AC7.svg?style=for-the-badge)](https://pkg.go.dev/github.com/kataras/jwt)
[![build status](https://img.shields.io/github/workflow/status/kataras/jwt/CI/main?style=for-the-badge)](https://github.com/kataras/jwt/actions) [![gocov](https://img.shields.io/badge/Go%20Coverage-92%25-brightgreen.svg?style=for-the-badge)](https://travis-ci.org/github/kataras/jwt/jobs/740739405#L322) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=for-the-badge)](https://goreportcard.com/report/github.com/kataras/jwt) [![godocs](https://img.shields.io/badge/go-%20docs-488AC7.svg?style=for-the-badge)](https://pkg.go.dev/github.com/kataras/jwt)

Fast and simple [JWT](https://jwt.io/#libraries-io) implementation written in [Go](https://golang.org/dl). This package was designed with security, performance and simplicity in mind, it protects your tokens from [critical vulnerabilities that you may find in other libraries](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries).

Expand Down
2 changes: 2 additions & 0 deletions _examples/custom-header/main.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package main

// Check the https://github.com/kataras/jwt/blob/main/kid_keys.go too.

import (
"fmt"
"log"
Expand Down
4 changes: 2 additions & 2 deletions alg.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (

var (
// ErrTokenSignature indicates that the verification failed.
ErrTokenSignature = errors.New("invalid token signature")
ErrTokenSignature = errors.New("jwt: invalid token signature")
// ErrInvalidKey indicates that an algorithm required secret key is not a valid type.
ErrInvalidKey = errors.New("invalid key")
ErrInvalidKey = errors.New("jwt: invalid key")
)

// Alg represents a signing and verifying algorithm.
Expand Down
2 changes: 1 addition & 1 deletion blocklist.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

// ErrBlocked indicates that the token has not yet expired
// but was blocked by the server's Blocklist.
var ErrBlocked = errors.New("token is blocked")
var ErrBlocked = errors.New("jwt: token is blocked")

// Blocklist is an in-memory storage of tokens that should be
// immediately invalidated by the server-side.
Expand Down
6 changes: 3 additions & 3 deletions claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import (

var (
// ErrExpired indicates that token is used after expiry time indicated in "exp" claim.
ErrExpired = errors.New("token expired")
ErrExpired = errors.New("jwt: token expired")
// ErrNotValidYet indicates that token is used before time indicated in "nbf" claim.
ErrNotValidYet = errors.New("token not valid yet")
ErrNotValidYet = errors.New("jwt: token not valid yet")
// ErrIssuedInTheFuture indicates that the "iat" claim is in the future.
ErrIssuedInTheFuture = errors.New("token issued in the future")
ErrIssuedInTheFuture = errors.New("jwt: token issued in the future")
)

// Claims holds the standard JWT claims (payload fields).
Expand Down
2 changes: 1 addition & 1 deletion expected.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ var _ TokenValidator = Expected{}
// if errors.Is(ErrExpected, err) {
//
// }
var ErrExpected = errors.New("field not match")
var ErrExpected = errors.New("jwt: field not match")

// ValidateToken completes the TokenValidator interface.
// It performs simple checks against the expected "e" and the verified "c" claims.
Expand Down
2 changes: 1 addition & 1 deletion gcm.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

// ErrDecrypt indicates a failure on payload decryption.
var ErrDecrypt = errors.New("decrypt: payload authentication failed")
var ErrDecrypt = errors.New("jwt: decrypt: payload authentication failed")

// GCM sets the `Encrypt` and `Decrypt` package-level functions
// to provide encryption over the token's payload on Sign and decryption on Verify
Expand Down
117 changes: 117 additions & 0 deletions kid_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package jwt

import "errors"

var (
// ErrEmptyKid fires when the header is missing a "kid" field.
ErrEmptyKid = errors.New("jwt: kid is empty")
// ErrUnknownKid fires when the header has a "kid" field
// but does not match with any of the registered ones.
ErrUnknownKid = errors.New("jwt: unknown kid")
)

type (
// HeaderWithKid represents a simple header part which
// holds the "kid" and "alg" fields.
HeaderWithKid struct {
Kid string `json:"kid"`
Alg string `json:"alg"`
}

// Key holds the Go parsed key pairs.
// This package has all the helpers you need to parse
// a file or a string to go crypto keys,
// e.g. `ParsePublicKeyRSA` and `ParsePrivateKeyRSA` package-level functions.
Key struct {
ID string
Alg Alg
Public PublicKey
Private PrivateKey
}

// Keys is a map which holds the key id and a key pair.
// User should initialize the keys once, not safe for concurrent writes.
// See its `SignToken`, `VerifyToken` and `ValidateHeader` methods.
// Usage:
// var keys jwt.Keys
// keys.Register("api", jwt.RS256, apiPubKey, apiPrivKey)
// keys.Register("cognito", jwt.RS256, cognitoPubKey, nil)
// ...
// token, err := keys.SignToken("api", myClaims{...}, jwt.MaxAge(15*time.Minute))
// ...
// var c myClaims
// err := keys.VerifyToken("api", token, &myClaims)
// }
Keys map[string]*Key
)

// Get returns the key based on its id.
func (keys Keys) Get(kid string) (*Key, bool) {
k, ok := keys[kid]
return k, ok
}

// Register registers a keypair to a unique identifier per key.
func (keys Keys) Register(alg Alg, kid string, pubKey PublicKey, privKey PrivateKey) {
keys[kid] = &Key{
ID: kid,
Alg: alg,
Public: pubKey,
Private: privKey,
}
}

// ValidateHeader validates the given json header value (base64 decoded) based on the "keys".
// Keys structure completes the `HeaderValidator` interface.
func (keys Keys) ValidateHeader(alg string, headerDecoded []byte) (Alg, PublicKey, error) {
var h HeaderWithKid

err := Unmarshal(headerDecoded, &h)
if err != nil {
return nil, nil, err
}

if h.Kid == "" {
return nil, nil, ErrEmptyKid
}

key, ok := keys.Get(h.Kid)
if !ok {
return nil, nil, ErrUnknownKid
}

if h.Alg != key.Alg.Name() {
return nil, nil, ErrTokenAlg
}

// If for some reason a specific alg was given by the caller then check that as well.
if alg != "" && alg != h.Alg {
return nil, nil, ErrTokenAlg
}

return key.Alg, key.Public, nil
}

// SignToken signs the "claims" using the given "alg" based a specific key.
func (keys Keys) SignToken(kid string, claims interface{}, opts ...SignOption) ([]byte, error) {
k, ok := keys.Get(kid)
if !ok {
return nil, ErrUnknownKid
}

return SignWithHeader(k.Alg, k.Private, claims, HeaderWithKid{
Kid: kid,
Alg: k.Alg.Name(),
}, opts...)
}

// VerifyToken verifies the "token" using the given "alg" based on the registered public key(s)
// and sets the custom claims to the destination "claimsPtr".
func (keys Keys) VerifyToken(token []byte, claimsPtr interface{}, validators ...TokenValidator) error {
verifiedToken, err := VerifyWithHeaderValidator(nil, nil, token, keys.ValidateHeader, validators...)
if err != nil {
return err
}

return verifiedToken.Claims(&claimsPtr)
}
2 changes: 1 addition & 1 deletion required.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

// ErrMissingKey when token does not contain a required JSON field.
// Check with errors.Is.
var ErrMissingKey = errors.New("token is missing a required field")
var ErrMissingKey = errors.New("jwt: token is missing a required field")

// HasRequiredJSONTag reports whether a specific value of "i"
// contains one or more `json:"xxx,required"` struct fields tags.
Expand Down
3 changes: 3 additions & 0 deletions sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ func signToken(alg Alg, key PrivateKey, encrypt InjectFunc, claims interface{},
if len(opts) > 0 {
var standardClaims Claims
for _, opt := range opts {
if opt == nil {
continue
}
opt.ApplyClaims(&standardClaims)
}

Expand Down
40 changes: 27 additions & 13 deletions token.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import (

var (
// ErrMissing indicates that a given token to `Verify` is empty.
ErrMissing = errors.New("token is empty")
ErrMissing = errors.New("jwt: token is empty")
// ErrTokenForm indicates that the extracted token has not the expected form .
ErrTokenForm = errors.New("invalid token form")
ErrTokenForm = errors.New("jwt: invalid token form")
// ErrTokenAlg indicates that the given algorithm does not match the extracted one.
ErrTokenAlg = errors.New("unexpected token algorithm")
ErrTokenAlg = errors.New("jwt: unexpected token algorithm")
)

type (
// PrivateKey is a generic type, this key is responsible for signing the token.
PrivateKey interface{}
PrivateKey = interface{}
// PublicKey is a generic type, this key is responsible to verify the token.
PublicKey interface{}
PublicKey = interface{}
)

func encodeToken(alg Alg, key PrivateKey, payload []byte, customHeader interface{}) ([]byte, error) {
Expand Down Expand Up @@ -78,11 +78,22 @@ func decodeToken(alg Alg, key PublicKey, token []byte, compareHeaderFunc HeaderV
compareHeaderFunc = CompareHeader
}

pubKey, err := compareHeaderFunc(alg.Name(), headerDecoded)
// algorithm can be specified hard-coded
// or extracted per token if a custom header validator given.
algName := ""
if alg != nil {
algName = alg.Name()
}

dynamicAlg, pubKey, err := compareHeaderFunc(algName, headerDecoded)
if err != nil {
return nil, nil, nil, err
}

if alg == nil {
alg = dynamicAlg
}

// Override the key given, which could be a nil if this "pubKey" always expected on success.
if pubKey != nil {
key = pubKey
Expand Down Expand Up @@ -187,16 +198,19 @@ func createHeaderReversed(alg string) []byte {

// HeaderValidator is a function which can be used to customize how the header is validated,
// by default it makes sure the algorithm is the same as the "alg" field.
//
// If the "alg" is empty then this function should return a non-nil algorithm
// based on the token contents.
// It should return a nil PublicKey and a non-nil error on validation failure.
// On success, if public key is not nil then it overrides the VerifyXXX method's one.
type HeaderValidator func(alg string, headerDecoded []byte) (PublicKey, error)
type HeaderValidator func(alg string, headerDecoded []byte) (Alg, PublicKey, error)

// Note that this check is fully hard coded for known
// algorithms and it is fully hard coded in terms of
// its serialized format.
func compareHeader(alg string, headerDecoded []byte) (PublicKey, error) {
func compareHeader(alg string, headerDecoded []byte) (Alg, PublicKey, error) {
if len(headerDecoded) < 25 /* 28 but allow custom short algs*/ {
return nil, ErrTokenAlg
return nil, nil, ErrTokenAlg
}

// Fast check if the order is reversed.
Expand All @@ -206,18 +220,18 @@ func compareHeader(alg string, headerDecoded []byte) (PublicKey, error) {
if headerDecoded[2] == 't' {
expectedHeader := createHeaderReversed(alg)
if !bytes.Equal(expectedHeader, headerDecoded) {
return nil, ErrTokenAlg
return nil, nil, ErrTokenAlg
}

return nil, nil
return nil, nil, nil
}

expectedHeader := createHeaderRaw(alg)
if !bytes.Equal(expectedHeader, headerDecoded) {
return nil, ErrTokenAlg
return nil, nil, ErrTokenAlg
}

return nil, nil
return nil, nil, nil
}

func createSignature(alg Alg, key PrivateKey, headerAndPayload []byte) ([]byte, error) {
Expand Down
2 changes: 1 addition & 1 deletion token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func TestCompareHeader(t *testing.T) {
}

for i, tt := range tests {
_, err := compareHeader(tt.alg, []byte(tt.header))
_, _, err := compareHeader(tt.alg, []byte(tt.header))
if tt.ok && err != nil {
t.Fatalf("[%d] expected to pass but got error: %v", i, err)
}
Expand Down
2 changes: 1 addition & 1 deletion verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func (t *VerifiedToken) Claims(dest interface{}) error {
return Unmarshal(t.Payload, dest)
}

var errPayloadNotJSON = errors.New("payload is not a type of JSON") // malformed JSON or it's not a JSON at all.
var errPayloadNotJSON = errors.New("jwt: payload is not a type of JSON") // malformed JSON or it's not a JSON at all.

// Plain can be provided as a Token Validator at `Verify` and `VerifyEncrypted` functions
// to allow tokens with plain payload (no JSON or malformed JSON) to be successfully validated.
Expand Down

0 comments on commit 1639fcf

Please sign in to comment.