@feasibleone/blong-gogo 1.13.2 → 1.13.3
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 +7 -0
- package/package.json +1 -2
- package/src/Gateway.ts +2 -2
- package/src/GatewayCodec.ts +5 -5
- package/src/busGateway.ts +179 -0
- package/src/codec/adapter/mle/ready.ts +1 -1
- package/src/lib.ts +128 -4
- package/src/oidc.ts +208 -0
- package/src/swagger.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.13.3](https://github.com/feasibleone/blong/compare/blong-gogo-v1.13.2...blong-gogo-v1.13.3) (2026-03-17)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* reduce dependencies ([e1798f5](https://github.com/feasibleone/blong/commit/e1798f590a2e2c6d9de99477cfe09c282433dcbc))
|
|
9
|
+
|
|
3
10
|
## [1.13.2](https://github.com/feasibleone/blong/compare/blong-gogo-v1.13.1...blong-gogo-v1.13.2) (2026-03-17)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feasibleone/blong-gogo",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.3",
|
|
4
4
|
"repository": {
|
|
5
5
|
"url": "git+https://github.com/feasibleone/blong.git"
|
|
6
6
|
},
|
|
@@ -56,7 +56,6 @@
|
|
|
56
56
|
"typebox": "^1.1.5",
|
|
57
57
|
"ulidx": "^2.4.1",
|
|
58
58
|
"ut-bitsyntax": "^6.2.7",
|
|
59
|
-
"ut-bus": "^7.65.2",
|
|
60
59
|
"ut-dns-discovery": "^6.2.3",
|
|
61
60
|
"ut-function.interpolate": "1.1.3",
|
|
62
61
|
"ut-function.merge": "^1.5.6",
|
package/src/Gateway.ts
CHANGED
|
@@ -144,7 +144,7 @@ export default class Gateway extends Internal implements IGateway {
|
|
|
144
144
|
errorFields: [],
|
|
145
145
|
jwt: {
|
|
146
146
|
cache: {},
|
|
147
|
-
audience: '
|
|
147
|
+
audience: 'blong',
|
|
148
148
|
},
|
|
149
149
|
};
|
|
150
150
|
|
|
@@ -353,7 +353,7 @@ export default class Gateway extends Internal implements IGateway {
|
|
|
353
353
|
value.auth === false
|
|
354
354
|
? {}
|
|
355
355
|
: {
|
|
356
|
-
'
|
|
356
|
+
'blong-login': ['api'],
|
|
357
357
|
},
|
|
358
358
|
],
|
|
359
359
|
tags: [method.split('.')[0] + ' ' + pkg.name + '@' + pkg.version],
|
package/src/GatewayCodec.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type {IMeta} from '@feasibleone/blong/types';
|
|
2
2
|
import got, {type HTTPAlias, type Headers} from 'got';
|
|
3
3
|
import type {JWTPayload} from 'jose';
|
|
4
|
-
import busGateway from '
|
|
5
|
-
import jose from '
|
|
6
|
-
import oidc from '
|
|
4
|
+
import busGateway from './busGateway.ts';
|
|
5
|
+
import jose from './jose.ts';
|
|
6
|
+
import oidc from './oidc.ts';
|
|
7
7
|
|
|
8
8
|
import type {IResolution} from './Resolution.ts';
|
|
9
9
|
import tls from './tls.ts';
|
|
@@ -45,7 +45,7 @@ export interface IConfig {
|
|
|
45
45
|
port?: string;
|
|
46
46
|
service?: string;
|
|
47
47
|
openId?: unknown;
|
|
48
|
-
|
|
48
|
+
blongLogin?: unknown;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
type Sender = (a: unknown, b: unknown) => Promise<unknown>;
|
|
@@ -153,7 +153,7 @@ export default class GatewayCodecImpl implements IGatewayCodec {
|
|
|
153
153
|
session,
|
|
154
154
|
tls: this.#tlsClient,
|
|
155
155
|
issuers: config.openId || {
|
|
156
|
-
...(config.
|
|
156
|
+
...(config.blongLogin !== false && {'blong-login': {audience: 'blong'}}),
|
|
157
157
|
},
|
|
158
158
|
});
|
|
159
159
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// const request = (process.type === 'renderer') ? require('ut-browser-request') : require('request');
|
|
2
|
+
// const [httpPost] = [request.post].map(require('util').promisify);
|
|
3
|
+
import ky from 'ky';
|
|
4
|
+
|
|
5
|
+
const decode = (result, method, unpack) => (unpack ? result : [result, {method, mtid: 'response'}]);
|
|
6
|
+
|
|
7
|
+
export default ({serverInfo, mleClient, errors, get}) => {
|
|
8
|
+
const localCache = {};
|
|
9
|
+
const localKeys = mleClient.keys.sign &&
|
|
10
|
+
mleClient.keys.encrypt && {mlsk: mleClient.keys.sign, mlek: mleClient.keys.encrypt};
|
|
11
|
+
|
|
12
|
+
function tokenInfo(auth) {
|
|
13
|
+
const now = Date.now() - 5000; // latency tolerance of 5 seconds
|
|
14
|
+
return {
|
|
15
|
+
tokenExpire: now + auth.expires_in * 1000,
|
|
16
|
+
refreshTokenExpire: now + auth.refresh_token_expires_in * 1000,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function login(cache, url, username, password, channel) {
|
|
21
|
+
const {sign, encrypt} = (localKeys && (cache.auth || cache.remoteKeys)) || {};
|
|
22
|
+
if (sign && encrypt) {
|
|
23
|
+
const {result, error} = await ky
|
|
24
|
+
.post<{result: unknown; error: unknown}>(`${url}/rpc/login/identity/exchange`, {
|
|
25
|
+
json: {
|
|
26
|
+
jsonrpc: '2.0',
|
|
27
|
+
method: 'login.identity.exchange',
|
|
28
|
+
id: 1,
|
|
29
|
+
params: await mleClient.signEncrypt(
|
|
30
|
+
{username, password, channel},
|
|
31
|
+
encrypt,
|
|
32
|
+
localKeys,
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
.json();
|
|
37
|
+
if (error) throw Object.assign(new Error(), await mleClient.decryptVerify(error, sign));
|
|
38
|
+
else if (result) cache.auth = await mleClient.decryptVerify(result, sign);
|
|
39
|
+
else throw errors['bus.jsonRpcEmpty']();
|
|
40
|
+
} else {
|
|
41
|
+
const {result, error} = await ky
|
|
42
|
+
.post<{result: unknown; error: unknown}>(`${url}/rpc/login/identity/check`, {
|
|
43
|
+
json: {
|
|
44
|
+
jsonrpc: '2.0',
|
|
45
|
+
method: 'login.identity.check',
|
|
46
|
+
id: 1,
|
|
47
|
+
params: {username, password, channel},
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
.json();
|
|
51
|
+
if (error) throw Object.assign(new Error(), error);
|
|
52
|
+
else if (result) cache.auth = result;
|
|
53
|
+
else throw errors['bus.jsonRpcEmpty']();
|
|
54
|
+
}
|
|
55
|
+
cache.tokenInfo = tokenInfo(cache.auth);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return async function gateway({
|
|
59
|
+
username,
|
|
60
|
+
password,
|
|
61
|
+
channel = 'web',
|
|
62
|
+
protocol = serverInfo('protocol'),
|
|
63
|
+
host: hostname = 'localhost',
|
|
64
|
+
port = serverInfo('port'),
|
|
65
|
+
url,
|
|
66
|
+
tls,
|
|
67
|
+
auth,
|
|
68
|
+
encrypt = true,
|
|
69
|
+
method,
|
|
70
|
+
}) {
|
|
71
|
+
// don't put a default value for uri in arguments as it can be empty string or null
|
|
72
|
+
if (url) {
|
|
73
|
+
const parsed = new URL(url);
|
|
74
|
+
hostname = parsed.hostname;
|
|
75
|
+
port = parsed.port;
|
|
76
|
+
protocol = parsed.protocol.split(':')[0];
|
|
77
|
+
if (parsed.username) username = parsed.username;
|
|
78
|
+
if (parsed.password) password = parsed.password;
|
|
79
|
+
} else {
|
|
80
|
+
protocol = protocol && protocol.split(':')[0];
|
|
81
|
+
url = `${protocol}://${hostname}:${port}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const codec = {
|
|
85
|
+
requestParams: {
|
|
86
|
+
protocol,
|
|
87
|
+
hostname,
|
|
88
|
+
port,
|
|
89
|
+
path: `/rpc/${method.replace(/\./g, '/')}`,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const cache = (localCache[url] = localCache[url] || {});
|
|
94
|
+
|
|
95
|
+
if (localKeys && !cache.remoteKeys) {
|
|
96
|
+
const body = await get(
|
|
97
|
+
`${url}/rpc/login/.well-known/mle`,
|
|
98
|
+
errors['bus.jsonRpcHttp'],
|
|
99
|
+
errors['bus.jsonRpcEmpty'],
|
|
100
|
+
);
|
|
101
|
+
if (body.sign && body.encrypt) cache.remoteKeys = body;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (auth) {
|
|
105
|
+
cache.auth = auth;
|
|
106
|
+
cache.tokenInfo = tokenInfo(auth);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!cache.auth && !(username && password)) {
|
|
110
|
+
if (cache.remoteKeys) {
|
|
111
|
+
codec.encode = async params => ({
|
|
112
|
+
params: await mleClient.signEncrypt(
|
|
113
|
+
params,
|
|
114
|
+
cache.remoteKeys.encrypt,
|
|
115
|
+
localKeys,
|
|
116
|
+
),
|
|
117
|
+
method,
|
|
118
|
+
});
|
|
119
|
+
codec.decode = async (result, unpack) =>
|
|
120
|
+
decode(
|
|
121
|
+
await mleClient.decryptVerify(result, cache.remoteKeys.sign),
|
|
122
|
+
method,
|
|
123
|
+
unpack,
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
codec.encode = params => ({params, method});
|
|
127
|
+
codec.decode = (result, unpack) => decode(result, method, unpack);
|
|
128
|
+
}
|
|
129
|
+
return codec;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!cache.auth) await login(cache, url, username, password, channel);
|
|
133
|
+
|
|
134
|
+
const exp = Date.now();
|
|
135
|
+
|
|
136
|
+
if (exp > cache.tokenInfo.tokenExpire) {
|
|
137
|
+
if (exp > cache.tokenInfo.refreshTokenExpire) {
|
|
138
|
+
await login(cache, url, username, password, channel);
|
|
139
|
+
} else {
|
|
140
|
+
const {body} = await ky
|
|
141
|
+
.post<{body: {access_token: string; refresh_token: string}}>(
|
|
142
|
+
`${url}/rpc/login/token`,
|
|
143
|
+
{
|
|
144
|
+
json: {
|
|
145
|
+
grant_type: 'refresh_token',
|
|
146
|
+
refresh_token: cache.auth.refresh_token,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
.json();
|
|
151
|
+
Object.assign(cache.auth, body);
|
|
152
|
+
cache.tokenInfo = tokenInfo(body);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (cache.auth.sign && cache.auth.encrypt) {
|
|
157
|
+
codec.encode = async params => ({
|
|
158
|
+
params: await mleClient.signEncrypt(params, cache.auth.encrypt),
|
|
159
|
+
headers: {
|
|
160
|
+
authorization: 'Bearer ' + cache.auth.access_token,
|
|
161
|
+
},
|
|
162
|
+
method,
|
|
163
|
+
});
|
|
164
|
+
codec.decode = async (result, unpack) =>
|
|
165
|
+
decode(await mleClient.decryptVerify(result, cache.auth.sign), method, unpack);
|
|
166
|
+
} else {
|
|
167
|
+
codec.encode = params => ({
|
|
168
|
+
params,
|
|
169
|
+
headers: {
|
|
170
|
+
authorization: 'Bearer ' + cache.auth.access_token,
|
|
171
|
+
},
|
|
172
|
+
method,
|
|
173
|
+
});
|
|
174
|
+
codec.decode = (result, unpack) => decode(result, method, unpack);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return codec;
|
|
178
|
+
};
|
|
179
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {handler} from '@feasibleone/blong/types';
|
|
2
2
|
import {type Response} from 'got';
|
|
3
3
|
import {exportJWK, generateKeyPair} from 'jose';
|
|
4
|
-
import joseFactory from '
|
|
4
|
+
import joseFactory from '../../../jose.ts';
|
|
5
5
|
|
|
6
6
|
const isBrowser: boolean = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
7
7
|
|
package/src/lib.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import ky from 'ky';
|
|
2
|
+
|
|
1
3
|
export function methodId<T>(what: T): T {
|
|
2
4
|
return (
|
|
3
5
|
what &&
|
|
4
6
|
((typeof what === 'string'
|
|
5
7
|
? what.replace(/\./g, '').toLowerCase()
|
|
6
8
|
: typeof what === 'object'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
? Object.fromEntries(
|
|
10
|
+
Object.entries(what).map(([name, value]) => [methodId(name), value]),
|
|
11
|
+
)
|
|
12
|
+
: what) as T)
|
|
11
13
|
);
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -27,3 +29,125 @@ export function identifier(string: string): string {
|
|
|
27
29
|
string = snakeToCamel(string);
|
|
28
30
|
return /[^\w$]/.test(string) ? `'${string}'` : string;
|
|
29
31
|
}
|
|
32
|
+
|
|
33
|
+
let loginCache: unknown;
|
|
34
|
+
export async function loginService(discovery) {
|
|
35
|
+
if (!loginCache) loginCache = discovery('login');
|
|
36
|
+
try {
|
|
37
|
+
return await loginCache;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
loginCache = false;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function requestGet(
|
|
45
|
+
url: string,
|
|
46
|
+
errorHttp: (params: Record<string, unknown>) => unknown,
|
|
47
|
+
errorEmpty: () => unknown,
|
|
48
|
+
headers: Record<string, string | undefined> | undefined,
|
|
49
|
+
protocol: string,
|
|
50
|
+
tls: Record<string, unknown> | undefined,
|
|
51
|
+
): Promise<unknown> {
|
|
52
|
+
const response = await ky.get(url, {
|
|
53
|
+
...tls,
|
|
54
|
+
...(headers && {
|
|
55
|
+
headers: {
|
|
56
|
+
'x-forwarded-proto': headers['x-forwarded-proto'] || protocol,
|
|
57
|
+
'x-forwarded-host': headers['x-forwarded-host'] || headers.host,
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
throwHttpErrors: false,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const responseText = await response.text();
|
|
64
|
+
let body: unknown;
|
|
65
|
+
try {
|
|
66
|
+
body = responseText ? JSON.parse(responseText) : undefined;
|
|
67
|
+
} catch {
|
|
68
|
+
body = undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (response.status < 200 || response.status >= 300) {
|
|
72
|
+
throw errorHttp({
|
|
73
|
+
statusCode: response.status,
|
|
74
|
+
statusText: response.statusText,
|
|
75
|
+
statusMessage: response.statusText,
|
|
76
|
+
validation: (body as any)?.validation,
|
|
77
|
+
debug: (body as any)?.debug,
|
|
78
|
+
params: {
|
|
79
|
+
code: response.status,
|
|
80
|
+
},
|
|
81
|
+
req: {
|
|
82
|
+
url: response.url,
|
|
83
|
+
method: 'GET',
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (body) return body;
|
|
89
|
+
throw errorEmpty();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function requestPostForm(
|
|
93
|
+
url: string,
|
|
94
|
+
errorHttp: (params: Record<string, unknown>) => unknown,
|
|
95
|
+
errorEmpty: () => unknown,
|
|
96
|
+
headers: Record<string, string | undefined> | undefined,
|
|
97
|
+
protocol: string,
|
|
98
|
+
tls: Record<string, unknown> | undefined,
|
|
99
|
+
form: Record<string, string | number | boolean | null | undefined> | URLSearchParams,
|
|
100
|
+
): Promise<string> {
|
|
101
|
+
const forwardedHeaders = headers && {
|
|
102
|
+
'x-forwarded-proto': headers['x-forwarded-proto'] || protocol,
|
|
103
|
+
'x-forwarded-host': headers['x-forwarded-host'] || headers.host,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const body =
|
|
107
|
+
form instanceof URLSearchParams
|
|
108
|
+
? form
|
|
109
|
+
: new URLSearchParams(
|
|
110
|
+
Object.entries(form).map(([key, value]) => [
|
|
111
|
+
key,
|
|
112
|
+
value == null ? '' : String(value),
|
|
113
|
+
]),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const response = await ky.post(url, {
|
|
117
|
+
...tls,
|
|
118
|
+
headers: {
|
|
119
|
+
...(forwardedHeaders || {}),
|
|
120
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
121
|
+
},
|
|
122
|
+
body,
|
|
123
|
+
throwHttpErrors: false,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const responseText = await response.text();
|
|
127
|
+
let responseJson: any;
|
|
128
|
+
try {
|
|
129
|
+
responseJson = responseText ? JSON.parse(responseText) : undefined;
|
|
130
|
+
} catch {
|
|
131
|
+
responseJson = undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (response.status < 200 || response.status >= 300) {
|
|
135
|
+
throw errorHttp({
|
|
136
|
+
statusCode: response.status,
|
|
137
|
+
statusText: response.statusText,
|
|
138
|
+
statusMessage: response.statusText,
|
|
139
|
+
validation: responseJson?.validation,
|
|
140
|
+
debug: responseJson?.debug,
|
|
141
|
+
params: {
|
|
142
|
+
code: response.status,
|
|
143
|
+
},
|
|
144
|
+
req: {
|
|
145
|
+
url: response.url,
|
|
146
|
+
method: 'POST',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (responseText) return responseText;
|
|
152
|
+
throw errorEmpty();
|
|
153
|
+
}
|
package/src/oidc.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {createLocalJWKSet, decodeJwt, decodeProtectedHeader, jwtVerify} from 'jose';
|
|
2
|
+
import {requestGet as get, loginService} from './lib.ts';
|
|
3
|
+
|
|
4
|
+
export default ({
|
|
5
|
+
issuers,
|
|
6
|
+
tls = {},
|
|
7
|
+
discoverService = false,
|
|
8
|
+
session = false,
|
|
9
|
+
errorPrefix,
|
|
10
|
+
errors: {
|
|
11
|
+
[`${errorPrefix}oidcEmpty`]: errorOidcEmpty,
|
|
12
|
+
[`${errorPrefix}oidcHttp`]: errorOidcHttp,
|
|
13
|
+
[`${errorPrefix}actionEmpty`]: errorActionEmpty,
|
|
14
|
+
[`${errorPrefix}actionHttp`]: errorActionHttp,
|
|
15
|
+
[`${errorPrefix}unauthorized`]: errorUnauthorized,
|
|
16
|
+
[`${errorPrefix}oidcNoIssuer`]: errorNoIssuer,
|
|
17
|
+
[`${errorPrefix}oidcNoKid`]: errorNoKid,
|
|
18
|
+
[`${errorPrefix}oidcBadIssuer`]: errorBadIssuer,
|
|
19
|
+
[`${errorPrefix}jwtInvalid`]: errorInvalid,
|
|
20
|
+
},
|
|
21
|
+
}) => {
|
|
22
|
+
async function openIdConfig(issuer, headers, protocol) {
|
|
23
|
+
if (issuer === 'blong-login') {
|
|
24
|
+
const {protocol: loginProtocol, hostname, port} = await loginService(discoverService);
|
|
25
|
+
issuer = `${loginProtocol}://${hostname}:${port}/rpc/login/.well-known/openid-configuration`;
|
|
26
|
+
} else {
|
|
27
|
+
headers = false;
|
|
28
|
+
}
|
|
29
|
+
return await get(issuer, errorOidcHttp, errorOidcEmpty, headers, protocol, tls);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let actionsCache;
|
|
33
|
+
async function actions(method) {
|
|
34
|
+
if (actionsCache) return actionsCache[method];
|
|
35
|
+
const {protocol, hostname, port} = await loginService(discoverService);
|
|
36
|
+
const actionsMap = await get(
|
|
37
|
+
`${protocol}://${hostname}:${port}/rpc/login/action`,
|
|
38
|
+
errorActionHttp,
|
|
39
|
+
errorActionEmpty,
|
|
40
|
+
{},
|
|
41
|
+
undefined,
|
|
42
|
+
tls,
|
|
43
|
+
);
|
|
44
|
+
const fuzzyMap = Object.entries(actionsMap).reduce((all, [action, bit]) => {
|
|
45
|
+
for (let i = 1, segments = action.split('.'), n = segments.length; i < n; i += 1) {
|
|
46
|
+
const key = segments.slice(0, i).concat('%').join('.');
|
|
47
|
+
if (!all[key]) all[key] = [];
|
|
48
|
+
all[key].push(bit);
|
|
49
|
+
}
|
|
50
|
+
return all;
|
|
51
|
+
}, {});
|
|
52
|
+
|
|
53
|
+
actionsCache = {...fuzzyMap, ...actionsMap};
|
|
54
|
+
|
|
55
|
+
return actionsCache[method];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function checkPermission(bit, map) {
|
|
59
|
+
bit -= 1;
|
|
60
|
+
const index = Math.floor(bit / 8);
|
|
61
|
+
return Number.isInteger(index) && index < map.length && map[index] & (1 << (bit % 8));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function checkAuthSingle(method, map) {
|
|
65
|
+
if (Array.isArray(method)) {
|
|
66
|
+
for (const m of method) {
|
|
67
|
+
if (!(await checkAuthSingle(m, map))) return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
const bit = await actions(method);
|
|
72
|
+
return Array.isArray(bit)
|
|
73
|
+
? bit.some(b => checkPermission(b, map))
|
|
74
|
+
: checkPermission(bit, map);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function checkAuth(method, map, dontThrow) {
|
|
78
|
+
if (!(await checkAuthSingle(method, map)) && !(await checkAuthSingle('%', map))) {
|
|
79
|
+
if (dontThrow) return false;
|
|
80
|
+
throw errorUnauthorized({params: {method}});
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const issuerUrl = (base, url) =>
|
|
86
|
+
base === 'blong-login' ? 'blong-login' : new URL(url, base.replace(/\/?$/, '/')).href;
|
|
87
|
+
|
|
88
|
+
const loadIssuers = () =>
|
|
89
|
+
Promise.all(
|
|
90
|
+
Object.entries(issuers)
|
|
91
|
+
.filter(([, config]) => config)
|
|
92
|
+
.map(
|
|
93
|
+
([
|
|
94
|
+
issuerId,
|
|
95
|
+
{
|
|
96
|
+
configuration,
|
|
97
|
+
url = '.well-known/openid-configuration',
|
|
98
|
+
audience = 'blong',
|
|
99
|
+
...rest
|
|
100
|
+
},
|
|
101
|
+
]) =>
|
|
102
|
+
(async () => [
|
|
103
|
+
issuerId,
|
|
104
|
+
{
|
|
105
|
+
...(await openIdConfig(configuration || issuerUrl(issuerId, url))),
|
|
106
|
+
audience,
|
|
107
|
+
issuerId,
|
|
108
|
+
...rest,
|
|
109
|
+
},
|
|
110
|
+
])(),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
async function cache() {
|
|
115
|
+
return (await loadIssuers()).reduce(
|
|
116
|
+
(prev, [issuer, config]) => ({...prev, [config.issuer]: config, [issuer]: config}),
|
|
117
|
+
{},
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const getIssuers = (headers, protocol) =>
|
|
122
|
+
Promise.all(
|
|
123
|
+
Object.entries(issuers)
|
|
124
|
+
.filter(([, config]) => config)
|
|
125
|
+
.map(([issuer, {configuration, url = '.well-known/openid-configuration'}]) =>
|
|
126
|
+
openIdConfig(configuration || issuerUrl(issuer, url), headers, protocol),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
let issuersCache;
|
|
131
|
+
|
|
132
|
+
async function issuerConfig(issuerId) {
|
|
133
|
+
if (issuerId === 'blong-login') return openIdConfig('blong-login');
|
|
134
|
+
issuersCache = issuersCache || cache();
|
|
135
|
+
const result = (await issuersCache)[issuerId];
|
|
136
|
+
if (!result) {
|
|
137
|
+
throw errorBadIssuer({params: {issuerId}});
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function jwks(issuerId) {
|
|
143
|
+
return get(
|
|
144
|
+
(await issuerConfig(issuerId)).jwks_uri,
|
|
145
|
+
errorOidcHttp,
|
|
146
|
+
errorOidcEmpty,
|
|
147
|
+
{},
|
|
148
|
+
undefined,
|
|
149
|
+
tls,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const keys = {};
|
|
154
|
+
|
|
155
|
+
async function getKey(decoded, protectedHeader) {
|
|
156
|
+
const issuerId = decoded?.iss;
|
|
157
|
+
if (!issuerId) throw errorNoIssuer();
|
|
158
|
+
const kid = protectedHeader?.kid;
|
|
159
|
+
if (!kid) throw errorNoKid();
|
|
160
|
+
if (!keys[issuerId]) keys[issuerId] = createLocalJWKSet(await jwks(issuerId));
|
|
161
|
+
const result = await keys[issuerId](protectedHeader, decoded);
|
|
162
|
+
if (!result) throw errorInvalid({params: {message: 'Invalid OIDC key id'}});
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function verify(token, {nonce, audience = 'blong'}, isId) {
|
|
167
|
+
let decoded;
|
|
168
|
+
let protectedHeader;
|
|
169
|
+
try {
|
|
170
|
+
decoded = decodeJwt(token);
|
|
171
|
+
protectedHeader = decodeProtectedHeader(token);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throw errorInvalid({params: {message: error.message}, cause: error});
|
|
174
|
+
}
|
|
175
|
+
const config = (decoded.iss &&
|
|
176
|
+
decoded.iss !== 'blong-login' &&
|
|
177
|
+
(await issuerConfig(decoded.iss))) || {audience};
|
|
178
|
+
try {
|
|
179
|
+
if (isId) {
|
|
180
|
+
await jwtVerify(token, await getKey(decoded, protectedHeader), {
|
|
181
|
+
audience: config.audience,
|
|
182
|
+
issuer: decoded.iss,
|
|
183
|
+
nonce,
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
await jwtVerify(token, await getKey(decoded, protectedHeader), {
|
|
187
|
+
audience: config.audience,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
throw errorInvalid({params: {message: error.message}, cause: error});
|
|
192
|
+
}
|
|
193
|
+
if (session && audience === 'blong' && !decoded.ses) await session(decoded);
|
|
194
|
+
return {
|
|
195
|
+
...decoded,
|
|
196
|
+
config,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
get: (url, errorHttp, errorEmpty, headers, protocol) =>
|
|
202
|
+
get(url, errorHttp, errorEmpty, headers, protocol, tls),
|
|
203
|
+
verify,
|
|
204
|
+
getIssuers,
|
|
205
|
+
checkAuth,
|
|
206
|
+
issuerConfig,
|
|
207
|
+
};
|
|
208
|
+
};
|
package/src/swagger.ts
CHANGED
|
@@ -5,7 +5,7 @@ import fp from 'fastify-plugin';
|
|
|
5
5
|
|
|
6
6
|
export default fp<{version: string}>(async function swaggerPlugin(
|
|
7
7
|
fastify: FastifyInstance,
|
|
8
|
-
{version}: FastifyPluginOptions
|
|
8
|
+
{version}: FastifyPluginOptions,
|
|
9
9
|
) {
|
|
10
10
|
await fastify.register(swagger, {
|
|
11
11
|
openapi: {
|
|
@@ -15,7 +15,7 @@ export default fp<{version: string}>(async function swaggerPlugin(
|
|
|
15
15
|
},
|
|
16
16
|
components: {
|
|
17
17
|
securitySchemes: {
|
|
18
|
-
'
|
|
18
|
+
'blong-login': {
|
|
19
19
|
flows: {
|
|
20
20
|
authorizationCode: {
|
|
21
21
|
authorizationUrl: '/rpc/login/form',
|