-
-
Notifications
You must be signed in to change notification settings - Fork 932
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 support for scopes #149
Conversation
Resolves https://github.com/thomseddon/node-oauth2-server#49 by obtaining an optional scope parameter from a grant request, which is passed to the model for processing and storage. The authorization flow includes a call to `checkScope` which receives the access token and the request object. In order to support the access code grant flow, the scope param is passed to `saveAuthCode` so that it may be persisted for a subsequent token exchange.
Custom middleware allows for defining scope key requirements on routers or single routes. If a scope requirement is set, the model's checkScope() method is tasked with verifying whether or not the access token possesses the required key(s).
This change allows for simpler access to the scope information, should the developer chose to include it in the getAccessToken response.
Added method descriptors for saveScope and checkScope to the Readme; added a scope param to saveAuthCode, since the user-accepted string must be passed along server-side to the token creation step. Corresponding updates were made to the postgresql example, which now includes a simple usage scenario with a scope string being stored in the oauth_access_tokens table.
Hi Thanks for working on this I think this will be a very valuable addition to this project. Just a question - why a different middleware function ? May I suggest that instead of - We can have - Or maybe ? Thanks Sent from my iPhone On 28 בינו 2015, at 03:53, Lars F. Karlström [email protected] wrote:
|
Hello there, Thank you for the feedback! There are two reasons that I went with a separate middleware here, please tell me if the following seems odd. First, since express manages the concept of routers, I thought it might be valid to protect an entire collection of routes with a single "instance" of the auth middleware, and then define finer-grained scope permissions on a per-route level (though you may, of course, also protect the entire router at once as well). Example: var account = express.Router();
account.all('/*', app.oauth.authorise());
account.get('/', app.oauth.scope({ account: ['basic'] }), function(req, res) {
// ...
});
account.put('/', app.oauth.scope({ account: ['write'] }), function(req, res) {
// ...
});
// ...
app.use('/account', account); The second reason has a bit to do with the first. I might have some misconceptions here, but I believe that, since every inclusion of middleware basically creates a new function (thus my use of "instance" above), it would be preferable to keep said function as lightweight as possible. I'm afraid I can't back this up with a more solid claim though, since I haven't really investigated how this works. |
Ok - that's a great example of why you would want those separate. Looks On 28 January 2015 at 21:13, Lars F. Karlström [email protected]
|
- *mixed* **error** | ||
- Truthy to indicate an error | ||
|
||
#### saveScope (scope, accessToken, callback) |
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.
Any particular reason why the order of parameters differs from checkScope()
?
@lfk: I was in need of scopes and was planning on doing my own PR. Thank you for working on this! |
model.saveScope = function (scope, accessToken, callback) { | ||
// Here you will want to validate that what the client is soliciting | ||
// makes sense. You might then proceed by storing the validated scope. | ||
// In this example, the scope is simly stored as a string in the |
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.
Typo: simly
-> simply
.
@avnersorek: Thanks for validating the concept. :) @nunofgs: You're welcome! I do agree with the issues you pointed out in the documentation and examples, and I'll fix them asap -- just got to extend the tests to make the build pass as well. |
Hi @lfk - firstly thank you for your work on this PR already, it's probably one of the best this project had ever had with regards to implementation quality and code style likeness. Here's my thoughts: Authorisation
Granting
General
Thoughts appreciated |
}); | ||
// Note: To avoid replicating the scope string in both token tables, the old | ||
// access token's scope string must be retrieved and passed along from here. | ||
client.query('SELECT rt.refresh_token, rt.client_id, rt.expires, rt.user_id, at.scope FROM ' + |
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 assumes that expired accessToken
's are not deleted. This takes away control from the developer since he'll be forced to keep all of the access tokens around.
I see two possible solutions for this:
- We can add the
scopes
column to theoauth_refresh_tokens
table. - We can merge the two tables into one:
oauth_access_tokens
andoauth_refresh_tokens
would becomeoauth_tokens
with atype
column. There really isn't any difference between these tables right now.
@thomseddon Maybe I'm missing something. What is the reason for having two tables right now? Is it for the purpose of this example or is it recommended by the spec?
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.
One more note. The OAuth RFC 6749 states in section 6 that when using the refresh_token
grant_type, the client can send a "scope" and the server MUST validate that the scope cannot include any scopes that were not part of the original access_token. Also, if the scope is omitted, it must be inherited from the last access_token.
However, that implementation has been left to the model in this PR. Should this be something that node-oauth2-server is responsible for handling?
@thomseddon: Thank you for the encouraging words! Addressing your feedback in order:
@nunofgs: You're absolutely correct -- whith the understanding that this is not the single, mandated manner of implementing scope storage. I opted for the most straightforward, string-based example here, leveraging the available example code, but there's nothing limiting the developer from creating, say, a connection table between access tokens and a table of available scopes. Since all interaction between the library and the application is defined via |
In addition to the dedicated scope middleware, required scope may now also be defined through the authorise middleware. Multiple scope definitions cascade, meaning that it is possible to protect a router instance via authorise, and add finer-grained requirements for individual routes.
var scope = this.scope || this.req.body.scope; | ||
|
||
var self = this; | ||
this.model.saveScope(scope, self.accessToken, function (err, acceptedScope) { |
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 saveScope function will be called on the model even if the user did not send a scope. In fact, scope here will be undefined
.
I suggest following the pattern set by the saveRefreshToken
function (and even your checkScope
):
if (!scope) return 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.
What is the appropiate course of action if no scope is solicited by the client though? Either the system could assign a default scope and indicate that in the returned payload, or an error message could be displayed. Currently, the decision-making is delegated to the model's saveScope
method. (Note, also, that this whole method might well come to be merged into saveAccessToken),
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.
That's a very good point. I agree.
Also, 👍 for merging this into saveAccessToken
.
@lfk: Are you working on this PR? I'd like to give you a hand but I don't want to spend time implementing anything you've already done :) |
@nunofgs: Absolutely, I'm still working on it. :) I'm awaiting some response on the above discussion before proceeding with any major changes (such as creating a default scope validation method), but there's still plenty to be done, and your help would be much appreciated. I suppose the easiest way to proceed here would be for you to fork ubilogix/node-oauth2-server and submit a PR to the |
I'm thinking we roll this, full On internal representation, the spec specifies that scopes should be sent as a space separated string so we can't really make any more assumptions that that - I see you've implemented nested scopes (JSON body?) but I think trying to support that is outside the scope of the module, of course it would be relatively trivial to parse the scopes yourself... Have I missed any other outstanding questions? |
@thomseddon I agree that the In fact, the more I think about it, the more I believe that |
I agree with @nunofgs above, Plus it takes no effort to parse a string of scopes if they are comma or space separated. |
|
||
var self = this; | ||
this.model.saveScope(scope, self.accessToken, function (err, acceptedScope) { | ||
if (err) return done(error('server_error', false, err)); |
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.
@lfk: I was in the middle of writing a PR (to your PR 😄) and came upon a snag. In the saveScope
implementation you are returning a server_error instead of invalid_scope.
This means that when requesting a token, if you pass an invalid scope, the model has no chance to return a 400 error. It can only return an error which eventually gets mapped as a 503 server error. The only thing the model can do is ignore the invalid scope which will then generate a token anyway.
Also, the AuthCodeGrant middleware also passes the scope
parameter to the saveAuthCode
model function but it too can only cause a 503 server error.
So, right now I don't see a quick way of validating whether the scope is invalid or not, other than using the scope
middleware.
Do you have any suggestions on how to proceed?
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.
@nunofgs: Yeah, I encountered the same issue when I first wrote that part, and I couldn't really find a clean workaround. Nowhere else in the library are we handling "cases" of returned errors, so it would feel awkward to introduce something of the sort here.
The only thing I can think of is to have a dedicated model method, such as validateScope
. I'm writing some more notes on this topic in the PR #149 discussion in a moment, including the saveAuthCode matter.
The scope middleware is utilized for access validation once a token has already been created, it does not influence how scopes are first processed and stored.
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.
Yup, I felt the same pain. I had your PR ready to go but I'm holding off on submitting because I felt the need to create a new error architecture, exactly as you described.
I incorporated almost every change we discussed in this PR, with the exception of passing scope
to the saveAccessToken()
function since it prevents me from throwing an "invalid scope" error, as I described previously. I also added tests for all of the new functionality.
You can take a look at my commits here: https://github.com/seegno-forks/node-oauth2-server/commits/feature-scope but I'll hold off on submitting it until @thomseddon weighs in on 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.
I think you solved the aforementioned issue in a very neat fashion here. 👍
Regardless of how the scope middleware matter turns out, I'd definitely like to incorporate these fixes into the PR.
The tests were updated for the modified scope behaviours.
Looking good!! Two remaining things:
For 2 I prefer the first solution proposed by @nunofgs of adding a "scope" column to the refresh_tokens table? |
Since Other than keeping the model methods lean, is there any reason we don't run |
This commit removes remaining references to `saveScope`, and brings the postgresql example up to date by adding the `validateScope` method and storing the scope string through `saveAccessToken`.
Added the `validScope` callback parameter for the `validateScope` method. Fixed variable name mismatch in Readme.
if (!user) { | ||
return done(error('invalid_grant', 'Client credentials are invalid')); | ||
} | ||
|
||
self.user = user; | ||
self.scope = self.client.scope; |
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 currently always assigns undefined
. I see two possibilities here: either the client is allowed to solicit scopes on its own behalf when issuing the client_credentials grant request, or the model's getClient
will need to include a scope string (as proposed previously by @nunofgs). Is it sound to let the client request its own scope? Any input 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.
I think this should be done on a per request basis and so should be read from the request?
I think my preference would be to just add the scope to the |
Question - when issuing access tokens, how does one filter scopes that are granted to the user who's logging in? If the scopes 'readonly', 'edit', 'admin' are valid for my app, how do I use this PR to allow me to selectively issue only the "admin" scope to certain users? I would have expected the authorization server to check which scopes the user is authorized for in the client's context. The updated samples have the client passing in scope to the /token endpoint, which seems to indicate they already know which scopes a user should have, and the auth server simply validates that the requested scopes are whitelisted. It seems to me that both ...unless of course I'm missing something completely, which wouldn't be unprecedented. Much appreciated. |
Scopes are pretty handy, yes. I found a way to implement them without having to change this library:
And a piece of middleware to check:
Feel free to reach out for some more details. |
@@ -104,8 +104,15 @@ model.saveAccessToken = function (accessToken, clientId, expires, userId, callba | |||
model.saveRefreshToken = function (refreshToken, clientId, expires, userId, callback) { | |||
pg.connect(connString, function (err, client, done) { | |||
if (err) return callback(err); | |||
client.query('INSERT INTO oauth_refresh_tokens(refresh_token, client_id, user_id, ' + | |||
'expires) VALUES ($1, $2, $3, $4)', [refreshToken, clientId, userId, expires], | |||
// Retrieve the scope string from the access token entry |
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.
Out of curiosity, why not just pass in the scope?
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.
Sweet. I've updated my PR with this change.
What is the status on this? Looks about complete, would be great to get this merged in! |
+1 for a merge! |
This was added in #203. |
Thank you @ccamarat for your insight. I'll wait a couple of days more and then decide what to do. |
@mjsalinger: This is now deprecated. |
This is now covered by the 3.0.0 rewrite. @lfk, please open a new issue if anything is missing from this feature. |
These changes allow for scoping routes by introducing the Scope middleware, which may be used as follows:
It requires two new methods in the model:
checkScope
andsaveScope
. In order to properly support scopes in the authorization code grant flow, thesaveAuthCode
method must now also accept a scope parameter, which should be stored and later retrieved during the call togetAuthCode
.In implementations,
saveScope
should sanitize and validate the requested scope keys before proceeding with storage. How scopes are represented is left up to the developer.When the authorisation flow is executed for a scope-protected route, the middleware passes the token object and the scope requirements to
checkScope
, which might return an 'invalid_scope' error if the criteria are not matched.The Readme file has been updated to reflect these changes, and some updates were made to the postgresql example. I'd be happy to provide a more complete example if you feel this is heading in the right direction. I'm working on the tests at the moment, but I'd appreciate any and all feedback/advice on how best to write them, as I don't have much experience in the matter.
Don't hesitate to ask if anything's unclear.
Thanks for reviewing!