Skip to content

Commit bf37828

Browse files
AprilNEAyusukebe
andauthored
feat(basic-auth): add context key and callback options (#4645)
* feat(basic-auth): add usernameContextKey and onAuthSuccess options Add two new options to basicAuth middleware: - `usernameContextKey`: stores authenticated username in context - `true`: uses default key 'basicAuthUsername' - `string`: uses custom key name - `onAuthSuccess`: callback invoked after successful authentication This allows route handlers to access the authenticated username without re-parsing the Authorization header. * test(basic-auth): add tests for usernameContextKey and onAuthSuccess Add test cases for: - usernameContextKey with default key 'basicAuthUsername' - usernameContextKey with custom key - usernameContextKey not set (should not store username) - usernameContextKey with verifyUser mode - onAuthSuccess callback execution - onAuthSuccess async callback support - onAuthSuccess not called on failed auth - onAuthSuccess with verifyUser mode - Both options used together * remove `usernameContextKey` * remove unnecessary comments * remove unnecessary comments --------- Co-authored-by: Yusuke Wada <yusuke@kamawada.com>
1 parent 9524923 commit bf37828

2 files changed

Lines changed: 138 additions & 0 deletions

File tree

src/middleware/basic-auth/index.test.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,116 @@ describe('Basic Auth by Middleware', () => {
319319
expect(await res.text()).toBe('{"message":"Custom unauthorized message as function object"}')
320320
})
321321
})
322+
323+
describe('Basic Auth with onAuthSuccess', () => {
324+
const username = 'callback-user'
325+
const password = 'callback-pass'
326+
327+
it('should call onAuthSuccess callback on successful auth', async () => {
328+
type Env = { Variables: { custom: string } }
329+
const app = new Hono<Env>()
330+
let callbackCalled = false
331+
let callbackUsername = ''
332+
333+
app.use(
334+
'/*',
335+
basicAuth({
336+
username,
337+
password,
338+
onAuthSuccess: (c, u) => {
339+
callbackCalled = true
340+
callbackUsername = u
341+
c.set('custom', 'value')
342+
},
343+
})
344+
)
345+
app.get('/', (c) => c.text(c.get('custom') || 'no-custom'))
346+
347+
const credential = Buffer.from(`${username}:${password}`).toString('base64')
348+
const res = await app.request('/', {
349+
headers: { Authorization: `Basic ${credential}` },
350+
})
351+
352+
expect(callbackCalled).toBe(true)
353+
expect(callbackUsername).toBe(username)
354+
expect(res.status).toBe(200)
355+
expect(await res.text()).toBe('value')
356+
})
357+
358+
it('should support async onAuthSuccess callback', async () => {
359+
type Env = { Variables: { asyncValue: string } }
360+
const app = new Hono<Env>()
361+
362+
app.use(
363+
'/*',
364+
basicAuth({
365+
username,
366+
password,
367+
onAuthSuccess: async (c) => {
368+
await new Promise((resolve) => setTimeout(resolve, 10))
369+
c.set('asyncValue', 'done')
370+
},
371+
})
372+
)
373+
app.get('/', (c) => c.text(c.get('asyncValue') || 'not-done'))
374+
375+
const credential = Buffer.from(`${username}:${password}`).toString('base64')
376+
const res = await app.request('/', {
377+
headers: { Authorization: `Basic ${credential}` },
378+
})
379+
expect(res.status).toBe(200)
380+
expect(await res.text()).toBe('done')
381+
})
382+
383+
it('should not call onAuthSuccess on failed auth', async () => {
384+
const app = new Hono()
385+
let callbackCalled = false
386+
387+
app.use(
388+
'/*',
389+
basicAuth({
390+
username,
391+
password,
392+
onAuthSuccess: () => {
393+
callbackCalled = true
394+
},
395+
})
396+
)
397+
app.get('/', (c) => c.text('ok'))
398+
399+
const credential = Buffer.from('wrong:wrong').toString('base64')
400+
const res = await app.request('/', {
401+
headers: { Authorization: `Basic ${credential}` },
402+
})
403+
404+
expect(callbackCalled).toBe(false)
405+
expect(res.status).toBe(401)
406+
})
407+
408+
it('should work with verifyUser mode', async () => {
409+
type Env = { Variables: { verified: string } }
410+
const app = new Hono<Env>()
411+
let callbackUsername = ''
412+
413+
app.use(
414+
'/*',
415+
basicAuth({
416+
verifyUser: (u, p) => u === username && p === password,
417+
onAuthSuccess: (c, u) => {
418+
callbackUsername = u
419+
c.set('verified', 'yes')
420+
},
421+
})
422+
)
423+
app.get('/', (c) => c.text(c.get('verified') || 'no'))
424+
425+
const credential = Buffer.from(`${username}:${password}`).toString('base64')
426+
const res = await app.request('/', {
427+
headers: { Authorization: `Basic ${credential}` },
428+
})
429+
430+
expect(callbackUsername).toBe(username)
431+
expect(res.status).toBe(200)
432+
expect(await res.text()).toBe('yes')
433+
})
434+
})

