Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/middleware/bearer-auth/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,48 @@ describe('Bearer Auth by Middleware', () => {
handlerExecuted = true
return c.text('auth-custom-header')
})

app.use(
'/auth-custom-no-authentication-header-message/*',
bearerAuth({
token,
noAuthenticationHeaderMessage: (c) => {
return c.json({ message: 'Custom no authentication header message' })
},
})
)
app.get('/auth-custom-no-authentication-header-message/*', (c) => {
handlerExecuted = true
return c.text('auth')
})

app.use(
'/auth-custom-invalid-authentication-header-message/*',
bearerAuth({
token,
invalidAuthenticationHeaderMeasage: (c) => {
return c.json({ message: 'Custom invalid authentication header message' })
},
})
)
app.get('/auth-custom-invalid-authentication-header-message/*', (c) => {
handlerExecuted = true
return c.text('auth')
})

app.use(
'/auth-custom-invalid-token-message/*',
bearerAuth({
token,
invalidTokenMessage: (c) => {
return c.json({ message: 'Custom invalid token message' })
},
})
)
app.get('/auth-custom-invalid-token-message/*', (c) => {
handlerExecuted = true
return c.text('auth')
})
})

it('Should authorize', async () => {
Expand Down Expand Up @@ -228,4 +270,36 @@ 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', async () => {
const req = new Request('http://localhost/auth-custom-no-authentication-header-message')
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"}')
})

it('Should not authorize - custom invalid authentication header message', async () => {
const req = new Request('http://localhost/auth-custom-invalid-authentication-header-message')
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"}')
})

it('Should not authorize - custom invalid token message as string', async () => {
const req = new Request('http://localhost/auth-custom-invalid-token-message')
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"}')
})
})
61 changes: 40 additions & 21 deletions src/middleware/bearer-auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,28 @@ const TOKEN_STRINGS = '[A-Za-z0-9._~+/-]+=*'
const PREFIX = 'Bearer'
const HEADER = 'Authorization'

type CustomMessageFunction = (c: Context) => Response | Promise<Response>

type BearerAuthOptions =
| {
token: string | string[]
realm?: string
prefix?: string
headerName?: string
hashFunction?: Function
noAuthenticationHeaderMessage?: CustomMessageFunction
invalidAuthenticationHeaderMeasage?: CustomMessageFunction
invalidTokenMessage?: CustomMessageFunction
}
| {
realm?: string
prefix?: string
headerName?: string
verifyToken: (token: string, c: Context) => boolean | Promise<boolean>
hashFunction?: Function
noAuthenticationHeaderMessage?: CustomMessageFunction
invalidAuthenticationHeaderMeasage?: CustomMessageFunction
invalidTokenMessage?: CustomMessageFunction
}

/**
Expand All @@ -40,6 +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 {CustomMessageFunction} [options.noAuthenticationHeaderMessage="Unauthorized"] - The no authentication header message.
* @param {CustomMessageFunction} [options.invalidAuthenticationHeaderMeasage="Bad Request"] - The invalid authentication header message.
* @param {CustomMessageFunction} [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.
Expand Down Expand Up @@ -77,24 +88,30 @@ 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
c.status(status)
c.header('WWW-Authenticate', `${wwwAuthenticatePrefix}realm="` + realm + '"')
const defaultNoAuthenticationHeaderMessage = (c: Context) => {
return c.text('Unauthorized')
}
const noAuthenticationHeaderMessage =
options.noAuthenticationHeaderMessage ?? defaultNoAuthenticationHeaderMessage
const res = await noAuthenticationHeaderMessage(c)
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
c.status(status)
c.header('WWW-Authenticate', `${wwwAuthenticatePrefix}error="invalid_request"`)
const defaultInvalidAuthenticationHeaderMeasage = (c: Context) => {
return c.text('Bad Request')
}
const invalidAuthenticationHeaderMeasage =
options.invalidAuthenticationHeaderMeasage ?? defaultInvalidAuthenticationHeaderMeasage
const res = await invalidAuthenticationHeaderMeasage(c)
throw new HTTPException(status, { res })
} else {
let equal = false
if ('verifyToken' in options) {
Expand All @@ -111,13 +128,15 @@ 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
c.status(status)
c.header('WWW-Authenticate', `${wwwAuthenticatePrefix}error="invalid_token"`)
const defaultInvalidTokenMessage = (c: Context) => {
return c.text('Unauthorized')
}
const invalidTokenMessage = options.invalidTokenMessage ?? defaultInvalidTokenMessage
const res = await invalidTokenMessage(c)
throw new HTTPException(status, { res })
}
}
}
Expand Down
Loading