2023-03-19 09:19:01 +00:00
|
|
|
import { encodeBase64Url, decodeBase64Url } from '../../utils/encode.ts'
|
2023-09-20 13:48:14 +00:00
|
|
|
import type { AlgorithmTypes } from './types.ts'
|
|
|
|
import { JwtTokenIssuedAt } from './types.ts'
|
2022-07-02 06:09:45 +00:00
|
|
|
import {
|
|
|
|
JwtTokenInvalid,
|
|
|
|
JwtTokenNotBefore,
|
|
|
|
JwtTokenExpired,
|
|
|
|
JwtTokenSignatureMismatched,
|
2022-07-16 23:54:43 +00:00
|
|
|
JwtAlgorithmNotImplemented,
|
2022-07-02 06:09:45 +00:00
|
|
|
} from './types.ts'
|
|
|
|
|
|
|
|
interface AlgorithmParams {
|
|
|
|
name: string
|
|
|
|
namedCurve?: string
|
|
|
|
hash?: {
|
|
|
|
name: string
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum CryptoKeyFormat {
|
|
|
|
RAW = 'raw',
|
|
|
|
PKCS8 = 'pkcs8',
|
|
|
|
SPKI = 'spki',
|
|
|
|
JWK = 'jwk',
|
|
|
|
}
|
|
|
|
|
|
|
|
enum CryptoKeyUsage {
|
|
|
|
Ecrypt = 'encrypt',
|
|
|
|
Decrypt = 'decrypt',
|
|
|
|
Sign = 'sign',
|
|
|
|
Verify = 'verify',
|
|
|
|
Deriverkey = 'deriveKey',
|
|
|
|
DeriveBits = 'deriveBits',
|
|
|
|
WrapKey = 'wrapKey',
|
|
|
|
UnwrapKey = 'unwrapKey',
|
|
|
|
}
|
|
|
|
|
2023-09-20 13:48:14 +00:00
|
|
|
type AlgorithmTypeName = keyof typeof AlgorithmTypes
|
|
|
|
|
2023-03-19 09:19:01 +00:00
|
|
|
const utf8Encoder = new TextEncoder()
|
|
|
|
const utf8Decoder = new TextDecoder()
|
|
|
|
|
|
|
|
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): unknown =>
|
|
|
|
JSON.parse(utf8Decoder.decode(decodeBase64Url(part)))
|
|
|
|
|
2023-09-20 13:48:14 +00:00
|
|
|
const param = (name: AlgorithmTypeName): AlgorithmParams => {
|
2022-07-02 06:09:45 +00:00
|
|
|
switch (name.toUpperCase()) {
|
|
|
|
case 'HS256':
|
|
|
|
return {
|
|
|
|
name: 'HMAC',
|
|
|
|
hash: {
|
|
|
|
name: 'SHA-256',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
case 'HS384':
|
|
|
|
return {
|
|
|
|
name: 'HMAC',
|
|
|
|
hash: {
|
|
|
|
name: 'SHA-384',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
case 'HS512':
|
|
|
|
return {
|
|
|
|
name: 'HMAC',
|
|
|
|
hash: {
|
|
|
|
name: 'SHA-512',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
default:
|
2022-07-16 23:54:43 +00:00
|
|
|
throw new JwtAlgorithmNotImplemented(name)
|
2022-07-02 06:09:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const signing = async (
|
|
|
|
data: string,
|
|
|
|
secret: string,
|
2023-09-20 13:48:14 +00:00
|
|
|
alg: AlgorithmTypeName = 'HS256'
|
2022-07-02 06:09:45 +00:00
|
|
|
): Promise<ArrayBuffer> => {
|
2022-07-16 01:26:14 +00:00
|
|
|
if (!crypto.subtle || !crypto.subtle.importKey) {
|
|
|
|
throw new Error('`crypto.subtle.importKey` is undefined. JWT auth middleware requires it.')
|
|
|
|
}
|
|
|
|
|
2023-03-19 09:19:01 +00:00
|
|
|
const utf8Encoder = new TextEncoder()
|
2022-07-02 06:09:45 +00:00
|
|
|
const cryptoKey = await crypto.subtle.importKey(
|
|
|
|
CryptoKeyFormat.RAW,
|
2023-03-19 09:19:01 +00:00
|
|
|
utf8Encoder.encode(secret),
|
2022-07-02 06:09:45 +00:00
|
|
|
param(alg),
|
|
|
|
false,
|
|
|
|
[CryptoKeyUsage.Sign]
|
|
|
|
)
|
2023-03-19 09:19:01 +00:00
|
|
|
return await crypto.subtle.sign(param(alg), cryptoKey, utf8Encoder.encode(data))
|
2022-07-02 06:09:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export const sign = async (
|
|
|
|
payload: unknown,
|
|
|
|
secret: string,
|
2023-09-20 13:48:14 +00:00
|
|
|
alg: AlgorithmTypeName = 'HS256'
|
2022-07-02 06:09:45 +00:00
|
|
|
): Promise<string> => {
|
2023-03-19 09:19:01 +00:00
|
|
|
const encodedPayload = encodeJwtPart(payload)
|
|
|
|
const encodedHeader = encodeJwtPart({ alg, typ: 'JWT' })
|
2022-07-02 06:09:45 +00:00
|
|
|
|
|
|
|
const partialToken = `${encodedHeader}.${encodedPayload}`
|
|
|
|
|
2023-03-19 09:19:01 +00:00
|
|
|
const signaturePart = await signing(partialToken, secret, alg)
|
|
|
|
const signature = encodeSignaturePart(signaturePart)
|
2022-07-02 06:09:45 +00:00
|
|
|
|
|
|
|
return `${partialToken}.${signature}`
|
|
|
|
}
|
|
|
|
|
|
|
|
export const verify = async (
|
|
|
|
token: string,
|
|
|
|
secret: string,
|
2023-09-20 13:48:14 +00:00
|
|
|
alg: AlgorithmTypeName = 'HS256'
|
2023-03-31 09:39:26 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
): Promise<any> => {
|
2022-07-02 06:09:45 +00:00
|
|
|
const tokenParts = token.split('.')
|
|
|
|
if (tokenParts.length !== 3) {
|
|
|
|
throw new JwtTokenInvalid(token)
|
|
|
|
}
|
|
|
|
|
|
|
|
const { payload } = decode(token)
|
2023-03-20 14:29:32 +00:00
|
|
|
const now = Math.floor(Date.now() / 1000)
|
|
|
|
if (payload.nbf && payload.nbf > now) {
|
2022-07-02 06:09:45 +00:00
|
|
|
throw new JwtTokenNotBefore(token)
|
|
|
|
}
|
2023-03-20 14:29:32 +00:00
|
|
|
if (payload.exp && payload.exp <= now) {
|
2022-07-02 06:09:45 +00:00
|
|
|
throw new JwtTokenExpired(token)
|
|
|
|
}
|
2023-03-20 14:29:32 +00:00
|
|
|
if (payload.iat && now < payload.iat) {
|
|
|
|
throw new JwtTokenIssuedAt(now, payload.iat)
|
|
|
|
}
|
2022-07-02 06:09:45 +00:00
|
|
|
|
2023-03-19 09:19:01 +00:00
|
|
|
const signaturePart = tokenParts.slice(0, 2).join('.')
|
|
|
|
const signature = await signing(signaturePart, secret, alg)
|
|
|
|
const encodedSignature = encodeSignaturePart(signature)
|
|
|
|
if (encodedSignature !== tokenParts[2]) {
|
2022-07-02 06:09:45 +00:00
|
|
|
throw new JwtTokenSignatureMismatched(token)
|
|
|
|
}
|
|
|
|
|
2023-03-31 09:39:26 +00:00
|
|
|
return payload
|
2022-07-02 06:09:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line
|
|
|
|
export const decode = (token: string): { header: any; payload: any } => {
|
|
|
|
try {
|
|
|
|
const [h, p] = token.split('.')
|
2023-03-19 09:19:01 +00:00
|
|
|
const header = decodeJwtPart(h)
|
|
|
|
const payload = decodeJwtPart(p)
|
2022-07-02 06:09:45 +00:00
|
|
|
return {
|
|
|
|
header,
|
|
|
|
payload,
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
throw new JwtTokenInvalid(token)
|
|
|
|
}
|
|
|
|
}
|