@relyper/sp-auth 0.1.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.
Files changed (44) hide show
  1. package/README.md +212 -69
  2. package/dist/client.d.ts +33 -1
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +57 -4
  5. package/dist/client.js.map +1 -1
  6. package/dist/coins.d.ts +118 -0
  7. package/dist/coins.d.ts.map +1 -0
  8. package/dist/coins.js +158 -0
  9. package/dist/coins.js.map +1 -0
  10. package/dist/oidc/claims.d.ts +4 -0
  11. package/dist/oidc/claims.d.ts.map +1 -0
  12. package/dist/oidc/claims.js +50 -0
  13. package/dist/oidc/claims.js.map +1 -0
  14. package/dist/oidc/client.d.ts +44 -0
  15. package/dist/oidc/client.d.ts.map +1 -0
  16. package/dist/oidc/client.js +340 -0
  17. package/dist/oidc/client.js.map +1 -0
  18. package/dist/oidc/discovery.d.ts +15 -0
  19. package/dist/oidc/discovery.d.ts.map +1 -0
  20. package/dist/oidc/discovery.js +90 -0
  21. package/dist/oidc/discovery.js.map +1 -0
  22. package/dist/oidc/pkce.d.ts +6 -0
  23. package/dist/oidc/pkce.d.ts.map +1 -0
  24. package/dist/oidc/pkce.js +26 -0
  25. package/dist/oidc/pkce.js.map +1 -0
  26. package/dist/oidc/session.d.ts +47 -0
  27. package/dist/oidc/session.d.ts.map +1 -0
  28. package/dist/oidc/session.js +119 -0
  29. package/dist/oidc/session.js.map +1 -0
  30. package/dist/oidc/types.d.ts +130 -0
  31. package/dist/oidc/types.d.ts.map +1 -0
  32. package/dist/oidc/types.js +17 -0
  33. package/dist/oidc/types.js.map +1 -0
  34. package/dist/oidc-fastify.d.ts +123 -0
  35. package/dist/oidc-fastify.d.ts.map +1 -0
  36. package/dist/oidc-fastify.js +264 -0
  37. package/dist/oidc-fastify.js.map +1 -0
  38. package/dist/oidc.d.ts +15 -0
  39. package/dist/oidc.d.ts.map +1 -0
  40. package/dist/oidc.js +14 -0
  41. package/dist/oidc.js.map +1 -0
  42. package/dist/types.d.ts +13 -1
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +25 -5
@@ -0,0 +1,15 @@
1
+ import { type OidcDiscoveryDocument } from './types.js';
2
+ export type DiscoveryLoaderOptions = {
3
+ issuer: string;
4
+ ttlMs: number;
5
+ requestTimeoutMs: number;
6
+ fetch: typeof globalThis.fetch;
7
+ };
8
+ export type DiscoveryLoader = {
9
+ load(): Promise<OidcDiscoveryDocument>;
10
+ /** Drops the cache, e.g. after a JWKS lookup failed against a rotated IdP. */
11
+ invalidate(): void;
12
+ };
13
+ export declare function discoveryUrl(issuer: string): string;
14
+ export declare function createDiscoveryLoader(options: DiscoveryLoaderOptions): DiscoveryLoader;
15
+ //# sourceMappingURL=discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/oidc/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAY1E,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACvC,8EAA8E;IAC9E,UAAU,IAAI,IAAI,CAAC;CACpB,CAAC;AAEF,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe,CA8EtF"}
@@ -0,0 +1,90 @@
1
+ import { RelyperOidcError } from './types.js';
2
+ /**
3
+ * Reads and caches the IdP's discovery document.
4
+ *
5
+ * The endpoints are never hardcoded: an IdP is free to move them, and the
6
+ * document is the contract. Concurrent callers share one in-flight request so a
7
+ * burst of logins does not turn into a burst of requests against the IdP.
8
+ */
9
+ const REQUIRED_FIELDS = ['issuer', 'authorization_endpoint', 'token_endpoint', 'jwks_uri'];
10
+ export function discoveryUrl(issuer) {
11
+ const base = issuer.replace(/\/+$/, '');
12
+ return base + '/.well-known/openid-configuration';
13
+ }
14
+ export function createDiscoveryLoader(options) {
15
+ let cached = null;
16
+ let inFlight = null;
17
+ async function fetchDocument() {
18
+ const url = discoveryUrl(options.issuer);
19
+ let response;
20
+ try {
21
+ response = await options.fetch(url, {
22
+ method: 'GET',
23
+ headers: { accept: 'application/json' },
24
+ signal: AbortSignal.timeout(options.requestTimeoutMs)
25
+ });
26
+ }
27
+ catch (cause) {
28
+ throw new RelyperOidcError('discovery_failed', 'The identity provider could not be reached.', {
29
+ cause,
30
+ detail: 'GET ' + url
31
+ });
32
+ }
33
+ if (!response.ok) {
34
+ throw new RelyperOidcError('discovery_failed', 'The identity provider could not be reached.', {
35
+ detail: 'GET ' + url + ' returned HTTP ' + response.status
36
+ });
37
+ }
38
+ let document;
39
+ try {
40
+ document = (await response.json());
41
+ }
42
+ catch (cause) {
43
+ throw new RelyperOidcError('discovery_failed', 'The identity provider returned a malformed discovery document.', {
44
+ cause,
45
+ detail: 'GET ' + url
46
+ });
47
+ }
48
+ for (const field of REQUIRED_FIELDS) {
49
+ if (typeof document?.[field] !== 'string' || !document[field]) {
50
+ throw new RelyperOidcError('discovery_failed', 'The discovery document is incomplete.', {
51
+ detail: 'missing field: ' + field
52
+ });
53
+ }
54
+ }
55
+ // The issuer is the identity of the IdP and is compared against the `iss`
56
+ // claim later. A document that names a different issuer than the one we
57
+ // configured means we are talking to the wrong party.
58
+ if (trimSlash(document.issuer) !== trimSlash(options.issuer)) {
59
+ throw new RelyperOidcError('discovery_failed', 'The identity provider reports a different issuer.', {
60
+ detail: 'configured ' + options.issuer + ', document ' + document.issuer
61
+ });
62
+ }
63
+ return document;
64
+ }
65
+ return {
66
+ async load() {
67
+ const now = Date.now();
68
+ if (cached && cached.expiresAt > now)
69
+ return cached.document;
70
+ if (inFlight)
71
+ return inFlight;
72
+ inFlight = fetchDocument()
73
+ .then((document) => {
74
+ cached = { document, expiresAt: Date.now() + options.ttlMs };
75
+ return document;
76
+ })
77
+ .finally(() => {
78
+ inFlight = null;
79
+ });
80
+ return inFlight;
81
+ },
82
+ invalidate() {
83
+ cached = null;
84
+ }
85
+ };
86
+ }
87
+ function trimSlash(value) {
88
+ return value.replace(/\/+$/, '');
89
+ }
90
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/oidc/discovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export declare function createCodeVerifier(): string;
2
+ export declare function createState(): string;
3
+ export declare function createNonce(): string;
4
+ /** S256 challenge for a verifier. `plain` is deliberately not offered. */
5
+ export declare function codeChallengeFor(verifier: string): string;
6
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/oidc/pkce.ts"],"names":[],"mappings":"AAeA,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEzD"}
@@ -0,0 +1,26 @@
1
+ import { randomBytes, createHash } from 'node:crypto';
2
+ /**
3
+ * Client side of Proof Key for Code Exchange (RFC 7636) plus the random values
4
+ * that protect the authorization request: `state` against CSRF on the callback,
5
+ * `nonce` against a replayed ID token.
6
+ *
7
+ * All three are 256 bits of CSPRNG output rendered as base64url, which lands in
8
+ * the 43 characters RFC 7636 asks for as a minimum.
9
+ */
10
+ function randomUrlSafe(byteLength = 32) {
11
+ return randomBytes(byteLength).toString('base64url');
12
+ }
13
+ export function createCodeVerifier() {
14
+ return randomUrlSafe(32);
15
+ }
16
+ export function createState() {
17
+ return randomUrlSafe(32);
18
+ }
19
+ export function createNonce() {
20
+ return randomUrlSafe(32);
21
+ }
22
+ /** S256 challenge for a verifier. `plain` is deliberately not offered. */
23
+ export function codeChallengeFor(verifier) {
24
+ return createHash('sha256').update(verifier).digest('base64url');
25
+ }
26
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../src/oidc/pkce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEtD;;;;;;;GAOG;AAEH,SAAS,aAAa,CAAC,UAAU,GAAG,EAAE;IACpC,OAAO,WAAW,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,aAAa,CAAC,EAAE,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,aAAa,CAAC,EAAE,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,aAAa,CAAC,EAAE,CAAC,CAAC;AAC3B,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AACnE,CAAC"}
@@ -0,0 +1,47 @@
1
+ import type { RelyperIdentity } from '../types.js';
2
+ import type { RelyperLoginTransaction } from './types.js';
3
+ export type SessionPayload = {
4
+ identity: RelyperIdentity;
5
+ /** Seconds since the epoch at which the IdP login happened. */
6
+ authenticatedAt: number;
7
+ /**
8
+ * Identifier of this login. The session itself is stateless, so this exists
9
+ * for the application: recording it at logout and rejecting it afterwards is
10
+ * what turns "the browser dropped its cookie" into a revocation that also
11
+ * stops a cookie someone copied beforehand.
12
+ */
13
+ sessionId: string;
14
+ /**
15
+ * ID token, kept only when `keepIdToken` is on -- it is needed as the
16
+ * `id_token_hint` of an RP-initiated logout and for nothing else.
17
+ */
18
+ idToken?: string;
19
+ };
20
+ /** Opaque, unguessable identifier for one login. */
21
+ export declare function createSessionId(): string;
22
+ export type SealedCodec<T> = {
23
+ seal(value: T, ttlSeconds: number): Promise<string>;
24
+ /** Returns null for anything that is not a valid, unexpired token of this codec. */
25
+ open(token: string | undefined | null): Promise<T | null>;
26
+ };
27
+ export declare function createSessionCodec(secret: string): SealedCodec<SessionPayload>;
28
+ export declare function createLoginCodec(secret: string): SealedCodec<RelyperLoginTransaction>;
29
+ /** Constant-time string comparison for CSRF-style tokens of equal expected length. */
30
+ export declare function safeEqual(a: string, b: string): boolean;
31
+ export type CookieOptions = {
32
+ path?: string;
33
+ domain?: string;
34
+ maxAgeSeconds?: number;
35
+ httpOnly?: boolean;
36
+ secure?: boolean;
37
+ sameSite?: 'Lax' | 'Strict' | 'None';
38
+ };
39
+ /**
40
+ * Minimal cookie serialisation, so consumers are not forced to install a cookie
41
+ * plugin just to use this package.
42
+ */
43
+ export declare function serializeCookie(name: string, value: string, options?: CookieOptions): string;
44
+ /** Cookie header that deletes `name`. */
45
+ export declare function clearCookie(name: string, options?: CookieOptions): string;
46
+ export declare function parseCookies(header: string | undefined | null): Record<string, string>;
47
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/oidc/session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAwB1D,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,eAAe,CAAC;IAC1B,+DAA+D;IAC/D,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,oDAAoD;AACpD,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD,oFAAoF;IACpF,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;CAC3D,CAAC;AAqCF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAAC,cAAc,CAAC,CAE9E;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAAC,uBAAuB,CAAC,CAErF;AAED,sFAAsF;AACtF,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAKvD;AAID,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;CACtC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAchG;AAED,yCAAyC;AACzC,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAE7E;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBtF"}
@@ -0,0 +1,119 @@
1
+ import { hkdfSync, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { EncryptJWT, jwtDecrypt } from 'jose';
3
+ /**
4
+ * Cookie-backed state for the OIDC flow, with no server-side store.
5
+ *
6
+ * Two cookies are involved and both are encrypted, not merely signed:
7
+ *
8
+ * - the login cookie, which lives for the seconds between the redirect out and
9
+ * the callback and carries `state`, `nonce` and the PKCE `code_verifier`;
10
+ * - the session cookie, which carries the identity after a successful login.
11
+ *
12
+ * Encryption (JWE, direct A256GCM) rather than a signature means the browser
13
+ * never sees the verifier or the user's claims, and A256GCM authenticates the
14
+ * ciphertext, so tampering fails to decrypt instead of yielding a forged value.
15
+ *
16
+ * Both keys are derived from one application secret through HKDF with distinct
17
+ * info labels, so the two cookies can never be swapped for one another.
18
+ */
19
+ const MIN_SECRET_LENGTH = 32;
20
+ const HKDF_SALT = 'relyper-sp-auth/v1';
21
+ const SESSION_LABEL = 'session';
22
+ const LOGIN_LABEL = 'login-transaction';
23
+ /** Opaque, unguessable identifier for one login. */
24
+ export function createSessionId() {
25
+ return randomBytes(16).toString('base64url');
26
+ }
27
+ function deriveKey(secret, label) {
28
+ if (typeof secret !== 'string' || secret.length < MIN_SECRET_LENGTH) {
29
+ throw new TypeError('@relyper/sp-auth/oidc: the session secret must be at least ' + MIN_SECRET_LENGTH + ' characters.');
30
+ }
31
+ return new Uint8Array(hkdfSync('sha256', Buffer.from(secret, 'utf8'), HKDF_SALT, label, 32));
32
+ }
33
+ function createCodec(secret, label) {
34
+ const key = deriveKey(secret, label);
35
+ return {
36
+ async seal(value, ttlSeconds) {
37
+ return new EncryptJWT(value)
38
+ .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
39
+ .setIssuedAt()
40
+ .setExpirationTime(Math.floor(Date.now() / 1000) + ttlSeconds)
41
+ .encrypt(key);
42
+ },
43
+ async open(token) {
44
+ if (!token)
45
+ return null;
46
+ try {
47
+ const { payload } = await jwtDecrypt(token, key, { clockTolerance: 5 });
48
+ return payload;
49
+ }
50
+ catch {
51
+ // Expired, tampered with, or sealed by an older secret. All of these mean
52
+ // the same thing to the caller: there is no usable state here.
53
+ return null;
54
+ }
55
+ }
56
+ };
57
+ }
58
+ export function createSessionCodec(secret) {
59
+ return createCodec(secret, SESSION_LABEL);
60
+ }
61
+ export function createLoginCodec(secret) {
62
+ return createCodec(secret, LOGIN_LABEL);
63
+ }
64
+ /** Constant-time string comparison for CSRF-style tokens of equal expected length. */
65
+ export function safeEqual(a, b) {
66
+ const left = Buffer.from(String(a), 'utf8');
67
+ const right = Buffer.from(String(b), 'utf8');
68
+ if (left.length !== right.length)
69
+ return false;
70
+ return timingSafeEqual(left, right);
71
+ }
72
+ /**
73
+ * Minimal cookie serialisation, so consumers are not forced to install a cookie
74
+ * plugin just to use this package.
75
+ */
76
+ export function serializeCookie(name, value, options = {}) {
77
+ const parts = [name + '=' + encodeURIComponent(value)];
78
+ parts.push('Path=' + (options.path ?? '/'));
79
+ if (options.domain)
80
+ parts.push('Domain=' + options.domain);
81
+ if (typeof options.maxAgeSeconds === 'number') {
82
+ parts.push('Max-Age=' + Math.max(0, Math.floor(options.maxAgeSeconds)));
83
+ // Expires alongside Max-Age for the benefit of clients that ignore the latter.
84
+ const expires = new Date(Date.now() + Math.max(0, options.maxAgeSeconds) * 1000);
85
+ parts.push('Expires=' + expires.toUTCString());
86
+ }
87
+ if (options.httpOnly !== false)
88
+ parts.push('HttpOnly');
89
+ if (options.secure)
90
+ parts.push('Secure');
91
+ parts.push('SameSite=' + (options.sameSite ?? 'Lax'));
92
+ return parts.join('; ');
93
+ }
94
+ /** Cookie header that deletes `name`. */
95
+ export function clearCookie(name, options = {}) {
96
+ return serializeCookie(name, '', { ...options, maxAgeSeconds: 0 });
97
+ }
98
+ export function parseCookies(header) {
99
+ const result = {};
100
+ if (!header)
101
+ return result;
102
+ for (const part of header.split(';')) {
103
+ const index = part.indexOf('=');
104
+ if (index < 1)
105
+ continue;
106
+ const name = part.slice(0, index).trim();
107
+ if (!name || name in result)
108
+ continue;
109
+ const raw = part.slice(index + 1).trim();
110
+ try {
111
+ result[name] = decodeURIComponent(raw);
112
+ }
113
+ catch {
114
+ result[name] = raw;
115
+ }
116
+ }
117
+ return result;
118
+ }
119
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/oidc/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,130 @@
1
+ import type { RelyperIdentity } from '../types.js';
2
+ /** Subset of the OIDC discovery document this client relies on. */
3
+ export type OidcDiscoveryDocument = {
4
+ issuer: string;
5
+ authorization_endpoint: string;
6
+ token_endpoint: string;
7
+ jwks_uri: string;
8
+ userinfo_endpoint?: string;
9
+ end_session_endpoint?: string;
10
+ code_challenge_methods_supported?: string[];
11
+ token_endpoint_auth_methods_supported?: string[];
12
+ scopes_supported?: string[];
13
+ };
14
+ export type TokenEndpointAuthMethod = 'client_secret_basic' | 'client_secret_post';
15
+ export type RelyperOidcOptions = {
16
+ /**
17
+ * Base URL of the Relyper IdP, exactly as it appears in the `iss` claim.
18
+ * Discovery is read from `<issuer>/.well-known/openid-configuration`.
19
+ */
20
+ issuer: string;
21
+ /** Client ID registered at the IdP for this service provider. */
22
+ clientId: string;
23
+ /** Client secret generated at the IdP. Never ships to the browser. */
24
+ clientSecret: string;
25
+ /** Must match one of the redirect URIs registered for this client, byte for byte. */
26
+ redirectUri: string;
27
+ /** Default: `openid email profile roles tenant teams`. */
28
+ scope?: string;
29
+ /**
30
+ * How this client authenticates at the token endpoint. Default: whichever of
31
+ * the two the discovery document advertises, preferring `client_secret_basic`.
32
+ */
33
+ tokenEndpointAuthMethod?: TokenEndpointAuthMethod;
34
+ /** Role(s) required for this service provider. Empty means: no role check. */
35
+ requiredRole?: string | string[];
36
+ /** With several required roles: one suffices ('any', default) or all are needed ('all'). */
37
+ roleMatch?: 'any' | 'all';
38
+ /** Default: true. Set to false if the IdP does not supply an e-mail address. */
39
+ requireEmail?: boolean;
40
+ /**
41
+ * Also call the UserInfo endpoint after the token exchange. Default: false,
42
+ * because the Relyper IdP already puts every claim into the ID token. Turn it
43
+ * on if you need claims the ID token does not carry.
44
+ */
45
+ useUserInfo?: boolean;
46
+ /** Leeway for `exp`/`iat`/`nbf` in seconds. Default: 60. */
47
+ clockToleranceSeconds?: number;
48
+ /** How long a discovery document is reused, in milliseconds. Default: 3600000 (1h). */
49
+ discoveryTtlMs?: number;
50
+ /** Timeout for every call to the IdP, in milliseconds. Default: 10000. */
51
+ requestTimeoutMs?: number;
52
+ /** Custom fetch implementation, e.g. for tests or a proxy-aware agent. */
53
+ fetch?: typeof globalThis.fetch;
54
+ /** Maps IdP claims onto the identity. Default: {@link defaultClaimsToIdentity}. */
55
+ mapClaims?: (claims: Record<string, unknown>) => RelyperIdentity;
56
+ };
57
+ export type ResolvedRelyperOidcOptions = {
58
+ issuer: string;
59
+ clientId: string;
60
+ clientSecret: string;
61
+ redirectUri: string;
62
+ scope: string;
63
+ tokenEndpointAuthMethod: TokenEndpointAuthMethod | null;
64
+ requiredRoles: string[];
65
+ roleMatch: 'any' | 'all';
66
+ requireEmail: boolean;
67
+ useUserInfo: boolean;
68
+ clockToleranceSeconds: number;
69
+ discoveryTtlMs: number;
70
+ requestTimeoutMs: number;
71
+ };
72
+ /** State this client must remember between the redirect out and the callback. */
73
+ export type RelyperLoginTransaction = {
74
+ state: string;
75
+ nonce: string;
76
+ codeVerifier: string;
77
+ /** Application path to return to after the login. Always a local path. */
78
+ returnTo: string;
79
+ };
80
+ export type RelyperAuthorizationRequest = {
81
+ /** The IdP URL the browser has to be redirected to. */
82
+ url: string;
83
+ transaction: RelyperLoginTransaction;
84
+ };
85
+ export type RelyperTokenSet = {
86
+ accessToken: string;
87
+ idToken: string;
88
+ tokenType: string;
89
+ expiresIn: number | null;
90
+ refreshToken: string | null;
91
+ scope: string | null;
92
+ };
93
+ export type RelyperLoginResult = {
94
+ identity: RelyperIdentity;
95
+ claims: Record<string, unknown>;
96
+ tokens: RelyperTokenSet;
97
+ };
98
+ export type RelyperOidcErrorCode =
99
+ /** The IdP redirected back with an `error` parameter. */
100
+ 'idp_error'
101
+ /** `state` missing, unknown, or not the one this browser started with. */
102
+ | 'invalid_state'
103
+ /** No login transaction cookie -- expired, or a callback nobody started. */
104
+ | 'missing_transaction'
105
+ /** The IdP refused the code exchange. */
106
+ | 'token_exchange_failed'
107
+ /** The ID token failed signature, issuer, audience, nonce, or expiry checks. */
108
+ | 'invalid_id_token'
109
+ /** UserInfo could not be read. */
110
+ | 'userinfo_failed'
111
+ /** Discovery document or JWKS unreachable or malformed. */
112
+ | 'discovery_failed'
113
+ /** Authenticated, but without the role this service provider requires. */
114
+ | 'missing_role'
115
+ /** The IdP supplied no e-mail address although one is required. */
116
+ | 'missing_email';
117
+ export declare class RelyperOidcError extends Error {
118
+ readonly code: RelyperOidcErrorCode;
119
+ /** HTTP status an adapter should answer with. */
120
+ readonly status: number;
121
+ readonly cause?: unknown;
122
+ /** Safe to log: never contains tokens or the client secret. */
123
+ readonly detail?: string;
124
+ constructor(code: RelyperOidcErrorCode, message: string, options?: {
125
+ status?: number;
126
+ cause?: unknown;
127
+ detail?: string;
128
+ });
129
+ }
130
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/oidc/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ export class RelyperOidcError extends Error {
2
+ code;
3
+ /** HTTP status an adapter should answer with. */
4
+ status;
5
+ cause;
6
+ /** Safe to log: never contains tokens or the client secret. */
7
+ detail;
8
+ constructor(code, message, options = {}) {
9
+ super(message);
10
+ this.name = 'RelyperOidcError';
11
+ this.code = code;
12
+ this.status = options.status ?? 502;
13
+ this.cause = options.cause;
14
+ this.detail = options.detail;
15
+ }
16
+ }
17
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/oidc/types.ts"],"names":[],"mappings":"AAkIA,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAChC,IAAI,CAAuB;IACpC,iDAAiD;IACxC,MAAM,CAAS;IACf,KAAK,CAAW;IACzB,+DAA+D;IACtD,MAAM,CAAU;IAEzB,YACE,IAA0B,EAC1B,OAAe,EACf,OAAO,GAA0D,EAAE;QAEnE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;QAC/B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC;QACpC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC/B,CAAC;CACF"}
@@ -0,0 +1,123 @@
1
+ import type { FastifyInstance, FastifyRequest } from 'fastify';
2
+ import { type RelyperOidcClient } from './oidc/client.js';
3
+ import { type SessionPayload } from './oidc/session.js';
4
+ import { type RelyperLoginResult, type RelyperOidcOptions } from './oidc/types.js';
5
+ import type { RelyperIdentity } from './types.js';
6
+ /**
7
+ * Fastify adapter for the OIDC login against the Relyper IdP.
8
+ *
9
+ * Registers the three routes a browser-facing service provider needs -- start
10
+ * the login, receive the callback, end the session -- and guards the rest of the
11
+ * application with the resulting session cookie.
12
+ */
13
+ declare module 'fastify' {
14
+ interface FastifyRequest {
15
+ /** Identity from the verified ID token. Set once a protected route has passed the guard. */
16
+ relyperIdentity: RelyperIdentity;
17
+ /** Raw session as it was sealed into the cookie. */
18
+ relyperSession: SessionPayload | null;
19
+ }
20
+ interface FastifyInstance {
21
+ relyperOidc: RelyperOidcClient;
22
+ }
23
+ }
24
+ export type RelyperOidcFastifyOptions = RelyperOidcOptions & {
25
+ /**
26
+ * Secret this application seals its cookies with, at least 32 characters.
27
+ * Unrelated to the client secret and never shared with the IdP. Rotating it
28
+ * invalidates every open session.
29
+ */
30
+ sessionSecret: string;
31
+ /** Default: 'relyper_session'. */
32
+ sessionCookieName?: string;
33
+ /** Default: 'relyper_login'. */
34
+ loginCookieName?: string;
35
+ cookieDomain?: string;
36
+ cookiePath?: string;
37
+ /**
38
+ * `Secure` flag. Default: on, unless NODE_ENV is 'development' or 'test'.
39
+ * Leaving this on over plain HTTP means the browser silently drops the cookie.
40
+ */
41
+ cookieSecure?: boolean;
42
+ /** Lifetime of a session in seconds. Default: 28800 (8 hours). */
43
+ sessionTtlSeconds?: number;
44
+ /** Lifetime of the login handshake in seconds. Default: 600 (10 minutes). */
45
+ loginTtlSeconds?: number;
46
+ /**
47
+ * Re-issue the session cookie on activity so an active user is not logged out
48
+ * mid-session. Default: true. The IdP login time is preserved either way.
49
+ */
50
+ rollingSession?: boolean;
51
+ /** Absolute lifetime in seconds a session can reach through rolling renewal. Default: 86400 (24h). */
52
+ sessionAbsoluteTtlSeconds?: number;
53
+ /** Keep the ID token in the session so RP-initiated logout can pass it. Default: false. */
54
+ keepIdToken?: boolean;
55
+ /** Default: '/auth/login'. */
56
+ loginPath?: string;
57
+ /** Default: '/auth/callback'. Must match the redirect URI registered at the IdP. */
58
+ callbackPath?: string;
59
+ /** Default: '/auth/logout'. Registered for both GET and POST. */
60
+ logoutPath?: string;
61
+ /** Local path to land on after logout. Default: '/'. */
62
+ postLogoutRedirect?: string;
63
+ /** Local path the callback redirects to when the login fails. Default: none, a JSON error is sent. */
64
+ loginErrorRedirect?: string;
65
+ /**
66
+ * Decides which requests the session guard covers. Default: everything except
67
+ * the login, callback and logout routes.
68
+ */
69
+ protect?: (request: FastifyRequest) => boolean;
70
+ /** Hook the guard runs in. Default: 'onRequest'. */
71
+ hook?: 'onRequest' | 'preHandler';
72
+ /**
73
+ * Translates the IdP identity into the application's own user object, usually
74
+ * an upsert into its database. Runs on every guarded request.
75
+ */
76
+ resolveUser?: (identity: RelyperIdentity, request: FastifyRequest) => unknown | Promise<unknown>;
77
+ /** Request property holding the result of `resolveUser`. Default: 'principal'. */
78
+ principalKey?: string;
79
+ /** Path for a /me route. Default: false. */
80
+ meRoute?: string | false;
81
+ /** Response of the /me route. Default: `{ user: request[principalKey] }`. */
82
+ meResponse?: (request: FastifyRequest) => unknown;
83
+ /**
84
+ * Whether an unauthenticated request is answered with a redirect to the login
85
+ * instead of a 401. Default: redirect for top-level HTML navigation, 401 for
86
+ * everything else, which is what a single-page app wants.
87
+ */
88
+ redirectUnauthenticated?: boolean | ((request: FastifyRequest) => boolean);
89
+ /** Body of a 401/403. Default: `{ error, code, loginUrl }`. */
90
+ errorBody?: (failure: RelyperAuthFailureInfo, request: FastifyRequest) => unknown;
91
+ /** Audit hook for rejected requests and failed logins. */
92
+ onAuthFailure?: (failure: RelyperAuthFailureInfo, request: FastifyRequest) => void | Promise<void>;
93
+ /**
94
+ * Called after a successful login, before the redirect. Receives the session id
95
+ * that identifies this login, so an application keeping a revocation list can
96
+ * record it here.
97
+ */
98
+ onLogin?: (result: RelyperLoginResult, request: FastifyRequest, sessionId: string) => void | Promise<void>;
99
+ /**
100
+ * Called when a session ends, with the id from {@link RelyperOidcFastifyOptions.onLogin}.
101
+ * Implement this together with `isSessionRevoked` to make logout binding: the
102
+ * session cookie itself is stateless, so clearing it only affects the browser
103
+ * that asked. Recording the id here and rejecting it afterwards also stops a
104
+ * cookie that was copied before the logout.
105
+ */
106
+ onLogout?: (sessionId: string, session: SessionPayload, request: FastifyRequest) => void | Promise<void>;
107
+ /**
108
+ * Consulted on every guarded request. Return true to refuse a session whose id
109
+ * has been revoked. Default: no revocation list, sessions live until they expire.
110
+ */
111
+ isSessionRevoked?: (sessionId: string, request: FastifyRequest) => boolean | Promise<boolean>;
112
+ };
113
+ export type RelyperAuthFailureInfo = {
114
+ status: number;
115
+ code: string;
116
+ message: string;
117
+ /** Safe to log; never contains tokens or secrets. */
118
+ detail?: string;
119
+ };
120
+ declare function plugin(app: FastifyInstance, options: RelyperOidcFastifyOptions): Promise<void>;
121
+ export declare const relyperOidcAuth: typeof plugin;
122
+ export default relyperOidcAuth;
123
+ //# sourceMappingURL=oidc-fastify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc-fastify.d.ts","sourceRoot":"","sources":["../src/oidc-fastify.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAgB,cAAc,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAyC,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACjG,OAAO,EAQL,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAoB,KAAK,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;;GAMG;AAEH,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,4FAA4F;QAC5F,eAAe,EAAE,eAAe,CAAC;QACjC,oDAAoD;QACpD,cAAc,EAAE,cAAc,GAAG,IAAI,CAAC;KACvC;IACD,UAAU,eAAe;QACvB,WAAW,EAAE,iBAAiB,CAAC;KAChC;CACF;AAED,MAAM,MAAM,yBAAyB,GAAG,kBAAkB,GAAG;IAC3D;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB,kCAAkC;IAClC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gCAAgC;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kEAAkE;IAClE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,6EAA6E;IAC7E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,sGAAsG;IACtG,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,2FAA2F;IAC3F,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,8BAA8B;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,sGAAsG;IACtG,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC;IAC/C,oDAAoD;IACpD,IAAI,CAAC,EAAE,WAAW,GAAG,YAAY,CAAC;IAClC;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,eAAe,EAAE,OAAO,EAAE,cAAc,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjG,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACzB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC;IAElD;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,CAAC;IAC3E,+DAA+D;IAC/D,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,EAAE,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC;IAClF,0DAA0D;IAC1D,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,EAAE,OAAO,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnG;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3G;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzG;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC/F,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAeF,iBAAe,MAAM,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoS7F;AAED,eAAO,MAAM,eAAe,eAG1B,CAAC;eAEY,eAAe"}