Skip to content

Commit 2a5e525

Browse files
authored
Merge pull request #47 from Canner/feature/auth-route
Feature: provide getting auth token and user profile api for client-server used.
2 parents 5e89046 + 2bce5e8 commit 2a5e525

33 files changed

+1504
-286
lines changed

labs/playground1/vulcan.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ rate-limit:
3636
max: 10000
3737
enforce-https:
3838
enabled: false
39+
auth:
40+
enabled: false

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@
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",
55+
"@types/koa-bodyparser": "^4.3.8",
5656
"@types/koa-compose": "^3.2.5",
5757
"@types/koa-router": "^7.4.4",
5858
"@types/koa-sslify": "^4.0.3",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { VulcanBuilder } from '@vulcan-sql/build';
2+
import { VulcanServer } from '@vulcan-sql/serve';
3+
import * as supertest from 'supertest';
4+
import projectConfig from './projectConfig';
5+
6+
let server: VulcanServer;
7+
8+
afterEach(async () => {
9+
await server?.close();
10+
});
11+
12+
it.each([
13+
[
14+
'436193eb-f686-4105-ad7b-b5945276c14a',
15+
[
16+
{
17+
id: '436193eb-f686-4105-ad7b-b5945276c14a',
18+
name: 'ivan',
19+
},
20+
],
21+
],
22+
['2dc839e0-0f65-4dba-ac38-4eaf023d0008', []],
23+
])(
24+
'Example1: Build and serve should work',
25+
async (userId, expected) => {
26+
const builder = new VulcanBuilder(projectConfig);
27+
await builder.build();
28+
server = new VulcanServer(projectConfig);
29+
const httpServer = (await server.start())['http'];
30+
31+
const agent = supertest(httpServer);
32+
const result = await agent.get(`/api/user/${userId}`);
33+
expect(JSON.stringify(result.body)).toEqual(JSON.stringify(expected));
34+
},
35+
10000
36+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { IBuildOptions, VulcanBuilder } from '@vulcan-sql/build';
2+
import { ServeConfig, VulcanServer } from '@vulcan-sql/serve';
3+
4+
import * as supertest from 'supertest';
5+
import defaultConfig from './projectConfig';
6+
7+
let server: VulcanServer;
8+
9+
const projectConfig: ServeConfig & IBuildOptions = {
10+
...defaultConfig,
11+
auth: {
12+
enabled: true,
13+
options: {
14+
basic: {
15+
'users-list': [
16+
{
17+
name: 'user1',
18+
// md5('test1')
19+
md5Password: '5a105e8b9d40e1329780d62ea2265d8a',
20+
attr: {
21+
role: 'admin',
22+
},
23+
},
24+
],
25+
},
26+
},
27+
},
28+
};
29+
30+
afterEach(async () => {
31+
await server.close();
32+
});
33+
34+
it('Example1-2: authenticate user identity by POST /auth/token API', async () => {
35+
const builder = new VulcanBuilder(projectConfig);
36+
await builder.build();
37+
server = new VulcanServer(projectConfig);
38+
const httpServer = (await server.start())['http'];
39+
40+
const agent = supertest(httpServer);
41+
const result = await agent
42+
.post('/auth/token')
43+
.send({
44+
type: 'basic',
45+
username: 'user1',
46+
password: 'test1',
47+
})
48+
.set('Accept', 'application/json');
49+
expect(result.body).toEqual({
50+
token: 'dXNlcjE6dGVzdDE=',
51+
});
52+
}, 10000);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { IBuildOptions, VulcanBuilder } from '@vulcan-sql/build';
2+
import { ServeConfig, VulcanServer } from '@vulcan-sql/serve';
3+
import * as supertest from 'supertest';
4+
import defaultConfig from './projectConfig';
5+
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+
],
27+
},
28+
},
29+
},
30+
};
31+
});
32+
33+
afterEach(async () => {
34+
await server?.close();
35+
});
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+
});
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
1-
import {
2-
VulcanBuilder,
3-
IBuildOptions,
4-
SchemaReaderType,
5-
} from '@vulcan-sql/build';
1+
import { SchemaReaderType } from '@vulcan-sql/build';
62
import {
73
ArtifactBuilderProviderType,
84
ArtifactBuilderSerializerType,
95
TemplateProviderType,
106
DocumentSpec,
117
} from '@vulcan-sql/core';
12-
import { VulcanServer, ServeConfig, APIProviderType } from '@vulcan-sql/serve';
8+
import { APIProviderType } from '@vulcan-sql/serve';
139
import * as path from 'path';
14-
import * as supertest from 'supertest';
1510
import faker from '@faker-js/faker';
1611

