@broberg/sso 0.12.0 → 0.13.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 +29 -1
- package/dist/hono.cjs +5 -1
- package/dist/hono.cjs.map +1 -1
- package/dist/hono.js +5 -1
- package/dist/hono.js.map +1 -1
- package/dist/index.cjs +98 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -1
- package/dist/index.d.ts +48 -1
- package/dist/index.js +98 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -666,6 +666,103 @@ function createSsoClient(config, options = {}) {
|
|
|
666
666
|
}
|
|
667
667
|
};
|
|
668
668
|
}
|
|
669
|
+
var PRINCIPAL_TYPES = ["human", "service", "agent"];
|
|
670
|
+
function createTicketVerifier(options) {
|
|
671
|
+
const {
|
|
672
|
+
issuer,
|
|
673
|
+
audience,
|
|
674
|
+
maxLifetimeSec = 15 * 60,
|
|
675
|
+
clockToleranceSec = 30,
|
|
676
|
+
fetchImpl = fetch
|
|
677
|
+
} = options;
|
|
678
|
+
if (!issuer) throw new SsoError("createTicketVerifier needs an issuer");
|
|
679
|
+
if (!audience) throw new SsoError("createTicketVerifier needs this receiver's audience");
|
|
680
|
+
let cachePromise = null;
|
|
681
|
+
const cacheFor = (jwksUri) => createJwksCache({
|
|
682
|
+
jwksUri,
|
|
683
|
+
fetchImpl,
|
|
684
|
+
...options.warmUp ? { warmUp: true } : {},
|
|
685
|
+
...options.minRefetchIntervalMs !== void 0 ? { minRefetchIntervalMs: options.minRefetchIntervalMs } : {}
|
|
686
|
+
});
|
|
687
|
+
async function keys() {
|
|
688
|
+
cachePromise ??= (async () => {
|
|
689
|
+
if (options.jwksUri) return cacheFor(options.jwksUri);
|
|
690
|
+
const url = `${issuer}/.well-known/openid-configuration`;
|
|
691
|
+
let doc;
|
|
692
|
+
try {
|
|
693
|
+
const res = await fetchImpl(url);
|
|
694
|
+
if (!res.ok) throw new Error(`answered ${res.status}`);
|
|
695
|
+
doc = await res.json();
|
|
696
|
+
} catch (cause) {
|
|
697
|
+
throw new JwksUnavailableError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
698
|
+
}
|
|
699
|
+
if (doc.issuer !== issuer) {
|
|
700
|
+
throw new SsoError(`issuer is ${issuer} but ${url} says ${doc.issuer} \u2014 they must match exactly`);
|
|
701
|
+
}
|
|
702
|
+
if (!doc.jwks_uri) throw new SsoError(`${url} names no jwks_uri`);
|
|
703
|
+
return cacheFor(doc.jwks_uri);
|
|
704
|
+
})().catch((err) => {
|
|
705
|
+
cachePromise = null;
|
|
706
|
+
throw err;
|
|
707
|
+
});
|
|
708
|
+
return cachePromise;
|
|
709
|
+
}
|
|
710
|
+
if (options.warmUp) void keys().catch(() => {
|
|
711
|
+
});
|
|
712
|
+
return {
|
|
713
|
+
async verify(token, opts = {}) {
|
|
714
|
+
let header;
|
|
715
|
+
try {
|
|
716
|
+
header = jose.decodeProtectedHeader(token);
|
|
717
|
+
} catch {
|
|
718
|
+
throw new SsoError("ticket is not a JWS");
|
|
719
|
+
}
|
|
720
|
+
if (!header.kid) throw new SsoError("ticket has no kid \u2014 cannot pick a signing key");
|
|
721
|
+
if (header.typ === "logout+jwt") throw new SsoError("a logout token is not a ticket");
|
|
722
|
+
const cache = await keys();
|
|
723
|
+
let payload;
|
|
724
|
+
try {
|
|
725
|
+
({ payload } = await jose.jwtVerify(token, async () => cache.getKey(header.kid, header.alg ?? "EdDSA"), {
|
|
726
|
+
issuer,
|
|
727
|
+
audience,
|
|
728
|
+
algorithms: ALLOWED_ALGS,
|
|
729
|
+
clockTolerance: clockToleranceSec,
|
|
730
|
+
requiredClaims: ["exp", "iat", "jti", "sub"]
|
|
731
|
+
}));
|
|
732
|
+
} catch (e) {
|
|
733
|
+
if (e instanceof JwksUnavailableError) throw e;
|
|
734
|
+
throw new SsoError(`ticket rejected: ${e instanceof Error ? e.message : String(e)}`);
|
|
735
|
+
}
|
|
736
|
+
if ("events" in payload) throw new SsoError("a logout token is not a ticket");
|
|
737
|
+
if ("nonce" in payload) throw new SsoError("an ID token is not a ticket");
|
|
738
|
+
const type = payload.principal_type;
|
|
739
|
+
if (typeof type !== "string" || !PRINCIPAL_TYPES.includes(type)) {
|
|
740
|
+
throw new SsoError(`ticket has no valid principal_type (got ${JSON.stringify(type)})`);
|
|
741
|
+
}
|
|
742
|
+
const lifetime = payload.exp - payload.iat;
|
|
743
|
+
if (lifetime > maxLifetimeSec) {
|
|
744
|
+
throw new SsoError(`ticket claims a ${lifetime}s lifetime; at most ${maxLifetimeSec}s is accepted`);
|
|
745
|
+
}
|
|
746
|
+
if (typeof payload.sub !== "string" || payload.sub === "") throw new SsoError("ticket has no sub");
|
|
747
|
+
const scopes = typeof payload.scope === "string" ? payload.scope.split(" ").filter(Boolean) : [];
|
|
748
|
+
const required2 = opts.scope === void 0 ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];
|
|
749
|
+
const missing = required2.filter((s) => !scopes.includes(s));
|
|
750
|
+
if (missing.length) throw new SsoError(`ticket lacks scope ${missing.join(", ")}`);
|
|
751
|
+
const actRaw = payload.act;
|
|
752
|
+
const act = actRaw && typeof actRaw.sub === "string" && actRaw.sub ? { sub: actRaw.sub } : null;
|
|
753
|
+
return {
|
|
754
|
+
principal: payload.sub,
|
|
755
|
+
type,
|
|
756
|
+
clientId: typeof payload.client_id === "string" && payload.client_id ? payload.client_id : payload.sub,
|
|
757
|
+
org: typeof payload.org === "string" && payload.org ? payload.org : null,
|
|
758
|
+
act,
|
|
759
|
+
scopes,
|
|
760
|
+
exp: payload.exp,
|
|
761
|
+
jti: String(payload.jti)
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
}
|
|
669
766
|
|
|
670
767
|
// src/session.ts
|
|
671
768
|
var enc = new TextEncoder();
|
|
@@ -802,6 +899,7 @@ exports.SsoReauthError = SsoReauthError;
|
|
|
802
899
|
exports.cookieHeader = cookieHeader;
|
|
803
900
|
exports.createJwksCache = createJwksCache;
|
|
804
901
|
exports.createSsoClient = createSsoClient;
|
|
902
|
+
exports.createTicketVerifier = createTicketVerifier;
|
|
805
903
|
exports.loadSsoConfig = loadSsoConfig;
|
|
806
904
|
exports.memoryTokenStore = memoryTokenStore;
|
|
807
905
|
exports.readCookie = readCookie;
|