From ed347a035f357b574aeb37e4eab1ad73d568e8c4 Mon Sep 17 00:00:00 2001 From: Marcel Overdijk Date: Tue, 3 Sep 2024 18:18:27 +0200 Subject: [PATCH 1/3] feat(bearer-auth): added custom response message options --- src/middleware/bearer-auth/index.test.ts | 297 +++++++++++++++++++++++ src/middleware/bearer-auth/index.ts | 96 ++++++-- 2 files changed, 372 insertions(+), 21 deletions(-) diff --git a/src/middleware/bearer-auth/index.test.ts b/src/middleware/bearer-auth/index.test.ts index 632d9c9af..8c017f48e 100644 --- a/src/middleware/bearer-auth/index.test.ts +++ b/src/middleware/bearer-auth/index.test.ts @@ -68,6 +68,163 @@ describe('Bearer Auth by Middleware', () => { handlerExecuted = true return c.text('auth-custom-header') }) + + app.use( + '/auth-custom-no-authentication-header-message-string/*', + bearerAuth({ + token, + noAuthenticationHeaderMessage: 'Custom no authentication header message as string', + }) + ) + app.get('/auth-custom-no-authentication-header-message-string/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + + app.use( + '/auth-custom-no-authentication-header-message-object/*', + bearerAuth({ + token, + noAuthenticationHeaderMessage: { + message: 'Custom no authentication header message as object', + }, + }) + ) + app.get('/auth-custom-no-authentication-header-message-object/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + + app.use( + '/auth-custom-no-authentication-header-message-function-string/*', + bearerAuth({ + token, + noAuthenticationHeaderMessage: () => + 'Custom no authentication header message as function string', + }) + ) + app.get('/auth-custom-no-authentication-header-message-function-string/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + + app.use( + '/auth-custom-no-authentication-header-message-function-object/*', + bearerAuth({ + token, + noAuthenticationHeaderMessage: () => ({ + message: 'Custom no authentication header message as function object', + }), + }) + ) + app.get('/auth-custom-no-authentication-header-message-function-object/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + + app.use( + '/auth-custom-invalid-authentication-header-message-string/*', + bearerAuth({ + token, + invalidAuthenticationHeaderMeasage: + 'Custom invalid authentication header message as string', + }) + ) + app.get('/auth-custom-invalid-authentication-header-message-string/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + + app.use( + '/auth-custom-invalid-authentication-header-message-object/*', + bearerAuth({ + token, + invalidAuthenticationHeaderMeasage: { + message: 'Custom invalid authentication header message as object', + }, + }) + ) + app.get('/auth-custom-invalid-authentication-header-message-object/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + + app.use( + '/auth-custom-invalid-authentication-header-message-function-string/*', + bearerAuth({ + token, + invalidAuthenticationHeaderMeasage: () => + 'Custom invalid authentication header message as function string', + }) + ) + app.get('/auth-custom-invalid-authentication-header-message-function-string/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + + app.use( + '/auth-custom-invalid-authentication-header-message-function-object/*', + bearerAuth({ + token, + invalidAuthenticationHeaderMeasage: () => ({ + message: 'Custom invalid authentication header message as function object', + }), + }) + ) + app.get('/auth-custom-invalid-authentication-header-message-function-object/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + + app.use( + '/auth-custom-invalid-token-message-string/*', + bearerAuth({ + token, + invalidTokenMessage: 'Custom invalid token message as string', + }) + ) + app.get('/auth-custom-invalid-token-message-string/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + + app.use( + '/auth-custom-invalid-token-message-object/*', + bearerAuth({ + token, + invalidTokenMessage: { message: 'Custom invalid token message as object' }, + }) + ) + app.get('/auth-custom-invalid-token-message-object/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + + app.use( + '/auth-custom-invalid-token-message-function-string/*', + bearerAuth({ + token, + invalidTokenMessage: () => 'Custom invalid token message as function string', + }) + ) + app.get('/auth-custom-invalid-token-message-function-string/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + + app.use( + '/auth-custom-invalid-token-message-function-object/*', + bearerAuth({ + token, + invalidTokenMessage: () => ({ + message: 'Custom invalid token message as function object', + }), + }) + ) + app.get('/auth-custom-invalid-token-message-function-object/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) }) it('Should authorize', async () => { @@ -228,4 +385,144 @@ describe('Bearer Auth by Middleware', () => { expect(res.status).toBe(401) expect(await res.text()).toBe('Unauthorized') }) + + it('Should not authorize - custom no authorization header message as string', async () => { + const req = new Request('http://localhost/auth-custom-no-authentication-header-message-string') + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(401) + expect(handlerExecuted).toBeFalsy() + expect(await res.text()).toBe('Custom no authentication header message as string') + }) + + it('Should not authorize - custom no authorization header message as object', async () => { + const req = new Request('http://localhost/auth-custom-no-authentication-header-message-object') + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(401) + expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(handlerExecuted).toBeFalsy() + expect(await res.text()).toBe('{"message":"Custom no authentication header message as object"}') + }) + + it('Should not authorize - custom no authorization header message as function string', async () => { + const req = new Request( + 'http://localhost/auth-custom-no-authentication-header-message-function-string' + ) + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(401) + expect(handlerExecuted).toBeFalsy() + expect(await res.text()).toBe('Custom no authentication header message as function string') + }) + + it('Should not authorize - custom no authorization header message as function object', async () => { + const req = new Request( + 'http://localhost/auth-custom-no-authentication-header-message-function-object' + ) + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(401) + expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(handlerExecuted).toBeFalsy() + expect(await res.text()).toBe( + '{"message":"Custom no authentication header message as function object"}' + ) + }) + + it('Should not authorize - custom invalid authentication header message as string', async () => { + const req = new Request( + 'http://localhost/auth-custom-invalid-authentication-header-message-string' + ) + req.headers.set('Authorization', 'Beare abcdefg12345-._~+/=') + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(400) + expect(handlerExecuted).toBeFalsy() + expect(await res.text()).toBe('Custom invalid authentication header message as string') + }) + + it('Should not authorize - custom invalid authentication header message as object', async () => { + const req = new Request( + 'http://localhost/auth-custom-invalid-authentication-header-message-object' + ) + req.headers.set('Authorization', 'Beare abcdefg12345-._~+/=') + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(400) + expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(handlerExecuted).toBeFalsy() + expect(await res.text()).toBe( + '{"message":"Custom invalid authentication header message as object"}' + ) + }) + + it('Should not authorize - custom invalid authentication header message as function string', async () => { + const req = new Request( + 'http://localhost/auth-custom-invalid-authentication-header-message-function-string' + ) + req.headers.set('Authorization', 'Beare abcdefg12345-._~+/=') + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(400) + expect(handlerExecuted).toBeFalsy() + expect(await res.text()).toBe('Custom invalid authentication header message as function string') + }) + + it('Should not authorize - custom invalid authentication header message as function object', async () => { + const req = new Request( + 'http://localhost/auth-custom-invalid-authentication-header-message-function-object' + ) + req.headers.set('Authorization', 'Beare abcdefg12345-._~+/=') + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(400) + expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(handlerExecuted).toBeFalsy() + expect(await res.text()).toBe( + '{"message":"Custom invalid authentication header message as function object"}' + ) + }) + + it('Should not authorize - custom invalid token message as string', async () => { + const req = new Request('http://localhost/auth-custom-invalid-token-message-string') + req.headers.set('Authorization', 'Bearer invalid-token') + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(401) + expect(handlerExecuted).toBeFalsy() + expect(await res.text()).toBe('Custom invalid token message as string') + }) + + it('Should not authorize - custom invalid token message as object', async () => { + const req = new Request('http://localhost/auth-custom-invalid-token-message-object') + req.headers.set('Authorization', 'Bearer invalid-token') + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(401) + expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(handlerExecuted).toBeFalsy() + expect(await res.text()).toBe('{"message":"Custom invalid token message as object"}') + }) + + it('Should not authorize - custom invalid token message as function string', async () => { + const req = new Request('http://localhost/auth-custom-invalid-token-message-function-string') + req.headers.set('Authorization', 'Bearer invalid-token') + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(401) + expect(handlerExecuted).toBeFalsy() + expect(await res.text()).toBe('Custom invalid token message as function string') + }) + + it('Should not authorize - custom invalid token message as function object', async () => { + const req = new Request('http://localhost/auth-custom-invalid-token-message-function-object') + req.headers.set('Authorization', 'Bearer invalid-token') + const res = await app.request(req) + expect(res).not.toBeNull() + expect(res.status).toBe(401) + expect(res.headers.get('Content-Type')).toMatch('application/json; charset=UTF-8') + expect(handlerExecuted).toBeFalsy() + expect(await res.text()).toBe('{"message":"Custom invalid token message as function object"}') + }) }) diff --git a/src/middleware/bearer-auth/index.ts b/src/middleware/bearer-auth/index.ts index 6f6208ba4..15074aa3b 100644 --- a/src/middleware/bearer-auth/index.ts +++ b/src/middleware/bearer-auth/index.ts @@ -19,6 +19,9 @@ type BearerAuthOptions = prefix?: string headerName?: string hashFunction?: Function + noAuthenticationHeaderMessage?: string | object | Function + invalidAuthenticationHeaderMeasage?: string | object | Function + invalidTokenMessage?: string | object | Function } | { realm?: string @@ -26,6 +29,9 @@ type BearerAuthOptions = headerName?: string verifyToken: (token: string, c: Context) => boolean | Promise hashFunction?: Function + noAuthenticationHeaderMessage?: string | object | Function + invalidAuthenticationHeaderMeasage?: string | object | Function + invalidTokenMessage?: string | object | Function } /** @@ -40,6 +46,9 @@ type BearerAuthOptions = * @param {string} [options.prefix="Bearer"] - The prefix (or known as `schema`) for the Authorization header value. If set to the empty string, no prefix is expected. * @param {string} [options.headerName=Authorization] - The header name. * @param {Function} [options.hashFunction] - A function to handle hashing for safe comparison of authentication tokens. + * @param {string | object | Function} [options.noAuthenticationHeaderMessage="Unauthorized"] - The no authentication header message. + * @param {string | object | Function} [options.invalidAuthenticationHeaderMeasage="Bad Request"] - The invalid authentication header message. + * @param {string | object | Function} [options.invalidTokenMessage="Unauthorized"] - The invalid token message. * @returns {MiddlewareHandler} The middleware handler function. * @throws {Error} If neither "token" nor "verifyToken" options are provided. * @throws {HTTPException} If authentication fails, with 401 status code for missing or invalid token, or 400 status code for invalid request. @@ -67,6 +76,15 @@ export const bearerAuth = (options: BearerAuthOptions): MiddlewareHandler => { if (options.prefix === undefined) { options.prefix = PREFIX } + if (!options.noAuthenticationHeaderMessage) { + options.noAuthenticationHeaderMessage = 'Unauthorized' + } + if (!options.invalidAuthenticationHeaderMeasage) { + options.invalidAuthenticationHeaderMeasage = 'Bad Request' + } + if (!options.invalidTokenMessage) { + options.invalidTokenMessage = 'Unauthorized' + } const realm = options.realm?.replace(/"/g, '\\"') const prefixRegexStr = options.prefix === '' ? '' : `${options.prefix} +` @@ -77,24 +95,48 @@ export const bearerAuth = (options: BearerAuthOptions): MiddlewareHandler => { const headerToken = c.req.header(options.headerName || HEADER) if (!headerToken) { // No Authorization header - const res = new Response('Unauthorized', { - status: 401, - headers: { - 'WWW-Authenticate': `${wwwAuthenticatePrefix}realm="` + realm + '"', - }, - }) - throw new HTTPException(401, { res }) + const status = 401 + const headers = { + 'WWW-Authenticate': `${wwwAuthenticatePrefix}realm="` + realm + '"', + } + const responseMessage = + typeof options.noAuthenticationHeaderMessage === 'function' + ? await options.noAuthenticationHeaderMessage(c) + : options.noAuthenticationHeaderMessage + const res = + typeof responseMessage === 'string' + ? new Response(responseMessage, { status, headers }) + : new Response(JSON.stringify(responseMessage), { + status, + headers: { + ...headers, + 'content-type': 'application/json; charset=UTF-8', + }, + }) + throw new HTTPException(status, { res }) } else { const match = regexp.exec(headerToken) if (!match) { // Invalid Request - const res = new Response('Bad Request', { - status: 400, - headers: { - 'WWW-Authenticate': `${wwwAuthenticatePrefix}error="invalid_request"`, - }, - }) - throw new HTTPException(400, { res }) + const status = 400 + const headers = { + 'WWW-Authenticate': `${wwwAuthenticatePrefix}error="invalid_request"`, + } + const responseMessage = + typeof options.invalidAuthenticationHeaderMeasage === 'function' + ? await options.invalidAuthenticationHeaderMeasage(c) + : options.invalidAuthenticationHeaderMeasage + const res = + typeof responseMessage === 'string' + ? new Response(responseMessage, { status, headers }) + : new Response(JSON.stringify(responseMessage), { + status, + headers: { + ...headers, + 'content-type': 'application/json; charset=UTF-8', + }, + }) + throw new HTTPException(status, { res }) } else { let equal = false if ('verifyToken' in options) { @@ -111,13 +153,25 @@ export const bearerAuth = (options: BearerAuthOptions): MiddlewareHandler => { } if (!equal) { // Invalid Token - const res = new Response('Unauthorized', { - status: 401, - headers: { - 'WWW-Authenticate': `${wwwAuthenticatePrefix}error="invalid_token"`, - }, - }) - throw new HTTPException(401, { res }) + const status = 401 + const headers = { + 'WWW-Authenticate': `${wwwAuthenticatePrefix}error="invalid_token"`, + } + const responseMessage = + typeof options.invalidTokenMessage === 'function' + ? await options.invalidTokenMessage(c) + : options.invalidTokenMessage + const res = + typeof responseMessage === 'string' + ? new Response(responseMessage, { status, headers }) + : new Response(JSON.stringify(responseMessage), { + status, + headers: { + ...headers, + 'content-type': 'application/json; charset=UTF-8', + }, + }) + throw new HTTPException(status, { res }) } } } From c339a570df2d5bbbe907f8bd4c8933f01ba83687 Mon Sep 17 00:00:00 2001 From: Marcel Overdijk Date: Tue, 10 Sep 2024 07:31:00 +0200 Subject: [PATCH 2/3] feat(bearer-auth): using specific MessageFunction type --- src/middleware/bearer-auth/index.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/middleware/bearer-auth/index.ts b/src/middleware/bearer-auth/index.ts index 15074aa3b..f1571cf59 100644 --- a/src/middleware/bearer-auth/index.ts +++ b/src/middleware/bearer-auth/index.ts @@ -12,6 +12,8 @@ const TOKEN_STRINGS = '[A-Za-z0-9._~+/-]+=*' const PREFIX = 'Bearer' const HEADER = 'Authorization' +type MessageFunction = (c: Context) => string | object | Promise + type BearerAuthOptions = | { token: string | string[] @@ -19,9 +21,9 @@ type BearerAuthOptions = prefix?: string headerName?: string hashFunction?: Function - noAuthenticationHeaderMessage?: string | object | Function - invalidAuthenticationHeaderMeasage?: string | object | Function - invalidTokenMessage?: string | object | Function + noAuthenticationHeaderMessage?: string | object | MessageFunction + invalidAuthenticationHeaderMeasage?: string | object | MessageFunction + invalidTokenMessage?: string | object | MessageFunction } | { realm?: string @@ -29,9 +31,9 @@ type BearerAuthOptions = headerName?: string verifyToken: (token: string, c: Context) => boolean | Promise hashFunction?: Function - noAuthenticationHeaderMessage?: string | object | Function - invalidAuthenticationHeaderMeasage?: string | object | Function - invalidTokenMessage?: string | object | Function + noAuthenticationHeaderMessage?: string | object | MessageFunction + invalidAuthenticationHeaderMeasage?: string | object | MessageFunction + invalidTokenMessage?: string | object | MessageFunction } /** @@ -46,9 +48,9 @@ type BearerAuthOptions = * @param {string} [options.prefix="Bearer"] - The prefix (or known as `schema`) for the Authorization header value. If set to the empty string, no prefix is expected. * @param {string} [options.headerName=Authorization] - The header name. * @param {Function} [options.hashFunction] - A function to handle hashing for safe comparison of authentication tokens. - * @param {string | object | Function} [options.noAuthenticationHeaderMessage="Unauthorized"] - The no authentication header message. - * @param {string | object | Function} [options.invalidAuthenticationHeaderMeasage="Bad Request"] - The invalid authentication header message. - * @param {string | object | Function} [options.invalidTokenMessage="Unauthorized"] - The invalid token message. + * @param {string | object | MessageFunction} [options.noAuthenticationHeaderMessage="Unauthorized"] - The no authentication header message. + * @param {string | object | MessageFunction} [options.invalidAuthenticationHeaderMeasage="Bad Request"] - The invalid authentication header message. + * @param {string | object | MessageFunction} [options.invalidTokenMessage="Unauthorized"] - The invalid token message. * @returns {MiddlewareHandler} The middleware handler function. * @throws {Error} If neither "token" nor "verifyToken" options are provided. * @throws {HTTPException} If authentication fails, with 401 status code for missing or invalid token, or 400 status code for invalid request. From 3817ad6bf3035000134f59af0ba2763411e13e38 Mon Sep 17 00:00:00 2001 From: Marcel Overdijk Date: Tue, 10 Sep 2024 17:59:17 +0200 Subject: [PATCH 3/3] feat(bearer-auth): refactored to du-duplicate code --- src/middleware/bearer-auth/index.ts | 109 +++++++++++----------------- 1 file changed, 43 insertions(+), 66 deletions(-) diff --git a/src/middleware/bearer-auth/index.ts b/src/middleware/bearer-auth/index.ts index f1571cf59..a8db32f43 100644 --- a/src/middleware/bearer-auth/index.ts +++ b/src/middleware/bearer-auth/index.ts @@ -7,6 +7,7 @@ import type { Context } from '../../context' import { HTTPException } from '../../http-exception' import type { MiddlewareHandler } from '../../types' import { timingSafeEqual } from '../../utils/buffer' +import type { StatusCode } from '../../utils/http-status' const TOKEN_STRINGS = '[A-Za-z0-9._~+/-]+=*' const PREFIX = 'Bearer' @@ -78,67 +79,56 @@ export const bearerAuth = (options: BearerAuthOptions): MiddlewareHandler => { if (options.prefix === undefined) { options.prefix = PREFIX } - if (!options.noAuthenticationHeaderMessage) { - options.noAuthenticationHeaderMessage = 'Unauthorized' - } - if (!options.invalidAuthenticationHeaderMeasage) { - options.invalidAuthenticationHeaderMeasage = 'Bad Request' - } - if (!options.invalidTokenMessage) { - options.invalidTokenMessage = 'Unauthorized' - } const realm = options.realm?.replace(/"/g, '\\"') const prefixRegexStr = options.prefix === '' ? '' : `${options.prefix} +` const regexp = new RegExp(`^${prefixRegexStr}(${TOKEN_STRINGS}) *$`) const wwwAuthenticatePrefix = options.prefix === '' ? '' : `${options.prefix} ` + const throwHTTPException = async ( + c: Context, + status: StatusCode, + wwwAuthenticateHeader: string, + messageOption: string | object | MessageFunction + ): Promise => { + const headers = { + 'WWW-Authenticate': wwwAuthenticateHeader, + } + const responseMessage = + typeof messageOption === 'function' ? await messageOption(c) : messageOption + const res = + typeof responseMessage === 'string' + ? new Response(responseMessage, { status, headers }) + : new Response(JSON.stringify(responseMessage), { + status, + headers: { + ...headers, + 'content-type': 'application/json; charset=UTF-8', + }, + }) + throw new HTTPException(status, { res }) + } + return async function bearerAuth(c, next) { const headerToken = c.req.header(options.headerName || HEADER) if (!headerToken) { // No Authorization header - const status = 401 - const headers = { - 'WWW-Authenticate': `${wwwAuthenticatePrefix}realm="` + realm + '"', - } - const responseMessage = - typeof options.noAuthenticationHeaderMessage === 'function' - ? await options.noAuthenticationHeaderMessage(c) - : options.noAuthenticationHeaderMessage - const res = - typeof responseMessage === 'string' - ? new Response(responseMessage, { status, headers }) - : new Response(JSON.stringify(responseMessage), { - status, - headers: { - ...headers, - 'content-type': 'application/json; charset=UTF-8', - }, - }) - throw new HTTPException(status, { res }) + await throwHTTPException( + c, + 401, + `${wwwAuthenticatePrefix}realm="${realm}"`, + options.noAuthenticationHeaderMessage || 'Unauthorized' + ) } else { const match = regexp.exec(headerToken) if (!match) { // Invalid Request - const status = 400 - const headers = { - 'WWW-Authenticate': `${wwwAuthenticatePrefix}error="invalid_request"`, - } - const responseMessage = - typeof options.invalidAuthenticationHeaderMeasage === 'function' - ? await options.invalidAuthenticationHeaderMeasage(c) - : options.invalidAuthenticationHeaderMeasage - const res = - typeof responseMessage === 'string' - ? new Response(responseMessage, { status, headers }) - : new Response(JSON.stringify(responseMessage), { - status, - headers: { - ...headers, - 'content-type': 'application/json; charset=UTF-8', - }, - }) - throw new HTTPException(status, { res }) + await throwHTTPException( + c, + 400, + `${wwwAuthenticatePrefix}error="invalid_request"`, + options.invalidAuthenticationHeaderMeasage || 'Bad Request' + ) } else { let equal = false if ('verifyToken' in options) { @@ -155,25 +145,12 @@ export const bearerAuth = (options: BearerAuthOptions): MiddlewareHandler => { } if (!equal) { // Invalid Token - const status = 401 - const headers = { - 'WWW-Authenticate': `${wwwAuthenticatePrefix}error="invalid_token"`, - } - const responseMessage = - typeof options.invalidTokenMessage === 'function' - ? await options.invalidTokenMessage(c) - : options.invalidTokenMessage - const res = - typeof responseMessage === 'string' - ? new Response(responseMessage, { status, headers }) - : new Response(JSON.stringify(responseMessage), { - status, - headers: { - ...headers, - 'content-type': 'application/json; charset=UTF-8', - }, - }) - throw new HTTPException(status, { res }) + await throwHTTPException( + c, + 401, + `${wwwAuthenticatePrefix}error="invalid_token"`, + options.invalidTokenMessage || 'Unauthorized' + ) } } }