diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..589fa54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode +debug.test diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 7510ba9..b9e9527 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -247,7 +247,7 @@ func TestJWTAuthorizationHandler(t *testing.T) { req := restaudit.NewRequest("GET", "/jwt/"+test.id+"/1234567890") if test.tokener != nil { req.SetRequestProcessor(func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, test.tokener()) + return jwt.AddToRequest(req, test.tokener()) }) } // Make request(s). diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index 4d5b2cb..ba22855 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -142,7 +142,7 @@ func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { } // Now do the checks. if err != nil { - return h.deny(job, rest.StatusBadRequest, err.Error()) + return h.deny(job, rest.StatusUnauthorized, err.Error()) } if token == nil { return h.deny(job, rest.StatusUnauthorized, "no JSON Web Token") diff --git a/jwt/errors.go b/jwt/errors.go index 1d1c51f..862f4da 100644 --- a/jwt/errors.go +++ b/jwt/errors.go @@ -39,27 +39,31 @@ const ( ErrNoECDSAKey ErrCannotParseRSA ErrNoRSAKey + ErrNoAuthorizationHeader + ErrInvalidAuthorizationHeader ) var errorMessages = errors.Messages{ - ErrCannotEncode: "cannot encode the %s", - ErrCannotDecode: "cannot decode the %s", - ErrCannotSign: "cannot sign the token", - ErrCannotVerify: "cannot verify the %s", - ErrNoKey: "no key available, only after encoding or verifying", - ErrJSONMarshalling: "error marshalling to JSON", - ErrJSONUnmarshalling: "error unmarshalling from JSON", - ErrInvalidTokenPart: "part of the token contains invalid data", - ErrInvalidCombination: "invalid combination of algorithm %q and key type %q", - ErrInvalidAlgorithm: "signature algorithm %q is invalid", - ErrInvalidKeyType: "key type %T is invalid", - ErrInvalidSignature: "token signature is invalid", - ErrCannotReadPEM: "cannot read the PEM", - ErrCannotDecodePEM: "cannot decode the PEM", - ErrCannotParseECDSA: "cannot parse the ECDSA", - ErrNoECDSAKey: "passed key is no ECDSA key", - ErrCannotParseRSA: "cannot parse the RSA", - ErrNoRSAKey: "passed key is no RSA key", + ErrCannotEncode: "cannot encode the %s", + ErrCannotDecode: "cannot decode the %s", + ErrCannotSign: "cannot sign the token", + ErrCannotVerify: "cannot verify the %s", + ErrNoKey: "no key available, only after encoding or verifying", + ErrJSONMarshalling: "error marshalling to JSON", + ErrJSONUnmarshalling: "error unmarshalling from JSON", + ErrInvalidTokenPart: "part of the token contains invalid data", + ErrInvalidCombination: "invalid combination of algorithm %q and key type %q", + ErrInvalidAlgorithm: "signature algorithm %q is invalid", + ErrInvalidKeyType: "key type %T is invalid", + ErrInvalidSignature: "token signature is invalid", + ErrCannotReadPEM: "cannot read the PEM", + ErrCannotDecodePEM: "cannot decode the PEM", + ErrCannotParseECDSA: "cannot parse the ECDSA", + ErrNoECDSAKey: "passed key is no ECDSA key", + ErrCannotParseRSA: "cannot parse the RSA", + ErrNoRSAKey: "passed key is no RSA key", + ErrNoAuthorizationHeader: "request contains no authorization header", + ErrInvalidAuthorizationHeader: "invalid authorization header: '%s'", } // EOF diff --git a/jwt/header.go b/jwt/header.go index ed671ca..bfc9345 100644 --- a/jwt/header.go +++ b/jwt/header.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - JSON Web Token - Header // -// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2016-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. @@ -15,61 +15,77 @@ import ( "net/http" "strings" + "github.com/tideland/golib/errors" + "github.com/tideland/gorest/rest" ) //-------------------- -// JOB AND REQUEST HANDLING +// REQUEST AND JOB HANDLING //-------------------- +// AddTokenToRequest adds a token as header to a request for +// usage by a client. +// +// DEPRECATED: Now AddToRequest(). +func AddTokenToRequest(req *http.Request, jwt JWT) *http.Request { + return AddToRequest(req, jwt) +} + +// AddToRequest adds a token as header to a request for +// usage by a client. +func AddToRequest(req *http.Request, jwt JWT) *http.Request { + req.Header.Add("Authorization", "Bearer "+jwt.String()) + return req +} + +// DecodeFromRequest tries to retrieve a token from a request +// header. +func DecodeFromRequest(req *http.Request) (JWT, error) { + return decodeFromRequest(req, nil, nil) +} + // DecodeFromJob retrieves a possible JWT from // the request inside a REST job. The JWT is only decoded. func DecodeFromJob(job rest.Job) (JWT, error) { - return retrieveFromJob(job, nil, nil) + return decodeFromRequest(job.Request(), nil, nil) } // DecodeCachedFromJob retrieves a possible JWT from the request // inside a REST job and checks if it already is cached. The JWT is // only decoded. In case of no error the token is added to the cache. func DecodeCachedFromJob(job rest.Job, cache Cache) (JWT, error) { - return retrieveFromJob(job, cache, nil) + return decodeFromRequest(job.Request(), cache, nil) } // VerifyFromJob retrieves a possible JWT from // the request inside a REST job. The JWT is verified. func VerifyFromJob(job rest.Job, key Key) (JWT, error) { - return retrieveFromJob(job, nil, key) + return decodeFromRequest(job.Request(), nil, key) } // VerifyCachedFromJob retrieves a possible JWT from the request // inside a REST job and checks if it already is cached. The JWT is // verified. In case of no error the token is added to the cache. func VerifyCachedFromJob(job rest.Job, cache Cache, key Key) (JWT, error) { - return retrieveFromJob(job, cache, key) -} - -// AddTokenToRequest adds a token as header to a request for -// usage by a client. -func AddTokenToRequest(req *http.Request, jwt JWT) *http.Request { - req.Header.Add("Authorization", "Bearer "+jwt.String()) - return req + return decodeFromRequest(job.Request(), cache, key) } //-------------------- // PRIVATE HELPERS //-------------------- -// retrieveFromJob is the generic retrieval function with possible -// caching and verifaction. -func retrieveFromJob(job rest.Job, cache Cache, key Key) (JWT, error) { +// decodeFromRequest is the generic decoder with possible +// caching and verification. +func decodeFromRequest(req *http.Request, cache Cache, key Key) (JWT, error) { // Retrieve token from header. - authorization := job.Request().Header.Get("Authorization") + authorization := req.Header.Get("Authorization") if authorization == "" { - return nil, nil + return nil, errors.New(ErrNoAuthorizationHeader, errorMessages) } fields := strings.Fields(authorization) if len(fields) != 2 || fields[0] != "Bearer" { - return nil, nil + return nil, errors.New(ErrInvalidAuthorizationHeader, errorMessages, authorization) } // Check cache. if cache != nil { diff --git a/jwt/header_test.go b/jwt/header_test.go index 94ecdf1..fd3986f 100644 --- a/jwt/header_test.go +++ b/jwt/header_test.go @@ -29,6 +29,27 @@ import ( // TESTS //-------------------- +// TestDecodeInvalidRequest tests the decoding of requests +// without a header or an invalid one. +func TestDecodeInvalidRequest(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing decode invalid requests") + // Setup the test server. + mux := newMultiplexer(assert) + ts := restaudit.StartServer(mux, assert) + defer ts.Close() + asserter := newHeaderAsserter(assert, ".* request contains no authorization header") + err := mux.Register("test", "jwt", newTestHandler("jwt", asserter)) + assert.Nil(err) + // Perform request without authorization. + req := restaudit.NewRequest("GET", "/test/jwt/1234567890") + req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) + resp := ts.DoRequest(req) + ok := "" + resp.AssertUnmarshalledBody(&ok) + assert.Equal(ok, "OK") +} + // TestDecodeRequest tests the decoding of a token // in a handler. func TestDecodeRequest(t *testing.T) { @@ -42,13 +63,14 @@ func TestDecodeRequest(t *testing.T) { mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() - err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, nil, false)) + asserter := newDecodeAsserter(assert, false) + err = mux.Register("test", "jwt", newTestHandler("jwt", asserter)) assert.Nil(err) // Perform test request. req := restaudit.NewRequest("GET", "/test/jwt/1234567890") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) req.SetRequestProcessor(func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwtIn) + return jwt.AddToRequest(req, jwtIn) }) resp := ts.DoRequest(req) claimsOut := jwt.Claims{} @@ -69,13 +91,14 @@ func TestDecodeCachedRequest(t *testing.T) { mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() - err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, nil, true)) + asserter := newDecodeAsserter(assert, true) + err = mux.Register("test", "jwt", newTestHandler("jwt", asserter)) assert.Nil(err) // Perform first test request. req := restaudit.NewRequest("GET", "/test/jwt/1234567890") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) req.SetRequestProcessor(func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwtIn) + return jwt.AddToRequest(req, jwtIn) }) resp := ts.DoRequest(req) claimsOut := jwt.Claims{} @@ -101,13 +124,14 @@ func TestVerifyRequest(t *testing.T) { mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() - err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, key, false)) + asserter := newVerifyAsserter(assert, key, false) + err = mux.Register("test", "jwt", newTestHandler("jwt", asserter)) assert.Nil(err) // Perform test request. req := restaudit.NewRequest("GET", "/test/jwt/1234567890") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) req.SetRequestProcessor(func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwtIn) + return jwt.AddToRequest(req, jwtIn) }) resp := ts.DoRequest(req) claimsOut := jwt.Claims{} @@ -128,13 +152,14 @@ func TestVerifyCachedRequest(t *testing.T) { mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() - err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, key, true)) + asserter := newVerifyAsserter(assert, key, true) + err = mux.Register("test", "jwt", newTestHandler("jwt", asserter)) assert.Nil(err) // Perform first test request. req := restaudit.NewRequest("GET", "/test/jwt/1234567890") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) req.SetRequestProcessor(func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwtIn) + return jwt.AddToRequest(req, jwtIn) }) resp := ts.DoRequest(req) claimsOut := jwt.Claims{} @@ -150,70 +175,88 @@ func TestVerifyCachedRequest(t *testing.T) { // HANDLER //-------------------- -// testHandler is used in the test scenarios. -type testHandler struct { - id string - assert audit.Assertion - key jwt.Key - cache jwt.Cache +// testAsserter instances will handle the assertions in the testHandler. +type testAsserter func(job rest.Job) (bool, error) + +func newHeaderAsserter(assert audit.Assertion, pattern string) testAsserter { + return func(job rest.Job) (bool, error) { + token, err := jwt.DecodeFromJob(job) + assert.Nil(token) + assert.ErrorMatch(err, pattern) + job.JSON(true).Write(rest.StatusOK, "OK") + return true, nil + } } -func NewTestHandler(id string, assert audit.Assertion, key jwt.Key, useCache bool) rest.ResourceHandler { +func newDecodeAsserter(assert audit.Assertion, cached bool) testAsserter { var cache jwt.Cache - if useCache { + if cached { cache = jwt.NewCache(time.Minute, time.Minute, time.Minute, 10) } - return &testHandler{id, assert, key, cache} + return func(job rest.Job) (bool, error) { + var token jwt.JWT + var err error + if cached { + token, err = jwt.DecodeCachedFromJob(job, cache) + } else { + token, err = jwt.DecodeFromJob(job) + } + assert.Nil(err) + assert.True(token.IsValid(time.Minute)) + subject, ok := token.Claims().Subject() + assert.True(ok) + assert.Equal(subject, job.ResourceID()) + job.JSON(true).Write(rest.StatusOK, token.Claims()) + return true, nil + } } -func (th *testHandler) ID() string { - return th.id +func newVerifyAsserter(assert audit.Assertion, key jwt.Key, cached bool) testAsserter { + var cache jwt.Cache + if cached { + cache = jwt.NewCache(time.Minute, time.Minute, time.Minute, 10) + } + return func(job rest.Job) (bool, error) { + var token jwt.JWT + var err error + if cached { + token, err = jwt.VerifyCachedFromJob(job, cache, key) + } else { + token, err = jwt.VerifyFromJob(job, key) + } + assert.Nil(err) + assert.True(token.IsValid(time.Minute)) + subject, ok := token.Claims().Subject() + assert.True(ok) + assert.Equal(subject, job.ResourceID()) + job.JSON(true).Write(rest.StatusOK, token.Claims()) + return true, nil + } } -func (th *testHandler) Init(env rest.Environment, domain, resource string) error { - return nil +// testHandler is used in the test scenarios. +type testHandler struct { + id string + asserter testAsserter } -func (th *testHandler) Get(job rest.Job) (bool, error) { - if th.key == nil { - return th.testDecode(job) - } else { - return th.testVerify(job) +func newTestHandler(id string, asserter testAsserter) rest.ResourceHandler { + return &testHandler{ + id: id, + asserter: asserter, } } -func (th *testHandler) testDecode(job rest.Job) (bool, error) { - decode := func() (jwt.JWT, error) { - if th.cache == nil { - return jwt.DecodeFromJob(job) - } - return jwt.DecodeCachedFromJob(job, th.cache) - } - jwtOut, err := decode() - th.assert.Nil(err) - th.assert.True(jwtOut.IsValid(time.Minute)) - subject, ok := jwtOut.Claims().Subject() - th.assert.True(ok) - th.assert.Equal(subject, job.ResourceID()) - job.JSON(true).Write(rest.StatusOK, jwtOut.Claims()) - return true, nil +func (th *testHandler) ID() string { + return th.id } -func (th *testHandler) testVerify(job rest.Job) (bool, error) { - verify := func() (jwt.JWT, error) { - if th.cache == nil { - return jwt.VerifyFromJob(job, th.key) - } - return jwt.VerifyCachedFromJob(job, th.cache, th.key) - } - jwtOut, err := verify() - th.assert.Nil(err) - th.assert.True(jwtOut.IsValid(time.Minute)) - subject, ok := jwtOut.Claims().Subject() - th.assert.True(ok) - th.assert.Equal(subject, job.ResourceID()) - job.JSON(true).Write(rest.StatusOK, jwtOut.Claims()) - return true, nil +func (th *testHandler) Init(env rest.Environment, domain, resource string) error { + return nil +} + +func (th *testHandler) Get(job rest.Job) (bool, error) { + return th.asserter(job) } //-------------------- diff --git a/request/request.go b/request/request.go index e9324f3..c3c2477 100644 --- a/request/request.go +++ b/request/request.go @@ -443,7 +443,7 @@ func (c *caller) prepareRequest(method, urlStr string, params *Parameters) (*htt request.Header.Set("Version", params.Version.String()) } if params.Token != nil { - request = jwt.AddTokenToRequest(request, params.Token) + request = jwt.AddToRequest(request, params.Token) } if params.Accept == "" { params.Accept = params.ContentType