Skip to content

Commit a268f86

Browse files
authored
feat(configuration): Allow app configuration to be validated against a schema (#2590)
1 parent 5bc9d44 commit a268f86

File tree

13 files changed

+205
-21
lines changed

13 files changed

+205
-21
lines changed

packages/authentication/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
},
6565
"devDependencies": {
6666
"@feathersjs/memory": "^5.0.0-pre.17",
67+
"@feathersjs/schema": "^5.0.0-pre.17",
6768
"@types/lodash": "^4.14.181",
6869
"@types/mocha": "^9.1.0",
6970
"@types/node": "^17.0.23",

packages/authentication/src/core.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { NotAuthenticated } from '@feathersjs/errors';
55
import { createDebug } from '@feathersjs/commons';
66
import { Application, Params } from '@feathersjs/feathers';
77
import { IncomingMessage, ServerResponse } from 'http';
8-
import defaultOptions from './options';
8+
import { defaultOptions } from './options';
99

1010
const debug = createDebug('@feathersjs/authentication/base');
1111

@@ -167,7 +167,7 @@ export class AuthenticationBase {
167167

168168
/**
169169
* Returns a single strategy by name
170-
*
170+
*
171171
* @param name The strategy name
172172
* @returns The authentication strategy or undefined
173173
*/

packages/authentication/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export {
1010
export { AuthenticationBaseStrategy } from './strategy';
1111
export { AuthenticationService } from './service';
1212
export { JWTStrategy } from './jwt';
13+
export { authenticationSettingsSchema } from './options';
Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
export default {
2-
authStrategies: [],
1+
export const defaultOptions = {
2+
authStrategies: [] as string[],
33
jwtOptions: {
44
header: { typ: 'access' }, // by default is an access token but can be any type
55
audience: 'https://yourdomain.com', // The resource server where the token is processed
@@ -8,3 +8,108 @@ export default {
88
expiresIn: '1d'
99
}
1010
};
11+
12+
export const authenticationSettingsSchema = {
13+
type: 'object',
14+
required: ['secret', 'entity', 'authStrategies'],
15+
properties: {
16+
secret: {
17+
type: 'string',
18+
description: 'The JWT signing secret'
19+
},
20+
entity: {
21+
oneOf: [{
22+
type: 'null'
23+
}, {
24+
type: 'string'
25+
}],
26+
description: 'The name of the authentication entity (e.g. user)'
27+
},
28+
entityId: {
29+
type: 'string',
30+
description: 'The name of the authentication entity id property'
31+
},
32+
service: {
33+
type: 'string',
34+
description: 'The path of the entity service'
35+
},
36+
authStrategies: {
37+
type: 'array',
38+
items: { type: 'string' },
39+
description: 'A list of authentication strategy names that are allowed to create JWT access tokens'
40+
},
41+
parseStrategies: {
42+
type: 'array',
43+
items: { type: 'string' },
44+
description: 'A list of authentication strategy names that should parse HTTP headers for authentication information (defaults to `authStrategies`)'
45+
},
46+
jwtOptions: {
47+
type: 'object'
48+
},
49+
jwt: {
50+
type: 'object',
51+
properties: {
52+
header: {
53+
type: 'string',
54+
default: 'Authorization',
55+
description: 'The HTTP header containing the JWT'
56+
},
57+
schemes: {
58+
type: 'array',
59+
items: { type: 'string' },
60+
description: 'An array of schemes to support'
61+
}
62+
}
63+
},
64+
local: {
65+
type: 'object',
66+
required: ['usernameField', 'passwordField'],
67+
properties: {
68+
usernameField: {
69+
type: 'string',
70+
description: 'Name of the username field (e.g. `email`)'
71+
},
72+
passwordField: {
73+
type: 'string',
74+
description: 'Name of the password field (e.g. `password`)'
75+
},
76+
hashSize: {
77+
type: 'number',
78+
description: 'The BCrypt salt length'
79+
},
80+
errorMessage: {
81+
type: 'string',
82+
default: 'Invalid login',
83+
description: 'The error message to return on errors'
84+
},
85+
entityUsernameField: {
86+
type: 'string',
87+
description: 'Name of the username field on the entity if authentication request data and entity field names are different'
88+
},
89+
entityPasswordField: {
90+
type: 'string',
91+
description: 'Name of the password field on the entity if authentication request data and entity field names are different'
92+
}
93+
}
94+
},
95+
oauth: {
96+
type: 'object',
97+
properties: {
98+
redirect: {
99+
type: 'string'
100+
},
101+
origins: {
102+
type: 'array',
103+
items: { type: 'string' }
104+
},
105+
defaults: {
106+
type: 'object',
107+
properties: {
108+
key: { type: 'string' },
109+
secret: { type: 'string' }
110+
}
111+
}
112+
}
113+
}
114+
}
115+
} as const;

packages/authentication/test/core.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import assert from 'assert';
22
import { feathers, Application } from '@feathersjs/feathers';
33
import jwt from 'jsonwebtoken';
4+
import { Infer, schema } from '@feathersjs/schema';
45

56
import { AuthenticationBase, AuthenticationRequest } from '../src/core';
7+
import { authenticationSettingsSchema } from '../src/options';
68
import { Strategy1, Strategy2, MockRequest } from './fixtures';
79
import { ServerResponse } from 'http';
810

@@ -31,6 +33,21 @@ describe('authentication/core', () => {
3133
});
3234

3335
describe('configuration', () => {
36+
it('infers configuration from settings schema', async () => {
37+
const settingsSchema = schema({
38+
$id: 'AuthSettingsSchema',
39+
...authenticationSettingsSchema
40+
} as const);
41+
type Settings = Infer<typeof settingsSchema>;
42+
const config: Settings = {
43+
entity: 'user',
44+
secret: 'supersecret',
45+
authStrategies: [ 'some', 'thing' ]
46+
}
47+
48+
await settingsSchema.validate(config);
49+
});
50+
3451
it('throws an error when app is not provided', () => {
3552
try {
3653
// @ts-ignore

packages/authentication/test/service.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import jwt from 'jsonwebtoken';
44
import { feathers, Application } from '@feathersjs/feathers';
55
import { memory, Service as MemoryService } from '@feathersjs/memory';
66

7-
import defaultOptions from '../src/options';
7+
import { defaultOptions } from '../src/options';
88
import { AuthenticationService } from '../src';
99

1010
import { Strategy1 } from './fixtures';

packages/configuration/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"dependencies": {
6060
"@feathersjs/commons": "^5.0.0-pre.17",
6161
"@feathersjs/feathers": "^5.0.0-pre.17",
62+
"@feathersjs/schema": "^5.0.0-pre.17",
6263
"@types/config": "^0.0.41",
6364
"config": "^3.3.7"
6465
},

packages/configuration/src/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { Application } from '@feathersjs/feathers';
1+
import { Application, ApplicationHookContext, NextFunction } from '@feathersjs/feathers';
22
import { createDebug } from '@feathersjs/commons';
3+
import { Schema } from '@feathersjs/schema'
34
import config from 'config';
45

56
const debug = createDebug('@feathersjs/configuration');
67

7-
export = function init () {
8+
export = function init (schema?: Schema<any>) {
89
return (app?: Application) => {
910
if (!app) {
1011
return config;
@@ -18,6 +19,15 @@ export = function init () {
1819
app.set(name, value);
1920
});
2021

22+
if (schema) {
23+
app.hooks({
24+
setup: [async (context: ApplicationHookContext, next: NextFunction) => {
25+
await schema.validate(context.app.settings);
26+
await next();
27+
}]
28+
})
29+
}
30+
2131
return config;
2232
};
2333
}

packages/configuration/test/index.test.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { strict as assert } from 'assert';
22
import { feathers, Application } from '@feathersjs/feathers';
3-
import plugin from '../src';
3+
import { Ajv, schema } from '@feathersjs/schema';
4+
import configuration from '../src';
45

56
describe('@feathersjs/configuration', () => {
6-
const app: Application = feathers().configure(plugin());
7+
const app: Application = feathers().configure(configuration());
78

89
it('initialized app with default.json', () => {
910
assert.equal(app.get('port'), 3030);
@@ -15,9 +16,49 @@ describe('@feathersjs/configuration', () => {
1516
});
1617

1718
it('works when called directly', () => {
18-
const fn = plugin();
19+
const fn = configuration();
1920
const conf = fn() as any;
2021

2122
assert.strictEqual(conf.port, 3030);
2223
});
24+
25+
it('errors on .setup when a schema is passed and the configuration is invalid', async () => {
26+
const configurationSchema = schema({
27+
$id: 'ConfigurationSchema',
28+
additionalProperties: false,
29+
type: 'object',
30+
properties: {
31+
port: { type: 'number' },
32+
deep: {
33+
type: 'object',
34+
properties: {
35+
base: {
36+
type: 'boolean'
37+
}
38+
}
39+
},
40+
array: {
41+
type: 'array',
42+
items: { type: 'string' }
43+
},
44+
nullish: {
45+
type: 'string'
46+
}
47+
}
48+
} as const, new Ajv());
49+
50+
const schemaApp = feathers().configure(configuration(configurationSchema))
51+
52+
await assert.rejects(() => schemaApp.setup(), {
53+
data: [{
54+
instancePath: '/nullish',
55+
keyword: 'type',
56+
message: 'must be string',
57+
params: {
58+
type: 'string'
59+
},
60+
schemaPath: '#/properties/nullish/type'
61+
}]
62+
});
63+
});
2364
});

packages/schema/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
"scripts": {
4444
"prepublish": "npm run compile",
4545
"compile": "shx rm -rf lib/ && tsc",
46-
"test": "mocha --config ../../.mocharc.json --recursive test/**.test.ts test/**/*.test.ts"
46+
"mocha": "mocha --config ../../.mocharc.json --recursive test/**.test.ts test/**/*.test.ts",
47+
"test": "npm run compile && npm run mocha"
4748
},
4849
"directories": {
4950
"lib": "lib"

0 commit comments

Comments
 (0)