-
Notifications
You must be signed in to change notification settings - Fork 32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: Add Auth middleware and Authenticator. #26
Conversation
17b8eee
to
a22b01b
Compare
a22b01b
to
02a19de
Compare
05e1464
to
6b094ef
Compare
Codecov ReportBase: 91.36% // Head: 91.39% // Increases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## develop #26 +/- ##
===========================================
+ Coverage 91.36% 91.39% +0.02%
===========================================
Files 199 205 +6
Lines 2630 2871 +241
Branches 280 336 +56
===========================================
+ Hits 2403 2624 +221
- Misses 177 184 +7
- Partials 50 63 +13
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have some suggestions about auth design. Thanks for adding test and factorying!
): Promise<AuthResult>; | ||
} | ||
|
||
@VulcanExtension(TYPES.Extension_Authenticator) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@VulcanExtension(TYPES.Extension_Authenticator) | |
@VulcanExtension(TYPES.Extension_Authenticator, { enforcedId: true }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for suggesting, I have fixed the part to add it.
ansToken = matched | ||
? (process.env[matched[1]] as string) | ||
: (userOptions.auth['token'] as string); | ||
|
||
if (!matched) ansToken = ansToken || (userOptions.auth['token'] as string); | ||
// matched[1] is env variable | ||
if (matched) ansToken = ansToken || (process.env[matched[1]] as string); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are the last two lines duplicated? We have already assigned ansToken at line 49.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for catching the part, I have removed it.
const pattern = /^{{([\w]+|[ \w ]+)}}$/; | ||
const matched = pattern.exec(userOptions.auth['token'] as string); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's nice to use environment parameters in property values, but we should do global config rendering instead of replacing them at individual extensions.
We can render our config YAML file with environment parameters, extensions can expect to receive rendered config.
It might also help us do secret masking, context switching ...etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for @oscar60310 suggesting, according to our discussion, I will create the other PR to add it, currently, in this PR I remove environment variable part
export interface AuthOptions { | ||
['user-auth']?: Array<UserAuthOptions>; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about moving the per-user config into authenticators' scope?
In our current design, we need to set user information for every user:
auth:
options:
user-auth:
- name: ivan
token: some-secret
attr:
admin: false
- name: eason
file: pw-file
attr:
admin: true
It's necessary for us now because we use static user lists, but when we use further auth services like OIDC, we won't want to add users to Vulcan one by one.
So maybe we can change the config like the following (don't mind the property names and their structure):
Using basic auth
auth:
options:
# For basic auth with password files (we can set attributes at these files too.)
password-files:
- pw-file
# For basic auth with static token, this list is only updated when new user with static token added.
static-users:
- name: ivan
token: some-secret
attr:
admin: false
- name: eason
file: pw-file
attr:
admin: true
Using OIDC (access token)
auth:
options:
# For OIDC
oidc:
client-id: xxx
client-secret: xxx
attribute-mapper:
- group
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for @oscar60310 reviewing and suggesting! I make each authenticator option defined by identifier under the options
of auth
.
auth:
options:
basic:
... // basic authenticator options
simple-token:
... // simple-tokenauthenticator options
password-file:
... // password-file authenticator options
So like OIDC, oauth2 could also put under the options
of auth
.
Thanks alot !
// authenticate each user by selected auth method in config | ||
for (const name of Object.keys(this.authenticators)) { | ||
const result = await this.authenticators[name].authenticate( | ||
options['user-auth'] || [], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: I'd prefer inject these options to authenticators directly than passing them here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for reviewing and suggesting, after I adapt your suggestion for initializing in onActivate
method, I won't pass the options
into each authenticate
method. Therefore, also no need to inject options.
await next(); | ||
return; | ||
} | ||
throw new Error('authentication failed.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basic authentication should return 401 with www-authenticate header or users can't see the username/password dialog on browsers.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate
We need to find a way to throw this error without conflicting with other methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for suggesting, that after I add the multiple auth status for distinguishing what is authentication successful, or failed and "skip to the next authenticator" ( The status called incorrect
now ) for checking.
Now I have added the www-authenticate
header in response when the authriozation request is basic and options have set basic options data.
for (const userOptions of usersOptions) { | ||
if (userOptions.auth['token']) { | ||
const result = await this.verifyToken(token, userOptions); | ||
if (result.authenticated) return result; | ||
} | ||
if (userOptions.auth['file']) { | ||
const result = await this.verifyTokenInFile(token, userOptions); | ||
if (result.authenticated) return result; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'll have better performance if we load the configurations into memory and map them with credentials to the user.
- We can call "onActivate" function when middlewares are initializing to load the files and configuration.
- We can use Credentials -> Users mapping instead of testing for all users.
public override async onActivate() {
// Load config
for (const userOptions of usersOptions) {
this.credentialMapping.set(userOptions.token, {name, attr ....})
}
// Load the password files
const reader = readline.createInterface({
input: fs.createReadStream(filePath),
});
for await (const line of reader) {
this.credentialMapping.set(token, {name, attr ....})
}
}
private async verifyTokenIn(
srcToken: string,
) {
const user = this.credentialMapping.get(scrToken);
if(!user) {
xxxx
}
return user;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for reviewing and suggesting, it's a great suggestion, I have fixed to add the read options in onActivate
, thanks !
} | ||
} | ||
|
||
if (srcToken !== ansToken) return { authenticated: false }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can request users to hash their passwords before setting Vulcan like Trino did.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just want to confirm:
Browsers send basic auth with base64(username + ':' + password)
, but I haven't seen any decoding process in this file. Does it mean users need to set the correctly base64 encoded password to config?
For example:
base64('test) = dGVzdA==
base64('ivan:test') = aXZhbjp0ZXN0
I need to set user=ivan and token=aXZhbjp0ZXN0 to allow ivan/test to login.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for reviewing and suggesting!
I will change to Trino suggesting a password format ( bcrypt or PBKDF2 ), currently, seems bcrypt will be great for checking, I think.
And for the question, Does it mean users need to set the correctly base64 encoded password to config?
> Yes.
But I seems I miss the checking username part, I will add it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for suggesting the hash password idea, I have adopted it in BasicAuthenticator
and PasswordFileAuthenticator
, each using a different hash way.
For the SimpleTokenAuthetnciator
, I make it really simple way for matching whether the value is the same or not.
break; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can return here if no ansToken found:
if (ansToken === '') return {authenticated: false};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for suggesting, I have fixed it!
- support "BaseAuthenticator" for user extending to customize authenticator. - add built-in "HttpBasicAuthenticator" for authenticate http basic token. - refactor type "KoaRouterConext" and "KoaNext". - create "AuthMiddleware" to handle authenticator through implemented authenticators.
- modify the middleware order in "BuiltInRouteMiddlewares". - fix "AuthMiddleware" to set user auth data to correct place in context. - add basic authenticator test cases. - add auth middleware test cases.
- refactor "KeysetBasedStrategy" for passing "keyName" directly. - add cursor, offset, keyset pgination strategy test cases.
f780a55
to
9f31a85
Compare
Hi @oscar60310, I have fixed all parts from your comment and suggestion! I have also updated the PR description. Thanks a lot for your reviewing and suggestion! |
- 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.
9f31a85
to
c95709b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
*/ | ||
SUCCESS = 'SUCCESS', | ||
FAIL = 'FAIL', | ||
INCORRECT = 'INCORRECT', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: Can we use other words like "indeterminate", "skipped", "bypass" instead of "incorrect"? "Incorrect" makes me think about "bad credentials", and "fail" means failure to authenticate due to internal issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion! I chose the indeterminate
for renaming,
|
||
this.authenticators = authenticators.reduce<AuthenticatorMap>( | ||
(prev, authenticator) => { | ||
if (authenticator.activate) authenticator.activate(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Activate functions are asynchronous, we can't wait for them to fulfill in the constructor, maybe we need a activate function for middleware too, and call it from upstream.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for suggestion, I have refactored it, please check :D
…hen active auth middleware
Hi @oscar60310 I have fixed the remaining two-part, please check it, thanks so much! |
Description
Support to provide built-in basic authenticator and authenticator abstract class for user customizing.
The Authenticator provides the authentication feature to make our user ( DA/DE ) could define the access way (e.g: username/password or token or credential ) and read each DC user's role with some customized attribute ( e.g: department ) for authorization used.
For the implementation, Vulcan provides three built-in authenticators and sets each authenticator option under
options
ofauth
.BasicAuthenticator
The HTTP Basic authenticator uses the authorization header
Basic <credential>
(insensitive for auth type) to authenticate. The credential encode base64 from<username>:<password>
, and it supports two set user credentials method defined in project YAML.The above
htpasswd-file
could set the user and md5 hash password in the file,user-list
could let you set in yaml directly.After you authenticate failed by request with authorization header
Basic <credential>
, then it will return the result withWWW-Authenticate
header in the response.SimpleTokenAuthenticator
The Simple Token authenticator uses the authorization header
Simple-Token <credential>
to authenticate. The credential could be of any value and it authenticates from your user credentials in project YAML.The above
token
in config is also could be any value, so the simple token authenticator is just a simple way to authenticate the same credentials from the authorization headerSimple-Token <credential>
(insensitive for auth type). InSimpleTokenAuthenticator
, it will respond a JSON failed result like below when you authenticate failed:PasswordFileAuthenticator
The Password File authenticator uses the authorization header
Password-File <credential>
(insensitive for auth type) to authenticate. The credential encodes base64 from<username>:<password>
, and it authenticates by the password file, we could set the path in project YAML.The password file should be
<username>:<bcrypt-password>
format in each line, it means the password need to encodebcrypt
with round 10 first and then add the file. If not, we will show an error message to the notice user.If authenticate failed, it will respond json format like the below:
How to use it
You can use our built-in authenticator and define the property with its identifier under
options
ofauth
module name in project config YAML to get the auth default defined information. You could define one to multiple authenticator options in YAML depend on what authentication you want.Please make sure to keep the same
attr
for consistency when you define the same user data in each different authenticator, but you could also omit this consistency when you want.Sample: doing the auth by the basic authenticator
The new version authenticator which connected which client-server is still designing, so I will create a new branch and pr to develop it after the new design confirms in the next meeting.
How To Test / Expected Results
For the test result, please see the below test cases that passed the unit test:
Commit Message