@marktiderman/genesis-connect 0.2.0 → 0.3.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/README.md +34 -15
- package/dist/index.cjs +98 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -3
- package/dist/index.d.ts +16 -3
- package/dist/index.js +98 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -108,7 +108,12 @@ function identityFromTokenEndpoint(idToken, expect) {
|
|
|
108
108
|
const audiences = typeof aud === "string" ? [aud] : Array.isArray(aud) ? aud : [];
|
|
109
109
|
if (!audiences.includes(expect.clientId))
|
|
110
110
|
return { reason: "audience mismatch" };
|
|
111
|
-
if (audiences.
|
|
111
|
+
if (audiences.some(
|
|
112
|
+
(a) => a !== expect.clientId && !(typeof a === "string" && expect.trustedAudiences.includes(a))
|
|
113
|
+
))
|
|
114
|
+
return { reason: "untrusted audience" };
|
|
115
|
+
const azp = claims["azp"];
|
|
116
|
+
if (azp !== void 0 && azp !== expect.clientId || audiences.length > 1 && azp === void 0)
|
|
112
117
|
return { reason: "authorized party mismatch" };
|
|
113
118
|
const exp = claims["exp"];
|
|
114
119
|
if (typeof exp !== "number" || exp * 1e3 <= expect.now)
|
|
@@ -183,6 +188,21 @@ function checkOrigin(value, what) {
|
|
|
183
188
|
}
|
|
184
189
|
return origin;
|
|
185
190
|
}
|
|
191
|
+
function checkEndpoint(value, what) {
|
|
192
|
+
let url;
|
|
193
|
+
try {
|
|
194
|
+
url = new URL(value);
|
|
195
|
+
} catch {
|
|
196
|
+
throw new ConnectError("config", `The provider's ${what} is not a URL`);
|
|
197
|
+
}
|
|
198
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocal(url.hostname))) {
|
|
199
|
+
throw new ConnectError(
|
|
200
|
+
"config",
|
|
201
|
+
`The provider's ${what} must be https (http only on localhost)`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
186
206
|
function checkPath(value, what) {
|
|
187
207
|
if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//") || /[\\\s\x00-\x1f\x7f]/.test(value) || value.length > 512) {
|
|
188
208
|
throw new ConnectError(
|
|
@@ -219,6 +239,7 @@ function scopeList(scopes, separator) {
|
|
|
219
239
|
return list;
|
|
220
240
|
}
|
|
221
241
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
242
|
+
var ABANDON_TIMEOUT_MS = 3e3;
|
|
222
243
|
async function timedFetch(deps, url, init, signal) {
|
|
223
244
|
const timeout = AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
224
245
|
const combined = signal ? anySignal([timeout, signal]) : timeout;
|
|
@@ -288,6 +309,8 @@ function createConnector(config) {
|
|
|
288
309
|
"config",
|
|
289
310
|
"The provider needs an id, an authorize URL and a token URL"
|
|
290
311
|
);
|
|
312
|
+
checkEndpoint(provider.tokenUrl, "token URL");
|
|
313
|
+
if (provider.revokeUrl) checkEndpoint(provider.revokeUrl, "revoke URL");
|
|
291
314
|
if (!config.clientId || !config.clientSecret)
|
|
292
315
|
throw new ConnectError(
|
|
293
316
|
"config",
|
|
@@ -357,6 +380,21 @@ function createConnector(config) {
|
|
|
357
380
|
);
|
|
358
381
|
const aliases = provider.scopeAliases ?? {};
|
|
359
382
|
const normalScope = (scope) => aliases[scope] ?? scope;
|
|
383
|
+
const asksOpenid = (scopes) => scopes.some((scope) => normalScope(scope) === "openid");
|
|
384
|
+
const issuers = (typeof provider.issuer === "string" ? [provider.issuer] : provider.issuer ?? []).filter((issuer) => typeof issuer === "string" && issuer.trim() !== "");
|
|
385
|
+
if (config.requireIdentity) {
|
|
386
|
+
if (issuers.length === 0)
|
|
387
|
+
throw new ConnectError(
|
|
388
|
+
"config",
|
|
389
|
+
`requireIdentity needs the ${provider.id} provider's issuer`
|
|
390
|
+
);
|
|
391
|
+
if (!asksOpenid(defaultScopes))
|
|
392
|
+
throw new ConnectError(
|
|
393
|
+
"config",
|
|
394
|
+
"requireIdentity needs the openid scope in the connector's scopes"
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const trustedAudiences = [...config.trustedAudiences ?? []];
|
|
360
398
|
const now = () => config.now?.() ?? Date.now();
|
|
361
399
|
const log = (event) => {
|
|
362
400
|
try {
|
|
@@ -607,6 +645,46 @@ ${refreshToken}`);
|
|
|
607
645
|
tokenCache.set(await cacheKey(plain), entry);
|
|
608
646
|
}
|
|
609
647
|
}
|
|
648
|
+
async function revokeToken(token, hint, signal) {
|
|
649
|
+
if (!provider.revokeUrl) return false;
|
|
650
|
+
const body = new URLSearchParams({ token, token_type_hint: hint });
|
|
651
|
+
const headers = {
|
|
652
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
653
|
+
accept: "application/json"
|
|
654
|
+
};
|
|
655
|
+
if (provider.revokeWithClientAuth) clientAuth(body, headers);
|
|
656
|
+
try {
|
|
657
|
+
const response = await timedFetch(
|
|
658
|
+
config,
|
|
659
|
+
provider.revokeUrl,
|
|
660
|
+
{ method: "POST", headers, body: body.toString() },
|
|
661
|
+
signal
|
|
662
|
+
);
|
|
663
|
+
let gone = response.ok;
|
|
664
|
+
if (!gone && response.status === 400) {
|
|
665
|
+
gone = errorCode(await jsonBody(response)) === "invalid_token";
|
|
666
|
+
} else {
|
|
667
|
+
discard(response);
|
|
668
|
+
}
|
|
669
|
+
log({
|
|
670
|
+
step: "revoke",
|
|
671
|
+
result: gone ? "revoked" : "failed",
|
|
672
|
+
...gone ? {} : { detail: `revoke endpoint answered ${response.status}` }
|
|
673
|
+
});
|
|
674
|
+
return gone;
|
|
675
|
+
} catch {
|
|
676
|
+
log({ step: "revoke", result: "unavailable" });
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function abandonGrant(refreshToken, accessToken) {
|
|
681
|
+
if (config.revokeFailedGrants !== true) return;
|
|
682
|
+
const [token, hint] = typeof refreshToken === "string" && refreshToken ? [refreshToken, "refresh_token"] : [accessToken, "access_token"];
|
|
683
|
+
revokeToken(token, hint, AbortSignal.timeout(ABANDON_TIMEOUT_MS)).catch(
|
|
684
|
+
() => {
|
|
685
|
+
}
|
|
686
|
+
);
|
|
687
|
+
}
|
|
610
688
|
return {
|
|
611
689
|
provider,
|
|
612
690
|
redirectUri,
|
|
@@ -639,6 +717,11 @@ ${refreshToken}`);
|
|
|
639
717
|
"usage",
|
|
640
718
|
"No scopes to ask for: give the connector or start some"
|
|
641
719
|
);
|
|
720
|
+
if (config.requireIdentity && !asksOpenid(scopes))
|
|
721
|
+
throw new ConnectError(
|
|
722
|
+
"usage",
|
|
723
|
+
"requireIdentity needs the openid scope: start's scopes leave it out"
|
|
724
|
+
);
|
|
642
725
|
const nonce = randomToken(16);
|
|
643
726
|
const verifier = randomToken(32);
|
|
644
727
|
const state = await signState({
|
|
@@ -742,8 +825,10 @@ ${refreshToken}`);
|
|
|
742
825
|
return back(request, state, "failed", {}, "no access token");
|
|
743
826
|
}
|
|
744
827
|
const { scope, scopes } = granted(body, state.scopes);
|
|
745
|
-
if (!state.scopes.every((s) => scopes.includes(normalScope(s))))
|
|
828
|
+
if (!state.scopes.every((s) => scopes.includes(normalScope(s)))) {
|
|
829
|
+
abandonGrant(refreshToken, accessToken);
|
|
746
830
|
return back(request, state, "scope");
|
|
831
|
+
}
|
|
747
832
|
if ((config.requireRefreshToken ?? true) && (typeof refreshToken !== "string" || !refreshToken)) {
|
|
748
833
|
return back(
|
|
749
834
|
request,
|
|
@@ -754,16 +839,19 @@ ${refreshToken}`);
|
|
|
754
839
|
);
|
|
755
840
|
}
|
|
756
841
|
const idToken = typeof body["id_token"] === "string" ? body["id_token"] : void 0;
|
|
757
|
-
const
|
|
758
|
-
const read = idToken ? identityFromTokenEndpoint(idToken, {
|
|
759
|
-
issuers
|
|
842
|
+
const wantsIdentity = config.requireIdentity === true || asksOpenid(state.scopes);
|
|
843
|
+
const read = !wantsIdentity ? void 0 : idToken ? identityFromTokenEndpoint(idToken, {
|
|
844
|
+
issuers,
|
|
760
845
|
clientId: config.clientId,
|
|
846
|
+
trustedAudiences,
|
|
761
847
|
now: now()
|
|
762
|
-
}) : { reason: "no ID token
|
|
763
|
-
const identity = "subject" in read ? read : void 0;
|
|
764
|
-
const identityNote = "reason" in read ? `no identity: ${read.reason}` : void 0;
|
|
765
|
-
if (!identity && config.requireIdentity)
|
|
848
|
+
}) : { reason: "no ID token" };
|
|
849
|
+
const identity = read && "subject" in read ? read : void 0;
|
|
850
|
+
const identityNote = read && "reason" in read ? `no identity: ${read.reason}` : void 0;
|
|
851
|
+
if (!identity && config.requireIdentity) {
|
|
852
|
+
abandonGrant(refreshToken, accessToken);
|
|
766
853
|
return back(request, state, "failed", {}, identityNote);
|
|
854
|
+
}
|
|
767
855
|
let extra;
|
|
768
856
|
try {
|
|
769
857
|
const id = config.randomId?.() ?? crypto.randomUUID();
|
|
@@ -819,39 +907,7 @@ ${refreshToken}`);
|
|
|
819
907
|
async revoke(sealedRefreshToken, aad, options = {}) {
|
|
820
908
|
const refreshToken = await config.secrets.open(sealedRefreshToken, aad);
|
|
821
909
|
tokenCache.delete(await cacheKey(refreshToken));
|
|
822
|
-
|
|
823
|
-
const body = new URLSearchParams({
|
|
824
|
-
token: refreshToken,
|
|
825
|
-
token_type_hint: "refresh_token"
|
|
826
|
-
});
|
|
827
|
-
const headers = {
|
|
828
|
-
"content-type": "application/x-www-form-urlencoded",
|
|
829
|
-
accept: "application/json"
|
|
830
|
-
};
|
|
831
|
-
if (provider.revokeWithClientAuth) clientAuth(body, headers);
|
|
832
|
-
try {
|
|
833
|
-
const response = await timedFetch(
|
|
834
|
-
config,
|
|
835
|
-
provider.revokeUrl,
|
|
836
|
-
{ method: "POST", headers, body: body.toString() },
|
|
837
|
-
options.signal
|
|
838
|
-
);
|
|
839
|
-
let gone = response.ok;
|
|
840
|
-
if (!gone && response.status === 400) {
|
|
841
|
-
gone = errorCode(await jsonBody(response)) === "invalid_token";
|
|
842
|
-
} else {
|
|
843
|
-
discard(response);
|
|
844
|
-
}
|
|
845
|
-
log({
|
|
846
|
-
step: "revoke",
|
|
847
|
-
result: gone ? "revoked" : "failed",
|
|
848
|
-
...gone ? {} : { detail: `revoke endpoint answered ${response.status}` }
|
|
849
|
-
});
|
|
850
|
-
return gone;
|
|
851
|
-
} catch {
|
|
852
|
-
log({ step: "revoke", result: "unavailable" });
|
|
853
|
-
return false;
|
|
854
|
-
}
|
|
910
|
+
return revokeToken(refreshToken, "refresh_token", options.signal);
|
|
855
911
|
}
|
|
856
912
|
};
|
|
857
913
|
}
|