Skip to content

Commit 8a3ad94

Browse files
committed
feat(serve): add auth source normalizer middleware to support query string source auth data
- add "authSourceNormalizerMiddleware" and its options to support client providing auth data from query string or payload. - rename "authRouteMiddleware" to "authRouterMiddleware". - create "BaseAuthMiddleware" to collect same logistic part for activate method in auth credential and auth router middleware. - remove "is-base64" package. - add "authSourceNormalizerMiddleware" test cases. - add new test cases for testing auth api with providing query string or paylod auth data source.
1 parent 14f3bb9 commit 8a3ad94

15 files changed

+538
-212
lines changed

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
"@types/from2": "^2.3.1",
5050
"@types/glob": "^7.2.0",
5151
"@types/inquirer": "^8.0.0",
52-
"@types/is-base64": "^1.1.1",
5352
"@types/jest": "27.4.1",
5453
"@types/js-yaml": "^4.0.5",
5554
"@types/koa": "^2.13.4",

packages/integration-testing/src/example1/example1-3.spec.ts

+125-40
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,132 @@ import { ServeConfig, VulcanServer } from '@vulcan-sql/serve';
33
import * as supertest from 'supertest';
44
import defaultConfig from './projectConfig';
55

6-
let server: VulcanServer;
7-
8-
const projectConfig: ServeConfig & IBuildOptions = {
9-
...defaultConfig,
10-
auth: {
11-
enabled: true,
12-
options: {
13-
basic: {
14-
'users-list': [
15-
{
16-
name: 'user1',
17-
// md5('test1')
18-
md5Password: '5a105e8b9d40e1329780d62ea2265d8a',
19-
attr: {
20-
role: 'admin',
21-
},
6+
describe('Example1-3: get user profile by GET /auth/user-profile API with Authorization', () => {
7+
let server: VulcanServer;
8+
let projectConfig: ServeConfig & IBuildOptions;
9+
10+
beforeEach(async () => {
11+
projectConfig = {
12+
...defaultConfig,
13+
auth: {
14+
enabled: true,
15+
options: {
16+
basic: {
17+
'users-list': [
18+
{
19+
name: 'user1',
20+
// md5('test1')
21+
md5Password: '5a105e8b9d40e1329780d62ea2265d8a',
22+
attr: {
23+
role: 'admin',
24+
},
25+
},
26+
],
2227
},
23-
],
28+
},
2429
},
25-
},
26-
},
27-
};
28-
29-
afterEach(async () => {
30-
await server.close();
31-
});
30+
};
31+
});
3232

33-
it('Example1-3: get user profile by GET /auth/user-profile API with Authorization', async () => {
34-
const builder = new VulcanBuilder(projectConfig);
35-
await builder.build();
36-
server = new VulcanServer(projectConfig);
37-
const httpServer = (await server.start())['http'];
38-
39-
const agent = supertest(httpServer);
40-
const result = await agent
41-
.get('/auth/user-profile')
42-
.set('Authorization', 'basic dXNlcjE6dGVzdDE=');
43-
expect(result.body).toEqual({
44-
name: 'user1',
45-
attr: {
46-
role: 'admin',
47-
},
33+
afterEach(async () => {
34+
await server?.close();
4835
});
49-
}, 10000);
36+
37+
it('Example1-3-1: set Authorization in header with default options', async () => {
38+
const builder = new VulcanBuilder(projectConfig);
39+
await builder.build();
40+
server = new VulcanServer(projectConfig);
41+
const httpServer = (await server.start())['http'];
42+
43+
const agent = supertest(httpServer);
44+
const result = await agent
45+
.get('/auth/user-profile')
46+
.set('Authorization', 'basic dXNlcjE6dGVzdDE=');
47+
expect(result.body).toEqual({
48+
name: 'user1',
49+
attr: {
50+
role: 'admin',
51+
},
52+
});
53+
}, 10000);
54+
55+
it('Example1-3-2: set Authorization in querying with default options', async () => {
56+
const builder = new VulcanBuilder(projectConfig);
57+
await builder.build();
58+
server = new VulcanServer(projectConfig);
59+
const httpServer = (await server.start())['http'];
60+
61+
const auth = Buffer.from(
62+
JSON.stringify({ Authorization: 'basic dXNlcjE6dGVzdDE=' })
63+
).toString('base64');
64+
65+
const agent = supertest(httpServer);
66+
const result = await agent.get(`/auth/user-profile?auth=${auth}`);
67+
68+
expect(result.body).toEqual({
69+
name: 'user1',
70+
attr: {
71+
role: 'admin',
72+
},
73+
});
74+
}, 10000);
75+
76+
it('Example1-3-3: set Authorization in querying with specific auth "key" options', async () => {
77+
projectConfig['auth-source'] = {
78+
options: {
79+
key: 'x-auth',
80+
},
81+
};
82+
const builder = new VulcanBuilder(projectConfig);
83+
await builder.build();
84+
server = new VulcanServer(projectConfig);
85+
const httpServer = (await server.start())['http'];
86+
87+
const auth = Buffer.from(
88+
JSON.stringify({ Authorization: 'basic dXNlcjE6dGVzdDE=' })
89+
).toString('base64');
90+
91+
const agent = supertest(httpServer);
92+
const result = await agent.get(`/auth/user-profile?x-auth=${auth}`);
93+
94+
expect(result.body).toEqual({
95+
name: 'user1',
96+
attr: {
97+
role: 'admin',
98+
},
99+
});
100+
}, 10000);
101+
102+
it('Example1-3-4: set Authorization in json payload specific auth "x-key" options', async () => {
103+
projectConfig['auth-source'] = {
104+
options: {
105+
key: 'x-auth',
106+
in: 'payload',
107+
},
108+
};
109+
const builder = new VulcanBuilder(projectConfig);
110+
await builder.build();
111+
server = new VulcanServer(projectConfig);
112+
const httpServer = (await server.start())['http'];
113+
114+
const auth = Buffer.from(
115+
JSON.stringify({ Authorization: 'basic dXNlcjE6dGVzdDE=' })
116+
).toString('base64');
117+
118+
const agent = supertest(httpServer);
119+
120+
const result = await agent
121+
.get('/auth/user-profile')
122+
.send({
123+
['x-auth']: auth,
124+
})
125+
.set('Accept', 'application/json');
126+
127+
expect(result.body).toEqual({
128+
name: 'user1',
129+
attr: {
130+
role: 'admin',
131+
},
132+
});
133+
}, 10000);
134+
});

packages/serve/src/lib/middleware/auth/authCredentialMiddleware.ts

-88
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { VulcanInternalExtension } from '@vulcan-sql/core';
2+
import { Next, KoaContext, AuthStatus } from '@vulcan-sql/serve/models';
3+
import { BaseAuthMiddleware } from './authMiddleware';
4+
5+
/** The middleware responsible for checking request auth credentials.
6+
* It seek the 'auth' module name to match data through built-in and customized authenticator by BaseAuthenticator
7+
* */
8+
@VulcanInternalExtension('auth')
9+
export class AuthCredentialsMiddleware extends BaseAuthMiddleware {
10+
public override async onActivate() {
11+
await this.initialize();
12+
}
13+
14+
public async handle(context: KoaContext, next: Next) {
15+
// return to stop the middleware, if disabled
16+
if (!this.enabled) return next();
17+
18+
// The /auth/token endpoint not need contains auth credentials
19+
if (context.path === '/auth/token') return next();
20+
21+
// pass current context to auth token for users
22+
for (const name of Object.keys(this.authenticators)) {
23+
// skip the disappeared auth type name in options
24+
if (!this.options[name]) continue;
25+
// auth token
26+
const result = await this.authenticators[name].authCredential(context);
27+
// if state is indeterminate, change to next authentication
28+
if (result.status === AuthStatus.INDETERMINATE) continue;
29+
// if state is failed, return directly
30+
if (result.status === AuthStatus.FAIL) {
31+
context.status = 401;
32+
context.body = {
33+
type: result.type,
34+
message: result.message || 'verify token failed',
35+
};
36+
return;
37+
}
38+
// set auth user information to context
39+
context.state.user = result.user!;
40+
await next();
41+
return;
42+
}
43+
44+
throw new Error('all types of authenticator failed.');
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { isEmpty } from 'lodash';
2+
import { inject, multiInject } from 'inversify';
3+
import { TYPES as CORE_TYPES } from '@vulcan-sql/core';
4+
import {
5+
BuiltInMiddleware,
6+
BaseAuthenticator,
7+
AuthOptions,
8+
} from '@vulcan-sql/serve/models';
9+
import { TYPES } from '@vulcan-sql/serve/containers';
10+
11+
type AuthenticatorMap = {
12+
[name: string]: BaseAuthenticator<any>;
13+
};
14+
15+
export abstract class BaseAuthMiddleware extends BuiltInMiddleware<AuthOptions> {
16+
protected options = (this.getOptions() as AuthOptions) || {};
17+
protected authenticators: AuthenticatorMap;
18+
19+
constructor(
20+
@inject(CORE_TYPES.ExtensionConfig) config: any,
21+
@inject(CORE_TYPES.ExtensionName) name: string,
22+
@multiInject(TYPES.Extension_Authenticator)
23+
authenticators: BaseAuthenticator<any>[]
24+
) {
25+
super(config, name);
26+
27+
this.authenticators = authenticators.reduce<AuthenticatorMap>(
28+
(prev, authenticator) => {
29+
prev[authenticator.getExtensionId()!] = authenticator;
30+
return prev;
31+
},
32+
{}
33+
);
34+
}
35+
public async initialize() {
36+
if (this.enabled && isEmpty(this.options)) {
37+
throw new Error(
38+
'please set at least one auth type and user credential when you enable the "auth" options.'
39+
);
40+
}
41+
42+
const names = Object.keys(this.authenticators);
43+
// check setup auth type in options also valid in authenticators
44+
Object.keys(this.options).map((type) => {
45+
if (!names.includes(type))
46+
throw new Error(
47+
`The auth type "${type}" in options not supported, authenticator only supported ${names}.`
48+
);
49+
});
50+
51+
for (const name of names) {
52+
const authenticator = this.authenticators[name];
53+
if (authenticator.activate) await authenticator.activate();
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)