@txstate-mws/graphql-server
Version:
A simple graphql server designed to work with typegraphql.
168 lines (167 loc) • 6.7 kB
JavaScript
;
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;