-
-
Notifications
You must be signed in to change notification settings - Fork 444
/
index.ts
93 lines (83 loc) · 2.78 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import type { Context } from '../../context.ts'
import { HTTPException } from '../../http-exception.ts'
import type { HonoRequest } from '../../request.ts'
import type { MiddlewareHandler } from '../../types.ts'
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 = /^([^:]*):(.*)$/
const utf8Decoder = new TextDecoder()
const auth = (req: HonoRequest) => {
const match = CREDENTIALS_REGEXP.exec(req.header('Authorization') || '')
if (!match) {
return undefined
}
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
if (!userPass) {
return undefined
}
return { username: userPass[1], password: userPass[2] }
}
type BasicAuthOptions =
| {
username: string
password: string
realm?: string
hashFunction?: Function
}
| {
verifyUser: (username: string, password: string, c: Context) => boolean | Promise<boolean>
realm?: string
hashFunction?: Function
}
export const basicAuth = (
options: BasicAuthOptions,
...users: { username: string; password: string }[]
): MiddlewareHandler => {
const usernamePasswordInOptions = 'username' in options && 'password' in options
const verifyUserInOptions = 'verifyUser' in options
if (!(usernamePasswordInOptions || verifyUserInOptions)) {
throw new Error(
'basic auth middleware requires options for "username and password" or "verifyUser"'
)
}
if (!options.realm) {
options.realm = 'Secure Area'
}
if (usernamePasswordInOptions) {
users.unshift({ username: options.username, password: options.password })
}
return async function basicAuth(ctx, next) {
const requestUser = auth(ctx.req)
if (requestUser) {
if (verifyUserInOptions) {
if (await options.verifyUser(requestUser.username, requestUser.password, ctx)) {
await next()
return
}
} else {
for (const user of users) {
const [usernameEqual, passwordEqual] = await Promise.all([
timingSafeEqual(user.username, requestUser.username, options.hashFunction),
timingSafeEqual(user.password, requestUser.password, options.hashFunction),
])
if (usernameEqual && passwordEqual) {
await next()
return
}
}
}
}
const res = new Response('Unauthorized', {
status: 401,
headers: {
'WWW-Authenticate': 'Basic realm="' + options.realm?.replace(/"/g, '\\"') + '"',
},
})
throw new HTTPException(401, { res })
}
}