@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.d.cts
CHANGED
|
@@ -422,6 +422,53 @@ interface CreateSsoClientOptions {
|
|
|
422
422
|
}
|
|
423
423
|
declare function createSsoClient(config: SsoConfig, options?: CreateSsoClientOptions): SsoClient;
|
|
424
424
|
|
|
425
|
+
type PrincipalType = "human" | "service" | "agent";
|
|
426
|
+
/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */
|
|
427
|
+
interface TicketPrincipal {
|
|
428
|
+
/** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */
|
|
429
|
+
principal: string;
|
|
430
|
+
type: PrincipalType;
|
|
431
|
+
/** The client the ticket was issued to (`client_id`, falling back to `sub`). */
|
|
432
|
+
clientId: string;
|
|
433
|
+
/** The organisation, when BID stated one. */
|
|
434
|
+
org: string | null;
|
|
435
|
+
/** On delegation: who the caller is acting for (RFC 8693 `act.sub`). */
|
|
436
|
+
act: {
|
|
437
|
+
sub: string;
|
|
438
|
+
} | null;
|
|
439
|
+
scopes: string[];
|
|
440
|
+
/** Seconds since the epoch. */
|
|
441
|
+
exp: number;
|
|
442
|
+
jti: string;
|
|
443
|
+
}
|
|
444
|
+
interface TicketVerifierOptions {
|
|
445
|
+
/** BID's issuer, exactly as it signs `iss` — e.g. `https://id.broberg.ai`. */
|
|
446
|
+
issuer: string;
|
|
447
|
+
/** THIS receiver's audience — its own URL, e.g. `https://discovery.broberg.ai`. */
|
|
448
|
+
audience: string;
|
|
449
|
+
/** The key set URL. Omitted: read once from the issuer's discovery document. */
|
|
450
|
+
jwksUri?: string;
|
|
451
|
+
/**
|
|
452
|
+
* The longest lifetime (exp − iat) a ticket may claim. BID issues 5 min
|
|
453
|
+
* (service) and 15 min (agent); anything longer was not minted by the policy
|
|
454
|
+
* we were told about, so it is refused rather than trusted for longer.
|
|
455
|
+
*/
|
|
456
|
+
maxLifetimeSec?: number;
|
|
457
|
+
/** Seconds of clock skew to allow on exp/iat. Default 30. */
|
|
458
|
+
clockToleranceSec?: number;
|
|
459
|
+
/** Fetch the key set at construction, so the first caller does not wait. */
|
|
460
|
+
warmUp?: boolean;
|
|
461
|
+
fetchImpl?: typeof fetch;
|
|
462
|
+
minRefetchIntervalMs?: number;
|
|
463
|
+
}
|
|
464
|
+
interface TicketVerifier {
|
|
465
|
+
/** Verify a ticket; with `scope`, also require that scope to be granted. */
|
|
466
|
+
verify(token: string, options?: {
|
|
467
|
+
scope?: string | string[];
|
|
468
|
+
}): Promise<TicketPrincipal>;
|
|
469
|
+
}
|
|
470
|
+
declare function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier;
|
|
471
|
+
|
|
425
472
|
/**
|
|
426
473
|
* The app's OWN session cookie — signed, not encrypted.
|
|
427
474
|
*
|
|
@@ -577,4 +624,4 @@ declare function memoryTokenStore(options?: {
|
|
|
577
624
|
readonly size: number;
|
|
578
625
|
};
|
|
579
626
|
|
|
580
|
-
export { AVATAR_TYPES, type AddressOwnership, type BeginLoginOptions, type BidProfile, type CreateSsoClientOptions, DEFAULT_SESSION_MAX_AGE, type Discovery, type InvitationOutcome, type InvitationResult, type InviteUsersInput, type JwksCache, type JwksCacheOptions, JwksError, JwksUnavailableError, JwksUnknownKeyError, type LoginResult, type LoginStart, type LogoutToken, MAX_AVATAR_BYTES, MAX_INVITATIONS, type MigrationState, type MigrationStatus, type SessionPayload, SsoAppApiError, SsoAvatarRejectedError, type SsoClaims, type SsoClient, type SsoConfig, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, type TokenSet, type TokenStore, cookieHeader, createJwksCache, createSsoClient, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
|
627
|
+
export { AVATAR_TYPES, type AddressOwnership, type BeginLoginOptions, type BidProfile, type CreateSsoClientOptions, DEFAULT_SESSION_MAX_AGE, type Discovery, type InvitationOutcome, type InvitationResult, type InviteUsersInput, type JwksCache, type JwksCacheOptions, JwksError, JwksUnavailableError, JwksUnknownKeyError, type LoginResult, type LoginStart, type LogoutToken, MAX_AVATAR_BYTES, MAX_INVITATIONS, type MigrationState, type MigrationStatus, type PrincipalType, type SessionPayload, SsoAppApiError, SsoAvatarRejectedError, type SsoClaims, type SsoClient, type SsoConfig, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, type TicketPrincipal, type TicketVerifier, type TicketVerifierOptions, type TokenSet, type TokenStore, cookieHeader, createJwksCache, createSsoClient, createTicketVerifier, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
package/dist/index.d.ts
CHANGED
|
@@ -422,6 +422,53 @@ interface CreateSsoClientOptions {
|
|
|
422
422
|
}
|
|
423
423
|
declare function createSsoClient(config: SsoConfig, options?: CreateSsoClientOptions): SsoClient;
|
|
424
424
|
|
|
425
|
+
type PrincipalType = "human" | "service" | "agent";
|
|
426
|
+
/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */
|
|
427
|
+
interface TicketPrincipal {
|
|
428
|
+
/** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */
|
|
429
|
+
principal: string;
|
|
430
|
+
type: PrincipalType;
|
|
431
|
+
/** The client the ticket was issued to (`client_id`, falling back to `sub`). */
|
|
432
|
+
clientId: string;
|
|
433
|
+
/** The organisation, when BID stated one. */
|
|
434
|
+
org: string | null;
|
|
435
|
+
/** On delegation: who the caller is acting for (RFC 8693 `act.sub`). */
|
|
436
|
+
act: {
|
|
437
|
+
sub: string;
|
|
438
|
+
} | null;
|
|
439
|
+
scopes: string[];
|
|
440
|
+
/** Seconds since the epoch. */
|
|
441
|
+
exp: number;
|
|
442
|
+
jti: string;
|
|
443
|
+
}
|
|
444
|
+
interface TicketVerifierOptions {
|
|
445
|
+
/** BID's issuer, exactly as it signs `iss` — e.g. `https://id.broberg.ai`. */
|
|
446
|
+
issuer: string;
|
|
447
|
+
/** THIS receiver's audience — its own URL, e.g. `https://discovery.broberg.ai`. */
|
|
448
|
+
audience: string;
|
|
449
|
+
/** The key set URL. Omitted: read once from the issuer's discovery document. */
|
|
450
|
+
jwksUri?: string;
|
|
451
|
+
/**
|
|
452
|
+
* The longest lifetime (exp − iat) a ticket may claim. BID issues 5 min
|
|
453
|
+
* (service) and 15 min (agent); anything longer was not minted by the policy
|
|
454
|
+
* we were told about, so it is refused rather than trusted for longer.
|
|
455
|
+
*/
|
|
456
|
+
maxLifetimeSec?: number;
|
|
457
|
+
/** Seconds of clock skew to allow on exp/iat. Default 30. */
|
|
458
|
+
clockToleranceSec?: number;
|
|
459
|
+
/** Fetch the key set at construction, so the first caller does not wait. */
|
|
460
|
+
warmUp?: boolean;
|
|
461
|
+
fetchImpl?: typeof fetch;
|
|
462
|
+
minRefetchIntervalMs?: number;
|
|
463
|
+
}
|
|
464
|
+
interface TicketVerifier {
|
|
465
|
+
/** Verify a ticket; with `scope`, also require that scope to be granted. */
|
|
466
|
+
verify(token: string, options?: {
|
|
467
|
+
scope?: string | string[];
|
|
468
|
+
}): Promise<TicketPrincipal>;
|
|
469
|
+
}
|
|
470
|
+
declare function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier;
|
|
471
|
+
|
|
425
472
|
/**
|
|
426
473
|
* The app's OWN session cookie — signed, not encrypted.
|
|
427
474
|
*
|
|
@@ -577,4 +624,4 @@ declare function memoryTokenStore(options?: {
|
|
|
577
624
|
readonly size: number;
|
|
578
625
|
};
|
|
579
626
|
|
|
580
|
-
export { AVATAR_TYPES, type AddressOwnership, type BeginLoginOptions, type BidProfile, type CreateSsoClientOptions, DEFAULT_SESSION_MAX_AGE, type Discovery, type InvitationOutcome, type InvitationResult, type InviteUsersInput, type JwksCache, type JwksCacheOptions, JwksError, JwksUnavailableError, JwksUnknownKeyError, type LoginResult, type LoginStart, type LogoutToken, MAX_AVATAR_BYTES, MAX_INVITATIONS, type MigrationState, type MigrationStatus, type SessionPayload, SsoAppApiError, SsoAvatarRejectedError, type SsoClaims, type SsoClient, type SsoConfig, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, type TokenSet, type TokenStore, cookieHeader, createJwksCache, createSsoClient, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
|
627
|
+
export { AVATAR_TYPES, type AddressOwnership, type BeginLoginOptions, type BidProfile, type CreateSsoClientOptions, DEFAULT_SESSION_MAX_AGE, type Discovery, type InvitationOutcome, type InvitationResult, type InviteUsersInput, type JwksCache, type JwksCacheOptions, JwksError, JwksUnavailableError, JwksUnknownKeyError, type LoginResult, type LoginStart, type LogoutToken, MAX_AVATAR_BYTES, MAX_INVITATIONS, type MigrationState, type MigrationStatus, type PrincipalType, type SessionPayload, SsoAppApiError, SsoAvatarRejectedError, type SsoClaims, type SsoClient, type SsoConfig, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, type TicketPrincipal, type TicketVerifier, type TicketVerifierOptions, type TokenSet, type TokenStore, cookieHeader, createJwksCache, createSsoClient, createTicketVerifier, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
package/dist/index.js
CHANGED
|
@@ -664,6 +664,103 @@ function createSsoClient(config, options = {}) {
|
|
|
664
664
|
}
|
|
665
665
|
};
|
|
666
666
|
}
|
|
667
|
+
var PRINCIPAL_TYPES = ["human", "service", "agent"];
|
|
668
|
+
function createTicketVerifier(options) {
|
|
669
|
+
const {
|
|
670
|
+
issuer,
|
|
671
|
+
audience,
|
|
672
|
+
maxLifetimeSec = 15 * 60,
|
|
673
|
+
clockToleranceSec = 30,
|
|
674
|
+
fetchImpl = fetch
|
|
675
|
+
} = options;
|
|
676
|
+
if (!issuer) throw new SsoError("createTicketVerifier needs an issuer");
|
|
677
|
+
if (!audience) throw new SsoError("createTicketVerifier needs this receiver's audience");
|
|
678
|
+
let cachePromise = null;
|
|
679
|
+
const cacheFor = (jwksUri) => createJwksCache({
|
|
680
|
+
jwksUri,
|
|
681
|
+
fetchImpl,
|
|
682
|
+
...options.warmUp ? { warmUp: true } : {},
|
|
683
|
+
...options.minRefetchIntervalMs !== void 0 ? { minRefetchIntervalMs: options.minRefetchIntervalMs } : {}
|
|
684
|
+
});
|
|
685
|
+
async function keys() {
|
|
686
|
+
cachePromise ??= (async () => {
|
|
687
|
+
if (options.jwksUri) return cacheFor(options.jwksUri);
|
|
688
|
+
const url = `${issuer}/.well-known/openid-configuration`;
|
|
689
|
+
let doc;
|
|
690
|
+
try {
|
|
691
|
+
const res = await fetchImpl(url);
|
|
692
|
+
if (!res.ok) throw new Error(`answered ${res.status}`);
|
|
693
|
+
doc = await res.json();
|
|
694
|
+
} catch (cause) {
|
|
695
|
+
throw new JwksUnavailableError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
696
|
+
}
|
|
697
|
+
if (doc.issuer !== issuer) {
|
|
698
|
+
throw new SsoError(`issuer is ${issuer} but ${url} says ${doc.issuer} \u2014 they must match exactly`);
|
|
699
|
+
}
|
|
700
|
+
if (!doc.jwks_uri) throw new SsoError(`${url} names no jwks_uri`);
|
|
701
|
+
return cacheFor(doc.jwks_uri);
|
|
702
|
+
})().catch((err) => {
|
|
703
|
+
cachePromise = null;
|
|
704
|
+
throw err;
|
|
705
|
+
});
|
|
706
|
+
return cachePromise;
|
|
707
|
+
}
|
|
708
|
+
if (options.warmUp) void keys().catch(() => {
|
|
709
|
+
});
|
|
710
|
+
return {
|
|
711
|
+
async verify(token, opts = {}) {
|
|
712
|
+
let header;
|
|
713
|
+
try {
|
|
714
|
+
header = decodeProtectedHeader(token);
|
|
715
|
+
} catch {
|
|
716
|
+
throw new SsoError("ticket is not a JWS");
|
|
717
|
+
}
|
|
718
|
+
if (!header.kid) throw new SsoError("ticket has no kid \u2014 cannot pick a signing key");
|
|
719
|
+
if (header.typ === "logout+jwt") throw new SsoError("a logout token is not a ticket");
|
|
720
|
+
const cache = await keys();
|
|
721
|
+
let payload;
|
|
722
|
+
try {
|
|
723
|
+
({ payload } = await jwtVerify(token, async () => cache.getKey(header.kid, header.alg ?? "EdDSA"), {
|
|
724
|
+
issuer,
|
|
725
|
+
audience,
|
|
726
|
+
algorithms: ALLOWED_ALGS,
|
|
727
|
+
clockTolerance: clockToleranceSec,
|
|
728
|
+
requiredClaims: ["exp", "iat", "jti", "sub"]
|
|
729
|
+
}));
|
|
730
|
+
} catch (e) {
|
|
731
|
+
if (e instanceof JwksUnavailableError) throw e;
|
|
732
|
+
throw new SsoError(`ticket rejected: ${e instanceof Error ? e.message : String(e)}`);
|
|
733
|
+
}
|
|
734
|
+
if ("events" in payload) throw new SsoError("a logout token is not a ticket");
|
|
735
|
+
if ("nonce" in payload) throw new SsoError("an ID token is not a ticket");
|
|
736
|
+
const type = payload.principal_type;
|
|
737
|
+
if (typeof type !== "string" || !PRINCIPAL_TYPES.includes(type)) {
|
|
738
|
+
throw new SsoError(`ticket has no valid principal_type (got ${JSON.stringify(type)})`);
|
|
739
|
+
}
|
|
740
|
+
const lifetime = payload.exp - payload.iat;
|
|
741
|
+
if (lifetime > maxLifetimeSec) {
|
|
742
|
+
throw new SsoError(`ticket claims a ${lifetime}s lifetime; at most ${maxLifetimeSec}s is accepted`);
|
|
743
|
+
}
|
|
744
|
+
if (typeof payload.sub !== "string" || payload.sub === "") throw new SsoError("ticket has no sub");
|
|
745
|
+
const scopes = typeof payload.scope === "string" ? payload.scope.split(" ").filter(Boolean) : [];
|
|
746
|
+
const required2 = opts.scope === void 0 ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];
|
|
747
|
+
const missing = required2.filter((s) => !scopes.includes(s));
|
|
748
|
+
if (missing.length) throw new SsoError(`ticket lacks scope ${missing.join(", ")}`);
|
|
749
|
+
const actRaw = payload.act;
|
|
750
|
+
const act = actRaw && typeof actRaw.sub === "string" && actRaw.sub ? { sub: actRaw.sub } : null;
|
|
751
|
+
return {
|
|
752
|
+
principal: payload.sub,
|
|
753
|
+
type,
|
|
754
|
+
clientId: typeof payload.client_id === "string" && payload.client_id ? payload.client_id : payload.sub,
|
|
755
|
+
org: typeof payload.org === "string" && payload.org ? payload.org : null,
|
|
756
|
+
act,
|
|
757
|
+
scopes,
|
|
758
|
+
exp: payload.exp,
|
|
759
|
+
jti: String(payload.jti)
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
}
|
|
667
764
|
|
|
668
765
|
// src/session.ts
|
|
669
766
|
var enc = new TextEncoder();
|
|
@@ -784,6 +881,6 @@ function memoryTokenStore(options = {}) {
|
|
|
784
881
|
};
|
|
785
882
|
}
|
|
786
883
|
|
|
787
|
-
export { AVATAR_TYPES, DEFAULT_SESSION_MAX_AGE, JwksError, JwksUnavailableError, JwksUnknownKeyError, MAX_AVATAR_BYTES, MAX_INVITATIONS, SsoAppApiError, SsoAvatarRejectedError, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, cookieHeader, createJwksCache, createSsoClient, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
|
884
|
+
export { AVATAR_TYPES, DEFAULT_SESSION_MAX_AGE, JwksError, JwksUnavailableError, JwksUnknownKeyError, MAX_AVATAR_BYTES, MAX_INVITATIONS, SsoAppApiError, SsoAvatarRejectedError, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, cookieHeader, createJwksCache, createSsoClient, createTicketVerifier, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
|
788
885
|
//# sourceMappingURL=index.js.map
|
|
789
886
|
//# sourceMappingURL=index.js.map
|