Skip to content

Commit c95709b

Browse files
committed
fix(serve): update authenticator and auth middleware test cases
- update "httpBasicAuthenticator" to change apache-md5 to general md5 for hashing. - refactor to update "httpBasicAuthenticator" test cases. - add "simpleTokenAuthenticator" test cases. - add "passwordFileAuthenticator" test cases. - update "authMiddleware" to activate authenticator and update test cases.
1 parent a1c8572 commit c95709b

17 files changed

+762
-451
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
"private": true,
99
"dependencies": {
1010
"@koa/cors": "^3.3.0",
11-
"apache-md5": "^1.1.7",
1211
"class-validator": "^0.13.2",
1312
"commander": "^9.4.0",
1413
"dayjs": "^1.11.2",
@@ -23,6 +22,7 @@
2322
"koa-router": "^10.1.1",
2423
"koa2-ratelimit": "^1.1.1",
2524
"lodash": "^4.17.21",
25+
"md5": "^2.3.0",
2626
"nunjucks": "^3.2.3",
2727
"openapi3-ts": "^2.0.2",
2828
"ora": "^5.4.1",
@@ -52,6 +52,7 @@
5252
"@types/koa2-ratelimit": "^0.9.3",
5353
"@types/koa__cors": "^3.3.0",
5454
"@types/lodash": "^4.14.182",
55+
"@types/md5": "^2.3.2",
5556
"@types/node": "16.11.7",
5657
"@types/supertest": "^2.0.12",
5758
"@types/uuid": "^8.3.4",
@@ -63,7 +64,6 @@
6364
"eslint": "~8.12.0",
6465
"eslint-config-prettier": "8.1.0",
6566
"from2": "^2.3.0",
66-
"is-base64": "^1.1.0",
6767
"jest": "27.5.1",
6868
"mongoose": "^6.5.2",
6969
"nx": "14.0.3",

packages/serve/src/containers/types.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ export const TYPES = {
55
PaginationTransformer: Symbol.for('PaginationTransformer'),
66
Route: Symbol.for('Route'),
77
RouteGenerator: Symbol.for('RouteGenerator'),
8-
// Authenticator
9-
UserAuthOptions: Symbol.for('UserAuthOptions'),
8+
109
// Application
1110
AppConfig: Symbol.for('AppConfig'),
1211
VulcanApplication: Symbol.for('VulcanApplication'),
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import * as fs from 'fs';
22
import * as readline from 'readline';
3-
import md5 from 'apache-md5';
3+
import * as md5 from 'md5';
44
import {
55
BaseAuthenticator,
66
KoaContext,
77
AuthStatus,
88
AuthResult,
99
} from '@vulcan-sql/serve/models';
1010
import { VulcanExtensionId, VulcanInternalExtension } from '@vulcan-sql/core';
11+
import { isEmpty } from 'lodash';
1112

1213
interface AuthUserOptions {
1314
/* user name */
@@ -16,82 +17,89 @@ interface AuthUserOptions {
1617
attr: { [field: string]: string | boolean | number };
1718
}
1819

19-
interface PasswordFileOptions {
20+
interface HTPasswdFileOptions {
2021
/** password file path */
2122
['path']: string;
2223
/** each user information */
2324
['users']: Array<AuthUserOptions>;
2425
}
2526

26-
interface TokenListOptions {
27+
export interface AuthUserListOptions {
2728
/* user name */
2829
name: string;
29-
/* hashed password by apache md5 */
30-
hashPassword: string;
30+
/* hashed password by md5 */
31+
md5Password: string;
3132
/* the user attribute which could used after auth successful */
3233
attr: { [field: string]: string | boolean | number };
3334
}
3435

3536
export interface BasicOptions {
36-
['password-file']?: PasswordFileOptions;
37-
['token-users']?: Array<TokenListOptions>;
37+
['htpasswd-file']?: HTPasswdFileOptions;
38+
['users-list']?: Array<AuthUserListOptions>;
3839
}
3940

4041
type UserCredentialsMap = {
4142
[name: string]: {
42-
/* hashed password by apache md5 */
43-
hashPassword: string;
43+
/* hashed password by md5 */
44+
md5Password: string;
4445
/* the user attribute which could used after auth successful */
4546
attr: { [field: string]: string | boolean | number };
4647
};
4748
};
4849

49-
/** The http basic authenticator */
50-
@VulcanInternalExtension()
50+
/** The http basic authenticator.
51+
*
52+
* Able to set user credentials by file path through "htpasswd-file" or list directly in config by "users-list".
53+
* The password must hash by md5 when setting into "htpasswd-file" or "users-list".
54+
*
55+
* It authenticate by passing encode base64 {username}:{password} to authorization
56+
*/
57+
@VulcanInternalExtension('auth')
5158
@VulcanExtensionId('basic')
5259
export class BasicAuthenticator extends BaseAuthenticator<BasicOptions> {
5360
private usersCredentials: UserCredentialsMap = {};
5461
private options: BasicOptions = {};
5562
/** read basic options to initialize and load user credentials */
5663
public override async onActivate() {
5764
this.options = (this.getOptions() as BasicOptions) || this.options;
58-
// load "token-users" in options
59-
for (const option of this.options['token-users'] || []) {
60-
const { name, hashPassword, attr } = option;
61-
this.isMD5Hashed(hashPassword);
62-
this.usersCredentials[name] = { hashPassword, attr };
65+
// load "users-list" in options
66+
for (const option of this.options['users-list'] || []) {
67+
const { name, md5Password, attr } = option;
68+
this.usersCredentials[name] = { md5Password, attr };
6369
}
64-
// load "password-file" in options
65-
if (!this.options['password-file']) return;
66-
const { path, users } = this.options['password-file'];
70+
// load "htpasswd-file" in options
71+
if (!this.options['htpasswd-file']) return;
72+
const { path, users } = this.options['htpasswd-file'];
6773

6874
if (!fs.existsSync(path) || !fs.statSync(path).isFile()) return;
6975
const reader = readline.createInterface({
7076
input: fs.createReadStream(path),
7177
});
72-
// username:hashPassword
78+
// username:md5Password
7379
for await (const line of reader) {
7480
const name = line.split(':')[0] || '';
75-
const hashPassword = line.split(':')[1] || '';
76-
this.isMD5Hashed(hashPassword);
81+
const md5Password = line.split(':')[1] || '';
7782
// if users exist the same name, add attr to here, or as empty
7883
this.usersCredentials[name] = {
79-
hashPassword,
80-
attr: users.find((user) => user.name === name)?.attr || {},
84+
md5Password,
85+
attr: users?.find((user) => user.name === name)?.attr || {},
8186
};
8287
}
8388
}
8489

8590
public async authenticate(context: KoaContext) {
91+
const incorrect = {
92+
status: AuthStatus.INCORRECT,
93+
type: this.getExtensionId()!,
94+
};
95+
if (isEmpty(this.options)) return incorrect;
96+
8697
const authRequest = context.request.headers['authorization'];
8798
if (
8899
!authRequest ||
89100
!authRequest.toLowerCase().startsWith(this.getExtensionId()!)
90101
)
91-
return {
92-
status: AuthStatus.INCORRECT,
93-
type: this.getExtensionId()!,
94-
};
102+
return incorrect;
95103

96104
// validate request auth token
97105
const token = authRequest.trim().split(' ')[1];
@@ -117,10 +125,10 @@ export class BasicAuthenticator extends BaseAuthenticator<BasicOptions> {
117125
// if authenticated, return user data
118126
if (
119127
!(username in this.usersCredentials) ||
120-
!(md5(password) === this.usersCredentials[username].hashPassword)
128+
!(md5(password) === this.usersCredentials[username].md5Password)
121129
)
122130
throw new Error(
123-
`authenticate user by ${this.getExtensionId()} type failed.`
131+
`authenticate user by "${this.getExtensionId()}" type failed.`
124132
);
125133

126134
return {
@@ -132,9 +140,4 @@ export class BasicAuthenticator extends BaseAuthenticator<BasicOptions> {
132140
},
133141
} as AuthResult;
134142
}
135-
136-
private isMD5Hashed(value: string) {
137-
if (!value.startsWith('$apr1$'))
138-
throw new Error(`"${this.getExtensionId()}" type must hash apache md5.`);
139-
}
140143
}

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

+35-23
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,84 @@
11
import * as fs from 'fs';
22
import * as readline from 'readline';
3+
import * as bcrypt from 'bcryptjs';
34
import {
45
BaseAuthenticator,
56
KoaContext,
67
AuthStatus,
78
AuthResult,
89
} from '@vulcan-sql/serve/models';
910
import { VulcanExtensionId, VulcanInternalExtension } from '@vulcan-sql/core';
11+
import { isEmpty } from 'lodash';
1012

11-
interface AuthUserOptions {
13+
export interface PasswordFileUserOptions {
1214
/* user name */
1315
name: string;
1416
/* the user attribute which could used after auth successful */
1517
attr: { [field: string]: string | boolean | number };
1618
}
17-
export interface PasswordFileOptions {
19+
interface PasswordFileOptions {
1820
/** password file path */
19-
['path']: string;
21+
['path']?: string;
2022
/** each user information */
21-
['users']: Array<AuthUserOptions>;
23+
['users']?: Array<PasswordFileUserOptions>;
2224
}
2325

2426
type UserCredentialsMap = {
2527
[name: string]: {
2628
/* hashed password by bcrypt */
27-
hashPassword: string;
29+
bcryptPassword: string;
2830
/* the user attribute which could used after auth successful */
2931
attr: { [field: string]: string | boolean | number };
3032
};
3133
};
3234

33-
/** The password-file authenticator */
34-
@VulcanInternalExtension()
35+
/** The password-file authenticator.
36+
*
37+
* Setting the password file with {username}:{bcrypt-password} format, we use the bcrypt round 10.
38+
* Then authenticate by passing encode base64 {username}:{password} to authorization.
39+
*/
40+
@VulcanInternalExtension('auth')
3541
@VulcanExtensionId('password-file')
3642
export class PasswordFileAuthenticator extends BaseAuthenticator<PasswordFileOptions> {
3743
private usersCredentials: UserCredentialsMap = {};
38-
private options: PasswordFileOptions = { path: '', users: [] };
44+
private options: PasswordFileOptions = {};
3945

4046
/** read password file and users info to initialize user credentials */
4147
public override async onActivate() {
4248
this.options = (this.getOptions() as PasswordFileOptions) || this.options;
4349
const { path, users } = this.options;
44-
if (!fs.existsSync(path) || !fs.statSync(path).isFile()) return;
50+
if (!path || !fs.existsSync(path) || !fs.statSync(path).isFile()) return;
4551
const reader = readline.createInterface({
4652
input: fs.createReadStream(path),
4753
});
48-
// <username>:<hashed-password>
54+
// <username>:<bcrypt-password>
4955
for await (const line of reader) {
5056
const name = line.split(':')[0] || '';
51-
const hashPassword = line.split(':')[1] || '';
52-
if (!hashPassword.startsWith('$2y$'))
53-
throw new Error(`"${this.getExtensionId()}" type must hash bcrypt.`);
57+
const bcryptPassword = line.split(':')[1] || '';
58+
if (!isEmpty(bcryptPassword) && !bcryptPassword.startsWith('$2y$'))
59+
throw new Error(`"${this.getExtensionId()}" type must bcrypt in file.`);
5460

5561
// if users exist the same name, add attr to here, or as empty
5662
this.usersCredentials[name] = {
57-
hashPassword,
58-
attr: users.find((user) => user.name === name)?.attr || {},
63+
bcryptPassword,
64+
attr: users?.find((user) => user.name === name)?.attr || {},
5965
};
6066
}
6167
}
6268

6369
public async authenticate(context: KoaContext) {
70+
const incorrect = {
71+
status: AuthStatus.INCORRECT,
72+
type: this.getExtensionId()!,
73+
};
74+
if (isEmpty(this.options)) return incorrect;
75+
6476
const authRequest = context.request.headers['authorization'];
6577
if (
6678
!authRequest ||
6779
!authRequest.toLowerCase().startsWith(this.getExtensionId()!)
6880
)
69-
return {
70-
status: AuthStatus.INCORRECT,
71-
type: this.getExtensionId()!,
72-
};
81+
return incorrect;
7382
// validate request auth token
7483
const token = authRequest.trim().split(' ')[1];
7584
const bareToken = Buffer.from(token, 'base64').toString();
@@ -87,15 +96,18 @@ export class PasswordFileAuthenticator extends BaseAuthenticator<PasswordFileOpt
8796

8897
private async verify(baredToken: string) {
8998
const username = baredToken.split(':')[0] || '';
90-
// hashed password in token
91-
const hashPassword = baredToken.split(':')[1] || '';
99+
// bare password in token
100+
const password = baredToken.split(':')[1] || '';
92101
// if authenticated, return user data
93102
if (
94103
!(username in this.usersCredentials) ||
95-
!(this.usersCredentials[username].hashPassword === hashPassword)
104+
!bcrypt.compareSync(
105+
password,
106+
this.usersCredentials[username].bcryptPassword
107+
)
96108
)
97109
throw new Error(
98-
`authenticate user by ${this.getExtensionId()} type authentication failed.`
110+
`authenticate user by "${this.getExtensionId()}" type failed.`
99111
);
100112

101113
return {

0 commit comments

Comments
 (0)