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
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,86 +15,98 @@ 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;
|
|
@@ -109,68 +117,118 @@ export class Client extends EventEmitter {
|
|
|
109
117
|
});
|
|
110
118
|
}
|
|
111
119
|
}
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
/** Start interactive authorization for API access with the user */
|
|
121
|
+
async authorize() {
|
|
122
|
+
return await this.tokenLock.runExclusive(async () => {
|
|
123
|
+
console.log('running authorize from external call');
|
|
124
|
+
return await this._authorize();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Start interactive authorization for API access with the user _without_
|
|
129
|
+
* checking for tokenLock mutex
|
|
130
|
+
*
|
|
131
|
+
* Should be called _only_ from within a `tokenLock.runExclusive()` callback
|
|
132
|
+
*/
|
|
133
|
+
async _authorize() {
|
|
134
|
+
const session = new Localhost.Server({
|
|
135
|
+
client: this,
|
|
136
|
+
reason: this.reason,
|
|
137
|
+
...this.localhostOptions
|
|
138
|
+
});
|
|
114
139
|
const token = await this.save(await session.authorizationCodeGrant());
|
|
115
140
|
return token;
|
|
116
141
|
}
|
|
117
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Validate the authorization response and then executes the !"Authorization
|
|
144
|
+
* Code Grant" at the Authorization Server's token endpoint to obtain an
|
|
145
|
+
* access token. ID Token and Refresh Token are also optionally issued by the
|
|
146
|
+
* server.
|
|
147
|
+
*
|
|
148
|
+
* @param request Authorization Server's request to the Localhost redirect
|
|
149
|
+
* server
|
|
150
|
+
* @param session Contains the current `state` and `code_verifier` for the
|
|
151
|
+
* Authorization Code flow session
|
|
152
|
+
*/
|
|
153
|
+
async handleAuthorizationCodeRedirect(request, session) {
|
|
118
154
|
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), {
|
|
155
|
+
return await OpenIDClient.authorizationCodeGrant(await this.getConfiguration(), gcrtl.expand(request.url, this.credentials.redirect_uri), {
|
|
125
156
|
pkceCodeVerifier: session.code_verifier,
|
|
126
157
|
expectedState: session.state
|
|
127
158
|
}, this.inject?.search
|
|
128
|
-
?
|
|
129
|
-
: undefined)
|
|
159
|
+
? URLSearchParams.from(this.inject.search)
|
|
160
|
+
: undefined);
|
|
130
161
|
}
|
|
131
162
|
catch (cause) {
|
|
132
|
-
|
|
163
|
+
throw new Error(`${this.name} authorization code grant failed.`, {
|
|
164
|
+
cause
|
|
165
|
+
});
|
|
133
166
|
}
|
|
134
167
|
}
|
|
135
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Perform an OAuth 2.0 Refresh Token Grant at the Authorization Server's
|
|
170
|
+
* token endpoint, allowing the client to obtain a new access token using a
|
|
171
|
+
* valid `refresh_token`.
|
|
172
|
+
*
|
|
173
|
+
* @see {@link Options.Refresh}
|
|
174
|
+
*/
|
|
175
|
+
async refreshTokenGrant({ refresh_token = this.token?.refresh_token, inject } = {}) {
|
|
136
176
|
if (!refresh_token && !this.token && this.storage) {
|
|
137
177
|
refresh_token = await this.storage.load();
|
|
138
178
|
}
|
|
139
179
|
if (!refresh_token || refresh_token === '') {
|
|
140
180
|
return undefined;
|
|
141
181
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
182
|
+
try {
|
|
183
|
+
const token = await OpenIDClient.refreshTokenGrant(await this.getConfiguration(), refresh_token, this.inject?.search
|
|
184
|
+
? URLSearchParams.from(this.inject.search)
|
|
185
|
+
: undefined, {
|
|
186
|
+
// @ts-expect-error 2322 undocumented arg pass-through to oauth4webapi
|
|
187
|
+
headers: Headers.merge(this.headers, inject?.headers)
|
|
188
|
+
});
|
|
189
|
+
return await this.save(token);
|
|
190
|
+
}
|
|
191
|
+
catch (cause) {
|
|
192
|
+
throw new Error(`Could not refresh access to ${this.name}`, { cause });
|
|
193
|
+
}
|
|
149
194
|
}
|
|
150
195
|
/**
|
|
151
196
|
* Get an unexpired access token
|
|
152
197
|
*
|
|
153
198
|
* Depending on provided and/or stored access token and refresh token values,
|
|
154
199
|
* this may require interactive authorization
|
|
200
|
+
*
|
|
201
|
+
* @see {@link Options.GetToken}
|
|
155
202
|
*/
|
|
156
203
|
async getToken({ token, inject: request } = {}) {
|
|
157
204
|
return await this.tokenLock.runExclusive(async () => {
|
|
158
205
|
token = token || this.token;
|
|
159
206
|
if (!this.token?.expiresIn() && this.storage) {
|
|
160
|
-
|
|
207
|
+
try {
|
|
208
|
+
this.token = await this.refreshTokenGrant({ inject: request });
|
|
209
|
+
}
|
|
210
|
+
catch (_) {
|
|
211
|
+
// token definitely expired and refrehing it failed
|
|
212
|
+
this.token = undefined;
|
|
213
|
+
}
|
|
161
214
|
}
|
|
162
215
|
if (!this.token) {
|
|
163
|
-
this.token = await this.
|
|
216
|
+
this.token = await this._authorize();
|
|
164
217
|
}
|
|
165
218
|
return this.token;
|
|
166
219
|
});
|
|
167
220
|
}
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Persist `refresh_token` if Token.Storage is configured and `refresh_token`
|
|
223
|
+
* provided
|
|
224
|
+
*
|
|
225
|
+
* @throws If `response` does not include `access_token` property
|
|
226
|
+
*/
|
|
227
|
+
async save(response) {
|
|
228
|
+
this.token = response;
|
|
229
|
+
if (!response.access_token) {
|
|
230
|
+
throw new Error(`${this.name} token response does not contain access_token`, {
|
|
231
|
+
cause: response
|
|
174
232
|
});
|
|
175
233
|
}
|
|
176
234
|
if (this.storage && this.token.refresh_token) {
|
|
@@ -180,25 +238,30 @@ export class Client extends EventEmitter {
|
|
|
180
238
|
return this.token;
|
|
181
239
|
}
|
|
182
240
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
241
|
+
* Request a protected resource using the client's access token.
|
|
242
|
+
*
|
|
243
|
+
* This ensures that the access token is unexpired, and interactively requests
|
|
244
|
+
* user authorization if it has not yet been provided.
|
|
245
|
+
*
|
|
246
|
+
* @param url If an `base_url` or `issuer` has been defined, `url` accepts
|
|
247
|
+
* paths relative to the `issuer` URL as well as absolute URLs
|
|
185
248
|
* @param method Optional, defaults to `GET` unless otherwise specified
|
|
186
249
|
* @param body Optional
|
|
187
250
|
* @param headers Optional
|
|
188
|
-
* @param dPoPOptions Optional
|
|
251
|
+
* @param dPoPOptions Optional, see {@link OpenIDClient.DPoPOptions}
|
|
189
252
|
*/
|
|
190
253
|
async request(url, method = 'GET', body, headers = {}, dPoPOptions) {
|
|
191
254
|
try {
|
|
192
|
-
url =
|
|
255
|
+
url = URL.from(url);
|
|
193
256
|
}
|
|
194
257
|
catch (error) {
|
|
195
258
|
if (this.base_url || this.credentials.issuer) {
|
|
196
259
|
url = path.join(
|
|
197
260
|
// @ts-expect-error 2345 TS, I _just_ tested this!
|
|
198
|
-
|
|
261
|
+
URL.toString(this.base_url || this.credentials.issuer), URL.toString(url).replace(/^\/?/, ''));
|
|
199
262
|
}
|
|
200
263
|
else {
|
|
201
|
-
throw new Error(
|
|
264
|
+
throw new Error(`${this.name} request url invalid`, {
|
|
202
265
|
cause: {
|
|
203
266
|
base_url: this.base_url,
|
|
204
267
|
issuer: this.credentials.issuer,
|
|
@@ -207,29 +270,36 @@ export class Client extends EventEmitter {
|
|
|
207
270
|
});
|
|
208
271
|
}
|
|
209
272
|
}
|
|
210
|
-
const request = async () => await OpenIDClient.fetchProtectedResource(await this.getConfiguration(), (await this.getToken()).access_token,
|
|
273
|
+
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
274
|
try {
|
|
212
275
|
return await request();
|
|
213
276
|
}
|
|
214
|
-
catch (
|
|
215
|
-
if (
|
|
216
|
-
error !== null &&
|
|
217
|
-
'status' in error &&
|
|
218
|
-
error.status === 401) {
|
|
277
|
+
catch (cause) {
|
|
278
|
+
if (Error.isError(cause) && 'status' in cause && cause.status === 401) {
|
|
219
279
|
await this.authorize();
|
|
220
280
|
return await request();
|
|
221
281
|
}
|
|
222
282
|
else {
|
|
223
|
-
throw
|
|
283
|
+
throw new Error(`${this.name} request failed`, { cause });
|
|
224
284
|
}
|
|
225
285
|
}
|
|
226
286
|
}
|
|
287
|
+
/** Parse a fetch response as JSON, typing it as J */
|
|
227
288
|
async toJSON(response) {
|
|
228
289
|
if (response.ok) {
|
|
229
|
-
|
|
290
|
+
try {
|
|
291
|
+
return (await response.json());
|
|
292
|
+
}
|
|
293
|
+
catch (cause) {
|
|
294
|
+
throw new Error(`${this.name} response could not be parsed as JSON`, {
|
|
295
|
+
cause
|
|
296
|
+
});
|
|
297
|
+
}
|
|
230
298
|
}
|
|
231
299
|
else {
|
|
232
|
-
throw new Error(
|
|
300
|
+
throw new Error(`${this.name} response status not ok`, {
|
|
301
|
+
cause: { response }
|
|
302
|
+
});
|
|
233
303
|
}
|
|
234
304
|
}
|
|
235
305
|
/**
|
|
@@ -239,9 +309,25 @@ export class Client extends EventEmitter {
|
|
|
239
309
|
async requestJSON(url, method = 'GET', body, headers = {}, dPoPOptions) {
|
|
240
310
|
return await this.toJSON(await this.request(url, method, body, headers, dPoPOptions));
|
|
241
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Request a protected resource using the client's access token.
|
|
314
|
+
*
|
|
315
|
+
* This ensures that the access token is unexpired, and interactively requests
|
|
316
|
+
* user authorization if it has not yet been provided.
|
|
317
|
+
*
|
|
318
|
+
* @param input If a `base_url` or `issuer` has been defined, `url` accepts
|
|
319
|
+
* paths relative to the `issuer` URL as well as absolute URLs
|
|
320
|
+
* @param init Optional
|
|
321
|
+
* @param dPoPOptions Optional, see {@link OpenIDClient.DPoPOptions}
|
|
322
|
+
* @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
|
|
323
|
+
*/
|
|
242
324
|
async fetch(input, init, dPoPOptions) {
|
|
243
|
-
return await this.request(input, init?.method, await
|
|
325
|
+
return await this.request(input, init?.method, await Body.from(init?.body), Headers.from(init?.headers), dPoPOptions);
|
|
244
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Returns the result of {@link fetch} as a parsed JSON object, optionally
|
|
329
|
+
* typed as `J`
|
|
330
|
+
*/
|
|
245
331
|
async fetchJSON(input, init, dPoPOptions) {
|
|
246
332
|
return await this.toJSON(await this.fetch(input, init, dPoPOptions));
|
|
247
333
|
}
|
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
|
+
}
|