oauth2-cli 0.8.9 → 1.0.1
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 +28 -2
- package/README.md +36 -12
- package/dist/Client.d.ts +103 -79
- package/dist/Client.js +185 -94
- 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 +5 -4
- 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
package/dist/Client.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { Mutex } from 'async-mutex';
|
|
2
|
+
import * as gcrtl from 'gcrtl';
|
|
2
3
|
import { EventEmitter } from 'node:events';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import * as OpenIDClient from 'openid-client';
|
|
5
|
-
import
|
|
6
|
-
import * as
|
|
7
|
-
import
|
|
8
|
-
/**
|
|
9
|
-
* A generic `redirect_uri` to use if the server does not require pre-registered
|
|
10
|
-
* `redirect_uri` values
|
|
11
|
-
*/
|
|
12
|
-
export const DEFAULT_REDIRECT_URI = 'http://localhost:3000/oauth2-cli/redirect';
|
|
6
|
+
import { Body, Headers, URL, URLSearchParams } from 'requestish';
|
|
7
|
+
import * as Localhost from './Localhost/index.js';
|
|
8
|
+
import * as Token from './Token/index.js';
|
|
13
9
|
/**
|
|
14
10
|
* Wrap {@link https://www.npmjs.com/package/openid-client openid-client} in a
|
|
15
11
|
* class instance specific to a particular OAuth/OpenID server credential-set,
|
|
@@ -19,158 +15,225 @@ export const DEFAULT_REDIRECT_URI = 'http://localhost:3000/oauth2-cli/redirect';
|
|
|
19
15
|
*/
|
|
20
16
|
export class Client extends EventEmitter {
|
|
21
17
|
static TokenEvent = 'token';
|
|
22
|
-
|
|
18
|
+
_name;
|
|
19
|
+
/** Human-readable name for client in messages */
|
|
20
|
+
get name() {
|
|
21
|
+
if (this._name && this._name.length > 0) {
|
|
22
|
+
return this._name;
|
|
23
|
+
}
|
|
24
|
+
return 'API';
|
|
25
|
+
}
|
|
26
|
+
/** Human-readable reason for authorization in messages */
|
|
27
|
+
reason;
|
|
28
|
+
/** Credentials for server access */
|
|
23
29
|
credentials;
|
|
30
|
+
/** Base URL for all non-absolute requests */
|
|
24
31
|
base_url;
|
|
32
|
+
/**
|
|
33
|
+
* `openid-client` configuration metadata (either dervied from
|
|
34
|
+
* {@link credentials}) or requested from the well-known OpenID configuration
|
|
35
|
+
* endpoint of the `issuer`
|
|
36
|
+
*/
|
|
25
37
|
config;
|
|
38
|
+
/** Optional request components to inject */
|
|
26
39
|
inject;
|
|
27
|
-
|
|
40
|
+
/** Optional configuration options for web server listening for redirect */
|
|
41
|
+
localhostOptions;
|
|
42
|
+
/** Optional {@link TokenStorage} implementation to manage tokens */
|
|
43
|
+
storage;
|
|
44
|
+
/** Current response to an access token grant request, if available */
|
|
28
45
|
token;
|
|
46
|
+
/** Mutex preventing multiple simultaneous access token grant requests */
|
|
29
47
|
tokenLock = new Mutex();
|
|
30
|
-
|
|
31
|
-
constructor({ name, credentials, base_url,
|
|
48
|
+
/** @see {@link Options.Client} */
|
|
49
|
+
constructor({ name, reason, credentials, base_url, inject, storage, localhost }) {
|
|
32
50
|
super();
|
|
33
|
-
this.
|
|
51
|
+
this._name = name;
|
|
52
|
+
this.reason = reason;
|
|
34
53
|
this.credentials = credentials;
|
|
35
54
|
this.base_url = base_url;
|
|
36
|
-
this.
|
|
55
|
+
this.localhostOptions = localhost;
|
|
37
56
|
this.inject = inject;
|
|
38
57
|
this.storage = storage;
|
|
39
58
|
}
|
|
40
|
-
clientName() {
|
|
41
|
-
if (this.name && this.name.length > 0) {
|
|
42
|
-
return this.name;
|
|
43
|
-
}
|
|
44
|
-
return 'oauth2-cli';
|
|
45
|
-
}
|
|
46
|
-
get redirect_uri() {
|
|
47
|
-
return this.credentials.redirect_uri;
|
|
48
|
-
}
|
|
49
59
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
60
|
+
* Build a client configuration either via `issuer` discovery or from provided
|
|
61
|
+
* `credentials`
|
|
52
62
|
*/
|
|
53
63
|
async getConfiguration() {
|
|
54
|
-
let
|
|
64
|
+
let discovery = undefined;
|
|
55
65
|
if (!this.config && this.credentials.issuer) {
|
|
56
66
|
try {
|
|
57
|
-
this.config = await OpenIDClient.discovery(
|
|
67
|
+
this.config = await OpenIDClient.discovery(URL.from(this.credentials.issuer), this.credentials.client_id, { client_secret: this.credentials.client_secret });
|
|
58
68
|
}
|
|
59
|
-
catch (
|
|
60
|
-
|
|
69
|
+
catch (error) {
|
|
70
|
+
discovery = error;
|
|
61
71
|
}
|
|
62
72
|
}
|
|
63
73
|
if (!this.config && this.credentials?.authorization_endpoint) {
|
|
64
74
|
this.config = new OpenIDClient.Configuration({
|
|
65
|
-
issuer: `https://${
|
|
66
|
-
authorization_endpoint:
|
|
67
|
-
token_endpoint:
|
|
75
|
+
issuer: `https://${URL.from(this.credentials.authorization_endpoint).hostname}`,
|
|
76
|
+
authorization_endpoint: URL.toString(this.credentials.authorization_endpoint),
|
|
77
|
+
token_endpoint: URL.toString(this.credentials.token_endpoint ||
|
|
68
78
|
this.credentials.authorization_endpoint)
|
|
69
79
|
}, this.credentials.client_id, { client_secret: this.credentials.client_secret });
|
|
70
80
|
}
|
|
71
81
|
if (!this.config) {
|
|
72
|
-
throw new Error(`
|
|
82
|
+
throw new Error(`Client configuration for ${this.name} could not be discovered or derived from credentials`, {
|
|
73
83
|
cause: {
|
|
74
84
|
credentials: this.credentials,
|
|
75
|
-
|
|
85
|
+
discovery
|
|
76
86
|
}
|
|
77
87
|
});
|
|
78
88
|
}
|
|
79
89
|
return this.config;
|
|
80
90
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Build a URL to redirect the user-agent to, in order to request
|
|
93
|
+
* authorization at the Authorization Server
|
|
94
|
+
*
|
|
95
|
+
* @param session Contains the current `state` and `code_verifier` for the
|
|
96
|
+
* Authorization Code flow session
|
|
97
|
+
*/
|
|
98
|
+
async buildAuthorizationUrl(session) {
|
|
99
|
+
const params = URLSearchParams.from(this.inject?.search);
|
|
100
|
+
params.set('redirect_uri', URL.toString(this.credentials.redirect_uri));
|
|
84
101
|
params.set('code_challenge', await OpenIDClient.calculatePKCECodeChallenge(session.code_verifier));
|
|
85
102
|
params.set('code_challenge_method', 'S256');
|
|
86
103
|
params.set('state', session.state);
|
|
87
104
|
if (this.credentials.scope) {
|
|
88
|
-
params.set('scope', Scope.toString(this.credentials.scope));
|
|
105
|
+
params.set('scope', Token.Scope.toString(this.credentials.scope));
|
|
89
106
|
}
|
|
90
|
-
return params;
|
|
91
|
-
}
|
|
92
|
-
async getAuthorizationUrl(session) {
|
|
93
|
-
return OpenIDClient.buildAuthorizationUrl(await this.getConfiguration(), await this.getParameters(session));
|
|
94
|
-
}
|
|
95
|
-
createSession({ views, ...options }) {
|
|
96
|
-
return new Session({
|
|
97
|
-
client: this,
|
|
98
|
-
views: views || this.views,
|
|
99
|
-
...options
|
|
100
|
-
});
|
|
107
|
+
return OpenIDClient.buildAuthorizationUrl(await this.getConfiguration(), params);
|
|
101
108
|
}
|
|
109
|
+
/** Does the client hold or have access to an unexpired API access token? */
|
|
102
110
|
async isAuthorized() {
|
|
103
111
|
if (this.token?.expiresIn()) {
|
|
104
112
|
return true;
|
|
105
113
|
}
|
|
106
114
|
else {
|
|
107
115
|
return await this.tokenLock.runExclusive(async () => {
|
|
108
|
-
|
|
116
|
+
try {
|
|
117
|
+
return !!(await this.refreshTokenGrant());
|
|
118
|
+
}
|
|
119
|
+
catch (_) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
109
122
|
});
|
|
110
123
|
}
|
|
111
124
|
}
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
/** Start interactive authorization for API access with the user */
|
|
126
|
+
async authorize() {
|
|
127
|
+
return await this.tokenLock.runExclusive(async () => {
|
|
128
|
+
console.log('running authorize from external call');
|
|
129
|
+
return await this._authorize();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Start interactive authorization for API access with the user _without_
|
|
134
|
+
* checking for tokenLock mutex
|
|
135
|
+
*
|
|
136
|
+
* Should be called _only_ from within a `tokenLock.runExclusive()` callback
|
|
137
|
+
*/
|
|
138
|
+
async _authorize() {
|
|
139
|
+
const session = new Localhost.Server({
|
|
140
|
+
client: this,
|
|
141
|
+
reason: this.reason,
|
|
142
|
+
...this.localhostOptions
|
|
143
|
+
});
|
|
114
144
|
const token = await this.save(await session.authorizationCodeGrant());
|
|
115
145
|
return token;
|
|
116
146
|
}
|
|
117
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Validate the authorization response and then executes the !"Authorization
|
|
149
|
+
* Code Grant" at the Authorization Server's token endpoint to obtain an
|
|
150
|
+
* access token. ID Token and Refresh Token are also optionally issued by the
|
|
151
|
+
* server.
|
|
152
|
+
*
|
|
153
|
+
* @param request Authorization Server's request to the Localhost redirect
|
|
154
|
+
* server
|
|
155
|
+
* @param session Contains the current `state` and `code_verifier` for the
|
|
156
|
+
* Authorization Code flow session
|
|
157
|
+
*/
|
|
158
|
+
async handleAuthorizationCodeRedirect(request, session) {
|
|
118
159
|
try {
|
|
119
|
-
|
|
120
|
-
* Do _NOT_ await this promise: the WebServer needs to send the
|
|
121
|
-
* authorization complete response asynchronously before this can resolve,
|
|
122
|
-
* and awaiting session.resolve() will block that response.
|
|
123
|
-
*/
|
|
124
|
-
session.resolve(await OpenIDClient.authorizationCodeGrant(await this.getConfiguration(), new URL(req.url, this.redirect_uri), {
|
|
160
|
+
return await OpenIDClient.authorizationCodeGrant(await this.getConfiguration(), gcrtl.expand(request.url, this.credentials.redirect_uri), {
|
|
125
161
|
pkceCodeVerifier: session.code_verifier,
|
|
126
162
|
expectedState: session.state
|
|
127
163
|
}, this.inject?.search
|
|
128
|
-
?
|
|
129
|
-
: undefined)
|
|
164
|
+
? URLSearchParams.from(this.inject.search)
|
|
165
|
+
: undefined);
|
|
130
166
|
}
|
|
131
167
|
catch (cause) {
|
|
132
|
-
|
|
168
|
+
throw new Error(`${this.name} authorization code grant failed.`, {
|
|
169
|
+
cause
|
|
170
|
+
});
|
|
133
171
|
}
|
|
134
172
|
}
|
|
135
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Perform an OAuth 2.0 Refresh Token Grant at the Authorization Server's
|
|
175
|
+
* token endpoint, allowing the client to obtain a new access token using a
|
|
176
|
+
* valid `refresh_token`.
|
|
177
|
+
*
|
|
178
|
+
* @see {@link Options.Refresh}
|
|
179
|
+
*/
|
|
180
|
+
async refreshTokenGrant({ refresh_token = this.token?.refresh_token, inject } = {}) {
|
|
136
181
|
if (!refresh_token && !this.token && this.storage) {
|
|
137
182
|
refresh_token = await this.storage.load();
|
|
138
183
|
}
|
|
139
184
|
if (!refresh_token || refresh_token === '') {
|
|
140
185
|
return undefined;
|
|
141
186
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
187
|
+
try {
|
|
188
|
+
const token = await OpenIDClient.refreshTokenGrant(await this.getConfiguration(), refresh_token, this.inject?.search
|
|
189
|
+
? URLSearchParams.from(this.inject.search)
|
|
190
|
+
: undefined, {
|
|
191
|
+
// @ts-expect-error 2322 undocumented arg pass-through to oauth4webapi
|
|
192
|
+
headers: Headers.merge(this.headers, inject?.headers)
|
|
193
|
+
});
|
|
194
|
+
return await this.save(token);
|
|
195
|
+
}
|
|
196
|
+
catch (cause) {
|
|
197
|
+
throw new Error(`Could not refresh access to ${this.name}`, { cause });
|
|
198
|
+
}
|
|
149
199
|
}
|
|
150
200
|
/**
|
|
151
201
|
* Get an unexpired access token
|
|
152
202
|
*
|
|
153
203
|
* Depending on provided and/or stored access token and refresh token values,
|
|
154
204
|
* this may require interactive authorization
|
|
205
|
+
*
|
|
206
|
+
* @see {@link Options.GetToken}
|
|
155
207
|
*/
|
|
156
208
|
async getToken({ token, inject: request } = {}) {
|
|
157
209
|
return await this.tokenLock.runExclusive(async () => {
|
|
158
210
|
token = token || this.token;
|
|
159
211
|
if (!this.token?.expiresIn() && this.storage) {
|
|
160
|
-
|
|
212
|
+
try {
|
|
213
|
+
this.token = await this.refreshTokenGrant({ inject: request });
|
|
214
|
+
}
|
|
215
|
+
catch (_) {
|
|
216
|
+
// token definitely expired and refrehing it failed
|
|
217
|
+
this.token = undefined;
|
|
218
|
+
}
|
|
161
219
|
}
|
|
162
220
|
if (!this.token) {
|
|
163
|
-
this.token = await this.
|
|
221
|
+
this.token = await this._authorize();
|
|
164
222
|
}
|
|
165
223
|
return this.token;
|
|
166
224
|
});
|
|
167
225
|
}
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Persist `refresh_token` if Token.Storage is configured and `refresh_token`
|
|
228
|
+
* provided
|
|
229
|
+
*
|
|
230
|
+
* @throws If `response` does not include `access_token` property
|
|
231
|
+
*/
|
|
232
|
+
async save(response) {
|
|
233
|
+
this.token = response;
|
|
234
|
+
if (!response.access_token) {
|
|
235
|
+
throw new Error(`${this.name} token response does not contain access_token`, {
|
|
236
|
+
cause: response
|
|
174
237
|
});
|
|
175
238
|
}
|
|
176
239
|
if (this.storage && this.token.refresh_token) {
|
|
@@ -180,25 +243,30 @@ export class Client extends EventEmitter {
|
|
|
180
243
|
return this.token;
|
|
181
244
|
}
|
|
182
245
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
246
|
+
* Request a protected resource using the client's access token.
|
|
247
|
+
*
|
|
248
|
+
* This ensures that the access token is unexpired, and interactively requests
|
|
249
|
+
* user authorization if it has not yet been provided.
|
|
250
|
+
*
|
|
251
|
+
* @param url If an `base_url` or `issuer` has been defined, `url` accepts
|
|
252
|
+
* paths relative to the `issuer` URL as well as absolute URLs
|
|
185
253
|
* @param method Optional, defaults to `GET` unless otherwise specified
|
|
186
254
|
* @param body Optional
|
|
187
255
|
* @param headers Optional
|
|
188
|
-
* @param dPoPOptions Optional
|
|
256
|
+
* @param dPoPOptions Optional, see {@link OpenIDClient.DPoPOptions}
|
|
189
257
|
*/
|
|
190
258
|
async request(url, method = 'GET', body, headers = {}, dPoPOptions) {
|
|
191
259
|
try {
|
|
192
|
-
url =
|
|
260
|
+
url = URL.from(url);
|
|
193
261
|
}
|
|
194
262
|
catch (error) {
|
|
195
263
|
if (this.base_url || this.credentials.issuer) {
|
|
196
264
|
url = path.join(
|
|
197
265
|
// @ts-expect-error 2345 TS, I _just_ tested this!
|
|
198
|
-
|
|
266
|
+
URL.toString(this.base_url || this.credentials.issuer), URL.toString(url).replace(/^\/?/, ''));
|
|
199
267
|
}
|
|
200
268
|
else {
|
|
201
|
-
throw new Error(
|
|
269
|
+
throw new Error(`${this.name} request url invalid`, {
|
|
202
270
|
cause: {
|
|
203
271
|
base_url: this.base_url,
|
|
204
272
|
issuer: this.credentials.issuer,
|
|
@@ -207,29 +275,36 @@ export class Client extends EventEmitter {
|
|
|
207
275
|
});
|
|
208
276
|
}
|
|
209
277
|
}
|
|
210
|
-
const request = async () => await OpenIDClient.fetchProtectedResource(await this.getConfiguration(), (await this.getToken()).access_token,
|
|
278
|
+
const request = async () => await OpenIDClient.fetchProtectedResource(await this.getConfiguration(), (await this.getToken()).access_token, URL.from(URLSearchParams.appendTo(url, this.inject?.search || {})), method, body, Headers.merge(this.inject?.headers, headers), dPoPOptions);
|
|
211
279
|
try {
|
|
212
280
|
return await request();
|
|
213
281
|
}
|
|
214
|
-
catch (
|
|
215
|
-
if (
|
|
216
|
-
error !== null &&
|
|
217
|
-
'status' in error &&
|
|
218
|
-
error.status === 401) {
|
|
282
|
+
catch (cause) {
|
|
283
|
+
if (Error.isError(cause) && 'status' in cause && cause.status === 401) {
|
|
219
284
|
await this.authorize();
|
|
220
285
|
return await request();
|
|
221
286
|
}
|
|
222
287
|
else {
|
|
223
|
-
throw
|
|
288
|
+
throw new Error(`${this.name} request failed`, { cause });
|
|
224
289
|
}
|
|
225
290
|
}
|
|
226
291
|
}
|
|
292
|
+
/** Parse a fetch response as JSON, typing it as J */
|
|
227
293
|
async toJSON(response) {
|
|
228
294
|
if (response.ok) {
|
|
229
|
-
|
|
295
|
+
try {
|
|
296
|
+
return (await response.json());
|
|
297
|
+
}
|
|
298
|
+
catch (cause) {
|
|
299
|
+
throw new Error(`${this.name} response could not be parsed as JSON`, {
|
|
300
|
+
cause
|
|
301
|
+
});
|
|
302
|
+
}
|
|
230
303
|
}
|
|
231
304
|
else {
|
|
232
|
-
throw new Error(
|
|
305
|
+
throw new Error(`${this.name} response status not ok`, {
|
|
306
|
+
cause: { response }
|
|
307
|
+
});
|
|
233
308
|
}
|
|
234
309
|
}
|
|
235
310
|
/**
|
|
@@ -239,9 +314,25 @@ export class Client extends EventEmitter {
|
|
|
239
314
|
async requestJSON(url, method = 'GET', body, headers = {}, dPoPOptions) {
|
|
240
315
|
return await this.toJSON(await this.request(url, method, body, headers, dPoPOptions));
|
|
241
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Request a protected resource using the client's access token.
|
|
319
|
+
*
|
|
320
|
+
* This ensures that the access token is unexpired, and interactively requests
|
|
321
|
+
* user authorization if it has not yet been provided.
|
|
322
|
+
*
|
|
323
|
+
* @param input If a `base_url` or `issuer` has been defined, `url` accepts
|
|
324
|
+
* paths relative to the `issuer` URL as well as absolute URLs
|
|
325
|
+
* @param init Optional
|
|
326
|
+
* @param dPoPOptions Optional, see {@link OpenIDClient.DPoPOptions}
|
|
327
|
+
* @see {@link request} for which this is an alias for {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API Fetch API}-style requests
|
|
328
|
+
*/
|
|
242
329
|
async fetch(input, init, dPoPOptions) {
|
|
243
|
-
return await this.request(input, init?.method, await
|
|
330
|
+
return await this.request(input, init?.method, await Body.from(init?.body), Headers.from(init?.headers), dPoPOptions);
|
|
244
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Returns the result of {@link fetch} as a parsed JSON object, optionally
|
|
334
|
+
* typed as `J`
|
|
335
|
+
*/
|
|
245
336
|
async fetchJSON(input, init, dPoPOptions) {
|
|
246
337
|
return await this.toJSON(await this.fetch(input, init, dPoPOptions));
|
|
247
338
|
}
|
package/dist/Credentials.d.ts
CHANGED
|
@@ -1,16 +1,48 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { URL } from 'requestish';
|
|
2
|
+
import { Scope } from './Token/index.js';
|
|
3
3
|
export type Credentials = {
|
|
4
|
+
/** OAuth 2.0 / OpenID Connect `client_id` value */
|
|
4
5
|
client_id: string;
|
|
6
|
+
/** OAuth 2.0 / OpenID Connect `client_secret` value */
|
|
5
7
|
client_secret: string;
|
|
6
|
-
|
|
8
|
+
/**
|
|
9
|
+
* OAuth 2.0 / OpenID Connect `redirect_uri` value
|
|
10
|
+
*
|
|
11
|
+
* This must either be a URL of the form `http://localhost` or redirect to
|
|
12
|
+
* `http://localhost` for the client to be able to provide support for it. If
|
|
13
|
+
* SSL encryption is required, you must provide that configuration
|
|
14
|
+
* independently.
|
|
15
|
+
*/
|
|
16
|
+
redirect_uri: URL.ish;
|
|
17
|
+
/** OAuth 2.0 / OpenID Connect access `scope` value */
|
|
7
18
|
scope?: Scope.ish;
|
|
8
19
|
} & ({
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Optional OpenID Connect `issuer` to use for the well-known URL
|
|
22
|
+
* discovery process
|
|
23
|
+
*/
|
|
24
|
+
issuer?: URL.ish;
|
|
25
|
+
/**
|
|
26
|
+
* OAuth 2.0 `authorization_endpoint` URL to for during Authorization Code
|
|
27
|
+
* flow
|
|
28
|
+
*/
|
|
29
|
+
authorization_endpoint: URL.ish;
|
|
30
|
+
/**
|
|
31
|
+
* OAuth 2.0 `authorization_endpoint` URL to for during Authorization Code
|
|
32
|
+
* flow
|
|
33
|
+
*/
|
|
34
|
+
token_endpoint: URL.ish;
|
|
12
35
|
} | {
|
|
13
|
-
issuer
|
|
14
|
-
|
|
15
|
-
|
|
36
|
+
/** OpenID Connect `issuer` to use for the well-known URL discovery process */
|
|
37
|
+
issuer: URL.ish;
|
|
38
|
+
/**
|
|
39
|
+
* Optional OAuth 2.0 `authorization_endpoint` URL to for during
|
|
40
|
+
* Authorization Code flow
|
|
41
|
+
*/
|
|
42
|
+
authorization_endpoint?: URL.ish;
|
|
43
|
+
/**
|
|
44
|
+
* Optional OAuth 2.0 `authorization_endpoint` URL to for during
|
|
45
|
+
* Authorization Code flow
|
|
46
|
+
*/
|
|
47
|
+
token_endpoint?: URL.ish;
|
|
16
48
|
});
|
package/dist/Injection.d.ts
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { Body, Headers, URLSearchParams } from 'requestish';
|
|
2
|
+
import { Scope } from './Token/index.js';
|
|
3
|
+
/** Request components to inject when communicating with the API server */
|
|
3
4
|
export type Injection = {
|
|
4
5
|
/**
|
|
5
6
|
* Search query parameters to include in server request (may be ovewritten by
|
|
6
7
|
* computed values such as `state` or `challenge_code`)
|
|
7
8
|
*/
|
|
8
|
-
search?:
|
|
9
|
+
search?: URLSearchParams.ish;
|
|
9
10
|
/**
|
|
10
11
|
* HTTP headers to include in server request (may be overwritten by computed
|
|
11
12
|
* values such as `Authorization: Bearer <token>`)
|
|
12
13
|
*/
|
|
13
|
-
headers?:
|
|
14
|
+
headers?: Headers.ish;
|
|
14
15
|
/**
|
|
15
16
|
* HTTP request body parameters to include in server request (if request
|
|
16
17
|
* method allows)
|
|
17
18
|
*/
|
|
18
|
-
body?:
|
|
19
|
+
body?: Body.ish;
|
|
19
20
|
/** Specific scope or scopes */
|
|
20
21
|
scope?: Scope.ish;
|
|
21
22
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DEFAULT_LAUNCH_ENDPOINT = "/oauth2-cli/authorize";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_LAUNCH_ENDPOINT = '/oauth2-cli/authorize';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { PathString } from '@battis/descriptive-types';
|
|
2
|
+
import { Client } from '../index.js';
|
|
3
|
+
export type Options = {
|
|
4
|
+
client: Client;
|
|
5
|
+
/** Human-readable name of the app requesting API authorization */
|
|
6
|
+
reason?: string;
|
|
7
|
+
/** See {@link Session.setViews setViews()} */
|
|
8
|
+
views?: PathString;
|
|
9
|
+
/**
|
|
10
|
+
* Local web server launch endpoint
|
|
11
|
+
*
|
|
12
|
+
* This is separate and distinct from the OpenID/OAuth server's authorization
|
|
13
|
+
* endpoint. This endpoint is the first path that the user is directed to in
|
|
14
|
+
* their browser. It can present an explanation of what is being authorized
|
|
15
|
+
* and why. By default it redirects to the OpenID/OAuth server's authorization
|
|
16
|
+
* URL, the first step in the Authorization Code Grant flow.
|
|
17
|
+
*/
|
|
18
|
+
launch_endpoint?: PathString;
|
|
19
|
+
/**
|
|
20
|
+
* The number of milliseconds of inactivity before a socket is presumed to
|
|
21
|
+
* have timed out. This can be reduced to limit potential wait times during
|
|
22
|
+
* interactive authentication, but must still be long enough to allow time for
|
|
23
|
+
* the authorization code to be exchanged for an access token.
|
|
24
|
+
*
|
|
25
|
+
* Defaults to 1000 milliseconds
|
|
26
|
+
*/
|
|
27
|
+
timeout?: number;
|
|
28
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { PathString } from '@battis/descriptive-types';
|
|
2
|
+
import { Request, Response } from 'express';
|
|
3
|
+
import * as Token from '../Token/index.js';
|
|
4
|
+
import { Options } from './Options.js';
|
|
5
|
+
/**
|
|
6
|
+
* Minimal HTTP server running on localhost to handle the redirect step of
|
|
7
|
+
* OpenID/OAuth flows
|
|
8
|
+
*/
|
|
9
|
+
export declare class Server {
|
|
10
|
+
private static activePorts;
|
|
11
|
+
/** PKCE code_verifier */
|
|
12
|
+
readonly code_verifier: string;
|
|
13
|
+
/** OAuth 2.0 state (if PKCE is not supported) */
|
|
14
|
+
readonly state: string;
|
|
15
|
+
private client;
|
|
16
|
+
private reason;
|
|
17
|
+
private views?;
|
|
18
|
+
private packageViews;
|
|
19
|
+
private spinner;
|
|
20
|
+
protected readonly port: string;
|
|
21
|
+
readonly launch_endpoint: PathString;
|
|
22
|
+
private server;
|
|
23
|
+
private resolveAuthorizationCodeFlow?;
|
|
24
|
+
private rejectAuthorizationCodeFlow?;
|
|
25
|
+
constructor({ reason, client, views, launch_endpoint, timeout }: Options);
|
|
26
|
+
authorizationCodeGrant(): Promise<Token.Response>;
|
|
27
|
+
/**
|
|
28
|
+
* Set the path to folder of *.ejs templates
|
|
29
|
+
*
|
|
30
|
+
* Expected templates include:
|
|
31
|
+
*
|
|
32
|
+
* - `launch.ejs` presents information prior to the authorization to the user,
|
|
33
|
+
* and the user must follow `authorize_url` data property to interactively
|
|
34
|
+
* launch authorization
|
|
35
|
+
* - `complete.ejs` presented to user upon successful completion of
|
|
36
|
+
* authorization flow
|
|
37
|
+
* - `error.ejs` presented to user upon receipt of an error from the server,
|
|
38
|
+
* includes `error` as data
|
|
39
|
+
*
|
|
40
|
+
* `complete.ejs` and `error.ejs` are included with oauth2-cli and those
|
|
41
|
+
* templates will be used if `ejs` is imported but no replacement templates
|
|
42
|
+
* are found.
|
|
43
|
+
*
|
|
44
|
+
* All views receive a data property `name` which is the human-readable name
|
|
45
|
+
* of the Client managing the authorization code flow, and `reason` indicating
|
|
46
|
+
* the human-readable reason (i.e. name of the app) requesting authorization,
|
|
47
|
+
* which should be displayed in messages for transparency.
|
|
48
|
+
*
|
|
49
|
+
* @param views Should be an absolute path
|
|
50
|
+
*/
|
|
51
|
+
setViews(views: PathString): void;
|
|
52
|
+
protected render(res: Response, template: string, data?: Record<string, unknown>): Promise<boolean>;
|
|
53
|
+
/** Handles request to `/authorize` */
|
|
54
|
+
protected handleAuthorizationEndpoint(req: Request, res: Response): Promise<void>;
|
|
55
|
+
/** Handles request to `redirect_uri` */
|
|
56
|
+
protected handleRedirect(req: Request, res: Response): Promise<void>;
|
|
57
|
+
/** Shut down web server */
|
|
58
|
+
close(): Promise<void>;
|
|
59
|
+
}
|