From 984734ee8dc49652a17f07277b4d90c403356683 Mon Sep 17 00:00:00 2001 From: "Pascal S. de Kloe" Date: Sun, 24 Mar 2019 18:12:13 +0100 Subject: [PATCH] Replace ErrAlgUnk with a more appropriate AlgError and drop the redundant ErrUnsecured. --- README.md | 28 ++++++++++++---------------- check.go | 10 +--------- check_test.go | 25 +++++++------------------ jwt.go | 12 +++++++++--- register.go | 8 ++++---- register_test.go | 4 ++-- sign.go | 8 ++++---- web_test.go | 26 +++++++++++++++++++++++--- 8 files changed, 62 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 99d808a..6e8dac4 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 @@ -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 @@ -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") @@ -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) diff --git a/check.go b/check.go index 6ec6bc7..ac0abaa 100644 --- a/check.go +++ b/check.go @@ -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. @@ -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 diff --git a/check_test.go b/check_test.go index 75b4a36..d24620e 100644 --- a/check_test.go +++ b/check_test.go @@ -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 { @@ -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) } } diff --git a/jwt.go b/jwt.go index d78edc2..01b3444 100644 --- a/jwt.go +++ b/jwt.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "encoding/json" "errors" + "strconv" "time" ) @@ -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") @@ -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.” diff --git a/register.go b/register.go index 03249e3..d578119 100644 --- a/register.go +++ b/register.go @@ -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 != "" { @@ -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 != "" { @@ -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 diff --git a/register_test.go b/register_test.go index f9d0267..51eba6c 100644 --- a/register_test.go +++ b/register_test.go @@ -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 diff --git a/sign.go b/sign.go index fad74ba..3dd0958 100644 --- a/sign.go +++ b/sign.go @@ -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) { @@ -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 @@ -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 @@ -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 diff --git a/web_test.go b/web_test.go index 83177e4..386f465 100644 --- a/web_test.go +++ b/web_test.go @@ -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) @@ -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 {