-
-
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
added an RFC 7662 compliant OAuth2 auth adapter #4910
added an RFC 7662 compliant OAuth2 auth adapter #4910
Conversation
Codecov Report
@@ Coverage Diff @@
## master #4910 +/- ##
=========================================
+ Coverage 93.93% 94% +0.06%
=========================================
Files 123 125 +2
Lines 8975 9091 +116
=========================================
+ Hits 8431 8546 +115
- Misses 544 545 +1
Continue to review full report at Codecov.
|
* changed option names in auth adapter from snake case to camel case * added underscore prefix to helper function names * merged consecutive logger calls into one call and use JSON.stringify() to convert JSON objects to strings * changed error handling (ParseErrors are no longer thrown, but returned)
I've added some test coverage, but I'll need some help here. First of all: how can my PR affect test coverage of code in files like MongoStorageAdapter.js or RestWrite.js? :o I'm new to nodejs unit testing, have no idea how codecov.io works, etc. Second: how can I mock an OAuth2 provider? It seems to me that the largest part of my code missing test coverage is the one that does the HTTP request to the OAuth2 server. |
First of all, thanks for the PR.
Don’t worry about thiose, those are false positives that we’ll need to address at one point.
Did you try jasmine spies that can help you with that? |
@flovilmart Thanks for the tip. I'm not really familiar with jasmine spies, but I'll look into it. |
I've noticed that all authentication adapters (that do any logging) use a
The latter looks like this (eg. in src/Adapters/Storage/Mongo/MongoStorageAdapter.js):
It seems to me that the two behave quite differently. I tested this by passing the path to a config.json to the parse server launching commandline and set the "logLevel" in this config.json to "silly". Then I added logging calls to my auth adapter (for each loglevel that exists in LoggerController.js) and tested both the I've no idea why do these two approaches work differently, but they do. So I opted to use the |
…uire() of the logger with an import (the former does not work correctly)
@zsmuller this is likely that when using require, it loads the default logger from before it’s configured, and the other method gets the logger after it’s configured. I need to have a look at it at one point or refactor so the logger is passed as a contextual parameter or accessed differently. For now, don’t worry much about it :) |
@zsmuller I have a question for you, How do we use multiple auth providers with this? The client always referes to the oauth by it's name, in this case this would be Perhaps the options for
What do you think? |
Currently you don't. Since all present authentication adapters support accesstoken validation only with a single source, I didn't anticipate the use-case of having multiple OAuth2 providers behind a single Parse instance. But it's a good idea and easy to implement. I'll look into it. |
I believe it’s critical to implement (as everyone will expect it to work this way). Also, do you think we can refactor all oauth 2 providers to leverage your module? |
Unfortunately no. I added already in the description of this PR that many social network providers implemented their own solutions for access token validation way before RFC 7662 was finalized and these implementations are not compliant (with the RFC). So you cannot use this module with Facebook, Google, GitHub, Instagram, etc. I'd have to check each auth adapter whether the respective providers have any token introspection endpoint, but I seem to remember that eg. Google and Facebook do not. |
That's too bad :/ but I guess it's ok. |
Yes. It would have been nice if two auth adapters could handle all providers (one for OAuth1 and one for OAuth2). |
Supporting multiple OAuth2 providers with a single oauth2 adapter would also mean that the client should send a provider name (as specified in the Parse Server auth config for the adapter) in the authData request.
Otherwise the adapter would have to try accesstoken authentication with each configured provider. This would be very ineffective from a performance PoV and also a security issue since the accesstoken from the client would end up at all the configured OAuth2 providers and only one of them should have it (the others could "misuse" the received accesstoken ... even if that's quite unlikely, they shouldn't ever have the chance to do so). |
Yeah it has to send the provider name, but not this way (otherwise we'll need a lot more refactoring). The client should send it this way:
Then on the server we could have this logic:
|
I don't see why would we need more refactoring. I thought that the client can send in the authData almost whatever it wants, only the "id" field is mandatory, but other fields are allowed as well. Not even the "access_token" field is mandatory ... "twitter" doesn't seem to use it. Eg. in the Parse Server Guide there're many adapter-specific fields that only occur with a few or even a single adapter. https://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication The "facebook" adapter accepts and uses a "expiration_date" field, the "facebookaccountkit" adapter accepts a "last_refresh" field, the "twitter" adapter has lots of custom fields (and no "access_token" field! since it's based on OAuth 1.0), "google" accepts an "id_token" as well, "linkedin" accepts an "is_mobile_sdk", etc. Why couldn't we put in the oauth2 authData a "provider" field (or an "oauth2_provider" if that's less likely to conflict with possible future changes of the Parse authentiction adapter API)? :o Moreover my suggestion would require changes only in the oauth2 adapter. Your approach requires modification of the auth adapter handling code. |
The problem is not the payload but the key. Auth data is stored as
In the oauth2 case we're designing now, This is why, I suggest we keep the same strategy, and The oauth2.js file should not expose directly the validation methods, but instead generate validators based on the configuration. |
I see what you mean. But if that's the case, your example for configuration is not aligned with this concept. You wrote this:
The following seems to be a better match conceptwise:
I mean if oauth2.js is not a "provider" itself and it generates the validators for the actual providers, then shouldn't the configuration reflect this structure as well? |
That works this way too :) I like it! |
src/Adapters/Auth/index.js would require some refactoring. |
I'm not convinced that the provider/adapter object generation should be in oauth2.js. If this seems OK to you, I'll go in this direction with the next commit. |
That makes sense as it’s index.js that provides all the logic :) |
Is the AuthAdapter class (in src/Adapters/Auth/AuthAdapter.js) used for anything? I don't see any other code referencing it. :o |
It’s used as an example for valid auth adapters, but you’re right it’s not in used now. The intent was to use it as a base to validate the passed adapters, like the other adapters (email, files etc...) in the project. |
Currently most/many authentication adapters can be used ootb, without any configuration. This is by design, as described in the documentation:
Is this not a (security) problem? Shouldn't authentication adapters be disabled by default and only enabled if the config (server admin) says so? Eg. having an "enabled" property set to "true" in the config for the given adapter? I know that this would be a backwards incompatible change and obviously something like this would at least mandate it's own pull request or even maybe a major Parse server release (i.e. a step in the major version). So my question is theoretical at this point. |
I'm not quite sure what's happening.
What am I supposed to do? :o |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
Hi @flovilmart! I can continue as well, I was waiting for some help (see my last comments) and you never replied. |
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.
@zsmuller Thanks for getting this to this point!
I've read over the conversation and a quick review from me.
Left some comments on how more tests are 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.
this looks good to me.
the adapter validation would be a nice addition so we can detect mis config at config time instead of when trying to use, if i'm groking it right, but this seems plenty useful as is.
I have a question about the handling of promises and throwing that isn't specific to this functionality, but I'd like to resolve.
I can try and test my concern later this afternoon or tonight if no one beats me to it....
* } | ||
*/ | ||
|
||
const Parse = require('parse/node').Parse; |
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.
prefer destructuring.
}); | ||
}); | ||
try { | ||
await adapter.validateAuthData(authData, providerOptions); |
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 not done async, not sure how the validate auth data will behave? worth trying....
!response.active || | ||
(options.useridField && authData.id !== response[options.useridField]) | ||
) { | ||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS); |
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.
shouldn't this be return Promise.reject(new Parse.Error....);
} | ||
return requestTokenInfo(options, authData.access_token).then(response => { | ||
if (!response || !response.active) { | ||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS); |
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.
again, mixing of promise and throw concerns me. maybe not issue, just haven't seen.
used to async / throw, non-async / Promise.reject.
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 you look at the other authAdapters this is how it's handled. I'll let you fiddle around with it. I probably missed something somewhere
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.
here's a decent writeup that shows a subtle difference, but in this case, i think we're all good. https://stackoverflow.com/questions/33445415/javascript-promises-reject-vs-throw
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.
Nice!
@zsmuller Thanks for the PR! Sorry it took so long. |
* added an RFC 7662 compliant OAuth2 auth adapter * forgot to add the actual auth adapter to the previous commit * fixed lint errors * * added test coverage * changed option names in auth adapter from snake case to camel case * added underscore prefix to helper function names * merged consecutive logger calls into one call and use JSON.stringify() to convert JSON objects to strings * changed error handling (ParseErrors are no longer thrown, but returned) * added description of the "debug" option and added this option to the tests too * added a check of the "debug" option to the unittests and replaced require() of the logger with an import (the former does not work correctly) * added AuthAdapter based auth adapter runtime validation to src/Adapters/Auth/index.js, added capability to define arbitrary providernames with an "adapter" property in auth config, replaced various "var" keywords with "const" in oauth2.js * incorporated changes requested by flovilmart (mainly that oauth2 is now not a standalone adapter, but can be selected by setting the "oauth2" property to true in auth config * modified oauth2 adapter as requested by flovilmart * bugfix: defaultAdapter can be null in loadAuthAdapter() of index.js (my change broke the tests) * added TODO on need for a validateAdapter() to validate auth adapters * test cases and cleanup
Shortly after the OAuth2 specification (meaning the two core specs: RFC 6749 and 6750) were finalized and approved in October 2012, people realized that one important aspect was completely overlooked: access token validation by resource servers.
In 2012 November a new RFC draft was started to cover this area: RFC 7662 with the title "OAuth 2.0 Token Introspection". It was finalized and published in October 2015, three full years later!
At this point most companies in the OAuth2 business already implemented their own custom endpoints for token validation and Parse Server's authentication adapters are based on these custom solutions.
However there are many OAuth2 providers and products that already implement the token introspection endpoint.
Eg.
(etc.)
This PR contains an implementation of a Parse Server authentication adapter based on RFC 7662 and it's flexible enough (with various configuration options) to be used with any RFC-compatible token introspection implementation.
It was written for a project at NNG Llc (www.nng.com).