-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
feat: new auth adapter #7079
feat: new auth adapter #7079
Conversation
Codecov Report
@@ Coverage Diff @@
## alpha #7079 +/- ##
==========================================
+ Coverage 94.17% 94.30% +0.13%
==========================================
Files 182 182
Lines 13717 13880 +163
==========================================
+ Hits 12918 13090 +172
+ Misses 799 790 -9
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
WebAuthn adapter finished, Parse Server is ready to handle FIDO:
Since we need a username for webauthn : a developer can combine anonymous user + webauthn to allow full secure password less authentication :) Need: #7052 to reduce changes @davimacedo we can also merge this one directly and close #7052 , this branch has some small corrections compared to #7052 |
Okay in real world implementation of an OTP SMS system i detected wrong implementation on challenge handling, i will push the fix then we will be okay ! |
Okay i pushed some fixes after some beta testing on my company project that use extensively the Auth System. i implemented an SMS OTP adapter, everything works perfectly. OTP Adapter pseudo code export const OTPAuth = {
policy: 'additional',
async challenge(
challengeData: boolean,
authData: OtpAuthDataInput,
options: any,
req: any,
user?: Parse.User,
) {
if (!user || !user.get('phone')) throw new Error('User not found')
const otp = new OTP(user.id, user.get('phone'))
await SMSAdapter.send(
user.get('phone'),
`OTP code : ${await otp.generate()}`,
)
},
async validateSetUp(authData: CreateUpdateOtpAuthDataInput, options: any, req: AuthReq) {
return checkAuthorization(req)
},
async validateUpdate(authData: CreateUpdateOtpAuthDataInput, options: any, req: AuthReq) {
return checkAuthorization(req)
},
async validateLogin(authData: OtpAuthDataInput, options: any, req: AuthReq, user?: Parse.User) {
try {
// As an additional auth system user should be already identified
// with another default auth system
if (!authData.code || !user?.id) return throwError()
if (!user.get('phone')) return throwError()
const otp = new OTP(user.id, user.get('phone'))
await otp.check(authData.code)
return { doNotSave: true }
} catch (e) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw new Parse.Error(403, e.message)
}
},
} |
@davimacedo @mtrezza @dplewis , so this one is now fully finished and also tested on a critical security project ! |
closed #7052 in favor of this one |
@Moumouls it looks the tests are failing after the last commit |
Thanks @davimacedo , I'll take look this morning. Do you think we can merge this one asap ?
|
spec/ParseUser.spec.js
Outdated
@@ -1789,7 +1789,7 @@ describe('Parse.User testing', () => { | |||
}); | |||
}); | |||
|
|||
it('should allow login with old authData token', done => { | |||
it('should not allow login with old authData token', async () => { |
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.
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 pick up the discussion in the advisory, I think there were some open questions what it means to change 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.
Should we add that as a breaking change?
Looks good so far @Moumouls! Would you see any value in adding a helper method to For example, I have all my login code in It would be nice to be able to write: Parse.Cloud.registerAuth('authName', {...}); Rather than having to export and import back to index.js. EDIT: you’ve already done so much on this PR so I might tackle this one after this is merged |
So here after a master update, how can help to make this one merged ? :) |
Resolving the open comments. There is an open discussion about stale auth data in the advisory. |
@mtrezza @davimacedo this pr is now synced with master, but it seems that CI have issue with mongodb runner, any idea on this ? |
Not sure what the issue here is, but looking at other PRs like #7312 the CI works (except for some nasty flaky tests). You could try to revert the package-lock file and add the new dependencies without deleting the package-lock. Maybe this issue is due a sub-dependency. |
I guess #7079 (comment) cannot be easily suggested via review, so I hope @Moumouls can add this to the PR. |
Co-authored-by: dblythy <[email protected]>
Co-authored-by: dblythy <[email protected]>
@mtrezza @dblythy , I'll not add #7079 (comment) The AuthAdapter file is only used for documentation/example purposes. Also as you can see, all other adapters are in fact not classes, there is no notion of constructor and parameter validation. The Anyway, I added validation on the policy key. it could help a developer to debug an auth adapter ! |
How would the developer add that key to the adapter? |
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 haven't seen any tests for how a developer would throw a custom error from an auth adapter, such as
challenge
. -
I have also found that if an error is thrown from validateSetUp, validateUpdate, it is not returned to the client or caught by the server. Errors thrown from
challenge
are returned as{"code":1,"message":"Internal server error."}
. -
I noticed the last argument in all the adapter functions is a
Config
option, which is inconsistent with the Parse Cloud protocol, and pending discussion in Expose Parse Server config in Cloud Code #7869. Config should either be exposed viaconfig.get
orreq.config
(as per the Parse Cloud trigger request), assigning as the last argument seems like a completely different option. -
The request param (
Parse.Cloud.TriggerRequest
) fieldtriggerName
is always empty, it would be good to passauthAdapter.field
to there (e.gchallengeAdapter.challenge
). -
The request param (
Parse.Cloud.TriggerRequest
) fielduser
is always empty, I think it's intuitive for the user calling the auth to be passed in here (can other users' with write access update authData on other users?) -
The third argument for
challenge
and the second argument for the other functions is set to JSDocs as "additional options", but in my usage it shows up as the Auth adapter itself (perhaps it should be "this", but then what's the point of the argument):
{
validateSetUp: [Function: validateSetUp],
validateUpdate: [Function: validateUpdate],
validateLogin: [Function: validateLogin],
validateAppId: [Function: validateAppId],
validateAuthData: [Function: validateAuthData],
challenge: [Function: challenge],
options: [Object]
},
-
Throwing from
validateAuthData
returnsUser not found
regardless of the error -
I think error code "OTHER CAUSE" is overused here, I think we should probably get some new error codes considering this is an Auth adapter upgrade. Other cause is code
-1
which I think should probably only be used sparingly as a last resort. Your thoughts @mtrezza? -
Perhaps the new Auth Adapter protocol should get its own spec - the spec file this in is quite bloated and more or less refers to custom auth providers.
-
I also think the tests should start with a simple test which maps out usage (no spies,
expects
onvalidateAppId
fields for example) so developers can get an easy view of how to implement. All the existing tests are quite complex to cover the code - a few simple ones at the start e.g how Parse Cloud tests are done could be nice. -
It would be good for an enum on
policy
so it's clear what "default", "solo" and "additional" actually mean -
I like how you don't have to return promises - nice improvement.
Thanks again for your hard work here @Moumouls!
@@ -659,7 +659,7 @@ RestQuery.prototype.runFind = function (options = {}) { | |||
.then(results => { | |||
if (this.className === '_User' && !findOptions.explain) { | |||
for (var result of results) { | |||
cleanResultAuthData(result); | |||
this.cleanResultAuthData(result, this.auth, this.config); |
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.
What's the purpose of these arguments?
key => this.data.authData[key] && this.data.authData[key].id | ||
); | ||
|
||
if (!hasAuthDataId) return; |
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 (!hasAuthDataId) return; | |
if (!hasAuthDataId) { | |
return; | |
} |
|
||
if (!hasAuthDataId) return; | ||
|
||
const r = await Auth.findUsersWithAuthData(this.config, this.data.authData); |
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.
Poorly named variable
}; | ||
|
||
RestWrite.prototype.handleAuthData = async function (authData) { | ||
const r = await Auth.findUsersWithAuthData(this.config, authData); |
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.
Poorly named variable
} catch (e) { | ||
// Rewrite the error to avoid guess id attack | ||
logger.error(e); | ||
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'User not found.'); |
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.
Should the error here be ignored here? Could the error be thrown from await validator
|
||
return authDataValidator(adapter, appIds, providerOptions); | ||
const authAdapter = loadAuthAdapter(provider, authOptions); | ||
if (!authAdapter) return; |
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 (!authAdapter) return; | |
if (!authAdapter) { | |
return; | |
} |
}; | ||
|
||
const hasMutatedAuthData = (authData, userAuthData) => { | ||
if (!userAuthData) return { hasMutatedAuthData: true, mutatedAuthData: authData }; |
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 (!userAuthData) return { hasMutatedAuthData: true, mutatedAuthData: authData }; | |
if (!userAuthData) { | |
return { hasMutatedAuthData: true, mutatedAuthData: authData }; | |
} |
const mutatedAuthData = {}; | ||
Object.keys(authData).forEach(provider => { | ||
// Anonymous provider is not handled this way | ||
if (provider === 'anonymous') return; |
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 (provider === 'anonymous') return; | |
if (provider === 'anonymous') { | |
return; | |
} |
user = await this._authenticateUserFromRequest(req); | ||
} | ||
|
||
if (!challengeData) throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Nothing to challenge.'); |
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 (!challengeData) throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Nothing to challenge.'); | |
if (!challengeData) { | |
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'Nothing to challenge.'); | |
} |
if (typeof challengeData !== 'object') | ||
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'challengeData should be an object.'); |
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 (typeof challengeData !== 'object') | |
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'challengeData should be an object.'); | |
if (typeof challengeData !== 'object') { | |
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'challengeData should be an object.'); | |
} |
Yes |
@Moumouls, @rhuanbarreto, any feedback to @dblythy's points? |
My only feedback is that this PR will never be merged once many discussions are outside the scope of the PR, like correct usage of error messages. I think its functionality should be split in many different PRs with features. In this PR, the only feature I'm interested in is the ability to not save sensitive data (authData) in the database, once this is a security issue. If the database leaks, not only the session token but also auth data gets leaked which can lead to a bigger liability due to GDPR. |
Which of the points in #7079 (review) do you see outside of the scope of this PR? Let's see what we can split off into separate PRs. |
Our community has been very vocal on breaking changes, and if we introduce new features only to potentially break them (such as the expected error codes - e.g, a developer builds an auth system around the error This PR is almost there in my view though and most of my feedback is around future proofing / making tests easier to interpret. |
Hi @mtrezza, @rhuanbarreto @dblythy Yes, I tried to do my best in this PR, but then I start to "waste" time trying to make these new features join the official Parse Server repo (like some other features). This PR helped a lot, my team, to better control the auth, implement reusable 2FA, MFA, and improve the security of data saved in the database. Parse Server auth system needed a major refactor. Maybe this PR could have been split into many tiny PR (the future will tell us maybe if a contributor wants to try), but a global overview was also needed to perform this refactor. I tried to find the best time/effort/value to produce these changes. But for now, I'll not provide updates to this PR. And I'll not probably sync new changes that we do internally with my team to continue to improve Parse Auth. I always try to push new features to Parse Server, like Defined Schema, Parse Server config in the request object, Mongo Serverless support, and a new auth system, but even for a "frequent" committer, it feels harder and harder to contribute to Parse Server. I've many plans to improve Parse Server, but based on my current "feeling" and how things going on here I'll prefer to preserve my effort for my team and maybe start a complete fork (Yarn Monorepo, Typescript, Serverless goal, Terraform templates to run Parse Server in 100% serverless on GCP, focus on GraphQL, Test helper package for Jest, improve GraphQL API performance, Improve Parse Server query performance, A Job task adapter to send Jobs to services like Google Cloud Tasks). I really want to send all of this to the community repo but it feels nearly impossible when I see how this PR is going. Have a nice day all ! |
Complex changes require more scrutiny. Everybody is thankful that you share your code and you surely invested a lot of time writing this change. Let's be mindful that others invested a lot of time as well to review your change. Review feedback is an important part of the open-source process to uphold code quality and we want to be thankful to everyone involved as their time is as valuable as yours. Not every feedback needs to be incorporated. Some feedback may be ideas to inspire follow-up contributions while other feedback may be a requirement to merge. Then there is feedback in between where the change works as is but merging it likely causes complications later on. Evaluating these in a mindful discussion is what everyone hopefully aims for. We have seen a similar pattern in #7091 where you submitted a change but didn't have the capacity to incorporate the review feedback. Someone else then continued your PR in #7418. That's legitimate in a collaborative environment. So let's better invest our time to look at the detailed feedback point by point and determine:
|
Continued in #8156. |
New Pull Request Checklist
Issue Description
Allow user to set up webauthn (biometric authentication on mobile device) once he is logged, and then i can log in with webauthn (fingerprint scan on mobile device). Rework auth adapter too support MFA systems, auth challenges systems and better interfaces when writing an auth adapter.
Related issue: #7042
Approach
Implement new WebAuthn Adapter with a challenge JWT strategy to avoid to store challenge into the db.
Add new auth hook methods tiggered in dedicated use cases.
TODOs before merging
Futur work: Need to implement client side webauthn system in JS SDK for an easy usage.