@seamless-auth/oidc-provider 0.0.0-stage → 0.1.1

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 (91) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +201 -0
  3. package/README.md +273 -2
  4. package/dist/adapter.d.ts +44 -0
  5. package/dist/adapter.js +249 -0
  6. package/dist/adapter.js.map +1 -0
  7. package/dist/apiClient.d.ts +35 -0
  8. package/dist/apiClient.js +217 -0
  9. package/dist/apiClient.js.map +1 -0
  10. package/dist/authRequirements.d.ts +37 -0
  11. package/dist/authRequirements.js +58 -0
  12. package/dist/authRequirements.js.map +1 -0
  13. package/dist/clientSecrets.d.ts +10 -0
  14. package/dist/clientSecrets.js +102 -0
  15. package/dist/clientSecrets.js.map +1 -0
  16. package/dist/configCache.d.ts +14 -0
  17. package/dist/configCache.js +45 -0
  18. package/dist/configCache.js.map +1 -0
  19. package/dist/createOidcEdge.d.ts +45 -0
  20. package/dist/createOidcEdge.js +132 -0
  21. package/dist/createOidcEdge.js.map +1 -0
  22. package/dist/deviceFlow.d.ts +53 -0
  23. package/dist/deviceFlow.js +168 -0
  24. package/dist/deviceFlow.js.map +1 -0
  25. package/dist/env.d.ts +16 -0
  26. package/dist/env.js +36 -0
  27. package/dist/env.js.map +1 -0
  28. package/dist/errors.d.ts +18 -0
  29. package/dist/errors.js +40 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/firstPartySession.d.ts +21 -0
  32. package/dist/firstPartySession.js +28 -0
  33. package/dist/firstPartySession.js.map +1 -0
  34. package/dist/hostedPages.d.ts +51 -0
  35. package/dist/hostedPages.js +172 -0
  36. package/dist/hostedPages.js.map +1 -0
  37. package/dist/index.d.ts +20 -0
  38. package/dist/index.js +20 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/interactions.d.ts +17 -0
  41. package/dist/interactions.js +209 -0
  42. package/dist/interactions.js.map +1 -0
  43. package/dist/jwtAccessTokens.d.ts +23 -0
  44. package/dist/jwtAccessTokens.js +125 -0
  45. package/dist/jwtAccessTokens.js.map +1 -0
  46. package/dist/logout.d.ts +22 -0
  47. package/dist/logout.js +86 -0
  48. package/dist/logout.js.map +1 -0
  49. package/dist/originGuard.d.ts +10 -0
  50. package/dist/originGuard.js +32 -0
  51. package/dist/originGuard.js.map +1 -0
  52. package/dist/pkce.d.ts +16 -0
  53. package/dist/pkce.js +32 -0
  54. package/dist/pkce.js.map +1 -0
  55. package/dist/providerConfig.d.ts +50 -0
  56. package/dist/providerConfig.js +305 -0
  57. package/dist/providerConfig.js.map +1 -0
  58. package/dist/rateLimit.d.ts +13 -0
  59. package/dist/rateLimit.js +36 -0
  60. package/dist/rateLimit.js.map +1 -0
  61. package/dist/refreshReuse.d.ts +3 -0
  62. package/dist/refreshReuse.js +56 -0
  63. package/dist/refreshReuse.js.map +1 -0
  64. package/dist/requestContext.d.ts +40 -0
  65. package/dist/requestContext.js +43 -0
  66. package/dist/requestContext.js.map +1 -0
  67. package/dist/resourceServers.d.ts +16 -0
  68. package/dist/resourceServers.js +59 -0
  69. package/dist/resourceServers.js.map +1 -0
  70. package/dist/resumeCookie.d.ts +8 -0
  71. package/dist/resumeCookie.js +46 -0
  72. package/dist/resumeCookie.js.map +1 -0
  73. package/dist/server.d.ts +2 -0
  74. package/dist/server.js +36 -0
  75. package/dist/server.js.map +1 -0
  76. package/dist/signingKey.d.ts +29 -0
  77. package/dist/signingKey.js +142 -0
  78. package/dist/signingKey.js.map +1 -0
  79. package/dist/startup.d.ts +4 -0
  80. package/dist/startup.js +19 -0
  81. package/dist/startup.js.map +1 -0
  82. package/dist/tokenAudit.d.ts +4 -0
  83. package/dist/tokenAudit.js +42 -0
  84. package/dist/tokenAudit.js.map +1 -0
  85. package/dist/tokenLifetimes.d.ts +31 -0
  86. package/dist/tokenLifetimes.js +51 -0
  87. package/dist/tokenLifetimes.js.map +1 -0
  88. package/dist/ui/assets/index-CydLXRvo.js +18 -0
  89. package/dist/ui/assets/index-y3LME-Gv.css +1 -0
  90. package/dist/ui/index.html +15 -0
  91. package/package.json +115 -4
