2023-02-11 10:05:50 +01:00
|
|
|
import { HTTPException } from '../../http-exception.ts'
|
2022-12-20 23:05:00 +01:00
|
|
|
import type { HonoRequest } from '../../request.ts'
|
2022-10-31 16:07:56 +01:00
|
|
|
import type { MiddlewareHandler } from '../../types.ts'
|
2022-07-02 08:09:45 +02:00
|
|
|
import { timingSafeEqual } from '../../utils/buffer.ts'
|
|
|
|
import { decodeBase64 } from '../../utils/encode.ts'
|
|
|
|
|
|
|
|
const CREDENTIALS_REGEXP = /^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/
|
|
|
|
const USER_PASS_REGEXP = /^([^:]*):(.*)$/
|
2023-03-19 10:19:01 +01:00
|
|
|
const utf8Decoder = new TextDecoder()
|
2022-12-20 23:05:00 +01:00
|
|
|
const auth = (req: HonoRequest) => {
|
2022-07-02 08:09:45 +02:00
|
|
|
const match = CREDENTIALS_REGEXP.exec(req.headers.get('Authorization') || '')
|
|
|
|
if (!match) {
|
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
|
2023-05-21 13:48:35 +02:00
|
|
|
let userPass = undefined
|
|
|
|
// If an invalid string is passed to atob(), it throws a `DOMException`.
|
|
|
|
try {
|
|
|
|
userPass = USER_PASS_REGEXP.exec(utf8Decoder.decode(decodeBase64(match[1])))
|
|
|
|
} catch {} // Do nothing
|
2022-07-02 08:09:45 +02:00
|
|
|
|
|
|
|
if (!userPass) {
|
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
|
|
|
|
return { username: userPass[1], password: userPass[2] }
|
|
|
|
}
|
|
|
|
|
|
|
|
export const basicAuth = (
|
|
|
|
options: { username: string; password: string; realm?: string; hashFunction?: Function },
|
|
|
|
...users: { username: string; password: string }[]
|
2022-09-14 01:17:20 +02:00
|
|
|
): MiddlewareHandler => {
|
2022-07-02 08:09:45 +02:00
|
|
|
if (!options) {
|
|
|
|
throw new Error('basic auth middleware requires options for "username and password"')
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!options.realm) {
|
|
|
|
options.realm = 'Secure Area'
|
|
|
|
}
|
|
|
|
users.unshift({ username: options.username, password: options.password })
|
|
|
|
|
2023-11-29 11:22:09 +01:00
|
|
|
return async function basicAuth(ctx, next) {
|
2022-07-02 08:09:45 +02:00
|
|
|
const requestUser = auth(ctx.req)
|
|
|
|
if (requestUser) {
|
|
|
|
for (const user of users) {
|
|
|
|
const usernameEqual = await timingSafeEqual(
|
|
|
|
user.username,
|
|
|
|
requestUser.username,
|
|
|
|
options.hashFunction
|
|
|
|
)
|
|
|
|
const passwordEqual = await timingSafeEqual(
|
|
|
|
user.password,
|
|
|
|
requestUser.password,
|
|
|
|
options.hashFunction
|
|
|
|
)
|
|
|
|
if (usernameEqual && passwordEqual) {
|
|
|
|
await next()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-01-12 10:53:13 +01:00
|
|
|
const res = new Response('Unauthorized', {
|
2022-07-02 08:09:45 +02:00
|
|
|
status: 401,
|
|
|
|
headers: {
|
|
|
|
'WWW-Authenticate': 'Basic realm="' + options.realm?.replace(/"/g, '\\"') + '"',
|
|
|
|
},
|
|
|
|
})
|
2023-01-12 10:53:13 +01:00
|
|
|
throw new HTTPException(401, { res })
|
2022-07-02 08:09:45 +02:00
|
|
|
}
|
|
|
|
}
|