-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Expand file tree
/
Copy pathjwt.ts
More file actions
114 lines (100 loc) · 3.16 KB
/
jwt.ts
File metadata and controls
114 lines (100 loc) · 3.16 KB
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
105
106
107
108
109
110
111
112
113
114
/**
* @module
* JSON Web Token (JWT)
* https://datatracker.ietf.org/doc/html/rfc7519
*/
import { decodeBase64Url, encodeBase64Url } from '../../utils/encode'
import { AlgorithmTypes } from './jwa'
import type { SignatureAlgorithm } from './jwa'
import { signing, verifying } from './jws'
import type { SignatureKey } from './jws'
import {
JwtHeaderInvalid,
JwtTokenExpired,
JwtTokenInvalid,
JwtTokenIssuedAt,
JwtTokenNotBefore,
JwtTokenSignatureMismatched,
} from './types'
import type { JWTPayload } from './types'
import { utf8Decoder, utf8Encoder } from './utf8'
const encodeJwtPart = (part: unknown): string =>
encodeBase64Url(utf8Encoder.encode(JSON.stringify(part))).replace(/=/g, '')
const encodeSignaturePart = (buf: ArrayBufferLike): string => encodeBase64Url(buf).replace(/=/g, '')
const decodeJwtPart = (part: string): TokenHeader | JWTPayload | undefined =>
JSON.parse(utf8Decoder.decode(decodeBase64Url(part)))
export interface TokenHeader {
alg: SignatureAlgorithm
typ?: 'JWT'
}
export function isTokenHeader(obj: unknown): obj is TokenHeader {
if (typeof obj === 'object' && obj !== null) {
const objWithAlg = obj as { [key: string]: unknown }
return (
'alg' in objWithAlg &&
Object.values(AlgorithmTypes).includes(objWithAlg.alg as AlgorithmTypes) &&
(!('typ' in objWithAlg) || objWithAlg.typ === 'JWT')
)
}
return false
}
export const sign = async (
payload: JWTPayload,
privateKey: SignatureKey,
alg: SignatureAlgorithm = 'HS256'
): Promise<string> => {
const encodedPayload = encodeJwtPart(payload)
const encodedHeader = encodeJwtPart({ alg, typ: 'JWT' } satisfies TokenHeader)
const partialToken = `${encodedHeader}.${encodedPayload}`
const signaturePart = await signing(privateKey, alg, utf8Encoder.encode(partialToken))
const signature = encodeSignaturePart(signaturePart)
return `${partialToken}.${signature}`
}
export const verify = async (
token: string,
publicKey: SignatureKey,
alg: SignatureAlgorithm = 'HS256'
): Promise<JWTPayload> => {
const tokenParts = token.split('.')
if (tokenParts.length !== 3) {
throw new JwtTokenInvalid(token)
}
const { header, payload } = decode(token)
if (!isTokenHeader(header)) {
throw new JwtHeaderInvalid(header)
}
const now = (Date.now() / 1000) | 0
if (payload.nbf && payload.nbf > now) {
throw new JwtTokenNotBefore(token)
}
if (payload.exp && payload.exp <= now) {
throw new JwtTokenExpired(token)
}
if (payload.iat && now < payload.iat) {
throw new JwtTokenIssuedAt(now, payload.iat)
}
const headerPayload = token.substring(0, token.lastIndexOf('.'))
const verified = await verifying(
publicKey,
alg,
decodeBase64Url(tokenParts[2]),
utf8Encoder.encode(headerPayload)
)
if (!verified) {
throw new JwtTokenSignatureMismatched(token)
}
return payload
}
export const decode = (token: string): { header: TokenHeader; payload: JWTPayload } => {
try {
const [h, p] = token.split('.')
const header = decodeJwtPart(h) as TokenHeader
const payload = decodeJwtPart(p) as JWTPayload
return {
header,
payload,
}
} catch {
throw new JwtTokenInvalid(token)
}
}