Per today's SA, http://drupal.org/node/1890222, Cookie-based authentication for REST POST requests is insecure, particularly if you're running "certain browser plugins". (Need we guess which?) That can be worked around with a token, and we'll have to add support for that to Drupal 8 as well.
However, cookie-based authentication is not an appropriate approach for REST in the first place. It's not really "RESTful" because it's stateful, and it's really designed for browsers, not for web services. Instead, there are two more appropriate methods for doing user authentication for REST:
1) Http Basic Auth: Baked into the HTTP Spec, very simple.
2) OAuth: A big complex mess with less standardization than you might think.
OAuth is not going to make it into core for Drupal 8, and that's fine. It can live in contrib. However, we really should try to get Http Auth in so that we can do "proper" authenticated REST requests out of the box. It's very likely that we can steal code from http://drupal.org/project/services_basic_auth to save time.
The tricky part is that we don't have an authentication API right now. We have a session-login API (aka, a cookie-based one) that sets a global. This may necessitate cleaning that up so that we have an actual API to work with.
Also no doubt related: #1858196: [meta] Leverage Symfony Session components
I think this definitely qualifies as a feature, but because of the security implications I am marking it major. Also it probably should not go in REST module directly, but that seems the closest component for the time being.
Comment | File | Size | Author |
---|---|---|---|
#156 | 1890878.eighth-bootstrap-phase.patch | 407 bytes | rszrama |
#153 | drupal-1890878-153.patch | 41.68 KB | corvus_ch |
#149 | 1890878-149.patch | 41.65 KB | fubhy |
#149 | interdiff.txt | 6.11 KB | fubhy |
#144 | 1890878-144.patch | 41.47 KB | fubhy |
Comments
Comment #1
moshe weitzman CreditAttribution: moshe weitzman commentedrestws also has code we can borrow here.
this is a few lines of code. i really question the wisdom of making it depend on the new Session stuff. sometimes it feels like we are fishing for things to depend on.
Comment #2
moshe weitzman CreditAttribution: moshe weitzman commentedComment #3
moshe weitzman CreditAttribution: moshe weitzman commentedComment #4
berenddeboer CreditAttribution: berenddeboer commentedFirst thing working perfectly: you get a 403 response when not authorised :-)
So that REST part is handled.
Comment #5
berenddeboer CreditAttribution: berenddeboer commentedFirst step then will be to return a 401 to indicate that authentication will be required.
Comment #6
berenddeboer CreditAttribution: berenddeboer commentedThis is what you expect to see for a 401 response:
Comment #7
berenddeboer CreditAttribution: berenddeboer commentedWhen trying to figure out if I could override the REST stuff to have a 401 just on the REST stuff, my interpretation of this is that this was not possible.
In core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php it seems permissions are set in _permissions, but this is simply a Drupal permission a user has or not.
So we need to return a 401 at the Drupal core level itself, so obviously that has more impact than just a REST services patch.
Comment #8
berenddeboer CreditAttribution: berenddeboer commentedCrell suggested we don't do it global, but try to keep it to REST only for now.
Comment #9
berenddeboer CreditAttribution: berenddeboer commentedAnd after some discussion, this is the consensus on how to approach this:
What we therefore will do is catch the 403 exception handler, and return a 401 there, if and only if someone is not logged in.
Crell on IRC:
Comment #10
berenddeboer CreditAttribution: berenddeboer commentedFinally back to this: forgot actually that digest authentication is not easy to do with Drupal, as the password is never transmitted with digest authentication.
To make such things work you would need to store the md5 digest of the username, realm (site name), and plain text password in the database. Yes, that means if you change the site name, all your passwords become invalid...
So we can only do basic authentication. But I think it will be a good idea to disallow basic authentication unless https is enabled as the password goes in plain text over the wire (just like your good old Drupal login). So perhaps we should like to let the user tick a box to enable http?
On the other hand, ssl encryption isn't as good as you think it is, as a man-in-the-middle attack is much easier than most people realise.
So working on basic authentication right now. It's better than cookies, but just as problemetic from a security point of view.
Comment #11
berenddeboer CreditAttribution: berenddeboer commentedI've got a primitive basic authentication patch ready, proof of concept. However, got stuck when running this with the latest Drupal 8 + new entity patch #1818556: Convert nodes to the new Entity Field API. So waiting for that be resolved.
Comment #12
berenddeboer CreditAttribution: berenddeboer commentedHere is a concept patch. It's really basic, so should be quick to understand and review.
What I'm looking for is a reviewer who can give me some guidance on how to proceed and make this a patch that can be accepted:
All suggestions most appreciated!
Comment #13
berenddeboer CreditAttribution: berenddeboer commentedI forget: this patch is on top of the entity-ng patch #1818556: Convert nodes to the new Entity Field API, you need that first.
Comment #14
berenddeboer CreditAttribution: berenddeboer commentedFurthermore, on how to do digest authentication. I've come up with 2 approaches:
1. We add a realm config value to REST services, and add a table with tuples (uid, digest hash). See below for the digest hash calculation.
Advantage: new users, and when you update your password, can use http digest authentication.
Disadvantages: when enabled to an existing site, existing users cannot use digest authentication until they have updated their password. So we need a list showing what users do not have a password for authentication yet. Also when you change the realm, all passwords become invalid.
The digest hash is calculated as follows: md5 (username, ':', realm, ':', plain text password)
Or from the command-line:
echo -n 'myname:My Realm:password' | md5sum
Obviously md5 passwords can be calculated quickly. So another disadvantage could be that if your database is stolen, the passwords could be calculated (which you can do now too, but perhaps slightly slower). If a hacker has access to your db, you probably have bigger worries.
The second method gives every user immediate access, if you ask them not to use their password, but to use their hassed pass or a variant hereof.
I.e. when using digest authentication you do not give your plain text password to the browser, but the "pass" value as stored in Drupal or an md5 version or such thereof. I.e. to login with wget you do:
wget --user admin --password '$S$EZLEJ0YCPdMUb7lFtpGfI/2AxsPqJ/oBk7FoxKByh06cLW.2Xmuc' http://localhost/entity/node/1
The disadvantage is obviously that this is not intuitive, and you have to explain that to people.
PS: the reason we need to make this more complex is that the password is never sent over the wire, so we need to use a "password" that is known at both ends. So either we precalculate (first option) when we know the plain text password, or we use the hashed password (or the hash of that as the password).
Comment #16
Crell CreditAttribution: Crell commentedDoes digest mandate md5? Because md5() in Drupal core is verbotten at this point since it runs afoul of government security standards. (Basically the US Govt requires any use of something as insecure as md5 to be justified before they will install software that uses it; Drupal's answer has been to just kill use of md5 entirely.)
No space between get and (.
We definitely need some way to flag a route as supporting HTTP auth, since presumably most sites won't want to do this on all pages. I think the request is available to us in the Exception Controller, or can be, so we should be able to retrieve the route and check its options array.
Comment #17
kim.pepperAccording to this apache page, it only allows one format for digest authentication, md5.
http://httpd.apache.org/docs/2.2/misc/password_encryptions.html#digest
But I don;t see anywhere in the spec that says it must be md5.
http://tools.ietf.org/html/rfc2617#section-4.13
Comment #18
berenddeboer CreditAttribution: berenddeboer commentedkim.pepper, there is an algorithm parameter, but most clients only support md5/md5-sess/ntlm.
Comment #19
berenddeboer CreditAttribution: berenddeboer commentedCrell> Does digest mandate md5?
See previous reply.
Crell> No space between get and (.
Yes, will clean up for final. I'm just used to write code like writing text. Will compress and densify it when done :-)
Crell> We definitely need some way to flag a route as supporting HTTP auth
Working on that.
Comment #20
berenddeboer CreditAttribution: berenddeboer commentedWe have the request, but it does not contain what route objected as far as I can see. Debugging.
Comment #21
Crell CreditAttribution: Crell commentedThe route object should be on the request at $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT);
At least I think that's the right spot. It's something like that. :-)
Comment #22
scor CreditAttribution: scor commentedNot meaning to derail this issue, but has anyone looked at HMAC, which can be used in combination with md5 or the more robust sha1? see http://rc3.org/2011/12/02/using-hmac-to-authenticate-web-service-requests/
Edit: apparently Facebook uses HMAC (updated link).
Comment #23
moshe weitzman CreditAttribution: moshe weitzman commentedWe discussed this and decided that MD5 is not a problem here. With Symfony, we added back in a bunch of MD5 related code and we want to be compliant with clients.
I have not looked into HMAC.
Comment #24
berenddeboer CreditAttribution: berenddeboer commentedHMAC is not standard, so works only with custom clients. So should be in addition to more widely supported approaches.
Comment #25
berenddeboer CreditAttribution: berenddeboer commentedThis is simply updating patch #12 against the latest greatest. Nothing new.
Comment #26
berenddeboer CreditAttribution: berenddeboer commentedThis always returns NULL in the exception handler.
Comment #27
ygerasimov CreditAttribution: ygerasimov commentedI propose to add an attribute to route object that defines what authentication mechanism should be triggered as we will need to have others as well like oauth etc.
When subscriber sees that this attribute is available it should presave current user, authenticate the according to its rules and after execution of the controller to restore user that was active before authentication. This is how Services in D7 work. This will allow administrator, who is session authenticated to try authentication calls and not get logged off.
Probably we should have authentication plugins to support different types of authentication. Each plugin will have its own key for route attribute.
Comment #28
Crell CreditAttribution: Crell commentedIf I understand you correctly, that's effectively how the Access Check objects work already. They don't use Plugins API, but they don't need to.
Comment #29
ygerasimov CreditAttribution: ygerasimov commentedAfter extensive discussion at codesprint the plan is:
- add listener that will authenticate user with any possible authentication provider. If request has information about authentication for more than one method -- we throw 400 error response (Bad Authentication Request). In case of successful authentication we store information about the way user authenticated in request object.
- we add access check that will ensure that route allows to use authentication provider that user authenticated with. If not we throw 401 Not Authorized.
Comment #30
moshe weitzman CreditAttribution: moshe weitzman commentedThis is looking good. Do you plan to work on a second auth provider?
Ideally we add a test for the second provider. I think the standard login done by all the tests is adequate testing of the cookie provider.
Thanks for working on this.
Comment #31
klausiThis will of course break page caching. Depending on the used authentication mechanisms in a request (cookie headers, HTTP basic auth headers, oauth headers etc.) you need to vary the page cache key or bypass page caching. But page caching is broken anyway right now, I'm not even sure which issue to link here. Examples: #1973206: ViewSubscriber with page caching - Denial of Service (SA-CONTRIB-2013-042), #1855260: Page caching broken by accept header-based routing, #1597696: Consider whether HttpCache offers any significant benefit over the existing page cache
Comment #32
atchijov CreditAttribution: atchijov commentedSorry for silly question, but does page caching even applicable to REST? I can see some situations when we do want to cache REST requests, but it is really difficult to control(decide) what to cache and what not to cache in case of REST, so my assumption would be that Drupal is not going to cache any of REST request.
Comment #33
bshaffer CreditAttribution: bshaffer commented@atchijov - Caching can be respected using the
Cache-Control
header. This allows the client to call the APIs with various levels of caching, i.e. "no-cache" to bypass the cache, "max-age" to prevent stale cache, "No-store" to prevent writing to the cache, etc. - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9Although exactly what Drupal should cache is outside of my knowledge, but I imagine some things can and should be cached for REST requests
Comment #34
Crell CreditAttribution: Crell commentedWe're working on switching page caching as we have it now over to proper HTTP header based caching. We absolutely should be caching RESTful GET requests, just like we do HTML requests.
That said, let's not worry about caching here for now. There's 8 other issues dealing with that already. :-)
Moshe: Yes, the idea is that we can then easily add an HttpAuth-based auth provider next to the cookie one, and/or whatever else anyone feels like adding.
Needs docblock.
Needs {@inheritdoc}
This can be collapsed to $auth_providers = $route->getAttribute('auth') ?: array('cookie');
This interface is not needed. It's redundant with the provider interface. The Manager class should use the exact same interface as the providers, which makes swapping things out much easier.
Needs docblocks.
We need a priority marker for these services. See breadcrumbs and path processors for easy to copy examples.
This should be injected via the container. Drupal:: should never be called from within a class.
Comment #36
ygerasimov CreditAttribution: ygerasimov commentedFinally have something working (still waiting for all tests to run).
This patch still needs:
- comments in the code
- I do not like authentications providers have to register in container with 'authentication.XXX' key. But I do not have idea how to change this.
- I do not like to include sessions.inc in AuthenticationSubscriber::onKernelRequestAuthenticate because of FinishResponseSubscriber. This opens session (sets a cookies) even in case we do a call with basic authentication only. Can we remove drupal_session_commit() from FinishResponseSubscriber? Or maybe check if authentication is done with 'cookie' authentication provider and only then run drupal_session_commit()?
Comment #38
ygerasimov CreditAttribution: ygerasimov commentedReroll of the patch.
Comment #39
juampynr CreditAttribution: juampynr commentedWhoa! I just read the issue and I have to admit I understood little of it. For what I understand, we are building a system that lets other authentication systems such as OAuth to subscribe to an Authentication manager, right? Therefore, we could do two-legged authentication for REST endpoints as we currently do with Services + OAuth in Drupal 7.
Since I started maintaining the OAuth I have just done support. I do not know the OAuth protocol in detail, but I could port the module to Drupal 8 so it uses this system. Thoughts?
By the way, the OAuth module uses OAuth1. @bojanz (pinged to chime in here through personal contact form) created recently an OAuth2 Server module.
Comment #41
ygerasimov CreditAttribution: ygerasimov commented#38: core-1890878-authentication-providers-38.patch queued for re-testing.
Comment #43
ygerasimov CreditAttribution: ygerasimov commentedMove drupal_commit_session() from FinishResponseSubscriber to AuthenticationSubscriber listener (running through cleanup() method of authentication provider). Lets see how tests will pass.
Also I have done changes in simpletest/TestBase.php to avoid calling drupal_save_session() if this function does not exist.
Comment #44
ygerasimov CreditAttribution: ygerasimov commentedComment #45
Crell CreditAttribution: Crell commentedI think most of my comments in #34 are still outstanding.
Don't make this class ContainerAware. Just inject the authentication service via the constructor and be done with it. Also, don't use settings() in here. That's also an injectable service.
Comment #46
Damien Tournoud CreditAttribution: Damien Tournoud commentedI never really understood what those special variables are actually for... but I'm pretty sure we should read
->headers->get('Authorization')
directly instead.Comment #47
Damien Tournoud CreditAttribution: Damien Tournoud commentedAlso, wouldn't it be waaaay simpler to use the Symfony Security component instead of trying to come up with our own, broken, security framework?
Comment #48
Crell CreditAttribution: Crell commentedThe Symfony security component is a large and complex beast. The ripple effects of trying to integrate that are huge at this stage of the dev cycle; we haven't even managed to get Symfony sessions in properly yet. :-(
Comment #49
Damien Tournoud CreditAttribution: Damien Tournoud commentedJust browsing through the component, I do not actually think that's true. The Firewall part of the component seems fairly self-supporting and is basically what this patch is doing.
We can and should start small. The only thing we want to leverage right now is the
Firewall
class (which is basically whatAuthenticationManager
in this patch does), and the basic authentication tools (BasicAuthenticationEntryPoint
,BasicAuthenticationListener
).In turn, we are going to need a
SecurityContextInterface
: we can reuse the defaultSecurityContext
implementation, but that requires aAuthenticationManagerInterface
and aAccessDecisionManagerInterface
.For the
AuthenticationManagerInterface
, we can just go with a Drupal-specific implementation for now, and hardcode it to check the user database.For the
AccessDecisionManagerInterface
, we can just go with a Drupal-specific check of permissions (ie. wrapuser_access()
).No need to touch anything else in the
Security
component for now. We are going to need aFirewallMap
to initialize theFirewall
, but I assume that's just glue code to the router.Then, once we have this basic infrastructure in place, we can move on to adopt more elements of the
Security
framework.Comment #50
Damien Tournoud CreditAttribution: Damien Tournoud commentedJust clarifying: Digest auth is not on the table. Digest was designed in 1997/1999, at a time where it was though as a good idea to store the plaintext password (or a MD5 of username - realm - password) on the server side. There are other advanced authentication schemes (Negotiate, SASL, etc.), but nothing that is broadly adopted and has value against just using OAuth.
Comment #51
ygerasimov CreditAttribution: ygerasimov commentedRerolled patch plus:
- added code comments
- removed AuthenticationManagerInterface in favour of AuthenticationProviderInterface
- changed from using $request->server->get('PHP_AUTH_USER') to $request->headers->get('PHP_AUTH_USER') like implemented in Security component
I personally do not think we are able to adapt Security component at the moment as it needs more efforts and we have limited time before code freeze.
Comment #52
kim.pepperHi ygerasimov,
Looking good.
I haven't followed the thread closely, so I'm not sure why we are removing DRUPAL_BOOTSTRAP_SESSION as part of this patch?
These are some minor codestyle/docs issues I picked up:
Needs newline at end of file.
Needs newline at end of file.
Extra blank lines should be removed.
Needs docs.
Needs docs.
Needs docs.
Needs newline at end of file.
Needs docs.
Needs docs.
Needs newline at end of file.
Comment #53
ygerasimov CreditAttribution: ygerasimov commentedKim, thank you very much for the review.
I have fixed these issues plus now less tests should fail.
Comment #55
ygerasimov CreditAttribution: ygerasimov commentedReroll of the patch.
Comment #57
ygerasimov CreditAttribution: ygerasimov commentedRerolling the patch. Checking tests.
Comment #59
berenddeboer CreditAttribution: berenddeboer commentedNot sure I'm getting you. You don't want to support any HTTP authentication method?
OAuth is barely supported, compared to the enormous range of http clients that support http authentication. Digest is infinitely more secure than Drupal sending plain text passwords over the wire, but it's complicated to get this in.
But having basic authentication over ssl is definitely something one must support to reach a wide audience.
Comment #60
Damien Tournoud CreditAttribution: Damien Tournoud commented@berenddeboer: please re-read. "Digest" authentication cannot be used, because it forces us to store the password (or a digest of it) on the server side. "Basic" is fine. The most future-proof solution is OAuth.
Comment #61
Crell CreditAttribution: Crell commentedHTTP Basic (over SSL) in core, OAuth in contrib. That's the plan, as I understand it.
Comment #62
ygerasimov CreditAttribution: ygerasimov commentedPatch reroll. Trying to fix tests.
Comment #64
ygerasimov CreditAttribution: ygerasimov commentedAnother reroll. Add initializing of session during install.
Comment #66
corvus_ch CreditAttribution: corvus_ch commentedAccessCheckInterface::access()
Is expected to returnNULL
if the implementation is not responsible. Changing the implementation ofAuthenticationProviderAccessCheck
seems to fix most of the tests. The errors I have seen so far are related to language negotiation.By my opinion just wrapping the session handling with the cookie class , will give us a lot of headache. The session handling should be more decoupled. Sadly I do not have an idea how. Note that there is an issue trying to make use of symphony for the authentication (#1858196: [meta] Leverage Symfony Session components).
And this is list of some more issues that might interfere with what is happening here.
Comment #67
corvus_ch CreditAttribution: corvus_ch commentedTriggering testbot.
Comment #69
Crell CreditAttribution: Crell commentedUnfortunately #1858196: [meta] Leverage Symfony Session components keeps running into problems with simpletest. We've been trying on and off for months. We can't wait for it.
Comment #70
ygerasimov CreditAttribution: ygerasimov commentedWe are getting closer with simpletest! Another round.
Comment #71
corvus_ch CreditAttribution: corvus_ch commentedJoggling around some event subscriber and dealing with an edge case in autorize.php striped down most of the remaining test failures. Let's see how much is remaining.
Comment #72
corvus_ch CreditAttribution: corvus_ch commentedOh, sorry. Did not see the previous patch.
Comment #73
corvus_ch CreditAttribution: corvus_ch commentedClaim that for now.
Comment #75
corvus_ch CreditAttribution: corvus_ch commentedThat one should give it the rest ;)
Comment #76
Crell CreditAttribution: Crell commentedThis is looking great, guys! Comments below, most of them are nitpicky.
Bad grammar. I suggest "Ensure the fallback provider always fires last."
That said, I think we could do better. Just unset the fallback provider... Then add it again at the end of the array. That will eliminate the second redundant code block below.
Why not just break; from the foreach loop on the first provider we find? Do we need to keep going to force only one to work?
Grammar: "Save the ID of the triggered provider to the request so that it can be accessed in AuthenticationProviderAccessCheck."
Technically not relevant here, since one COULD call cleanup at any time. The comment is a tight coupling. :-)
I'm pretty sure we can inject the user entity manager and do a proper injected load here now. No need to call user_load() directly. (I don't know if we can inject user_authenticate yet...)
Do we need a priority marker here? We have it almost everywhere else that we have this pattern.
I think our standard pattern here is "Account name is displayed". Vis, describe the success condition as if it happened.
First line is longer than 80 chars. The first line should be what this method does, not why we use it instead of something else. What's currently the first line should be a longdesc paragraph after the first lien.
Missing $path param.
The main thing that is missing that I see is how to force authentication with HTTP Basic. I am no expert on it, but my understanding is that in case of a Basic Auth failure an HTTP 401 should be returned so the browser knows to prompt the user for credentials (or the non-browser client application knows that it sent bad credentials): http://httpstatus.es/401
How are we going to trigger that?
Comment #77
corvus_ch CreditAttribution: corvus_ch commentedComment #78
corvus_ch CreditAttribution: corvus_ch commentedThis patch fixed the coding styles mentioned in the comment above.
I thought the same but I do not have any real arguments for one or the other solution. My gut tells my to do a break and forget about the double credentials.
Currently if no provider feels responsible the global user is populated with an anonymous account. This will give us a 404. We probably can trigger a 401 respons instead and skip any further page processing (how can that be done by now?).
Comment #79
BerdirNo, it does not. It continues with the process and then autorization decides if the anon user has access to that route.
What @Crell meant is that in case of having authentication information but it being wrong, it should abort, even if the anon user would have access to that page. Not sure if we can already throw an AccessDeniedException at this point?
Comment #81
corvus_ch CreditAttribution: corvus_ch commentedHandle the least prioritized provider as fallback and make sure its clean up gets triggered.
Comment #82
corvus_ch CreditAttribution: corvus_ch commentedThat was what I meant.
Comment #83
klausiAmazing work, I like what I see here! This seems to be almost ready, so I'm starting the nitpicks:
missing doc block for the class. Also elsewhere, please check all your classes.
@param and @return description must not be empty. Better use @inheritdoc here and add the comment under it. Also elsewhere.
2 white spaces before "Priority".
if one provider successfully authenticated we should break the loop, right? There is no point in asking the other providers then?
Why and what does a provider have to cleanup? Is this a common pattern for authentication providers? To me it looks like session cookie authentication is messy and needs this ... but anyone else?
What cleanup? That comment should give an example and refer to the session cookie cleanup mess. I find it ugly that we have this method on the interface, can we avoid it?
I really think the cookie auth provider should cleanup on its own, maybe by registering as subscriber to a response kernel event and invoking drupal_session_commit() there? That way we could avoid the cleanup() method on the interface.
Yep, this should go to the session cookie provider, or is there a compelling reason other authentication providers want to cleanup?
You should also assert the HTTP response status codes here.
Comment #84
Crell CreditAttribution: Crell commentedThis seems weird. See the PathProcessors for the boilerplate code we're using in a half-dozen places now. We don't need to sort until we actually use it. Sorting every time a provider is added is needlessly slow.
The cleanup() method on providers is something we added because cookie needs it; I don't think it's that ugly, really. I don't know what other methods would need it, but it seemed better to make it an easy no-op than to try and do something weird and hacky for cookie. (Especially as cookie is what we'll be using 98% of the time anyway.)
Comment #85
klausiOk, the ugliness of cleanup() should not hold up this issue. Please document it properly on the interface with a reference to the session cookie implementation. I would argue that we should have an abstract base class that implements an empty cleanup(), but that is also a minor detail and not blocking this effort.
Comment #86
Crell CreditAttribution: Crell commentedBerdir: Actually no, I meant sending a 401 rather than 403. :-) Because if you're using basic auth, AFAIK "you can't access this, what's your password?" is the standard logic. We should try to support that.
Comment #87
Crell CreditAttribution: Crell commentedOK, we discussed this a bit in IRC. Sounds like we're still missing a piece here. Here's the suggested way forward:
* We need to differentiate 401 (Who are you?) from 403 (Oh, you, I hate you). Specifically, that should be done here:
http://drupalcode.org/project/drupal.git/blob/refs/heads/8.x:/core/lib/D...
That needs to be tweaked to throw a 403 exception if the user is authenticated, and a 401 exception if the user is not authenticated. (Right now we're just throwing the 403; I'm not sure if Symfony already has an exception for 401. Probably?)
* We then need an exception listener (which could maybe live in that same subscriber?) that only catches the 401 exception and prompts the user for credentials. To do so, it calls a method on each provider that is relevant to that route (just as we're already doing for auth), and lets the provider return an appropriate response. In the case of http basic, that would be a 401 response with the WWW-Authenticate header. For cookie... it probably wouldn't do anything, at least for right now. :-)
* If it's a 403, we already handle that based on mime type so ignore it for now.
So that should be just one more method on the Providers that gets called in the case of "user unknown" and returns an appropriate 401 challenge-response. Not sure what to call it right now, but that's the basic idea.
Comment #88
corvus_ch CreditAttribution: corvus_ch commentedAs a starter, here comes a lot of documentation work.
As of the 401/403, I will have a look at it once I have talked with @Berdir tomorrow in the office.
I also feel the need to do more tests. Suggestions on how to improve them are welcome.
Comment #89
ygerasimov CreditAttribution: ygerasimov commentedRegarding 401, in case user is not authenticated we can run through all providers and run some method on them. After first provider that returns not NULL (means that provider *did* something, in case of basic authentication it sets up 401) we stop.
So in case of basic authentication has higher priority than any others we will get 401 with a challenge.
Comment #90
corvus_ch CreditAttribution: corvus_ch commentedThrowing exceptions is fun. Now authentication returns 401 and 403 as expected.
I am unsure what the
WWW-Authenticate
header should look like nor if this is actually the way to go. I have not done anything with the test for I wanted to have some feedback first.Comment #91
ygerasimov CreditAttribution: ygerasimov commentedNo. I do not think patch #90 is the way to go. We need to set 401 and proper headers with realm only in case if user get access denied and user is anonymous and basic auth is enabled for this call.
I will work on this patch tomorrow.
Comment #92
ygerasimov CreditAttribution: ygerasimov commentedHere is patch that makes 401 in case basic authentication is enabled for the route and access denied exception thrown.
The problem that still exists is that I haven't found the way to set
WWW-Authenticate
header. Maybe it needs some fresh look.Comment #94
corvus_ch CreditAttribution: corvus_ch commentedAdded a redirect to the login form int the cookie exception handler and improved the http basic chalenge as well and som coding style improvements.
Besides that I am stuck here too.
In
AuthenticationManager::handleException()
, if I remove the checks for the route, I get the 401 with the expected chalenge. I also manage to get the redirect to the user form for cookie.The problem is that the route is not set by the time the exception handling happens and I have not found any other means to get a route object.
I tried to set a rout variable on the
AuthenticationManager
during access check inAuthenticationProviderAccessCheck
but for some reason the execution does not com that far.Comment #96
BerdirShould be lowercase false/null I think.
As discussed, I don't really like an API that can return an object, false or NULL from the same method. That feels like its doing too much.
Maybe there should be, similar to access checks, a separate method for a provider today if he applies for that request that can return true/false and an authenticate method that can return either an AccountInterface and or null if authentication failed?
Not sure about this, that seems like a considerable behavior change? And hardcoding this seems.. questionable. It should at least have a destination redirect back to this url?
Should inject the config factory (@config) into this. Not sure if the fallback is necessary, I think we usually assume that the site name is set?
I guess this should use \Drupal\Component\Utility\String::format()?
What should we do about session.inc? What actually prevents us from using the container (or a special container with hardcoded services) in here, update.php and install.php?
We're currently using array for these cases, not [].
Authenticated in Drupal usually means a user/account with the authenticated role, which excludes anonymous users, but they still can have a valid session and be... identified?
That also makes the whole naming here.. a bit problematic.
Missing namespace.
not sure, we usually don't seem to bother to document the return value of a sort callback?
Missing \
Reads strange...
"Cleans up"?
Missing namespace.
\, Same for all other files.
is called, also namespace. Not sure if this is even relevant to be documented here? It's called with a request, it doesn't need to know when?
namespace.
This seems unnecessary? We either have to assume that by now, this has been loaded or actually, it should probably become a method on the authentication manager if we want to assume that we might be running test with basic auth :)
Does.
Should be lowercase false/null I think.
As discussed, I don't really like an API that can return an object, false or NULL from the same method. That feels like one thing too much :)
Maybe there should be, similar to access checks, a separate method for a provider today if he applies for that request that can return true/false and an authenticate method that can return either an AccountInterface and or null if authentication failed?
namespace\class::drupalGet(). Deja-vu :)
In general, this still feels a bit backwards. We first try all providers that we have only to then later on check if the one that responded (if any did), is actually valid for this request?
Comment #97
corvus_ch CreditAttribution: corvus_ch commentedSimilar to other services there is now a
applies()
method on theAuthenticationProviderInterface
.As of the login redirect. We probably can make this configurable. For now I added the destination query.
The configuration is no injected to
HttpBasic
. I hope I did that the right way.I guess it is update and install. I have not touched it so far, because I wanted to have the rest working.
Things will go really bad if session.inc is not loaded there.
That was obvios :p
Comment #98
corvus_ch CreditAttribution: corvus_ch commentedAgreed. Yet i have no Idea how to do this different.
Comment #100
Crell CreditAttribution: Crell commentedWorking on this. A lot of the fails are due to legacy menu items, which don't have a route! (duh) I should have a fix soon but need sleep first.
Comment #101
Crell CreditAttribution: Crell commentedOK, this took a bit longer than I intended but I think it's worth the wait. :-)
I fixed the legacy route problem and cleaned up a lot of other code. However, that very easily lent itself to some additional cleanup. Most notably, rather than saving the authenticated user to just global $user, we also add it to the request as a "session" attribute. Why "session"? Because it's not a user object. It's a UserSession object. Any similarity between that and a user entity is and always has been coincidental. (Yes, calling the global $user has always been a bug.) That means...
... This patch now deprecates global $user And there was so much rejoicing...
I also moved the "were you authenticated by the RIGHT authenticator" check to its own route enhancer, since it's going to fire all the time anyway and what it actually does is not deny access but change the active user.
The upshot of all this is that we now have true separation between Authentication (Who are you?) and Authorization (Do I let you see this?) I have not renamed the "Access" system to "Authorization" to avoid scope creep, but really that's what it is. And both are pluggable.
This also means that Http Digest or OAuth should be way easier to implement from contrib now, too.
Let's see what the bot thinks. Big bucks no wammy!
Comment #102
Crell CreditAttribution: Crell commentedNote: I'm bumping this to critical because allowing alternate authentication mechanisms is a major factor for any sort of decent web service support; also, it's deprecating a global which is a factor for all of the caching stuff going on.
Comment #103
Crell CreditAttribution: Crell commentedAnd I am setting it to needs review, because I'm an idiot and forgot do so in either my earlier comments. *sigh*.
Comment #105
corvus_ch CreditAttribution: corvus_ch commentedThen lets see what I gan do with all those test fails.
Comment #106
corvus_ch CreditAttribution: corvus_ch commentedHaving the cookie provider doing a redirect on access denied was a nobel thought but this brakes most of the test for this returns 200 and not 403. I removed it for now.
Also took care about the warnings.
Comment #108
corvus_ch CreditAttribution: corvus_ch commentedThis should fixes the Basic Auth test.
The interdiff is against #101.
Comment #110
corvus_ch CreditAttribution: corvus_ch commentedThis should also fix the unit tests.
Interdiff still against #101.
Comment #112
corvus_ch CreditAttribution: corvus_ch commented#110: drupal-1890878-110.patch queued for re-testing.
Comment #113
Crell CreditAttribution: Crell commentedThanks, corvus!
Technically we should be using RouteObjectInterface::ROUTE_OBJECT, which is the constant for _route_object. But yes, going to $defaults is better.
Aside from that, I think we're close to done here. Youri, Moshe, anything you want to add and/or RTBC? :-)
Comment #114
ygerasimov CreditAttribution: ygerasimov commentedVery very cool! We are getting really close to RTBC!
I do not quite agree with this. We should let all allowed providers to handle exception and to break once it is handled. For example top priority may be provider that does not handle exception at all (so default behavior will be expected).
This is about my top comment. Here is documentation about what handleException() returns. So in case FALSE is returned we go through other providers.
One semicolon should be removed.
We use these two lines pretty often in different places. Maybe lets have a method in Cookie provider and then using container execute that method. In this way we will have control over initializing session in one place. Maybe some other other place or even simple function will do the job as well.
Comment #115
corvus_ch CreditAttribution: corvus_ch commentedIt wasn't my choice to switch. The
RouteObjectInterface::ROUTE_OBJECT
is just not set at that moment of the execution cycle.Comment #116
corvus_ch CreditAttribution: corvus_ch commentedYou will have a pretty hard time to determine if a provider (e.g basic auth) is to handle the exception or not.
Given we cokkie is the one that is responsible and we ended with an anonymous user. Basic will kick in for there are no basic credentials and the user is anonymous. The other way around, Cookie has no way to determine if it is responsible or not or it is at least pretty hard by the look of the code in session.inc.
Two questions here from my side:
This concernes install.php and update.php. Do we have a fully fledged container there? I fear that we won't be able to do so but that is just a feeling and I have not checked it.
Comment #117
Berdirinstall.php and update.php have containers, install.php just has a manually set up container for early phases and update.php some overrides. So you just need to make sure to manually register the services in install.core.inc I think.
Comment #118
corvus_ch CreditAttribution: corvus_ch commentedReadded provider iteration on error handling.
Comment #120
ygerasimov CreditAttribution: ygerasimov commentedInstead of array(), $auth_providers should be assigned to default provider. And then if user was authenticated with not default provider -- we set him back to anonymous.
Lets add another test. To try to open /admin page while trying to login using basic authentication as administrator. This is to ensure we can't login to legacy route with basic authentication and get 403.
Comment #121
corvus_ch CreditAttribution: corvus_ch commentedLet's give this a try.
Comment #122
ygerasimov CreditAttribution: ygerasimov commentedNo need for "if (!empty($auth_providers)) {" as it always will be non-empty.
User should have 'access administration pages' permission for last check.
Everything else looks good to me. Lets wait for tests to pass.
Comment #123
corvus_ch CreditAttribution: corvus_ch commentedNow for legacy stuff only cookie authentication is allowed.
Comment #125
corvus_ch CreditAttribution: corvus_ch commentedMy bad, wrong condition. Added additional check.
Comment #126
effulgentsia CreditAttribution: effulgentsia commentedThis all looks great. Here's some very minor nits. If a follow up is opened for them (one follow up for all the remaining nits is fine, doesn't need to be one per nit), I'd be fine with this being RTBC'd as-is. I haven't read all the issue comments though, so don't know if there remains any other unaddressed feedback or remaining concerns.
Should we decrement the higher constants accordingly?
This could use a little grammar polish (e.g., singular/plural, his/its, etc.).
s/Add/Adds a/
Should this just be an {@inheritdoc}? If not, then could use a couple spelling and grammar fixes.
Code doesn't "feel".
Code doesn't "feel".
Let's split this into two parts then. First the part that isn't deprecated. Then the part that is. And add a @todo for that part.
Could use a more informative short sentence.
Handles an exception.
Why are we checking $router_item['access']? Should we instead check $router_item itself?
Should we use the ROUTE_OBJECT constant here instead?
Comment #127
Crell CreditAttribution: Crell commentedThanks, Alex! Consider your nits picked. And no, I don't believe there's anything left to do here unless Yuriy or Corvus have anything more to add.
Comment #128
ygerasimov CreditAttribution: ygerasimov commentedComment #129
BerdirNot completely happy with this being called session.
Discussed this with @corvus_ch for a while last week, it doesn't make much sense to have a session thing when you're doing basic authentication and it's actually your user/account.
Wondering how hard it would be at this point to treat the session data and the account object as different things (remove session methods completely from AccountInterface, I added them because it's there right now but that can change) and then this would be account and session would be added separately by the cookie provider.... somehow.
All that might or might not be possible in 8.x. All I'm sying here is, shouldn't this be called 'account'? :)
Comment #130
corvus_ch CreditAttribution: corvus_ch commentedThe naming of this was not introduced by this patch. I tried to change it and a lot of stuff broke. Changing the naming of this should be handled in a follow up.
Comment #131
ParisLiakos CreditAttribution: ParisLiakos commentedi agree that the attribute key should be named account.
Right now it is confusing moreover because the Request object has a getSession() method..which it gives you the session service from request's property..So even if we dont have a session service, lets give this the correct name
Comment #132
corvus_ch CreditAttribution: corvus_ch commentedSession
is now renamed toaccount
.Well I really thought it was already there. :)
Comment #134
Berdir#132: drupal-1890878-132.patch queued for re-testing.
Comment #135
corvus_ch CreditAttribution: corvus_ch commentedBack to RTBC then.
Comment #136
effulgentsia CreditAttribution: effulgentsia commented#132 didn't include the changes from #127. This just applies the #132 interdiff on top of #127. Leaving at RTBC.
Comment #137
Crell CreditAttribution: Crell commentedI can live with "account", but remember that it is *not* a user object! It's a UserSession object, which is NOT a full User object. We've often used "account" to mean "a User object that is not global $user". Let's just hope that's not even more confusing. :-)
But, I'm fine with the RTBC here.
Comment #139
corvus_ch CreditAttribution: corvus_ch commented#136: drupal-1890878-136.patch queued for re-testing.
Comment #140
Crell CreditAttribution: Crell commentedSilly HEAD.
Comment #141
pounardIn Symfony Security component seems to just call that "user" pretty much like Drupal 7 which make some sort of sense. "Account" is a good term too since our user entities are named "User". The name "identity" seems good too (if I remember well I saw this term used in ZF1).
Comment #142
moshe weitzman CreditAttribution: moshe weitzman commentedIdentity works for me.
Comment #143
juampynr CreditAttribution: juampynr commentedI am implementing an OAuthProvider that plugs into the AuthenticationManager at #2027863: Make Oauth module an AuthenticationProvider.
Comment #144
fubhy CreditAttribution: fubhy commentedI know this is RTBC but I couldn't resist.
Even though it turns this into a ContainerAware service I think we should use the same pattern we also use in the AccessManager and not hand references into the addProvider() method directly so that we do not needlessly instantiate all of the providers for every request.
Fixes some other minor issues too.
I find defaultProvider() a bit weird and especially the AuthenticationManager::__construct() is a bit suspicious with that substr() on the service id. Hmm...
Comment #145
fubhy CreditAttribution: fubhy commentedComment #146
corvus_ch CreditAttribution: corvus_ch commentedThis certainly has some improvements. Thanks.
Comment #147
Crell CreditAttribution: Crell commentedI completely disagree with #144. Services should not be ContainerAware unless absolutely necessary. For Access we do that because we know that there will be potentially dozens of access checkers on a given site, so lazy loading them is worthwhile. That's not the case here. I cannot envision a site that will have more than 2 providers active, frankly. 3 at the outside. Certainly not dozens. And even then... we iterate through all of them anyway so there would not possibly be a performance benefit. As such, ContainerAware is unnecessary and superfluous.
#136 is fine as is and still RTBC. The changes from #144 are not OK.
Comment #148
fubhy CreditAttribution: fubhy commentedOkay, agreed...
The other changes are still valid though (code style/documentation, missing interface, incorrect naming of variables in #136).
Will post a re-roll without ContainerAware asap.
Comment #149
fubhy CreditAttribution: fubhy commentedThis is probably better than. Still not too happy with defaultProviderId(). In fact i'd prefer it if we could live without such a thing and through that also make the ManagerInterface obsolute. But if we have it (public) and call it from outside we have to have that interface.
Comment #150
Crell CreditAttribution: Crell commentedOK, that's a good improvement. Thanks, fubhy!
Comment #151
Dries CreditAttribution: Dries commentedI reviewed this patch and it looks good. I like the architecture -- pretty easy to follow. However, when it came to committing this patch, it no longer applied because of the changes to CoreBundle.php. I'm afraid this requires a reroll.
Comment #152
corvus_ch CreditAttribution: corvus_ch commentedOn it.
Comment #153
corvus_ch CreditAttribution: corvus_ch commentedRerolled to lates HEAD.
Comment #154
jibranOnce again.
Comment #155
alexpottCommitted aa9c2cc and pushed to 8.x. Thanks!
Comment #156
rszrama CreditAttribution: rszrama commentedRe: patch workflows, should I open a separate issue to change "Eighth" to "Sixth"? : )
Comment #157
Crell CreditAttribution: Crell commentedI don't know, but it's here so let's just fix it. :-)
Comment #158
Dries CreditAttribution: Dries commentedA critical typo! I'm on it. Committed to 8.x. ;-)
Comment #159
larowlanBack to active - needs change notice
Comment #160
ygerasimov CreditAttribution: ygerasimov commentedI have created a change notice https://drupal.org/node/2031999 please correct it for language and add missing pieces. It is only about module authentication system.
Comment #161
corvus_ch CreditAttribution: corvus_ch commentedI changed paragraph on the authentication sequence from
to
And I did some gramar improvements. I am not a native englisch speaker, so those changes might be wrong.
Comment #162
corvus_ch CreditAttribution: corvus_ch commentedI added #2032115: Remove session.inc and move code to the cookie authentication provider as a follow up.
Comment #163
corvus_ch CreditAttribution: corvus_ch commentedI have closed #27972: Make authentication pluggable as a duplicate of this one. For #1157310: base authentication class to make multiple and external authn methods easier to implement I am not sure if it is a duplicate or not.
Comment #164
ygerasimov CreditAttribution: ygerasimov commentedWe need another change notice that global $user is deprecated.
Comment #165
Crell CreditAttribution: Crell commentedAdded: https://drupal.org/node/2032447
Updated: https://drupal.org/node/2017231
This feels so good. :-)
Comment #166
XanoFollow-up: #2032553: The _account attribute on the Request is not available during web tests.
Comment #167
jibranUntagging.