UNPKG

@txstate-mws/graphql-server

Version:

A simple graphql server designed to work with typegraphql.

168 lines (167 loc) 6.7 kB
"use strict"; var _a; Object.defineProperty(exports, "__esModule", { value: true }); exports.FastifyTxStateContext = exports.TxStateUAuthContext = exports.Context = exports.MockContext = void 0; exports.cleanPem = cleanPem; const node_crypto_1 = require("node:crypto"); const dataloader_factory_1 = require("dataloader-factory"); const jose_1 = require("jose"); const txstate_utils_1 = require("txstate-utils"); const errors_1 = require("./errors"); function cleanPem(secretOrPem) { return secretOrPem?.replace(/(-+BEGIN [\w\s]+ KEY-+)\s*(.*?)\s*(-+END [\w\s]+ KEY-+)/, '$1\n$2\n$3'); } class MockContext { constructor(auth) { this.loaders = new dataloader_factory_1.DataLoaderFactory(this); this.serviceInstances = new Map(); this.auth = auth; } async waitForAuth() { } static init() { } svc(ServiceType) { if (!this.serviceInstances.has(ServiceType)) this.serviceInstances.set(ServiceType, new ServiceType(this)); return this.serviceInstances.get(ServiceType); } timing(...messages) { const now = new Date(); console.debug(now.getTime() - (this.lasttime ?? now).getTime(), ...messages); this.lasttime = now; } authForLog() { return this.auth; } requireAuth() { if (this.auth == null) throw new errors_1.AuthError(); } async query(query, variables) { return await MockContext.executeQuery(this, query, variables); } setParts(parts) { this.parts = parts; } async *files() { if (!this.parts) return; let idx = 0; for await (const p of this.parts) { if (p.type === 'file') { yield { multipartIndex: idx, name: p.filename, mime: p.mimetype, stream: p.file }; idx++; } } } } exports.MockContext = MockContext; class Context extends MockContext { constructor(req) { super(undefined); this.authPromise = this.authFromReq(req); } async waitForAuth() { this.auth = await this.authPromise; } static init() { let secret = cleanPem(process.env.JWT_SECRET_VERIFY); if (secret != null) { _a.jwtVerifyKey = (0, node_crypto_1.createPublicKey)(secret); } else { secret = cleanPem(process.env.JWT_SECRET); if (secret != null) { try { _a.jwtVerifyKey = (0, node_crypto_1.createPublicKey)(secret); } catch (e) { console.info('JWT_SECRET was not a private key, treating it as symmetric.'); _a.jwtVerifyKey = (0, node_crypto_1.createSecretKey)(Buffer.from(secret, 'ascii')); } } } if (process.env.JWT_TRUSTED_ISSUERS) { const issuers = (0, txstate_utils_1.toArray)(JSON.parse(process.env.JWT_TRUSTED_ISSUERS)); for (const issuer of issuers) { this.issuerConfig.set(issuer.iss, this.processIssuerConfig?.((0, txstate_utils_1.omit)(issuer, 'publicKey', 'secret'))); if (issuer.url) this.issuerKeys.set(issuer.iss, (0, jose_1.createRemoteJWKSet)(new URL(issuer.url))); else if (issuer.publicKey) this.issuerKeys.set(issuer.iss, (0, node_crypto_1.createPublicKey)(issuer.publicKey)); else if (issuer.secret) this.issuerKeys.set(issuer.iss, (0, node_crypto_1.createSecretKey)(Buffer.from(issuer.secret, 'ascii'))); } } } tokenFromReq(req) { const m = req?.headers.authorization?.match(/^bearer (.*)$/i); return m?.[1]; } async authFromReq(req) { const token = this.tokenFromReq(req); if (!token) return undefined; return await this.constructor.tokenCache.get(token, { req, ctx: this }); } async authFromPayload(payload) { return payload; } } exports.Context = Context; _a = Context; Context.issuerKeys = new Map(); Context.issuerConfig = new Map(); Context.tokenCache = new txstate_utils_1.Cache(async (token, { req, ctx }) => { // `this` is always the Context class, even if we are making instances of a subclass of Context // we need to get the instance's constructor instead in case it has overridden one of our // static methods/variables const ctxStatic = ctx.constructor; const logger = req?.log ?? console; let verifyKey = _a.jwtVerifyKey; try { const claims = (0, jose_1.decodeJwt)(token); if (claims.iss && ctxStatic.issuerKeys.has(claims.iss)) verifyKey = ctxStatic.issuerKeys.get(claims.iss); if (!verifyKey) { logger.info(`Received token with issuer: ${claims.iss} but JWT secret could not be found. The server may be misconfigured or the user may have presented a JWT from an untrusted issuer.`); return undefined; } await ctxStatic.validateToken?.(token, ctxStatic.issuerConfig.get(claims.iss), claims); const { payload } = await (0, jose_1.jwtVerify)(token, verifyKey); return await ctx.authFromPayload(payload); } catch (e) { // squelch errors about bad tokens, we can already see the 401 in the log if (e.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') logger.error(e); return undefined; } }, { freshseconds: 10 }); class TxStateUAuthContext extends Context { static processIssuerConfig(config) { if (config.iss === 'unified-auth') { config.validateUrl = new URL(config.url); config.validateUrl.pathname = '/validateToken'; } return config; } static async validateToken(token, issuerConfig, claims) { if (claims.iss === 'unified-auth') { const validateUrl = new URL(issuerConfig.validateUrl); validateUrl.searchParams.set('unifiedJwt', token); const resp = await fetch(validateUrl); const validate = await resp.json(); if (!validate.valid) throw new Error(validate.reason ?? 'Your session has been ended on another device or in another browser tab/window. It\'s also possible your NetID is no longer active.'); } } } exports.TxStateUAuthContext = TxStateUAuthContext; class FastifyTxStateContext extends Context { init() { } async authFromReq(req) { return req?.auth; } authForLog() { return this.auth ? (0, txstate_utils_1.omit)(this.auth, 'token', 'issuerConfig') : undefined; } } exports.FastifyTxStateContext = FastifyTxStateContext;