fastify-txstate 3.2.16 → 3.3.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.
- package/README.md +22 -7
- package/lib/error.d.ts +18 -5
- package/lib/error.js +25 -1
- package/lib/index.js +12 -3
- package/lib/unified-auth.d.ts +2 -61
- package/lib/unified-auth.js +94 -141
- package/lib-esm/index.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,19 +28,34 @@ server.app.get('/yourpath', async (req, res) => {
|
|
|
28
28
|
This will result in a 400 error being returned to the client, with a plain text body: `Please provide an id.`
|
|
29
29
|
|
|
30
30
|
You may skip the message string and a default will be used, e.g. `throw new HttpError(401)` sends a plain text body: `Authentication is required.`
|
|
31
|
-
##
|
|
32
|
-
This class helps an API communicate with its client about errors that occured during a validation or writing operation.
|
|
31
|
+
## ValidationErrors
|
|
32
|
+
This class helps an API communicate with its client about errors that occured during a validation or writing operation. The constructor takes three arguments: a message to be displayed to the user, a dot-separated path to the property of the input object that the message is related to, and a message type, which could be 'error', 'info', 'warning', 'success', or 'system' (system is for errors that the user is not responsible for like a database being offline).
|
|
33
33
|
```javascript
|
|
34
|
-
|
|
34
|
+
import { ValidationErrors } from 'fastify-txstate'
|
|
35
|
+
import { hasFatalErrors } from '@txstate-mws/fastify-shared'
|
|
35
36
|
server.app.post('/saveathing', async (req, res) => {
|
|
36
37
|
const thing = req.body
|
|
37
|
-
const
|
|
38
|
-
if (!thing.title)
|
|
39
|
-
if (!thing?.address?.zip)
|
|
40
|
-
if (
|
|
38
|
+
const messages = []
|
|
39
|
+
if (!thing.title) messages.push({ message: 'Title is required.', path: 'title', type: 'error' })
|
|
40
|
+
if (!thing?.address?.zip) messages.push({ message: 'Zip code is required.', path: 'address.zip', type: 'error' })
|
|
41
|
+
if (hasFatalErrors(messages)) throw new ValidationErrors(messages)
|
|
41
42
|
/* continue processing request */
|
|
42
43
|
})
|
|
43
44
|
```
|
|
45
|
+
The client will receive HTTP status 422 and a JSON body that looks like this:
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"success": false,
|
|
49
|
+
"messages": [
|
|
50
|
+
{ "type": "error", "message": "Zip code is required.", "path": "address.zip" }
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
This format is well supported by our @txstate-mws/svelte-forms library, so it should be easy to pass the errors into your form.
|
|
55
|
+
|
|
56
|
+
### ValidationError
|
|
57
|
+
`ValidationErrors` is preferred since it will show multiple errors at once, instead of making the user fix errors one at a time and not know how far they are from being done. If you just need to throw a quick single error, `throw new ValidationError('Wrong!', 'answer')` is also available.
|
|
58
|
+
|
|
44
59
|
## Custom Error Handling
|
|
45
60
|
If you would like special treatment for certain errors, `addErrorHandler` provides an easy way:
|
|
46
61
|
```javascript
|
package/lib/error.d.ts
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ValidationMessage } from '@txstate-mws/fastify-shared';
|
|
2
|
+
import type { FastifySchemaValidationError } from 'fastify/types/schema';
|
|
2
3
|
export declare class HttpError extends Error {
|
|
3
4
|
statusCode: number;
|
|
4
5
|
constructor(statusCode: number, message?: string);
|
|
5
6
|
}
|
|
6
|
-
|
|
7
|
+
/**
|
|
8
|
+
* @deprecated This response format is less flexible than the one based on the ValidationMessage
|
|
9
|
+
* interface. Use ValidationError or ValidationErrors instead, and adjust the client to expect
|
|
10
|
+
* the new format.
|
|
11
|
+
*/
|
|
7
12
|
export declare class FailedValidationError extends HttpError {
|
|
8
|
-
errors:
|
|
9
|
-
constructor(errors:
|
|
13
|
+
errors: Record<string, string[]>;
|
|
14
|
+
constructor(errors: Record<string, string[]>);
|
|
15
|
+
}
|
|
16
|
+
export declare class ValidationError extends HttpError {
|
|
17
|
+
path?: string | undefined;
|
|
18
|
+
type?: "error" | "success" | "warning" | "system" | undefined;
|
|
19
|
+
constructor(message: string, path?: string | undefined, type?: "error" | "success" | "warning" | "system" | undefined);
|
|
20
|
+
}
|
|
21
|
+
export declare class ValidationErrors extends HttpError {
|
|
22
|
+
errors: ValidationMessage[];
|
|
23
|
+
constructor(errors: ValidationMessage[]);
|
|
10
24
|
}
|
|
11
25
|
export declare function fstValidationToMessage(v: FastifySchemaValidationError): {
|
|
12
26
|
message: string | undefined;
|
|
13
27
|
path: string;
|
|
14
28
|
type: string;
|
|
15
29
|
};
|
|
16
|
-
export {};
|
package/lib/error.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.fstValidationToMessage = exports.FailedValidationError = exports.HttpError = void 0;
|
|
3
|
+
exports.fstValidationToMessage = exports.ValidationErrors = exports.ValidationError = exports.FailedValidationError = exports.HttpError = void 0;
|
|
4
4
|
const http_status_codes_1 = require("http-status-codes");
|
|
5
5
|
class HttpError extends Error {
|
|
6
6
|
statusCode;
|
|
@@ -18,14 +18,38 @@ class HttpError extends Error {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
exports.HttpError = HttpError;
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated This response format is less flexible than the one based on the ValidationMessage
|
|
23
|
+
* interface. Use ValidationError or ValidationErrors instead, and adjust the client to expect
|
|
24
|
+
* the new format.
|
|
25
|
+
*/
|
|
21
26
|
class FailedValidationError extends HttpError {
|
|
22
27
|
errors;
|
|
23
28
|
constructor(errors) {
|
|
24
29
|
super(422, 'Validation failure.');
|
|
25
30
|
this.errors = errors;
|
|
31
|
+
this.errors = errors;
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
34
|
exports.FailedValidationError = FailedValidationError;
|
|
35
|
+
class ValidationError extends HttpError {
|
|
36
|
+
path;
|
|
37
|
+
type;
|
|
38
|
+
constructor(message, path, type) {
|
|
39
|
+
super(422, message);
|
|
40
|
+
this.path = path;
|
|
41
|
+
this.type = type;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.ValidationError = ValidationError;
|
|
45
|
+
class ValidationErrors extends HttpError {
|
|
46
|
+
errors;
|
|
47
|
+
constructor(errors) {
|
|
48
|
+
super(422, errors[0]?.message);
|
|
49
|
+
this.errors = errors;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.ValidationErrors = ValidationErrors;
|
|
29
53
|
function fstValidationToMessage(v) {
|
|
30
54
|
const instancePath = v.keyword === 'required' ? v.instancePath + '/' + v.params.missingProperty : v.instancePath;
|
|
31
55
|
return { message: v.message, path: instancePath.substring(1).replace(/\//g, '.'), type: 'error' };
|
package/lib/index.js
CHANGED
|
@@ -153,12 +153,15 @@ class Server {
|
|
|
153
153
|
return JSON.stringify(data);
|
|
154
154
|
};
|
|
155
155
|
});
|
|
156
|
+
this.app.addHook('onRequest', (req, res, done) => {
|
|
157
|
+
res.extraLogInfo = {};
|
|
158
|
+
done();
|
|
159
|
+
});
|
|
156
160
|
if (!config.skipOriginCheck && !process.env.SKIP_ORIGIN_CHECK) {
|
|
157
161
|
this.setValidOrigins([...(config.validOrigins ?? []), ...(process.env.VALID_ORIGINS?.split(',') ?? [])]);
|
|
158
162
|
this.setValidOriginHosts([...(config.validOriginHosts ?? []), ...(process.env.VALID_ORIGIN_HOSTS?.split(',') ?? [])]);
|
|
159
163
|
this.setValidOriginSuffixes([...(config.validOriginSuffixes ?? []), ...(process.env.VALID_ORIGIN_SUFFIXES?.split(',') ?? [])]);
|
|
160
164
|
this.app.addHook('onRequest', async (req, res) => {
|
|
161
|
-
res.extraLogInfo = {};
|
|
162
165
|
if (!req.headers.origin)
|
|
163
166
|
return;
|
|
164
167
|
let passed = this.validOrigins[req.headers.origin];
|
|
@@ -207,7 +210,7 @@ class Server {
|
|
|
207
210
|
DELETE: true
|
|
208
211
|
};
|
|
209
212
|
this.app.addHook('onRequest', async (req, res) => {
|
|
210
|
-
if (!authenticatedMethods[req.method])
|
|
213
|
+
if (!authenticatedMethods[req.method] || req.routeOptions.url === '/health')
|
|
211
214
|
return;
|
|
212
215
|
try {
|
|
213
216
|
req.auth = await config.authenticate(req);
|
|
@@ -241,6 +244,12 @@ class Server {
|
|
|
241
244
|
if (err instanceof error_1.FailedValidationError) {
|
|
242
245
|
await res.status(err.statusCode).send(err.errors);
|
|
243
246
|
}
|
|
247
|
+
else if (err instanceof error_1.ValidationError) {
|
|
248
|
+
await res.status(err.statusCode).send({ success: false, messages: [{ message: err.message, path: err.path, type: err.type ?? 'error' }] });
|
|
249
|
+
}
|
|
250
|
+
else if (err instanceof error_1.ValidationErrors) {
|
|
251
|
+
await res.status(err.statusCode).send({ success: false, messages: err.errors });
|
|
252
|
+
}
|
|
244
253
|
else if (err instanceof error_1.HttpError) {
|
|
245
254
|
await res.status(err.statusCode).send(err.message);
|
|
246
255
|
}
|
|
@@ -343,7 +352,7 @@ class Server {
|
|
|
343
352
|
setValidOriginSuffixes(suffixes) {
|
|
344
353
|
this.validOriginSuffixes.clear();
|
|
345
354
|
for (const s of suffixes)
|
|
346
|
-
this.validOriginSuffixes.add(s);
|
|
355
|
+
this.validOriginSuffixes.add(s.replace(/^\./, ''));
|
|
347
356
|
}
|
|
348
357
|
async swagger(opts) {
|
|
349
358
|
let openapi = opts?.openapi ?? {};
|
package/lib/unified-auth.d.ts
CHANGED
|
@@ -1,63 +1,4 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
/// <reference types="node" />
|
|
3
|
-
import { type KeyObject } from 'crypto';
|
|
4
1
|
import { type FastifyRequest } from 'fastify';
|
|
5
|
-
import { type JWTPayload, type JWTVerifyGetKey, type KeyLike } from 'jose';
|
|
6
|
-
import { Cache } from 'txstate-utils';
|
|
7
2
|
import { type FastifyTxStateAuthInfo } from '.';
|
|
8
|
-
declare
|
|
9
|
-
|
|
10
|
-
constructor(auth: any);
|
|
11
|
-
waitForAuth(): Promise<AuthType | undefined>;
|
|
12
|
-
static init(): void;
|
|
13
|
-
}
|
|
14
|
-
interface IssuerConfig {
|
|
15
|
-
iss: string;
|
|
16
|
-
url?: string;
|
|
17
|
-
publicKey?: string;
|
|
18
|
-
secret?: string;
|
|
19
|
-
validateUrl: URL;
|
|
20
|
-
}
|
|
21
|
-
declare class Context<AuthType extends FastifyTxStateAuthInfo = FastifyTxStateAuthInfo> extends MockContext<AuthType> {
|
|
22
|
-
private readonly authPromise;
|
|
23
|
-
protected static jwtVerifyKey: KeyObject | undefined;
|
|
24
|
-
protected static issuerKeys: Map<string, KeyLike | JWTVerifyGetKey>;
|
|
25
|
-
protected static issuerConfig: Map<string, any>;
|
|
26
|
-
protected static tokenCache: Cache<string, any, {
|
|
27
|
-
req?: FastifyRequest<import("fastify").RouteGenericInterface, import("fastify").RawServerDefault, import("http").IncomingMessage, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown, import("fastify").FastifyBaseLogger, import("fastify/types/type-provider").ResolveFastifyRequestType<import("fastify").FastifyTypeProviderDefault, import("fastify").FastifySchema, import("fastify").RouteGenericInterface>> | undefined;
|
|
28
|
-
ctx: Context;
|
|
29
|
-
}>;
|
|
30
|
-
constructor(req?: FastifyRequest);
|
|
31
|
-
waitForAuth(): Promise<AuthType | undefined>;
|
|
32
|
-
protected static hasInitialized: boolean;
|
|
33
|
-
static init(): void;
|
|
34
|
-
/**
|
|
35
|
-
* If implemented, this method will be called on startup, once per configured issuer. It receives
|
|
36
|
-
* the issuer configuration from the JWT_TRUSTED_ISSUERS environment variable and allows you to manipulate
|
|
37
|
-
* the configuration before storing it.
|
|
38
|
-
*
|
|
39
|
-
* Once stored, whatever you create may be used in your custom validateToken method. For example,
|
|
40
|
-
* you might want to create an in-memory URL object with an issuer's URL so that it can be manipulated
|
|
41
|
-
* easily to send validation checks to the issuer.
|
|
42
|
-
*/
|
|
43
|
-
static processIssuerConfig: undefined | ((config: any) => any);
|
|
44
|
-
/**
|
|
45
|
-
* If implemented, this method is called after a token's signature is checked and passes. You would
|
|
46
|
-
* typically implement this method to check whether the user has manually signed out, or the token has
|
|
47
|
-
* been otherwise deauthorized before its expiration date.
|
|
48
|
-
*
|
|
49
|
-
* If the token is not valid, this method should throw an error with an appropriate message.
|
|
50
|
-
*/
|
|
51
|
-
static validateToken: undefined | ((token: string, issuerConfig: any, claims: JWTPayload) => void | Promise<void>);
|
|
52
|
-
tokenFromReq(req?: FastifyRequest): string | undefined;
|
|
53
|
-
authFromReq(req?: FastifyRequest): Promise<AuthType | undefined>;
|
|
54
|
-
authFromPayload(payload: JWTPayload): Promise<AuthType>;
|
|
55
|
-
}
|
|
56
|
-
declare class TxStateUAuthContext extends Context {
|
|
57
|
-
static processIssuerConfig(config: IssuerConfig): IssuerConfig;
|
|
58
|
-
static validateToken(token: string, issuerConfig: IssuerConfig, claims: any): Promise<void>;
|
|
59
|
-
authFromPayload(payload: JWTPayload): Promise<FastifyTxStateAuthInfo>;
|
|
60
|
-
}
|
|
61
|
-
export declare function unifiedAuthenticate(req: FastifyRequest, ContextClass?: typeof TxStateUAuthContext): Promise<FastifyTxStateAuthInfo | undefined>;
|
|
62
|
-
export declare function unifiedAuthenticateAll(req: FastifyRequest, ContextClass?: typeof TxStateUAuthContext): Promise<FastifyTxStateAuthInfo | undefined>;
|
|
63
|
-
export {};
|
|
3
|
+
export declare function unifiedAuthenticate(req: FastifyRequest): Promise<FastifyTxStateAuthInfo | undefined>;
|
|
4
|
+
export declare function unifiedAuthenticateAll(req: FastifyRequest): Promise<FastifyTxStateAuthInfo>;
|
package/lib/unified-auth.js
CHANGED
|
@@ -4,161 +4,114 @@ exports.unifiedAuthenticateAll = exports.unifiedAuthenticate = void 0;
|
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const jose_1 = require("jose");
|
|
6
6
|
const txstate_utils_1 = require("txstate-utils");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
let hasInit = false;
|
|
8
|
+
const issuerKeys = new Map();
|
|
9
|
+
const issuerConfig = new Map();
|
|
10
|
+
const trustedClients = new Set();
|
|
11
|
+
const tokenCache = new txstate_utils_1.Cache(async (token, req) => {
|
|
12
|
+
const claims = (0, jose_1.decodeJwt)(token);
|
|
13
|
+
let verifyKey;
|
|
14
|
+
if (claims.iss && issuerKeys.has(claims.iss))
|
|
15
|
+
verifyKey = issuerKeys.get(claims.iss);
|
|
16
|
+
if (!verifyKey) {
|
|
17
|
+
req.log.warn(`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.`);
|
|
18
|
+
return undefined;
|
|
17
19
|
}
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
static jwtVerifyKey;
|
|
23
|
-
static issuerKeys = new Map();
|
|
24
|
-
static issuerConfig = new Map();
|
|
25
|
-
static tokenCache = new txstate_utils_1.Cache(async (token, { req, ctx }) => {
|
|
26
|
-
// `this` is always the Context class, even if we are making instances of a subclass of Context
|
|
27
|
-
// we need to get the instance's constructor instead in case it has overridden one of our
|
|
28
|
-
// static methods/variables
|
|
29
|
-
const ctxStatic = ctx.constructor;
|
|
30
|
-
const logger = req?.log ?? console;
|
|
31
|
-
let verifyKey = Context.jwtVerifyKey;
|
|
32
|
-
try {
|
|
33
|
-
const claims = (0, jose_1.decodeJwt)(token);
|
|
34
|
-
if (claims.iss && ctxStatic.issuerKeys.has(claims.iss))
|
|
35
|
-
verifyKey = ctxStatic.issuerKeys.get(claims.iss);
|
|
36
|
-
if (!verifyKey) {
|
|
37
|
-
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.`);
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
await ctxStatic.validateToken?.(token, ctxStatic.issuerConfig.get(claims.iss), claims);
|
|
41
|
-
const { payload } = await (0, jose_1.jwtVerify)(token, verifyKey);
|
|
42
|
-
return await ctx.authFromPayload(payload);
|
|
43
|
-
}
|
|
44
|
-
catch (e) {
|
|
45
|
-
// squelch errors about bad tokens, we can already see the 401 in the log
|
|
46
|
-
if (e.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED')
|
|
47
|
-
logger.error(e);
|
|
20
|
+
try {
|
|
21
|
+
const { payload } = await (0, jose_1.jwtVerify)(token, verifyKey);
|
|
22
|
+
if (trustedClients.size && !trustedClients.has(payload.client_id)) {
|
|
23
|
+
req.log.warn(`Received token with untrusted client_id: ${payload.client_id}.`);
|
|
48
24
|
return undefined;
|
|
49
25
|
}
|
|
50
|
-
|
|
51
|
-
constructor(req) {
|
|
52
|
-
super(undefined);
|
|
53
|
-
this.authPromise = this.authFromReq(req);
|
|
54
|
-
}
|
|
55
|
-
async waitForAuth() {
|
|
56
|
-
this.auth = await this.authPromise;
|
|
57
|
-
return this.auth;
|
|
58
|
-
}
|
|
59
|
-
static hasInitialized = false;
|
|
60
|
-
static init() {
|
|
61
|
-
if (this.hasInitialized)
|
|
62
|
-
return;
|
|
63
|
-
this.hasInitialized = true;
|
|
64
|
-
let secret = cleanPem(process.env.JWT_SECRET_VERIFY);
|
|
65
|
-
if (secret != null) {
|
|
66
|
-
Context.jwtVerifyKey = (0, crypto_1.createPublicKey)(secret);
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
secret = cleanPem(process.env.JWT_SECRET);
|
|
70
|
-
if (secret != null) {
|
|
71
|
-
try {
|
|
72
|
-
Context.jwtVerifyKey = (0, crypto_1.createPublicKey)(secret);
|
|
73
|
-
}
|
|
74
|
-
catch (e) {
|
|
75
|
-
console.info('JWT_SECRET was not a private key, treating it as symmetric.');
|
|
76
|
-
Context.jwtVerifyKey = (0, crypto_1.createSecretKey)(Buffer.from(secret, 'ascii'));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (process.env.JWT_TRUSTED_ISSUERS) {
|
|
81
|
-
const issuers = (0, txstate_utils_1.toArray)(JSON.parse(process.env.JWT_TRUSTED_ISSUERS));
|
|
82
|
-
for (const issuer of issuers) {
|
|
83
|
-
this.issuerConfig.set(issuer.iss, this.processIssuerConfig?.((0, txstate_utils_1.omit)(issuer, 'publicKey', 'secret')));
|
|
84
|
-
if (issuer.url)
|
|
85
|
-
this.issuerKeys.set(issuer.iss, (0, jose_1.createRemoteJWKSet)(new URL(issuer.url)));
|
|
86
|
-
else if (issuer.publicKey)
|
|
87
|
-
this.issuerKeys.set(issuer.iss, (0, crypto_1.createPublicKey)(issuer.publicKey));
|
|
88
|
-
else if (issuer.secret)
|
|
89
|
-
this.issuerKeys.set(issuer.iss, (0, crypto_1.createSecretKey)(Buffer.from(issuer.secret, 'ascii')));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* If implemented, this method will be called on startup, once per configured issuer. It receives
|
|
95
|
-
* the issuer configuration from the JWT_TRUSTED_ISSUERS environment variable and allows you to manipulate
|
|
96
|
-
* the configuration before storing it.
|
|
97
|
-
*
|
|
98
|
-
* Once stored, whatever you create may be used in your custom validateToken method. For example,
|
|
99
|
-
* you might want to create an in-memory URL object with an issuer's URL so that it can be manipulated
|
|
100
|
-
* easily to send validation checks to the issuer.
|
|
101
|
-
*/
|
|
102
|
-
static processIssuerConfig;
|
|
103
|
-
/**
|
|
104
|
-
* If implemented, this method is called after a token's signature is checked and passes. You would
|
|
105
|
-
* typically implement this method to check whether the user has manually signed out, or the token has
|
|
106
|
-
* been otherwise deauthorized before its expiration date.
|
|
107
|
-
*
|
|
108
|
-
* If the token is not valid, this method should throw an error with an appropriate message.
|
|
109
|
-
*/
|
|
110
|
-
static validateToken;
|
|
111
|
-
tokenFromReq(req) {
|
|
112
|
-
const m = req?.headers.authorization?.match(/^bearer (.*)$/i);
|
|
113
|
-
return m?.[1];
|
|
26
|
+
return payload;
|
|
114
27
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
return
|
|
28
|
+
catch (e) {
|
|
29
|
+
// squelch errors about bad tokens, we can already see the 401 in the log
|
|
30
|
+
if (e.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED')
|
|
31
|
+
req.log.error(e);
|
|
32
|
+
return undefined;
|
|
120
33
|
}
|
|
121
|
-
|
|
122
|
-
|
|
34
|
+
}, { freshseconds: 3600 });
|
|
35
|
+
const validateCache = new txstate_utils_1.Cache(async (token, payload) => {
|
|
36
|
+
const config = issuerConfig.get(payload.iss);
|
|
37
|
+
if (!config?.validateUrl)
|
|
38
|
+
return;
|
|
39
|
+
// avoid checking for deauth until the token is more than 5 minutes old
|
|
40
|
+
if (new Date(payload.iat * 1000) > new Date(new Date().getTime() - 1000 * 60 * 5))
|
|
41
|
+
return;
|
|
42
|
+
const validateUrl = new URL(config.validateUrl);
|
|
43
|
+
validateUrl.searchParams.set('unifiedJwt', token);
|
|
44
|
+
const resp = await fetch(validateUrl);
|
|
45
|
+
const validate = await resp.json();
|
|
46
|
+
if (!validate.valid)
|
|
47
|
+
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.');
|
|
48
|
+
});
|
|
49
|
+
const jwkCache = new txstate_utils_1.Cache(async (url) => {
|
|
50
|
+
const { keys } = await (await fetch(url)).json();
|
|
51
|
+
const publicKeyByKid = {};
|
|
52
|
+
for (const jwk of keys) {
|
|
53
|
+
if (jwk.kid)
|
|
54
|
+
publicKeyByKid[jwk.kid] = await (0, jose_1.importJWK)(jwk);
|
|
123
55
|
}
|
|
56
|
+
return publicKeyByKid;
|
|
57
|
+
});
|
|
58
|
+
function remoteJWKSet(jwkUrl) {
|
|
59
|
+
return async (protectedHeader) => {
|
|
60
|
+
const publicKeyByKid = await jwkCache.get(jwkUrl);
|
|
61
|
+
return publicKeyByKid[protectedHeader.kid];
|
|
62
|
+
};
|
|
124
63
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
config.validateUrl.pathname = '/validateToken';
|
|
130
|
-
}
|
|
131
|
-
return config;
|
|
64
|
+
function processIssuerConfig(config) {
|
|
65
|
+
if (config.iss === 'unified-auth') {
|
|
66
|
+
config.validateUrl = new URL(config.url ?? '');
|
|
67
|
+
config.validateUrl.pathname = '/validateToken';
|
|
132
68
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
69
|
+
return config;
|
|
70
|
+
}
|
|
71
|
+
function init() {
|
|
72
|
+
hasInit = true;
|
|
73
|
+
if (process.env.JWT_TRUSTED_ISSUERS) {
|
|
74
|
+
const issuers = (0, txstate_utils_1.toArray)(JSON.parse(process.env.JWT_TRUSTED_ISSUERS));
|
|
75
|
+
for (const issuer of issuers) {
|
|
76
|
+
issuerConfig.set(issuer.iss, processIssuerConfig(issuer));
|
|
77
|
+
if (issuer.iss === 'unified-auth')
|
|
78
|
+
issuerKeys.set(issuer.iss, remoteJWKSet(issuer.url));
|
|
79
|
+
else if (issuer.url)
|
|
80
|
+
issuerKeys.set(issuer.iss, (0, jose_1.createRemoteJWKSet)(new URL(issuer.url)));
|
|
81
|
+
else if (issuer.publicKey)
|
|
82
|
+
issuerKeys.set(issuer.iss, (0, crypto_1.createPublicKey)(issuer.publicKey));
|
|
83
|
+
else if (issuer.secret)
|
|
84
|
+
issuerKeys.set(issuer.iss, (0, crypto_1.createSecretKey)(Buffer.from(issuer.secret, 'ascii')));
|
|
141
85
|
}
|
|
142
86
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
username: payload.sub,
|
|
146
|
-
sessionId: payload.sub + '-' + payload.iat,
|
|
147
|
-
clientId: payload.client_id,
|
|
148
|
-
impersonatedBy: payload.impersonatedBy
|
|
149
|
-
};
|
|
87
|
+
for (const clientId of (process.env.JWT_TRUSTED_CLIENTIDS?.split(',').filter(txstate_utils_1.isNotBlank).map(clientId => clientId.trim()) ?? [])) {
|
|
88
|
+
trustedClients.add(clientId);
|
|
150
89
|
}
|
|
151
90
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
91
|
+
function tokenFromReq(req) {
|
|
92
|
+
const m = req?.headers.authorization?.match(/^bearer (.*)$/i);
|
|
93
|
+
return m?.[1];
|
|
94
|
+
}
|
|
95
|
+
async function unifiedAuthenticate(req) {
|
|
96
|
+
if (!hasInit)
|
|
97
|
+
init();
|
|
98
|
+
const token = tokenFromReq(req);
|
|
99
|
+
if (!token)
|
|
100
|
+
return undefined;
|
|
101
|
+
const payload = await tokenCache.get(token, req);
|
|
102
|
+
if (!payload)
|
|
103
|
+
return undefined;
|
|
104
|
+
await validateCache.get(token, payload);
|
|
105
|
+
return {
|
|
106
|
+
username: payload.sub,
|
|
107
|
+
sessionId: payload.sub + '-' + payload.iat,
|
|
108
|
+
clientId: payload.client_id,
|
|
109
|
+
impersonatedBy: payload.act?.sub
|
|
110
|
+
};
|
|
156
111
|
}
|
|
157
112
|
exports.unifiedAuthenticate = unifiedAuthenticate;
|
|
158
|
-
async function unifiedAuthenticateAll(req
|
|
159
|
-
|
|
160
|
-
const ctx = new ContextClass(req);
|
|
161
|
-
const auth = await ctx.waitForAuth();
|
|
113
|
+
async function unifiedAuthenticateAll(req) {
|
|
114
|
+
const auth = await unifiedAuthenticate(req);
|
|
162
115
|
if (!auth?.username.length && !req.routeOptions.url?.startsWith('/docs'))
|
|
163
116
|
throw new Error('All requests require authentication.');
|
|
164
117
|
return auth;
|
package/lib-esm/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export const devLogger = ftxst.devLogger
|
|
|
4
4
|
export const prodLogger = ftxst.prodLogger
|
|
5
5
|
export const HttpError = ftxst.HttpError
|
|
6
6
|
export const FailedValidationError = ftxst.FailedValidationError
|
|
7
|
+
export const ValidationError = ftxst.ValidationError
|
|
8
|
+
export const ValidationErrors = ftxst.ValidationErrors
|
|
7
9
|
export const unifiedAuthenticate = ftxst.unifiedAuthenticate
|
|
8
10
|
export const unifiedAuthenticateAll = ftxst.unifiedAuthenticateAll
|
|
9
11
|
export const analyticsPlugin = ftxst.analyticsPlugin
|