oauth2-cli 0.5.0 → 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 +29 -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 +12 -8
- 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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isRecord } from '@battis/typescript-tricks';
|
|
2
|
+
function isString(obj) {
|
|
3
|
+
return typeof obj === 'string';
|
|
4
|
+
}
|
|
5
|
+
export async function from(body) {
|
|
6
|
+
if (body === undefined ||
|
|
7
|
+
body === null ||
|
|
8
|
+
typeof body === 'string' ||
|
|
9
|
+
body instanceof ArrayBuffer ||
|
|
10
|
+
body instanceof ReadableStream ||
|
|
11
|
+
body instanceof Uint8Array ||
|
|
12
|
+
body instanceof URLSearchParams) {
|
|
13
|
+
return body;
|
|
14
|
+
}
|
|
15
|
+
else if (isRecord(body, isString, isString)) {
|
|
16
|
+
return new URLSearchParams(body);
|
|
17
|
+
}
|
|
18
|
+
return new Response(body).arrayBuffer();
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function from(headers) {
|
|
2
|
+
if (headers instanceof Headers) {
|
|
3
|
+
return headers;
|
|
4
|
+
}
|
|
5
|
+
return new Headers(headers);
|
|
6
|
+
}
|
|
7
|
+
export function merge(a, b) {
|
|
8
|
+
if (a && !b) {
|
|
9
|
+
return from(a);
|
|
10
|
+
}
|
|
11
|
+
else if (!a && b) {
|
|
12
|
+
return from(b);
|
|
13
|
+
}
|
|
14
|
+
else if (a && b) {
|
|
15
|
+
const headers = from(a);
|
|
16
|
+
from(b).forEach((value, key) => headers.set(key, value));
|
|
17
|
+
return headers;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as Body from './Body.js';
|
|
2
|
+
import * as Headers from './Headers.js';
|
|
3
|
+
import * as Scope from './Scope.js';
|
|
4
|
+
import * as URLSearchParams from './URLSearchParams.js';
|
|
5
|
+
export type Injection = {
|
|
6
|
+
/**
|
|
7
|
+
* Search query parameters to include in server request (may be ovewritten by
|
|
8
|
+
* computed values such as `state` or `challenge_code`)
|
|
9
|
+
*/
|
|
10
|
+
search?: URLSearchParams.ish;
|
|
11
|
+
/**
|
|
12
|
+
* HTTP headers to include in server request (may be overwritten by computed
|
|
13
|
+
* values such as `Authorization: Bearer <token>`)
|
|
14
|
+
*/
|
|
15
|
+
headers?: Headers.ish;
|
|
16
|
+
/**
|
|
17
|
+
* HTTP request body parameters to include in server request (if request
|
|
18
|
+
* method allows)
|
|
19
|
+
*/
|
|
20
|
+
body?: Body.ish;
|
|
21
|
+
/** Specific scope or scopes */
|
|
22
|
+
scope?: Scope.ish;
|
|
23
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ish as URLish } from './URL.js';
|
|
2
|
+
export type ish = URLSearchParams | Record<string, string>;
|
|
3
|
+
export declare function from(search: ish): URLSearchParams;
|
|
4
|
+
export declare function toString(search: ish): string;
|
|
5
|
+
export declare function merge(a?: ish, b?: ish): URLSearchParams | undefined;
|
|
6
|
+
export declare function appendTo(url: URLish, search: ish): URLish;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function from(search) {
|
|
2
|
+
if (search instanceof URLSearchParams) {
|
|
3
|
+
return search;
|
|
4
|
+
}
|
|
5
|
+
return new URLSearchParams(search);
|
|
6
|
+
}
|
|
7
|
+
export function toString(search) {
|
|
8
|
+
const query = from(search).toString();
|
|
9
|
+
if (query.length) {
|
|
10
|
+
return `?${query}`;
|
|
11
|
+
}
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
export function merge(a, b) {
|
|
15
|
+
if (a && !b) {
|
|
16
|
+
return from(a);
|
|
17
|
+
}
|
|
18
|
+
else if (!a && b) {
|
|
19
|
+
return from(b);
|
|
20
|
+
}
|
|
21
|
+
else if (a && b) {
|
|
22
|
+
const search = from(a);
|
|
23
|
+
from(b).forEach((value, key) => search.set(key, value));
|
|
24
|
+
return search;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
export function appendTo(url, search) {
|
|
29
|
+
if (url instanceof URL) {
|
|
30
|
+
const result = new URL(url);
|
|
31
|
+
result.search = toString(search);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
return url.replace(/(.+)(\?.*)?$/, `$1${toString(search)}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { PathString } from '@battis/descriptive-types';
|
|
2
|
+
import { Request } from 'express';
|
|
3
|
+
import { Client } from './Client.js';
|
|
4
|
+
import * as Req from './Request/index.js';
|
|
5
|
+
import * as Token from './Token/index.js';
|
|
6
|
+
import * as WebServer from './WebServer.js';
|
|
7
|
+
export type SessionOptions = {
|
|
8
|
+
client: Client;
|
|
9
|
+
/** See {@link WebServer.setViews Webserver.setViews()} */
|
|
10
|
+
views?: PathString;
|
|
11
|
+
/** Additional request injection for authorization code grant flow */
|
|
12
|
+
inject?: Req.Injection;
|
|
13
|
+
};
|
|
14
|
+
export type Resolver = (response?: Token.Response, error?: Error) => void | Promise<void>;
|
|
15
|
+
export declare class Session {
|
|
16
|
+
private readonly client;
|
|
17
|
+
private readonly outOfBandRedirectServer;
|
|
18
|
+
/** PKCE code_verifier */
|
|
19
|
+
readonly code_verifier: string;
|
|
20
|
+
/** OAuth 2.0 state (if PKCE is not supported) */
|
|
21
|
+
readonly state: string;
|
|
22
|
+
/** Additional request injection for Authorization Code Grant request */
|
|
23
|
+
readonly inject?: Req.Injection;
|
|
24
|
+
private _resolve?;
|
|
25
|
+
private spinner?;
|
|
26
|
+
/**
|
|
27
|
+
* Method that resolves or rejects the promise returned from the
|
|
28
|
+
* {@link authorizationCodeGrant}
|
|
29
|
+
*/
|
|
30
|
+
get resolve(): Resolver;
|
|
31
|
+
constructor({ client, views, inject: request }: SessionOptions);
|
|
32
|
+
/** Instantiate the web server that will listen for the out-of-band redirect */
|
|
33
|
+
createWebServer(options: Omit<WebServer.WebServerOptions, 'session'>): WebServer.WebServerInterface;
|
|
34
|
+
/**
|
|
35
|
+
* Trigger the start of the Authorization Code Grant flow, returnig a Promise
|
|
36
|
+
* that will resolve into the eventual token
|
|
37
|
+
*/
|
|
38
|
+
authorizationCodeGrant(): Promise<Token.Response>;
|
|
39
|
+
/** OAuth 2.0 redirect_uri that this session is handling */
|
|
40
|
+
get redirect_uri(): Req.URL.ish;
|
|
41
|
+
getAuthorizationUrl(): Promise<string>;
|
|
42
|
+
/**
|
|
43
|
+
* Express RequestHandler for the out-of-band redirect in the Authorization
|
|
44
|
+
* Code Grant flow
|
|
45
|
+
*/
|
|
46
|
+
handleAuthorizationCodeRedirect(req: Request): Promise<void>;
|
|
47
|
+
}
|
package/dist/Session.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Colors } from '@qui-cli/colors';
|
|
2
|
+
import * as gcrtl from 'gcrtl';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import * as OpenIDClient from 'openid-client';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import * as Errors from './Errors/index.js';
|
|
7
|
+
import * as WebServer from './WebServer.js';
|
|
8
|
+
export class Session {
|
|
9
|
+
client;
|
|
10
|
+
outOfBandRedirectServer;
|
|
11
|
+
/** PKCE code_verifier */
|
|
12
|
+
code_verifier = OpenIDClient.randomPKCECodeVerifier();
|
|
13
|
+
/** OAuth 2.0 state (if PKCE is not supported) */
|
|
14
|
+
state = OpenIDClient.randomState();
|
|
15
|
+
/** Additional request injection for Authorization Code Grant request */
|
|
16
|
+
inject;
|
|
17
|
+
_resolve;
|
|
18
|
+
spinner;
|
|
19
|
+
/**
|
|
20
|
+
* Method that resolves or rejects the promise returned from the
|
|
21
|
+
* {@link authorizationCodeGrant}
|
|
22
|
+
*/
|
|
23
|
+
get resolve() {
|
|
24
|
+
if (!this._resolve) {
|
|
25
|
+
throw new Error('callback is missing');
|
|
26
|
+
}
|
|
27
|
+
return this._resolve;
|
|
28
|
+
}
|
|
29
|
+
constructor({ client, views, inject: request }) {
|
|
30
|
+
this.client = client;
|
|
31
|
+
this.inject = request;
|
|
32
|
+
this.outOfBandRedirectServer = this.createWebServer({ views });
|
|
33
|
+
}
|
|
34
|
+
/** Instantiate the web server that will listen for the out-of-band redirect */
|
|
35
|
+
createWebServer(options) {
|
|
36
|
+
return new WebServer.WebServer({ session: this, ...options });
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Trigger the start of the Authorization Code Grant flow, returnig a Promise
|
|
40
|
+
* that will resolve into the eventual token
|
|
41
|
+
*/
|
|
42
|
+
authorizationCodeGrant() {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
this._resolve = async (response, error) => {
|
|
45
|
+
if (error) {
|
|
46
|
+
reject(error);
|
|
47
|
+
}
|
|
48
|
+
if (response) {
|
|
49
|
+
resolve(response);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
reject(new Errors.MissingAccessToken());
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const url = gcrtl
|
|
56
|
+
.expand(this.outOfBandRedirectServer.authorization_endpoint, this.client.redirect_uri)
|
|
57
|
+
.toString();
|
|
58
|
+
this.spinner = ora(`Waiting for interactive authorization at ${Colors.url(url)}`).start();
|
|
59
|
+
open(url);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** OAuth 2.0 redirect_uri that this session is handling */
|
|
63
|
+
get redirect_uri() {
|
|
64
|
+
return this.client.redirect_uri;
|
|
65
|
+
}
|
|
66
|
+
async getAuthorizationUrl() {
|
|
67
|
+
return (await this.client.getAuthorizationUrl(this)).toString();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Express RequestHandler for the out-of-band redirect in the Authorization
|
|
71
|
+
* Code Grant flow
|
|
72
|
+
*/
|
|
73
|
+
async handleAuthorizationCodeRedirect(req) {
|
|
74
|
+
this.spinner?.succeed('Interactive authorization begun');
|
|
75
|
+
return await this.client.handleAuthorizationCodeRedirect(req, this);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { Token } from './Token.js';
|
|
2
1
|
import { TokenStorage } from './TokenStorage.js';
|
|
3
2
|
export declare class FileStorage implements TokenStorage {
|
|
4
3
|
private fileLock;
|
|
5
4
|
private readonly filePath;
|
|
6
5
|
constructor(filePath: string);
|
|
7
|
-
load(): Promise<
|
|
8
|
-
save(
|
|
6
|
+
load(): Promise<string | undefined>;
|
|
7
|
+
save(refresh_token: string): Promise<void>;
|
|
9
8
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Mutex } from 'async-mutex';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { Token } from './Token.js';
|
|
5
4
|
export class FileStorage {
|
|
6
5
|
fileLock = new Mutex();
|
|
7
6
|
filePath;
|
|
@@ -11,18 +10,18 @@ export class FileStorage {
|
|
|
11
10
|
async load() {
|
|
12
11
|
return await this.fileLock.runExclusive((async () => {
|
|
13
12
|
if (fs.existsSync(this.filePath)) {
|
|
14
|
-
return
|
|
13
|
+
return fs.readFileSync(this.filePath).toString();
|
|
15
14
|
}
|
|
16
15
|
return undefined;
|
|
17
16
|
}).bind(this));
|
|
18
17
|
}
|
|
19
|
-
async save(
|
|
18
|
+
async save(refresh_token) {
|
|
20
19
|
await this.fileLock.runExclusive((async () => {
|
|
21
20
|
const dirPath = path.dirname(this.filePath);
|
|
22
21
|
if (!fs.existsSync(dirPath)) {
|
|
23
22
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
24
23
|
}
|
|
25
|
-
fs.writeFileSync(this.filePath,
|
|
24
|
+
fs.writeFileSync(this.filePath, refresh_token);
|
|
26
25
|
}).bind(this));
|
|
27
26
|
}
|
|
28
27
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reaching into the bowels of openid-client to be able to fully de-serialize a
|
|
3
|
+
* serialized token response
|
|
4
|
+
*/
|
|
5
|
+
import * as oauth from 'oauth4webapi';
|
|
6
|
+
import { TokenEndpointResponseHelpers } from 'openid-client';
|
|
7
|
+
/** @see https://github.com/panva/openid-client/blob/79386b7f120dc9bdc7606886a26e4ea7d011ee51/src/index.ts#L2018-L2022 */
|
|
8
|
+
export declare function addHelpers(response: oauth.TokenEndpointResponse): asserts response is typeof response & TokenEndpointResponseHelpers;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reaching into the bowels of openid-client to be able to fully de-serialize a
|
|
3
|
+
* serialized token response
|
|
4
|
+
*/
|
|
5
|
+
import * as oauth from 'oauth4webapi';
|
|
6
|
+
/** @see https://github.com/panva/openid-client/blob/79386b7f120dc9bdc7606886a26e4ea7d011ee51/src/index.ts#L1981-L2016 */
|
|
7
|
+
function getHelpers(response) {
|
|
8
|
+
let exp = undefined;
|
|
9
|
+
if (response.expires_in !== undefined) {
|
|
10
|
+
const now = new Date();
|
|
11
|
+
now.setSeconds(now.getSeconds() + response.expires_in);
|
|
12
|
+
exp = now.getTime();
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
expiresIn: {
|
|
16
|
+
__proto__: null,
|
|
17
|
+
value() {
|
|
18
|
+
if (exp) {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
if (exp > now) {
|
|
21
|
+
return Math.floor((exp - now) / 1000);
|
|
22
|
+
}
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
claims: {
|
|
29
|
+
__proto__: null,
|
|
30
|
+
value() {
|
|
31
|
+
try {
|
|
32
|
+
return oauth.getValidatedIdTokenClaims(this);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** @see https://github.com/panva/openid-client/blob/79386b7f120dc9bdc7606886a26e4ea7d011ee51/src/index.ts#L2018-L2022 */
|
|
42
|
+
export function addHelpers(response) {
|
|
43
|
+
Object.defineProperties(response, getHelpers(response));
|
|
44
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { PathString } from '@battis/descriptive-types';
|
|
2
|
+
import { Request, Response } from 'express';
|
|
3
|
+
import { Session } from './Session.js';
|
|
4
|
+
export type WebServerOptions = {
|
|
5
|
+
session: Session;
|
|
6
|
+
/** See {@link WebServer.setViews setViews()} */
|
|
7
|
+
views?: PathString;
|
|
8
|
+
/**
|
|
9
|
+
* Local web server authorize endpoint
|
|
10
|
+
*
|
|
11
|
+
* This is separate and distinct from the OpenID/OAuth server's authorization
|
|
12
|
+
* endpoint. This endpoint is the first path that the user is directed to in
|
|
13
|
+
* their browser. It can present an explanation of what is being authorized
|
|
14
|
+
* and why. By default it redirects to the OpenID/OAuth server's authorization
|
|
15
|
+
* URL, the first step in the Authorization Code Grant flow.
|
|
16
|
+
*/
|
|
17
|
+
authorize_endpoint?: PathString;
|
|
18
|
+
};
|
|
19
|
+
export declare const DEFAULT_AUTHORIZE_ENDPOINT = "/oauth2-cli/authorize";
|
|
20
|
+
export interface WebServerInterface {
|
|
21
|
+
/** See {@link WebServerOptions} */
|
|
22
|
+
readonly authorization_endpoint: PathString;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Minimal HTTP server running on localhost to handle the redirect step of
|
|
26
|
+
* OpenID/OAuth flows
|
|
27
|
+
*/
|
|
28
|
+
export declare class WebServer implements WebServerInterface {
|
|
29
|
+
private static activePorts;
|
|
30
|
+
protected readonly session: Session;
|
|
31
|
+
private views?;
|
|
32
|
+
private packageViews;
|
|
33
|
+
protected readonly port: string;
|
|
34
|
+
readonly authorization_endpoint: PathString;
|
|
35
|
+
private server;
|
|
36
|
+
constructor({ session, views, authorize_endpoint }: WebServerOptions);
|
|
37
|
+
/**
|
|
38
|
+
* Set the path to folder of *.ejs templates
|
|
39
|
+
*
|
|
40
|
+
* Expected templates include:
|
|
41
|
+
*
|
|
42
|
+
* - `authorize.ejs` presents information prior to the authorization to the
|
|
43
|
+
* user, and the user must follow `authorize_url` data property to
|
|
44
|
+
* interactively initiate authorization
|
|
45
|
+
* - `complete.ejs` presented to user upon successful completion of
|
|
46
|
+
* authorization flow
|
|
47
|
+
* - `error.ejs` presented to user upon receipt of an error from the server,
|
|
48
|
+
* includes `error` as data
|
|
49
|
+
*
|
|
50
|
+
* `complete.ejs` and `error.ejs` are included with oauth2-cli and those
|
|
51
|
+
* templates will be used if `ejs` is imported but no replacement templates
|
|
52
|
+
* are found.
|
|
53
|
+
*
|
|
54
|
+
* @param views Should be an absolute path
|
|
55
|
+
*/
|
|
56
|
+
setViews(views: PathString): void;
|
|
57
|
+
protected render(res: Response, template: string, data?: Record<string, unknown>): Promise<boolean>;
|
|
58
|
+
/** Handles request to `/authorize` */
|
|
59
|
+
protected handleAuthorizationEndpoint(req: Request, res: Response): Promise<void>;
|
|
60
|
+
/** Handles request to `redirect_uri` */
|
|
61
|
+
protected handleRedirect(req: Request, res: Response): Promise<void>;
|
|
62
|
+
/** Close server */
|
|
63
|
+
close(): Promise<void>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import * as gcrtl from 'gcrtl';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import * as Errors from './Errors/index.js';
|
|
6
|
+
import * as Req from './Request/index.js';
|
|
7
|
+
let ejs = undefined;
|
|
8
|
+
try {
|
|
9
|
+
ejs = (await import('ejs')).default;
|
|
10
|
+
}
|
|
11
|
+
catch (_) {
|
|
12
|
+
// ignore error
|
|
13
|
+
}
|
|
14
|
+
export const DEFAULT_AUTHORIZE_ENDPOINT = '/oauth2-cli/authorize';
|
|
15
|
+
/**
|
|
16
|
+
* Minimal HTTP server running on localhost to handle the redirect step of
|
|
17
|
+
* OpenID/OAuth flows
|
|
18
|
+
*/
|
|
19
|
+
export class WebServer {
|
|
20
|
+
static activePorts = [];
|
|
21
|
+
session;
|
|
22
|
+
views;
|
|
23
|
+
packageViews = '../views';
|
|
24
|
+
port;
|
|
25
|
+
authorization_endpoint;
|
|
26
|
+
server;
|
|
27
|
+
constructor({ session, views, authorize_endpoint = DEFAULT_AUTHORIZE_ENDPOINT }) {
|
|
28
|
+
this.session = session;
|
|
29
|
+
this.authorization_endpoint = authorize_endpoint;
|
|
30
|
+
this.views = views;
|
|
31
|
+
const url = Req.URL.from(this.session.redirect_uri);
|
|
32
|
+
this.port = url.port;
|
|
33
|
+
if (WebServer.activePorts.includes(this.port)) {
|
|
34
|
+
throw new Errors.PortCollision(url.port);
|
|
35
|
+
}
|
|
36
|
+
WebServer.activePorts.push(this.port);
|
|
37
|
+
const app = express();
|
|
38
|
+
app.get(this.authorization_endpoint, this.handleAuthorizationEndpoint.bind(this));
|
|
39
|
+
app.get(gcrtl.path(url), this.handleRedirect.bind(this));
|
|
40
|
+
this.server = app.listen(gcrtl.port(url));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Set the path to folder of *.ejs templates
|
|
44
|
+
*
|
|
45
|
+
* Expected templates include:
|
|
46
|
+
*
|
|
47
|
+
* - `authorize.ejs` presents information prior to the authorization to the
|
|
48
|
+
* user, and the user must follow `authorize_url` data property to
|
|
49
|
+
* interactively initiate authorization
|
|
50
|
+
* - `complete.ejs` presented to user upon successful completion of
|
|
51
|
+
* authorization flow
|
|
52
|
+
* - `error.ejs` presented to user upon receipt of an error from the server,
|
|
53
|
+
* includes `error` as data
|
|
54
|
+
*
|
|
55
|
+
* `complete.ejs` and `error.ejs` are included with oauth2-cli and those
|
|
56
|
+
* templates will be used if `ejs` is imported but no replacement templates
|
|
57
|
+
* are found.
|
|
58
|
+
*
|
|
59
|
+
* @param views Should be an absolute path
|
|
60
|
+
*/
|
|
61
|
+
setViews(views) {
|
|
62
|
+
this.views = views;
|
|
63
|
+
}
|
|
64
|
+
async render(res, template, data = {}) {
|
|
65
|
+
async function attemptToRender(views) {
|
|
66
|
+
if (ejs && views) {
|
|
67
|
+
const viewPath = path.resolve(import.meta.dirname, views, template);
|
|
68
|
+
if (fs.existsSync(viewPath)) {
|
|
69
|
+
res.send(await ejs.renderFile(viewPath, data));
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return ((await attemptToRender(this.views)) ||
|
|
76
|
+
(await attemptToRender(this.packageViews)));
|
|
77
|
+
}
|
|
78
|
+
/** Handles request to `/authorize` */
|
|
79
|
+
async handleAuthorizationEndpoint(req, res) {
|
|
80
|
+
const authorization_url = await this.session.getAuthorizationUrl();
|
|
81
|
+
if (!(await this.render(res, 'authorize.ejs', { authorization_url }))) {
|
|
82
|
+
res.redirect(authorization_url);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Handles request to `redirect_uri` */
|
|
86
|
+
async handleRedirect(req, res) {
|
|
87
|
+
try {
|
|
88
|
+
await this.session.handleAuthorizationCodeRedirect(req);
|
|
89
|
+
if (!(await this.render(res, 'complete.ejs'))) {
|
|
90
|
+
res.send('You may close this window.');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (!(await this.render(res, 'error.ejs', { error }))) {
|
|
95
|
+
res.send(error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
this.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Close server */
|
|
103
|
+
async close() {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
this.server.close((err) => {
|
|
106
|
+
if (err) {
|
|
107
|
+
reject(err);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
WebServer.activePorts.splice(WebServer.activePorts.indexOf(this.port), 1);
|
|
111
|
+
resolve();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|