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.
Files changed (52) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +87 -1
  3. package/dist/Client.d.ts +111 -45
  4. package/dist/Client.js +175 -112
  5. package/dist/Credentials.d.ts +29 -0
  6. package/dist/Errors/BadResponse.d.ts +3 -0
  7. package/dist/Errors/BadResponse.js +7 -0
  8. package/dist/Errors/IndeterminateConfiguration.d.ts +3 -0
  9. package/dist/Errors/IndeterminateConfiguration.js +5 -0
  10. package/dist/Errors/MissingAccessToken.d.ts +3 -0
  11. package/dist/Errors/MissingAccessToken.js +5 -0
  12. package/dist/Errors/PortCollision.d.ts +3 -0
  13. package/dist/Errors/PortCollision.js +5 -0
  14. package/dist/Errors/index.d.ts +4 -0
  15. package/dist/Errors/index.js +4 -0
  16. package/dist/Request/Body.d.ts +3 -0
  17. package/dist/Request/Body.js +19 -0
  18. package/dist/Request/Headers.d.ts +3 -0
  19. package/dist/Request/Headers.js +20 -0
  20. package/dist/Request/Injection.d.ts +23 -0
  21. package/dist/Request/Injection.js +1 -0
  22. package/dist/Request/Scope.d.ts +2 -0
  23. package/dist/Request/Scope.js +6 -0
  24. package/dist/Request/URL.d.ts +4 -0
  25. package/dist/Request/URL.js +12 -0
  26. package/dist/Request/URLSearchParams.d.ts +6 -0
  27. package/dist/Request/URLSearchParams.js +37 -0
  28. package/dist/Request/index.d.ts +6 -0
  29. package/dist/Request/index.js +6 -0
  30. package/dist/Session.d.ts +47 -0
  31. package/dist/Session.js +77 -0
  32. package/dist/{FileStorage.d.ts → Token/FileStorage.d.ts} +2 -3
  33. package/dist/{FileStorage.js → Token/FileStorage.js} +3 -4
  34. package/dist/Token/Response.d.ts +2 -0
  35. package/dist/Token/Response.js +1 -0
  36. package/dist/Token/TokenStorage.d.ts +4 -0
  37. package/dist/Token/TokenStorage.js +1 -0
  38. package/dist/Token/addHelpers.d.ts +8 -0
  39. package/dist/Token/addHelpers.js +44 -0
  40. package/dist/Token/index.d.ts +3 -0
  41. package/dist/Token/index.js +3 -0
  42. package/dist/WebServer.d.ts +64 -0
  43. package/dist/WebServer.js +116 -0
  44. package/dist/index.d.ts +5 -3
  45. package/dist/index.js +5 -3
  46. package/package.json +10 -6
  47. package/dist/Localhost.d.ts +0 -14
  48. package/dist/Localhost.js +0 -88
  49. package/dist/Token.d.ts +0 -18
  50. package/dist/Token.js +0 -26
  51. package/dist/TokenStorage.d.ts +0 -5
  52. /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
