From c50be25c9ef6d770a077ead0ec7f0b5224237d86 Mon Sep 17 00:00:00 2001 From: Marcel Overdijk Date: Wed, 11 Sep 2024 04:10:17 +0200 Subject: [PATCH] feat(basic-auth): added custom response message option (#3371) * feat(basic-auth): added custom response message option * feat(basic-auth): using specific MessageFunction type --- src/middleware/basic-auth/index.test.ts | 92 +++++++++++++++++++++++++ src/middleware/basic-auth/index.ts | 36 ++++++++-- 2 files changed, 121 insertions(+), 7 deletions(-) diff --git a/src/middleware/basic-auth/index.test.ts b/src/middleware/basic-auth/index.test.ts index 147b3c95..1fc7015a 100644 --- a/src/middleware/basic-auth/index.test.ts +++ b/src/middleware/basic-auth/index.test.ts @@ -83,6 +83,42 @@ describe('Basic Auth by Middleware', () => { return auth(c, next) }) + app.use( + '/auth-custom-invalid-user-message-string/*', + basicAuth({ + username, + password, + invalidUserMessage: 'Custom unauthorized message as string', + }) + ) + + app.use( + '/auth-custom-invalid-user-message-object/*', + basicAuth({ + username, + password, + invalidUserMessage: { message: 'Custom unauthorized message as object' }, + }) + ) + + app.use( + '/auth-custom-invalid-user-message-function-string/*', + basicAuth({ + username, + password, + invalidUserMessage: () => 'Custom unauthorized message as function string', + }) + ) + + app.use( + '/auth-custom-invalid-user-message-function-object/*', + basicAuth({ + username, + password, + invalidUserMessage: () => ({ message: 'Custom unauthorized message as function object' }), + }) + ) + app.get('/auth/*', (c) => { handlerExecuted = true return c.text('auth') @@ -110,6 +146,24 @@ describe('Basic Auth by Middleware', () => { return c.text('verify-user') }) + app.get('/auth-custom-invalid-user-message-string/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + app.get('/auth-custom-invalid-user-message-object/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + app.get('/auth-custom-invalid-user-message-function-string/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + + app.get('/auth-custom-invalid-user-message-function-object/*', (c) => { + handlerExecuted = true + return c.text('auth') + }) + it('Should not authorize', async () => { const req = new Request('http://localhost/auth/a') const res = await app.request(req) @@ -226,4 +280,42 @@ describe('Basic Auth by Middleware', () => { expect(res.status).toBe(401) expect(await res.text()).toBe('Unauthorized') }) + + it('Should not authorize - custom invalid user message as string', async () => { + const req = new Request('http://localhost/auth-custom-invalid-user-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 unauthorized message as string') + }) + + it('Should not authorize - custom invalid user message as object', async () => { + const req = new Request('http://localhost/auth-custom-invalid-user-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 unauthorized message as object"}') + }) + + it('Should not authorize - custom invalid user message as function string', async () => { + const req = new Request('http://localhost/auth-custom-invalid-user-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 unauthorized message as function string') + }) + + it('Should not authorize - custom invalid user message as function object', async () => { + const req = new Request('http://localhost/auth-custom-invalid-user-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 unauthorized message as function object"}') + }) }) diff --git a/src/middleware/basic-auth/index.ts b/src/middleware/basic-auth/index.ts index f1a2c379..aa4ca72d 100644 --- a/src/middleware/basic-auth/index.ts +++ b/src/middleware/basic-auth/index.ts @@ -9,17 +9,21 @@ import type { MiddlewareHandler } from '../../types' import { auth } from '../../utils/basic-auth' import { timingSafeEqual } from '../../utils/buffer' +type MessageFunction = (c: Context) => string | object | Promise + type BasicAuthOptions = | { username: string password: string realm?: string hashFunction?: Function + invalidUserMessage?: string | object | MessageFunction } | { verifyUser: (username: string, password: string, c: Context) => boolean | Promise realm?: string hashFunction?: Function + invalidUserMessage?: string | object | MessageFunction } /** @@ -33,6 +37,7 @@ type BasicAuthOptions = * @param {string} [options.realm="Secure Area"] - The realm attribute for the WWW-Authenticate header. * @param {Function} [options.hashFunction] - The hash function used for secure comparison. * @param {Function} [options.verifyUser] - The function to verify user credentials. + * @param {string | object | MessageFunction} [options.invalidUserMessage="Unauthorized"] - The invalid user message. * @returns {MiddlewareHandler} The middleware handler function. * @throws {HTTPException} If neither "username and password" nor "verifyUser" options are provided. * @@ -70,6 +75,10 @@ export const basicAuth = ( options.realm = 'Secure Area' } + if (!options.invalidUserMessage) { + options.invalidUserMessage = 'Unauthorized' + } + if (usernamePasswordInOptions) { users.unshift({ username: options.username, password: options.password }) } @@ -95,12 +104,25 @@ export const basicAuth = ( } } } - const res = new Response('Unauthorized', { - status: 401, - headers: { - 'WWW-Authenticate': 'Basic realm="' + options.realm?.replace(/"/g, '\\"') + '"', - }, - }) - throw new HTTPException(401, { res }) + // Invalid user. + const status = 401 + const headers = { + 'WWW-Authenticate': 'Basic realm="' + options.realm?.replace(/"/g, '\\"') + '"', + } + const responseMessage = + typeof options.invalidUserMessage === 'function' + ? await options.invalidUserMessage(ctx) + : options.invalidUserMessage + 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 }) } }