Skip to content

Commit

Permalink
Replace ErrAlgUnk with a more appropriate AlgError and drop the redun…
Browse files Browse the repository at this point in the history
…dant ErrUnsecured.
  • Loading branch information
pascaldekloe committed Mar 24, 2019
1 parent 82a3d18 commit 984734e
Show file tree
Hide file tree
Showing 8 changed files with 62 additions and 59 deletions.
28 changes: 12 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

A JSON Web Token (JWT) library for the Go programming language.

The API enforces secure use by design. Unsigned tokens are
[rejected](https://godoc.org/github.com/pascaldekloe/jwt#ErrUnsecured)
and there is no support for encryption—use wire encryption instead.
The API enforces secure use by design. Unsigned tokens are rejected
and no support for encrypted tokens—use wire encryption instead.

* Compact implementation
* No third party dependencies
* Full unit test coverage

Expand All @@ -16,7 +16,7 @@ This is free and unencumbered software released into the

## Get Started

The package comes with functions to verify and issue claims.
The package comes with functions to issue and verify claims.

```go
// create a JWT
Expand All @@ -29,11 +29,7 @@ The register helps with key migrations and fallback scenarios.

```go
var keys jwt.KeyRegister
keyCount, err := keys.LoadPEM(text, nil)
if err != nil {
log.Fatal("JWT key import: ", err)
}
log.Print(keyCount, " JWT key(s) ready")
_, err := keys.LoadPEM(text, nil)
```

```go
Expand All @@ -50,13 +46,13 @@ if !claims.Valid(time.Now()) {
log.Print("hello ", claims.Audiences)
```

For server side security an `http.Handler` based setup can be used as well.
For server side security, an `http.Handler` based setup can be used as well.
The following example enforces the subject, formatted name and roles to be
present as a valid JWT in all requests towards the `MyAPI` handler.
present as a valid JWT in all requests towards `MyAPI`.

```go
http.DefaultServeMux.Handle("/api/v1", &jwt.Handler{
Target: MyAPI, // the protected service multiplexer
http.Handle("/api/v1", &jwt.Handler{
Target: MyAPI, // the protected handler
RSAKey: JWTPublicKey,

// map some claims to HTTP headers
Expand All @@ -67,7 +63,7 @@ http.DefaultServeMux.Handle("/api/v1", &jwt.Handler{

// customise further with RBAC
Func: func(w http.ResponseWriter, req *http.Request, claims *jwt.Claims) (pass bool) {
log.Printf("got a valid JWT %q for %q", claims.ID, claims.Audience)
log.Printf("got a valid JWT %q for %q", claims.ID, claims.Audiences)

// map role enumeration
s, ok := claims.String("roles")
Expand All @@ -93,8 +89,8 @@ func Greeting(w http.ResponseWriter, req *http.Request) {
}
```

Optionally one can use the claims object from the HTTP request as shown in the
[“context” example](https://godoc.org/github.com/pascaldekloe/jwt#example-Handler--Context).
The parsed claims are also available from the HTTP
[request context](https://godoc.org/github.com/pascaldekloe/jwt#example-Handler--Context).


### Performance on a Mac Pro (late 2013)
Expand Down
10 changes: 1 addition & 9 deletions check.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ import (
// ErrSigMiss means the signature check failed.
var ErrSigMiss = errors.New("jwt: signature mismatch")

// ErrUnsecured signals the "none" algorithm.
var ErrUnsecured = errors.New("jwt: unsecured—no signature")

var errPart = errors.New("jwt: missing base64 part")

// ECDSACheck parses a JWT if, and only if, the signature checks out.
Expand Down Expand Up @@ -146,15 +143,10 @@ type header struct {
}

func (h *header) match(algs map[string]crypto.Hash) (crypto.Hash, error) {
// why would anyone do this?
if h.Alg == "none" {
return 0, ErrUnsecured
}

// availability check
hash, ok := algs[h.Alg]
if !ok {
return 0, ErrAlgUnk
return 0, AlgError(h.Alg)
}
if !hash.Available() {
return 0, errHashLink
Expand Down
25 changes: 7 additions & 18 deletions check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,21 +132,6 @@ func TestCheckMiss(t *testing.T) {
}
}

func TestCheckAlgWrong(t *testing.T) {
_, err := ECDSACheck([]byte(goldenRSAs[0].token), nil)
if err != ErrAlgUnk {
t.Errorf("RSA alg for ECDSA got error %v, want %v", err, ErrAlgUnk)
}
_, err = HMACCheck([]byte(goldenRSAs[0].token), nil)
if err != ErrAlgUnk {
t.Errorf("RSA alg for HMAC got error %v, want %v", err, ErrAlgUnk)
}
_, err = RSACheck([]byte(goldenHMACs[0].token), &testKeyRSA1024.PublicKey)
if err != ErrAlgUnk {
t.Errorf("HMAC alg for RSA got error %v, want %v", err, ErrAlgUnk)
}
}

func TestCheckHashNotLinked(t *testing.T) {
alg := "HB2b256"
if _, ok := HMACAlgs[alg]; ok {
Expand Down Expand Up @@ -189,11 +174,15 @@ func TestErrPart(t *testing.T) {
}
}

func TestErrUnsecured(t *testing.T) {
func TestRejectNone(t *testing.T) {
// example from RFC 7519, subsection 6.1.
const token = "eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ."
if _, err := HMACCheck([]byte(token), nil); err != ErrUnsecured {
t.Errorf("unsecured got error %v", err)
_, err := HMACCheck([]byte(token), nil)
if err == nil {
t.Fatal("no error")
}
if want := `jwt: algorithm "none" not in use`; err.Error() != want {
t.Errorf("got error %v, want %s", err, want)
}
}

Expand Down
12 changes: 9 additions & 3 deletions jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"strconv"
"time"
)

Expand Down Expand Up @@ -55,9 +56,6 @@ var (
}
)

// ErrAlgUnk means that the specified "alg" is not in use.
var ErrAlgUnk = errors.New("jwt: algorithm unknown")

// See crypto.Hash.Available.
var errHashLink = errors.New("jwt: hash function not linked into binary")

Expand Down Expand Up @@ -248,6 +246,14 @@ func (c *Claims) Number(name string) (value float64, ok bool) {
return
}

// AlgError signals that the specified algorithm is not in use.
type AlgError string

// Error honnors the error interface.
func (e AlgError) Error() string {
return "jwt: algorithm " + strconv.Quote(string(e)) + " not in use"
}

// NumericTime, named NumericDate, is “A JSON numeric value representing
// the number of seconds from 1970-01-01T00:00:00Z UTC until the specified
// UTC date/time, ignoring leap seconds.”
Expand Down
8 changes: 4 additions & 4 deletions register.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (keys *KeyRegister) Check(token []byte) (*Claims, error) {
return nil, err
}

switch hash, err := header.match(HMACAlgs); err {
switch hash, err := header.match(HMACAlgs); err.(type) {
case nil:
keyOptions := keys.Secrets
if header.Kid != "" {
Expand All @@ -54,13 +54,13 @@ func (keys *KeyRegister) Check(token []byte) (*Claims, error) {
}
return nil, ErrSigMiss

case ErrAlgUnk:
case AlgError:
break // next
default:
return nil, err
}

switch hash, err := header.match(RSAAlgs); err {
switch hash, err := header.match(RSAAlgs); err.(type) {
case nil:
keyOptions := keys.RSAs
if header.Kid != "" {
Expand All @@ -87,7 +87,7 @@ func (keys *KeyRegister) Check(token []byte) (*Claims, error) {
}
return nil, ErrSigMiss

case ErrAlgUnk:
case AlgError:
break // next
default:
return nil, err
Expand Down
4 changes: 2 additions & 2 deletions register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,8 @@ P9j/1Whc92wzd4Osod3U6Tw9g+C1LuHuHOoLJhj5nUQQcP8UQk6jzKPwr4L4uKAc
// check unsupported algorithm
const encryptedToken = "eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.QR1Owv2ug2WyPBnbQrRARTeEk9kDO2w8qDcjiHnSJflSdv1iNqhWXaKH4MqAkQtMoNfABIPJaZm0HaA415sv3aeuBWnD8J-Ui7Ah6cWafs3ZwwFKDFUUsWHSK-IPKxLGTkND09XyjORj_CHAgOPJ-Sd8ONQRnJvWn_hXV1BNMHzUjPyYwEsRhDhzjAD26imasOTsgruobpYGoQcXUwFDn7moXPRfDE8-NoQX7N7ZYMmpUDkR-Cx9obNGwJQ3nM52YCitxoQVPzjbl7WBuB7AohdBoZOdZ24WlN1lVIeh8v1K4krB8xgKvRU8kgFrEn_a1rZgN5TiysnmzTROF869lQ.AxY8DCtDaGlsbGljb3RoZQ.MKOle7UQrG6nSxTLX6Mqwt0orbHvAKeWnDYvpIAeZ72deHxz3roJDXQyhxx0wKaMHDjUEOKIwrtkHthpqEanSBNYHZgmNOV7sln1Eu9g3J8.fiK51VwhsxJ-siBMR-YFiA"
_, err = keys.Check([]byte(encryptedToken))
if err != ErrAlgUnk {
t.Errorf("encrypted token got error %q, want %q", err, ErrAlgUnk)
if want := AlgError("RSA1_5"); err != want {
t.Errorf("encrypted token got error %v, want %v", err, want)
}

// check golden cases
Expand Down
8 changes: 4 additions & 4 deletions sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

// ECDSASign updates the Raw field and returns a new JWT.
// When the algorithm is not in ECDSAAlgs, then the error is ErrAlgUnk.
// The return is an AlgError when alg is not in ECDSAAlgs.
// The caller must use the correct key for the respective algorithm (P-256 for
// ES256, P-384 for ES384 and P-521 for ES512) or risk malformed token production.
func (c *Claims) ECDSASign(alg string, key *ecdsa.PrivateKey) (token []byte, err error) {
Expand Down Expand Up @@ -48,7 +48,7 @@ func (c *Claims) ECDSASign(alg string, key *ecdsa.PrivateKey) (token []byte, err
}

// HMACSign updates the Raw field and returns a new JWT.
// When the algorithm is not in HMACAlgs, then the error is ErrAlgUnk.
// The return is an AlgError when alg is not in HMACAlgs.
func (c *Claims) HMACSign(alg string, secret []byte) (token []byte, err error) {
if err := c.sync(); err != nil {
return nil, err
Expand All @@ -69,7 +69,7 @@ func (c *Claims) HMACSign(alg string, secret []byte) (token []byte, err error) {
}

// RSASign updates the Raw field and returns a new JWT.
// When the algorithm is not in RSAAlgs, then the error is ErrAlgUnk.
// The return is an AlgError when alg is not in RSAAlgs.
func (c *Claims) RSASign(alg string, key *rsa.PrivateKey) (token []byte, err error) {
if err := c.sync(); err != nil {
return nil, err
Expand Down Expand Up @@ -118,7 +118,7 @@ func (c *Claims) newTokenNoSig(encHeader string, encSigLen int, digest hash.Hash
// FormatHeader encodes the JOSE header and validates the hash.
func (c *Claims) formatHeader(alg string, hash crypto.Hash) (encHeader string, err error) {
if hash == 0 {
return "", ErrAlgUnk
return "", AlgError(alg)
}
if !hash.Available() {
return "", errHashLink
Expand Down
26 changes: 23 additions & 3 deletions web_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"testing"
)

func TestCheckHeaderPass(t *testing.T) {
func TestCheckHeader(t *testing.T) {
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -79,13 +79,33 @@ func TestCheckHeaderSchema(t *testing.T) {
}
}

func TestSignHeaderErrPass(t *testing.T) {
func TestCheckHeaderError(t *testing.T) {
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
// example from RFC 7519, subsection 6.1.
req.Header.Set("Authorization", "Bearer eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.")
want := AlgError("none")

if _, err := ECDSACheckHeader(req, &testKeyEC256.PublicKey); err != want {
t.Errorf("ECDSA got error %v, want %v", err, want)
}
if _, err := HMACCheckHeader(req, nil); err != want {
t.Errorf("HMAC got error %v, want %v", err, want)
}
if _, err := RSACheckHeader(req, &testKeyRSA1024.PublicKey); err != want {
t.Errorf("RSA got error %v, want %v", err, want)
}
}

func TestSignHeaderError(t *testing.T) {
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
unknownAlg := "doesnotexist"
want := ErrAlgUnk
want := AlgError(unknownAlg)

c := new(Claims)
if err := c.ECDSASignHeader(req, unknownAlg, testKeyEC256); err != want {
Expand Down

0 comments on commit 984734e

Please sign in to comment.