@canva/cli 1.14.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +1 -0
- package/cli.js +308 -308
- package/package.json +1 -1
- package/templates/base/backend/base_backend/create.ts +10 -0
- package/templates/base/backend/routers/auth.ts +12 -9
- package/templates/base/package.json +3 -3
- package/templates/content_publisher/README.md +58 -0
- package/templates/content_publisher/canva-app.json +17 -0
- package/templates/content_publisher/declarations/declarations.d.ts +29 -0
- package/templates/content_publisher/eslint.config.mjs +14 -0
- package/templates/content_publisher/package.json +90 -0
- package/templates/content_publisher/scripts/copy_env.ts +13 -0
- package/templates/content_publisher/scripts/ssl/ssl.ts +131 -0
- package/templates/content_publisher/scripts/start/app_runner.ts +223 -0
- package/templates/content_publisher/scripts/start/context.ts +171 -0
- package/templates/content_publisher/scripts/start/start.ts +46 -0
- package/templates/content_publisher/src/index.tsx +4 -0
- package/templates/content_publisher/src/intents/content_publisher/index.tsx +113 -0
- package/templates/content_publisher/src/intents/content_publisher/post_preview.tsx +226 -0
- package/templates/content_publisher/src/intents/content_publisher/preview_ui.tsx +53 -0
- package/templates/content_publisher/src/intents/content_publisher/settings_ui.tsx +71 -0
- package/templates/content_publisher/src/intents/content_publisher/types.ts +29 -0
- package/templates/content_publisher/styles/components.css +56 -0
- package/templates/content_publisher/styles/preview_ui.css +88 -0
- package/templates/content_publisher/tsconfig.json +56 -0
- package/templates/content_publisher/webpack.config.ts +247 -0
- package/templates/dam/backend/server.ts +2 -3
- package/templates/dam/package.json +5 -4
- package/templates/dam/utils/backend/base_backend/create.ts +10 -0
- package/templates/data_connector/package.json +3 -3
- package/templates/gen_ai/backend/server.ts +2 -3
- package/templates/gen_ai/package.json +4 -3
- package/templates/gen_ai/utils/backend/base_backend/create.ts +10 -0
- package/templates/hello_world/package.json +3 -3
- package/templates/mls/package.json +3 -3
- package/templates/base/backend/jwt_middleware/index.ts +0 -1
- package/templates/base/backend/jwt_middleware/jwt_middleware.ts +0 -224
- package/templates/dam/utils/backend/jwt_middleware/index.ts +0 -1
- package/templates/dam/utils/backend/jwt_middleware/jwt_middleware.ts +0 -224
- package/templates/gen_ai/utils/backend/jwt_middleware/index.ts +0 -1
- package/templates/gen_ai/utils/backend/jwt_middleware/jwt_middleware.ts +0 -224
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
import debug from "debug";
|
|
3
|
-
import type { NextFunction, Request, Response } from "express";
|
|
4
|
-
import jwt from "jsonwebtoken";
|
|
5
|
-
import { JwksClient, SigningKeyNotFoundError } from "jwks-rsa";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Prefix your start command with `DEBUG=express:middleware:jwt` to enable debug logging
|
|
9
|
-
* for this middleware
|
|
10
|
-
*/
|
|
11
|
-
const debugLogger = debug("express:middleware:jwt");
|
|
12
|
-
|
|
13
|
-
const CANVA_BASE_URL = "https://api.canva.com";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Augment the Express request context to include the appId/userId/brandId fields decoded
|
|
17
|
-
* from the JWT.
|
|
18
|
-
*/
|
|
19
|
-
declare module "express-serve-static-core" {
|
|
20
|
-
export interface Request {
|
|
21
|
-
canva: {
|
|
22
|
-
appId: string;
|
|
23
|
-
userId: string;
|
|
24
|
-
brandId: string;
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type CanvaJwt = Omit<jwt.Jwt, "payload"> & {
|
|
30
|
-
payload: {
|
|
31
|
-
aud?: string;
|
|
32
|
-
userId?: string;
|
|
33
|
-
brandId?: string;
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const PUBLIC_KEY_DEFAULT_EXPIRY_MS = 60 * 60 * 1_000; // 60 minutes
|
|
38
|
-
const PUBLIC_KEY_DEFAULT_FETCH_TIMEOUT_MS = 30 * 1_000; // 30 seconds
|
|
39
|
-
|
|
40
|
-
const sendUnauthorizedResponse = (res: Response, message?: string) =>
|
|
41
|
-
res.status(401).json({ error: "unauthorized", message });
|
|
42
|
-
|
|
43
|
-
const createJwksUrl = (appId: string) =>
|
|
44
|
-
`${CANVA_BASE_URL}/rest/v1/apps/${appId}/jwks`;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* An Express.js middleware for decoding and verifying a JSON Web Token (JWT).
|
|
48
|
-
* By default, this middleware extracts the token from the `Authorization` header.
|
|
49
|
-
*
|
|
50
|
-
* @remarks
|
|
51
|
-
* If a JWT is successfully decoded, the following properties are added to the request object:
|
|
52
|
-
* - `request.canva.appId` - The ID of the app.
|
|
53
|
-
* - `request.canva.brandId` - The ID of the user's team.
|
|
54
|
-
* - `request.canva.userId` - The ID of the user.
|
|
55
|
-
*
|
|
56
|
-
* @param appId - The ID of the app.
|
|
57
|
-
* @param getTokenFromRequest - A function that extracts a token from the request. If a token isn't found, throw a `JWTAuthorizationError`.
|
|
58
|
-
* @returns An Express.js middleware for verifying and decoding JWTs.
|
|
59
|
-
*/
|
|
60
|
-
export function createJwtMiddleware(
|
|
61
|
-
appId: string,
|
|
62
|
-
getTokenFromRequest: GetTokenFromRequest = getTokenFromHttpHeader,
|
|
63
|
-
): (req: Request, res: Response, next: NextFunction) => void {
|
|
64
|
-
const jwksClient = new JwksClient({
|
|
65
|
-
cache: true,
|
|
66
|
-
cacheMaxAge: PUBLIC_KEY_DEFAULT_EXPIRY_MS,
|
|
67
|
-
timeout: PUBLIC_KEY_DEFAULT_FETCH_TIMEOUT_MS,
|
|
68
|
-
rateLimit: true,
|
|
69
|
-
jwksUri: createJwksUrl(appId),
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
return async (req, res, next) => {
|
|
73
|
-
try {
|
|
74
|
-
debugLogger(`processing JWT for '${req.url}'`);
|
|
75
|
-
|
|
76
|
-
const token = await getTokenFromRequest(req);
|
|
77
|
-
const unverifiedDecodedToken = jwt.decode(token, {
|
|
78
|
-
complete: true,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
if (unverifiedDecodedToken?.header?.kid == null) {
|
|
82
|
-
console.trace(
|
|
83
|
-
`jwtMiddleware: expected token to contain 'kid' claim header`,
|
|
84
|
-
);
|
|
85
|
-
return sendUnauthorizedResponse(res);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const key = await jwksClient.getSigningKey(
|
|
89
|
-
unverifiedDecodedToken.header.kid,
|
|
90
|
-
);
|
|
91
|
-
const publicKey = key.getPublicKey();
|
|
92
|
-
const verifiedToken = jwt.verify(token, publicKey, {
|
|
93
|
-
audience: appId,
|
|
94
|
-
complete: true,
|
|
95
|
-
}) as CanvaJwt;
|
|
96
|
-
const { payload } = verifiedToken;
|
|
97
|
-
debugLogger("payload: %O", payload);
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
payload.userId == null ||
|
|
101
|
-
payload.brandId == null ||
|
|
102
|
-
payload.aud == null
|
|
103
|
-
) {
|
|
104
|
-
console.trace(
|
|
105
|
-
"jwtMiddleware: failed to decode jwt missing fields from payload",
|
|
106
|
-
);
|
|
107
|
-
return sendUnauthorizedResponse(res);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
req["canva"] = {
|
|
111
|
-
appId: payload.aud,
|
|
112
|
-
brandId: payload.brandId,
|
|
113
|
-
userId: payload.userId,
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
return next();
|
|
117
|
-
} catch (e) {
|
|
118
|
-
if (e instanceof JWTAuthorizationError) {
|
|
119
|
-
return sendUnauthorizedResponse(res, e.message);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (e instanceof SigningKeyNotFoundError) {
|
|
123
|
-
return sendUnauthorizedResponse(
|
|
124
|
-
res,
|
|
125
|
-
`Public key not found. Ensure you have the correct App_ID set`,
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (e instanceof jwt.JsonWebTokenError) {
|
|
130
|
-
return sendUnauthorizedResponse(res, "Token is invalid");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (e instanceof jwt.TokenExpiredError) {
|
|
134
|
-
return sendUnauthorizedResponse(res, "Token expired");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return next(e);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export type GetTokenFromRequest = (req: Request) => Promise<string> | string;
|
|
143
|
-
|
|
144
|
-
export const getTokenFromQueryString: GetTokenFromRequest = (
|
|
145
|
-
req: Request,
|
|
146
|
-
): string => {
|
|
147
|
-
// The name of a query string parameter bearing the JWT
|
|
148
|
-
const tokenQueryStringParamName = "canva_user_token";
|
|
149
|
-
|
|
150
|
-
const queryParam = req.query[tokenQueryStringParamName];
|
|
151
|
-
if (!queryParam || typeof queryParam !== "string") {
|
|
152
|
-
console.trace(
|
|
153
|
-
`jwtMiddleware: missing "${tokenQueryStringParamName}" query parameter`,
|
|
154
|
-
);
|
|
155
|
-
throw new JWTAuthorizationError(
|
|
156
|
-
`Missing "${tokenQueryStringParamName}" query parameter`,
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (!looksLikeJWT(queryParam)) {
|
|
161
|
-
console.trace(
|
|
162
|
-
`jwtMiddleware: invalid "${tokenQueryStringParamName}" query parameter`,
|
|
163
|
-
);
|
|
164
|
-
throw new JWTAuthorizationError(
|
|
165
|
-
`Invalid "${tokenQueryStringParamName}" query parameter`,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return queryParam;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
export const getTokenFromHttpHeader: GetTokenFromRequest = (
|
|
173
|
-
req: Request,
|
|
174
|
-
): string => {
|
|
175
|
-
// The names of a HTTP header bearing the JWT, and a scheme
|
|
176
|
-
const headerName = "Authorization";
|
|
177
|
-
const schemeName = "Bearer";
|
|
178
|
-
|
|
179
|
-
const header = req.header(headerName);
|
|
180
|
-
if (!header) {
|
|
181
|
-
throw new JWTAuthorizationError(`Missing the "${headerName}" header`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (!header.match(new RegExp(`^${schemeName}\\s+[^\\s]+$`, "i"))) {
|
|
185
|
-
console.trace(
|
|
186
|
-
`jwtMiddleware: failed to match token in "${headerName}" header`,
|
|
187
|
-
);
|
|
188
|
-
throw new JWTAuthorizationError(
|
|
189
|
-
`Missing a "${schemeName}" token in the "${headerName}" header`,
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const token = header.replace(new RegExp(`^${schemeName}\\s+`, "i"), "");
|
|
194
|
-
if (!token || !looksLikeJWT(token)) {
|
|
195
|
-
throw new JWTAuthorizationError(
|
|
196
|
-
`Invalid "${schemeName}" token in the "${headerName}" header`,
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return token;
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* A class representing JWT validation errors in the JWT middleware.
|
|
205
|
-
* The error message provided to the constructor will be forwarded to the
|
|
206
|
-
* API consumer trying to access a JWT-protected endpoint.
|
|
207
|
-
* @private
|
|
208
|
-
*/
|
|
209
|
-
export class JWTAuthorizationError extends Error {
|
|
210
|
-
constructor(message: string) {
|
|
211
|
-
super(message);
|
|
212
|
-
|
|
213
|
-
Object.setPrototypeOf(this, JWTAuthorizationError.prototype);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const looksLikeJWT = (
|
|
218
|
-
token: string,
|
|
219
|
-
): boolean => // Base64 alphabet includes
|
|
220
|
-
// - letters (a-z and A-Z)
|
|
221
|
-
// - digits (0-9)
|
|
222
|
-
// - two special characters (+/ or -_)
|
|
223
|
-
// - padding (=)
|
|
224
|
-
token.match(/^[a-z0-9+/\-_=.]+$/i) != null;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { createJwtMiddleware } from "./jwt_middleware";
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
import debug from "debug";
|
|
3
|
-
import type { NextFunction, Request, Response } from "express";
|
|
4
|
-
import jwt from "jsonwebtoken";
|
|
5
|
-
import { JwksClient, SigningKeyNotFoundError } from "jwks-rsa";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Prefix your start command with `DEBUG=express:middleware:jwt` to enable debug logging
|
|
9
|
-
* for this middleware
|
|
10
|
-
*/
|
|
11
|
-
const debugLogger = debug("express:middleware:jwt");
|
|
12
|
-
|
|
13
|
-
const CANVA_BASE_URL = "https://api.canva.com";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Augment the Express request context to include the appId/userId/brandId fields decoded
|
|
17
|
-
* from the JWT.
|
|
18
|
-
*/
|
|
19
|
-
declare module "express-serve-static-core" {
|
|
20
|
-
export interface Request {
|
|
21
|
-
canva: {
|
|
22
|
-
appId: string;
|
|
23
|
-
userId: string;
|
|
24
|
-
brandId: string;
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type CanvaJwt = Omit<jwt.Jwt, "payload"> & {
|
|
30
|
-
payload: {
|
|
31
|
-
aud?: string;
|
|
32
|
-
userId?: string;
|
|
33
|
-
brandId?: string;
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const PUBLIC_KEY_DEFAULT_EXPIRY_MS = 60 * 60 * 1_000; // 60 minutes
|
|
38
|
-
const PUBLIC_KEY_DEFAULT_FETCH_TIMEOUT_MS = 30 * 1_000; // 30 seconds
|
|
39
|
-
|
|
40
|
-
const sendUnauthorizedResponse = (res: Response, message?: string) =>
|
|
41
|
-
res.status(401).json({ error: "unauthorized", message });
|
|
42
|
-
|
|
43
|
-
const createJwksUrl = (appId: string) =>
|
|
44
|
-
`${CANVA_BASE_URL}/rest/v1/apps/${appId}/jwks`;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* An Express.js middleware for decoding and verifying a JSON Web Token (JWT).
|
|
48
|
-
* By default, this middleware extracts the token from the `Authorization` header.
|
|
49
|
-
*
|
|
50
|
-
* @remarks
|
|
51
|
-
* If a JWT is successfully decoded, the following properties are added to the request object:
|
|
52
|
-
* - `request.canva.appId` - The ID of the app.
|
|
53
|
-
* - `request.canva.brandId` - The ID of the user's team.
|
|
54
|
-
* - `request.canva.userId` - The ID of the user.
|
|
55
|
-
*
|
|
56
|
-
* @param appId - The ID of the app.
|
|
57
|
-
* @param getTokenFromRequest - A function that extracts a token from the request. If a token isn't found, throw a `JWTAuthorizationError`.
|
|
58
|
-
* @returns An Express.js middleware for verifying and decoding JWTs.
|
|
59
|
-
*/
|
|
60
|
-
export function createJwtMiddleware(
|
|
61
|
-
appId: string,
|
|
62
|
-
getTokenFromRequest: GetTokenFromRequest = getTokenFromHttpHeader,
|
|
63
|
-
): (req: Request, res: Response, next: NextFunction) => void {
|
|
64
|
-
const jwksClient = new JwksClient({
|
|
65
|
-
cache: true,
|
|
66
|
-
cacheMaxAge: PUBLIC_KEY_DEFAULT_EXPIRY_MS,
|
|
67
|
-
timeout: PUBLIC_KEY_DEFAULT_FETCH_TIMEOUT_MS,
|
|
68
|
-
rateLimit: true,
|
|
69
|
-
jwksUri: createJwksUrl(appId),
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
return async (req, res, next) => {
|
|
73
|
-
try {
|
|
74
|
-
debugLogger(`processing JWT for '${req.url}'`);
|
|
75
|
-
|
|
76
|
-
const token = await getTokenFromRequest(req);
|
|
77
|
-
const unverifiedDecodedToken = jwt.decode(token, {
|
|
78
|
-
complete: true,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
if (unverifiedDecodedToken?.header?.kid == null) {
|
|
82
|
-
console.trace(
|
|
83
|
-
`jwtMiddleware: expected token to contain 'kid' claim header`,
|
|
84
|
-
);
|
|
85
|
-
return sendUnauthorizedResponse(res);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const key = await jwksClient.getSigningKey(
|
|
89
|
-
unverifiedDecodedToken.header.kid,
|
|
90
|
-
);
|
|
91
|
-
const publicKey = key.getPublicKey();
|
|
92
|
-
const verifiedToken = jwt.verify(token, publicKey, {
|
|
93
|
-
audience: appId,
|
|
94
|
-
complete: true,
|
|
95
|
-
}) as CanvaJwt;
|
|
96
|
-
const { payload } = verifiedToken;
|
|
97
|
-
debugLogger("payload: %O", payload);
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
payload.userId == null ||
|
|
101
|
-
payload.brandId == null ||
|
|
102
|
-
payload.aud == null
|
|
103
|
-
) {
|
|
104
|
-
console.trace(
|
|
105
|
-
"jwtMiddleware: failed to decode jwt missing fields from payload",
|
|
106
|
-
);
|
|
107
|
-
return sendUnauthorizedResponse(res);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
req["canva"] = {
|
|
111
|
-
appId: payload.aud,
|
|
112
|
-
brandId: payload.brandId,
|
|
113
|
-
userId: payload.userId,
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
return next();
|
|
117
|
-
} catch (e) {
|
|
118
|
-
if (e instanceof JWTAuthorizationError) {
|
|
119
|
-
return sendUnauthorizedResponse(res, e.message);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (e instanceof SigningKeyNotFoundError) {
|
|
123
|
-
return sendUnauthorizedResponse(
|
|
124
|
-
res,
|
|
125
|
-
`Public key not found. Ensure you have the correct App_ID set`,
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (e instanceof jwt.JsonWebTokenError) {
|
|
130
|
-
return sendUnauthorizedResponse(res, "Token is invalid");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (e instanceof jwt.TokenExpiredError) {
|
|
134
|
-
return sendUnauthorizedResponse(res, "Token expired");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return next(e);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export type GetTokenFromRequest = (req: Request) => Promise<string> | string;
|
|
143
|
-
|
|
144
|
-
export const getTokenFromQueryString: GetTokenFromRequest = (
|
|
145
|
-
req: Request,
|
|
146
|
-
): string => {
|
|
147
|
-
// The name of a query string parameter bearing the JWT
|
|
148
|
-
const tokenQueryStringParamName = "canva_user_token";
|
|
149
|
-
|
|
150
|
-
const queryParam = req.query[tokenQueryStringParamName];
|
|
151
|
-
if (!queryParam || typeof queryParam !== "string") {
|
|
152
|
-
console.trace(
|
|
153
|
-
`jwtMiddleware: missing "${tokenQueryStringParamName}" query parameter`,
|
|
154
|
-
);
|
|
155
|
-
throw new JWTAuthorizationError(
|
|
156
|
-
`Missing "${tokenQueryStringParamName}" query parameter`,
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (!looksLikeJWT(queryParam)) {
|
|
161
|
-
console.trace(
|
|
162
|
-
`jwtMiddleware: invalid "${tokenQueryStringParamName}" query parameter`,
|
|
163
|
-
);
|
|
164
|
-
throw new JWTAuthorizationError(
|
|
165
|
-
`Invalid "${tokenQueryStringParamName}" query parameter`,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return queryParam;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
export const getTokenFromHttpHeader: GetTokenFromRequest = (
|
|
173
|
-
req: Request,
|
|
174
|
-
): string => {
|
|
175
|
-
// The names of a HTTP header bearing the JWT, and a scheme
|
|
176
|
-
const headerName = "Authorization";
|
|
177
|
-
const schemeName = "Bearer";
|
|
178
|
-
|
|
179
|
-
const header = req.header(headerName);
|
|
180
|
-
if (!header) {
|
|
181
|
-
throw new JWTAuthorizationError(`Missing the "${headerName}" header`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (!header.match(new RegExp(`^${schemeName}\\s+[^\\s]+$`, "i"))) {
|
|
185
|
-
console.trace(
|
|
186
|
-
`jwtMiddleware: failed to match token in "${headerName}" header`,
|
|
187
|
-
);
|
|
188
|
-
throw new JWTAuthorizationError(
|
|
189
|
-
`Missing a "${schemeName}" token in the "${headerName}" header`,
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const token = header.replace(new RegExp(`^${schemeName}\\s+`, "i"), "");
|
|
194
|
-
if (!token || !looksLikeJWT(token)) {
|
|
195
|
-
throw new JWTAuthorizationError(
|
|
196
|
-
`Invalid "${schemeName}" token in the "${headerName}" header`,
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return token;
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* A class representing JWT validation errors in the JWT middleware.
|
|
205
|
-
* The error message provided to the constructor will be forwarded to the
|
|
206
|
-
* API consumer trying to access a JWT-protected endpoint.
|
|
207
|
-
* @private
|
|
208
|
-
*/
|
|
209
|
-
export class JWTAuthorizationError extends Error {
|
|
210
|
-
constructor(message: string) {
|
|
211
|
-
super(message);
|
|
212
|
-
|
|
213
|
-
Object.setPrototypeOf(this, JWTAuthorizationError.prototype);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const looksLikeJWT = (
|
|
218
|
-
token: string,
|
|
219
|
-
): boolean => // Base64 alphabet includes
|
|
220
|
-
// - letters (a-z and A-Z)
|
|
221
|
-
// - digits (0-9)
|
|
222
|
-
// - two special characters (+/ or -_)
|
|
223
|
-
// - padding (=)
|
|
224
|
-
token.match(/^[a-z0-9+/\-_=.]+$/i) != null;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { createJwtMiddleware } from "./jwt_middleware";
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
import debug from "debug";
|
|
3
|
-
import type { NextFunction, Request, Response } from "express";
|
|
4
|
-
import jwt from "jsonwebtoken";
|
|
5
|
-
import { JwksClient, SigningKeyNotFoundError } from "jwks-rsa";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Prefix your start command with `DEBUG=express:middleware:jwt` to enable debug logging
|
|
9
|
-
* for this middleware
|
|
10
|
-
*/
|
|
11
|
-
const debugLogger = debug("express:middleware:jwt");
|
|
12
|
-
|
|
13
|
-
const CANVA_BASE_URL = "https://api.canva.com";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Augment the Express request context to include the appId/userId/brandId fields decoded
|
|
17
|
-
* from the JWT.
|
|
18
|
-
*/
|
|
19
|
-
declare module "express-serve-static-core" {
|
|
20
|
-
export interface Request {
|
|
21
|
-
canva: {
|
|
22
|
-
appId: string;
|
|
23
|
-
userId: string;
|
|
24
|
-
brandId: string;
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type CanvaJwt = Omit<jwt.Jwt, "payload"> & {
|
|
30
|
-
payload: {
|
|
31
|
-
aud?: string;
|
|
32
|
-
userId?: string;
|
|
33
|
-
brandId?: string;
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const PUBLIC_KEY_DEFAULT_EXPIRY_MS = 60 * 60 * 1_000; // 60 minutes
|
|
38
|
-
const PUBLIC_KEY_DEFAULT_FETCH_TIMEOUT_MS = 30 * 1_000; // 30 seconds
|
|
39
|
-
|
|
40
|
-
const sendUnauthorizedResponse = (res: Response, message?: string) =>
|
|
41
|
-
res.status(401).json({ error: "unauthorized", message });
|
|
42
|
-
|
|
43
|
-
const createJwksUrl = (appId: string) =>
|
|
44
|
-
`${CANVA_BASE_URL}/rest/v1/apps/${appId}/jwks`;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* An Express.js middleware for decoding and verifying a JSON Web Token (JWT).
|
|
48
|
-
* By default, this middleware extracts the token from the `Authorization` header.
|
|
49
|
-
*
|
|
50
|
-
* @remarks
|
|
51
|
-
* If a JWT is successfully decoded, the following properties are added to the request object:
|
|
52
|
-
* - `request.canva.appId` - The ID of the app.
|
|
53
|
-
* - `request.canva.brandId` - The ID of the user's team.
|
|
54
|
-
* - `request.canva.userId` - The ID of the user.
|
|
55
|
-
*
|
|
56
|
-
* @param appId - The ID of the app.
|
|
57
|
-
* @param getTokenFromRequest - A function that extracts a token from the request. If a token isn't found, throw a `JWTAuthorizationError`.
|
|
58
|
-
* @returns An Express.js middleware for verifying and decoding JWTs.
|
|
59
|
-
*/
|
|
60
|
-
export function createJwtMiddleware(
|
|
61
|
-
appId: string,
|
|
62
|
-
getTokenFromRequest: GetTokenFromRequest = getTokenFromHttpHeader,
|
|
63
|
-
): (req: Request, res: Response, next: NextFunction) => void {
|
|
64
|
-
const jwksClient = new JwksClient({
|
|
65
|
-
cache: true,
|
|
66
|
-
cacheMaxAge: PUBLIC_KEY_DEFAULT_EXPIRY_MS,
|
|
67
|
-
timeout: PUBLIC_KEY_DEFAULT_FETCH_TIMEOUT_MS,
|
|
68
|
-
rateLimit: true,
|
|
69
|
-
jwksUri: createJwksUrl(appId),
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
return async (req, res, next) => {
|
|
73
|
-
try {
|
|
74
|
-
debugLogger(`processing JWT for '${req.url}'`);
|
|
75
|
-
|
|
76
|
-
const token = await getTokenFromRequest(req);
|
|
77
|
-
const unverifiedDecodedToken = jwt.decode(token, {
|
|
78
|
-
complete: true,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
if (unverifiedDecodedToken?.header?.kid == null) {
|
|
82
|
-
console.trace(
|
|
83
|
-
`jwtMiddleware: expected token to contain 'kid' claim header`,
|
|
84
|
-
);
|
|
85
|
-
return sendUnauthorizedResponse(res);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const key = await jwksClient.getSigningKey(
|
|
89
|
-
unverifiedDecodedToken.header.kid,
|
|
90
|
-
);
|
|
91
|
-
const publicKey = key.getPublicKey();
|
|
92
|
-
const verifiedToken = jwt.verify(token, publicKey, {
|
|
93
|
-
audience: appId,
|
|
94
|
-
complete: true,
|
|
95
|
-
}) as CanvaJwt;
|
|
96
|
-
const { payload } = verifiedToken;
|
|
97
|
-
debugLogger("payload: %O", payload);
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
payload.userId == null ||
|
|
101
|
-
payload.brandId == null ||
|
|
102
|
-
payload.aud == null
|
|
103
|
-
) {
|
|
104
|
-
console.trace(
|
|
105
|
-
"jwtMiddleware: failed to decode jwt missing fields from payload",
|
|
106
|
-
);
|
|
107
|
-
return sendUnauthorizedResponse(res);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
req["canva"] = {
|
|
111
|
-
appId: payload.aud,
|
|
112
|
-
brandId: payload.brandId,
|
|
113
|
-
userId: payload.userId,
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
return next();
|
|
117
|
-
} catch (e) {
|
|
118
|
-
if (e instanceof JWTAuthorizationError) {
|
|
119
|
-
return sendUnauthorizedResponse(res, e.message);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (e instanceof SigningKeyNotFoundError) {
|
|
123
|
-
return sendUnauthorizedResponse(
|
|
124
|
-
res,
|
|
125
|
-
`Public key not found. Ensure you have the correct App_ID set`,
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (e instanceof jwt.JsonWebTokenError) {
|
|
130
|
-
return sendUnauthorizedResponse(res, "Token is invalid");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (e instanceof jwt.TokenExpiredError) {
|
|
134
|
-
return sendUnauthorizedResponse(res, "Token expired");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return next(e);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export type GetTokenFromRequest = (req: Request) => Promise<string> | string;
|
|
143
|
-
|
|
144
|
-
export const getTokenFromQueryString: GetTokenFromRequest = (
|
|
145
|
-
req: Request,
|
|
146
|
-
): string => {
|
|
147
|
-
// The name of a query string parameter bearing the JWT
|
|
148
|
-
const tokenQueryStringParamName = "canva_user_token";
|
|
149
|
-
|
|
150
|
-
const queryParam = req.query[tokenQueryStringParamName];
|
|
151
|
-
if (!queryParam || typeof queryParam !== "string") {
|
|
152
|
-
console.trace(
|
|
153
|
-
`jwtMiddleware: missing "${tokenQueryStringParamName}" query parameter`,
|
|
154
|
-
);
|
|
155
|
-
throw new JWTAuthorizationError(
|
|
156
|
-
`Missing "${tokenQueryStringParamName}" query parameter`,
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (!looksLikeJWT(queryParam)) {
|
|
161
|
-
console.trace(
|
|
162
|
-
`jwtMiddleware: invalid "${tokenQueryStringParamName}" query parameter`,
|
|
163
|
-
);
|
|
164
|
-
throw new JWTAuthorizationError(
|
|
165
|
-
`Invalid "${tokenQueryStringParamName}" query parameter`,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return queryParam;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
export const getTokenFromHttpHeader: GetTokenFromRequest = (
|
|
173
|
-
req: Request,
|
|
174
|
-
): string => {
|
|
175
|
-
// The names of a HTTP header bearing the JWT, and a scheme
|
|
176
|
-
const headerName = "Authorization";
|
|
177
|
-
const schemeName = "Bearer";
|
|
178
|
-
|
|
179
|
-
const header = req.header(headerName);
|
|
180
|
-
if (!header) {
|
|
181
|
-
throw new JWTAuthorizationError(`Missing the "${headerName}" header`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (!header.match(new RegExp(`^${schemeName}\\s+[^\\s]+$`, "i"))) {
|
|
185
|
-
console.trace(
|
|
186
|
-
`jwtMiddleware: failed to match token in "${headerName}" header`,
|
|
187
|
-
);
|
|
188
|
-
throw new JWTAuthorizationError(
|
|
189
|
-
`Missing a "${schemeName}" token in the "${headerName}" header`,
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const token = header.replace(new RegExp(`^${schemeName}\\s+`, "i"), "");
|
|
194
|
-
if (!token || !looksLikeJWT(token)) {
|
|
195
|
-
throw new JWTAuthorizationError(
|
|
196
|
-
`Invalid "${schemeName}" token in the "${headerName}" header`,
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return token;
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* A class representing JWT validation errors in the JWT middleware.
|
|
205
|
-
* The error message provided to the constructor will be forwarded to the
|
|
206
|
-
* API consumer trying to access a JWT-protected endpoint.
|
|
207
|
-
* @private
|
|
208
|
-
*/
|
|
209
|
-
export class JWTAuthorizationError extends Error {
|
|
210
|
-
constructor(message: string) {
|
|
211
|
-
super(message);
|
|
212
|
-
|
|
213
|
-
Object.setPrototypeOf(this, JWTAuthorizationError.prototype);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const looksLikeJWT = (
|
|
218
|
-
token: string,
|
|
219
|
-
): boolean => // Base64 alphabet includes
|
|
220
|
-
// - letters (a-z and A-Z)
|
|
221
|
-
// - digits (0-9)
|
|
222
|
-
// - two special characters (+/ or -_)
|
|
223
|
-
// - padding (=)
|
|
224
|
-
token.match(/^[a-z0-9+/\-_=.]+$/i) != null;
|