-
Notifications
You must be signed in to change notification settings - Fork 21
/
validate.ts
104 lines (96 loc) · 3.39 KB
/
validate.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
94
95
96
97
98
99
100
101
102
103
104
import makeJwt, { Payload, Jose } from "./create.ts"
import { convertBase64ToUint8Array } from "./base64/base64.ts"
import { convertBase64urlToBase64 } from "./base64/base64url.ts"
type JwtObject = { header: Jose; payload?: Payload; signature: string }
type Handlers = {
[key: string]: (header?: Jose[string]) => any
}
/*
* A present 'crit' header parameter indicates that the JWS signature validator
* must understand and process additional claims (JWS §4.1.11)
*/
function checkHeaderCrit(header: Jose, handlers?: Handlers): Promise<any[]> {
// prettier-ignore
const reservedNames = new Set([
"alg", "jku", "jwk", "kid", "x5u", "x5c", "x5t", "x5t#S256", "typ", "cty",
"crit", "enc", "zip", "epk", "apu", "apv", "iv", "tag", "p2s", "p2c",
])
if (
!Array.isArray(header.crit) ||
header.crit.some((str: string) => typeof str !== "string" || !str)
)
throw Error("header parameter 'crit' must be an array of non-empty strings")
if (header.crit.some((str: string) => reservedNames.has(str)))
throw Error("the 'crit' list contains a non-extension header parameter")
if (
!handlers ||
header.crit.some(
(str: string) => !header[str] || typeof handlers[str] !== "function"
)
)
throw Error("critical extension header parameters are not understood")
return Promise.all(
header.crit.map((str: string) => handlers[str](header[str]))
)
}
/*
* Implementers MAY provide for some small leeway to account for clock skew (JWT §4.1.4)
*/
function isExpired(exp: Payload["exp"]): boolean {
if (typeof exp !== "number" || new Date(exp + 10000) < new Date()) return true
else return false
}
function validateAndHandleHeaders(
{ header, payload }: JwtObject,
critHandlers?: Handlers
): Promise<any> {
if (!header.alg) throw ReferenceError("header parameter 'alg' is empty")
if (typeof payload === "object" && "exp" in payload && isExpired(payload.exp))
throw RangeError("the jwt is expired")
return "crit" in header
? checkHeaderCrit(header, critHandlers)
: Promise.resolve()
}
function convertUint8ArrayToHex(uint8Array: Uint8Array): string {
return uint8Array.reduce(
(acc, el) => acc + el.toString(16).padStart(2, "0"),
""
)
}
function parseAndDecode(jwt: string): JwtObject {
if (!/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*$/.test(jwt))
throw Error("no valid JWT serialization")
const parsedArray = jwt
.split(".")
.map(convertBase64urlToBase64)
.map((str, index) => {
return index === 2
? convertUint8ArrayToHex(convertBase64ToUint8Array(str))
: JSON.parse(new TextDecoder().decode(convertBase64ToUint8Array(str)))
})
return {
header: parsedArray[0],
payload: parsedArray[1] === "" ? undefined : parsedArray[1],
signature: parsedArray[2],
} as JwtObject
}
async function validateJwt(
jwt: string,
key: string,
isThrowing = true,
critHandlers?: Handlers
): Promise<JwtObject | null> {
try {
const oldJwtObject = parseAndDecode(jwt)
await validateAndHandleHeaders(oldJwtObject, critHandlers)
const signature = parseAndDecode(makeJwt(oldJwtObject, key)).signature
if (oldJwtObject.signature === signature) return oldJwtObject
else throw Error("signatures don't match")
} catch (err) {
err.message = `Invalid JWT: ${err.message}`
if (isThrowing) throw err
else return null
}
}
export default validateJwt
export { convertUint8ArrayToHex, Jose, Payload }