@@ -0,0 +1,102 @@
1
+ /*
2
+ * Copyright © 2026 Fells Code, LLC
3
+ * Licensed under the Apache License, Version 2.0
4
+ * See LICENSE file in the project root for full license information
5
+ */
6
+ import { randomBytes } from 'node:crypto';
7
+ import { currentRequestContext } from './requestContext.js';
8
+ // The library's own body limit, so a body it would refuse is refused here first.
9
+ const BODY_LIMIT = 56 * 1024;
10
+ const FORM = 'application/x-www-form-urlencoded';
11
+ /** RFC 6749 section 2.3.1: Basic credentials are form-encoded before base64. */
12
+ function formDecode(value) {
13
+ return decodeURIComponent(value.replace(/\+/g, ' '));
14
+ }
15
+ function formEncode(value) {
16
+ return encodeURIComponent(value);
17
+ }
18
+ function fromBasic(header) {
19
+ const [scheme, token, extra] = (header ?? '').split(' ');
20
+ if (scheme?.toLowerCase() !== 'basic' || !token || extra !== undefined) {
21
+ return undefined;
22
+ }
23
+ const decoded = Buffer.from(token, 'base64').toString('utf8');
24
+ const colon = decoded.indexOf(':');
25
+ if (colon < 1) {
26
+ return undefined;
27
+ }
28
+ try {
29
+ const clientId = formDecode(decoded.slice(0, colon));
30
+ const secret = formDecode(decoded.slice(colon + 1));
31
+ return secret ? { clientId, secret, via: 'basic' } : undefined;
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ async function readBody(stream) {
38
+ const chunks = [];
39
+ let received = 0;
40
+ for await (const chunk of stream) {
41
+ received += chunk.length;
42
+ if (received > BODY_LIMIT) {
43
+ return undefined;
44
+ }
45
+ chunks.push(chunk);
46
+ }
47
+ return Buffer.concat(chunks, received);
48
+ }
49
+ /**
50
+ * ADR D6: a shared secret is checked by the API, never by this service. A presented secret
51
+ * is sent to `verify-secret`, and when the API accepts it, it is swapped in the request for a
52
+ * one-time value that the client's metadata carries for this request only (see
53
+ * `ApiClientAdapter`). The library compares against that value, so it never holds the real
54
+ * secret, and a secret the API did not accept can match nothing.
55
+ */
56
+ export function verifyClientSecrets(client) {
57
+ return async (ctx, next) => {
58
+ if (ctx.method !== 'POST') {
59
+ await next();
60
+ return;
61
+ }
62
+ let body;
63
+ if (ctx.is(FORM) && ctx.req.readable) {
64
+ const raw = await readBody(ctx.req);
65
+ if (raw === undefined) {
66
+ ctx.status = 413;
67
+ ctx.body = { error: 'invalid_request', error_description: 'request entity too large' };
68
+ return;
69
+ }
70
+ // The library reads a body an upstream parser already consumed from req.body.
71
+ ctx.req.body = raw;
72
+ body = new URLSearchParams(raw.toString('utf8'));
73
+ }
74
+ const basic = fromBasic(ctx.get('authorization') || undefined);
75
+ const postSecret = body?.get('client_secret');
76
+ const postClientId = body?.get('client_id');
77
+ // Two mechanisms at once is the library's to refuse, so it gets the request untouched.
78
+ const presented = basic && postSecret
79
+ ? undefined
80
+ : (basic ??
81
+ (postSecret && postClientId
82
+ ? { clientId: postClientId, secret: postSecret, via: 'post' }
83
+ : undefined));
84
+ if (presented && (await client.verifySecret(presented.clientId, presented.secret))) {
85
+ const oneTime = randomBytes(32).toString('base64url');
86
+ const context = currentRequestContext();
87
+ if (context) {
88
+ context.verifiedClientSecret = { clientId: presented.clientId, value: oneTime };
89
+ }
90
+ if (presented.via === 'basic') {
91
+ const credentials = `${formEncode(presented.clientId)}:${formEncode(oneTime)}`;
92
+ ctx.req.headers.authorization = `Basic ${Buffer.from(credentials).toString('base64')}`;
93
+ }
94
+ else if (body) {
95
+ body.set('client_secret', oneTime);
96
+ ctx.req.body = Buffer.from(body.toString());
97
+ }
98
+ }
99
+ await next();
100
+ };
101
+ }
102
+ //# sourceMappingURL=clientSecrets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clientSecrets.js","sourceRoot":"","sources":["../src/clientSecrets.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { OidcEdgeConfigResponse } from '@seamless-auth/types';
2
+ import type { OidcApiClient } from './apiClient.js';
3
+ /**
4
+ * The API's `/oidc/config`, re-read at most every `ttlMs`, so an admin turning a flag off
5
+ * (third-party clients, or the provider itself) takes effect without restarting this service.
6
+ */
7
+ export declare class EdgeConfigCache {
8
+ #private;
9
+ constructor(client: OidcApiClient, initial: OidcEdgeConfigResponse, { ttlMs, now }?: {
10
+ ttlMs?: number;
11
+ now?: () => number;
12
+ });
13
+ get(): Promise<OidcEdgeConfigResponse>;
14
+ }
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Copyright © 2026 Fells Code, LLC
3
+ * Licensed under the Apache License, Version 2.0
4
+ * See LICENSE file in the project root for full license information
5
+ */
6
+ import { OidcProviderDisabledError } from './errors.js';
7
+ const DEFAULT_TTL_MS = 30_000;
8
+ /**
9
+ * The API's `/oidc/config`, re-read at most every `ttlMs`, so an admin turning a flag off
10
+ * (third-party clients, or the provider itself) takes effect without restarting this service.
11
+ */
12
+ export class EdgeConfigCache {
13
+ #client;
14
+ #ttlMs;
15
+ #now;
16
+ #value;
17
+ #fetchedAt;
18
+ #pending;
19
+ constructor(client, initial, { ttlMs = DEFAULT_TTL_MS, now = Date.now } = {}) {
20
+ this.#client = client;
21
+ this.#ttlMs = ttlMs;
22
+ this.#now = now;
23
+ this.#value = initial;
24
+ this.#fetchedAt = now();
25
+ }
26
+ async get() {
27
+ if (this.#now() - this.#fetchedAt < this.#ttlMs) {
28
+ return this.#value;
29
+ }
30
+ this.#pending ??= this.#refresh().finally(() => {
31
+ this.#pending = undefined;
32
+ });
33
+ return this.#pending;
34
+ }
35
+ async #refresh() {
36
+ const config = await this.#client.config();
37
+ if (!config) {
38
+ throw new OidcProviderDisabledError();
39
+ }
40
+ this.#value = config;
41
+ this.#fetchedAt = this.#now();
42
+ return config;
43
+ }
44
+ }
45
+ //# sourceMappingURL=configCache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configCache.js","sourceRoot":"","sources":["../src/configCache.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAExD,MAAM,cAAc,GAAG,MAAM,CAAC;AAE9B;;;GAGG;AACH,MAAM,OAAO,eAAe;IACjB,OAAO,CAAgB;IACvB,MAAM,CAAS;IACf,IAAI,CAAe;IAC5B,MAAM,CAAyB;IAC/B,UAAU,CAAS;IACnB,QAAQ,CAA8C;IAEtD,YACE,MAAqB,EACrB,OAA+B,EAC/B,EAAE,KAAK,GAAG,cAAc,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,KAA6C,EAAE;QAEvF,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;QAChB,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC;QACtB,IAAI,CAAC,UAAU,GAAG,GAAG,EAAE,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,GAAG;QACP,IAAI,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YAChD,OAAO,IAAI,CAAC,MAAM,CAAC;QACrB,CAAC;QACD,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YAC7C,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;QAC5B,CAAC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QAC3C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,yBAAyB,EAAE,CAAC;QACxC,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC9B,OAAO,MAAM,CAAC;IAChB,CAAC;CACF"}
@@ -0,0 +1,45 @@
1
+ import type { OidcEdgeConfigResponse } from '@seamless-auth/types';
2
+ import { type Router } from 'express';
3
+ import Provider from 'oidc-provider';
4
+ import { OidcApiClient } from './apiClient.js';
5
+ export declare const ADAPTER_COOKIE_NAMES: {
6
+ readonly access: "__Host-seamless-access";
7
+ readonly registration: "__Host-seamless-ephemeral";
8
+ readonly refresh: "__Host-seamless-refresh";
9
+ readonly preAuth: "__Host-seamless-pre-auth";
10
+ };
11
+ export interface OidcEdgeOptions {
12
+ /** The private Seamless Auth API, as this service reaches it. */
13
+ apiUrl: string;
14
+ /** The API's `ISSUER`, when it differs from `apiUrl`. */
15
+ apiIssuer?: string;
16
+ /**
17
+ * This service's own service token secret (`SEAMLESS_OIDC_SERVICE_TOKEN` on the API), used
18
+ * for `/oidc/*` and for the adapter's first-party calls. Never the adapters' secret.
19
+ */
20
+ serviceSecret: string;
21
+ serviceTokenKid?: string;
22
+ /** Signs the provider's cookies. The first key signs, the rest still verify. */
23
+ cookieKeys: string[];
24
+ /** Signs the first-party session cookies the hosted UI uses. */
25
+ adapterCookieSecret: string;
26
+ /**
27
+ * Set when TLS ends at a proxy in front of this service. The host Express app needs
28
+ * `trust proxy` set to match.
29
+ */
30
+ trustProxy?: boolean;
31
+ /** `false` keeps the adapter on its bundled route manifest. */
32
+ fetchAdapterManifest?: boolean;
33
+ /** The built hosted pages. Defaults to the copy this package ships in `dist/ui`. */
34
+ uiDir?: string;
35
+ fetch?: typeof fetch;
36
+ }
37
+ export interface OidcEdge {
38
+ /** Mount at the root of the app that serves the issuer's origin. */
39
+ router: Router;
40
+ provider: Provider;
41
+ issuer: string;
42
+ config: OidcEdgeConfigResponse;
43
+ client: OidcApiClient;
44
+ }
45
+ export declare function createOidcEdge(options: OidcEdgeOptions): Promise<OidcEdge>;
@@ -0,0 +1,132 @@
1
+ /*
2
+ * Copyright © 2026 Fells Code, LLC
3
+ * Licensed under the Apache License, Version 2.0
4
+ * See LICENSE file in the project root for full license information
5
+ */
6
+ import { randomUUID } from 'node:crypto';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { createSeamlessAuthServer } from '@seamless-auth/express';
9
+ import express from 'express';
10
+ import Provider from 'oidc-provider';
11
+ import { OidcApiClient } from './apiClient.js';
12
+ import { verifyClientSecrets } from './clientSecrets.js';
13
+ import { EdgeConfigCache } from './configCache.js';
14
+ import { deviceFlowMiddleware } from './deviceFlow.js';
15
+ import { createHostedPageRenderer, createHostedPagesRouter } from './hostedPages.js';
16
+ import { createInteractionRouter } from './interactions.js';
17
+ import { acceptJwtAccessTokens, createJwtAccessTokenLookup } from './jwtAccessTokens.js';
18
+ import { endFirstPartySessionOnLogout, leaveBackchannelLogoutToApi } from './logout.js';
19
+ import { createSameOriginGuard } from './originGuard.js';
20
+ import { buildProviderConfiguration, PROVIDER_COOKIE_NAMES } from './providerConfig.js';
21
+ import { createRateLimiter } from './rateLimit.js';
22
+ import { revokeSessionOnRefreshReuse } from './refreshReuse.js';
23
+ import { runWithRequestContext } from './requestContext.js';
24
+ import { hostOnlyResumeCookie } from './resumeCookie.js';
25
+ import { signingKeysFrom } from './signingKey.js';
26
+ import { checkApiCompatibility } from './startup.js';
27
+ import { auditRevocations } from './tokenAudit.js';
28
+ export const ADAPTER_COOKIE_NAMES = {
29
+ access: '__Host-seamless-access',
30
+ registration: '__Host-seamless-ephemeral',
31
+ refresh: '__Host-seamless-refresh',
32
+ preAuth: '__Host-seamless-pre-auth',
33
+ };
34
+ export async function createOidcEdge(options) {
35
+ const client = new OidcApiClient({
36
+ apiUrl: options.apiUrl,
37
+ serviceSecret: options.serviceSecret,
38
+ ...(options.serviceTokenKid ? { serviceTokenKid: options.serviceTokenKid } : {}),
39
+ ...(options.fetch ? { fetch: options.fetch } : {}),
40
+ });
41
+ const config = await checkApiCompatibility(client);
42
+ const configCache = new EdgeConfigCache(client, config);
43
+ const signingKeys = signingKeysFrom(await client.jwks(), client);
44
+ const hostedPages = {
45
+ uiDir: options.uiDir ?? fileURLToPath(new URL('./ui/', import.meta.url)),
46
+ configCache,
47
+ };
48
+ const renderPage = createHostedPageRenderer(hostedPages);
49
+ const provider = new Provider(config.issuer, buildProviderConfiguration({
50
+ client,
51
+ config,
52
+ configCache,
53
+ signingKeys,
54
+ cookieKeys: options.cookieKeys,
55
+ renderPage,
56
+ }));
57
+ leaveBackchannelLogoutToApi(provider);
58
+ provider.proxy = options.trustProxy === true;
59
+ provider.use(hostOnlyResumeCookie(PROVIDER_COOKIE_NAMES.resume, '/authorize'));
60
+ provider.use(verifyClientSecrets(client));
61
+ provider.use(revokeSessionOnRefreshReuse(client));
62
+ provider.use(auditRevocations(client));
63
+ provider.use(endFirstPartySessionOnLogout(client, Object.values(ADAPTER_COOKIE_NAMES)));
64
+ provider.use(acceptJwtAccessTokens(createJwtAccessTokenLookup(signingKeys, config.issuer), [
65
+ '/token/introspection',
66
+ '/token/revocation',
67
+ ]));
68
+ // The library answers a bare server_error and logs nothing, so this is the only trace.
69
+ provider.on('server_error', (_ctx, error) => {
70
+ console.error(`[oidc-provider] ${error.name}: ${error.message}`);
71
+ });
72
+ const issuerUrl = new URL(config.issuer);
73
+ const issuerBase = issuerUrl.pathname.replace(/\/$/, '');
74
+ provider.use(deviceFlowMiddleware());
75
+ const router = express.Router();
76
+ // The library builds endpoint URLs from the request's scheme and host, so a request for
77
+ // any other origin could put a foreign one into discovery or a redirect. Behind a TLS
78
+ // proxy the host app must set Express's `trust proxy` for the scheme to read as https.
79
+ const issuerScheme = issuerUrl.protocol.slice(0, -1);
80
+ router.use((req, res, next) => {
81
+ if (req.host !== issuerUrl.host || req.protocol !== issuerScheme) {
82
+ res.status(421).json({ error: 'misdirected_request' });
83
+ return;
84
+ }
85
+ next();
86
+ });
87
+ router.use((req, _res, next) => {
88
+ const userAgent = req.get('user-agent');
89
+ runWithRequestContext({
90
+ correlationId: randomUUID(),
91
+ ...(req.ip ? { clientIp: req.ip } : {}),
92
+ ...(userAgent ? { userAgent } : {}),
93
+ }, next);
94
+ });
95
+ const sameOrigin = createSameOriginGuard(issuerUrl.origin);
96
+ router.use('/auth', sameOrigin, createSeamlessAuthServer({
97
+ authServerUrl: options.apiUrl,
98
+ audience: options.apiIssuer ?? options.apiUrl,
99
+ ...(options.apiIssuer ? { authServerIssuer: options.apiIssuer } : {}),
100
+ cookieSecret: options.adapterCookieSecret,
101
+ serviceSecret: options.serviceSecret,
102
+ ...(options.serviceTokenKid ? { jwksKid: options.serviceTokenKid } : {}),
103
+ cookieSecure: true,
104
+ cookieSameSite: 'lax',
105
+ allowedOrigins: [issuerUrl.origin],
106
+ accessCookieName: ADAPTER_COOKIE_NAMES.access,
107
+ registrationCookieName: ADAPTER_COOKIE_NAMES.registration,
108
+ refreshCookieName: ADAPTER_COOKIE_NAMES.refresh,
109
+ preAuthCookieName: ADAPTER_COOKIE_NAMES.preAuth,
110
+ ...(options.fetchAdapterManifest === false ? { fetchManifest: false } : {}),
111
+ }));
112
+ router.use(createInteractionRouter({
113
+ provider,
114
+ client,
115
+ issuerOrigin: issuerUrl.origin,
116
+ adapterCookieSecret: options.adapterCookieSecret,
117
+ accessCookieName: ADAPTER_COOKIE_NAMES.access,
118
+ }));
119
+ router.use(createHostedPagesRouter(hostedPages, renderPage));
120
+ const tokenStatusLimiter = createRateLimiter({ limit: 1200, windowMs: 60_000 });
121
+ router.use([`${issuerBase}/token/introspection`, `${issuerBase}/token/revocation`], tokenStatusLimiter);
122
+ // User code entry (RFC 8628 section 5.1). Per IP for a guesser at one address, and across
123
+ // all of them for one spread over many, per instance.
124
+ const userCodeLimiters = [
125
+ createRateLimiter({ limit: 10, windowMs: 60_000 }),
126
+ createRateLimiter({ limit: 600, windowMs: 60_000, key: () => 'all' }),
127
+ ];
128
+ router.post(`${issuerBase}/device`, ...userCodeLimiters);
129
+ router.use(issuerUrl.pathname, provider.callback());
130
+ return { router, provider, issuer: config.issuer, config, client };
131
+ }
132
+ //# sourceMappingURL=createOidcEdge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createOidcEdge.js","sourceRoot":"","sources":["../src/createOidcEdge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import type { KoaContextWithOIDC } from 'oidc-provider';
2
+ import type { OidcApiClient } from './apiClient.js';
3
+ import type { HostedPageRenderer } from './hostedPages.js';
4
+ export declare const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
5
+ /** RFC 8628 section 3.5: a client that is told `slow_down` adds five seconds. */
6
+ export declare const DEVICE_POLL_INTERVAL_SECONDS = 5;
7
+ /** ADR D10: user codes live for at most ten minutes. */
8
+ export declare const DEVICE_CODE_TTL_SECONDS = 600;
9
+ /**
10
+ * Eight characters of the RFC 8628 section 6.1 base-20 set (no vowels, so no words, and no
11
+ * look-alikes), about 34.5 bits. With codes living ten minutes and entry rate limited,
12
+ * guessing a live one is out of reach.
13
+ */
14
+ export declare const USER_CODE_CHARSET: "base-20";
15
+ export declare const USER_CODE_MASK = "****-****";
16
+ /** Whether a stored token or code came from a device approval. */
17
+ export declare function isDeviceGrant(payload: {
18
+ kind?: unknown;
19
+ gty?: unknown;
20
+ }): boolean;
21
+ /**
22
+ * ADR D10. The approving session's `acr` says how the approver signed in, not who holds the
23
+ * device, and `acr_values` or `max_age` would be satisfied by an approval made somewhere
24
+ * else, so the device flow takes neither.
25
+ */
26
+ export declare function checkDeviceRequest(params: Record<string, unknown> | undefined): void;
27
+ export declare function withLoginPrompt(prompt: unknown): string;
28
+ export declare class DevicePollThrottle {
29
+ #private;
30
+ constructor(now?: () => number);
31
+ check(deviceCode: string): void;
32
+ }
33
+ /**
34
+ * Marks token endpoint posts, and adds `interval` to the device authorization response. The
35
+ * provider is mounted at the issuer's path, so paths here are relative to it, and the
36
+ * library's router ignores case.
37
+ */
38
+ export declare function deviceFlowMiddleware(tokenPath?: string): (ctx: KoaContextWithOIDC, next: () => Promise<unknown>) => Promise<void>;
39
+ /** The device verification pages (RFC 8628 section 3.3), rendered by the hosted UI. */
40
+ export declare function createDevicePages(render: HostedPageRenderer, client: OidcApiClient): {
41
+ charset: "base-20";
42
+ mask: string;
43
+ userCodeInputSource(ctx: KoaContextWithOIDC, _form: string, out?: {
44
+ error?: string;
45
+ }, err?: Error & {
46
+ userCode?: string;
47
+ }): Promise<void>;
48
+ userCodeConfirmSource(ctx: KoaContextWithOIDC, _form: string, found: {
49
+ clientId: string;
50
+ clientName?: string | undefined;
51
+ }, _deviceInfo: unknown, userCode: string): Promise<void>;
52
+ successSource(ctx: KoaContextWithOIDC): Promise<void>;
53
+ };
@@ -0,0 +1,168 @@
1
+ /*
2
+ * Copyright © 2026 Fells Code, LLC
3
+ * Licensed under the Apache License, Version 2.0
4
+ * See LICENSE file in the project root for full license information
5
+ */
6
+ import { errors } from 'oidc-provider';
7
+ import { currentRequestContext } from './requestContext.js';
8
+ export const DEVICE_CODE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
9
+ /** RFC 8628 section 3.5: a client that is told `slow_down` adds five seconds. */
10
+ export const DEVICE_POLL_INTERVAL_SECONDS = 5;
11
+ /** ADR D10: user codes live for at most ten minutes. */
12
+ export const DEVICE_CODE_TTL_SECONDS = 600;
13
+ /**
14
+ * Eight characters of the RFC 8628 section 6.1 base-20 set (no vowels, so no words, and no
15
+ * look-alikes), about 34.5 bits. With codes living ten minutes and entry rate limited,
16
+ * guessing a live one is out of reach.
17
+ */
18
+ export const USER_CODE_CHARSET = 'base-20';
19
+ export const USER_CODE_MASK = '****-****';
20
+ /** Whether a stored token or code came from a device approval. */
21
+ export function isDeviceGrant(payload) {
22
+ return (payload.kind === 'DeviceCode' ||
23
+ (typeof payload.gty === 'string' && payload.gty.split(' ').includes('device_code')));
24
+ }
25
+ /**
26
+ * ADR D10. The approving session's `acr` says how the approver signed in, not who holds the
27
+ * device, and `acr_values` or `max_age` would be satisfied by an approval made somewhere
28
+ * else, so the device flow takes neither.
29
+ */
30
+ export function checkDeviceRequest(params) {
31
+ if (params?.acr_values !== undefined || params?.max_age !== undefined) {
32
+ throw new errors.InvalidRequest('acr_values and max_age are not supported by the device flow');
33
+ }
34
+ }
35
+ export function withLoginPrompt(prompt) {
36
+ const prompts = new Set(typeof prompt === 'string' ? prompt.split(' ').filter(Boolean) : []);
37
+ prompts.add('login');
38
+ return [...prompts].join(' ');
39
+ }
40
+ /**
41
+ * Per device code, per instance. The library has no polling interval of its own, so a
42
+ * client polling sooner than `interval` is told `slow_down` and its interval grows.
43
+ */
44
+ const MAX_TRACKED_CODES = 10_000;
45
+ export class DevicePollThrottle {
46
+ #polls = new Map();
47
+ #now;
48
+ constructor(now = () => Date.now()) {
49
+ this.#now = now;
50
+ }
51
+ check(deviceCode) {
52
+ const now = this.#now();
53
+ if (this.#polls.size >= MAX_TRACKED_CODES) {
54
+ for (const [code, poll] of this.#polls) {
55
+ if (now - poll.at > DEVICE_CODE_TTL_SECONDS * 1000)
56
+ this.#polls.delete(code);
57
+ }
58
+ // Still full of live entries: drop the oldest, so the map has a hard ceiling.
59
+ for (const code of this.#polls.keys()) {
60
+ if (this.#polls.size < MAX_TRACKED_CODES)
61
+ break;
62
+ this.#polls.delete(code);
63
+ }
64
+ }
65
+ const last = this.#polls.get(deviceCode);
66
+ // Deleted first so the Map's insertion order stays oldest-poll first.
67
+ this.#polls.delete(deviceCode);
68
+ if (last && now - last.at < last.interval * 1000) {
69
+ this.#polls.set(deviceCode, {
70
+ at: now,
71
+ interval: last.interval + DEVICE_POLL_INTERVAL_SECONDS,
72
+ });
73
+ throw new errors.SlowDown();
74
+ }
75
+ this.#polls.set(deviceCode, {
76
+ at: now,
77
+ interval: last?.interval ?? DEVICE_POLL_INTERVAL_SECONDS,
78
+ });
79
+ }
80
+ }
81
+ /**
82
+ * Marks token endpoint posts, and adds `interval` to the device authorization response. The
83
+ * provider is mounted at the issuer's path, so paths here are relative to it, and the
84
+ * library's router ignores case.
85
+ */
86
+ export function deviceFlowMiddleware(tokenPath = '/token') {
87
+ return async (ctx, next) => {
88
+ const context = currentRequestContext();
89
+ if (context && ctx.method === 'POST' && ctx.path.toLowerCase() === tokenPath) {
90
+ context.tokenRequest = true;
91
+ }
92
+ await next();
93
+ if (ctx.oidc?.route === 'device_authorization' &&
94
+ ctx.status === 200 &&
95
+ ctx.body &&
96
+ typeof ctx.body === 'object') {
97
+ ctx.body.interval = DEVICE_POLL_INTERVAL_SECONDS;
98
+ }
99
+ };
100
+ }
101
+ function send(ctx, { html, headers }) {
102
+ ctx.set(headers);
103
+ ctx.type = 'html';
104
+ ctx.body = html;
105
+ }
106
+ function clientName(client) {
107
+ return client ? (client.clientName ?? client.clientId) : null;
108
+ }
109
+ const INPUT_ERRORS = {
110
+ NotFoundError: 'not_found',
111
+ ExpiredError: 'expired',
112
+ AlreadyUsedError: 'already_used',
113
+ AbortedError: 'aborted',
114
+ NoCodeError: 'no_code',
115
+ };
116
+ /** The device verification pages (RFC 8628 section 3.3), rendered by the hosted UI. */
117
+ export function createDevicePages(render, client) {
118
+ async function audit(type, clientId) {
119
+ const context = currentRequestContext();
120
+ if (!context)
121
+ return;
122
+ try {
123
+ await client.event({
124
+ type,
125
+ correlationId: context.correlationId,
126
+ occurredAt: new Date().toISOString(),
127
+ ...(clientId ? { clientId } : {}),
128
+ ...(context.sessionId ? { sessionId: context.sessionId } : {}),
129
+ });
130
+ }
131
+ catch (error) {
132
+ console.error(`[oidc-provider] could not record a device decision: ${String(error)}`);
133
+ }
134
+ }
135
+ return {
136
+ charset: USER_CODE_CHARSET,
137
+ mask: USER_CODE_MASK,
138
+ async userCodeInputSource(ctx, _form, out, err) {
139
+ if (err?.name === 'AbortedError') {
140
+ await audit('device_code_denied', ctx.oidc.entities.DeviceCode?.clientId);
141
+ }
142
+ const error = err ? (INPUT_ERRORS[err.name] ?? out?.error ?? 'server_error') : null;
143
+ send(ctx, await render({
144
+ view: 'device_input',
145
+ action: ctx.oidc.urlFor('code_verification'),
146
+ xsrf: ctx.oidc.session?.state?.secret,
147
+ userCode: err?.userCode ?? null,
148
+ error,
149
+ }));
150
+ },
151
+ async userCodeConfirmSource(ctx, _form, found, _deviceInfo, userCode) {
152
+ const scope = ctx.oidc.entities.DeviceCode?.params?.scope;
153
+ send(ctx, await render({
154
+ view: 'device_confirm',
155
+ action: ctx.oidc.urlFor('code_verification'),
156
+ xsrf: ctx.oidc.session?.state?.secret,
157
+ userCode,
158
+ clientName: clientName(found),
159
+ scopes: typeof scope === 'string' ? scope.split(' ').filter(Boolean) : [],
160
+ }));
161
+ },
162
+ async successSource(ctx) {
163
+ await audit('device_code_approved', ctx.oidc.client?.clientId);
164
+ send(ctx, await render({ view: 'device_done', clientName: clientName(ctx.oidc.client) }));
165
+ },
166
+ };
167
+ }
168
+ //# sourceMappingURL=deviceFlow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deviceFlow.js","sourceRoot":"","sources":["../src/deviceFlow.ts"],"names":[],"mappings":"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"}
package/dist/env.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+ export declare const EnvSchema: z.ZodObject<{
3
+ PORT: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
4
+ SEAMLESS_API_URL: z.ZodURL;
5
+ SEAMLESS_API_ISSUER: z.ZodOptional<z.ZodURL>;
6
+ SEAMLESS_OIDC_SERVICE_TOKEN: z.ZodString;
7
+ SEAMLESS_JWKS_KID: z.ZodOptional<z.ZodString>;
8
+ SEAMLESS_COOKIE_SECRET: z.ZodString;
9
+ OIDC_COOKIE_KEYS: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string[], string>>, z.ZodArray<z.ZodString>>;
10
+ TRUST_PROXY: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
11
+ true: "true";
12
+ false: "false";
13
+ }>>, z.ZodTransform<boolean, "true" | "false">>;
14
+ }, z.core.$strip>;
15
+ export type Env = z.infer<typeof EnvSchema>;
16
+ export declare function parseEnv(source?: NodeJS.ProcessEnv): Env;
package/dist/env.js ADDED
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Copyright © 2026 Fells Code, LLC
3
+ * Licensed under the Apache License, Version 2.0
4
+ * See LICENSE file in the project root for full license information
5
+ */
6
+ import { z } from 'zod';
7
+ const SECRET_MIN_LENGTH = 32;
8
+ const booleanString = z
9
+ .enum(['true', 'false'])
10
+ .default('false')
11
+ .transform((value) => value === 'true');
12
+ export const EnvSchema = z.object({
13
+ PORT: z.coerce.number().int().min(1).max(65535).default(5314),
14
+ SEAMLESS_API_URL: z.url(),
15
+ SEAMLESS_API_ISSUER: z.url().optional(),
16
+ SEAMLESS_OIDC_SERVICE_TOKEN: z.string().min(SECRET_MIN_LENGTH),
17
+ SEAMLESS_JWKS_KID: z.string().min(1).optional(),
18
+ SEAMLESS_COOKIE_SECRET: z.string().min(SECRET_MIN_LENGTH),
19
+ OIDC_COOKIE_KEYS: z
20
+ .string()
21
+ .transform((value) => value
22
+ .split(',')
23
+ .map((key) => key.trim())
24
+ .filter(Boolean))
25
+ .pipe(z.array(z.string().min(SECRET_MIN_LENGTH)).min(1)),
26
+ TRUST_PROXY: booleanString,
27
+ });
28
+ export function parseEnv(source = process.env) {
29
+ const parsed = EnvSchema.safeParse(source);
30
+ if (!parsed.success) {
31
+ const fields = parsed.error.issues.map((issue) => issue.path.join('.')).join(', ');
32
+ throw new Error(`Invalid environment: ${fields}`);
33
+ }
34
+ return parsed.data;
35
+ }
36
+ //# sourceMappingURL=env.js.map