0
0
mirror of https://github.com/honojs/hono.git synced 2024-11-24 11:07:29 +01:00

feat(bearer-auth): added custom response message options (#3372)

* feat(bearer-auth): added custom response message options

* feat(bearer-auth): using specific MessageFunction type

* feat(bearer-auth): refactored to du-duplicate code
This commit is contained in:
Marcel Overdijk 2024-09-11 04:13:27 +02:00 committed by GitHub
parent c50be25c9e
commit c240ea5590
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 351 additions and 21 deletions

View File

@ -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"}')
})
})

View File

@ -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<string | object>
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<boolean>
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<Response> => {
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'
)
}
}
}