-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtokenValidator.ts
131 lines (113 loc) · 3.28 KB
/
tokenValidator.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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import jwt, { JwtPayload, JwtHeader } from "jsonwebtoken";
import jwksRsa from "jwks-rsa";
export class ValidateTokenError extends Error {
status: number;
constructor(message: string, status: number = 401) {
super(message);
this.name = "ValidateTokenError";
this.status = status;
}
}
export interface ValidationResult {
valid: boolean;
decodedToken: JwtPayload | null;
error?: ValidateTokenError;
}
const DEFAULT_AUTHORITY = process.env.NEXT_PUBLIC_COGNITO_AUTHORITY || process.env.COGNITO_AUTHORITY || "";
const jwks = jwksRsa({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 10,
jwksUri: `${DEFAULT_AUTHORITY}/.well-known/jwks.json`,
})
async function getKey(header: JwtHeader) {
const key = await jwks.getSigningKey(header.kid);
return key.getPublicKey();
}
/**
* Validates a JWT token against the provided authority and required scopes
* @param token The JWT token to validate
* @param requiredScopes Array of scopes that the token must have
* @param options Additional options for validation
* @returns A validation result object
*/
export async function validateToken(
token: string,
requiredScopes: string[] = [],
options: {
authority: string;
issuer?: string;
} = {
authority: DEFAULT_AUTHORITY,
}
): Promise<ValidationResult> {
try {
if (!token) {
throw new ValidateTokenError("No token provided");
}
const authority = options.authority;
if (!authority) {
throw new ValidateTokenError(
"Authority not provided and NEXT_PUBLIC_COGNITO_AUTHORITY not set",
500
);
}
// Decode the token without verification to get the kid
const decodedJwt = jwt.decode(token, { complete: true });
if (!decodedJwt || typeof decodedJwt !== "object") {
throw new ValidateTokenError("Invalid token format");
}
const secret = await getKey(decodedJwt.header);
// Verify the token
const verified = jwt.verify(token, secret, {
issuer: options.issuer || authority,
}) as JwtPayload;
if (!verified) {
throw new ValidateTokenError("Token verification failed");
}
// Check for required scopes if any are specified
if (requiredScopes.length > 0) {
const tokenScopes = verified.scope ? verified.scope.split(" ") : [];
const missingScopes = requiredScopes.filter(
(scope) => !tokenScopes.includes(scope)
);
if (missingScopes.length > 0) {
throw new ValidateTokenError(
`Missing required scopes: ${missingScopes.join(", ")}`,
403
);
}
}
return {
valid: true,
decodedToken: verified,
};
} catch (error) {
if (error instanceof ValidateTokenError) {
return {
valid: false,
decodedToken: null,
error,
};
}
// Handle jwt.verify errors
if (error instanceof Error) {
const status = error.name === "TokenExpiredError" ? 401 : 400;
const tokenError = new ValidateTokenError(error.message, status);
return {
valid: false,
decodedToken: null,
error: tokenError,
};
}
// Fallback for unknown errors
return {
valid: false,
decodedToken: null,
error: new ValidateTokenError(
"Unknown error during token validation",
500
),
};
}
}