-
-
Notifications
You must be signed in to change notification settings - Fork 15
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
Add auth update endpoint #48
base: main
Are you sure you want to change the base?
Conversation
5096495
to
659692e
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.
Thanks for the PR!
Always add a link to the issue when you push a PR, e.g. Fixes #36
It allows people following the issue to be notified and readers of the issue to know that a PR has been pushed.
Your branch is out of date, you need to sync your fork with this repo, pull the changes and resolve conflicts.
/auth
is related to authentication, it must be used for login, logout, reset password, etc.
Regarding the CRUD of user details, you need to create a new folder routes/api/users
(the autoloader will automatically prefix all the routes defined in this folder with path /api/users
).
I wouldn't mix password update with other user details update, as password update require specific logic and is more security sensible. Most of the time, updating user details only require payload validation.
f4045d0
to
b87b39e
Compare
This is my first PR, I'm waiting for your review before committing to writing a test |
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.
You need to write tests that prove that the feature work as expected and reach 100% code coverage: https://en.wikipedia.org/wiki/Code_coverage
src/routes/api/user/index.ts
Outdated
.where({ username }) | ||
.first() | ||
|
||
if (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.
if (user) { | |
if (!user) { |
Prefer early return, if there is no user, the program can stop.
src/routes/api/user/index.ts
Outdated
const username = request.session.user.username | ||
|
||
try { | ||
const user = await fastify.knex<Auth>('users') |
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.
All these operations should be wrapped in a transaction to avoid race conditions.
src/schemas/users.ts
Outdated
export const UpdateCredentialsSchema = Type.Object({ | ||
currentPassword: Type.String(), | ||
newPassword: Type.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.
I would force to use a strong password and check that the two values are different.
src/routes/api/user/index.ts
Outdated
import { UpdateCredentialsSchema } from '../../../schemas/users.js' | ||
|
||
const plugin: FastifyPluginAsyncTypebox = async (fastify) => { | ||
fastify.put( |
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 think we should use a rate limiter to mitigate brute force attack, there is an example here:
Line 74 in 4470023
preHandler: fastify.rateLimit({ |
src/routes/api/user/index.ts
Outdated
} | ||
|
||
reply.status(401) | ||
return { message: 'Invalid username or password.' } |
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.
This error message is wrong.
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.
This error message is wrong.
I think we should not return to the client if the user or password is wrong, it is not a good security design to prevent potential attackers from getting information about the part of the login information that is incorrect.
Or i have missed something
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 think we should not return to the client if the user or password is wrong
For a auth endpoint yes, for a setting endpoint, no. Because we know this is the user/admin that do this operation (unless hacked, this is why we have rate limiter). We must inform why the request for password update failed.
Regarding the line 52
, if there is no user session, the error should be detected ahead of time in this hook:
demo/src/routes/api/autohooks.ts
Line 9 in 4470023
if (!request.session.user) { |
But you can still return: reply.unauthorized()
, it will set status code to 401
automatically.
Congrats for your first PR 👍 |
src/schemas/users.ts
Outdated
export const UpdateCredentialsSchema = Type.Object({ | ||
currentPassword: Type.String(), | ||
newPassword: Type.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.
export const UpdateCredentialsSchema = Type.Object({ | |
currentPassword: Type.String(), | |
newPassword: Type.String() | |
}) | |
const Password = Type.String({ | |
// at least 1 upper case, 1 lower case, 1 numeric, 1 special characters with minimal 8 characters. | |
pattern: "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$", | |
minLength: 8 | |
}) | |
export const UpdateCredentialsSchema = Type.Object({ | |
currentPassword: Password, | |
newPassword: Password | |
}) |
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.
const Password = Type.String({
// at least 1 upper case, 1 lower case, 1 numeric, 1 special characters with minimal 8 characters.
pattern: "^(?=.?[A-Z])(?=.?[a-z])(?=.?[0-9])(?=.?[#?!@$%^&*-]).{8,}$",
minLength: 8
})
Thanks for the suggestion, also the proposed pattern is draft to give me an idea because that it only checks for the presence of at least one uppercase, one lowercase, one digit and one special char, but it does not allow for any additional characters beyond these, finally the pattern defined only match strings that are exactly 4 characters long
The following i think is more accurate
const atLeastOneUpperCasePattern = '(?=.*?[A-Z])'
const atLeastOneLowerCasePattern = '(?=.*?[a-z])'
const atLeastOneNumericPattern = '(?=.*?[0-9])'
const atLeastOneSpecialCharPattern = '(?=.*?[#?!@$%^&*-])'
const passwordPattern = `^${atLeastOneUpperCasePattern}${atLeastOneLowerCasePattern}${atLeastOneNumericPattern}${atLeastOneSpecialCharPattern}.*$`
Also, the minimum length rule is duplicated in both the regex pattern and the JSON Schema definition, i'll keep the rules from the schema.
May be we have specific reason to keep both ?
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.
the pattern defined only match strings that are exactly 4 characters long
No, it matches any string that is 8 characters or longer. See the last part .{8,}
.
the minimum length rule is duplicated in both the regex pattern and the JSON Schema definition, i'll keep the rules from the schema.
By looking at the pattern, it is very hard to understand how it works and does it limit the length.
The minLength
is used here for ensure the string is at least 8 characters and faster understanding on it.
I don't knows which one take higher priority to check in ajv
but more details is better.
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.
"The minLength is used here for ensure the string is at least 8 characters and faster understanding on it."
True, thanks for your answer
faa87b6
to
b396657
Compare
test/routes/api/user/user.test.ts
Outdated
}) | ||
|
||
assert.strictEqual(res.statusCode, 400) | ||
assert.deepStrictEqual(JSON.parse(res.payload), { message: 'body/newPassword must match pattern "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).*$"' }) |
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 should find a way to return a more explicit error message
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.
Password must contain at least one uppercase letter, one lowercase letter, one digit, and one special character (#?!@$%^&*-).
b396657
to
cf565be
Compare
Code coverage is not reached, meaning some part of the code was not executed during test. |
68c3bb2
to
bac1131
Compare
src/routes/api/user/index.ts
Outdated
errorResponseBuilder: function (_, context) { | ||
return { | ||
statusCode: 429, | ||
error: 'Too Many Requests', | ||
message: `You have reached the request limit. Please try again in ${Math.floor(context.ttl / 1000)} seconds.`, | ||
date: new Date().toISOString(), | ||
retryAfter: context.ttl | ||
} |
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.
You leverage errorResponseBuilder
but the message looks like the default one.
You can write a more explicit message regarding password update or you could just pass the needed configuration and let the plugin build the error for you.
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.
No need to override the built in error message, I'll delete it
src/schemas/users.ts
Outdated
const atLeastOneUpperCasePattern = '(?=.*?[A-Z])' | ||
const atLeastOneLowerCasePattern = '(?=.*?[a-z])' | ||
const atLeastOneNumericPattern = '(?=.*?[0-9])' | ||
const atLeastOneSpecialCharPattern = '(?=.*?[#?!@$%^&*-])' | ||
|
||
const passwordPattern = `^${atLeastOneUpperCasePattern}${atLeastOneLowerCasePattern}${atLeastOneNumericPattern}${atLeastOneSpecialCharPattern}.*$` |
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.
Not fundamentally opposed, but imo the initial regex is very simple to read, the concatenation is disturbing.
src/schemas/users.ts
Outdated
const atLeastOneSpecialCharPattern = '(?=.*?[#?!@$%^&*-])' | ||
|
||
const passwordPattern = `^${atLeastOneUpperCasePattern}${atLeastOneLowerCasePattern}${atLeastOneNumericPattern}${atLeastOneSpecialCharPattern}.*$` | ||
const Password = Type.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.
const Password = Type.String({ | |
const PasswordSchema = Type.String({ |
test/routes/api/user/user.test.ts
Outdated
|
||
describe('User API', () => { | ||
// Hashed value of `Password123$` | ||
const Password123$ = 'ff57faf149a2bcab41bf7ecbbc8ce491.3ce6b34ea3edb3f0a09f811440885bfeda612832c04bfddc9d4b906019d97fa0' |
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.
const Password123$ = 'ff57faf149a2bcab41bf7ecbbc8ce491.3ce6b34ea3edb3f0a09f811440885bfeda612832c04bfddc9d4b906019d97fa0' | |
const hash = 'ff57faf149a2bcab41bf7ecbbc8ce491.3ce6b34ea3edb3f0a09f811440885bfeda612832c04bfddc9d4b906019d97fa0' |
Can we just hash it with scryptHash
function? If we change the algorithm implementation, this hash will become obsolete, this is not needed.
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.
Good point
src/routes/api/user/index.ts
Outdated
if (!user) { | ||
return reply.code(401).send({ message: 'User does not exist.' }) | ||
} |
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 think we are 99.999% sure that the user exist, as we should have checked session before...
The only reason for that query to return null is if that request.session.user
has been altered for some reason.
Personally, that's why I don't like the idea of working only with the decorator for the session, maybe we should create a getter that acts as a TypeScript guard:
// Request decorator
function getAuth (this: FastifyRequest): Auth {
if (!this.session.user) {
throw this.server.httpErrors.unauthorized()
}
return this.session.user
}
@climba03003 @melroy89 @Fdawgs
How do you handle that?
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.
Isn't it possible for a user to be deleted between the time they authenticate and the time they try to change their password?
By themselves or an admin
99.999% -> ?%
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.
Isn't it possible for a user to be deleted between the time they authenticate and the time they try to change their password?
There is no user administration for now, but this is a valid point. I think we should be able to invalidate/update a session when we remove a user or change any property that is related to authentication like user_roles
.
Maybe we can use Redis to store session with user id so we can write to 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.
Maybe we can use Redis to store session
I will create an issue about this.
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 do you handle that?
I use constraint to derive the route handler before, but currently I handle it inside one handler.
The user information is retrieved way before the handler and I would use it as reference.
Something like requset.user
if you have logged in.
test/routes/api/user/user.test.ts
Outdated
const users = ['random-user-0', 'random-user-1', 'random-user-2', 'random-user-3', 'random-user-4', 'random-user-5'] | ||
|
||
for (const user of users) { | ||
await createUser(app, { | ||
username: user, | ||
password: Password123$ | ||
}) | ||
} |
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 don't understand why you create all the users here, than recreate them individually in all the tests.
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.
This is an oversight, I will delete it and keep the preparation at the unit test level
test/routes/api/user/user.test.ts
Outdated
assert.strictEqual(res.statusCode, 200) | ||
assert.deepStrictEqual(JSON.parse(res.payload), { message: 'Password updated successfully' }) | ||
|
||
await deleteUser(app, 'random-user-0') |
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.
Why do you delete the users at the end of each test?
Just clean all of them in after
hook when you are done.
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.
Tests should be run in isolation as much as possible, shared dependencies across preparation hooks are hell.
But if that's a style you prefer, I can use hooks
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 told you that because you were creating all the users in the before
hook, but I agree with you.
test/routes/api/user/user.test.ts
Outdated
const updatePassword = async () => { | ||
return await updatePasswordWithLoginInjection(app, 'random-user-5', { | ||
currentPassword: 'WrongPassword123$', | ||
newPassword: 'Password123$' | ||
}) | ||
} | ||
|
||
await updatePassword() | ||
await updatePassword() | ||
await updatePassword() | ||
const res = await updatePassword() |
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.
You can also do a simple for loop.
const passwordPattern = '^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).*$' | ||
|
||
const PasswordSchema = Type.String({ | ||
pattern: passwordPattern, |
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.
Currently I don't think there is a way to customize the error message in case the payload doesn't match the pattern directly via the Type Builder Typebox.
I managed to do this via implementing fastify.setErrorHandler
in the password update endpoint configuration, have you ever encountered this problem ?
Is it correct to manage the error message in a custom way at the endpoint ?
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.
Is it correct to manage the error message in a custom way at the endpoint ?
You can using ajv-errors
, so the message can be customize per schema.
https://www.npmjs.com/package/ajv-errors
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.
config:
locally Node v20.12.2
ajv-errors 3.0.0
I have installed and attempted to integrate ajv-errors
with Fastify's AJV plugin, but it seems there is an issue when we add the errorMessage
keyword in the TypeBox or AJV schema.
My research is based on the Fastify documentation
And I think an obsolete implementation example in the documentation, more than 3 years example
// Excerpt from the documentation
const fastify = Fastify({
ajv: {
customOptions: {
jsonPointers: true, // `jsonPointers` is not still part of `AjvOptions` in Fastify is replaced by `jsPropertySyntax`
allErrors: true
},
plugins: [
require('ajv-errors')
]
}
})
@jean-michelet If this is the case, I suggest creating an issue to update this part of documentation.
I have create a specific commit as an example of implementation a304da3
// Error thrown by the server
FastifyError [Error]: Failed building the validation schema for PUT: /api/user/update-password, due to error strict mode: unknown keyword: "errorMessage"
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.
Regarding the custom error message for AJV formatting, I haven't found a solution, and it seems that @climba03003 hasn't either ? as they approved the PR following my message #48 (comment)
As for SonarQube's warning about duplication, that's code I don't want to abstract and SonarQube doesn't know the difference
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.
Is there a way to comment the lines with false positives to avoid warnings?
https://sonarcloud.io/project/security_hotspots?id=fastify_demo&pullRequest=48&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=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.
We can review on the dashboard and claim it is fixed or safe.
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.
@Eomm
Is it possible to give these rights to the Fastify members that collaborates on this repo?
Besides me, I am aware of @climba03003 and @Fdawgs.
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 believe it can identify the Github account and give the correct permission?
Because I have updated the sonarcloud issue when I using the Github OAuth.
a304da3
to
821c631
Compare
In RESTful architecture, resource naming conventions recommend using plural nouns for resource names. |
- Added a new schema `UpdateCredentialsSchema` for updating user credentials
- Change unauthorized response to return status code 401 and a message when user does not exist.
- Removed the errorResponseBuilder function from the user route configuration.
- Simplify the password pattern regex to require at least one uppercase, one lowercase, one numeric, and one special character
- Added rate limiting configuration to limit requests to 3 per minute.
- Rename `user` to `users` in file paths - Update describe block from 'User API' to 'Users API' - Update tags from 'User' to 'Users' in API endpoints definition
914466a
to
9326a67
Compare
- Remove rate limiting configuration from the home route to simplify the code and improve performance.
- Updated loop condition from `< 3` to `< 4` to match new rate limit of 4 requests
Quality Gate failedFailed conditions |
Description
Fixes #36
Key Changes
Checklist
npm run test
andnpm run benchmark
and the Code of conduct