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:
parent
c50be25c9e
commit
c240ea5590
@ -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"}')
|
||||
})
|
||||
})
|
||||
|
@ -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'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user