-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathauth.ts
284 lines (242 loc) · 9 KB
/
auth.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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
/* eslint-disable no-console */
import * as chalk from "chalk";
import * as crypto from "crypto";
import { config as envConfig } from "dotenv";
import * as express from "express";
import * as cookieParser from "cookie-parser";
import * as basicAuth from "express-basic-auth";
import { JSONFileDatabase } from "../database/database";
import { getTokenFromQueryString } from "../../utils/backend/jwt_middleware/jwt_middleware";
import { createJwtMiddleware } from "../../utils/backend/jwt_middleware";
envConfig();
/**
* These are the hard-coded credentials for logging in to this template.
*/
const USERNAME = "username";
const PASSWORD = "password";
const COOKIE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
const CANVA_BASE_URL = "https://canva.com";
interface Data {
users: string[];
}
/**
* For more information on Authentication, refer to our documentation: {@link https://www.canva.dev/docs/apps/authenticating-users/#types-of-authentication}.
*/
export const createAuthRouter = () => {
const APP_ID = getAppId();
/**
* Set up a database with a "users" table. In this example code, the
* database is simply a JSON file.
*/
const db = new JSONFileDatabase<Data>({ users: [] });
const router = express.Router();
/**
* The `cookieParser` middleware allows the routes to read and write cookies.
*
* By passing a value into the middleware, we enable the "signed cookies" feature of Express.js. The
* value should be static and cryptographically generated. If it's dynamic (as shown below), cookies
* won't persist between server restarts.
*
* TODO: Replace `crypto.randomUUID()` with a static value, loaded via an environment variable.
*/
router.use(cookieParser(crypto.randomUUID()));
/**
* This endpoint is hit at the start of the authentication flow. It contains a state which must
* be passed back to canva so that Canva can verify the response. It must also set a nonce in the
* user's browser cookies and send the nonce back to Canva as a url parameter.
*
* If Canva can validate the state, it will then redirect back to the chosen redirect url.
*/
router.get("/configuration/start", async (req, res) => {
/**
* Generate a unique nonce for each request. A nonce is a random, single-use value
* that's impossible to guess or enumerate. We recommended using a Version 4 UUID that
* is cryptographically secure, such as one generated by the `randomUUID` method.
*/
const nonce = crypto.randomUUID();
// Set the expiry time for the nonce. We recommend 5 minutes.
const expiry = Date.now() + COOKIE_EXPIRY_MS;
// Create a JSON string that contains the nonce and an expiry time
const nonceWithExpiry = JSON.stringify([nonce, expiry]);
// Set a cookie that contains the nonce and the expiry time
res.cookie("nonce", nonceWithExpiry, {
secure: true,
httpOnly: true,
maxAge: COOKIE_EXPIRY_MS,
signed: true,
});
// Create the query parameters that Canva requires
const params = new URLSearchParams({
nonce,
state: req?.query?.state?.toString() || "",
});
// Redirect to Canva with required parameters
res.redirect(302, `${CANVA_BASE_URL}/apps/configure/link?${params}`);
});
/**
* This endpoint renders a login page. Once the user logs in, they're
* redirected back to Canva, which completes the authentication flow.
*/
router.get(
"/redirect-url",
/**
* Use a JSON Web Token (JWT) to verify incoming requests. The JWT is
* extracted from the `canva_user_token` parameter.
*/
createJwtMiddleware(APP_ID, getTokenFromQueryString),
/**
* Warning: For demonstration purposes, we're using basic authentication and
* hard- coding a username and password. This is not a production-ready
* solution!
*/
basicAuth({
users: { [USERNAME]: PASSWORD },
challenge: true,
}),
async (req, res) => {
const failureResponse = () => {
const params = new URLSearchParams({
success: "false",
state: req.query.state?.toString() || "",
});
res.redirect(302, `${CANVA_BASE_URL}/apps/configured?${params}`);
};
// Get the nonce and expiry time stored in the cookie.
const cookieNonceAndExpiry = req.signedCookies.nonce;
// Get the nonce from the query parameter.
const queryNonce = req.query.nonce?.toString();
// After reading the cookie, clear it. This forces abandoned auth flows to be restarted.
res.clearCookie("nonce");
let cookieNonce = "";
let expiry = 0;
try {
[cookieNonce, expiry] = JSON.parse(cookieNonceAndExpiry);
} catch {
// If the nonce can't be parsed, assume something has been compromised and exit.
return failureResponse();
}
// If the nonces are empty, exit the authentication flow.
if (
isEmpty(cookieNonceAndExpiry) ||
isEmpty(queryNonce) ||
isEmpty(cookieNonce)
) {
return failureResponse();
}
/**
* Check that:
*
* - The nonce in the cookie and query parameter contain the same value
* - The nonce has not expired
*
* **Note:** We could rely on the cookie expiry, but that is vulnerable to tampering
* with the browser's time. This allows us to double-check based on server time.
*/
if (expiry < Date.now() || cookieNonce !== queryNonce) {
return failureResponse();
}
// Get the userId from JWT middleware
const { userId } = req.canva;
// Load the database
const data = await db.read();
// Add the user to the database
if (!data.users.includes(userId)) {
data.users.push(userId);
await db.write(data);
}
// Create query parameters for redirecting back to Canva
const params = new URLSearchParams({
success: "true",
state: req?.query?.state?.toString() || "",
});
// Redirect the user back to Canva
res.redirect(302, `${CANVA_BASE_URL}/apps/configured?${params}`);
}
);
/**
* TODO: Add this middleware to all routes that will receive requests from
* your app.
*/
const jwtMiddleware = createJwtMiddleware(APP_ID);
/**
* This endpoint is called when a user disconnects an app from their account.
* The app is expected to de-authenticate the user on its backend, so if the
* user reconnects the app, they'll need to re-authenticate.
*
* Note: The name of the endpoint is *not* configurable.
*
* Note: This endpoint is called by Canva's backend directly and must be
* exposed via a public URL. To test this endpoint, add a proxy URL, such as
* one generated by nGrok, to the 'Add authentication' section in the
* Developer Portal. Localhost addresses will not work to test this endpoint.
*/
router.post("/configuration/delete", jwtMiddleware, async (req, res) => {
// Get the userId from JWT middleware
const { userId } = req.canva;
// Load the database
const data = await db.read();
// Remove the user from the database
await db.write({
users: data.users.filter((user) => user !== userId),
});
// Confirm that the user was removed
res.send({
type: "SUCCESS",
});
});
/**
* All routes that start with /api will be protected by JWT authentication
*/
router.use("/api", jwtMiddleware);
/**
* This endpoint checks if a user is authenticated.
*/
router.post("/api/authentication/status", async (req, res) => {
// Load the database
const data = await db.read();
// Check if the user is authenticated
const isAuthenticated = data.users.includes(req.canva.userId);
// Return the authentication status
res.send({
isAuthenticated,
});
});
return router;
};
/**
* Checks if a given param is nullish or an empty string
*
* @param str The string to check
* @returns true if the string is nullish or empty, false otherwise
*/
function isEmpty(str?: string): boolean {
return str == null || str.length === 0;
}
/**
* Retrieves the CANVA_APP_ID from the environment variables.
* Throws an error if the CANVA_APP_ID environment variable is undefined or set to a default value.
*
* @returns {string} The Canva App ID
* @throws {Error} If CANVA_APP_ID environment variable is undefined or set to a default value
*/
function getAppId(): string {
// TODO: Set the CANVA_APP_ID environment variable in the project's .env file
const appId = process.env.CANVA_APP_ID;
if (!appId) {
throw new Error(
`The CANVA_APP_ID environment variable is undefined. Set the variable in the project's .env file.`
);
}
if (appId === "YOUR_APP_ID_HERE") {
console.log(
chalk.bgRedBright("Default 'CANVA_APP_ID' environment variable detected.")
);
console.log(
"Please update the 'CANVA_APP_ID' environment variable in your project's `.env` file " +
`with the App ID obtained from the Canva Developer Portal: ${chalk.greenBright(
"https://www.canva.com/developers/apps"
)}\n`
);
}
return appId;
}