oauth2-cli 0.8.9 → 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/CHANGELOG.md +21 -2
- package/README.md +36 -12
- package/dist/Client.d.ts +103 -79
- package/dist/Client.js +179 -93
- package/dist/Credentials.d.ts +41 -9
- package/dist/Injection.d.ts +6 -5
- package/dist/Localhost/Defaults.d.ts +1 -0
- package/dist/Localhost/Defaults.js +1 -0
- package/dist/Localhost/Options.d.ts +28 -0
- package/dist/Localhost/Options.js +1 -0
- package/dist/Localhost/Server.d.ts +59 -0
- package/dist/Localhost/Server.js +175 -0
- package/dist/Localhost/index.d.ts +3 -0
- package/dist/Localhost/index.js +3 -0
- package/dist/Options.d.ts +54 -0
- package/dist/Options.js +1 -0
- package/dist/Token/FileStorage.d.ts +15 -0
- package/dist/Token/FileStorage.js +15 -0
- package/dist/Token/Response.d.ts +1 -0
- package/dist/Token/Storage.d.ts +3 -0
- package/dist/Token/index.d.ts +1 -0
- package/dist/Token/index.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -2
- package/package.json +4 -3
- package/views/complete.ejs +23 -23
- package/views/error.ejs +29 -25
- package/dist/Session.d.ts +0 -49
- package/dist/Session.js +0 -111
- package/dist/Token/addHelpers.d.ts +0 -8
- package/dist/Token/addHelpers.js +0 -44
- package/dist/WebServer.d.ts +0 -75
- package/dist/WebServer.js +0 -123
- /package/dist/{Scope.d.ts → Token/Scope.d.ts} +0 -0
- /package/dist/{Scope.js → Token/Scope.js} +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Colors } from '@qui-cli/colors';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import * as gcrtl from 'gcrtl';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import open from 'open';
|
|
6
|
+
import * as OpenIDClient from 'openid-client';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { URL } from 'requestish';
|
|
10
|
+
import { DEFAULT_LAUNCH_ENDPOINT } from './Defaults.js';
|
|
11
|
+
let ejs = undefined;
|
|
12
|
+
try {
|
|
13
|
+
ejs = (await import('ejs')).default;
|
|
14
|
+
}
|
|
15
|
+
catch (_) {
|
|
16
|
+
// ejs peer dependency not installed
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Minimal HTTP server running on localhost to handle the redirect step of
|
|
20
|
+
* OpenID/OAuth flows
|
|
21
|
+
*/
|
|
22
|
+
export class Server {
|
|
23
|
+
static activePorts = [];
|
|
24
|
+
/** PKCE code_verifier */
|
|
25
|
+
code_verifier = OpenIDClient.randomPKCECodeVerifier();
|
|
26
|
+
/** OAuth 2.0 state (if PKCE is not supported) */
|
|
27
|
+
state = OpenIDClient.randomState();
|
|
28
|
+
client;
|
|
29
|
+
reason = 'an unnamed oauth2-cli app';
|
|
30
|
+
views;
|
|
31
|
+
packageViews = '../../views';
|
|
32
|
+
spinner;
|
|
33
|
+
port;
|
|
34
|
+
launch_endpoint;
|
|
35
|
+
server;
|
|
36
|
+
resolveAuthorizationCodeFlow = undefined;
|
|
37
|
+
rejectAuthorizationCodeFlow = undefined;
|
|
38
|
+
constructor({ reason, client, views, launch_endpoint = DEFAULT_LAUNCH_ENDPOINT, timeout = 1000 // milliseconds
|
|
39
|
+
}) {
|
|
40
|
+
this.client = client;
|
|
41
|
+
this.reason = reason || this.reason;
|
|
42
|
+
this.spinner = ora(`Awaiting interactive authorization for ${this.client.name} access by ${this.reason}`).start();
|
|
43
|
+
this.launch_endpoint = launch_endpoint;
|
|
44
|
+
this.views = views;
|
|
45
|
+
const url = URL.from(this.client.credentials.redirect_uri);
|
|
46
|
+
this.port = url.port;
|
|
47
|
+
if (Server.activePorts.includes(this.port)) {
|
|
48
|
+
throw new Error(`Another process is already running at http://localhost:${url.port}.`, { cause: { activePorts: Server.activePorts } });
|
|
49
|
+
}
|
|
50
|
+
Server.activePorts.push(this.port);
|
|
51
|
+
const app = express();
|
|
52
|
+
app.get(this.launch_endpoint, this.handleAuthorizationEndpoint.bind(this));
|
|
53
|
+
app.get(gcrtl.path(url), this.handleRedirect.bind(this));
|
|
54
|
+
this.server = app.listen(gcrtl.port(url));
|
|
55
|
+
this.server.timeout = timeout;
|
|
56
|
+
this.server.keepAliveTimeout = 0;
|
|
57
|
+
this.server.keepAliveTimeoutBuffer = 0;
|
|
58
|
+
}
|
|
59
|
+
async authorizationCodeGrant() {
|
|
60
|
+
const response = new Promise((resolve, reject) => {
|
|
61
|
+
this.resolveAuthorizationCodeFlow = resolve;
|
|
62
|
+
this.rejectAuthorizationCodeFlow = reject;
|
|
63
|
+
});
|
|
64
|
+
const url = URL.toString(gcrtl.expand(this.launch_endpoint, this.client.credentials.redirect_uri));
|
|
65
|
+
open(url, { wait: false });
|
|
66
|
+
this.spinner.text = `Please continue interactive authorization of ${this.client.name} for ${this.reason} at ${Colors.url(url)} in your browser`;
|
|
67
|
+
await response;
|
|
68
|
+
this.spinner.text = `Waiting for ${this.client.name} localhost redirect server to shut down`;
|
|
69
|
+
await this.close();
|
|
70
|
+
return response;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Set the path to folder of *.ejs templates
|
|
74
|
+
*
|
|
75
|
+
* Expected templates include:
|
|
76
|
+
*
|
|
77
|
+
* - `launch.ejs` presents information prior to the authorization to the user,
|
|
78
|
+
* and the user must follow `authorize_url` data property to interactively
|
|
79
|
+
* launch authorization
|
|
80
|
+
* - `complete.ejs` presented to user upon successful completion of
|
|
81
|
+
* authorization flow
|
|
82
|
+
* - `error.ejs` presented to user upon receipt of an error from the server,
|
|
83
|
+
* includes `error` as data
|
|
84
|
+
*
|
|
85
|
+
* `complete.ejs` and `error.ejs` are included with oauth2-cli and those
|
|
86
|
+
* templates will be used if `ejs` is imported but no replacement templates
|
|
87
|
+
* are found.
|
|
88
|
+
*
|
|
89
|
+
* All views receive a data property `name` which is the human-readable name
|
|
90
|
+
* of the Client managing the authorization code flow, and `reason` indicating
|
|
91
|
+
* the human-readable reason (i.e. name of the app) requesting authorization,
|
|
92
|
+
* which should be displayed in messages for transparency.
|
|
93
|
+
*
|
|
94
|
+
* @param views Should be an absolute path
|
|
95
|
+
*/
|
|
96
|
+
setViews(views) {
|
|
97
|
+
this.views = views;
|
|
98
|
+
}
|
|
99
|
+
async render(res, template, data = {}) {
|
|
100
|
+
const name = this.client.name;
|
|
101
|
+
const reason = this.reason;
|
|
102
|
+
async function attemptToRender(views) {
|
|
103
|
+
if (ejs && views) {
|
|
104
|
+
const viewPath = path.resolve(import.meta.dirname, views, template);
|
|
105
|
+
if (fs.existsSync(viewPath)) {
|
|
106
|
+
res.send(await ejs.renderFile(viewPath, {
|
|
107
|
+
name,
|
|
108
|
+
reason,
|
|
109
|
+
...data
|
|
110
|
+
}));
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
return ((await attemptToRender(this.views)) ||
|
|
117
|
+
(await attemptToRender(this.packageViews)));
|
|
118
|
+
}
|
|
119
|
+
/** Handles request to `/authorize` */
|
|
120
|
+
async handleAuthorizationEndpoint(req, res) {
|
|
121
|
+
const authorization_url = URL.toString(await this.client.buildAuthorizationUrl(this));
|
|
122
|
+
if (!(await this.render(res, 'launch.ejs', { authorization_url }))) {
|
|
123
|
+
res.redirect(authorization_url);
|
|
124
|
+
res.end();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Handles request to `redirect_uri` */
|
|
128
|
+
async handleRedirect(req, res) {
|
|
129
|
+
this.spinner.text = `Exchanging authorization code for ${this.client.name} access token for ${this.reason}`;
|
|
130
|
+
try {
|
|
131
|
+
if (this.resolveAuthorizationCodeFlow) {
|
|
132
|
+
this.resolveAuthorizationCodeFlow(await this.client.handleAuthorizationCodeRedirect(req, this));
|
|
133
|
+
this.spinner.text = `${this.client.name} authorization complete and access token received for ${this.reason}`;
|
|
134
|
+
if (!(await this.render(res, 'complete.ejs'))) {
|
|
135
|
+
res.send(`${this.client.name} authorization complete for ${this.reason}. You may close this window.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
throw new Error(`${this.client.name} localhost redirect session resolver is ${this.resolveAuthorizationCodeFlow}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (this.rejectAuthorizationCodeFlow) {
|
|
144
|
+
this.rejectAuthorizationCodeFlow(new Error(`${this.client.name} localhost server could not handle redirect`, {
|
|
145
|
+
cause: error
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
if (!(await this.render(res, 'error.ejs', { error }))) {
|
|
149
|
+
res.send({
|
|
150
|
+
client: this.client.name,
|
|
151
|
+
error
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Shut down web server */
|
|
157
|
+
async close() {
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
this.server.close((cause) => {
|
|
160
|
+
if (cause) {
|
|
161
|
+
const message = `Error shutting down ${this.client.name} localhost redirect server`;
|
|
162
|
+
this.spinner.fail(`Error shutting down ${this.client.name} localhost redirect server`);
|
|
163
|
+
reject(new Error(message, {
|
|
164
|
+
cause
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
Server.activePorts.splice(Server.activePorts.indexOf(this.port), 1);
|
|
169
|
+
this.spinner.succeed(`${this.client.name} authorization complete for ${this.reason}`);
|
|
170
|
+
resolve();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { URL } from 'requestish';
|
|
2
|
+
import { Credentials } from './Credentials.js';
|
|
3
|
+
import { Injection } from './Injection.js';
|
|
4
|
+
import * as Localhost from './Localhost/index.js';
|
|
5
|
+
import * as Token from './Token/index.js';
|
|
6
|
+
/** For use only within the Client implementation */
|
|
7
|
+
export type LocalhostOptions = Omit<Localhost.Options, 'client' | 'reason'>;
|
|
8
|
+
export type Client<C extends Credentials = Credentials> = {
|
|
9
|
+
/** Human-readable name for client in messages (e.g. the name of the API) */
|
|
10
|
+
name?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Human-readable reason for authorizing access in messages (e.g. the app
|
|
13
|
+
* name)
|
|
14
|
+
*/
|
|
15
|
+
reason?: string;
|
|
16
|
+
/** Credentials for server access */
|
|
17
|
+
credentials: C;
|
|
18
|
+
/** Optional request components to inject */
|
|
19
|
+
inject?: Injection;
|
|
20
|
+
/** Base URL for all non-absolute requests */
|
|
21
|
+
base_url?: URL.ish;
|
|
22
|
+
/** Optional {@link TokenStorage} implementation to manage tokens */
|
|
23
|
+
storage?: Token.Storage;
|
|
24
|
+
/**
|
|
25
|
+
* Optional configuration for localhost web server listening for authorization
|
|
26
|
+
* code redirect
|
|
27
|
+
*/
|
|
28
|
+
localhost?: LocalhostOptions;
|
|
29
|
+
};
|
|
30
|
+
export type Refresh = {
|
|
31
|
+
/**
|
|
32
|
+
* Optional refresh token
|
|
33
|
+
*
|
|
34
|
+
* If using {@link TokenStorage}, the refresh token should be stored with the
|
|
35
|
+
* access token and does not need to be separately managed and stored
|
|
36
|
+
*/
|
|
37
|
+
refresh_token?: string;
|
|
38
|
+
/** Additional request injection for refresh grant flow */
|
|
39
|
+
inject?: Injection;
|
|
40
|
+
};
|
|
41
|
+
export type GetToken = {
|
|
42
|
+
/**
|
|
43
|
+
* Optional access token
|
|
44
|
+
*
|
|
45
|
+
* If using {@link TokenStorage}, the access token does not need to be
|
|
46
|
+
* separately managed and stored
|
|
47
|
+
*/
|
|
48
|
+
token?: Token.Response;
|
|
49
|
+
/**
|
|
50
|
+
* Additional request injection for authorization code grant and/or refresh
|
|
51
|
+
* grant flows
|
|
52
|
+
*/
|
|
53
|
+
inject?: Injection;
|
|
54
|
+
};
|
package/dist/Options.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { Storage } from './Storage.js';
|
|
2
|
+
/**
|
|
3
|
+
* Save a refresh token to a specific file
|
|
4
|
+
*
|
|
5
|
+
* Care _must_ be taken when using this persistence strategy that:
|
|
6
|
+
*
|
|
7
|
+
* 1. The token file is not publicly accessible
|
|
8
|
+
* 2. The token file is _not_ commited to a public repository
|
|
9
|
+
*
|
|
10
|
+
* This strategy is appropriate only in very limited set of use cases.
|
|
11
|
+
*/
|
|
2
12
|
export declare class FileStorage implements Storage {
|
|
13
|
+
/** Prevent multiple simultaneous, conflicting file accesses */
|
|
3
14
|
private fileLock;
|
|
4
15
|
private readonly filePath;
|
|
16
|
+
/**
|
|
17
|
+
* @param filePath The path to the token file (optionally relative to the
|
|
18
|
+
* current working directory)
|
|
19
|
+
*/
|
|
5
20
|
constructor(filePath: string);
|
|
6
21
|
load(): Promise<string | undefined>;
|
|
7
22
|
save(refresh_token: string): Promise<void>;
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import { Mutex } from 'async-mutex';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Save a refresh token to a specific file
|
|
6
|
+
*
|
|
7
|
+
* Care _must_ be taken when using this persistence strategy that:
|
|
8
|
+
*
|
|
9
|
+
* 1. The token file is not publicly accessible
|
|
10
|
+
* 2. The token file is _not_ commited to a public repository
|
|
11
|
+
*
|
|
12
|
+
* This strategy is appropriate only in very limited set of use cases.
|
|
13
|
+
*/
|
|
4
14
|
export class FileStorage {
|
|
15
|
+
/** Prevent multiple simultaneous, conflicting file accesses */
|
|
5
16
|
fileLock = new Mutex();
|
|
6
17
|
filePath;
|
|
18
|
+
/**
|
|
19
|
+
* @param filePath The path to the token file (optionally relative to the
|
|
20
|
+
* current working directory)
|
|
21
|
+
*/
|
|
7
22
|
constructor(filePath) {
|
|
8
23
|
this.filePath = path.resolve(process.cwd(), filePath);
|
|
9
24
|
}
|
package/dist/Token/Response.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import * as OpenIDClient from 'openid-client';
|
|
2
|
+
/** @see {@link https://github.com/panva/openid-client/blob/79386b7f120dc9bdc7606886a26e4ea7d011ee51/src/index.ts#L4268 `openid-client` grant request return type} */
|
|
2
3
|
export type Response = OpenIDClient.TokenEndpointResponse & OpenIDClient.TokenEndpointResponseHelpers;
|
package/dist/Token/Storage.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
/** Persistent refresh token storage */
|
|
1
2
|
export interface Storage {
|
|
3
|
+
/** Load the stored refresh token from persistent storage, if present */
|
|
2
4
|
load(): Promise<string | undefined>;
|
|
5
|
+
/** Save a refresh token to persistent storage */
|
|
3
6
|
save(refresh_token: string): Promise<void>;
|
|
4
7
|
}
|
package/dist/Token/index.d.ts
CHANGED
package/dist/Token/index.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from './Client.js';
|
|
2
2
|
export * from './Credentials.js';
|
|
3
3
|
export * from './Injection.js';
|
|
4
|
-
export * as
|
|
4
|
+
export * as Localhost from './Localhost/index.js';
|
|
5
|
+
export { Client as Options } from './Options.js';
|
|
5
6
|
export * as Token from './Token/index.js';
|
|
6
|
-
export * from './WebServer.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export * from './Client.js';
|
|
2
2
|
export * from './Credentials.js';
|
|
3
3
|
export * from './Injection.js';
|
|
4
|
-
export * as
|
|
4
|
+
export * as Localhost from './Localhost/index.js';
|
|
5
5
|
export * as Token from './Token/index.js';
|
|
6
|
-
export * from './WebServer.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oauth2-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Acquire API access tokens via OAuth 2.0 within CLI tools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Acquire API access tokens via OAuth 2.0 / OpenID Connect within CLI tools",
|
|
5
5
|
"homepage": "https://github.com/battis/oauth2-cli/tree/main/packages/oauth2-cli#readme",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
"async-mutex": "^0.5.0",
|
|
21
21
|
"express": "^5.2.1",
|
|
22
22
|
"oauth4webapi": "^3.8.5",
|
|
23
|
+
"open": "^11.0.0",
|
|
23
24
|
"openid-client": "^6.8.2",
|
|
24
25
|
"ora": "^9.3.0",
|
|
25
|
-
"requestish": "0.1.
|
|
26
|
+
"requestish": "0.1.3",
|
|
26
27
|
"gcrtl": "0.1.7"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
package/views/complete.ejs
CHANGED
|
@@ -10,37 +10,37 @@
|
|
|
10
10
|
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
|
|
11
11
|
crossorigin="anonymous"
|
|
12
12
|
/>
|
|
13
|
-
<style>
|
|
14
|
-
.wrapper {
|
|
15
|
-
display: grid;
|
|
16
|
-
grid-template-columns: 1fr auto 1fr;
|
|
17
|
-
grid-template-rows: 1fr auto 1.61803fr;
|
|
18
|
-
position: absolute;
|
|
19
|
-
width: 100vw;
|
|
20
|
-
height: 100vh;
|
|
21
|
-
top: 0;
|
|
22
|
-
left: 0;
|
|
23
|
-
}
|
|
24
|
-
.center {
|
|
25
|
-
grid-row: 2;
|
|
26
|
-
grid-column: 2;
|
|
27
|
-
}
|
|
28
|
-
</style>
|
|
29
13
|
</head>
|
|
30
14
|
<body>
|
|
31
|
-
<div
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
15
|
+
<div
|
|
16
|
+
class="modal fade"
|
|
17
|
+
id="modal"
|
|
18
|
+
data-bs-backdrop="static"
|
|
19
|
+
data-bs-keyboard="false"
|
|
20
|
+
tabindex="-1"
|
|
21
|
+
aria-hidden="true"
|
|
22
|
+
>
|
|
23
|
+
<div class="modal-dialog">
|
|
24
|
+
<div class="modal-content">
|
|
25
|
+
<div class="modal-header">
|
|
26
|
+
<h1 class="modal-title fs-5"><%= name %> Authorization Complete</h1>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="modal-body">
|
|
29
|
+
You have authorized access to <%= name %> for <%= reason %> using
|
|
30
|
+
your credentials.
|
|
31
|
+
</div>
|
|
32
|
+
<div class="modal-footer">You may close this window.</div>
|
|
33
|
+
</div>
|
|
38
34
|
</div>
|
|
39
35
|
</div>
|
|
36
|
+
|
|
40
37
|
<script
|
|
41
38
|
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
|
42
39
|
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
|
43
40
|
crossorigin="anonymous"
|
|
44
41
|
></script>
|
|
42
|
+
<script>
|
|
43
|
+
bootstrap.Modal.getOrCreateInstance('#modal').show();
|
|
44
|
+
</script>
|
|
45
45
|
</body>
|
|
46
46
|
</html>
|
package/views/error.ejs
CHANGED
|
@@ -10,34 +10,35 @@
|
|
|
10
10
|
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
|
|
11
11
|
crossorigin="anonymous"
|
|
12
12
|
/>
|
|
13
|
-
<style>
|
|
14
|
-
.wrapper {
|
|
15
|
-
display: grid;
|
|
16
|
-
grid-template-columns: 1fr auto 1fr;
|
|
17
|
-
grid-template-rows: 1fr auto 1.61803fr;
|
|
18
|
-
position: absolute;
|
|
19
|
-
width: 100vw;
|
|
20
|
-
height: 100vh;
|
|
21
|
-
top: 0;
|
|
22
|
-
left: 0;
|
|
23
|
-
}
|
|
24
|
-
.center {
|
|
25
|
-
grid-row: 2;
|
|
26
|
-
grid-column: 2;
|
|
27
|
-
}
|
|
28
|
-
</style>
|
|
29
13
|
</head>
|
|
30
14
|
<body>
|
|
31
|
-
<div
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
15
|
+
<div
|
|
16
|
+
class="modal fade"
|
|
17
|
+
id="modal"
|
|
18
|
+
data-bs-backdrop="static"
|
|
19
|
+
data-bs-keyboard="false"
|
|
20
|
+
tabindex="-1"
|
|
21
|
+
aria-hidden="true"
|
|
22
|
+
>
|
|
23
|
+
<div class="modal-dialog">
|
|
24
|
+
<div class="modal-content">
|
|
25
|
+
<div class="modal-header">
|
|
26
|
+
<h1 class="modal-title fs-5"><%= name %> Authorization Error</h1>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="modal-body">
|
|
29
|
+
<p>
|
|
30
|
+
While authorizing access to
|
|
31
|
+
<%= name %>
|
|
32
|
+
for
|
|
33
|
+
<%= reason %>
|
|
34
|
+
an error was encountered:
|
|
35
|
+
</p>
|
|
36
|
+
<div class="alert alert-danger" role="alert">
|
|
37
|
+
<pre><%= JSON.stringify(error, null, 2) %></pre>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="modal-footer">You may close this window.</div>
|
|
39
41
|
</div>
|
|
40
|
-
<p>You may close this window.</p>
|
|
41
42
|
</div>
|
|
42
43
|
</div>
|
|
43
44
|
<script
|
|
@@ -45,5 +46,8 @@
|
|
|
45
46
|
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
|
46
47
|
crossorigin="anonymous"
|
|
47
48
|
></script>
|
|
49
|
+
<script>
|
|
50
|
+
bootstrap.Modal.getOrCreateInstance('#modal').show();
|
|
51
|
+
</script>
|
|
48
52
|
</body>
|
|
49
53
|
</html>
|
package/dist/Session.d.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { PathString } from '@battis/descriptive-types';
|
|
2
|
-
import { Request } from 'express';
|
|
3
|
-
import { Client } from './Client.js';
|
|
4
|
-
import { Injection } from './Injection.js';
|
|
5
|
-
import * as Token from './Token/index.js';
|
|
6
|
-
import * as WebServer from './WebServer.js';
|
|
7
|
-
export type SessionOptions = {
|
|
8
|
-
client: Client;
|
|
9
|
-
/** See {@link WebServer.setViews Webserver.setViews()} */
|
|
10
|
-
views?: PathString;
|
|
11
|
-
/** Additional request injection for authorization code grant flow */
|
|
12
|
-
inject?: Injection;
|
|
13
|
-
};
|
|
14
|
-
export type SessionResolver = (response: Token.Response) => void | Promise<void>;
|
|
15
|
-
export declare class Session {
|
|
16
|
-
readonly client: Client;
|
|
17
|
-
private readonly outOfBandRedirectServer;
|
|
18
|
-
/** PKCE code_verifier */
|
|
19
|
-
readonly code_verifier: string;
|
|
20
|
-
/** OAuth 2.0 state (if PKCE is not supported) */
|
|
21
|
-
readonly state: string;
|
|
22
|
-
/** Additional request injection for Authorization Code Grant request */
|
|
23
|
-
readonly inject?: Injection;
|
|
24
|
-
private spinner;
|
|
25
|
-
private _resolve?;
|
|
26
|
-
/**
|
|
27
|
-
* Method that resolves or rejects the promise returned from the
|
|
28
|
-
* {@link authorizationCodeGrant}
|
|
29
|
-
*/
|
|
30
|
-
get resolve(): SessionResolver;
|
|
31
|
-
reject(cause: unknown): void;
|
|
32
|
-
constructor({ client, views, inject: request }: SessionOptions);
|
|
33
|
-
/** Instantiate the web server that will listen for the out-of-band redirect */
|
|
34
|
-
protected instantiateWebServer(options: Omit<WebServer.WebServerOptions, 'session'>): WebServer.WebServerInterface;
|
|
35
|
-
/**
|
|
36
|
-
* Trigger the start of the Authorization Code Grant flow, returnig a Promise
|
|
37
|
-
* that will resolve into the eventual token. This will close the out-of-band
|
|
38
|
-
* redirect server that creating the session started.
|
|
39
|
-
*/
|
|
40
|
-
authorizationCodeGrant(): Promise<Token.Response>;
|
|
41
|
-
/** OAuth 2.0 redirect_uri that this session is handling */
|
|
42
|
-
get redirect_uri(): import("requestish/dist/URL.js").ish;
|
|
43
|
-
getAuthorizationUrl(): Promise<string>;
|
|
44
|
-
/**
|
|
45
|
-
* Express RequestHandler for the out-of-band redirect in the Authorization
|
|
46
|
-
* Code Grant flow
|
|
47
|
-
*/
|
|
48
|
-
handleAuthorizationCodeRedirect(req: Request): Promise<void>;
|
|
49
|
-
}
|
package/dist/Session.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { Colors } from '@qui-cli/colors';
|
|
2
|
-
import * as gcrtl from 'gcrtl';
|
|
3
|
-
import * as OpenIDClient from 'openid-client';
|
|
4
|
-
import ora from 'ora';
|
|
5
|
-
import * as WebServer from './WebServer.js';
|
|
6
|
-
export class Session {
|
|
7
|
-
client;
|
|
8
|
-
outOfBandRedirectServer;
|
|
9
|
-
/** PKCE code_verifier */
|
|
10
|
-
code_verifier = OpenIDClient.randomPKCECodeVerifier();
|
|
11
|
-
/** OAuth 2.0 state (if PKCE is not supported) */
|
|
12
|
-
state = OpenIDClient.randomState();
|
|
13
|
-
/** Additional request injection for Authorization Code Grant request */
|
|
14
|
-
inject;
|
|
15
|
-
spinner;
|
|
16
|
-
_resolve;
|
|
17
|
-
/**
|
|
18
|
-
* Method that resolves or rejects the promise returned from the
|
|
19
|
-
* {@link authorizationCodeGrant}
|
|
20
|
-
*/
|
|
21
|
-
get resolve() {
|
|
22
|
-
if (!this._resolve) {
|
|
23
|
-
throw new Error(`${this.client.clientName()}'s Session resolve method is ${this._resolve}`);
|
|
24
|
-
}
|
|
25
|
-
return this._resolve;
|
|
26
|
-
}
|
|
27
|
-
reject(cause) {
|
|
28
|
-
throw new Error(`${this.client.clientName()}'s Session failed`, { cause });
|
|
29
|
-
}
|
|
30
|
-
constructor({ client, views, inject: request }) {
|
|
31
|
-
this.client = client;
|
|
32
|
-
this.spinner = ora(`${this.client.clientName()} awaiting interactive authorization`).start();
|
|
33
|
-
this.inject = request;
|
|
34
|
-
this.outOfBandRedirectServer = this.instantiateWebServer({ views });
|
|
35
|
-
}
|
|
36
|
-
/** Instantiate the web server that will listen for the out-of-band redirect */
|
|
37
|
-
instantiateWebServer(options) {
|
|
38
|
-
return new WebServer.WebServer({ session: this, ...options });
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Trigger the start of the Authorization Code Grant flow, returnig a Promise
|
|
42
|
-
* that will resolve into the eventual token. This will close the out-of-band
|
|
43
|
-
* redirect server that creating the session started.
|
|
44
|
-
*/
|
|
45
|
-
async authorizationCodeGrant() {
|
|
46
|
-
return await new Promise((resolve, reject) => {
|
|
47
|
-
try {
|
|
48
|
-
this._resolve = (response) => {
|
|
49
|
-
let closed = false;
|
|
50
|
-
this.spinner.text = `${this.client.clientName()} waiting for out-of-band redirect server to shut down`;
|
|
51
|
-
this.outOfBandRedirectServer.close().then(() => {
|
|
52
|
-
closed = true;
|
|
53
|
-
this.spinner.succeed(`Interactive authorization for ${this.client.clientName()} complete`);
|
|
54
|
-
resolve(response);
|
|
55
|
-
});
|
|
56
|
-
setTimeout(() => {
|
|
57
|
-
if (!closed) {
|
|
58
|
-
this.spinner.text =
|
|
59
|
-
`Still waiting for out-of-band redirect server for ${this.client.clientName()} to shut down.\n` +
|
|
60
|
-
' Your browser may be holding the connection to the server open.\n\n' +
|
|
61
|
-
' Please close the "Authorization Complete" tab in your browser.';
|
|
62
|
-
}
|
|
63
|
-
}, 5000);
|
|
64
|
-
setTimeout(() => {
|
|
65
|
-
if (!closed) {
|
|
66
|
-
this.spinner.text =
|
|
67
|
-
`Still waiting for out-of-band redirect server for ${this.client.clientName()} to shut down.\n` +
|
|
68
|
-
' Your browser may be holding the connection to the server open.\n\n' +
|
|
69
|
-
' Please close the browser window.';
|
|
70
|
-
}
|
|
71
|
-
}, 10000);
|
|
72
|
-
setTimeout(() => {
|
|
73
|
-
if (!closed) {
|
|
74
|
-
this.spinner.text =
|
|
75
|
-
`Still waiting for out-of-band redirect server for ${this.client.clientName()} to shut down.\n` +
|
|
76
|
-
' Your browser may be holding the connection to the server open.\n\n' +
|
|
77
|
-
' Please quit the browser.';
|
|
78
|
-
}
|
|
79
|
-
}, 15000);
|
|
80
|
-
};
|
|
81
|
-
const url = gcrtl
|
|
82
|
-
.expand(this.outOfBandRedirectServer.authorization_endpoint, this.client.redirect_uri)
|
|
83
|
-
.toString();
|
|
84
|
-
//open(url);
|
|
85
|
-
this.spinner.text = `Please continue interactive authorization for ${this.client.clientName()} at ${Colors.url(url)} in your browser`;
|
|
86
|
-
}
|
|
87
|
-
catch (cause) {
|
|
88
|
-
this.spinner.text = `Waiting for out-of-band redirect server for ${this.client.clientName()} to shut down`;
|
|
89
|
-
this.outOfBandRedirectServer.close().then(() => {
|
|
90
|
-
this.spinner.fail(`Interactive authorization for ${this.client.clientName()} failed`);
|
|
91
|
-
reject(new Error(`Error in Authorization Code flow for ${this.client.clientName()}`, { cause }));
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
/** OAuth 2.0 redirect_uri that this session is handling */
|
|
97
|
-
get redirect_uri() {
|
|
98
|
-
return this.client.redirect_uri;
|
|
99
|
-
}
|
|
100
|
-
async getAuthorizationUrl() {
|
|
101
|
-
return (await this.client.getAuthorizationUrl(this)).toString();
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Express RequestHandler for the out-of-band redirect in the Authorization
|
|
105
|
-
* Code Grant flow
|
|
106
|
-
*/
|
|
107
|
-
async handleAuthorizationCodeRedirect(req) {
|
|
108
|
-
this.spinner.text = `Completing access token request fro ${this.client.clientName()} with provided authorization code`;
|
|
109
|
-
return await this.client.handleAuthorizationCodeRedirect(req, this);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reaching into the bowels of openid-client to be able to fully de-serialize a
|
|
3
|
-
* serialized token response
|
|
4
|
-
*/
|
|
5
|
-
import * as oauth from 'oauth4webapi';
|
|
6
|
-
import { TokenEndpointResponseHelpers } from 'openid-client';
|
|
7
|
-
/** @see https://github.com/panva/openid-client/blob/79386b7f120dc9bdc7606886a26e4ea7d011ee51/src/index.ts#L2018-L2022 */
|
|
8
|
-
export declare function addHelpers(response: oauth.TokenEndpointResponse): asserts response is typeof response & TokenEndpointResponseHelpers;
|