src/middleware/basic-auth/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ type BasicAuthOptions =
1818
realm?: string
1919
hashFunction?: Function
2020
invalidUserMessage?: string | object | MessageFunction
21+
onAuthSuccess?: (c: Context, username: string) => void | Promise<void>
2122
}
2223
| {
2324
verifyUser: (username: string, password: string, c: Context) => boolean | Promise<boolean>
2425
realm?: string
2526
hashFunction?: Function
2627
invalidUserMessage?: string | object | MessageFunction
28+
onAuthSuccess?: (c: Context, username: string) => void | Promise<void>
2729
}
2830

2931
/**
@@ -38,6 +40,7 @@ type BasicAuthOptions =
3840
* @param {Function} [options.hashFunction] - The hash function used for secure comparison.
3941
* @param {Function} [options.verifyUser] - The function to verify user credentials.
4042
* @param {string | object | MessageFunction} [options.invalidUserMessage="Unauthorized"] - The invalid user message.
43+
* @param {Function} [options.onAuthSuccess] - Callback function called on successful authentication.
4144
* @returns {MiddlewareHandler} The middleware handler function.
4245
* @throws {HTTPException} If neither "username and password" nor "verifyUser" options are provided.
4346
*
@@ -57,6 +60,22 @@ type BasicAuthOptions =
5760
* return c.text('You are authorized')
5861
* })
5962
* ```
63+
*
64+
* @example
65+
* ```ts
66+
* // With onAuthSuccess callback
67+
* app.use(
68+
* '/auth/*',
69+
* basicAuth({
70+
* username: 'hono',
71+
* password: 'ahotproject',
72+
* onAuthSuccess: (c, username) => {
73+
* c.set('user', { name: username, role: 'admin' })
74+
* console.log(`User ${username} authenticated`)
75+
* },
76+
* })
77+
* )
78+
* ```
6079
*/
6180
export const basicAuth = (
6281
options: BasicAuthOptions,
@@ -88,6 +107,9 @@ export const basicAuth = (
88107
if (requestUser) {
89108
if (verifyUserInOptions) {
90109
if (await options.verifyUser(requestUser.username, requestUser.password, ctx)) {
110+
if (options.onAuthSuccess) {
111+
await options.onAuthSuccess(ctx, requestUser.username)
112+
}
91113
await next()
92114
return
93115
}
@@ -98,6 +120,9 @@ export const basicAuth = (
98120
timingSafeEqual(user.password, requestUser.password, options.hashFunction),
99121
])
100122
if (usernameEqual && passwordEqual) {
123+
if (options.onAuthSuccess) {
124+
await options.onAuthSuccess(ctx, requestUser.username)
125+
}
101126
await next()
102127
return
103128
}

0 commit comments

Comments
 (0)