- See [examples/get-token](../../examples/get-token).
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 * as Configuration from '@battis/oauth2-configure';
2
- import { JSONValue } from '@battis/typescript-tricks';
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 { Token } from './Token.js';
5
- import { TokenStorage } from './TokenStorage.js';
6
- export type Credentials = Configuration.Options & {
7
- scope?: string;
8
- headers?: Record<string, string>;
9
- parameters?: Record<string, string>;
10
- store?: TokenStorage | string;
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
- /** Wrap an OpenID configuration in an object-oriented API client. */
13
- export declare class Client {
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 store?;
19
- constructor(credentials: Credentials);
20
- /** Acquire a valid, unexpired API access token. */
21
- getToken(): Promise<Token | undefined>;
22
- /** Refresh Token Grant */
23
- protected refreshToken(token: Token): Promise<Token | undefined>;
24
- /** Acquire a valid OpenID configuration. */
25
- protected getConfiguration(): Promise<OpenIDClient.Configuration>;
26
- /** Authorization Code Grant */
27
- protected authorize(): Promise<Token | undefined>;
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
- fetch(endpoint: string | URL | Request, init?: RequestInit): Promise<Response>;
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
- * Make an authorized request to the API, acquiring an unexpired acess token
43
- * if necessary and parse the JSON respomse.
97
+ * Get an unexpired access token
44
98
  *
45
- * @param validator Optional validator function test that the JSON response is
46
- * the expected type.
99
+ * Depending on provided and/or stored access token and refresh token values,
100
+ * this may require interactive authorization
47
101
  */
48
- requestJSON<T extends JSONValue = JSONValue>(url: URL | string, method?: string, body?: OpenIDClient.FetchBody, headers?: Headers, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<T>;
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
- * Make an authorized request to the API, acquiring an unexpired acess token
51
- * if necessary and parse the JSON respomse.
52
- *
53
- * This converts the Fetch API request into an OpenID request. To directly
54
- * make this request, use {@link requestJSON()}
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
- fetchJSON<T extends JSONValue = JSONValue>(endpoint: string | URL | Request, init?: RequestInit): Promise<T>;
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 { FileStorage } from './FileStorage.js';
5
- import * as Localhost from './Localhost.js';
6
- import { Token } from './Token.js';
7
- /** Wrap an OpenID configuration in an object-oriented API client. */
8
- export class Client {
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
- store;
14
- constructor(credentials) {
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
- if (this.credentials.store) {
17
- if (typeof this.credentials.store === 'string') {
18
- this.store = new FileStorage(this.credentials.store);
19
- }
20
- else {
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
- /** Acquire a valid, unexpired API access token. */
26
- async getToken() {
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
- /** Acquire a valid OpenID configuration. */
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
- this.config = await Configuration.acquire(this.credentials);
59
+ throw new Errors.IndeterminateConfiguration();
60
60
  }
61
61
  return this.config;
62
62
  }
63
- /** Authorization Code Grant */
64
- async authorize() {
65
- const { scope, redirect_uri, parameters: additionalParameters } = this.credentials;
66
- return new Promise((resolve, reject) => {
67
- const code_verifier = OpenIDClient.randomPKCECodeVerifier();
68
- OpenIDClient.calculatePKCECodeChallenge(code_verifier).then(async (code_challenge) => {
69
- let state = undefined;
70
- const parameters = {
71
- ...additionalParameters,
72
- redirect_uri,
73
- code_challenge,
74
- code_challenge_method: 'S256' // TODO make code challenge method configurable?
75
- };
76
- if (scope) {
77
- parameters.scope = scope;
78
- }
79
- if (!(await this.getConfiguration()).serverMetadata().supportsPKCE()) {
80
- state = OpenIDClient.randomState();
81
- parameters.state = state;
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
- * Make an authorized request to the API, acquiring an unexpired acess token
102
- * if necessary.
103
- */
104
- async request(url, method = 'GET', body, headers, dPoPOptions) {
105
- for (const header in this.credentials.headers) {
106
- headers?.append(header, this.credentials.headers[header]);
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
- return await OpenIDClient.fetchProtectedResource(await this.getConfiguration(), (await this.getToken()).access_token, new URL(url), method, body, headers, dPoPOptions);
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
- * Make an authorized request to the API, acquiring an unexpired acess token
112
- * if necessary.
116
+ * Get an unexpired access token
113
117
  *
114
- * This converts the Fetch API request into an OpenID request. To directly
115
- * make this request, use {@link request()}
118
+ * Depending on provided and/or stored access token and refresh token values,
119
+ * this may require interactive authorization
116
120
  */
117
- async fetch(endpoint, init) {
118
- return this.request(endpoint.toString(), init?.method, init?.body?.toString(), new Headers(init?.headers));
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
- * Make an authorized request to the API, acquiring an unexpired acess token
122
- * if necessary and parse the JSON respomse.
123
- *
124
- * @param validator Optional validator function test that the JSON response is
125
- * the expected type.
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 requestJSON(url, method = 'GET', body, headers, dPoPOptions) {
128
- return (await (await this.request(url, method, body, headers, dPoPOptions)).json());
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
- * Make an authorized request to the API, acquiring an unexpired acess token
132
- * if necessary and parse the JSON respomse.
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 fetchJSON(endpoint, init) {
138
- return (await (await this.fetch(endpoint, init)).json());
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
+ });
@@ -0,0 +1,3 @@
1
+ export declare class BadResponse extends Error {
2
+ constructor(response: Response);
3
+ }
@@ -0,0 +1,7 @@
1
+ export class BadResponse extends Error {
2
+ constructor(response) {
3
+ super(`Response error ${response.status}: ${response.statusText}`, {
4
+ cause: response
5
+ });
6
+ }
7
+ }