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 CHANGED
@@ -2,12 +2,31 @@
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.8.9](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.8.8...oauth2-cli/0.8.9) (2026-02-19)
5
+ ## [1.0.0](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.8.9...oauth2-cli/1.0.0) (2026-02-20)
6
+
7
+ ### ⚠ BREAKING CHANGES
8
+
9
+ - rename WebServer -> Localhost, authorize.ejs -> launch.ejs
10
+ - simplify access to client name by making it (only) a read-only property
11
+ - correctly expand gcrtl redirect_uri when requesting access token
12
+
13
+ ### Features
6
14
 
15
+ - auto-launch authorization flow in browser ([140e58e](https://github.com/battis/oauth2-cli/commit/140e58ea4c7a9c8d9c53624929e944d2ced1bd2e))
16
+ - human-readable reason for authorizing access ([0cce8c2](https://github.com/battis/oauth2-cli/commit/0cce8c2e13213e92befc4f58c81e6d7f9637ea63))
17
+ - rename WebServer -> Localhost, authorize.ejs -> launch.ejs ([09723a8](https://github.com/battis/oauth2-cli/commit/09723a81640cb8ad0e767584a81b9b56f5a617b2))
18
+ - simplify access to client name by making it (only) a read-only property ([401c870](https://github.com/battis/oauth2-cli/commit/401c870071c0e9ffc65282e8c42270dff6e897a2))
19
+
20
+ ### Bug Fixes
21
+
22
+ - correctly expand gcrtl redirect_uri when requesting access token ([387f9d8](https://github.com/battis/oauth2-cli/commit/387f9d83b81052fd19d80f7e7ff984390a90c822)), closes [#22](https://github.com/battis/oauth2-cli/issues/22)
23
+ - re-authorize if refresh token grant fails ([4fb42bb](https://github.com/battis/oauth2-cli/commit/4fb42bb1f1fa65b253938c051b29799cdbd058bc))
24
+
25
+ ## [0.8.9](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.8.8...oauth2-cli/0.8.9) (2026-02-19)
7
26
 
8
27
  ### Bug Fixes
9
28
 
10
- * further refinement in debug logging ([568e563](https://github.com/battis/oauth2-cli/commit/568e563bc0a918cd9741951654db685488f10330))
29
+ - further refinement in debug logging ([568e563](https://github.com/battis/oauth2-cli/commit/568e563bc0a918cd9741951654db685488f10330))
11
30
 
12
31
  ## [0.8.8](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.8.7...oauth2-cli/0.8.8) (2026-02-19)
13
32
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # oauth2-cli
2
2
 
3
- Acquire API access tokens via OAuth 2.0 within CLI tools
3
+ Acquire API access tokens via OAuth 2.0 / OpenID Connect within CLI tools
4
4
 
5
5
  [![npm version](https://badge.fury.io/js/oauth2-cli.svg)](https://badge.fury.io/js/oauth2-cli)
6
6
  [![Module type: ESM](https://img.shields.io/badge/module%20type-esm-brightgreen)](https://nodejs.org/api/esm.html)
@@ -21,11 +21,15 @@ type ExpectedResponse = {
21
21
  };
22
22
 
23
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',
24
+ name: 'Example API',
25
+ reason: 'an example script',
26
+ credentials: {
27
+ client_id: 'm3C6dGQJPJrvgwN97mTP4pWVH9smZGrr',
28
+ client_secret: '2XUktyxU2KQmQAoVHxQXNaHZ4G7XqJdP',
29
+ redirect_uri: 'http://localhost:3000/example/redirect',
30
+ authorization_endpoint: 'https://example.com/oauth2/auth',
31
+ token_endpoint: 'https://example.com/oauth2/token'
32
+ },
29
33
  storage: new FileStorage('/path/to/token/file.json');
30
34
  });
31
35
  console.log(
@@ -37,17 +41,29 @@ Broadly speaking, having provided the configuration, the client is immediately r
37
41
 
38
42
  ### Instantiate a `Client`
39
43
 
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)).
44
+ #### `credentials`
41
45
 
42
- In both cases, the token can be persisted by passing an implementation of [`Token.Storage`](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)._
46
+ A `Client` requires some minimal information in order to interact with an OAuth 2.0 authorized API. The OAuth 2.0 base `credentials` 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)).
43
47
 
44
- #### `redirect_uri` to Localhost
48
+ #### `name` and `reason`
49
+
50
+ It is strongly recommended that you provide a human-readable `name` for the client that will be used in user messages explaining _what_ is being accessed (e.g. the name of the API or service) and a human-readable `reason` for the user to provide this access (e.g. the name of your app or script). Messages are structured in the manner:
51
+
52
+ > ...to authorize access to `name` for `reason`, do this...
53
+
54
+ #### `storage`
55
+
56
+ The `refresh_token` can be persisted by passing an implementation of [`Token.Storage`](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)._
57
+
58
+ ## Registering localhost redirect URLs
59
+
60
+ ### `redirect_uri` to Localhost
45
61
 
46
62
  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
63
 
48
64
  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
65
 
50
- #### `http` protocol
66
+ ### `http` protocol
51
67
 
52
68
  If you would prefer an `https` connection to localhost, you have to roll your own SSL certificate.
53
69
 
@@ -75,7 +91,11 @@ class Client {
75
91
 
76
92
  [`requestish.URL.ish`](https://www.npmjs.com/package/requestish) are more forgiving types accepting not just those specific types, but reasonable facsimiles of them.
77
93
 
78
- ### `requestJSON<T>()`
94
+ #### `base_url` and `issuer` for relative paths
95
+
96
+ If you would prefer to make requests to relative paths, rather than absolute paths, either configure a `base_url` or include an `issuer` in the `credentials` when instantiating the client. A `base_url` will preempt an `issuer`, if both are defined (handy for when the `issuer` is a different subdomain than the API endpoints).
97
+
98
+ ### `requestJSON<J>()`
79
99
 
80
100
  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
101
 
@@ -84,7 +104,7 @@ class Client {
84
104
  // ...
85
105
 
86
106
  public async requestJSON<
87
- T extends OpenIDClient.JsonValue = OpenIDClient.JsonValue
107
+ J extends OpenIDClient.JsonValue = OpenIDClient.JsonValue
88
108
  >(
89
109
  url: requestish.URL.ish,
90
110
  method = 'GET',
@@ -97,6 +117,10 @@ class Client {
97
117
  }
98
118
  ```
99
119
 
120
+ ### `fetch()` and `fetchJSON<J>()`
121
+
122
+ Aliases for `request()` and `requestJSON<J>()` that use [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)-style arguments.
123
+
100
124
  ## Examples
101
125
 
102
126
  [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,122 +1,146 @@
1
- import { PathString } from '@battis/descriptive-types';
2
1
  import { JSONValue } from '@battis/typescript-tricks';
3
2
  import { Request } from 'express';
4
3
  import { EventEmitter } from 'node:events';
5
4
  import * as OpenIDClient from 'openid-client';
6
- import * as requestish from 'requestish';
5
+ import { Headers, URL } from 'requestish';
7
6
  import { Credentials } from './Credentials.js';
8
7
  import { Injection } from './Injection.js';
9
- import { Session, SessionOptions } from './Session.js';
8
+ import * as Localhost from './Localhost/index.js';
9
+ import * as Options from './Options.js';
10
10
  import * as Token from './Token/index.js';
11
11
  /**
12
- * A generic `redirect_uri` to use if the server does not require pre-registered
13
- * `redirect_uri` values
12
+ * Wrap {@link https://www.npmjs.com/package/openid-client openid-client} in a
13
+ * class instance specific to a particular OAuth/OpenID server credential-set,
14
+ * abstracting away most flows into {@link getToken}
15
+ *
16
+ * Emits {@link Client.TokenEvent} whenever a new access token is received
14
17
  */
15
- export declare const DEFAULT_REDIRECT_URI = "http://localhost:3000/oauth2-cli/redirect";
16
- export type ClientOptions<C extends Credentials = Credentials> = {
18
+ export declare class Client<C extends Credentials = Credentials> extends EventEmitter {
19
+ static readonly TokenEvent = "token";
20
+ private _name?;
17
21
  /** Human-readable name for client in messages */
18
- name?: string;
22
+ get name(): string;
23
+ /** Human-readable reason for authorization in messages */
24
+ private reason?;
19
25
  /** Credentials for server access */
20
26
  credentials: C;
21
- /** Optional request components to inject */
22
- inject?: {
23
- search?: requestish.URLSearchParams.ish;
24
- headers?: requestish.Headers.ish;
25
- body?: requestish.Body.ish;
26
- };
27
27
  /** Base URL for all non-absolute requests */
28
- base_url?: requestish.URL.ish;
28
+ base_url?: URL.ish;
29
29
  /**
30
- * Optional absolute path to EJS view templates directory, see
31
- * [WebServer.setViews()](./Webserver.ts)
30
+ * `openid-client` configuration metadata (either dervied from
31
+ * {@link credentials}) or requested from the well-known OpenID configuration
32
+ * endpoint of the `issuer`
32
33
  */
33
- views?: PathString;
34
+ protected config?: OpenIDClient.Configuration;
35
+ /** Optional request components to inject */
36
+ protected inject?: Injection;
37
+ /** Optional configuration options for web server listening for redirect */
38
+ private localhostOptions?;
34
39
  /** Optional {@link TokenStorage} implementation to manage tokens */
35
- storage?: Token.Storage;
36
- };
37
- type RefreshOptions = {
40
+ private storage?;
41
+ /** Current response to an access token grant request, if available */
42
+ private token?;
43
+ /** Mutex preventing multiple simultaneous access token grant requests */
44
+ private tokenLock;
45
+ /** @see {@link Options.Client} */
46
+ constructor({ name, reason, credentials, base_url, inject, storage, localhost }: Options.Client<C>);
47
+ /**
48
+ * Build a client configuration either via `issuer` discovery or from provided
49
+ * `credentials`
50
+ */
51
+ getConfiguration(): Promise<OpenIDClient.Configuration>;
38
52
  /**
39
- * Optional refresh token
53
+ * Build a URL to redirect the user-agent to, in order to request
54
+ * authorization at the Authorization Server
40
55
  *
41
- * If using {@link TokenStorage}, the refresh token should be stored with the
42
- * access token and does not need to be separately managed and stored
56
+ * @param session Contains the current `state` and `code_verifier` for the
57
+ * Authorization Code flow session
43
58
  */
44
- refresh_token?: string;
45
- /** Additional request injection for refresh grant flow */
46
- inject?: Injection;
47
- };
48
- type GetTokenOptions = {
59
+ buildAuthorizationUrl(session: Localhost.Server): Promise<URL>;
60
+ /** Does the client hold or have access to an unexpired API access token? */
61
+ isAuthorized(): Promise<boolean>;
62
+ /** Start interactive authorization for API access with the user */
63
+ authorize(): Promise<Token.Response>;
49
64
  /**
50
- * Optional access token
65
+ * Start interactive authorization for API access with the user _without_
66
+ * checking for tokenLock mutex
51
67
  *
52
- * If using {@link TokenStorage}, the access token does not need to be
53
- * separately managed and stored
68
+ * Should be called _only_ from within a `tokenLock.runExclusive()` callback
54
69
  */
55
- token?: Token.Response;
70
+ private _authorize;
56
71
  /**
57
- * Additional request injection for authorization code grant and/or refresh
58
- * grant flows
72
+ * Validate the authorization response and then executes the !"Authorization
73
+ * Code Grant" at the Authorization Server's token endpoint to obtain an
74
+ * access token. ID Token and Refresh Token are also optionally issued by the
75
+ * server.
76
+ *
77
+ * @param request Authorization Server's request to the Localhost redirect
78
+ * server
79
+ * @param session Contains the current `state` and `code_verifier` for the
80
+ * Authorization Code flow session
59
81
  */
60
- inject?: Injection;
61
- };
62
- /**
63
- * Wrap {@link https://www.npmjs.com/package/openid-client openid-client} in a
64
- * class instance specific to a particular OAuth/OpenID server credential-set,
65
- * abstracting away most flows into {@link getToken}
66
- *
67
- * Emits {@link Client.TokenEvent} whenever a new access token is received
68
- */
69
- export declare class Client<C extends Credentials = Credentials> extends EventEmitter {
70
- static readonly TokenEvent = "token";
71
- readonly name?: string;
72
- protected credentials: C;
73
- protected base_url?: requestish.URL.ish;
74
- protected config?: OpenIDClient.Configuration;
75
- protected inject?: Injection;
76
- protected views?: PathString;
77
- private token?;
78
- private tokenLock;
79
- private storage?;
80
- constructor({ name, credentials, base_url, views, inject, storage }: ClientOptions<C>);
81
- clientName(): string;
82
- get redirect_uri(): requestish.URL.ish;
82
+ handleAuthorizationCodeRedirect(request: Request, session: Localhost.Server): Promise<OpenIDClient.TokenEndpointResponse & OpenIDClient.TokenEndpointResponseHelpers>;
83
83
  /**
84
- * @throws IndeterminateConfiguration if provided credentials combined with
85
- * OpenID discovery fail to generate a complete configuration
84
+ * Perform an OAuth 2.0 Refresh Token Grant at the Authorization Server's
85
+ * token endpoint, allowing the client to obtain a new access token using a
86
+ * valid `refresh_token`.
87
+ *
88
+ * @see {@link Options.Refresh}
86
89
  */
87
- getConfiguration(): Promise<OpenIDClient.Configuration>;
88
- protected getParameters(session: Session): Promise<URLSearchParams>;
89
- getAuthorizationUrl(session: Session): Promise<URL>;
90
- createSession({ views, ...options }: Omit<SessionOptions, 'client'>): Session;
91
- isAuthorized(): Promise<boolean>;
92
- authorize(options?: Omit<SessionOptions, 'client'>): Promise<Token.Response>;
93
- handleAuthorizationCodeRedirect(req: Request, session: Session): Promise<void>;
94
- protected refreshTokenGrant({ refresh_token, inject: request }?: RefreshOptions): Promise<Token.Response | undefined>;
90
+ protected refreshTokenGrant({ refresh_token, inject }?: Options.Refresh): Promise<Token.Response | undefined>;
95
91
  /**
96
92
  * Get an unexpired access token
97
93
  *
98
94
  * Depending on provided and/or stored access token and refresh token values,
99
95
  * this may require interactive authorization
96
+ *
97
+ * @see {@link Options.GetToken}
100
98
  */
101
- getToken({ token, inject: request }?: GetTokenOptions): Promise<Token.Response>;
102
- /** @throws MissingAccessToken If response does not include `access_token` */
103
- protected save(token: Token.Response): Promise<Token.Response>;
99
+ getToken({ token, inject: request }?: Options.GetToken): Promise<Token.Response>;
104
100
  /**
105
- * @param url If an `issuer` has been defined, `url` accepts paths relative to
106
- * the `issuer` URL as well as absolute URLs
101
+ * Persist `refresh_token` if Token.Storage is configured and `refresh_token`
102
+ * provided
103
+ *
104
+ * @throws If `response` does not include `access_token` property
105
+ */
106
+ protected save(response: Token.Response): Promise<Token.Response>;
107
+ /**
108
+ * Request a protected resource using the client's access token.
109
+ *
110
+ * This ensures that the access token is unexpired, and interactively requests
111
+ * user authorization if it has not yet been provided.
112
+ *
113
+ * @param url If an `base_url` or `issuer` has been defined, `url` accepts
114
+ * paths relative to the `issuer` URL as well as absolute URLs
107
115
  * @param method Optional, defaults to `GET` unless otherwise specified
108
116
  * @param body Optional
109
117
  * @param headers Optional
110
- * @param dPoPOptions Optional
118
+ * @param dPoPOptions Optional, see {@link OpenIDClient.DPoPOptions}
111
119
  */
112
- request(url: requestish.URL.ish, method?: string, body?: OpenIDClient.FetchBody, headers?: requestish.Headers.ish, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<Response>;
120
+ request(url: URL.ish, method?: string, body?: OpenIDClient.FetchBody, headers?: Headers.ish, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<Response>;
121
+ /** Parse a fetch response as JSON, typing it as J */
113
122
  private toJSON;
114
123
  /**
115
124
  * Returns the result of {@link request} as a parsed JSON object, optionally
116
125
  * typed as `J`
117
126
  */
118
- requestJSON<J extends JSONValue = JSONValue>(url: requestish.URL.ish, method?: string, body?: OpenIDClient.FetchBody, headers?: requestish.Headers.ish, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<J>;
119
- fetch(input: requestish.URL.ish, init?: RequestInit, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<Response>;
120
- fetchJSON<J extends JSONValue = JSONValue>(input: requestish.URL.ish, init?: RequestInit, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<J>;
127
+ requestJSON<J extends JSONValue = JSONValue>(url: URL.ish, method?: string, body?: OpenIDClient.FetchBody, headers?: Headers.ish, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<J>;
128
+ /**
129
+ * Request a protected resource using the client's access token.
130
+ *
131
+ * This ensures that the access token is unexpired, and interactively requests
132
+ * user authorization if it has not yet been provided.
133
+ *
134
+ * @param input If a `base_url` or `issuer` has been defined, `url` accepts
135
+ * paths relative to the `issuer` URL as well as absolute URLs
136
+ * @param init Optional
137
+ * @param dPoPOptions Optional, see {@link OpenIDClient.DPoPOptions}
138
+ * @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
139
+ */
140
+ fetch(input: URL.ish, init?: RequestInit, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<Response>;
141
+ /**
142
+ * Returns the result of {@link fetch} as a parsed JSON object, optionally
143
+ * typed as `J`
144
+ */
145
+ fetchJSON<J extends JSONValue = JSONValue>(input: URL.ish, init?: RequestInit, dPoPOptions?: OpenIDClient.DPoPOptions): Promise<J>;
121
146
  }
122
- export {};