At the moment, when a resource server redirects a client to the auth server, the auth server asks consumer to authorize the access.
Above step is less pleasing and unnecessary when resource server can be completely trusted by auth server.
Below is an example from Google that doesn't make the user authorization step required:
Go to gmail.com, it redirects a user to account.google.com, sign in, it redirects to inbox.google.com, then go to youtube.com, the user is not logged in, but once click "sign in", it redirects the user to account.google.com and instantly redirects the user back to youtube.com. There is no: "allow youtube to access my Google account" step
The OAuth2 protocol RFC 6749, says: "The authorization server validates the request to ensure that all required parameters are present and valid. If the request is valid, the authorization server authenticates the resource owner and obtains an authorization decision (by asking the resource owner or by establishing approval via other means)."
On the other hand, if the resource server is not fully trusted, but 3rd party, making the user authorization step required is still the correct step
Todo:
1. Add a field in the client configuration, (tentatively name it: Is this client 3rd party?), default to ON
2. If above option is ON, do what exactly the current work flow does
3. If above option is OFF, skip the confirmation step and immediately redirect the client back to the resource server with access token
| Comment | File | Size | Author |
|---|---|---|---|
| #55 | commit-interdiff.txt | 321 bytes | e0ipso |
| #45 | interdiff-2932519-36-45.txt | 7.02 KB | alan_blake |
| #45 | 2932519-45.patch | 14.55 KB | alan_blake |
Comments
Comment #2
alan_blake commentedComment #4
alan_blake commentedAdd back page title and correct coding standards.
Comment #5
alan_blake commentedComment #7
e0ipsoI think that we may need a different approach. We could track (using the State API) if a user has allowed a consumer before, regardless of 3rd party or not. If the consumer was already allowed by this user in the past, then skip the confirmation form in subsequent requests.
There should be a UI for users to revoke this trusted access for consumers under their profile somehow.
Comment #8
skyredwang#7 is addressing a UX improvement over the existing solution, however, in my mind, this issue is trying to solve a new problem, which is to allow authorization server to skip user confirmation step if the consent can be acquired via other methods.
From the example above, Google wants YouTube users not bother confirming, while wants Feedly.com users to have to consent. The difference is YouTube is part of Google, while Feedly.com is 3rd party, Google wants a more unified and smooth digital experience within the apps they have total control.
We have another example, our product information and shopping cart is on one Drupal site, while orders is on another Drupal site. We'd like our users to move between two sites smoothly without pausing and clicking a button.
Comment #9
e0ipso@skyredwang @alan_blake you make good points. These are separate issues. However I see the your skipping issue a particular use case of my state tracking issue. Maybe in your example YouTube would always return
$accessPreviouslyConfirmed === TRUEbased on the 3rd party field.We may work these in separate tickets, but I think one would be blocking the other. Would you agree?
Comment #10
skyredwangBelow is my current understanding on how
$accessPreviouslyConfirmedwould improve UX:A resource owner has signed in on a resource server via authentication server. This user decides to sign out the resource server, then later decides to sign back in the same resource server. S/he clicks "sign in" on the resource server, which directs the user to the auth server, since
$accessPreviouslyConfirmed === TRUE, then this user does not need to confirm again. Hence, the auth server redirect the user back to the resource server. As #7 mentioned, there should be piece of UI to let the resource owner to remove the granted access.If my user story above is complete and correct,
$accessPreviouslyConfirmed === TRUEalso needs to be stored with each client. Using State API would work well, since losing these states between migration is not a big deal.On the other hand,
$clientIs3rdPartycheckbox is more like client configuration, migration between servers would need to keep these checkbox. Let me try to write the logic below:From above, I see this issue is trying to provide the first condition as well as the storage and controller behind it. I don't see it is very coupled with
$accessPreviouslyConfirmed === TRUEimprovement, and we can create a separate issue, and use State API for storage, and work out a UI for revoking, once it's ready, we can add in the second condition illustrated above.I probably missed something, as I haven't had the opportunities to dive into the simple_oauth code yet.
Comment #11
e0ipsoYes, I think we are on the same page!
One last detail. Where do you think the
third_partyfield fits better? I would say that it's best to have it in the Consumers project. My reasoning is that other projects may want to leverage that info even without needingsimple_oauth.Comment #12
e0ipso@skyredwang, @alan_blake a good first step may be to add the field definition to the Consumer entity type. Do you agree?
Comment #13
skyredwangI didn't know how Consumers project work until I learned it from @alan_blake today. I agree with #12, tracking a third-party field in Consumers could be useful in general purpose for future application. I created #2933496: Add a third-party field, so we can split the patch in this issue into two.
Comment #14
alan_blake commentedremove third-party field and add $accessPreviouslyConfirmed determine statements.
Comment #15
skyredwangAfter applying the #14 patch,
/oauth/authorizewill throw this error:Comment #16
skyredwangYou need to handle this exception and return the corresponding Response object.
Same as above
Comment #17
alan_blake commentedComment #18
skyredwangThe tests failed probably because you haven't update the test to include "third_party" config, which is NULL right now, causing an exception.
We also need to expand the test to cover when third_party is OFF, we are expecting the redirect response,
Comment #19
alan_blake commentedAdd test cover third_party field.
Comment #20
e0ipsoComment #22
e0ipso@alan_blake tests have failed. Can you please take a look?
Comment #23
skyredwang@e0ipso released a beta2 on Consumers project, now, the fails are down to 2. However, I cannot reproduce these 2 fails on my local environment.
Comment #24
berdirNot sure if we should return plain responses like that, this isn't an API but something that actual users are accessing.
That said, this is not a case that a normal user should ever see.
you could also use loadEntityByUuid() from the entity.repository service for this
this is the only local variable that uses camel case
seems pretty implicit to rely on an existing token? even refresh tokens eventually expire and wouldn't the most likely case to re-visit to be exactly when even the refresh token has expired?
Also, this would introduce a pretty serious security issue as far as I see, because it does not consider scopes. That means a client could first ask for a limited scope and then do a second request with additional scopes that is automatically approved.
Seems to me that approved/connected clients should be stored explicitly together with the approved scopes (or they should be checked for the scopes at least).
And if we'd have that, it should probably also be shown to the user so he can decide to delete clients again.. I can do that on twitter/google/..
So, seems to me that this connected-clients business could get fairly complicated. It also doesn't seem to have test coverage yet, unlike the third-party setting. Maybe it would be best to split it out into a new issue and focus on the third-party setting here?
Comment #25
berdirdidn't notice this before, this is a strange variable name, why "drupal", nobody said that the client is drupal? IMHO just use $client/$clients or $consumer/$consumers
One more thing, getString() should not be used like this. This will return a comma separated list all field items, each item having its properties comma-separted as well.
This happens to be a field type with a single property, so the result of all that logic is just that single property, but that's a complicated way to get that and not that reliable. Just use ->get('third_party')->value == 0 or even just !->get('third_party')->value.
Comment #26
e0ipsoThanks for the work here @alan_blake! You guys at Sparkpad are leading the charge in several fronts. Kudos to you.
Some thoughts about @Berdir's reviews (thanks for that @Berdir!):
#24.1: I agree. An HttpException should be caught by the appropriate response subscriber to turn it into JSON. Maybe try that and see what happens.
#24.2: True. However we've been doing that in other places already in the scenario where we are already injecting
entityTypeManagerfor other means. That allows us to skip an injected service. Any downsides to this approach?#24.3: +1
#25.1 There is some clash of terminology in the Drupal realm and the
thephpleague/oauth2-serverhence the weird naming. This happens elsewhere.#25.2: Agree. Let's cast into a boolean
!$client_drupal_entity->get('third_party')->value.My own review:
This patch makes the Simple OAuth Extras module to depend on
>=8.x-1.0-beta2. Let's make sure we add that to the info file.Let's move this to its own method:
wasAccessConfirmed(…).This could return a list of scopes that have been accepted. If the scopes requested are all in this list, then we can redirect without a prompt. If there is at least one missing scope, we need to prompt the user. This should also address @Berdir's concerns (please confirm).
Let's introduce local variables to improve legibility.
Is there a better way to detect that a flow requires manual user interaction? Probably not, but worth investigating a bit.
This code (and beyond) seems like a copy and paste from the submit handler in the form class.
Can we abstract it and be DRY?
We should inject the form builder, even if it's just because DI is fun.
Nit: additional blank lines.
If this is a copy & paste from another test we don't need to verify again that the
use_implicitflag works correctly.We should also assert the requested scopes.
Also, we will need a test for each scenario:
a) The user did not accept before.
b) The user accepted before for a more limited set of scopes.
c) The user accepted before for a broader set of scopes.
Comment #27
alan_blake commentedComment #28
skyredwangAs far as I know, @alan_blake has addressed above suggestions in her new patch in #27. She is also working on her communication, and in the future she would comment on the suggestions one by one for more transparency.
Comment #29
e0ipsoThis is really close. Some extra review considerations.
Worth adding a code comment explaining what the condition is actually testing.
If this is false, then the controller logic will not return. We must ensure that all execution paths return.
We could explore removing the last
else.What we need instead is to find out if the current user is in possession of any active token. Then we should check that any of the retrieved tokens has all the
$scopes.That way we cover the case where there is a token with
$scopesand more.Comment #30
alan_blake commentedFix #29 1&2.
#29.3 Why we need to check the token is active? If client access token expiration time is short, the token soon will be inactive, and then user must grant again in a short time. That's not we want. I think we should only check the user grants the same or broader scopes on the same client before and the token is not revoked.
Comment #31
alan_blake commentedComment #33
alan_blake commentedFix tests errors.
Comment #34
alan_blake commentedComment #35
e0ipsoI'll be focusing my attention here soon. Sorry for the delay.
Comment #36
e0ipsoI made some changes to the patch to handle errors and make the test DRY.
There are still some small standing issues with the main logic from previous comments not addressed with regards to scopes.
This limits the impact of session hijacking. If an attacker rides you session then they can get a long lived token to impersonate you even if the session expires. That impersonation in the future will be almost undetectable. That's why we want to limit this behavior to if the token is active.
I also added a slightly more intelligent reduced scope in the tests, instead of just dropping the scopes key to make sure
wasAccessConfirmedis performing the right task:Comment #37
berdirI'm still not convinced that this is a reliable approach.
access_tokens are usually (very) short-lived. And once they expired, they will get deleted.
Doesn't that mean that this whole logic will only work as long as there is an active access token?
That doesn't seem very useful, because it's unlikely that a user will access the page again *while* he is still has an active access token.
Maybe we could use refresh tokens instead, but they seem to be missing any reference to user/client/scopes based on what I see on the Tokens page.
Comment #38
e0ipsoThat's a good point.
I was thinking that the feature request was mainly for cross-consumer authorization when they are not 3rd party. The second part I considered more of a nice to have. Should we just drop this second part?
On a side note, after another quick look at the code we only want to call
wasAccessConfirmedif necessary. Move the call inside theifstatement. That is if we keep the feature.Comment #39
berdirYes, I suggested in #24 to move that to a separate issue. I don't think it's a nice to have, I think there are more use cases that need that feature than 1st-party clients. But I think it is better to split it up and focus on that in a separate issue.
Because I'm not sure if should implicitly, always do that. Maybe it should be configurable and probably users should be see what clients they gave access and be able to remove it, or maybe even make the remembering an opt-in feature with a checkbox. We don't have to support all that initially, but at least think about it..
It is something we need for our project, so I will most likely be able to work in it in the next 1-2 weeks.
Comment #40
skyredwangHere is a user story that the "nice to have" can improve the UX:
If I log in on feedly.com, using Google SSO, then decides to logout on feedly.com. Because feedly.com is a 3rd party, the sign-out function would only sign out the user on feedly.com *.google.com. Then, if I sign on feedly.com, which will redirect me to accounts.google.com, but because I still have an active session (stored in Cookies), I shall not see the authorization step, but immediately being redirected back to feedly.com.
Even if I signed out on feedly.com, and accounts.google.com, the next sign-on on feedly.com shall not see the authorization form again. I think #30 is a good attempt, but I am not sure if this solution check the case a user has revoked an access of a 3rd party? maybe @alan_blake knows the answer to my question.
Comment #41
berdirI'm not sure what you are arguing for exactly, but I fully agree that we want to have some version of what you are describing. But the patch right now doesn't work like that.
With oauth2, the access token that is being checked here is only valid for a *very* short time period. Could be 1 minute, maybe 5 but in most cases, that's it. It is *not* tied to the session that you have with the Drupal site in any way. Once it expired, the client can use the refresh token and get a new access token that is again valid for a few minutes.
And when the Drupal site (aka Google SSO in your scenario) runs cron, which it for example might do every hour, then it will delete that access cookie once it expired. So if the user logs out after a few minutes and then wants to log in again 1h later, it will *not* remember him with the current implementation.
And with Google, it is actually not tied to the session, Google SSO has a persistent list of connected sites, as long as I don't go there and remove it, logging in again on some other site will auto-accept it again, even if I have a new session on Google. This is basically what I'm proposing. But my suggestion is to do that implementation (or a first step toward something as fancy and complete as Google SSO) in a separate issue. One feature at a time.
Comment #42
skyredwangThis suggestion works for me, as I said:
in #10
Comment #43
e0ipsoThat sounds like consensus to my ears!
Anyone willing to create the follow up issue, so we can start trimming the patch in this issue?
Comment #44
e0ipsoComment #45
alan_blake commentedSo I remove the valid $accessPreviouslyConfirmed part. This patch skip the user authorization step only if the client is not third party.
Comment #46
berdirStarted doing manual testing and it basically works, but I think we have a bit of a problem with how the third party field works that was added to the consumer entity.
It was added as a new field, with default value FALSE. That means all existing clients are now *not* third-party clients but are trusted. Combined with this patch, just by updating those two modules, existing sites will suddenly automatically grant access to all existing clients until they edit them again.
I think the default value for the third party setting field should be TRUE as that is the more secure choice if you're not sure what you want and is then consistent with not having the field, where every client was assumed to by third party. The UI could also do a better job at explaining the implications of being/not being third party. Which is of course tricky as the field is now defined in the consume entity.
Also, the patch should have added an update function to install the new field definition, stable modules should *not* rely on users having to run drush entup themself, not everyone can (granted, most people using these modules can, but it should still not be required and should not be part of automated deploy processes).
Not quite sure how to move forward, but I would suggest a new issue in the consumer project to make those changes. But lets wait for feedback from @e0ipso.
Comment #47
berdirI created #2948267: Allow users to remember connected clients.
To be clear, I'm happy to work on the changes that I proposed above, just would like to have an OK from @e0ipso before I start.
Comment #48
alan_blake commented@Berdir I agree, and that's why I set default value TRUE on my patch.
Comment #49
berdirOh, and @e0ipso changed it on commit? That doesn't seem like a "very minor change", weird.
And in regards to the upgrade path, I see, you did add one, but that is not quite correct. When working with entity fields, you should be using the entity update manager to make schema changes. You added the field (without explicitly setting existing clients to TRUE) to the schema, but the entity system didn't know about that.
See block_content_update_8003() for an example that creates new fields on an existing entity type. And starting with 8.4, it is also possible to provide an explicit value, even depending on another field. See block_content_update_8400() for an example for that (that only exists in 8.5.x despite its name)
Comment #50
e0ipso@Berdir the reasoning is that (at least at the moment) you need
administer consumer entitiesto create any Consumer. I consider administering an entity type a high clearance level. If you have that permission it implies:In addition to that
void(before adding the field) andFALSE(after adding the field) are both falsey, making the transition smoother (hopefully). Finally, first party consumers is the most common use case (by far) in decoupled Drupal according to my observations.I want to be clear on this: I'm not married to that decision. If we collectively think that the reasoning above is incorrect, or there are more important reasons on the other side of the scale, I'm open to change. Should we change it?
I tend to do that. Some times it remember to post a
commit-interdiff.txt, but in all honesty that's not the majority of the cases. As you well know maintaining a module can be a burden, I try to be practical and alleviate some chores wherever I can. Hopefully the commit is enough to trace back the changes that happened.You are absolutely right. I just didn't know any better.
entuphas never been a problem in my setups so I never bothered with a different way of doing it. How would you suggest to do it?Comment #51
berdir> In addition to that void (before adding the field) and FALSE (after adding the field) are both falsey, making the transition smoother (hopefully).
They are both falsey as a value, but the meaning is not the same.
Before this patch, everything is third party and the user needs to confirm the client. With the patch, false means *not* third party and is automatically accepted.
It seems wrong to me that this automatically happens by just updating the module. So for BC, IMHO void is the same as having it set to TRUE.
I have no strong opinion on the default value for *new* consumers, but I really think the update function should explicitly set it to TRUE for existing entities.
> How would you suggest to do it?
I would suggest to add an explicit update function similar to the examples above that sets the initial value to TRUE. Problem is that sites that already updated now already have the schema, so it won't change that.
Considering that, we should possibly even explicitly again set all entities to TRUE, maybe with message to notify users about this on update. It would reset sites that already explicitly created them as first-party but that should be OK as nothing really relies on that yet until this issue is actually committed? (except they do in custom code..)
Comment #52
skyredwangJust for the record, I was aware that @e0ipso's commit wasn't exact same as the proposed patch. I didn't have strong opinion, and I still don't have strong opinion, because this module is for site-builder(s) and security conscious admin(s) who understand oauth2. Before this patch, I would image all use cases are for 1st party (otherwise, this issue, discussion and patch would have happened earlier). Leaving the default as it is might be OK.
I don't know about the entity updates yet.
Comment #53
berdirActually found an existing issue, provided a patch there: #2938932: Intall 'Is this consumer 3rd party?' on update
> Before this patch, I would image all use cases are for 1st party (otherwise, this issue, discussion and patch would have happened earlier)
I don't understand this.
Before this issue, every user was asked for every client if he wants to allow access. With the logic in this patch, that means that client is considered third-party. So to keep showing that form to users, they must be marked as third-party clients, and then site admins can decide to make them 1st party if they want to.
That's what my patch in the other issue is now doing, but it will only work for those who didn't update to beta2 yet, I guess it's not worth doing it explicitly for those too, it is just a beta version (but a dependency of a stable module, so technically, it should be considered stable).
Comment #54
berdirI think combined with my patch from the issue above, this is fine.
Comment #55
e0ipsoThanks all!
Comment #59
e0ipsoSorry for not explaining before. Every time I (as a maintainer) require multiple changes and tests to a contributor and they keep up with the work, I grant several (empty) commits for the credit. I hope this encourages you to keep the great work @alan_blake!