diff --git a/src/middleware/bearer-auth/index.test.ts b/src/middleware/bearer-auth/index.test.ts index 632d9c9a..8c017f48 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 6f6208ba..a8db32f4 100644 --- a/src/middleware/bearer-auth/index.ts +++ b/src/middleware/bearer-auth/index.ts @@ -7,11 +7,14 @@ 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' const HEADER = 'Authorization' +type MessageFunction = (c: Context) => string | object | Promise + type BearerAuthOptions = | { token: string | string[] @@ -19,6 +22,9 @@ type BearerAuthOptions = prefix?: string headerName?: string hashFunction?: Function + noAuthenticationHeaderMessage?: string | object | MessageFunction + invalidAuthenticationHeaderMeasage?: string | object | MessageFunction + invalidTokenMessage?: string | object | MessageFunction } | { realm?: string @@ -26,6 +32,9 @@ type BearerAuthOptions = headerName?: string verifyToken: (token: string, c: Context) => boolean | Promise hashFunction?: Function + noAuthenticationHeaderMessage?: string | object | MessageFunction + invalidAuthenticationHeaderMeasage?: string | object | MessageFunction + invalidTokenMessage?: string | object | MessageFunction } /** @@ -40,6 +49,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 | 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. @@ -73,28 +85,50 @@ export const bearerAuth = (options: BearerAuthOptions): MiddlewareHandler => { 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 res = new Response('Unauthorized', { - status: 401, - headers: { - 'WWW-Authenticate': `${wwwAuthenticatePrefix}realm="` + realm + '"', - }, - }) - throw new HTTPException(401, { res }) + await throwHTTPException( + c, + 401, + `${wwwAuthenticatePrefix}realm="${realm}"`, + options.noAuthenticationHeaderMessage || 'Unauthorized' + ) } 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 }) + await throwHTTPException( + c, + 400, + `${wwwAuthenticatePrefix}error="invalid_request"`, + options.invalidAuthenticationHeaderMeasage || 'Bad Request' + ) } else { let equal = false if ('verifyToken' in options) { @@ -111,13 +145,12 @@ 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 }) + await throwHTTPException( + c, + 401, + `${wwwAuthenticatePrefix}error="invalid_token"`, + options.invalidTokenMessage || 'Unauthorized' + ) } } }