-
Notifications
You must be signed in to change notification settings - Fork 32
/
Copy pathhttpBasicAuthenticator.ts
143 lines (130 loc) · 4.42 KB
/
httpBasicAuthenticator.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import * as fs from 'fs';
import * as readline from 'readline';
import * as md5 from 'md5';
import {
BaseAuthenticator,
KoaContext,
AuthStatus,
AuthResult,
} from '@vulcan-sql/serve/models';
import { VulcanExtensionId, VulcanInternalExtension } from '@vulcan-sql/core';
import { isEmpty } from 'lodash';
interface AuthUserOptions {
/* user name */
name: string;
/* the user attribute which could used after auth successful */
attr: { [field: string]: string | boolean | number };
}
interface HTPasswdFileOptions {
/** password file path */
['path']: string;
/** each user information */
['users']: Array<AuthUserOptions>;
}
export interface AuthUserListOptions {
/* user name */
name: string;
/* hashed password by md5 */
md5Password: string;
/* the user attribute which could used after auth successful */
attr: { [field: string]: string | boolean | number };
}
export interface BasicOptions {
['htpasswd-file']?: HTPasswdFileOptions;
['users-list']?: Array<AuthUserListOptions>;
}
type UserCredentialsMap = {
[name: string]: {
/* hashed password by md5 */
md5Password: string;
/* the user attribute which could used after auth successful */
attr: { [field: string]: string | boolean | number };
};
};
/** The http basic authenticator.
*
* Able to set user credentials by file path through "htpasswd-file" or list directly in config by "users-list".
* The password must hash by md5 when setting into "htpasswd-file" or "users-list".
*
* It authenticate by passing encode base64 {username}:{password} to authorization
*/
@VulcanInternalExtension('auth')
@VulcanExtensionId('basic')
export class BasicAuthenticator extends BaseAuthenticator<BasicOptions> {
private usersCredentials: UserCredentialsMap = {};
private options: BasicOptions = {};
/** read basic options to initialize and load user credentials */
public override async onActivate() {
this.options = (this.getOptions() as BasicOptions) || this.options;
// load "users-list" in options
for (const option of this.options['users-list'] || []) {
const { name, md5Password, attr } = option;
this.usersCredentials[name] = { md5Password, attr };
}
// load "htpasswd-file" in options
if (!this.options['htpasswd-file']) return;
const { path, users } = this.options['htpasswd-file'];
if (!fs.existsSync(path) || !fs.statSync(path).isFile()) return;
const reader = readline.createInterface({
input: fs.createReadStream(path),
});
// username:md5Password
for await (const line of reader) {
const name = line.split(':')[0] || '';
const md5Password = line.split(':')[1] || '';
// if users exist the same name, add attr to here, or as empty
this.usersCredentials[name] = {
md5Password,
attr: users?.find((user) => user.name === name)?.attr || {},
};
}
}
public async authenticate(context: KoaContext) {
const incorrect = {
status: AuthStatus.INDETERMINATE,
type: this.getExtensionId()!,
};
if (isEmpty(this.options)) return incorrect;
const authRequest = context.request.headers['authorization'];
if (
!authRequest ||
!authRequest.toLowerCase().startsWith(this.getExtensionId()!)
)
return incorrect;
// validate request auth token
const token = authRequest.trim().split(' ')[1];
const bareToken = Buffer.from(token, 'base64').toString();
try {
return await this.verify(bareToken);
} catch (err) {
// if not found matched user credential, add WWW-Authenticate and return failed
context.set('WWW-Authenticate', this.getExtensionId()!);
return {
status: AuthStatus.FAIL,
type: this.getExtensionId()!,
message: (err as Error).message,
};
}
}
private async verify(baredToken: string) {
const username = baredToken.split(':')[0] || '';
// bare password from Basic specification
const password = baredToken.split(':')[1] || '';
// if authenticated, return user data
if (
!(username in this.usersCredentials) ||
!(md5(password) === this.usersCredentials[username].md5Password)
)
throw new Error(
`authenticate user by "${this.getExtensionId()}" type failed.`
);
return {
status: AuthStatus.SUCCESS,
type: this.getExtensionId()!, // method name
user: {
name: username,
attr: this.usersCredentials[username].attr,
},
} as AuthResult;
}
}