oauth2-cli 0.5.1 → 0.6.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 +22 -0
- package/README.md +87 -1
- package/dist/Client.d.ts +111 -45
- package/dist/Client.js +175 -112
- package/dist/Credentials.d.ts +29 -0
- package/dist/Errors/BadResponse.d.ts +3 -0
- package/dist/Errors/BadResponse.js +7 -0
- package/dist/Errors/IndeterminateConfiguration.d.ts +3 -0
- package/dist/Errors/IndeterminateConfiguration.js +5 -0
- package/dist/Errors/MissingAccessToken.d.ts +3 -0
- package/dist/Errors/MissingAccessToken.js +5 -0
- package/dist/Errors/PortCollision.d.ts +3 -0
- package/dist/Errors/PortCollision.js +5 -0
- package/dist/Errors/index.d.ts +4 -0
- package/dist/Errors/index.js +4 -0
- package/dist/Request/Body.d.ts +3 -0
- package/dist/Request/Body.js +19 -0
- package/dist/Request/Headers.d.ts +3 -0
- package/dist/Request/Headers.js +20 -0
- package/dist/Request/Injection.d.ts +23 -0
- package/dist/Request/Injection.js +1 -0
- package/dist/Request/Scope.d.ts +2 -0
- package/dist/Request/Scope.js +6 -0
- package/dist/Request/URL.d.ts +4 -0
- package/dist/Request/URL.js +12 -0
- package/dist/Request/URLSearchParams.d.ts +6 -0
- package/dist/Request/URLSearchParams.js +37 -0
- package/dist/Request/index.d.ts +6 -0
- package/dist/Request/index.js +6 -0
- package/dist/Session.d.ts +47 -0
- package/dist/Session.js +77 -0
- package/dist/{FileStorage.d.ts → Token/FileStorage.d.ts} +2 -3
- package/dist/{FileStorage.js → Token/FileStorage.js} +3 -4
- package/dist/Token/Response.d.ts +2 -0
- package/dist/Token/Response.js +1 -0
- package/dist/Token/TokenStorage.d.ts +4 -0
- package/dist/Token/TokenStorage.js +1 -0
- package/dist/Token/addHelpers.d.ts +8 -0
- package/dist/Token/addHelpers.js +44 -0
- package/dist/Token/index.d.ts +3 -0
- package/dist/Token/index.js +3 -0
- package/dist/WebServer.d.ts +64 -0
- package/dist/WebServer.js +116 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/package.json +10 -6
- package/dist/Localhost.d.ts +0 -14
- package/dist/Localhost.js +0 -88
- package/dist/Token.d.ts +0 -18
- package/dist/Token.js +0 -26
- package/dist/TokenStorage.d.ts +0 -5
- /package/dist/{TokenStorage.js → Credentials.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [0.6.0](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.5.1...oauth2-cli/0.6.0) (2026-02-15)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* limit TokenStorage to storing _only_ refresh_tokens
|
|
11
|
+
* improve express shutdown, refactor entire package
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* compatible with @battis/google-cloud-run-to-localhost ([3e449b1](https://github.com/battis/oauth2-cli/commit/3e449b14d546759f7e6543c86350ea83b60a80a7))
|
|
16
|
+
* display authorization starting URL on command line as well as opening browser ([fb72bcf](https://github.com/battis/oauth2-cli/commit/fb72bcffafaf66453f8f8f0f8af4b27ef11827c6))
|
|
17
|
+
* externalize path/port identification to gcrtl ([0f6c28b](https://github.com/battis/oauth2-cli/commit/0f6c28b31a7888eac524b28bcdc9c8eabbf57d91))
|
|
18
|
+
* fallback to package templates if ejs present but no alternative template providedd ([6b05454](https://github.com/battis/oauth2-cli/commit/6b05454976b1fb4144f91ba51fc1c9331f1c0f34))
|
|
19
|
+
* limit TokenStorage to storing _only_ refresh_tokens ([3de9c96](https://github.com/battis/oauth2-cli/commit/3de9c96510d15eebd51a0be7d8df278614541f95))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* attempt to authorize and retry when encountering 401 error ([bc75fed](https://github.com/battis/oauth2-cli/commit/bc75fed052b548e41411287f7d98fa78c3f27ee5))
|
|
25
|
+
* improve express shutdown, refactor entire package ([a740313](https://github.com/battis/oauth2-cli/commit/a740313c33d26f07ebab5b282607d8828cf3c3a7))
|
|
26
|
+
|
|
5
27
|
## [0.5.1](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.5.0...oauth2-cli/0.5.1) (2026-01-20)
|
|
6
28
|
|
|
7
29
|
|
package/README.md
CHANGED
|
@@ -13,4 +13,90 @@ npm i oauth2-cli
|
|
|
13
13
|
|
|
14
14
|
## Usage
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
```ts
|
|
17
|
+
import { Client, FileStorage } from 'oauth2-cli';
|
|
18
|
+
|
|
19
|
+
type ExpectedResponse = {
|
|
20
|
+
value: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const client = new Client({
|
|
24
|
+
client_id: 'm3C6dGQJPJrvgwN97mTP4pWVH9smZGrr',
|
|
25
|
+
client_secret: '2XUktyxU2KQmQAoVHxQXNaHZ4G7XqJdP',
|
|
26
|
+
redirect_uri: 'http://localhost:3000/example/redirect',
|
|
27
|
+
authorization_endpoint: 'https://example.com/oauth2/auth',
|
|
28
|
+
token_endpoint: 'https://example.com/oauth2/token',
|
|
29
|
+
storage: new FileStorage('/path/to/token/file.json');
|
|
30
|
+
});
|
|
31
|
+
console.log(
|
|
32
|
+
client.fetchJSON<ExpectedResponse>('https://example.com/path/to/api/endpoint')
|
|
33
|
+
);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Broadly speaking, having provided the configuration, the client is immediately ready to accept requests. If an stored access token is available, it will be used (and transparently refreshed as necessary, if possible). If no access token is available, the authorization flow will be triggered by the request, opening a browser window for the user to sign in and provide their authoriztion.
|
|
37
|
+
|
|
38
|
+
### Instantiate a `Client`
|
|
39
|
+
|
|
40
|
+
A `Client` requires some minimal information in order to interact with an OAuth 2.0 authorized API. The OAuth 2.0 base set is a `client_id`, `client_secret`, `authorization_endpoint`, `token_endpoint`, and a `redirect_uri`. For an OpenID-authenticated API, you could provide a `client_id`, `client_secret`, `issuer`, and `redirect_uri` and the Client will query the issuer for further details regarding required connection parameters (it is built on to of [openid-client](https://www.npmjs.com/package/openid-client)).
|
|
41
|
+
|
|
42
|
+
In both cases, the token can be persisted by passing an implementation of [`TokenStorage`](https://github.com/battis/oauth2-cli/blob/main/packages/oauth2-cli/src/Token/TokenStorage.ts), such as [`FileStorage`](https://github.com/battis/oauth2-cli/blob/main/packages/oauth2-cli/src/Token/FileStorage.ts) which expects a path to a location to store a JSON file of access token data. _There are more secure ways to store your tokens, such as [@oauth2-cli/qui-cli](https://www.npmjs.com/package/@oauth2-cli/qui-cli)'s [`EnvironmentStorage`](https://github.com/battis/oauth2-cli/blob/main/packages/qui-cli/src/EnvironmentStorage.ts) which can be linked to a [1Password vault](https://github.com/battis/qui-cli/tree/main/packages/env#1password-integration)._
|
|
43
|
+
|
|
44
|
+
#### `redirect_uri` to Localhost
|
|
45
|
+
|
|
46
|
+
Since the `redirect_uri` is receiving the authorization code in the Authorization Code token flow, the Client needs to be able to "catch" that redirect. The easy way to do this is to register a localhost address with the API (e.g. `http://localhost:3000/my/redirect/path`). When such a redirect URI is given to the client, it stands up (briefly) a local web server to receive that request at the port and path provided.
|
|
47
|
+
|
|
48
|
+
Not every API accepts a `localhost` redirect (it creates the possibility of CORS exploits that could lead to XSS vulnerabilities). For these APIs, using [gcrtl](https://github.com/battis/google-cloud-run-to-localhost#readme) or a similar system will work as well. (In the specific case of `gcrtl`, `oauth2-cli` will trim the leading `/http/localhost:<port>` from provided `redirect_uri` and expect the _remainder_ of the path.)
|
|
49
|
+
|
|
50
|
+
#### `http` protocol
|
|
51
|
+
|
|
52
|
+
If you would prefer an `https` connection to localhost, you have to roll your own SSL certificate.
|
|
53
|
+
|
|
54
|
+
### Request an endpoint
|
|
55
|
+
|
|
56
|
+
#### `request()`
|
|
57
|
+
|
|
58
|
+
As noted above, `oauth2-cli` is built on top of [openid-client](https://www.npmjs.com/package/openid-client). The `request()` method is a pass-through to the `openid-client` [fetchProtectedResource()](https://github.com/panva/openid-client/blob/b77d87c1e2f5fef6fab501de615fb83a74a0251f/docs/functions/fetchProtectedResource.md) function, with the configuration and accessToken managed by the Client.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
class Client {
|
|
62
|
+
// ...
|
|
63
|
+
|
|
64
|
+
public async request(
|
|
65
|
+
url: Req.URL.ish,
|
|
66
|
+
method = 'GET',
|
|
67
|
+
body?: OpenIDClient.FetchBody,
|
|
68
|
+
headers: Req.Headers.ish = {},
|
|
69
|
+
dPoPOptions?: OpenIDClient.DPoPOptions
|
|
70
|
+
) {
|
|
71
|
+
// ...
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
[`Req.URL.ish`](https://github.com/battis/oauth2-cli/blob/main/packages/oauth2-cli/src/Request/URL.ts#L3) and [`Req.Headers.ish`](https://github.com/battis/oauth2-cli/blob/main/packages/oauth2-cli/src/Request/Headers.ts#L1) are more forgiving types accepting not just those specific types, but reasonable facsimiles of them.
|
|
77
|
+
|
|
78
|
+
### `requestJSON<T>()`
|
|
79
|
+
|
|
80
|
+
Given that many APIs return JSON-formatted responses, it is convenient to just get that JSON (optionally pre-typed based on what you expect to receive) rather than having to process the response yourself.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
class Client {
|
|
84
|
+
// ...
|
|
85
|
+
|
|
86
|
+
public async requestJSON<
|
|
87
|
+
T extends OpenIDClient.JsonValue = OpenIDClient.JsonValue
|
|
88
|
+
>(
|
|
89
|
+
url: Req.URL.ish,
|
|
90
|
+
method = 'GET',
|
|
91
|
+
body?: OpenIDClient.FetchBody,
|
|
92
|
+
headers: Req.Headers.ish = {},
|
|
93
|
+
dPoPOptions?: OpenIDClient.DPoPOptions
|
|
94
|
+
) {
|
|
95
|
+
// ...
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Examples
|
|
101
|
+
|
|
102
|
+
[Refer to examples for more detailed usage.](https://github.com/battis/oauth2-cli/tree/main/examples/oauth2-cli#readme)
|
package/dist/Client.d.ts
CHANGED
|
@@ -1,57 +1,123 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { PathString } from '@battis/descriptive-types';
|
|
2
|
+
import { Request } from 'express';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
3
4
|
import * as OpenIDClient from 'openid-client';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
import * as Credentials from './Credentials.js';
|
|
6
|
+
import * as Req from './Request/index.js';
|
|
7
|
+
import { Session, SessionOptions } from './Session.js';
|
|
8
|
+
import * as Token from './Token/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* A generic `redirect_uri` to use if the server does not require pre-registered
|
|
11
|
+
* `redirect_uri` values
|
|
12
|
+
*/
|
|
13
|
+
export declare const DEFAULT_REDIRECT_URI = "http://localhost:3000/oauth2-cli/redirect";
|
|
14
|
+
export type ClientOptions = {
|
|
15
|
+
/** Credentials for server access */
|
|
16
|
+
credentials: Credentials.Combined;
|
|
17
|
+
/**
|
|
18
|
+
* Optional absolute path to EJS view templates directory, see
|
|
19
|
+
* [WebServer.setViews()](./Webserver.ts)
|
|
20
|
+
*/
|
|
21
|
+
views?: PathString;
|
|
22
|
+
/** Optional {@link TokenStorage} implementation to manage tokens */
|
|
23
|
+
storage?: Token.TokenStorage;
|
|
24
|
+
/**
|
|
25
|
+
* Optional search query parameters to include in all server requests (see
|
|
26
|
+
* {@link RequestAddons.search})
|
|
27
|
+
*/
|
|
28
|
+
search?: Req.URLSearchParams.ish;
|
|
29
|
+
/**
|
|
30
|
+
* Optional headers to include in all server requests (see
|
|
31
|
+
* {@link RequestAddons.headers})
|
|
32
|
+
*/
|
|
33
|
+
headers?: Req.Headers.ish;
|
|
34
|
+
/**
|
|
35
|
+
* Optional body parameters to include in applicable server requests (see
|
|
36
|
+
* {@link RequestAddons.body})
|
|
37
|
+
*/
|
|
38
|
+
body?: Req.Body.ish;
|
|
39
|
+
};
|
|
40
|
+
type RefreshOptions = {
|
|
41
|
+
/**
|
|
42
|
+
* Optional refresh token
|
|
43
|
+
*
|
|
44
|
+
* If using {@link TokenStorage}, the refresh token should be stored with the
|
|
45
|
+
* access token and does not need to be separately managed and stored
|
|
46
|
+
*/
|
|
47
|
+
refresh_token?: string;
|
|
48
|
+
/** Additional request injection for refresh grant flow */
|
|
49
|
+
inject?: Req.Injection;
|
|
11
50
|
};
|
|
12
|
-
|
|
13
|
-
|
|
51
|
+
type GetTokenOptions = {
|
|
52
|
+
/**
|
|
53
|
+
* Optional access token
|
|
54
|
+
*
|
|
55
|
+
* If using {@link TokenStorage}, the access token does not need to be
|
|
56
|
+
* separately managed and stored
|
|
57
|
+
*/
|
|
58
|
+
token?: Token.Response;
|
|
59
|
+
/**
|
|
60
|
+
* Additional request injection for authorization code grant and/or refresh
|
|
61
|
+
* grant flows
|
|
62
|
+
*/
|
|
63
|
+
inject?: Req.Injection;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Wrap {@link https://www.npmjs.com/package/openid-client openid-client} in a
|
|
67
|
+
* class instance specific to a particular OAuth/OpenID server credential-set,
|
|
68
|
+
* abstracting away most flows into {@link getToken}
|
|
69
|
+
*
|
|
70
|
+
* Emits {@link Client.TokenEvent} whenever a new access token is received
|
|
71
|
+
*/
|
|
72
|
+
export declare class Client extends EventEmitter {
|
|
73
|
+
static readonly TokenEvent = "token";
|
|
14
74
|
private credentials;
|
|
15
|
-
private tokenMutex;
|
|
16
75
|
private config?;
|
|
76
|
+
private views?;
|
|
17
77
|
private token?;
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Make an authorized request to the API, acquiring an unexpired acess token
|
|
30
|
-
* if necessary.
|
|
31
|
-
*/
|
|
32
|
-
request(url: URL | string, method?: string, body?: OpenIDClient.FetchBody, headers?: Headers, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<Response>;
|
|
33
|
-
/**
|
|
34
|
-
* Make an authorized request to the API, acquiring an unexpired acess token
|
|
35
|
-
* if necessary.
|
|
36
|
-
*
|
|
37
|
-
* This converts the Fetch API request into an OpenID request. To directly
|
|
38
|
-
* make this request, use {@link request()}
|
|
78
|
+
private tokenLock;
|
|
79
|
+
private search?;
|
|
80
|
+
private headers?;
|
|
81
|
+
private body?;
|
|
82
|
+
private storage?;
|
|
83
|
+
constructor({ credentials, views, search, headers, body, storage }: ClientOptions);
|
|
84
|
+
get redirect_uri(): Req.URL.ish;
|
|
85
|
+
/**
|
|
86
|
+
* @throws IndeterminateConfiguration if provided credentials combined with
|
|
87
|
+
* OpenID discovery fail to generate a complete configuration
|
|
39
88
|
*/
|
|
40
|
-
|
|
89
|
+
getConfiguration(): Promise<OpenIDClient.Configuration>;
|
|
90
|
+
protected getParameters(session: Session): Promise<URLSearchParams>;
|
|
91
|
+
getAuthorizationUrl(session: Session): Promise<URL>;
|
|
92
|
+
createSession({ views, ...options }: Omit<SessionOptions, 'client'>): Session;
|
|
93
|
+
authorize(options?: Omit<SessionOptions, 'client'>): Promise<Token.Response>;
|
|
94
|
+
handleAuthorizationCodeRedirect(req: Request, session: Session): Promise<void>;
|
|
95
|
+
protected refreshTokenGrant({ refresh_token, inject: request }?: RefreshOptions): Promise<Token.Response | undefined>;
|
|
41
96
|
/**
|
|
42
|
-
*
|
|
43
|
-
* if necessary and parse the JSON respomse.
|
|
97
|
+
* Get an unexpired access token
|
|
44
98
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
99
|
+
* Depending on provided and/or stored access token and refresh token values,
|
|
100
|
+
* this may require interactive authorization
|
|
47
101
|
*/
|
|
48
|
-
|
|
102
|
+
getToken({ token, inject: request }?: GetTokenOptions): Promise<Token.Response>;
|
|
103
|
+
/** @throws MissingAccessToken If response does not include `access_token` */
|
|
104
|
+
protected save(token: Token.Response): Promise<Token.Response>;
|
|
49
105
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
106
|
+
* @param url If an `issuer` has been defined, `url` accepts paths relative to
|
|
107
|
+
* the `issuer` URL as well as absolute URLs
|
|
108
|
+
* @param method Optional, defaults to `GET` unless otherwise specified
|
|
109
|
+
* @param body Optional
|
|
110
|
+
* @param headers Optional
|
|
111
|
+
* @param dPoPOptions Optional
|
|
112
|
+
*/
|
|
113
|
+
request(url: Req.URL.ish, method?: string, body?: OpenIDClient.FetchBody, headers?: Req.Headers.ish, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<Response>;
|
|
114
|
+
private toJSON;
|
|
115
|
+
/**
|
|
116
|
+
* Returns the result of {@link request} as a parsed JSON object, optionally
|
|
117
|
+
* typed as `T`
|
|
55
118
|
*/
|
|
56
|
-
|
|
119
|
+
requestJSON<T extends OpenIDClient.JsonValue = OpenIDClient.JsonValue>(url: Req.URL.ish, method?: string, body?: OpenIDClient.FetchBody, headers?: Req.Headers.ish, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<T>;
|
|
120
|
+
fetch(input: Req.URL.ish, init?: RequestInit, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<Response>;
|
|
121
|
+
fetchJSON<T extends OpenIDClient.JsonValue = OpenIDClient.JsonValue>(input: Req.URL.ish, init?: RequestInit, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<T>;
|
|
57
122
|
}
|
|
123
|
+
export {};
|
package/dist/Client.js
CHANGED
|
@@ -1,140 +1,203 @@
|
|
|
1
|
-
import * as Configuration from '@battis/oauth2-configure';
|
|
2
1
|
import { Mutex } from 'async-mutex';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
3
|
import * as OpenIDClient from 'openid-client';
|
|
4
|
-
import
|
|
5
|
-
import * as
|
|
6
|
-
import {
|
|
7
|
-
/**
|
|
8
|
-
|
|
4
|
+
import * as Errors from './Errors/index.js';
|
|
5
|
+
import * as Req from './Request/index.js';
|
|
6
|
+
import { Session } from './Session.js';
|
|
7
|
+
/**
|
|
8
|
+
* A generic `redirect_uri` to use if the server does not require pre-registered
|
|
9
|
+
* `redirect_uri` values
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_REDIRECT_URI = 'http://localhost:3000/oauth2-cli/redirect';
|
|
12
|
+
/**
|
|
13
|
+
* Wrap {@link https://www.npmjs.com/package/openid-client openid-client} in a
|
|
14
|
+
* class instance specific to a particular OAuth/OpenID server credential-set,
|
|
15
|
+
* abstracting away most flows into {@link getToken}
|
|
16
|
+
*
|
|
17
|
+
* Emits {@link Client.TokenEvent} whenever a new access token is received
|
|
18
|
+
*/
|
|
19
|
+
export class Client extends EventEmitter {
|
|
20
|
+
static TokenEvent = 'token';
|
|
9
21
|
credentials;
|
|
10
|
-
tokenMutex = new Mutex();
|
|
11
22
|
config;
|
|
23
|
+
views;
|
|
12
24
|
token;
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
tokenLock = new Mutex();
|
|
26
|
+
search;
|
|
27
|
+
headers;
|
|
28
|
+
body;
|
|
29
|
+
storage;
|
|
30
|
+
constructor({ credentials, views, search, headers, body, storage }) {
|
|
31
|
+
super();
|
|
15
32
|
this.credentials = credentials;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.store = this.credentials.store;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
33
|
+
this.views = views;
|
|
34
|
+
this.search = search;
|
|
35
|
+
this.headers = headers;
|
|
36
|
+
this.body = body;
|
|
37
|
+
this.storage = storage;
|
|
24
38
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return await this.tokenMutex.runExclusive((async () => {
|
|
28
|
-
if (!this.token) {
|
|
29
|
-
this.token = await this.store?.load();
|
|
30
|
-
}
|
|
31
|
-
if (this.token?.hasExpired()) {
|
|
32
|
-
this.token = await this.refreshToken(this.token);
|
|
33
|
-
}
|
|
34
|
-
if (!this.token) {
|
|
35
|
-
this.token = await this.authorize();
|
|
36
|
-
}
|
|
37
|
-
return this.token;
|
|
38
|
-
}).bind(this));
|
|
39
|
-
}
|
|
40
|
-
/** Refresh Token Grant */
|
|
41
|
-
async refreshToken(token) {
|
|
42
|
-
if (token.refresh_token) {
|
|
43
|
-
const { headers, parameters } = this.credentials;
|
|
44
|
-
let freshTokens;
|
|
45
|
-
if ((freshTokens = Token.fromResponse(await OpenIDClient.refreshTokenGrant(await Configuration.acquire(this.credentials), token.refresh_token, parameters,
|
|
46
|
-
// @ts-expect-error 2322 undocumented arg pass-through to oauth4webapi
|
|
47
|
-
{ headers }), token.refresh_token))) {
|
|
48
|
-
if (this.store) {
|
|
49
|
-
await this.store.save(freshTokens);
|
|
50
|
-
}
|
|
51
|
-
return freshTokens;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return this.authorize();
|
|
39
|
+
get redirect_uri() {
|
|
40
|
+
return this.credentials.redirect_uri;
|
|
55
41
|
}
|
|
56
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* @throws IndeterminateConfiguration if provided credentials combined with
|
|
44
|
+
* OpenID discovery fail to generate a complete configuration
|
|
45
|
+
*/
|
|
57
46
|
async getConfiguration() {
|
|
47
|
+
if (!this.config && this.credentials.issuer) {
|
|
48
|
+
this.config = await OpenIDClient.discovery(Req.URL.from(this.credentials.issuer), this.credentials.client_id, { client_secret: this.credentials.client_secret });
|
|
49
|
+
}
|
|
50
|
+
if (!this.config && this.credentials?.authorization_endpoint) {
|
|
51
|
+
this.config = new OpenIDClient.Configuration({
|
|
52
|
+
issuer: `https://${Req.URL.from(this.credentials.authorization_endpoint).hostname}`,
|
|
53
|
+
authorization_endpoint: Req.URL.toString(this.credentials.authorization_endpoint),
|
|
54
|
+
token_endpoint: Req.URL.toString(this.credentials.token_endpoint ||
|
|
55
|
+
this.credentials.authorization_endpoint)
|
|
56
|
+
}, this.credentials.client_id, { client_secret: this.credentials.client_secret });
|
|
57
|
+
}
|
|
58
58
|
if (!this.config) {
|
|
59
|
-
|
|
59
|
+
throw new Errors.IndeterminateConfiguration();
|
|
60
60
|
}
|
|
61
61
|
return this.config;
|
|
62
62
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
await Localhost.redirectServer({
|
|
84
|
-
...this.credentials,
|
|
85
|
-
authorization_url: OpenIDClient.buildAuthorizationUrl(await this.getConfiguration(), parameters).href,
|
|
86
|
-
code_verifier,
|
|
87
|
-
state,
|
|
88
|
-
resolve: (async (response) => {
|
|
89
|
-
this.token = Token.fromResponse(response);
|
|
90
|
-
if (this.token && this.store) {
|
|
91
|
-
await this.store.save(this.token);
|
|
92
|
-
}
|
|
93
|
-
resolve(this.token);
|
|
94
|
-
}).bind(this),
|
|
95
|
-
reject
|
|
96
|
-
});
|
|
97
|
-
});
|
|
63
|
+
async getParameters(session) {
|
|
64
|
+
const params = Req.URLSearchParams.merge(this.search, session.inject?.search) ||
|
|
65
|
+
new URLSearchParams();
|
|
66
|
+
params.set('redirect_uri', Req.URL.toString(this.credentials.redirect_uri));
|
|
67
|
+
params.set('code_challenge', await OpenIDClient.calculatePKCECodeChallenge(session.code_verifier));
|
|
68
|
+
params.set('code_challenge_method', 'S256');
|
|
69
|
+
params.set('state', session.state);
|
|
70
|
+
if (this.credentials.scope) {
|
|
71
|
+
params.set('scope', Req.Scope.toString(this.credentials.scope));
|
|
72
|
+
}
|
|
73
|
+
return params;
|
|
74
|
+
}
|
|
75
|
+
async getAuthorizationUrl(session) {
|
|
76
|
+
return OpenIDClient.buildAuthorizationUrl(await this.getConfiguration(), await this.getParameters(session));
|
|
77
|
+
}
|
|
78
|
+
createSession({ views, ...options }) {
|
|
79
|
+
return new Session({
|
|
80
|
+
client: this,
|
|
81
|
+
views: views || this.views,
|
|
82
|
+
...options
|
|
98
83
|
});
|
|
99
84
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
85
|
+
async authorize(options = {}) {
|
|
86
|
+
const session = this.createSession(options);
|
|
87
|
+
const token = await this.save(await session.authorizationCodeGrant());
|
|
88
|
+
return token;
|
|
89
|
+
}
|
|
90
|
+
async handleAuthorizationCodeRedirect(req, session) {
|
|
91
|
+
try {
|
|
92
|
+
const response = await OpenIDClient.authorizationCodeGrant(await this.getConfiguration(), new URL(req.url, this.redirect_uri), {
|
|
93
|
+
pkceCodeVerifier: session.code_verifier,
|
|
94
|
+
expectedState: session.state
|
|
95
|
+
}, this.search ? Req.URLSearchParams.from(this.search) : undefined);
|
|
96
|
+
await session.resolve(response);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
await session.resolve(undefined, error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async refreshTokenGrant({ refresh_token = this.token?.refresh_token, inject: request } = {}) {
|
|
103
|
+
if (!refresh_token && !this.token && this.storage) {
|
|
104
|
+
refresh_token = await this.storage.load();
|
|
105
|
+
}
|
|
106
|
+
if (!refresh_token || refresh_token === '') {
|
|
107
|
+
return undefined;
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
+
const token = await OpenIDClient.refreshTokenGrant(await this.getConfiguration(), refresh_token, this.search ? Req.URLSearchParams.from(this.search) : undefined, {
|
|
110
|
+
// @ts-expect-error 2322 undocumented arg pass-through to oauth4webapi
|
|
111
|
+
headers: Req.Headers.merge(this.headers, request?.headers)
|
|
112
|
+
});
|
|
113
|
+
return await this.save(token);
|
|
109
114
|
}
|
|
110
115
|
/**
|
|
111
|
-
*
|
|
112
|
-
* if necessary.
|
|
116
|
+
* Get an unexpired access token
|
|
113
117
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
118
|
+
* Depending on provided and/or stored access token and refresh token values,
|
|
119
|
+
* this may require interactive authorization
|
|
116
120
|
*/
|
|
117
|
-
async
|
|
118
|
-
return this.
|
|
121
|
+
async getToken({ token, inject: request } = {}) {
|
|
122
|
+
return await this.tokenLock.runExclusive(async () => {
|
|
123
|
+
token = token || this.token;
|
|
124
|
+
if (!this.token?.expiresIn() && this.storage) {
|
|
125
|
+
this.token = await this.refreshTokenGrant({ inject: request });
|
|
126
|
+
}
|
|
127
|
+
if (!this.token) {
|
|
128
|
+
this.token = await this.authorize({ inject: request });
|
|
129
|
+
}
|
|
130
|
+
return this.token;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/** @throws MissingAccessToken If response does not include `access_token` */
|
|
134
|
+
async save(token) {
|
|
135
|
+
this.token = token;
|
|
136
|
+
if (!token.access_token) {
|
|
137
|
+
throw new Errors.MissingAccessToken();
|
|
138
|
+
}
|
|
139
|
+
if (this.storage && this.token.refresh_token) {
|
|
140
|
+
await this.storage.save(this.token.refresh_token);
|
|
141
|
+
}
|
|
142
|
+
this.emit(Client.TokenEvent, this.token);
|
|
143
|
+
return this.token;
|
|
119
144
|
}
|
|
120
145
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* @param
|
|
125
|
-
*
|
|
146
|
+
* @param url If an `issuer` has been defined, `url` accepts paths relative to
|
|
147
|
+
* the `issuer` URL as well as absolute URLs
|
|
148
|
+
* @param method Optional, defaults to `GET` unless otherwise specified
|
|
149
|
+
* @param body Optional
|
|
150
|
+
* @param headers Optional
|
|
151
|
+
* @param dPoPOptions Optional
|
|
126
152
|
*/
|
|
127
|
-
async
|
|
128
|
-
|
|
153
|
+
async request(url, method = 'GET', body, headers = {}, dPoPOptions) {
|
|
154
|
+
try {
|
|
155
|
+
url = Req.URL.from(url);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (this.credentials.issuer) {
|
|
159
|
+
url = new URL(url, this.credentials.issuer);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const request = async () => await OpenIDClient.fetchProtectedResource(await this.getConfiguration(), (await this.getToken()).access_token, Req.URL.from(Req.URLSearchParams.appendTo(url, this.search || {})), method, body, Req.Headers.merge(this.headers, headers), dPoPOptions);
|
|
166
|
+
try {
|
|
167
|
+
return await request();
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (typeof error === 'object' &&
|
|
171
|
+
error !== null &&
|
|
172
|
+
'status' in error &&
|
|
173
|
+
error.status === 401) {
|
|
174
|
+
await this.authorize();
|
|
175
|
+
return await request();
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async toJSON(response) {
|
|
183
|
+
if (response.ok) {
|
|
184
|
+
return (await response.json());
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
throw new Errors.BadResponse(response);
|
|
188
|
+
}
|
|
129
189
|
}
|
|
130
190
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* This converts the Fetch API request into an OpenID request. To directly
|
|
135
|
-
* make this request, use {@link requestJSON()}
|
|
191
|
+
* Returns the result of {@link request} as a parsed JSON object, optionally
|
|
192
|
+
* typed as `T`
|
|
136
193
|
*/
|
|
137
|
-
async
|
|
138
|
-
return
|
|
194
|
+
async requestJSON(url, method = 'GET', body, headers = {}, dPoPOptions) {
|
|
195
|
+
return await this.toJSON(await this.request(url, method, body, headers, dPoPOptions));
|
|
196
|
+
}
|
|
197
|
+
async fetch(input, init, dPoPOptions) {
|
|
198
|
+
return await this.request(input, init?.method, await Req.Body.from(init?.body), Req.Headers.from(init?.headers), dPoPOptions);
|
|
199
|
+
}
|
|
200
|
+
async fetchJSON(input, init, dPoPOptions) {
|
|
201
|
+
return await this.toJSON(await this.fetch(input, init, dPoPOptions));
|
|
139
202
|
}
|
|
140
203
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as Req from './Request/index.js';
|
|
2
|
+
export type OAuth2 = {
|
|
3
|
+
client_id: string;
|
|
4
|
+
client_secret: string;
|
|
5
|
+
redirect_uri: Req.URL.ish;
|
|
6
|
+
authorization_endpoint: Req.URL.ish;
|
|
7
|
+
token_endpoint: Req.URL.ish;
|
|
8
|
+
scope?: Req.Scope.ish;
|
|
9
|
+
};
|
|
10
|
+
export type OpenID = {
|
|
11
|
+
issuer: Req.URL.ish;
|
|
12
|
+
client_id: string;
|
|
13
|
+
client_secret: string;
|
|
14
|
+
redirect_uri: Req.URL.ish;
|
|
15
|
+
};
|
|
16
|
+
export type Combined = {
|
|
17
|
+
client_id: string;
|
|
18
|
+
client_secret: string;
|
|
19
|
+
redirect_uri: Req.URL.ish;
|
|
20
|
+
scope?: Req.Scope.ish;
|
|
21
|
+
} & ({
|
|
22
|
+
issuer?: Req.URL.ish;
|
|
23
|
+
authorization_endpoint: Req.URL.ish;
|
|
24
|
+
token_endpoint: Req.URL.ish;
|
|
25
|
+
} | {
|
|
26
|
+
issuer: Req.URL.ish;
|
|
27
|
+
authorization_endpoint?: Req.URL.ish;
|
|
28
|
+
token_endpoint?: Req.URL.ish;
|
|
29
|
+
});
|