oauth2-cli 0.1.2 → 0.1.4
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/.versionrc.json +5 -0
- package/CHANGELOG.md +23 -1
- package/README.md +3 -0
- package/dist/{TokenManager.d.ts → Client.d.ts} +7 -1
- package/dist/Client.js +97 -0
- package/dist/Localhost.d.ts +3 -2
- package/dist/Localhost.js +4 -4
- package/dist/Token.d.ts +3 -3
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/package.json +10 -8
- package/src/{TokenManager.ts → Client.ts} +63 -20
- package/src/Localhost.ts +8 -8
- package/src/Token.ts +4 -4
- package/src/index.ts +8 -1
- package/views/error.ejs +2 -2
- package/dist/TokenManager.js +0 -79
package/.versionrc.json
ADDED
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## [0.1.4](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.1.3...oauth2-cli/0.1.4) (2025-03-06)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **oauth2-cli:** cache token in memory ([68ac632](https://github.com/battis/oauth2-cli/commit/68ac6323031cbcaa0dd7b444dcd6da62b4f9a48d))
|
|
11
|
+
* **oauth2-cli:** Client.request() and Client.requestJSON() ([50c1198](https://github.com/battis/oauth2-cli/commit/50c11985c0ae8f135932d05bae2bf74ff1cd29df))
|
|
12
|
+
* **oauth2-cli:** deprecate TokenManager (replaced by Client) ([991ac42](https://github.com/battis/oauth2-cli/commit/991ac42eb2cc83b4b31e60856faf192233cd35f3))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **oauth2-cli:** improve error window title ([97a4c1c](https://github.com/battis/oauth2-cli/commit/97a4c1c9f98aaacf7ce63fb05a64cfee5f4dd0ce))
|
|
18
|
+
|
|
19
|
+
## [0.1.3](https://github.com/battis/oauth2-cli/compare/oauth2-cli/0.1.2...oauth2-cli/0.1.3) (2025-03-06)
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- efd09f9: docs: fix broken package paths
|
|
2
24
|
|
|
3
25
|
## 0.1.2
|
|
4
26
|
|
package/README.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Acquire API access tokens via OAuth 2.0 within CLI tools
|
|
4
4
|
|
|
5
|
+
[](https://badge.fury.io/js/oauth2-cli)
|
|
6
|
+
[](https://nodejs.org/api/esm.html)
|
|
7
|
+
|
|
5
8
|
## Install
|
|
6
9
|
|
|
7
10
|
```sh
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as Configuration from '@battis/oauth2-configure';
|
|
2
|
+
import * as OpenIDClient from 'openid-client';
|
|
2
3
|
import { Token } from './Token.js';
|
|
3
4
|
import { TokenStorage } from './TokenStorage.js';
|
|
4
5
|
export type Options = Configuration.Options & {
|
|
@@ -7,12 +8,17 @@ export type Options = Configuration.Options & {
|
|
|
7
8
|
parameters?: Record<string, string>;
|
|
8
9
|
store?: TokenStorage | string;
|
|
9
10
|
};
|
|
10
|
-
export declare class
|
|
11
|
+
export declare class Client {
|
|
11
12
|
private options;
|
|
12
13
|
private tokenMutex;
|
|
14
|
+
private config?;
|
|
15
|
+
private token?;
|
|
13
16
|
private store?;
|
|
14
17
|
constructor(options: Options);
|
|
15
18
|
getToken(): Promise<Token | undefined>;
|
|
16
19
|
private refreshToken;
|
|
20
|
+
private getConfiguration;
|
|
17
21
|
private authorize;
|
|
22
|
+
request(url: URL | string, method?: string, body?: OpenIDClient.FetchBody, headers?: Record<string, string>, options?: OpenIDClient.DPoPOptions): Promise<Response>;
|
|
23
|
+
requestJSON(url: URL | string, method?: string, body?: OpenIDClient.FetchBody, headers?: Record<string, string>, options?: OpenIDClient.DPoPOptions): Promise<unknown>;
|
|
18
24
|
}
|
package/dist/Client.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as Configuration from '@battis/oauth2-configure';
|
|
2
|
+
import { Mutex } from 'async-mutex';
|
|
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
|
+
export class Client {
|
|
8
|
+
options;
|
|
9
|
+
tokenMutex = new Mutex();
|
|
10
|
+
config;
|
|
11
|
+
token;
|
|
12
|
+
store;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
if (this.options.store) {
|
|
16
|
+
if (typeof this.options.store === 'string') {
|
|
17
|
+
this.store = new FileStorage(this.options.store);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
this.store = this.options.store;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async getToken() {
|
|
25
|
+
return await this.tokenMutex.runExclusive((async () => {
|
|
26
|
+
if (!this.token) {
|
|
27
|
+
this.token = await this.store?.load();
|
|
28
|
+
}
|
|
29
|
+
if (this.token?.hasExpired()) {
|
|
30
|
+
this.token = await this.refreshToken(this.token);
|
|
31
|
+
}
|
|
32
|
+
if (!this.token) {
|
|
33
|
+
this.token = await this.authorize();
|
|
34
|
+
}
|
|
35
|
+
return this.token;
|
|
36
|
+
}).bind(this));
|
|
37
|
+
}
|
|
38
|
+
async refreshToken(token) {
|
|
39
|
+
if (token.refresh_token) {
|
|
40
|
+
const { headers, parameters } = this.options;
|
|
41
|
+
let freshTokens;
|
|
42
|
+
if ((freshTokens = Token.fromResponse(await OpenIDClient.refreshTokenGrant(await Configuration.acquire(this.options), token.refresh_token, parameters,
|
|
43
|
+
// @ts-expect-error 2322 undocumented arg pass-through to oauth4webapi
|
|
44
|
+
{ headers })))) {
|
|
45
|
+
return this.store?.save(freshTokens) || freshTokens;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return this.authorize();
|
|
49
|
+
}
|
|
50
|
+
async getConfiguration() {
|
|
51
|
+
if (!this.config) {
|
|
52
|
+
this.config = await Configuration.acquire(this.options);
|
|
53
|
+
}
|
|
54
|
+
return this.config;
|
|
55
|
+
}
|
|
56
|
+
async authorize() {
|
|
57
|
+
const { scope, redirect_uri, parameters: additionalParameters } = this.options;
|
|
58
|
+
return new Promise(async (resolve, reject) => {
|
|
59
|
+
const code_verifier = OpenIDClient.randomPKCECodeVerifier();
|
|
60
|
+
const code_challenge = await OpenIDClient.calculatePKCECodeChallenge(code_verifier);
|
|
61
|
+
let state = undefined;
|
|
62
|
+
const parameters = {
|
|
63
|
+
...additionalParameters,
|
|
64
|
+
redirect_uri,
|
|
65
|
+
code_challenge,
|
|
66
|
+
code_challenge_method: 'S256' // TODO make code challenge method configurable?
|
|
67
|
+
};
|
|
68
|
+
if (scope) {
|
|
69
|
+
parameters.scope = scope;
|
|
70
|
+
}
|
|
71
|
+
if (!(await this.getConfiguration()).serverMetadata().supportsPKCE()) {
|
|
72
|
+
state = OpenIDClient.randomState();
|
|
73
|
+
parameters.state = state;
|
|
74
|
+
}
|
|
75
|
+
await Localhost.redirectServer({
|
|
76
|
+
...this.options,
|
|
77
|
+
authorization_url: OpenIDClient.buildAuthorizationUrl(await this.getConfiguration(), parameters).href,
|
|
78
|
+
code_verifier,
|
|
79
|
+
state,
|
|
80
|
+
resolve: (async (response) => {
|
|
81
|
+
this.token = Token.fromResponse(response);
|
|
82
|
+
if (this.token && this.store) {
|
|
83
|
+
await this.store.save(this.token);
|
|
84
|
+
}
|
|
85
|
+
resolve(this.token);
|
|
86
|
+
}).bind(this),
|
|
87
|
+
reject
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async request(url, method = 'GET', body, headers, options) {
|
|
92
|
+
return await OpenIDClient.fetchProtectedResource(await this.getConfiguration(), (await this.getToken()).access_token, new URL(url), method, body, new Headers({ ...this.options.headers, ...headers }), options);
|
|
93
|
+
}
|
|
94
|
+
async requestJSON(url, method = 'GET', body, headers, options) {
|
|
95
|
+
return await (await this.request(url, method, body, headers, options)).json();
|
|
96
|
+
}
|
|
97
|
+
}
|
package/dist/Localhost.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as Configuration from '@battis/oauth2-configure';
|
|
2
|
+
import * as OpenIDClient from 'openid-client';
|
|
2
3
|
type Options = Configuration.Options & {
|
|
3
4
|
authorization_url: string;
|
|
4
5
|
redirect_uri: string;
|
|
5
6
|
headers?: Record<string, string>;
|
|
6
7
|
code_verifier?: string;
|
|
7
8
|
state?: string;
|
|
8
|
-
resolve:
|
|
9
|
-
reject:
|
|
9
|
+
resolve: (tokens?: OpenIDClient.TokenEndpointResponse) => void;
|
|
10
|
+
reject: (error: unknown) => void;
|
|
10
11
|
views?: string;
|
|
11
12
|
};
|
|
12
13
|
export declare function redirectServer(options: Options): Promise<void>;
|
package/dist/Localhost.js
CHANGED
|
@@ -3,7 +3,7 @@ import express from 'express';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import open from 'open';
|
|
6
|
-
import * as
|
|
6
|
+
import * as OpenIDClient from 'openid-client';
|
|
7
7
|
export async function redirectServer(options) {
|
|
8
8
|
const { authorization_url, redirect_uri, code_verifier, state, headers, resolve, reject, views = '../views' } = options;
|
|
9
9
|
const redirectUrl = new URL(redirect_uri);
|
|
@@ -16,7 +16,7 @@ export async function redirectServer(options) {
|
|
|
16
16
|
let tokens = undefined;
|
|
17
17
|
let error = undefined;
|
|
18
18
|
app.get('/authorize', async (req, res) => {
|
|
19
|
-
|
|
19
|
+
const viewPath = path.resolve(import.meta.dirname, views, 'authorize');
|
|
20
20
|
if (ejs && fs.existsSync(viewPath)) {
|
|
21
21
|
res.send(await ejs.renderFile(viewPath));
|
|
22
22
|
}
|
|
@@ -27,11 +27,11 @@ export async function redirectServer(options) {
|
|
|
27
27
|
app.get(redirectUrl.pathname, async (req, res) => {
|
|
28
28
|
try {
|
|
29
29
|
const currentUrl = new URL(req.originalUrl, redirect_uri);
|
|
30
|
-
tokens = await
|
|
30
|
+
tokens = await OpenIDClient.authorizationCodeGrant(configuration, currentUrl, {
|
|
31
31
|
pkceCodeVerifier: code_verifier,
|
|
32
32
|
expectedState: state
|
|
33
33
|
},
|
|
34
|
-
// @ts-
|
|
34
|
+
// @ts-expect-error 2322 undocumented arg pass-through to oauth4webapi
|
|
35
35
|
{ headers });
|
|
36
36
|
}
|
|
37
37
|
catch (e) {
|
package/dist/Token.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
interface TokenResponse extends
|
|
1
|
+
import * as OpenIDClient from 'openid-client';
|
|
2
|
+
interface TokenResponse extends OpenIDClient.TokenEndpointResponse {
|
|
3
3
|
[key: string]: any;
|
|
4
4
|
}
|
|
5
5
|
export declare class Token implements TokenResponse {
|
|
@@ -12,7 +12,7 @@ export declare class Token implements TokenResponse {
|
|
|
12
12
|
readonly id_token?: string;
|
|
13
13
|
readonly expires_in?: number;
|
|
14
14
|
private constructor();
|
|
15
|
-
static fromResponse(response?:
|
|
15
|
+
static fromResponse(response?: OpenIDClient.TokenEndpointResponse): Token | undefined;
|
|
16
16
|
hasExpired(): boolean;
|
|
17
17
|
}
|
|
18
18
|
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { Client } from './Client.js';
|
|
2
|
+
export { Client } from './Client.js';
|
|
1
3
|
export { FileStorage } from './FileStorage.js';
|
|
2
4
|
export { Token } from './Token.js';
|
|
3
|
-
export { TokenManager } from './TokenManager.js';
|
|
4
5
|
export { TokenStorage } from './TokenStorage.js';
|
|
6
|
+
export {
|
|
7
|
+
/** @deprecated use Client */
|
|
8
|
+
Client as TokenManager };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { Client } from './Client.js';
|
|
2
|
+
export { Client } from './Client.js';
|
|
1
3
|
export { FileStorage } from './FileStorage.js';
|
|
2
4
|
export { Token } from './Token.js';
|
|
3
|
-
export {
|
|
5
|
+
export {
|
|
6
|
+
/** @deprecated use Client */
|
|
7
|
+
Client as TokenManager };
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oauth2-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Acquire API access tokens via OAuth 2.0 within CLI tools",
|
|
5
|
-
"homepage": "https://github.com/battis/
|
|
5
|
+
"homepage": "https://github.com/battis/oauth2-cli/tree/main/packages/oauth2-cli#readme",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/battis/
|
|
8
|
+
"url": "git+https://github.com/battis/oauth2-cli.git",
|
|
9
9
|
"directory": "packages/oauth2-cli"
|
|
10
10
|
},
|
|
11
11
|
"author": {
|
|
@@ -17,18 +17,19 @@
|
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"async-mutex": "^0.5.0",
|
|
20
|
-
"express": "^4.21.
|
|
20
|
+
"express": "^4.21.2",
|
|
21
21
|
"open": "^10.1.0",
|
|
22
|
-
"openid-client": "^6.
|
|
23
|
-
"@battis/oauth2-configure": "0.1.
|
|
22
|
+
"openid-client": "^6.3.3",
|
|
23
|
+
"@battis/oauth2-configure": "0.1.1"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@tsconfig/node20": "^20.1.4",
|
|
27
27
|
"@types/ejs": "^3.1.5",
|
|
28
28
|
"@types/express": "^5.0.0",
|
|
29
|
+
"commit-and-tag-version": "^12.5.0",
|
|
29
30
|
"del-cli": "^6.0.0",
|
|
30
31
|
"npm-run-all": "^4.1.5",
|
|
31
|
-
"typescript": "^5.
|
|
32
|
+
"typescript": "^5.8.2"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"ejs": "*"
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
"clean": "del ./dist",
|
|
47
48
|
"build": "run-s build:*",
|
|
48
49
|
"build:clean": "run-s clean",
|
|
49
|
-
"build:compile": "tsc"
|
|
50
|
+
"build:compile": "tsc",
|
|
51
|
+
"release": "commit-and-tag-version"
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Configuration from '@battis/oauth2-configure';
|
|
2
2
|
import { Mutex } from 'async-mutex';
|
|
3
|
-
import * as
|
|
3
|
+
import * as OpenIDClient from 'openid-client';
|
|
4
4
|
import { FileStorage } from './FileStorage.js';
|
|
5
5
|
import * as Localhost from './Localhost.js';
|
|
6
6
|
import { Token } from './Token.js';
|
|
@@ -13,8 +13,10 @@ export type Options = Configuration.Options & {
|
|
|
13
13
|
store?: TokenStorage | string;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
export class
|
|
16
|
+
export class Client {
|
|
17
17
|
private tokenMutex = new Mutex();
|
|
18
|
+
private config?: OpenIDClient.Configuration;
|
|
19
|
+
private token?: Token;
|
|
18
20
|
private store?: TokenStorage;
|
|
19
21
|
|
|
20
22
|
public constructor(private options: Options) {
|
|
@@ -30,11 +32,16 @@ export class TokenManager {
|
|
|
30
32
|
public async getToken() {
|
|
31
33
|
return await this.tokenMutex.runExclusive(
|
|
32
34
|
(async () => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
token = await this.refreshToken(token);
|
|
35
|
+
if (!this.token) {
|
|
36
|
+
this.token = await this.store?.load();
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
+
if (this.token?.hasExpired()) {
|
|
39
|
+
this.token = await this.refreshToken(this.token);
|
|
40
|
+
}
|
|
41
|
+
if (!this.token) {
|
|
42
|
+
this.token = await this.authorize();
|
|
43
|
+
}
|
|
44
|
+
return this.token;
|
|
38
45
|
}).bind(this)
|
|
39
46
|
);
|
|
40
47
|
}
|
|
@@ -45,11 +52,11 @@ export class TokenManager {
|
|
|
45
52
|
let freshTokens;
|
|
46
53
|
if (
|
|
47
54
|
(freshTokens = Token.fromResponse(
|
|
48
|
-
await
|
|
55
|
+
await OpenIDClient.refreshTokenGrant(
|
|
49
56
|
await Configuration.acquire(this.options),
|
|
50
57
|
token.refresh_token,
|
|
51
58
|
parameters,
|
|
52
|
-
// @ts-
|
|
59
|
+
// @ts-expect-error 2322 undocumented arg pass-through to oauth4webapi
|
|
53
60
|
{ headers }
|
|
54
61
|
)
|
|
55
62
|
))
|
|
@@ -60,6 +67,13 @@ export class TokenManager {
|
|
|
60
67
|
return this.authorize();
|
|
61
68
|
}
|
|
62
69
|
|
|
70
|
+
private async getConfiguration() {
|
|
71
|
+
if (!this.config) {
|
|
72
|
+
this.config = await Configuration.acquire(this.options);
|
|
73
|
+
}
|
|
74
|
+
return this.config;
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
private async authorize(): Promise<Token | undefined> {
|
|
64
78
|
const {
|
|
65
79
|
scope,
|
|
@@ -68,10 +82,9 @@ export class TokenManager {
|
|
|
68
82
|
} = this.options;
|
|
69
83
|
|
|
70
84
|
return new Promise(async (resolve, reject) => {
|
|
71
|
-
const
|
|
72
|
-
const code_verifier = client.randomPKCECodeVerifier();
|
|
85
|
+
const code_verifier = OpenIDClient.randomPKCECodeVerifier();
|
|
73
86
|
const code_challenge =
|
|
74
|
-
await
|
|
87
|
+
await OpenIDClient.calculatePKCECodeChallenge(code_verifier);
|
|
75
88
|
let state: string | undefined = undefined;
|
|
76
89
|
const parameters: Record<string, string> = {
|
|
77
90
|
...additionalParameters,
|
|
@@ -83,28 +96,58 @@ export class TokenManager {
|
|
|
83
96
|
if (scope) {
|
|
84
97
|
parameters.scope = scope;
|
|
85
98
|
}
|
|
86
|
-
if (!
|
|
87
|
-
state =
|
|
99
|
+
if (!(await this.getConfiguration()).serverMetadata().supportsPKCE()) {
|
|
100
|
+
state = OpenIDClient.randomState();
|
|
88
101
|
parameters.state = state;
|
|
89
102
|
}
|
|
90
103
|
|
|
91
104
|
await Localhost.redirectServer({
|
|
92
105
|
...this.options,
|
|
93
|
-
authorization_url:
|
|
94
|
-
|
|
106
|
+
authorization_url: OpenIDClient.buildAuthorizationUrl(
|
|
107
|
+
await this.getConfiguration(),
|
|
95
108
|
parameters
|
|
96
109
|
).href,
|
|
97
110
|
code_verifier,
|
|
98
111
|
state,
|
|
99
|
-
resolve: (async (response
|
|
100
|
-
|
|
101
|
-
if (token && this.store) {
|
|
102
|
-
await this.store.save(token);
|
|
112
|
+
resolve: (async (response?: OpenIDClient.TokenEndpointResponse) => {
|
|
113
|
+
this.token = Token.fromResponse(response);
|
|
114
|
+
if (this.token && this.store) {
|
|
115
|
+
await this.store.save(this.token);
|
|
103
116
|
}
|
|
104
|
-
resolve(token);
|
|
117
|
+
resolve(this.token);
|
|
105
118
|
}).bind(this),
|
|
106
119
|
reject
|
|
107
120
|
});
|
|
108
121
|
});
|
|
109
122
|
}
|
|
123
|
+
|
|
124
|
+
public async request(
|
|
125
|
+
url: URL | string,
|
|
126
|
+
method: string = 'GET',
|
|
127
|
+
body?: OpenIDClient.FetchBody,
|
|
128
|
+
headers?: Record<string, string>,
|
|
129
|
+
options?: OpenIDClient.DPoPOptions
|
|
130
|
+
) {
|
|
131
|
+
return await OpenIDClient.fetchProtectedResource(
|
|
132
|
+
await this.getConfiguration(),
|
|
133
|
+
(await this.getToken())!.access_token,
|
|
134
|
+
new URL(url),
|
|
135
|
+
method,
|
|
136
|
+
body,
|
|
137
|
+
new Headers({ ...this.options.headers, ...headers }),
|
|
138
|
+
options
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public async requestJSON(
|
|
143
|
+
url: URL | string,
|
|
144
|
+
method: string = 'GET',
|
|
145
|
+
body?: OpenIDClient.FetchBody,
|
|
146
|
+
headers?: Record<string, string>,
|
|
147
|
+
options?: OpenIDClient.DPoPOptions
|
|
148
|
+
) {
|
|
149
|
+
return await (
|
|
150
|
+
await this.request(url, method, body, headers, options)
|
|
151
|
+
).json();
|
|
152
|
+
}
|
|
110
153
|
}
|
package/src/Localhost.ts
CHANGED
|
@@ -3,7 +3,7 @@ import express from 'express';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import open from 'open';
|
|
6
|
-
import * as
|
|
6
|
+
import * as OpenIDClient from 'openid-client';
|
|
7
7
|
|
|
8
8
|
type Options = Configuration.Options & {
|
|
9
9
|
authorization_url: string;
|
|
@@ -11,8 +11,8 @@ type Options = Configuration.Options & {
|
|
|
11
11
|
headers?: Record<string, string>;
|
|
12
12
|
code_verifier?: string;
|
|
13
13
|
state?: string;
|
|
14
|
-
resolve:
|
|
15
|
-
reject:
|
|
14
|
+
resolve: (tokens?: OpenIDClient.TokenEndpointResponse) => void;
|
|
15
|
+
reject: (error: unknown) => void;
|
|
16
16
|
views?: string;
|
|
17
17
|
};
|
|
18
18
|
|
|
@@ -36,11 +36,11 @@ export async function redirectServer(options: Options) {
|
|
|
36
36
|
const server = app.listen(port);
|
|
37
37
|
const ejs = await import('ejs');
|
|
38
38
|
let view = 'complete.ejs';
|
|
39
|
-
let tokens:
|
|
40
|
-
let error:
|
|
39
|
+
let tokens: OpenIDClient.TokenEndpointResponse | undefined = undefined;
|
|
40
|
+
let error: unknown = undefined;
|
|
41
41
|
|
|
42
42
|
app.get('/authorize', async (req, res) => {
|
|
43
|
-
|
|
43
|
+
const viewPath = path.resolve(import.meta.dirname, views, 'authorize');
|
|
44
44
|
if (ejs && fs.existsSync(viewPath)) {
|
|
45
45
|
res.send(await ejs.renderFile(viewPath));
|
|
46
46
|
} else {
|
|
@@ -50,14 +50,14 @@ export async function redirectServer(options: Options) {
|
|
|
50
50
|
app.get(redirectUrl.pathname, async (req, res) => {
|
|
51
51
|
try {
|
|
52
52
|
const currentUrl = new URL(req.originalUrl, redirect_uri);
|
|
53
|
-
tokens = await
|
|
53
|
+
tokens = await OpenIDClient.authorizationCodeGrant(
|
|
54
54
|
configuration,
|
|
55
55
|
currentUrl,
|
|
56
56
|
{
|
|
57
57
|
pkceCodeVerifier: code_verifier,
|
|
58
58
|
expectedState: state
|
|
59
59
|
},
|
|
60
|
-
// @ts-
|
|
60
|
+
// @ts-expect-error 2322 undocumented arg pass-through to oauth4webapi
|
|
61
61
|
{ headers }
|
|
62
62
|
);
|
|
63
63
|
} catch (e) {
|
package/src/Token.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as OpenIDClient from 'openid-client';
|
|
2
2
|
|
|
3
|
-
interface TokenResponse extends
|
|
3
|
+
interface TokenResponse extends OpenIDClient.TokenEndpointResponse {
|
|
4
4
|
[key: string]: any;
|
|
5
5
|
}
|
|
6
6
|
|
|
@@ -14,14 +14,14 @@ export class Token implements TokenResponse {
|
|
|
14
14
|
public readonly id_token?: string;
|
|
15
15
|
public readonly expires_in?: number;
|
|
16
16
|
|
|
17
|
-
private constructor(response:
|
|
17
|
+
private constructor(response: OpenIDClient.TokenEndpointResponse) {
|
|
18
18
|
this.access_token = response.access_token;
|
|
19
19
|
this.token_type = response.token_type;
|
|
20
20
|
this.timestamp = response.timestamp;
|
|
21
21
|
Object.assign(this, response);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
public static fromResponse(response?:
|
|
24
|
+
public static fromResponse(response?: OpenIDClient.TokenEndpointResponse) {
|
|
25
25
|
if (response) {
|
|
26
26
|
return new Token({ timestamp: Date.now(), ...response });
|
|
27
27
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { Client } from './Client.js';
|
|
2
|
+
|
|
3
|
+
export { Client } from './Client.js';
|
|
1
4
|
export { FileStorage } from './FileStorage.js';
|
|
2
5
|
export { Token } from './Token.js';
|
|
3
|
-
export { TokenManager } from './TokenManager.js';
|
|
4
6
|
export { TokenStorage } from './TokenStorage.js';
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
/** @deprecated use Client */
|
|
10
|
+
Client as TokenManager
|
|
11
|
+
};
|
package/views/error.ejs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<title>Authorization
|
|
6
|
+
<title>Authorization Error</title>
|
|
7
7
|
<link
|
|
8
8
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
|
9
9
|
rel="stylesheet"
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<body>
|
|
31
31
|
<div class="wrapper">
|
|
32
32
|
<div class="center container">
|
|
33
|
-
<h1>Error
|
|
33
|
+
<h1>Authorization Error</h1>
|
|
34
34
|
<div class="alert alert-danger" role="alert">
|
|
35
35
|
<% if (typeof error === 'string') { %><%= error %><<% } else { %>
|
|
36
36
|
<pre><%= JSON.stringify(error,null,2) %></pre>
|
package/dist/TokenManager.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import * as Configuration from '@battis/oauth2-configure';
|
|
2
|
-
import { Mutex } from 'async-mutex';
|
|
3
|
-
import * as client from 'openid-client';
|
|
4
|
-
import { FileStorage } from './FileStorage.js';
|
|
5
|
-
import * as Localhost from './Localhost.js';
|
|
6
|
-
import { Token } from './Token.js';
|
|
7
|
-
export class TokenManager {
|
|
8
|
-
options;
|
|
9
|
-
tokenMutex = new Mutex();
|
|
10
|
-
store;
|
|
11
|
-
constructor(options) {
|
|
12
|
-
this.options = options;
|
|
13
|
-
if (this.options.store) {
|
|
14
|
-
if (typeof this.options.store === 'string') {
|
|
15
|
-
this.store = new FileStorage(this.options.store);
|
|
16
|
-
}
|
|
17
|
-
else {
|
|
18
|
-
this.store = this.options.store;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
async getToken() {
|
|
23
|
-
return await this.tokenMutex.runExclusive((async () => {
|
|
24
|
-
let token = await this.store?.load();
|
|
25
|
-
if (token?.hasExpired()) {
|
|
26
|
-
token = await this.refreshToken(token);
|
|
27
|
-
}
|
|
28
|
-
return token || (await this.authorize());
|
|
29
|
-
}).bind(this));
|
|
30
|
-
}
|
|
31
|
-
async refreshToken(token) {
|
|
32
|
-
if (token.refresh_token) {
|
|
33
|
-
const { headers, parameters } = this.options;
|
|
34
|
-
let freshTokens;
|
|
35
|
-
if ((freshTokens = Token.fromResponse(await client.refreshTokenGrant(await Configuration.acquire(this.options), token.refresh_token, parameters,
|
|
36
|
-
// @ts-ignore FIXME undocumented arg pass-through to oauth4webapi
|
|
37
|
-
{ headers })))) {
|
|
38
|
-
return this.store?.save(freshTokens) || freshTokens;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return this.authorize();
|
|
42
|
-
}
|
|
43
|
-
async authorize() {
|
|
44
|
-
const { scope, redirect_uri, parameters: additionalParameters } = this.options;
|
|
45
|
-
return new Promise(async (resolve, reject) => {
|
|
46
|
-
const configuration = await Configuration.acquire(this.options);
|
|
47
|
-
const code_verifier = client.randomPKCECodeVerifier();
|
|
48
|
-
const code_challenge = await client.calculatePKCECodeChallenge(code_verifier);
|
|
49
|
-
let state = undefined;
|
|
50
|
-
const parameters = {
|
|
51
|
-
...additionalParameters,
|
|
52
|
-
redirect_uri,
|
|
53
|
-
code_challenge,
|
|
54
|
-
code_challenge_method: 'S256' // TODO make code challenge method configurable?
|
|
55
|
-
};
|
|
56
|
-
if (scope) {
|
|
57
|
-
parameters.scope = scope;
|
|
58
|
-
}
|
|
59
|
-
if (!configuration.serverMetadata().supportsPKCE()) {
|
|
60
|
-
state = client.randomState();
|
|
61
|
-
parameters.state = state;
|
|
62
|
-
}
|
|
63
|
-
await Localhost.redirectServer({
|
|
64
|
-
...this.options,
|
|
65
|
-
authorization_url: client.buildAuthorizationUrl(configuration, parameters).href,
|
|
66
|
-
code_verifier,
|
|
67
|
-
state,
|
|
68
|
-
resolve: (async (response) => {
|
|
69
|
-
const token = Token.fromResponse(response);
|
|
70
|
-
if (token && this.store) {
|
|
71
|
-
await this.store.save(token);
|
|
72
|
-
}
|
|
73
|
-
resolve(token);
|
|
74
|
-
}).bind(this),
|
|
75
|
-
reject
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
}
|