Skip to content
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

Closed

Conversation

Moumouls
Copy link
Member

@Moumouls Moumouls commented Dec 16, 2020

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

  • Add tests
  • Add changes to documentation (guides, repository pages, in-code descriptions)
  • Add security check
  • Add new Parse Error codes to Parse JS SDK
  • A changelog entry is created automatically using the pull request title (do not manually add a changelog entry)

Futur work: Need to implement client side webauthn system in JS SDK for an easy usage.

@Moumouls Moumouls marked this pull request as draft December 16, 2020 17:51
@codecov
Copy link

codecov bot commented Dec 16, 2020

Codecov Report

Merging #7079 (bc0e38c) into alpha (4de1c9b) will increase coverage by 0.13%.
The diff coverage is 100.00%.

@@            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     
Impacted Files Coverage Δ
src/Deprecator/Deprecations.js 100.00% <ø> (ø)
src/GraphQL/loaders/parseClassTypes.js 94.87% <ø> (ø)
src/Options/Definitions.js 100.00% <ø> (ø)
src/Options/index.js 100.00% <ø> (ø)
src/cloud-code/Parse.Cloud.js 99.20% <ø> (ø)
src/Adapters/Auth/index.js 98.73% <100.00%> (+5.18%) ⬆️
...dapters/Storage/Postgres/PostgresStorageAdapter.js 95.72% <100.00%> (+<0.01%) ⬆️
src/Auth.js 99.54% <100.00%> (+0.23%) ⬆️
src/Config.js 89.13% <100.00%> (+0.12%) ⬆️
src/GraphQL/loaders/usersMutations.js 92.52% <100.00%> (+1.21%) ⬆️
... and 7 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

@Moumouls Moumouls marked this pull request as ready for review December 18, 2020 18:09
@Moumouls
Copy link
Member Author

Moumouls commented Dec 18, 2020

WebAuthn adapter finished, Parse Server is ready to handle FIDO:

  • biometric authentication on mobile device
  • security keys like yubikey
  • MBP with touch id
  • And more

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

@Moumouls Moumouls mentioned this pull request Jan 5, 2021
9 tasks
@davimacedo
Copy link
Member

Great job @Moumouls ! @dplewis @mtrezza @dblythy any additional thoughts here?

@Moumouls
Copy link
Member Author

Moumouls commented Jan 6, 2021

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 !

@Moumouls
Copy link
Member Author

Moumouls commented Jan 7, 2021

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)
		}
	},
}

@Moumouls
Copy link
Member Author

@davimacedo @mtrezza @dplewis , so this one is now fully finished and also tested on a critical security project !

@Moumouls Moumouls changed the title From Improved Auth Spec: Webauthn Adapter Improved Auth Spec & Webauthn Adapter Jan 18, 2021
This was linked to issues Jan 18, 2021
@Moumouls
Copy link
Member Author

closed #7052 in favor of this one

@davimacedo
Copy link
Member

@Moumouls it looks the tests are failing after the last commit

@Moumouls
Copy link
Member Author

Moumouls commented Jan 20, 2021

Thanks @davimacedo , I'll take look this morning. Do you think we can merge this one asap ?

Okay it seems that i pushed an fdescribe... now it should be okay

@@ -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 () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtrezza @dplewis should we change this? Any other comment for this PR?

Copy link
Member

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?

Copy link
Member

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?

@dblythy
Copy link
Member

dblythy commented Feb 1, 2021

Looks good so far @Moumouls!

Would you see any value in adding a helper method to Parse.Cloud that can register auth, rather then it all being required in index.js?

For example, I have all my login code in /functions/login.js.

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

@Moumouls
Copy link
Member Author

Moumouls commented Mar 2, 2021

So here after a master update, how can help to make this one merged ? :)
@davimacedo @mtrezza

@mtrezza
Copy link
Member

mtrezza commented Mar 2, 2021

Resolving the open comments. There is an open discussion about stale auth data in the advisory.

@mtrezza mtrezza mentioned this pull request Mar 8, 2021
@Moumouls
Copy link
Member Author

Moumouls commented Apr 2, 2021

@mtrezza @davimacedo this pr is now synced with master, but it seems that CI have issue with mongodb runner, any idea on this ?

CHANGELOG.md Outdated Show resolved Hide resolved
@mtrezza
Copy link
Member

mtrezza commented Apr 2, 2021

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.

package.json Outdated Show resolved Hide resolved
@mtrezza mtrezza closed this Apr 2, 2021
@mtrezza
Copy link
Member

mtrezza commented Jul 2, 2022

I guess #7079 (comment) cannot be easily suggested via review, so I hope @Moumouls can add this to the PR.

@Moumouls
Copy link
Member Author

Moumouls commented Jul 3, 2022

@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 policy key is just an optional key that a developer could add (or not add) to the adapter object to tell Parse Server how to handle the auth.

Anyway, I added validation on the policy key. it could help a developer to debug an auth adapter !

@mtrezza
Copy link
Member

mtrezza commented Jul 3, 2022

The policy key is just an optional key that a developer could add (or not add) to the adapter object to tell Parse Server how to handle the auth.

How would the developer add that key to the adapter?

Copy link
Member

@dblythy dblythy left a 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 via config.get or req.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) field triggerName is always empty, it would be good to pass authAdapter.field to there (e.g challengeAdapter.challenge).

  • The request param (Parse.Cloud.TriggerRequest) field user 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 returns User 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 on validateAppId 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);
Copy link
Member

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!hasAuthDataId) return;
if (!hasAuthDataId) {
return;
}


if (!hasAuthDataId) return;

const r = await Auth.findUsersWithAuthData(this.config, this.data.authData);
Copy link
Member

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);
Copy link
Member

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.');
Copy link
Member

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!authAdapter) return;
if (!authAdapter) {
return;
}

};

const hasMutatedAuthData = (authData, userAuthData) => {
if (!userAuthData) return { hasMutatedAuthData: true, mutatedAuthData: authData };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.');
}

Comment on lines +510 to +511
if (typeof challengeData !== 'object')
throw new Parse.Error(Parse.Error.OTHER_CAUSE, 'challengeData should be an object.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.');
}

@mtrezza
Copy link
Member

mtrezza commented Jul 6, 2022

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?

Yes

@mtrezza
Copy link
Member

mtrezza commented Jul 19, 2022

@Moumouls, @rhuanbarreto, any feedback to @dblythy's points?

@rhuanbarreto
Copy link
Contributor

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.

@mtrezza
Copy link
Member

mtrezza commented Jul 19, 2022

discussions are outside the scope of the PR

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.

@dblythy
Copy link
Member

dblythy commented Jul 19, 2022

like correct usage of error messages

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 OTHER_CAUSE for it to only change to a different error one version later) that’s only going to cause more frustration and delays in line with our policies.

This PR is almost there in my view though and most of my feedback is around future proofing / making tests easier to interpret.

@Moumouls
Copy link
Member Author

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 !

@mtrezza
Copy link
Member

mtrezza commented Jul 20, 2022

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:

  • what is an idea for a future improvment
  • what is essential to fix before merge
  • what is optional but would be highly beneficial before merge

@mtrezza mtrezza removed the request for review from davimacedo August 31, 2022 00:37
@mtrezza
Copy link
Member

mtrezza commented Sep 8, 2022

Continued in #8156.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve AuthAdapter capabilities
5 participants