17-
const projectConfig: ServeConfig & IBuildOptions = {
12+
export default {
1813
name: 'example project 1',
1914
description: 'Vulcan project for integration testing',
2015
version: '0.0.1',
@@ -48,37 +43,8 @@ const projectConfig: ServeConfig & IBuildOptions = {
4843
'enforce-https': {
4944
enabled: false,
5045
},
46+
auth: {
47+
enabled: false,
48+
},
5149
port: faker.datatype.number({ min: 20000, max: 30000 }),
5250
};
53-
54-
let server: VulcanServer;
55-
56-
afterEach(async () => {
57-
await server?.close();
58-
});
59-
60-
it.each([
61-
[
62-
'436193eb-f686-4105-ad7b-b5945276c14a',
63-
[
64-
{
65-
id: '436193eb-f686-4105-ad7b-b5945276c14a',
66-
name: 'ivan',
67-
},
68-
],
69-
],
70-
['2dc839e0-0f65-4dba-ac38-4eaf023d0008', []],
71-
])(
72-
'Example1: Build and serve should work',
73-
async (userId, expected) => {
74-
const builder = new VulcanBuilder(projectConfig);
75-
await builder.build();
76-
server = new VulcanServer(projectConfig);
77-
const httpServer = (await server.start())['http'];
78-
79-
const agent = supertest(httpServer);
80-
const result = await agent.get(`/api/user/${userId}`);
81-
expect(JSON.stringify(result.body)).toEqual(JSON.stringify(expected));
82-
},
83-
10000
84-
);

packages/serve/src/lib/app.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { APISchema } from '@vulcan-sql/core';
22
import * as Koa from 'koa';
33
import * as KoaRouter from 'koa-router';
4+
import * as koaParseBody from 'koa-bodyparser';
45
import { uniq } from 'lodash';
56
import {
67
RestfulRoute,
@@ -32,6 +33,9 @@ export class VulcanApplication {
3233
this.app = new Koa();
3334
this.restfulRouter = new KoaRouter();
3435
this.graphqlRouter = new KoaRouter();
36+
37+
// add koa parser body for parsing POST json and form data
38+
this.app.use(koaParseBody());
3539
}
3640

3741
/**

packages/serve/src/lib/auth/httpBasicAuthenticator.ts

+22-8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '@vulcan-sql/serve/models';
1010
import { VulcanExtensionId, VulcanInternalExtension } from '@vulcan-sql/core';
1111
import { isEmpty } from 'lodash';
12+
import 'koa-bodyparser';
1213

1314
interface AuthUserOptions {
1415
/* user name */
@@ -21,7 +22,7 @@ interface HTPasswdFileOptions {
2122
/** password file path */
2223
['path']: string;
2324
/** each user information */
24-
['users']: Array<AuthUserOptions>;
25+
['users']?: Array<AuthUserOptions>;
2526
}
2627

2728
export interface AuthUserListOptions {
@@ -87,26 +88,39 @@ export class BasicAuthenticator extends BaseAuthenticator<BasicOptions> {
8788
}
8889
}
8990

90-
public async authenticate(context: KoaContext) {
91+
public async getTokenInfo(ctx: KoaContext) {
92+
const username = ctx.request.body!['username'] as string;
93+
const password = ctx.request.body!['password'] as string;
94+
if (!username || !password)
95+
throw new Error('please provide "username" and "password".');
96+
97+
const token = Buffer.from(`${username}:${password}`).toString('base64');
98+
99+
return {
100+
token: token,
101+
};
102+
}
103+
104+
public async authCredential(context: KoaContext) {
91105
const incorrect = {
92106
status: AuthStatus.INDETERMINATE,
93107
type: this.getExtensionId()!,
94108
};
95109
if (isEmpty(this.options)) return incorrect;
96110

97-
const authRequest = context.request.headers['authorization'];
111+
const authorize = context.request.headers['authorization'];
98112
if (
99-
!authRequest ||
100-
!authRequest.toLowerCase().startsWith(this.getExtensionId()!)
113+
!authorize ||
114+
!authorize.toLowerCase().startsWith(this.getExtensionId()!)
101115
)
102116
return incorrect;
103117

104118
// validate request auth token
105-
const token = authRequest.trim().split(' ')[1];
119+
const token = authorize.trim().split(' ')[1];
106120
const bareToken = Buffer.from(token, 'base64').toString();
107121

108122
try {
109-
return await this.verify(bareToken);
123+
return await this.validate(bareToken);
110124
} catch (err) {
111125
// if not found matched user credential, add WWW-Authenticate and return failed
112126
context.set('WWW-Authenticate', this.getExtensionId()!);
@@ -118,7 +132,7 @@ export class BasicAuthenticator extends BaseAuthenticator<BasicOptions> {
118132
}
119133
}
120134

121-
private async verify(baredToken: string) {
135+
private async validate(baredToken: string) {
122136
const username = baredToken.split(':')[0] || '';
123137
// bare password from Basic specification
124138
const password = baredToken.split(':')[1] || '';

0 commit comments

Comments
 (0)