profoundjs 6.6.1 → 7.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.
@@ -47,7 +47,8 @@ const IBMi = iutils.isIBMi();
47
47
  "h",
48
48
  "c",
49
49
  "s",
50
- "v"
50
+ "v",
51
+ "installSamples"
51
52
  ],
52
53
  string: []
53
54
  };
@@ -62,6 +63,7 @@ const IBMi = iutils.isIBMi();
62
63
  -c, --configure Update config.js, if present.
63
64
  -s, --silent No interactive prompts.
64
65
  -v, --validate Validate silent mode arguments only.
66
+ --installSamples Install sample code
65
67
 
66
68
  Creates config.js, if not present, and completes installation.
67
69
  --configure can be specified to update an existing config.js before proceeding.
@@ -73,6 +75,9 @@ If --silent is specified, configuration values are read from arguments instead.
73
75
  If arguments are not valid, the script will exit with code 2 and error messages
74
76
  will be output to stderr in JSON format.
75
77
 
78
+ --installSamples installs sample modules, workspaces, plugins, and other sample
79
+ code.
80
+
76
81
  Valid arguments for --silent mode:
77
82
 
78
83
  ` + await buildArgHelp();
@@ -146,10 +151,12 @@ Valid arguments for --silent mode:
146
151
  const gitSupported = ["win32", "darwin", "linux"].includes(os.platform());
147
152
 
148
153
  const configPath = iutils.getConfigPath();
154
+ // Note: because an eval() may set the "config" variable subsequently, "config" cannot be a const.
155
+ // eslint-disable-next-line prefer-const
149
156
  let config = fileExists(configPath) ? require(configPath) : {};
150
157
  const strtcpsvr_config = {};
151
158
 
152
- let svrname, autostart, ccsid, nodePath;
159
+ let svrname, autostart, ccsid, nodePath, installSamples;
153
160
 
154
161
  // If config.js doesn't exist, or if --configure is passed, prompt and create/update config.js.
155
162
  if (!fileExists(configPath) || args["configure"] === true) {
@@ -256,6 +263,7 @@ Valid arguments for --silent mode:
256
263
  delete config.profounduiLibrary;
257
264
  }
258
265
  }
266
+
259
267
  fs.writeFileSync(configPath, stringifyConfig(config));
260
268
  console.log("");
261
269
  console.log("config.js updated.");
@@ -263,7 +271,7 @@ Valid arguments for --silent mode:
263
271
  }
264
272
  else {
265
273
  let content = fs.readFileSync(path.join(iutils.getSetupDir(), "config.js"), "utf8");
266
- eval("config = " + content.substr(content.indexOf("{")));
274
+ eval("config = " + content.substring(content.indexOf("{")));
267
275
  config.port = answers.port;
268
276
  if (answers.connectorLibrary) {
269
277
  config.connectorLibrary = answers.connectorLibrary;
@@ -304,6 +312,8 @@ Valid arguments for --silent mode:
304
312
  console.log("config.js created.");
305
313
  console.log("");
306
314
  }
315
+
316
+ installSamples = answers.installSamples == true;
307
317
  }
308
318
 
309
319
  // Complete installation.
@@ -316,7 +326,7 @@ Valid arguments for --silent mode:
316
326
  if (config.connectorLibrary !== undefined) {
317
327
  setupArgs.push("--ibmi-connector-library");
318
328
  }
319
- if (svrname) {
329
+ if (svrname) {
320
330
  setupArgs.push("--ibmi-instance=" + svrname);
321
331
  if (autostart) {
322
332
  setupArgs.push("--ibmi-instance-autostart");
@@ -335,6 +345,11 @@ Valid arguments for --silent mode:
335
345
  if (gitSupported && config.gitSupport !== false && nodeMajorVersion < 18) {
336
346
  setupArgs.push("--nodegit");
337
347
  }
348
+
349
+ if (installSamples || args.installSamples) {
350
+ setupArgs.push("--installSamples");
351
+ }
352
+
338
353
  if (setupArgs.length === 0) {
339
354
  setupArgs = "";
340
355
  }
@@ -372,7 +387,7 @@ async function ask(questionParm, configs, recursive) {
372
387
  if (typeof defaultAnswer !== "string") defaultAnswer = String(defaultAnswer);
373
388
 
374
389
  if (defaultAnswer !== "") {
375
- let lastChar = question.substr(question.length - 1, 1);
390
+ const lastChar = question.substr(question.length - 1, 1);
376
391
  if (lastChar === ":" || lastChar === "?") {
377
392
  question = question.substr(0, question.length - 1) + " (" + defaultAnswer + ")" + lastChar;
378
393
  }
@@ -408,9 +423,9 @@ async function ask(questionParm, configs, recursive) {
408
423
  }
409
424
 
410
425
  function fileExists(file) {
411
- var exists = false;
426
+ let exists = false;
412
427
  try {
413
- var stat = fs.statSync(file);
428
+ const stat = fs.statSync(file);
414
429
  if (stat && stat.isFile()) exists = true;
415
430
  }
416
431
  catch (err) {
@@ -430,7 +445,7 @@ function validateName(name) {
430
445
  function validateLibrary(name) {
431
446
  const err = validateName(name);
432
447
  if (err === true) {
433
- if (iutils.libraryExists(name)) return "Library " + name.toUpperCase() + " already exists. Please use a different name.";
448
+ if (iutils.libraryExists(name)) return "Library " + name.toUpperCase() + " already exists. Please use a different name.";
434
449
  }
435
450
  else return err;
436
451
  return true;
@@ -439,8 +454,8 @@ function validateLibrary(name) {
439
454
  function validateServer(name) {
440
455
  const err = validateName(name);
441
456
  if (err === true) {
442
- const exists = iutils.getIBMiInstances().some(el => el.name === name.toUpperCase());
443
- if (exists) return "Server Instance " + name.toUpperCase() + " already exists. Please use a different name.";
457
+ const exists = iutils.getIBMiInstances().some(el => el.name === name.toUpperCase());
458
+ if (exists) return "Server Instance " + name.toUpperCase() + " already exists. Please use a different name.";
444
459
  }
445
460
  else return err;
446
461
  return true;
@@ -487,12 +502,12 @@ function stringifyConfig(config) {
487
502
  // JavaScript functions (like connectorIPFilter) will be lost when doing JSON.stringify,
488
503
  // so we need to do some magic to preserve them.
489
504
 
490
- let funkz = [];
505
+ const funkz = [];
491
506
  let configString = "\nmodule.exports = ";
492
507
 
493
508
  configString += JSON.stringify(config, (key, val) => {
494
509
  if (typeof val === "function") {
495
- let n = funkz.length;
510
+ const n = funkz.length;
496
511
  funkz.push(val.toString());
497
512
  return `___function${n}___`;
498
513
  }
@@ -501,7 +516,7 @@ function stringifyConfig(config) {
501
516
 
502
517
  configString += "\n";
503
518
 
504
- for (var i = 0; i < funkz.length; i++) {
519
+ for (let i = 0; i < funkz.length; i++) {
505
520
  configString = configString.replace(`"___function${i}___"`, funkz[i]);
506
521
  }
507
522
 
@@ -605,7 +620,7 @@ function getPUIInstanceData(name) {
605
620
  }
606
621
 
607
622
  // Replace PUI Developer name in doc root, if set.
608
- let developer = getPUIDefine("DEVELOPER");
623
+ const developer = getPUIDefine("DEVELOPER");
609
624
  if (developer) {
610
625
  docRoot = docRoot.replace(/\$\{DEVELOPER\}/g, developer);
611
626
  }
@@ -824,13 +839,13 @@ function validate(question, answer) {
824
839
  validator = validatePUIInstance;
825
840
  }
826
841
  else if (question.id === "connectorLibrary") {
827
- validator = validateLibrary;
842
+ validator = validateLibrary;
828
843
  }
829
844
  else if (question.id === "connectorIASP") {
830
845
  validator = validateIASP;
831
846
  }
832
847
  else if (question.id === "strtcpsvr_svrname") {
833
- validator = validateServer;
848
+ validator = validateServer;
834
849
  }
835
850
  else if (question.type === "boolean") {
836
851
  validator = validateYesNo;
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const profoundjs = require("profoundjs");
6
+ profoundjs.genKey(__filename);
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const profoundjs = require("profoundjs");
6
+ profoundjs.getPJSCALLKey(__filename);
@@ -124,6 +124,13 @@
124
124
  "textType": "ibmi_name"
125
125
  }
126
126
  ]
127
+ },
128
+ {
129
+ "id": "installSamples",
130
+ "type": "boolean",
131
+ "label": "Install sample code",
132
+ "prompt": "Install sample code?",
133
+ "default": true
127
134
  }
128
135
  ],
129
136
  "commands": {
@@ -0,0 +1,9 @@
1
+ {
2
+ "open files": [
3
+ {
4
+ "name": "README.md"
5
+ }
6
+ ],
7
+ "active tab": 0,
8
+ "active widgets panel tab": 2
9
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "description": "Sample Sign on via OAuth2 and Call an API",
3
+ "routes": {
4
+ "useful-app.json": {
5
+ "method": "get",
6
+ "route": "useful-app",
7
+ "rdf": true
8
+ },
9
+ "authpage.js": {
10
+ "method": "get",
11
+ "route": "authpage"
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,251 @@
1
+ # OAuth2 Sample
2
+ This sample workspace provides a sample sign on screen, a screen for calling an API, a sample API, and back-end code to glue these parts together.
3
+
4
+ The sample application, "useful-app.json", requires users to authenticate via an OAuth2 provider. Once authenticated, a user can click a button causing the Profound.js (PJS) server to call the provided sample API route, defined in "mywebservices.api.json". The HTTP request to the API route includes an HTTP header containing the user's OAuth2 token, which verifies the user's identity. The API will return a message if the PJS security store permits the user to access the sample API.
5
+
6
+ Disclaimer: this sample is provided to demonstrate OAuth2 for Profound API. This example and its code are not meant as a demonstration of good sign-on coding and are provided AS-IS with no warranty. The sign-on code should not be used in production servers.
7
+ ## Setup
8
+
9
+ ### 1. Install Sample Workspace: oauth2sample
10
+ If you are reading this README from within your own Profound.js instance inside the /modules/oauth2sample/ directory, then you can skip this step.
11
+
12
+ Otherwise, the oauth2sample workspace can be installed by running this command from the directory of your Profound.js installation:
13
+ `node complete_install --installSamples`
14
+
15
+ If you do not already have a config.js file, then you will be prompted for some settings. When asked about sample code, answer y for yes.
16
+
17
+ For example, your terminal screen may look like below:
18
+ ```
19
+ $ cd /your-profoundjs-instance/
20
+ $ node complete_install --installSamples
21
+
22
+ Specify port number for Profound.js server (8081):
23
+ Install with Git integration (y)? n
24
+ Install sample code (y)? y
25
+
26
+ config.js updated.
27
+ [...]
28
+ Copying pjssamples.
29
+ Copying sample workspace into /your-profoundjs-instance/modules/oauth2sample
30
+ [...]
31
+ Profound.js installation complete.
32
+ ```
33
+
34
+ ### 2. Setup an app with an OAuth2 Provider
35
+ Before the oauth2sample app can authenticate users via OAuth2, an app must be defined with an OAuth2 authorization server (identity provider).
36
+
37
+ An authorization server authorizes a user or an API consumer to access some resource. Common providers of authorization servers are Google, Facebook, and Microsoft.
38
+
39
+ Basic steps with the provider should include:
40
+ * creating an account if you do not already have one.
41
+ * specifying one or more Redirect URIs.
42
+ * obtaining a Client ID
43
+ * obtaining a Client Secret
44
+
45
+ You may use Google, Amazon, Microsoft, Facebook, GitHub, Twitter, or any other authorization server. This guide covers implementing with Microsoft, Google, and GitHub. The sample app requires at least one OAuth2 provider to be configured.
46
+
47
+ #### Microsoft Entra (Azure Active Directory)
48
+ 1. Log into your Microsoft Entra / Azure account and go to: https://entra.microsoft.com/#blade/Microsoft_AAD_IAM/TenantOverview.ReactView
49
+ (or https://entra.microsoft.com/#home).
50
+ 2. In the left panel, click Applications > App registrations.
51
+ 1. Click the "+ New registration" near the top of the screen.
52
+ 2. Enter a name, e.g. "OAuth2 Sample"
53
+ 3. For Supported account types, specify the intended users; e.g. "Accounts in this organizational directory only ... Single tenant".
54
+ 4. For Redirect URI specify:
55
+ * Platform: "Platform: Single-page application (SPA)"
56
+ * URI: specify a URI used to reach the sample express route running on your PJS server and a special parameter for redirecting; e.g., https://localhost:8081/run/oauth2sample/authpage?redir=1
57
+ 5. Click "Register"
58
+ 3. After clicking Register you should be shown an "App registration" screen for the new app.
59
+ 1. Copy the "Application (client) ID" value, and paste it into a temporary text file. That value will become the "client_id" in the settings.json for the workspace.
60
+ 2. You should see an "Endpoints" button near the top of the screen. Click it to see some required URLs.
61
+ * Copy the "OAuth 2.0 authorization endpoint (v2)" URL into a temporary text file. (This will become the "authorizationUrl" in the openapi.json)
62
+ * Copy the "OAuth 2.0 token endpoint (v2)" URL into a temporary text file. (This will become the "tokenUrl" in the openapi.json)
63
+ 4. Click the "Authentication" link on the left panel under Manage.
64
+ 1. Make sure you see a platform, "Single-page application" with "Redirect URIs" and an entry with the redirect URI that you set previously. If you do not, then set one or more URIs now.
65
+ 2. Do NOT check anything under "Implicit grant and hybrid flows"
66
+ 3. Under "Supported account types" you should see "Accounts in this organizational directory only" or what you picked previously.
67
+ 4. Leave "Allow public client flows" > "Enable the following mobile and desktop flows": as "No".
68
+ 5. Click the "Certificates & secrets" link on the left panel under Manage.
69
+ 1. Click the "Client secrets" section.
70
+ 2. Click on the "+ New client secret" link.
71
+ 3. Set a name, e.g., "OAuth2 smaple secret"
72
+ 4. Set an expiration to something, e.g. "90 days (3 months)"
73
+ 5. Click "Add"
74
+ 6. While it is visible, copy the "Value" and paste it into a temporary text file. The value will become the "client_secret" later in your settings.js file in the workspace.
75
+ 6. Click the "API permissions" link on the left panel under Manage.
76
+ 1. Under "Configured permissions" click "+ Add a permission".
77
+ * Specify "Microsft Graph"
78
+ * Choose "Delegated permissions
79
+ * Add and enable permissions for "User.read" (Sign in and read user profile), "email" (View user's email address), and "openid" (Sign users in).
80
+ 7. Paste the client_id and client_secret into the /modules/oauth2sample/settings.js file under the providerSettings.Microsoft entries, replacing the text "__enter-your-client-id-here__" and "__enter-your-client-secret-here__".
81
+
82
+
83
+ #### Google
84
+ 1. Log in to a Google account and go to https://console.cloud.google.com/apis/credentials
85
+ 2. Click "Create Project " if there is none. Input the following data and click "Create".
86
+ 1. Project Name: PAPI OAuth2 Project (e.g.)
87
+ 2. Location: (pick the default)
88
+ 3. On the Left Panel, click "OAuth consent screen".
89
+ 1. For User Type, choose "External " and click "Create".
90
+ 2. For App Information, input the following data and click "Save and Continue".
91
+ * App name: PAPI OAuth2 App (e.g.)
92
+ * User support email: (pick the default)
93
+ * Developer contact information email addresses: (same as support)
94
+ 3. For Scopes and Test Users, click "Save and Continue" to skip.
95
+ 4. For Summary, click "Back to Dashboard".
96
+ 4. On the Left Panel, click "Credentials". Go to "+ Create Credentials -> OAuth client ID". Input the following data and click "Create".
97
+ * Application type: Web application
98
+ * Name: PAPI OAuth2 Client 1 (e.g.)
99
+ * Authorized redirect URIs: The URL of your Profound.js instance including the special path to the custom Express program, authpage: /run/oauth2sample/authpage?redir=1 ; Examples:
100
+ * https://myapp.example.com/run/oauth2sample/authpage?redir=1
101
+ * http://localhost:8081/run/oauth2sample/authpage?redir=1
102
+ 5. Copy the Client ID and the Client Secret and store it in the /modules/oauth2sample/settings.js file under Google.
103
+
104
+ #### GitHub
105
+ 1. Log in at https://github.com/
106
+ 2. Go to "Settings -> Developer Settings -> OAuth Apps -> New OAuth App".
107
+ 3. Input the following and click "Register Application".
108
+ * Application Name: PAPI OAuth2 Application (e.g.)
109
+ * Homepage URL: Specify the URL you use to reach your Profound.js instance in your browser; e.g., http://localhost:8081/
110
+ * Authorization callback URL: The URL of your Profound.js instance including the special path, /run/oauth2sample/authpage?redir=1 ; e.g. http://localhost:8081/run/oauth2sample/authpage?redir=1
111
+ 4. Click "Generate a new client secret" and authenticate on the security prompt.
112
+ 5. Copy the Client ID and the Client Secret and store it in the /modules/oauth2sample/settings.js file under GitHub.
113
+
114
+ ### 3. Configure Profound.js for OpenAPI
115
+ Your Profound.js instance will provide resources for users or API consumers and will handle the authentication. Your Profound.js server should already be setup and running. (The Profound.js server is considered the Resource Server in OAuth2 terminology. https://www.oauth.com/oauth2-servers/the-resource-server/)
116
+
117
+ Configuration for the OAuth2 component of PAPI is in the openapi.json file, which will exist in the base directory of your Profound.js installation when an OpenAPI security store is configured. (To setup the security store, please see https://docs.profoundlogic.com/Profound+API/Authentication+-+Authorization/Security+Store+Configuration)
118
+
119
+ If the openapi.json file does not exist yet, then the file can be created by navigating to the IDE (e.g. http://localhost:8081/ide), then clicking the "API Options" toolbar button, "OpenAPI Configuration". You will then be shown a basic editor with sample JSON for the openapi.json and can save the file to create it.
120
+
121
+ If the openapi.json file already exists but does not have entries for OAuth2 under components.securitySchemes, then those entries need to be added.
122
+
123
+ To make sure you test your security settings correctly, ensure the value of "x-allowAnonymous" is false. The security property should be an array of objects, including which authorization schemes are permitted. e.g. just to allow OAuth2:
124
+ ```
125
+ "security": [
126
+ {
127
+ "OAuth2": []
128
+ }
129
+ ]
130
+ ```
131
+ Or to allow either "ApiKeyAuth" or an OAuth2 security scheme defined as "OAuth2_Example":
132
+ ```
133
+ "security": [
134
+ {
135
+ "OAuth2_Example": []
136
+ },
137
+ {
138
+ "ApiKeyAuth": []
139
+ }
140
+ ]
141
+ ```
142
+
143
+ Instructions for adding entries for OAuth2 into openapi.json can be found here: https://docs.profoundlogic.com//Profound+API/Authentication+-+Authorization/OpenAPI+Configuration
144
+
145
+ #### 3.1 Sample openapi.json
146
+ ```
147
+ {
148
+ "openapi": "3.0.3",
149
+ "info": {
150
+ "title": "APIs",
151
+ "version": "1.0.0"
152
+ },
153
+ "components": {
154
+ "securitySchemes": {
155
+ "ApiKeyAuth": {
156
+ "type": "apiKey",
157
+ "in": "header",
158
+ "name": "X-API-Key"
159
+ },
160
+ "OAuth2_MS_Entra_Identity": {
161
+ "type": "oauth2",
162
+ "description": "Microsoft Azure Active Directory (Entra)",
163
+ "flows": {
164
+ "authorizationCode": {
165
+ "authorizationUrl": "https://login.microsoftonline.com/__your-tenant-id__/oauth2/v2.0/authorize",
166
+ "tokenUrl": "https://login.microsoftonline.com/__your-tenant-id__/oauth2/v2.0/token",
167
+ "refreshUrl": "https://example.com",
168
+ "scopes": {
169
+ "openid": "required for consent page",
170
+ "User.Read": "read user info, userPrincipalName"
171
+ },
172
+ "x-userinfoUrl": "https://graph.microsoft.com/v1.0/me",
173
+ "x-userinfoField": "userPrincipalName"
174
+ }
175
+ }
176
+ },
177
+ "OAuth2_GitHub": {
178
+ "type": "oauth2",
179
+ "flows": {
180
+ "authorizationCode": {
181
+ "authorizationUrl": "https://github.com/login/oauth/authorize",
182
+ "tokenUrl": "https://github.com/login/oauth/access_token",
183
+ "refreshUrl": "not implemented for GitHub",
184
+ "scopes": {
185
+ "user": "GitHub user profile"
186
+ },
187
+ "x-userinfoUrl": "https://api.github.com/user",
188
+ "x-userinfoField": "login",
189
+ "x-cacheTokenTTL": 10
190
+ }
191
+ }
192
+ },
193
+ "OAuth2_Google": {
194
+ "type": "oauth2",
195
+ "flows": {
196
+ "authorizationCode": {
197
+ "authorizationUrl": "https://accounts.google.com/o/oauth2/auth",
198
+ "tokenUrl": "https://oauth2.googleapis.com/token",
199
+ "refreshUrl": "https://oauth2.googleapis.com/token",
200
+ "scopes": {
201
+ "email": "Google email address"
202
+ },
203
+ "x-userinfoUrl": "https://www.googleapis.com/oauth2/v3/userinfo",
204
+ "x-userinfoField": "email",
205
+ "x-cacheTokenTTL": 5
206
+ }
207
+ }
208
+ }
209
+ }
210
+ },
211
+ "x-allowAnonymous": false,
212
+ "security": [
213
+ {
214
+ "OAuth2_MS_Entra_Identity": []
215
+ },
216
+ {
217
+ "OAuth2_GitHub": []
218
+ },
219
+ {
220
+ "OAuth2_Google": []
221
+ },
222
+ {
223
+ "ApiKeyAuth": []
224
+ }
225
+ ]
226
+ }
227
+ ```
228
+
229
+ ### 4. Specify Provider Details in Settings
230
+ Modify the oauth2sample/settings.js file.
231
+
232
+ Inside this workspace is a file, settings.js, which contains some values that should be edited. Read the comments in the file to understand what each setting does.
233
+
234
+ At a minimum, this sample app requires a provider to be set, and one entry must be setup in "providerSettings", where the entry's property name matches the value of the "provider" property. e.g., if your "provider" is "Microsoft", then you should have an object under the property name, "Microsoft", inside the "propertySettings" object.
235
+
236
+ The values inside of your "propertySettings" entry will include at least a client_id and client_secret. Depending on your OAuth2 provider, more settings may be required. If your provider is not listed, then read the options under the `[example]` to determine what your provider needs.
237
+
238
+ Set the serverDomainName to the domain name that you will use to access your PJS server, or leave it as "localhost" if you are running the example locally.
239
+
240
+ ### 5. Setup API Users
241
+
242
+ 1. From your Profound.js instance's IDE:
243
+ 2. Go to "Home Ribbon -> API Options -> User Authentication -> API Users -> Add New User "
244
+ 3. Input the user or login name of an end-user authorized to use PAPI OAuth2 and choose "New -> oauth2 -> OK"
245
+ * Note: the username added under this dialog must match the user's account via the OAuth2 provider, e.g. email address or username.
246
+
247
+ ## Run the App
248
+ Run the sample app from the starting point; e.g. http://localhost:8081/run/oauth2sample/authpage
249
+
250
+ ## More information
251
+ For documentation on OAuth2 in Profound.js, see: https://docs.profoundlogic.com/PAPI+OAuth2
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Handle the initial authentication page and the redirect from the OAuth2 provider
3
+ * to the Rich Display File application, "useful-app".
4
+ */
5
+ const crypto = require("crypto");
6
+
7
+ const HTML_HEAD = `<!doctype html>
8
+ <html>
9
+ <head>
10
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
11
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
12
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, target-densitydpi=device-dpi">
13
+ <link href="/profoundui/proddata/css/profoundui.css" rel="stylesheet" type="text/css">`;
14
+
15
+ const HTML_TAIL = "</body></html>";
16
+
17
+ /**
18
+ * Route a request to this URL to either show the initial auth page, or draw the redirect page.
19
+ * @param {HTTPRequest} request
20
+ * @param {HTTPResponse} response
21
+ */
22
+ function authpage(request, response) {
23
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
24
+ response.setHeader("Pragma", "no-cache");
25
+ response.setHeader("Expires", "0");
26
+
27
+ if (request.query.redir === "1") {
28
+ redirect(request, response);
29
+ }
30
+ else {
31
+ start(request, response);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Draw the initial auth page.
37
+ */
38
+ function start(request, response) {
39
+ // "state" is a cryptographically strong random sequence of characters, used later to
40
+ // protect against cross-site forgery attacks.
41
+ const formdata = {
42
+ response_type: "code",
43
+ redirect_uri: "",
44
+ client_id: "",
45
+ scope: "",
46
+ state: crypto.randomUUID(),
47
+ method: "GET",
48
+ url: "",
49
+ provider: ""
50
+ };
51
+
52
+ let message = request.query.error;
53
+
54
+ // Get 64 random characters needed for PKCE code verification. The characters must be URL-safe
55
+ // so that they can be passed without alteration to verify the code later.
56
+ let code_verifier_str = "";
57
+ while (code_verifier_str.length < 64) {
58
+ const rand = crypto.randomInt(45, 127);
59
+ // permitted: - . _ ~ 0-9 A-Z a-z
60
+ if (rand === 0x2d || rand === 0x2e || rand === 0x5f || rand === 0x7e ||
61
+ (rand >= 0x30 && rand <= 0x39) ||
62
+ (rand >= 0x41 && rand <= 0x5a) || (rand >= 0x61 && rand <= 0x7a)) {
63
+ code_verifier_str += String.fromCharCode(rand);
64
+ }
65
+ }
66
+
67
+ const hash = crypto.createHash("sha256");
68
+ hash.update(code_verifier_str, "utf8"); // read the random bytes into the hash object.
69
+
70
+ let oautils;
71
+
72
+ if (typeof message !== "string") message = "";
73
+ try {
74
+ oautils = require("./oautils.js");
75
+
76
+ formdata.redirect_uri = oautils.redirectURI;
77
+ formdata.client_id = oautils.client_id;
78
+ formdata.scope = oautils.scope;
79
+
80
+ formdata.code_challenge = hash.digest("base64url"); // the hash is expected to be base64url encoding.
81
+ formdata.code_challenge_method = "S256";
82
+
83
+ if (typeof oautils.authMethod === "string" && oautils.authMethod.length > 0 && oautils.authMethod !== "GET") {
84
+ formdata.method = oautils.authMethod;
85
+ }
86
+
87
+ formdata.provider = oautils.provider;
88
+ formdata.url = oautils.authorizationUrl;
89
+ }
90
+ catch (err) {
91
+ message = String(err);
92
+ }
93
+
94
+ response.send(`${HTML_HEAD}
95
+ <script type="text/javascript">
96
+ function init(){
97
+ /* Store value that are only available to pages in the same origin as this page. */
98
+ /* These values will persist even when the page redirects to the OAuth2 provider's site and back. */
99
+ localStorage.setItem("pjs_oadata", "${formdata.state}");
100
+ localStorage.setItem("pjs_codver", "${code_verifier_str}");
101
+ }
102
+ </script>
103
+ <style type="text/css">
104
+ pre {
105
+ color: #999999;
106
+ }
107
+ div#pui {
108
+ width: calc(100% - 20px);
109
+ left: 10px;
110
+ top: 10px;
111
+ }
112
+ #submit {
113
+ width: 100px;
114
+ height: 25px;
115
+ }
116
+ </style>
117
+ </head>
118
+ <body onload="init();">
119
+ <div id="pui">
120
+ <p>Click Sign On to use your ${formdata.provider} account to access this app.</p>
121
+ <form action="${formdata.url}" method="${formdata.method}">
122
+ <input type="hidden" name="response_type" value="${formdata.response_type}" />
123
+ <input type="hidden" name="redirect_uri" value="${formdata.redirect_uri}" />
124
+ <input type="hidden" name="client_id" value="${formdata.client_id}" />
125
+ <input type="hidden" name="scope" value="${formdata.scope}" />
126
+ <input type="hidden" name="state" value="${formdata.state}" />
127
+ <input type="hidden" name="code_challenge" value="${formdata.code_challenge}" />
128
+ <input type="hidden" name="code_challenge_method" value="${formdata.code_challenge_method}" />
129
+
130
+ <input type="submit" value="Sign On" class="pui-solid-button-yes blueprint-defaults" id="submit" />
131
+ </form>
132
+ <pre>${message}</pre>
133
+ </div>
134
+ ${HTML_TAIL}`);
135
+ }
136
+
137
+ /**
138
+ * Respond to a redirect from the OAuth2 authentication server.
139
+ */
140
+ async function redirect(request, response) {
141
+ let strResp;
142
+ try {
143
+ const rquery = request.query;
144
+ if (rquery.error) {
145
+ // Pass on any errors to the user.
146
+ throw new Error(`${rquery.error}\nDescription: ${rquery.error_description}\n${rquery.error_uri}\nState: ${rquery.state}`);
147
+ }
148
+
149
+ if (typeof rquery.code !== "string" || rquery.code.length < 1 ||
150
+ typeof rquery.state !== "string" || rquery.state.length < 1) {
151
+ throw new Error("Authentication server did not provide code or state.");
152
+ }
153
+
154
+ strResp = `${HTML_HEAD}
155
+ <script type="text/javascript">
156
+ function sendTokens(){
157
+ const state = localStorage.getItem("pjs_oadata");
158
+ if (typeof state === "string" && state.length > 0 && state === "${rquery.state}"){
159
+ localStorage.setItem("pjs_oadata", "${rquery.code}");
160
+ window.location = "/run/oauth2sample/useful-app";
161
+ }
162
+ else {
163
+ document.body.innerHTML = '<b>Error:</b> Unable to authenticate. Invalid state.';
164
+ }
165
+ }
166
+ </script>
167
+ </head>
168
+ <body onload="sendTokens();">Redirecting to session...</body>
169
+ </html>`;
170
+ }
171
+ catch (err) {
172
+ strResp = `${HTML_HEAD}</head><body><pre>${err.message}\n${err.stack}</pre>${HTML_TAIL}`;
173
+ }
174
+
175
+ response.send(strResp);
176
+ }
177
+
178
+ exports.run = authpage;