From 0436aa7dd03b63acd0a20f01ee1798abde76b9dc Mon Sep 17 00:00:00 2001 From: Ben Francis Date: Tue, 30 Jul 2024 18:52:17 +0100 Subject: [PATCH] Add WWW-Authenticate headers to auth middleware responses - fixes #3145 --- src/jwt-middleware.ts | 15 +++++++++++++-- src/test/integration/oauth-test.ts | 25 ++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/jwt-middleware.ts b/src/jwt-middleware.ts index 76e84d189..bb4425982 100644 --- a/src/jwt-middleware.ts +++ b/src/jwt-middleware.ts @@ -101,6 +101,8 @@ export function middleware(): express.Handler { authenticate(req) .then((jwt) => { if (!jwt) { + // Send 401 with WWW-Authenticate header as per RFC 6750 + res.set('WWW-Authenticate', 'Bearer'); res.status(401).end(); return; } @@ -111,12 +113,21 @@ export function middleware(): express.Handler { scope = `${Constants.OAUTH_PATH}:${Constants.READWRITE}`; } if (!scopeAllowsRequest(scope, req)) { - res.status(401).send(`Token of role ${payload.role} used out of scope: ${scope}`); + // insufficient_scope error code as per RFC 6750 + const wwwAuthenticate = `Bearer error="insufficient_scope", \ + error_description="Token of role ${payload.role} used out of \ + scope: ${scope}"`; + res.set('WWW-Authenticate', wwwAuthenticate); + res.status(403).end(); return; } if (payload.role !== Constants.USER_TOKEN) { if (!payload.scope) { - res.status(400).send('Token must contain scope'); + // invalid_request error code as per RFC 6750 + const wwwAuthenticate = `Bearer error="invalid_request", \ + error_description="Token must contain scope"`; + res.set('WWW-Authenticate', wwwAuthenticate); + res.status(400).end(); return; } } diff --git a/src/test/integration/oauth-test.ts b/src/test/integration/oauth-test.ts index 241302bf4..a86f1aed5 100644 --- a/src/test/integration/oauth-test.ts +++ b/src/test/integration/oauth-test.ts @@ -162,6 +162,19 @@ describe('oauth/', function () { customCallbackHandler = customCallbackHandlerProvided || null; } + it('rejects request with no JWT', async () => { + setupOAuth(); + + // Try using the access token + const res = await chai + .request(server) + .get(Constants.THINGS_PATH) + .set('Accept', 'application/json'); + expect(res.status).toEqual(401); + expect(res.header).toHaveProperty('www-authenticate'); + expect(res.get('WWW-Authenticate')).toEqual('Bearer'); + }); + it('performs simple authorization', async () => { setupOAuth(); @@ -203,7 +216,9 @@ describe('oauth/', function () { .get(Constants.OAUTHCLIENTS_PATH) .set('Accept', 'application/json') .set(...headerAuth(jwt)); - expect(err.status).toEqual(401); + expect(err.status).toEqual(403); + expect(err.header).toHaveProperty('www-authenticate'); + expect(err.get('www-authenticate')).toEqual(expect.stringContaining('insufficient_scope')); res = await chai .request(server) @@ -488,7 +503,9 @@ describe('oauth/', function () { .delete(`${Constants.THINGS_PATH}/${TEST_THING.id}`) .set('Accept', 'application/json') .set(...headerAuth(jwt)); - expect(err.status).toEqual(401); + expect(err.status).toEqual(403); + expect(err.header).toHaveProperty('www-authenticate'); + expect(err.get('www-authenticate')).toEqual(expect.stringContaining('insufficient_scope')); }); it('rejects use of authorization code as access token', async () => { @@ -521,6 +538,8 @@ describe('oauth/', function () { .get(Constants.THINGS_PATH) .set('Accept', 'application/json') .set(...headerAuth(jwt)); - expect(err.status).toEqual(401); + expect(err.status).toEqual(403); + expect(err.header).toHaveProperty('www-authenticate'); + expect(err.get('www-authenticate')).toEqual(expect.stringContaining('insufficient_scope')); }); });