configure-auth0-token-vault 1.0.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/README.md +292 -0
- package/package.json +27 -0
- package/src/index.js +1182 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,1182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
|
|
7
|
+
const DEBUG = process.env.DEBUG === "true" || process.argv.includes("--debug");
|
|
8
|
+
|
|
9
|
+
function log(message) {
|
|
10
|
+
if (DEBUG) {
|
|
11
|
+
console.log(pc.dim(`[debug] ${message}`));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function checkScopesChanged(text) {
|
|
16
|
+
if (!text) return;
|
|
17
|
+
|
|
18
|
+
const lowerText = text.toLowerCase();
|
|
19
|
+
if (
|
|
20
|
+
lowerText.includes("required scopes have changed") ||
|
|
21
|
+
lowerText.includes("required scopes") ||
|
|
22
|
+
lowerText.includes("insufficient scopes")
|
|
23
|
+
) {
|
|
24
|
+
log(`Detected scopes issue in: ${text.substring(0, 200)}`);
|
|
25
|
+
p.log.error(
|
|
26
|
+
"The Auth0 CLI requires additional scopes to complete this operation.",
|
|
27
|
+
);
|
|
28
|
+
const shouldReauth = await p.confirm({
|
|
29
|
+
message: "Would you like to reauthenticate with the required scopes?",
|
|
30
|
+
initialValue: true,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (p.isCancel(shouldReauth) || !shouldReauth) {
|
|
34
|
+
p.cancel("Please run 'auth0 login' manually with the required scopes.");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
p.log.info("Reauthenticating...");
|
|
39
|
+
await execa("auth0", ["login"], { stdio: "inherit" });
|
|
40
|
+
p.log.success("Reauthentication successful! Please run this script again.");
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function runAuth0Command(args, options = {}) {
|
|
46
|
+
log(`Running: auth0 ${args.join(" ")}`);
|
|
47
|
+
|
|
48
|
+
const result = await execa("auth0", args, {
|
|
49
|
+
...options,
|
|
50
|
+
reject: false,
|
|
51
|
+
timeout: 30000,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
log(`exitCode: ${result.exitCode}`);
|
|
55
|
+
log(`stdout: ${result.stdout?.substring(0, 500)}`);
|
|
56
|
+
log(`stderr: ${result.stderr?.substring(0, 500)}`);
|
|
57
|
+
|
|
58
|
+
const allText = [result.stdout, result.stderr].filter(Boolean).join(" ");
|
|
59
|
+
await checkScopesChanged(allText);
|
|
60
|
+
|
|
61
|
+
if (result.exitCode !== 0) {
|
|
62
|
+
const error = new Error(`Command failed: auth0 ${args.join(" ")}`);
|
|
63
|
+
error.stdout = result.stdout;
|
|
64
|
+
error.stderr = result.stderr;
|
|
65
|
+
error.exitCode = result.exitCode;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runAuth0Api(method, path, data = null) {
|
|
73
|
+
const args = ["api", method, path, "--no-input"];
|
|
74
|
+
const options = {};
|
|
75
|
+
|
|
76
|
+
if (data) {
|
|
77
|
+
const jsonData = JSON.stringify(data);
|
|
78
|
+
log(
|
|
79
|
+
`Running: auth0 api ${method} ${path} (with data: ${jsonData.substring(0, 100)}...)`,
|
|
80
|
+
);
|
|
81
|
+
options.input = jsonData;
|
|
82
|
+
} else {
|
|
83
|
+
log(`Running: auth0 api ${method} ${path}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = await execa("auth0", args, {
|
|
87
|
+
...options,
|
|
88
|
+
reject: false,
|
|
89
|
+
timeout: 30000,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
log(`exitCode: ${result.exitCode}`);
|
|
93
|
+
log(`stdout: ${result.stdout?.substring(0, 500)}`);
|
|
94
|
+
log(`stderr: ${result.stderr?.substring(0, 500)}`);
|
|
95
|
+
|
|
96
|
+
const allText = [result.stdout, result.stderr].filter(Boolean).join(" ");
|
|
97
|
+
await checkScopesChanged(allText);
|
|
98
|
+
|
|
99
|
+
if (result.exitCode !== 0) {
|
|
100
|
+
const error = new Error(`Command failed: auth0 api ${method} ${path}`);
|
|
101
|
+
error.stdout = result.stdout;
|
|
102
|
+
error.stderr = result.stderr;
|
|
103
|
+
error.exitCode = result.exitCode;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Constants
|
|
111
|
+
const TOKEN_VAULT_GRANT_TYPE =
|
|
112
|
+
"urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token";
|
|
113
|
+
|
|
114
|
+
const CONNECTED_ACCOUNTS_SCOPES = [
|
|
115
|
+
"create:me:connected_accounts",
|
|
116
|
+
"read:me:connected_accounts",
|
|
117
|
+
"delete:me:connected_accounts",
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const MY_ACCOUNT_API_SCOPES = [
|
|
121
|
+
{ value: "read:me", description: "Read user profile" },
|
|
122
|
+
{ value: "update:me", description: "Update user profile" },
|
|
123
|
+
{ value: "delete:me", description: "Delete user account" },
|
|
124
|
+
{
|
|
125
|
+
value: "create:me:connected_accounts",
|
|
126
|
+
description: "Link external accounts",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
value: "read:me:connected_accounts",
|
|
130
|
+
description: "Read linked accounts",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
value: "delete:me:connected_accounts",
|
|
134
|
+
description: "Unlink external accounts",
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const TOKEN_VAULT_FLAVORS = {
|
|
139
|
+
connected_accounts: {
|
|
140
|
+
label: "Connected Accounts",
|
|
141
|
+
hint: "User-managed linked accounts via My Account API",
|
|
142
|
+
description:
|
|
143
|
+
"Allows users to link multiple external accounts to their Auth0 profile and manage them via the My Account API.",
|
|
144
|
+
},
|
|
145
|
+
refresh_token_exchange: {
|
|
146
|
+
label: "Refresh Token Exchange",
|
|
147
|
+
hint: "Exchange Auth0 refresh tokens for external tokens",
|
|
148
|
+
description:
|
|
149
|
+
"Backend services exchange Auth0 refresh tokens to retrieve external provider tokens without user interaction.",
|
|
150
|
+
},
|
|
151
|
+
access_token_exchange: {
|
|
152
|
+
label: "Access Token Exchange",
|
|
153
|
+
hint: "Exchange Auth0 access tokens for external tokens",
|
|
154
|
+
description:
|
|
155
|
+
"Backend APIs exchange Auth0 access tokens to retrieve external provider tokens on behalf of users.",
|
|
156
|
+
},
|
|
157
|
+
privileged_worker: {
|
|
158
|
+
label: "Privileged Worker Token Exchange",
|
|
159
|
+
hint: "M2M apps exchange signed JWTs for external tokens",
|
|
160
|
+
description:
|
|
161
|
+
"Machine-to-machine applications use signed JWT bearer tokens to retrieve external provider tokens without active user sessions.",
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// Main Function
|
|
167
|
+
// ============================================================================
|
|
168
|
+
|
|
169
|
+
async function main() {
|
|
170
|
+
console.clear();
|
|
171
|
+
|
|
172
|
+
p.intro(pc.bgCyan(pc.black(" Auth0 Token Vault Setup ")));
|
|
173
|
+
|
|
174
|
+
// Check Auth0 CLI installation
|
|
175
|
+
const cliInstalled = await checkAuth0CLI();
|
|
176
|
+
if (!cliInstalled) {
|
|
177
|
+
p.log.error("Auth0 CLI is not installed.");
|
|
178
|
+
p.note(
|
|
179
|
+
`Install via Homebrew:\n${pc.cyan("brew tap auth0/auth0-cli && brew install auth0")}\n\nOr visit: ${pc.cyan("https://github.com/auth0/auth0-cli")}`,
|
|
180
|
+
"Installation Required",
|
|
181
|
+
);
|
|
182
|
+
p.outro(pc.red("Setup cancelled."));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
p.log.success("Auth0 CLI detected");
|
|
187
|
+
|
|
188
|
+
// Check if logged in
|
|
189
|
+
const loggedIn = await checkAuth0Login();
|
|
190
|
+
if (!loggedIn) {
|
|
191
|
+
const shouldLogin = await p.confirm({
|
|
192
|
+
message: "You need to log in to Auth0 CLI. Log in now?",
|
|
193
|
+
initialValue: true,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (p.isCancel(shouldLogin) || !shouldLogin) {
|
|
197
|
+
p.cancel("Login required to continue.");
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await runAuth0Login();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Get tenant info
|
|
205
|
+
const tenantInfo = await getTenantInfo();
|
|
206
|
+
p.log.success(`Connected to tenant: ${pc.cyan(tenantInfo.domain)}`);
|
|
207
|
+
|
|
208
|
+
// Ask about application setup
|
|
209
|
+
const appChoice = await p.select({
|
|
210
|
+
message: "How would you like to configure the application?",
|
|
211
|
+
options: [
|
|
212
|
+
{ value: "new", label: "Create a new application" },
|
|
213
|
+
{ value: "existing", label: "Use an existing application" },
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (p.isCancel(appChoice)) {
|
|
218
|
+
p.cancel("Setup cancelled.");
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let app;
|
|
223
|
+
|
|
224
|
+
if (appChoice === "new") {
|
|
225
|
+
app = await createNewApplication();
|
|
226
|
+
} else {
|
|
227
|
+
app = await selectExistingApplication();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!app) {
|
|
231
|
+
p.cancel("No application selected.");
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
p.log.info(`Using application: ${pc.cyan(app.name)} (${pc.dim(app.id)})`);
|
|
236
|
+
|
|
237
|
+
// Configure basic Token Vault settings (always required)
|
|
238
|
+
await configureApplicationForTokenVault(app.id);
|
|
239
|
+
|
|
240
|
+
// Configure connections for Token Vault
|
|
241
|
+
await configureConnections(app.id);
|
|
242
|
+
|
|
243
|
+
// Ask which Token Vault flavor to configure
|
|
244
|
+
const flavor = await p.select({
|
|
245
|
+
message: "Which Token Vault configuration do you need?",
|
|
246
|
+
options: Object.entries(TOKEN_VAULT_FLAVORS).map(([value, config]) => ({
|
|
247
|
+
value,
|
|
248
|
+
label: config.label,
|
|
249
|
+
hint: config.hint,
|
|
250
|
+
})),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (p.isCancel(flavor)) {
|
|
254
|
+
p.cancel("Setup cancelled.");
|
|
255
|
+
process.exit(0);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Set up Connected Accounts (foundation for all Token Vault flavors)
|
|
259
|
+
await setupConnectedAccounts(app.id, tenantInfo.domain);
|
|
260
|
+
|
|
261
|
+
// Apply flavor-specific configuration
|
|
262
|
+
switch (flavor) {
|
|
263
|
+
case "connected_accounts":
|
|
264
|
+
// Base Connected Accounts setup is complete
|
|
265
|
+
break;
|
|
266
|
+
|
|
267
|
+
case "refresh_token_exchange":
|
|
268
|
+
await setupRefreshTokenExchange(app.id, tenantInfo.domain);
|
|
269
|
+
break;
|
|
270
|
+
|
|
271
|
+
case "access_token_exchange":
|
|
272
|
+
await setupAccessTokenExchange(app.id, tenantInfo.domain);
|
|
273
|
+
break;
|
|
274
|
+
|
|
275
|
+
case "privileged_worker":
|
|
276
|
+
await setupPrivilegedWorker(app.id, tenantInfo.domain);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Show completion summary
|
|
281
|
+
showCompletionSummary(app, tenantInfo.domain, flavor);
|
|
282
|
+
|
|
283
|
+
p.outro(pc.green("All done!"));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ============================================================================
|
|
287
|
+
// Auth0 CLI Helpers
|
|
288
|
+
// ============================================================================
|
|
289
|
+
|
|
290
|
+
async function checkAuth0CLI() {
|
|
291
|
+
try {
|
|
292
|
+
await runAuth0Command(["--version"]);
|
|
293
|
+
return true;
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function checkAuth0Login() {
|
|
300
|
+
try {
|
|
301
|
+
const { stdout } = await runAuth0Command([
|
|
302
|
+
"tenants",
|
|
303
|
+
"list",
|
|
304
|
+
"--json",
|
|
305
|
+
"--no-input",
|
|
306
|
+
]);
|
|
307
|
+
const tenants = JSON.parse(stdout || "[]");
|
|
308
|
+
return tenants.length > 0;
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function runAuth0Login() {
|
|
315
|
+
p.log.info("Opening browser for Auth0 login...");
|
|
316
|
+
try {
|
|
317
|
+
await runAuth0Command(["login"], { stdio: "inherit" });
|
|
318
|
+
p.log.success("Login successful!");
|
|
319
|
+
} catch (error) {
|
|
320
|
+
p.log.error("Login failed");
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function getTenantInfo() {
|
|
326
|
+
const { stdout } = await runAuth0Command([
|
|
327
|
+
"tenants",
|
|
328
|
+
"list",
|
|
329
|
+
"--json",
|
|
330
|
+
"--no-input",
|
|
331
|
+
]);
|
|
332
|
+
const tenants = JSON.parse(stdout || "[]");
|
|
333
|
+
|
|
334
|
+
if (tenants.length === 0) {
|
|
335
|
+
throw new Error("No tenants found. Please log in first.");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const activeTenant = tenants.find((t) => t.active) || tenants[0];
|
|
339
|
+
return { domain: activeTenant.name };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// Application Management
|
|
344
|
+
// ============================================================================
|
|
345
|
+
|
|
346
|
+
async function createNewApplication() {
|
|
347
|
+
const details = await p.group(
|
|
348
|
+
{
|
|
349
|
+
name: () =>
|
|
350
|
+
p.text({
|
|
351
|
+
message: "Enter application name:",
|
|
352
|
+
placeholder: "Token Vault App",
|
|
353
|
+
defaultValue: "Token Vault App",
|
|
354
|
+
}),
|
|
355
|
+
type: () =>
|
|
356
|
+
p.select({
|
|
357
|
+
message: "Select application type:",
|
|
358
|
+
options: [
|
|
359
|
+
{
|
|
360
|
+
value: "regular",
|
|
361
|
+
label: "Regular Web Application",
|
|
362
|
+
hint: "Web apps with a secure backend",
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
value: "m2m",
|
|
366
|
+
label: "Machine to Machine",
|
|
367
|
+
hint: "Backend services and workers",
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
}),
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
onCancel: () => {
|
|
374
|
+
p.cancel("Setup cancelled.");
|
|
375
|
+
process.exit(0);
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const s = p.spinner();
|
|
381
|
+
s.start("Creating application...");
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const sanitizedName = details.name.replace(/\+/g, "").trim();
|
|
385
|
+
log(`Creating app: name="${sanitizedName}", type="${details.type}"`);
|
|
386
|
+
|
|
387
|
+
const createArgs = [
|
|
388
|
+
"apps",
|
|
389
|
+
"create",
|
|
390
|
+
"--name",
|
|
391
|
+
`${sanitizedName}`,
|
|
392
|
+
"--type",
|
|
393
|
+
details.type,
|
|
394
|
+
"--json",
|
|
395
|
+
"--no-input",
|
|
396
|
+
];
|
|
397
|
+
|
|
398
|
+
log(`Full command: auth0 ${createArgs.join(" ")}`);
|
|
399
|
+
const { stdout } = await runAuth0Command(createArgs);
|
|
400
|
+
|
|
401
|
+
log(`Response: ${stdout}`);
|
|
402
|
+
const app = JSON.parse(stdout);
|
|
403
|
+
s.stop(`Application created: ${pc.cyan(app.name)}`);
|
|
404
|
+
return { id: app.client_id, name: app.name };
|
|
405
|
+
} catch (error) {
|
|
406
|
+
s.stop("Failed to create application");
|
|
407
|
+
p.log.error(`Command failed: ${error.message}`);
|
|
408
|
+
if (error.stdout) {
|
|
409
|
+
p.log.error(`stdout: ${error.stdout}`);
|
|
410
|
+
}
|
|
411
|
+
if (error.stderr) {
|
|
412
|
+
p.log.error(`stderr: ${error.stderr}`);
|
|
413
|
+
}
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function selectExistingApplication() {
|
|
419
|
+
const s = p.spinner();
|
|
420
|
+
s.start("Fetching applications...");
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const { stdout } = await runAuth0Command([
|
|
424
|
+
"apps",
|
|
425
|
+
"list",
|
|
426
|
+
"--json",
|
|
427
|
+
"--no-input",
|
|
428
|
+
]);
|
|
429
|
+
const apps = JSON.parse(stdout);
|
|
430
|
+
s.stop("Applications loaded");
|
|
431
|
+
|
|
432
|
+
const filteredApps = apps.filter((app) => app.name !== "All Applications");
|
|
433
|
+
|
|
434
|
+
if (filteredApps.length === 0) {
|
|
435
|
+
p.log.warning("No applications found. Creating a new one.");
|
|
436
|
+
return createNewApplication();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const selected = await p.select({
|
|
440
|
+
message: "Select an application:",
|
|
441
|
+
options: filteredApps.map((app) => ({
|
|
442
|
+
value: { id: app.client_id, name: app.name },
|
|
443
|
+
label: app.name,
|
|
444
|
+
hint: app.client_id,
|
|
445
|
+
})),
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
if (p.isCancel(selected)) {
|
|
449
|
+
p.cancel("Setup cancelled.");
|
|
450
|
+
process.exit(0);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return selected;
|
|
454
|
+
} catch (error) {
|
|
455
|
+
s.stop("Failed to fetch applications");
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ============================================================================
|
|
461
|
+
// Basic Token Vault Configuration (always applied)
|
|
462
|
+
// ============================================================================
|
|
463
|
+
|
|
464
|
+
async function configureApplicationForTokenVault(appId) {
|
|
465
|
+
const s = p.spinner();
|
|
466
|
+
s.start("Configuring application for Token Vault...");
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
const { stdout } = await runAuth0Command([
|
|
470
|
+
"apps",
|
|
471
|
+
"show",
|
|
472
|
+
appId,
|
|
473
|
+
"--json",
|
|
474
|
+
"--no-input",
|
|
475
|
+
]);
|
|
476
|
+
const app = JSON.parse(stdout);
|
|
477
|
+
|
|
478
|
+
let grantTypes = app.grant_types || ["authorization_code", "refresh_token"];
|
|
479
|
+
if (!grantTypes.includes(TOKEN_VAULT_GRANT_TYPE)) {
|
|
480
|
+
grantTypes.push(TOKEN_VAULT_GRANT_TYPE);
|
|
481
|
+
}
|
|
482
|
+
if (!grantTypes.includes("refresh_token")) {
|
|
483
|
+
grantTypes.push("refresh_token");
|
|
484
|
+
}
|
|
485
|
+
if (!grantTypes.includes("authorization_code")) {
|
|
486
|
+
grantTypes.push("authorization_code");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const updatePayload = {
|
|
490
|
+
is_first_party: true,
|
|
491
|
+
oidc_conformant: true,
|
|
492
|
+
grant_types: grantTypes,
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// Ensure confidential client (required for Token Vault)
|
|
496
|
+
if (app.token_endpoint_auth_method === "none") {
|
|
497
|
+
updatePayload.token_endpoint_auth_method = "client_secret_post";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
await runAuth0Api("patch", `clients/${appId}`, updatePayload);
|
|
501
|
+
|
|
502
|
+
s.stop("Application configured for Token Vault");
|
|
503
|
+
} catch (error) {
|
|
504
|
+
s.stop("Failed to configure application");
|
|
505
|
+
p.log.error(error.message);
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function configureConnections(appId) {
|
|
511
|
+
const s = p.spinner();
|
|
512
|
+
s.start("Fetching connections...");
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
const { stdout } = await runAuth0Api("get", "connections");
|
|
516
|
+
const connections = JSON.parse(stdout);
|
|
517
|
+
|
|
518
|
+
const supportedStrategies = [
|
|
519
|
+
"google-oauth2",
|
|
520
|
+
"github",
|
|
521
|
+
"linkedin",
|
|
522
|
+
"microsoft",
|
|
523
|
+
"facebook",
|
|
524
|
+
"twitter",
|
|
525
|
+
"dropbox",
|
|
526
|
+
"box",
|
|
527
|
+
"salesforce",
|
|
528
|
+
"fitbit",
|
|
529
|
+
"slack",
|
|
530
|
+
"spotify",
|
|
531
|
+
"stripe-connect",
|
|
532
|
+
"oauth2",
|
|
533
|
+
"oidc",
|
|
534
|
+
];
|
|
535
|
+
|
|
536
|
+
const eligibleConnections = connections.filter(
|
|
537
|
+
(conn) =>
|
|
538
|
+
supportedStrategies.includes(conn.strategy) ||
|
|
539
|
+
conn.strategy?.startsWith("oauth") ||
|
|
540
|
+
conn.strategy?.startsWith("oidc"),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
s.stop("Connections loaded");
|
|
544
|
+
|
|
545
|
+
if (eligibleConnections.length === 0) {
|
|
546
|
+
p.log.warning("No eligible social/enterprise connections found.");
|
|
547
|
+
p.note(
|
|
548
|
+
"Create a social connection first in the Auth0 Dashboard.",
|
|
549
|
+
"No Connections",
|
|
550
|
+
);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const selectedConnections = await p.multiselect({
|
|
555
|
+
message: "Select connections to enable for Token Vault:",
|
|
556
|
+
options: eligibleConnections.map((conn) => ({
|
|
557
|
+
value: conn,
|
|
558
|
+
label: conn.name,
|
|
559
|
+
hint: conn.strategy,
|
|
560
|
+
})),
|
|
561
|
+
required: false,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
if (p.isCancel(selectedConnections)) {
|
|
565
|
+
p.cancel("Setup cancelled.");
|
|
566
|
+
process.exit(0);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (!selectedConnections || selectedConnections.length === 0) {
|
|
570
|
+
p.log.warning("No connections selected.");
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for (const conn of selectedConnections) {
|
|
575
|
+
const connSpinner = p.spinner();
|
|
576
|
+
connSpinner.start(`Configuring ${conn.name}...`);
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
// Enable Connected Accounts for the connection
|
|
580
|
+
await runAuth0Api("patch", `connections/${conn.id}`, {
|
|
581
|
+
options: {
|
|
582
|
+
...conn.options,
|
|
583
|
+
connected_accounts: { active: true },
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Enable the connection for this application
|
|
588
|
+
await runAuth0Api("patch", `connections/${conn.id}`, {
|
|
589
|
+
enabled_clients: [
|
|
590
|
+
...new Set([...(conn.enabled_clients || []), appId]),
|
|
591
|
+
],
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
connSpinner.stop(`${pc.cyan(conn.name)} configured for Token Vault`);
|
|
595
|
+
} catch {
|
|
596
|
+
connSpinner.stop(`Could not fully configure ${conn.name}`);
|
|
597
|
+
p.log.warning(
|
|
598
|
+
`Please enable Connected Accounts manually in the Dashboard for ${conn.name}`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
} catch {
|
|
603
|
+
s.stop("Could not fetch connections");
|
|
604
|
+
p.log.warning("Please configure connections manually in the Dashboard.");
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ============================================================================
|
|
609
|
+
// Connected Accounts Setup
|
|
610
|
+
// ============================================================================
|
|
611
|
+
|
|
612
|
+
async function setupConnectedAccounts(appId, domain) {
|
|
613
|
+
p.log.step("Setting up Connected Accounts...");
|
|
614
|
+
|
|
615
|
+
// Enable My Account API
|
|
616
|
+
await enableMyAccountAPI(domain);
|
|
617
|
+
|
|
618
|
+
// Create client grant for My Account API
|
|
619
|
+
await createClientGrant(appId, domain);
|
|
620
|
+
|
|
621
|
+
// Configure MRRT
|
|
622
|
+
await configureMRRT(appId, domain);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function enableMyAccountAPI(domain) {
|
|
626
|
+
const s = p.spinner();
|
|
627
|
+
s.start("Enabling My Account API...");
|
|
628
|
+
|
|
629
|
+
const myAccountIdentifier = `https://${domain}/me/`;
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
const { stdout } = await runAuth0Api(
|
|
633
|
+
"get",
|
|
634
|
+
`resource-servers?identifier=${encodeURIComponent(myAccountIdentifier)}`,
|
|
635
|
+
);
|
|
636
|
+
const apis = JSON.parse(stdout);
|
|
637
|
+
|
|
638
|
+
if (apis && apis.length > 0) {
|
|
639
|
+
const existingApi = apis[0];
|
|
640
|
+
const existingScopes = existingApi.scopes || [];
|
|
641
|
+
const existingScopeValues = existingScopes.map((scope) => scope.value);
|
|
642
|
+
|
|
643
|
+
const missingScopes = MY_ACCOUNT_API_SCOPES.filter(
|
|
644
|
+
(scope) => !existingScopeValues.includes(scope.value),
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
const updatePayload = {};
|
|
648
|
+
|
|
649
|
+
if (missingScopes.length > 0) {
|
|
650
|
+
log(
|
|
651
|
+
`Adding missing scopes: ${missingScopes.map((sc) => sc.value).join(", ")}`,
|
|
652
|
+
);
|
|
653
|
+
updatePayload.scopes = [...existingScopes, ...missingScopes];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const currentUserPolicy =
|
|
657
|
+
existingApi.subject_type_authorization?.user?.policy;
|
|
658
|
+
if (currentUserPolicy !== "require_client_grant") {
|
|
659
|
+
log(
|
|
660
|
+
`Setting user access policy to require_client_grant (current: ${currentUserPolicy})`,
|
|
661
|
+
);
|
|
662
|
+
updatePayload.subject_type_authorization = {
|
|
663
|
+
user: {
|
|
664
|
+
policy: "require_client_grant",
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (Object.keys(updatePayload).length > 0) {
|
|
670
|
+
await runAuth0Api(
|
|
671
|
+
"patch",
|
|
672
|
+
`resource-servers/${existingApi.id}`,
|
|
673
|
+
updatePayload,
|
|
674
|
+
);
|
|
675
|
+
s.stop("My Account API configured with access policy and scopes");
|
|
676
|
+
} else {
|
|
677
|
+
s.stop("My Account API is already enabled");
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return { identifier: myAccountIdentifier, id: existingApi.id };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// API doesn't exist, try to create it
|
|
684
|
+
log("My Account API not found, attempting to create it");
|
|
685
|
+
try {
|
|
686
|
+
const { stdout: createOutput } = await runAuth0Api(
|
|
687
|
+
"post",
|
|
688
|
+
"resource-servers",
|
|
689
|
+
{
|
|
690
|
+
identifier: myAccountIdentifier,
|
|
691
|
+
name: "My Account",
|
|
692
|
+
scopes: MY_ACCOUNT_API_SCOPES,
|
|
693
|
+
signing_alg: "RS256",
|
|
694
|
+
allow_offline_access: true,
|
|
695
|
+
token_lifetime: 86400,
|
|
696
|
+
token_lifetime_for_web: 7200,
|
|
697
|
+
skip_consent_for_verifiable_first_party_clients: true,
|
|
698
|
+
subject_type_authorization: {
|
|
699
|
+
user: {
|
|
700
|
+
policy: "require_client_grant",
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
);
|
|
705
|
+
const createdApi = JSON.parse(createOutput);
|
|
706
|
+
s.stop("My Account API created and enabled");
|
|
707
|
+
return { identifier: myAccountIdentifier, id: createdApi.id };
|
|
708
|
+
} catch (createError) {
|
|
709
|
+
log(`Creation failed: ${createError.message}`);
|
|
710
|
+
if (createError.stdout) log(`stdout: ${createError.stdout}`);
|
|
711
|
+
if (createError.stderr) log(`stderr: ${createError.stderr}`);
|
|
712
|
+
|
|
713
|
+
const dashboardUrl = buildDashboardUrl(domain, "apis");
|
|
714
|
+
|
|
715
|
+
s.stop("My Account API needs manual activation");
|
|
716
|
+
p.note(
|
|
717
|
+
`The My Account API requires manual activation:\n\n` +
|
|
718
|
+
`1. Go to the Auth0 Dashboard:\n` +
|
|
719
|
+
` ${pc.cyan(dashboardUrl)}\n\n` +
|
|
720
|
+
`2. Look for the ${pc.bold("My Account API")} banner\n\n` +
|
|
721
|
+
`3. Click ${pc.bold("Activate")}\n\n` +
|
|
722
|
+
`Once activated, run this script again to complete the setup.`,
|
|
723
|
+
"Action Required",
|
|
724
|
+
);
|
|
725
|
+
return { identifier: myAccountIdentifier, id: null };
|
|
726
|
+
}
|
|
727
|
+
} catch (error) {
|
|
728
|
+
s.stop("Could not verify My Account API status");
|
|
729
|
+
log(`My Account API error: ${error.message}`);
|
|
730
|
+
if (error.stdout) log(`stdout: ${error.stdout}`);
|
|
731
|
+
if (error.stderr) log(`stderr: ${error.stderr}`);
|
|
732
|
+
|
|
733
|
+
p.log.warning(
|
|
734
|
+
"Please ensure My Account API is enabled in your Auth0 Dashboard.",
|
|
735
|
+
);
|
|
736
|
+
return { identifier: myAccountIdentifier, id: null };
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async function createClientGrant(appId, domain) {
|
|
741
|
+
const s = p.spinner();
|
|
742
|
+
s.start("Creating client grant for My Account API...");
|
|
743
|
+
const myAccountIdentifier = `https://${domain}/me/`;
|
|
744
|
+
|
|
745
|
+
try {
|
|
746
|
+
const { stdout: grantsOutput } = await runAuth0Api(
|
|
747
|
+
"get",
|
|
748
|
+
`client-grants?client_id=${appId}&audience=${encodeURIComponent(myAccountIdentifier)}`,
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
const grants = JSON.parse(grantsOutput);
|
|
752
|
+
const userGrant = grants.find((g) => g.subject_type === "user");
|
|
753
|
+
|
|
754
|
+
if (userGrant) {
|
|
755
|
+
const existingScopes = userGrant.scope || [];
|
|
756
|
+
const newScopes = [
|
|
757
|
+
...new Set([...existingScopes, ...CONNECTED_ACCOUNTS_SCOPES]),
|
|
758
|
+
];
|
|
759
|
+
|
|
760
|
+
await runAuth0Api("patch", `client-grants/${userGrant.id}`, {
|
|
761
|
+
scope: newScopes,
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
s.stop("Client grant updated with Connected Accounts scopes");
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
} catch {
|
|
768
|
+
// Grant doesn't exist, create it
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
try {
|
|
772
|
+
await runAuth0Api("post", "client-grants", {
|
|
773
|
+
client_id: appId,
|
|
774
|
+
audience: myAccountIdentifier,
|
|
775
|
+
scope: CONNECTED_ACCOUNTS_SCOPES,
|
|
776
|
+
subject_type: "user",
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
s.stop("Client grant created for My Account API (user access)");
|
|
780
|
+
} catch (error) {
|
|
781
|
+
if (error.message?.includes("already exists")) {
|
|
782
|
+
s.stop("Client grant already exists");
|
|
783
|
+
} else {
|
|
784
|
+
s.stop("Could not create client grant automatically");
|
|
785
|
+
log(`Client grant error: ${error.message}`);
|
|
786
|
+
if (error.stdout) log(`stdout: ${error.stdout}`);
|
|
787
|
+
if (error.stderr) log(`stderr: ${error.stderr}`);
|
|
788
|
+
|
|
789
|
+
p.note(
|
|
790
|
+
`Create a client grant in the Auth0 Dashboard:\n\n` +
|
|
791
|
+
`1. Go to Applications -> APIs -> My Account\n` +
|
|
792
|
+
`2. Click the "Machine to Machine Applications" tab\n` +
|
|
793
|
+
`3. Find your application and authorize it\n` +
|
|
794
|
+
`4. Select the Connected Accounts scopes:\n` +
|
|
795
|
+
` - create:me:connected_accounts\n` +
|
|
796
|
+
` - read:me:connected_accounts\n` +
|
|
797
|
+
` - delete:me:connected_accounts`,
|
|
798
|
+
"Action Required",
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function configureMRRT(appId, domain) {
|
|
805
|
+
const s = p.spinner();
|
|
806
|
+
s.start("Configuring Multi-Resource Refresh Token (MRRT)...");
|
|
807
|
+
const myAccountIdentifier = `https://${domain}/me/`;
|
|
808
|
+
|
|
809
|
+
try {
|
|
810
|
+
const { stdout } = await runAuth0Command([
|
|
811
|
+
"apps",
|
|
812
|
+
"show",
|
|
813
|
+
appId,
|
|
814
|
+
"--json",
|
|
815
|
+
"--no-input",
|
|
816
|
+
]);
|
|
817
|
+
const app = JSON.parse(stdout);
|
|
818
|
+
|
|
819
|
+
const refreshTokenConfig = app.refresh_token || {};
|
|
820
|
+
const existingPolicies = refreshTokenConfig.policies || [];
|
|
821
|
+
|
|
822
|
+
const hasMyAccountInMRRT = existingPolicies.some(
|
|
823
|
+
(policy) => policy.audience === myAccountIdentifier,
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
if (!hasMyAccountInMRRT) {
|
|
827
|
+
const newPolicies = [
|
|
828
|
+
...existingPolicies,
|
|
829
|
+
{
|
|
830
|
+
audience: myAccountIdentifier,
|
|
831
|
+
scope: CONNECTED_ACCOUNTS_SCOPES,
|
|
832
|
+
},
|
|
833
|
+
];
|
|
834
|
+
|
|
835
|
+
await runAuth0Api("patch", `clients/${appId}`, {
|
|
836
|
+
refresh_token: {
|
|
837
|
+
...refreshTokenConfig,
|
|
838
|
+
policies: newPolicies,
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
s.stop("MRRT configured with My Account API");
|
|
843
|
+
} else {
|
|
844
|
+
s.stop("MRRT already includes My Account API");
|
|
845
|
+
}
|
|
846
|
+
} catch (error) {
|
|
847
|
+
s.stop("Could not configure MRRT automatically");
|
|
848
|
+
log(`MRRT error: ${error.message}`);
|
|
849
|
+
if (error.stdout) log(`stdout: ${error.stdout}`);
|
|
850
|
+
if (error.stderr) log(`stderr: ${error.stderr}`);
|
|
851
|
+
|
|
852
|
+
const settingsUrl = buildDashboardUrl(
|
|
853
|
+
domain,
|
|
854
|
+
`applications/${appId}/settings`,
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
p.note(
|
|
858
|
+
`Configure MRRT manually in the Auth0 Dashboard:\n\n` +
|
|
859
|
+
`1. Go to your application settings:\n` +
|
|
860
|
+
` ${pc.cyan(settingsUrl)}\n\n` +
|
|
861
|
+
`2. Scroll to ${pc.bold("Multi-Resource Refresh Token")}\n\n` +
|
|
862
|
+
`3. Click ${pc.bold("Edit Configuration")} and enable the My Account API`,
|
|
863
|
+
"Action Required: Configure MRRT",
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ============================================================================
|
|
869
|
+
// Refresh Token Exchange Setup
|
|
870
|
+
// ============================================================================
|
|
871
|
+
|
|
872
|
+
async function setupRefreshTokenExchange(appId, domain) {
|
|
873
|
+
// Show usage instructions
|
|
874
|
+
p.note(
|
|
875
|
+
`${pc.bold("Refresh Token Exchange Usage:")}\n\n` +
|
|
876
|
+
`To exchange an Auth0 refresh token for an external provider token:\n\n` +
|
|
877
|
+
`POST ${pc.cyan(`https://${domain}/oauth/token`)}\n\n` +
|
|
878
|
+
`${pc.bold("Parameters:")}\n` +
|
|
879
|
+
` grant_type: ${pc.dim(TOKEN_VAULT_GRANT_TYPE)}\n` +
|
|
880
|
+
` subject_token: ${pc.dim("<auth0_refresh_token>")}\n` +
|
|
881
|
+
` subject_token_type: ${pc.dim("urn:ietf:params:oauth:token-type:refresh_token")}\n` +
|
|
882
|
+
` requested_token_type: ${pc.dim("http://auth0.com/oauth/token-type/federated-connection-access-token")}\n` +
|
|
883
|
+
` connection: ${pc.dim("<connection_name>")}\n` +
|
|
884
|
+
` client_id: ${pc.dim("<your_client_id>")}\n` +
|
|
885
|
+
` client_secret: ${pc.dim("<your_client_secret>")}`,
|
|
886
|
+
"Token Exchange Endpoint",
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// ============================================================================
|
|
891
|
+
// Access Token Exchange Setup
|
|
892
|
+
// ============================================================================
|
|
893
|
+
|
|
894
|
+
async function setupAccessTokenExchange(appId, domain) {
|
|
895
|
+
// Ask if user wants to create a Custom API Client
|
|
896
|
+
const createCustomApiClient = await p.confirm({
|
|
897
|
+
message:
|
|
898
|
+
"Do you want to create a Custom API Client for your backend API? (Required for Access Token Exchange)",
|
|
899
|
+
initialValue: true,
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
if (p.isCancel(createCustomApiClient)) {
|
|
903
|
+
p.cancel("Setup cancelled.");
|
|
904
|
+
process.exit(0);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (createCustomApiClient) {
|
|
908
|
+
await createCustomAPIClient(domain);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Show usage instructions
|
|
912
|
+
p.note(
|
|
913
|
+
`${pc.bold("Access Token Exchange Usage:")}\n\n` +
|
|
914
|
+
`Your backend API exchanges Auth0 access tokens for external provider tokens:\n\n` +
|
|
915
|
+
`POST ${pc.cyan(`https://${domain}/oauth/token`)}\n\n` +
|
|
916
|
+
`${pc.bold("Parameters:")}\n` +
|
|
917
|
+
` grant_type: ${pc.dim(TOKEN_VAULT_GRANT_TYPE)}\n` +
|
|
918
|
+
` subject_token: ${pc.dim("<auth0_access_token>")}\n` +
|
|
919
|
+
` subject_token_type: ${pc.dim("urn:ietf:params:oauth:token-type:access_token")}\n` +
|
|
920
|
+
` requested_token_type: ${pc.dim("http://auth0.com/oauth/token-type/federated-connection-access-token")}\n` +
|
|
921
|
+
` connection: ${pc.dim("<connection_name>")}\n` +
|
|
922
|
+
` client_id: ${pc.dim("<custom_api_client_id>")}\n` +
|
|
923
|
+
` client_secret: ${pc.dim("<custom_api_client_secret>")}`,
|
|
924
|
+
"Token Exchange Endpoint",
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async function createCustomAPIClient(domain) {
|
|
929
|
+
const details = await p.group(
|
|
930
|
+
{
|
|
931
|
+
apiIdentifier: () =>
|
|
932
|
+
p.text({
|
|
933
|
+
message: "Enter your backend API identifier (audience):",
|
|
934
|
+
placeholder: "https://api.example.com",
|
|
935
|
+
validate: (value) => {
|
|
936
|
+
if (!value) return "API identifier is required";
|
|
937
|
+
if (!value.startsWith("http"))
|
|
938
|
+
return "API identifier should be a URL";
|
|
939
|
+
},
|
|
940
|
+
}),
|
|
941
|
+
name: () =>
|
|
942
|
+
p.text({
|
|
943
|
+
message: "Enter a name for the Custom API Client:",
|
|
944
|
+
placeholder: "Backend API Token Vault Client",
|
|
945
|
+
defaultValue: "Backend API Token Vault Client",
|
|
946
|
+
}),
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
onCancel: () => {
|
|
950
|
+
p.cancel("Setup cancelled.");
|
|
951
|
+
process.exit(0);
|
|
952
|
+
},
|
|
953
|
+
},
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
const s = p.spinner();
|
|
957
|
+
s.start("Creating Custom API Client...");
|
|
958
|
+
|
|
959
|
+
try {
|
|
960
|
+
// First, check if the API (resource server) exists
|
|
961
|
+
const { stdout: apisOutput } = await runAuth0Api(
|
|
962
|
+
"get",
|
|
963
|
+
`resource-servers?identifier=${encodeURIComponent(details.apiIdentifier)}`,
|
|
964
|
+
);
|
|
965
|
+
const apis = JSON.parse(apisOutput);
|
|
966
|
+
|
|
967
|
+
let apiId;
|
|
968
|
+
if (apis && apis.length > 0) {
|
|
969
|
+
apiId = apis[0].id;
|
|
970
|
+
log(`Found existing API: ${apiId}`);
|
|
971
|
+
} else {
|
|
972
|
+
// Create the API
|
|
973
|
+
const { stdout: newApiOutput } = await runAuth0Api(
|
|
974
|
+
"post",
|
|
975
|
+
"resource-servers",
|
|
976
|
+
{
|
|
977
|
+
identifier: details.apiIdentifier,
|
|
978
|
+
name: details.name.replace("Client", "").trim(),
|
|
979
|
+
signing_alg: "RS256",
|
|
980
|
+
allow_offline_access: true,
|
|
981
|
+
},
|
|
982
|
+
);
|
|
983
|
+
const newApi = JSON.parse(newApiOutput);
|
|
984
|
+
apiId = newApi.id;
|
|
985
|
+
log(`Created new API: ${apiId}`);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Create the Custom API Client (M2M application linked to the API)
|
|
989
|
+
const { stdout: clientOutput } = await runAuth0Command([
|
|
990
|
+
"apps",
|
|
991
|
+
"create",
|
|
992
|
+
"--name",
|
|
993
|
+
details.name,
|
|
994
|
+
"--type",
|
|
995
|
+
"m2m",
|
|
996
|
+
"--json",
|
|
997
|
+
"--no-input",
|
|
998
|
+
]);
|
|
999
|
+
const client = JSON.parse(clientOutput);
|
|
1000
|
+
|
|
1001
|
+
// Enable Token Vault grant type for the Custom API Client
|
|
1002
|
+
let clientGrantTypes = client.grant_types || ["client_credentials"];
|
|
1003
|
+
if (!clientGrantTypes.includes(TOKEN_VAULT_GRANT_TYPE)) {
|
|
1004
|
+
clientGrantTypes.push(TOKEN_VAULT_GRANT_TYPE);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
await runAuth0Api("patch", `clients/${client.client_id}`, {
|
|
1008
|
+
is_first_party: true,
|
|
1009
|
+
oidc_conformant: true,
|
|
1010
|
+
grant_types: clientGrantTypes,
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
s.stop("Custom API Client created");
|
|
1014
|
+
|
|
1015
|
+
p.note(
|
|
1016
|
+
`${pc.bold("Custom API Client Details:")}\n\n` +
|
|
1017
|
+
` Name: ${pc.cyan(client.name)}\n` +
|
|
1018
|
+
` Client ID: ${pc.dim(client.client_id)}\n` +
|
|
1019
|
+
` Client Secret: ${pc.dim(client.client_secret)}\n` +
|
|
1020
|
+
` API Audience: ${pc.dim(details.apiIdentifier)}\n\n` +
|
|
1021
|
+
`${pc.yellow("Save these credentials securely!")}`,
|
|
1022
|
+
"Custom API Client",
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
return { clientId: client.client_id, apiIdentifier: details.apiIdentifier };
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
s.stop("Failed to create Custom API Client");
|
|
1028
|
+
log(`Error: ${error.message}`);
|
|
1029
|
+
if (error.stdout) log(`stdout: ${error.stdout}`);
|
|
1030
|
+
if (error.stderr) log(`stderr: ${error.stderr}`);
|
|
1031
|
+
|
|
1032
|
+
p.log.warning(
|
|
1033
|
+
"Please create the Custom API Client manually in the Auth0 Dashboard.",
|
|
1034
|
+
);
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// ============================================================================
|
|
1040
|
+
// Privileged Worker Token Exchange Setup
|
|
1041
|
+
// ============================================================================
|
|
1042
|
+
|
|
1043
|
+
async function setupPrivilegedWorker(appId, domain) {
|
|
1044
|
+
// Configure the application for Private Key JWT authentication
|
|
1045
|
+
const configurePrivateKeyJwt = await p.confirm({
|
|
1046
|
+
message:
|
|
1047
|
+
"Do you want to configure Private Key JWT authentication for the worker application?",
|
|
1048
|
+
initialValue: true,
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
if (p.isCancel(configurePrivateKeyJwt)) {
|
|
1052
|
+
p.cancel("Setup cancelled.");
|
|
1053
|
+
process.exit(0);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (configurePrivateKeyJwt) {
|
|
1057
|
+
await configurePrivateKeyJwtAuth(appId, domain);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Show usage instructions
|
|
1061
|
+
p.note(
|
|
1062
|
+
`${pc.bold("Privileged Worker Token Exchange:")}\n\n` +
|
|
1063
|
+
`Worker applications use signed JWT bearer tokens to exchange for external provider tokens.\n\n` +
|
|
1064
|
+
`${pc.bold("JWT Requirements:")}\n` +
|
|
1065
|
+
` Header:\n` +
|
|
1066
|
+
` typ: "token-vault-req+jwt"\n` +
|
|
1067
|
+
` alg: "RS256" (or your signing algorithm)\n` +
|
|
1068
|
+
` kid: "<key_id>" (optional)\n\n` +
|
|
1069
|
+
` Payload:\n` +
|
|
1070
|
+
` sub: "<user_id>" (Auth0 user_id)\n` +
|
|
1071
|
+
` aud: "https://${domain}/"\n` +
|
|
1072
|
+
` iss: "<client_id>"\n` +
|
|
1073
|
+
` iat: <issued_at_timestamp>\n` +
|
|
1074
|
+
` exp: <expiration_timestamp>\n\n` +
|
|
1075
|
+
`${pc.bold("Token Exchange Request:")}\n` +
|
|
1076
|
+
`POST ${pc.cyan(`https://${domain}/oauth/token`)}\n\n` +
|
|
1077
|
+
` grant_type: ${pc.dim(TOKEN_VAULT_GRANT_TYPE)}\n` +
|
|
1078
|
+
` subject_token: ${pc.dim("<signed_jwt>")}\n` +
|
|
1079
|
+
` subject_token_type: ${pc.dim("urn:ietf:params:oauth:token-type:jwt")}\n` +
|
|
1080
|
+
` requested_token_type: ${pc.dim("http://auth0.com/oauth/token-type/federated-connection-access-token")}\n` +
|
|
1081
|
+
` connection: ${pc.dim("<connection_name>")}\n` +
|
|
1082
|
+
` client_id: ${pc.dim("<worker_client_id>")}\n` +
|
|
1083
|
+
` client_assertion_type: ${pc.dim("urn:ietf:params:oauth:client-assertion-type:jwt-bearer")}\n` +
|
|
1084
|
+
` client_assertion: ${pc.dim("<client_jwt>")}`,
|
|
1085
|
+
"Privileged Worker Configuration",
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async function configurePrivateKeyJwtAuth(appId, domain) {
|
|
1090
|
+
const s = p.spinner();
|
|
1091
|
+
s.start("Configuring Private Key JWT authentication...");
|
|
1092
|
+
|
|
1093
|
+
try {
|
|
1094
|
+
// Update the application to use Private Key JWT
|
|
1095
|
+
await runAuth0Api("patch", `clients/${appId}`, {
|
|
1096
|
+
token_endpoint_auth_method: "private_key_jwt",
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
s.stop("Application configured for Private Key JWT");
|
|
1100
|
+
|
|
1101
|
+
const settingsUrl = buildDashboardUrl(
|
|
1102
|
+
domain,
|
|
1103
|
+
`applications/${appId}/credentials`,
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1106
|
+
p.note(
|
|
1107
|
+
`${pc.bold("Next Steps:")}\n\n` +
|
|
1108
|
+
`1. Generate an RSA key pair for signing JWTs\n\n` +
|
|
1109
|
+
`2. Add the public key to your application:\n` +
|
|
1110
|
+
` ${pc.cyan(settingsUrl)}\n\n` +
|
|
1111
|
+
`3. Go to the ${pc.bold("Credentials")} tab\n\n` +
|
|
1112
|
+
`4. Under ${pc.bold("Public Key")}, upload your public key (PEM or JWKS format)\n\n` +
|
|
1113
|
+
`${pc.bold("Generate keys with OpenSSL:")}\n` +
|
|
1114
|
+
` ${pc.dim("openssl genrsa -out private.pem 2048")}\n` +
|
|
1115
|
+
` ${pc.dim("openssl rsa -in private.pem -pubout -out public.pem")}`,
|
|
1116
|
+
"Configure Public Key",
|
|
1117
|
+
);
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
s.stop("Could not configure Private Key JWT automatically");
|
|
1120
|
+
log(`Error: ${error.message}`);
|
|
1121
|
+
|
|
1122
|
+
p.log.warning(
|
|
1123
|
+
"Please configure Private Key JWT authentication manually in the Dashboard.",
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// ============================================================================
|
|
1129
|
+
// Helpers
|
|
1130
|
+
// ============================================================================
|
|
1131
|
+
|
|
1132
|
+
function buildDashboardUrl(domain, path) {
|
|
1133
|
+
const domainParts = domain.split(".");
|
|
1134
|
+
if (domainParts.length === 4) {
|
|
1135
|
+
const [tenant, region] = domainParts;
|
|
1136
|
+
return `https://manage.auth0.com/dashboard/${region}/${tenant}/${path}`;
|
|
1137
|
+
} else {
|
|
1138
|
+
const [tenant] = domainParts;
|
|
1139
|
+
return `https://manage.auth0.com/dashboard/us/${tenant}/${path}`;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function showCompletionSummary(app, domain, flavor) {
|
|
1144
|
+
const flavorConfig = TOKEN_VAULT_FLAVORS[flavor];
|
|
1145
|
+
const settingsUrl = buildDashboardUrl(
|
|
1146
|
+
domain,
|
|
1147
|
+
`applications/${app.id}/settings`,
|
|
1148
|
+
);
|
|
1149
|
+
|
|
1150
|
+
const docsUrls = {
|
|
1151
|
+
connected_accounts:
|
|
1152
|
+
"https://auth0.com/docs/secure/call-apis-on-users-behalf/token-vault/connected-accounts-for-token-vault",
|
|
1153
|
+
refresh_token_exchange:
|
|
1154
|
+
"https://auth0.com/docs/secure/call-apis-on-users-behalf/token-vault/refresh-token-exchange-with-token-vault",
|
|
1155
|
+
access_token_exchange:
|
|
1156
|
+
"https://auth0.com/docs/secure/call-apis-on-users-behalf/token-vault/access-token-exchange-with-token-vault",
|
|
1157
|
+
privileged_worker:
|
|
1158
|
+
"https://auth0.com/docs/secure/call-apis-on-users-behalf/token-vault/privileged-worker-token-exchange-with-token-vault",
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
p.note(
|
|
1162
|
+
`${pc.bold("Application Details:")}\n` +
|
|
1163
|
+
` Name: ${pc.cyan(app.name)}\n` +
|
|
1164
|
+
` Client ID: ${pc.dim(app.id)}\n\n` +
|
|
1165
|
+
`${pc.bold("Configuration:")}\n` +
|
|
1166
|
+
` Type: ${pc.cyan(flavorConfig.label)}\n\n` +
|
|
1167
|
+
`${pc.bold("Settings:")}\n` +
|
|
1168
|
+
` ${pc.cyan(settingsUrl)}\n\n` +
|
|
1169
|
+
`${pc.bold("Documentation:")}\n` +
|
|
1170
|
+
` ${pc.cyan(docsUrls[flavor])}`,
|
|
1171
|
+
"Setup Complete",
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// ============================================================================
|
|
1176
|
+
// Entry Point
|
|
1177
|
+
// ============================================================================
|
|
1178
|
+
|
|
1179
|
+
main().catch((error) => {
|
|
1180
|
+
p.log.error(error.message);
|
|
1181
|
+
process.exit(1);
|
|
1182
|
+
});
|