@naylence/runtime 0.3.5-test.911 → 0.3.5-test.913
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/dist/browser/index.cjs +72 -164
- package/dist/browser/index.mjs +72 -164
- package/dist/cjs/naylence/fame/config/extended-fame-config.js +52 -0
- package/dist/cjs/naylence/fame/http/jwks-api-router.js +16 -18
- package/dist/cjs/naylence/fame/http/oauth2-server.js +28 -31
- package/dist/cjs/naylence/fame/http/oauth2-token-router.js +153 -8
- package/dist/cjs/naylence/fame/http/openid-configuration-router.js +30 -32
- package/dist/cjs/naylence/fame/node/admission/admission-profile-factory.js +18 -0
- package/dist/cjs/naylence/fame/security/crypto/providers/default-crypto-provider.js +0 -162
- package/dist/cjs/version.js +2 -2
- package/dist/esm/naylence/fame/config/extended-fame-config.js +52 -0
- package/dist/esm/naylence/fame/http/jwks-api-router.js +16 -17
- package/dist/esm/naylence/fame/http/oauth2-server.js +28 -31
- package/dist/esm/naylence/fame/http/oauth2-token-router.js +153 -8
- package/dist/esm/naylence/fame/http/openid-configuration-router.js +30 -31
- package/dist/esm/naylence/fame/node/admission/admission-profile-factory.js +18 -0
- package/dist/esm/naylence/fame/security/crypto/providers/default-crypto-provider.js +0 -162
- package/dist/esm/version.js +2 -2
- package/dist/node/index.cjs +72 -164
- package/dist/node/index.mjs +72 -164
- package/dist/node/node.cjs +299 -249
- package/dist/node/node.mjs +299 -249
- package/dist/types/naylence/fame/http/jwks-api-router.d.ts +8 -8
- package/dist/types/naylence/fame/http/oauth2-server.d.ts +3 -3
- package/dist/types/naylence/fame/http/oauth2-token-router.d.ts +5 -5
- package/dist/types/naylence/fame/http/openid-configuration-router.d.ts +8 -8
- package/dist/types/naylence/fame/security/crypto/providers/default-crypto-provider.d.ts +0 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +4 -6
- package/dist/esm/naylence/fame/fastapi/oauth2-server.js +0 -205
- package/dist/types/naylence/fame/fastapi/oauth2-server.d.ts +0 -22
|
@@ -34,9 +34,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.DefaultCryptoProvider = void 0;
|
|
37
|
-
const asn1_schema_1 = require("@peculiar/asn1-schema");
|
|
38
|
-
const asn1_x509_1 = require("@peculiar/asn1-x509");
|
|
39
|
-
const asn1_csr_1 = require("@peculiar/asn1-csr");
|
|
40
37
|
const core_1 = require("@naylence/core");
|
|
41
38
|
const logging_js_1 = require("../../../util/logging.js");
|
|
42
39
|
const util_js_1 = require("../../../util/util.js");
|
|
@@ -52,11 +49,6 @@ const DEFAULT_AUDIENCE = 'router-dev';
|
|
|
52
49
|
const DEFAULT_TTL_SEC = 3600;
|
|
53
50
|
const DEFAULT_HMAC_SECRET_BYTES = 32;
|
|
54
51
|
const ENCRYPTION_ALG = 'ECDH-ES';
|
|
55
|
-
const EXTENSION_REQUEST_OID = '1.2.840.113549.1.9.14';
|
|
56
|
-
const COMMON_NAME_OID = '2.5.4.3';
|
|
57
|
-
const ED25519_OID = '1.3.101.112';
|
|
58
|
-
const CSR_PEM_TAG = 'CERTIFICATE REQUEST';
|
|
59
|
-
const LOGICAL_URI_PREFIX = 'naylence://';
|
|
60
52
|
function normalizeDefaultCryptoProviderOptions(options) {
|
|
61
53
|
if (!options) {
|
|
62
54
|
return {};
|
|
@@ -322,76 +314,6 @@ class DefaultCryptoProvider {
|
|
|
322
314
|
has_chain: Boolean(certificateChainPem),
|
|
323
315
|
});
|
|
324
316
|
}
|
|
325
|
-
async createCsr(nodeId, physicalPath, logicals, subjectName) {
|
|
326
|
-
const trimmedNodeId = assertNonEmptyString(nodeId, 'nodeId');
|
|
327
|
-
const trimmedPhysicalPath = assertNonEmptyString(physicalPath, 'physicalPath');
|
|
328
|
-
try {
|
|
329
|
-
if (this.artifacts.signing.algorithm !== 'EdDSA') {
|
|
330
|
-
throw new Error('CSR creation only supported for Ed25519 signing keys in the default crypto provider');
|
|
331
|
-
}
|
|
332
|
-
const cryptoImpl = await ensureWebCrypto();
|
|
333
|
-
const privateKey = await cryptoImpl.subtle.importKey('pkcs8', pemToArrayBuffer(this.signingPrivatePem), {
|
|
334
|
-
name: 'Ed25519',
|
|
335
|
-
}, false, ['sign']);
|
|
336
|
-
const publicKeyDer = pemToArrayBuffer(this.signingPublicPem);
|
|
337
|
-
const subjectPkInfo = asn1_schema_1.AsnConvert.parse(publicKeyDer, asn1_x509_1.SubjectPublicKeyInfo);
|
|
338
|
-
const sanitizedLogicals = Array.isArray(logicals)
|
|
339
|
-
? logicals.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
340
|
-
: [];
|
|
341
|
-
const commonName = typeof subjectName === 'string' && subjectName.trim().length > 0
|
|
342
|
-
? subjectName.trim()
|
|
343
|
-
: trimmedNodeId;
|
|
344
|
-
const subject = buildSubjectName(commonName);
|
|
345
|
-
const attributes = new asn1_csr_1.Attributes();
|
|
346
|
-
if (sanitizedLogicals.length > 0) {
|
|
347
|
-
const san = new asn1_x509_1.SubjectAlternativeName(sanitizedLogicals.map((logical) => new asn1_x509_1.GeneralName({
|
|
348
|
-
uniformResourceIdentifier: `${LOGICAL_URI_PREFIX}${logical}`,
|
|
349
|
-
})));
|
|
350
|
-
const extensions = new asn1_x509_1.Extensions([
|
|
351
|
-
new asn1_x509_1.Extension({
|
|
352
|
-
extnID: asn1_x509_1.id_ce_subjectAltName,
|
|
353
|
-
critical: false,
|
|
354
|
-
extnValue: new asn1_schema_1.OctetString(asn1_schema_1.AsnConvert.serialize(san)),
|
|
355
|
-
}),
|
|
356
|
-
]);
|
|
357
|
-
attributes.push(new asn1_x509_1.Attribute({
|
|
358
|
-
type: EXTENSION_REQUEST_OID,
|
|
359
|
-
values: [asn1_schema_1.AsnConvert.serialize(extensions)],
|
|
360
|
-
}));
|
|
361
|
-
}
|
|
362
|
-
const requestInfo = new asn1_csr_1.CertificationRequestInfo({
|
|
363
|
-
subject,
|
|
364
|
-
subjectPKInfo: subjectPkInfo,
|
|
365
|
-
attributes,
|
|
366
|
-
});
|
|
367
|
-
const requestInfoDer = asn1_schema_1.AsnConvert.serialize(requestInfo);
|
|
368
|
-
const signature = await cryptoImpl.subtle.sign('Ed25519', privateKey, requestInfoDer);
|
|
369
|
-
const certificationRequest = new asn1_csr_1.CertificationRequest({
|
|
370
|
-
certificationRequestInfo: requestInfo,
|
|
371
|
-
signatureAlgorithm: new asn1_x509_1.AlgorithmIdentifier({
|
|
372
|
-
algorithm: ED25519_OID,
|
|
373
|
-
}),
|
|
374
|
-
signature: encodeBitString(signature),
|
|
375
|
-
});
|
|
376
|
-
certificationRequest.certificationRequestInfoRaw = requestInfoDer;
|
|
377
|
-
const csrDer = asn1_schema_1.AsnConvert.serialize(certificationRequest);
|
|
378
|
-
const csrPem = arrayBufferToPem(csrDer, CSR_PEM_TAG);
|
|
379
|
-
logger.debug('csr_created', {
|
|
380
|
-
node_id: trimmedNodeId,
|
|
381
|
-
physical_path: trimmedPhysicalPath,
|
|
382
|
-
logical_count: sanitizedLogicals.length,
|
|
383
|
-
});
|
|
384
|
-
return csrPem;
|
|
385
|
-
}
|
|
386
|
-
catch (error) {
|
|
387
|
-
logger.error('csr_creation_failed', {
|
|
388
|
-
node_id: trimmedNodeId,
|
|
389
|
-
physical_path: trimmedPhysicalPath,
|
|
390
|
-
error: error instanceof Error ? error.message : String(error),
|
|
391
|
-
});
|
|
392
|
-
throw error;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
317
|
}
|
|
396
318
|
exports.DefaultCryptoProvider = DefaultCryptoProvider;
|
|
397
319
|
async function buildProviderArtifacts(options) {
|
|
@@ -628,90 +550,6 @@ function pemToDerBase64(pem) {
|
|
|
628
550
|
// Ensure the output is valid base64 without whitespace
|
|
629
551
|
return base64.replace(/\s+/g, '');
|
|
630
552
|
}
|
|
631
|
-
let cryptoPromise = null;
|
|
632
|
-
async function ensureWebCrypto() {
|
|
633
|
-
if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto?.subtle) {
|
|
634
|
-
return globalThis.crypto;
|
|
635
|
-
}
|
|
636
|
-
if (!cryptoPromise) {
|
|
637
|
-
if (typeof process !== 'undefined' &&
|
|
638
|
-
typeof process.versions?.node === 'string') {
|
|
639
|
-
cryptoPromise = Promise.resolve().then(() => __importStar(require('node:crypto'))).then((module) => {
|
|
640
|
-
const webcrypto = module.webcrypto;
|
|
641
|
-
if (!webcrypto || !webcrypto.subtle) {
|
|
642
|
-
throw new Error('WebCrypto API is not available in this Node.js runtime');
|
|
643
|
-
}
|
|
644
|
-
globalThis.crypto = webcrypto;
|
|
645
|
-
return webcrypto;
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
else {
|
|
649
|
-
cryptoPromise = Promise.reject(new Error('WebCrypto API is not available in this environment'));
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
return cryptoPromise;
|
|
653
|
-
}
|
|
654
|
-
function pemToArrayBuffer(pem) {
|
|
655
|
-
const normalized = pem
|
|
656
|
-
.replace(/-----BEGIN[^-]+-----/g, '')
|
|
657
|
-
.replace(/-----END[^-]+-----/g, '')
|
|
658
|
-
.replace(/\s+/g, '');
|
|
659
|
-
const bytes = base64ToBytes(normalized);
|
|
660
|
-
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
661
|
-
}
|
|
662
|
-
function base64ToBytes(base64) {
|
|
663
|
-
if (typeof Buffer !== 'undefined') {
|
|
664
|
-
const buffer = Buffer.from(base64, 'base64');
|
|
665
|
-
const bytes = new Uint8Array(buffer.length);
|
|
666
|
-
for (let i = 0; i < buffer.length; i += 1) {
|
|
667
|
-
bytes[i] = buffer[i];
|
|
668
|
-
}
|
|
669
|
-
return bytes;
|
|
670
|
-
}
|
|
671
|
-
if (typeof atob === 'function') {
|
|
672
|
-
const binary = atob(base64);
|
|
673
|
-
const bytes = new Uint8Array(binary.length);
|
|
674
|
-
for (let i = 0; i < binary.length; i += 1) {
|
|
675
|
-
bytes[i] = binary.charCodeAt(i);
|
|
676
|
-
}
|
|
677
|
-
return bytes;
|
|
678
|
-
}
|
|
679
|
-
throw new Error('No base64 decoder available in this environment');
|
|
680
|
-
}
|
|
681
|
-
function arrayBufferToPem(buffer, tag) {
|
|
682
|
-
const base64 = bytesToBase64(new Uint8Array(buffer));
|
|
683
|
-
return `-----BEGIN ${tag}-----\n${formatPem(base64)}\n-----END ${tag}-----\n`;
|
|
684
|
-
}
|
|
685
|
-
function formatPem(base64) {
|
|
686
|
-
const lines = [];
|
|
687
|
-
for (let i = 0; i < base64.length; i += 64) {
|
|
688
|
-
lines.push(base64.slice(i, i + 64));
|
|
689
|
-
}
|
|
690
|
-
return lines.join('\n');
|
|
691
|
-
}
|
|
692
|
-
function encodeBitString(signature) {
|
|
693
|
-
const bytes = new Uint8Array(signature);
|
|
694
|
-
const bitString = new Uint8Array(bytes.length + 1);
|
|
695
|
-
bitString.set(bytes, 1);
|
|
696
|
-
return bitString.buffer;
|
|
697
|
-
}
|
|
698
|
-
function buildSubjectName(commonName) {
|
|
699
|
-
const attribute = new asn1_x509_1.AttributeTypeAndValue({
|
|
700
|
-
type: COMMON_NAME_OID,
|
|
701
|
-
value: new asn1_x509_1.AttributeValue({ utf8String: commonName }),
|
|
702
|
-
});
|
|
703
|
-
return new asn1_x509_1.Name([new asn1_x509_1.RelativeDistinguishedName([attribute])]);
|
|
704
|
-
}
|
|
705
|
-
function assertNonEmptyString(value, name) {
|
|
706
|
-
if (typeof value !== 'string') {
|
|
707
|
-
throw new TypeError(`${name} must be a string`);
|
|
708
|
-
}
|
|
709
|
-
const trimmed = value.trim();
|
|
710
|
-
if (trimmed.length === 0) {
|
|
711
|
-
throw new TypeError(`${name} must be a non-empty string`);
|
|
712
|
-
}
|
|
713
|
-
return trimmed;
|
|
714
|
-
}
|
|
715
553
|
function cloneJson(value) {
|
|
716
554
|
return JSON.parse(JSON.stringify(value));
|
|
717
555
|
}
|
package/dist/cjs/version.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// This file is auto-generated during build - do not edit manually
|
|
3
|
-
// Generated from package.json version: 0.3.5-test.
|
|
3
|
+
// Generated from package.json version: 0.3.5-test.913
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.VERSION = void 0;
|
|
6
6
|
/**
|
|
7
7
|
* The package version, injected at build time.
|
|
8
8
|
* @internal
|
|
9
9
|
*/
|
|
10
|
-
exports.VERSION = '0.3.5-test.
|
|
10
|
+
exports.VERSION = '0.3.5-test.913';
|
|
@@ -14,6 +14,57 @@ const CONFIG_SEARCH_PATHS = [
|
|
|
14
14
|
];
|
|
15
15
|
const fsModuleSpecifier = String.fromCharCode(102) + String.fromCharCode(115);
|
|
16
16
|
let cachedFsModule = null;
|
|
17
|
+
// Capture this module's URL without triggering TypeScript's import.meta restriction on CJS builds
|
|
18
|
+
const currentModuleUrl = (() => {
|
|
19
|
+
try {
|
|
20
|
+
return (0, eval)('import.meta.url');
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
// Shared flag that allows synchronous waiting for the Node-specific require shim
|
|
27
|
+
const requireReadyFlag = isNode && typeof SharedArrayBuffer !== 'undefined'
|
|
28
|
+
? new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT))
|
|
29
|
+
: null;
|
|
30
|
+
if (requireReadyFlag) {
|
|
31
|
+
// 0 means initializing, 1 means ready (success or failure)
|
|
32
|
+
Atomics.store(requireReadyFlag, 0, 0);
|
|
33
|
+
// Prepare a CommonJS-style require when running in pure ESM contexts
|
|
34
|
+
void (async () => {
|
|
35
|
+
try {
|
|
36
|
+
if (typeof require !== 'function') {
|
|
37
|
+
const moduleNamespace = (await import('node:module'));
|
|
38
|
+
const createRequire = moduleNamespace.createRequire;
|
|
39
|
+
if (typeof createRequire === 'function') {
|
|
40
|
+
const fallbackPath = `${process.cwd()}/.__naylence_require_shim__.mjs`;
|
|
41
|
+
const nodeRequire = createRequire(currentModuleUrl ?? fallbackPath);
|
|
42
|
+
globalThis.require = nodeRequire;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Ignore failures – getFsModule will surface a helpful error when needed
|
|
48
|
+
}
|
|
49
|
+
})()
|
|
50
|
+
.catch(() => {
|
|
51
|
+
// Ignore async errors – the ready flag will still unblock consumers
|
|
52
|
+
})
|
|
53
|
+
.finally(() => {
|
|
54
|
+
Atomics.store(requireReadyFlag, 0, 1);
|
|
55
|
+
Atomics.notify(requireReadyFlag, 0);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function ensureRequireReady() {
|
|
59
|
+
if (!requireReadyFlag) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (Atomics.load(requireReadyFlag, 0) === 1) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Block until the asynchronous loader finishes initialising
|
|
66
|
+
Atomics.wait(requireReadyFlag, 0, 0);
|
|
67
|
+
}
|
|
17
68
|
function getFsModule() {
|
|
18
69
|
if (cachedFsModule) {
|
|
19
70
|
return cachedFsModule;
|
|
@@ -21,6 +72,7 @@ function getFsModule() {
|
|
|
21
72
|
if (!isNode) {
|
|
22
73
|
throw new Error('File system access is not available in this environment');
|
|
23
74
|
}
|
|
75
|
+
ensureRequireReady();
|
|
24
76
|
if (typeof require === 'function') {
|
|
25
77
|
try {
|
|
26
78
|
cachedFsModule = require(fsModuleSpecifier);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JWKS (JSON Web Key Set) API
|
|
2
|
+
* JWKS (JSON Web Key Set) API plugin for Fastify
|
|
3
3
|
*
|
|
4
4
|
* Provides /.well-known/jwks.json endpoint for public key discovery
|
|
5
5
|
* Used by OAuth2/JWT token verification
|
|
6
6
|
*/
|
|
7
|
-
import express from 'express';
|
|
8
7
|
import { getLogger } from '../util/logging.js';
|
|
9
8
|
const logger = getLogger('naylence.fame.http.jwks_api_router');
|
|
10
9
|
const DEFAULT_PREFIX = '';
|
|
@@ -84,23 +83,22 @@ function filterKeysByType(jwksData, allowedTypes) {
|
|
|
84
83
|
return { ...jwksData, keys: filteredKeys };
|
|
85
84
|
}
|
|
86
85
|
/**
|
|
87
|
-
* Create
|
|
86
|
+
* Create a Fastify plugin that exposes JWKS at /.well-known/jwks.json
|
|
88
87
|
*
|
|
89
88
|
* @param options - Router configuration options
|
|
90
|
-
* @returns
|
|
89
|
+
* @returns Fastify plugin with JWKS endpoint
|
|
91
90
|
*
|
|
92
91
|
* @example
|
|
93
92
|
* ```typescript
|
|
94
|
-
* import
|
|
93
|
+
* import Fastify from 'fastify';
|
|
95
94
|
* import { createJwksRouter } from '@naylence/runtime';
|
|
96
95
|
*
|
|
97
|
-
* const app =
|
|
96
|
+
* const app = Fastify();
|
|
98
97
|
* const cryptoProvider = new MyCryptoProvider();
|
|
99
|
-
* app.
|
|
98
|
+
* app.register(createJwksRouter({ cryptoProvider }));
|
|
100
99
|
* ```
|
|
101
100
|
*/
|
|
102
101
|
export function createJwksRouter(options = {}) {
|
|
103
|
-
const router = express.Router();
|
|
104
102
|
const { getJwksJson, cryptoProvider, prefix = DEFAULT_PREFIX, keyTypes, } = normalizeCreateJwksRouterOptions(options);
|
|
105
103
|
// Get JWKS data
|
|
106
104
|
let jwks;
|
|
@@ -123,14 +121,15 @@ export function createJwksRouter(options = {}) {
|
|
|
123
121
|
key_types: allowedKeyTypes,
|
|
124
122
|
total_keys: jwks.keys.length,
|
|
125
123
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
const plugin = async (instance) => {
|
|
125
|
+
instance.get(`${prefix}/.well-known/jwks.json`, async (_request, reply) => {
|
|
126
|
+
const filteredJwks = filterKeysByType(jwks, allowedKeyTypes);
|
|
127
|
+
logger.debug('jwks_served', {
|
|
128
|
+
total_keys: jwks.keys.length,
|
|
129
|
+
filtered_keys: filteredJwks.keys.length,
|
|
130
|
+
});
|
|
131
|
+
reply.send(filteredJwks);
|
|
132
132
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return router;
|
|
133
|
+
};
|
|
134
|
+
return plugin;
|
|
136
135
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* FAME_JWT_ISSUER: JWT issuer (default: https://auth.fame.fabric)
|
|
22
22
|
* FAME_JWT_ALGORITHM: JWT algorithm (default: EdDSA)
|
|
23
23
|
*/
|
|
24
|
-
import
|
|
24
|
+
import Fastify from 'fastify';
|
|
25
25
|
import { createOAuth2TokenRouter } from './oauth2-token-router.js';
|
|
26
26
|
import { createJwksRouter } from './jwks-api-router.js';
|
|
27
27
|
import { createOpenIDConfigurationRouter } from './openid-configuration-router.js';
|
|
@@ -53,23 +53,18 @@ async function getCryptoProvider() {
|
|
|
53
53
|
return DefaultCryptoProvider.create();
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* Create and configure the OAuth2
|
|
56
|
+
* Create and configure the OAuth2 Fastify application
|
|
57
57
|
*/
|
|
58
58
|
export async function createApp() {
|
|
59
|
-
const app =
|
|
60
|
-
// Middleware
|
|
61
|
-
app.use(express.json());
|
|
62
|
-
app.use(express.urlencoded({ extended: true }));
|
|
59
|
+
const app = Fastify({ logger: false });
|
|
63
60
|
// Get crypto provider
|
|
64
61
|
const cryptoProvider = await getCryptoProvider();
|
|
65
62
|
// Add routers
|
|
66
|
-
app.
|
|
67
|
-
app.
|
|
68
|
-
app.
|
|
63
|
+
app.register(createOAuth2TokenRouter({ cryptoProvider }));
|
|
64
|
+
app.register(createJwksRouter({ cryptoProvider }));
|
|
65
|
+
app.register(createOpenIDConfigurationRouter());
|
|
69
66
|
// Health check endpoint
|
|
70
|
-
app.get('/health', (
|
|
71
|
-
res.json({ status: 'ok' });
|
|
72
|
-
});
|
|
67
|
+
app.get('/health', async () => ({ status: 'ok' }));
|
|
73
68
|
return app;
|
|
74
69
|
}
|
|
75
70
|
/**
|
|
@@ -97,27 +92,29 @@ async function main() {
|
|
|
97
92
|
});
|
|
98
93
|
const app = await createApp();
|
|
99
94
|
// Start server
|
|
100
|
-
app.listen(port, host
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
});
|
|
95
|
+
await app.listen({ port, host });
|
|
96
|
+
logger.info('oauth2_server_started', {
|
|
97
|
+
host,
|
|
98
|
+
port,
|
|
99
|
+
endpoints: {
|
|
100
|
+
token: '/oauth/token',
|
|
101
|
+
jwks: '/.well-known/jwks.json',
|
|
102
|
+
openid_config: '/.well-known/openid-configuration',
|
|
103
|
+
health: '/health',
|
|
104
|
+
},
|
|
111
105
|
});
|
|
106
|
+
const shutdown = (signal) => {
|
|
107
|
+
logger.info('oauth2_server_shutting_down', { signal });
|
|
108
|
+
app
|
|
109
|
+
.close()
|
|
110
|
+
.catch((error) => logger.error('oauth2_server_shutdown_error', {
|
|
111
|
+
error: error instanceof Error ? error.message : String(error),
|
|
112
|
+
}))
|
|
113
|
+
.finally(() => process.exit(0));
|
|
114
|
+
};
|
|
112
115
|
// Graceful shutdown
|
|
113
|
-
process.on('SIGINT', () =>
|
|
114
|
-
|
|
115
|
-
process.exit(0);
|
|
116
|
-
});
|
|
117
|
-
process.on('SIGTERM', () => {
|
|
118
|
-
logger.info('oauth2_server_shutting_down', { signal: 'SIGTERM' });
|
|
119
|
-
process.exit(0);
|
|
120
|
-
});
|
|
116
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
117
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
121
118
|
}
|
|
122
119
|
// Export main for CLI usage
|
|
123
120
|
export { main };
|
|
@@ -1,15 +1,160 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OAuth2 client credentials and authorization code (PKCE) grant router for
|
|
2
|
+
* OAuth2 client credentials and authorization code (PKCE) grant router for Fastify
|
|
3
3
|
*
|
|
4
4
|
* Provides /oauth/token and /oauth/authorize endpoints for local development and testing.
|
|
5
5
|
* Implements OAuth2 client credentials grant with JWT token issuance and
|
|
6
6
|
* OAuth2 authorization code grant with PKCE verification.
|
|
7
7
|
*/
|
|
8
|
-
import
|
|
8
|
+
import formbody from '@fastify/formbody';
|
|
9
9
|
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
10
10
|
import { JWTTokenIssuer } from '../security/auth/jwt-token-issuer.js';
|
|
11
11
|
import { getLogger } from '../util/logging.js';
|
|
12
12
|
const logger = getLogger('naylence.fame.http.oauth2_token_router');
|
|
13
|
+
class RouterCompat {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.routes = [];
|
|
16
|
+
}
|
|
17
|
+
get(path, handler) {
|
|
18
|
+
this.routes.push({ method: 'GET', path, handler });
|
|
19
|
+
}
|
|
20
|
+
post(path, handler) {
|
|
21
|
+
this.routes.push({ method: 'POST', path, handler });
|
|
22
|
+
}
|
|
23
|
+
toPlugin() {
|
|
24
|
+
return async (fastify) => {
|
|
25
|
+
await fastify.register(formbody);
|
|
26
|
+
for (const route of this.routes) {
|
|
27
|
+
fastify.route({
|
|
28
|
+
method: route.method,
|
|
29
|
+
url: route.path,
|
|
30
|
+
handler: async (request, reply) => {
|
|
31
|
+
const compatRequest = toCompatRequest(request);
|
|
32
|
+
const compatResponse = new FastifyResponseAdapter(reply);
|
|
33
|
+
await route.handler(compatRequest, compatResponse);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
class FastifyResponseAdapter {
|
|
41
|
+
constructor(reply) {
|
|
42
|
+
this.reply = reply;
|
|
43
|
+
}
|
|
44
|
+
status(code) {
|
|
45
|
+
this.reply.status(code);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
set(field, value) {
|
|
49
|
+
if (field.toLowerCase() === 'set-cookie') {
|
|
50
|
+
this.appendHeader(field, value);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
this.reply.header(field, value);
|
|
54
|
+
}
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
type(contentType) {
|
|
58
|
+
const normalized = contentType === 'html'
|
|
59
|
+
? 'text/html'
|
|
60
|
+
: contentType === 'json'
|
|
61
|
+
? 'application/json'
|
|
62
|
+
: contentType;
|
|
63
|
+
this.reply.type(normalized);
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
json(payload) {
|
|
67
|
+
this.reply.send(payload);
|
|
68
|
+
}
|
|
69
|
+
send(payload) {
|
|
70
|
+
this.reply.send(payload);
|
|
71
|
+
}
|
|
72
|
+
redirect(statusOrUrl, maybeUrl) {
|
|
73
|
+
if (typeof statusOrUrl === 'number') {
|
|
74
|
+
if (maybeUrl === undefined) {
|
|
75
|
+
throw new Error('redirect url is required when status code is provided');
|
|
76
|
+
}
|
|
77
|
+
this.reply.status(statusOrUrl);
|
|
78
|
+
this.reply.header('Location', maybeUrl);
|
|
79
|
+
this.reply.send();
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
this.reply.redirect(statusOrUrl);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
cookie(name, value, options) {
|
|
86
|
+
const serialized = serializeCookie(name, value, options);
|
|
87
|
+
this.appendHeader('Set-Cookie', serialized);
|
|
88
|
+
}
|
|
89
|
+
appendHeader(name, value) {
|
|
90
|
+
const existing = this.reply.getHeader(name);
|
|
91
|
+
if (Array.isArray(existing)) {
|
|
92
|
+
this.reply.header(name, [...existing, value]);
|
|
93
|
+
}
|
|
94
|
+
else if (typeof existing === 'string') {
|
|
95
|
+
this.reply.header(name, [existing, value]);
|
|
96
|
+
}
|
|
97
|
+
else if (existing === undefined) {
|
|
98
|
+
this.reply.header(name, value);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
this.reply.header(name, [String(existing), value]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function toCompatRequest(request) {
|
|
106
|
+
const headers = {};
|
|
107
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
108
|
+
if (typeof value === 'string') {
|
|
109
|
+
headers[key.toLowerCase()] = value;
|
|
110
|
+
}
|
|
111
|
+
else if (Array.isArray(value)) {
|
|
112
|
+
headers[key.toLowerCase()] = value.join(', ');
|
|
113
|
+
}
|
|
114
|
+
else if (value !== undefined && value !== null) {
|
|
115
|
+
headers[key.toLowerCase()] = String(value);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
headers[key.toLowerCase()] = undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
body: request.body,
|
|
123
|
+
headers,
|
|
124
|
+
method: request.method,
|
|
125
|
+
originalUrl: request.raw.url ?? request.url,
|
|
126
|
+
query: request.query ?? {},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function serializeCookie(name, value, options) {
|
|
130
|
+
const segments = [
|
|
131
|
+
`${encodeURIComponent(name)}=${encodeURIComponent(value)}`,
|
|
132
|
+
];
|
|
133
|
+
if (options.maxAge !== undefined) {
|
|
134
|
+
const maxAgeMs = options.maxAge;
|
|
135
|
+
const maxAgeSeconds = Math.floor(maxAgeMs / 1000);
|
|
136
|
+
segments.push(`Max-Age=${maxAgeSeconds}`);
|
|
137
|
+
const expires = new Date(Date.now() + maxAgeMs).toUTCString();
|
|
138
|
+
segments.push(`Expires=${expires}`);
|
|
139
|
+
}
|
|
140
|
+
segments.push(`Path=${options.path ?? '/'}`);
|
|
141
|
+
if (options.httpOnly) {
|
|
142
|
+
segments.push('HttpOnly');
|
|
143
|
+
}
|
|
144
|
+
if (options.secure) {
|
|
145
|
+
segments.push('Secure');
|
|
146
|
+
}
|
|
147
|
+
if (options.sameSite) {
|
|
148
|
+
const normalized = options.sameSite.toLowerCase();
|
|
149
|
+
const formatted = normalized === 'strict'
|
|
150
|
+
? 'Strict'
|
|
151
|
+
: normalized === 'none'
|
|
152
|
+
? 'None'
|
|
153
|
+
: 'Lax';
|
|
154
|
+
segments.push(`SameSite=${formatted}`);
|
|
155
|
+
}
|
|
156
|
+
return segments.join('; ');
|
|
157
|
+
}
|
|
13
158
|
const DEFAULT_PREFIX = '/oauth';
|
|
14
159
|
const ENV_VAR_CLIENT_ID = 'FAME_JWT_CLIENT_ID';
|
|
15
160
|
const ENV_VAR_CLIENT_SECRET = 'FAME_JWT_CLIENT_SECRET';
|
|
@@ -431,11 +576,11 @@ function respondInvalidClient(res) {
|
|
|
431
576
|
});
|
|
432
577
|
}
|
|
433
578
|
/**
|
|
434
|
-
* Create
|
|
579
|
+
* Create a Fastify plugin that implements OAuth2 token and authorization endpoints
|
|
435
580
|
* with support for client credentials and authorization code (PKCE) grants.
|
|
436
581
|
*
|
|
437
582
|
* @param options - Router configuration options
|
|
438
|
-
* @returns
|
|
583
|
+
* @returns Fastify plugin with OAuth2 token and authorization endpoints
|
|
439
584
|
*
|
|
440
585
|
* Environment Variables:
|
|
441
586
|
* FAME_JWT_CLIENT_ID: OAuth2 client identifier
|
|
@@ -449,7 +594,7 @@ function respondInvalidClient(res) {
|
|
|
449
594
|
* FAME_OAUTH_CODE_TTL_SEC: Authorization code TTL in seconds (optional, default: 300)
|
|
450
595
|
*/
|
|
451
596
|
export function createOAuth2TokenRouter(options) {
|
|
452
|
-
const router =
|
|
597
|
+
const router = new RouterCompat();
|
|
453
598
|
const { cryptoProvider, prefix = DEFAULT_PREFIX, issuer, audience, tokenTtlSec, allowedScopes: configAllowedScopes, algorithm: configAlgorithm, enablePkce: configEnablePkce, allowPublicClients: configAllowPublicClients, authorizationCodeTtlSec: configAuthorizationCodeTtlSec, enableDevLogin: configEnableDevLogin, devLoginUsername: configDevLoginUsername, devLoginPassword: configDevLoginPassword, devLoginSessionTtlSec: configDevLoginSessionTtlSec, devLoginCookieName: configDevLoginCookieName, devLoginSecureCookie: configDevLoginSecureCookie, devLoginTitle: configDevLoginTitle, } = normalizeCreateOAuth2TokenRouterOptions(options);
|
|
454
599
|
if (!cryptoProvider) {
|
|
455
600
|
throw new Error('cryptoProvider is required to create OAuth2 token router');
|
|
@@ -748,7 +893,7 @@ export function createOAuth2TokenRouter(options) {
|
|
|
748
893
|
};
|
|
749
894
|
router.post(`${prefix}/logout`, logoutHandler);
|
|
750
895
|
router.get(`${prefix}/logout`, logoutHandler);
|
|
751
|
-
router.post(`${prefix}/token`, async (req, res
|
|
896
|
+
router.post(`${prefix}/token`, async (req, res) => {
|
|
752
897
|
try {
|
|
753
898
|
cleanupAuthorizationCodes(authorizationCodes, Date.now());
|
|
754
899
|
const { grant_type, client_id, client_secret, scope, audience: reqAudience, code, redirect_uri, code_verifier, } = req.body ?? {};
|
|
@@ -933,7 +1078,7 @@ export function createOAuth2TokenRouter(options) {
|
|
|
933
1078
|
}
|
|
934
1079
|
catch (error) {
|
|
935
1080
|
logger.error('oauth2_token_error', { error: error.message });
|
|
936
|
-
|
|
1081
|
+
throw error;
|
|
937
1082
|
}
|
|
938
1083
|
});
|
|
939
1084
|
async function issueTokenResponse(params) {
|
|
@@ -966,5 +1111,5 @@ export function createOAuth2TokenRouter(options) {
|
|
|
966
1111
|
}
|
|
967
1112
|
return response;
|
|
968
1113
|
}
|
|
969
|
-
return router;
|
|
1114
|
+
return router.toPlugin();
|
|
970
1115
|
}
|