Problem/Motivation
When you want to allow anonymous users to access a REST resource, you need to:
- grant the necessary permissions/node grants to the anonymous user role
- not list any authentication providers for the REST resource's configuration
And then it should work.
However:
// Check that authentication providers are defined.
if (empty($rest_resource_config->getAuthenticationProviders($method))) {
$this->logger->error('At least one authentication provider must be defined for resource @id', array(':id' => $rest_resource_config->id()));
continue;
}
This means that you must specify an authentication provider, otherwise the route won't be created. Then once you have specified e.g. cookie
, or basic_auth
(or both, or more — as long there is any authentication provider listed), the route will be created. If you then set up your permissions correctly, you'll still be able to use the resource as the anonymous user … due to quirks in how the routing system works.
Small detail: note how the error logging there is broken: @id
in the text, but :id
in the parameters. Which means the logging is broken.
Fun fact: this implies that \Drupal\user\Plugin\rest\resource\UserRegistrationResource
which was added in #2291055: REST resources for anonymous users: register cannot be used at all in practice! (See #27 for details.) This alone is sufficient to make this major.
I'm not convinced, because … how do anonymous REST tests work around this then, if it is so broken?
Glad you asked!
Above, we explained how setting the cookie
provider would allow anonymous users to use it. But, alternatively, you must specify the empty string as an authentication provider, i.e.:
supported_auth: ['']
That gets you past the above if-test (because the array is not empty: empty(['']) === FALSE
), and hence the route will be created, and it just happens to be that Symfony also doesn't validate this further… This is how NodeJsonAnonTest
etc are able to work: because \Drupal\Tests\rest\Functional\ResourceTestBase::$auth === FALSE
by default, and \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::provisionEntityResource()
does:
$auth = isset(static::$auth) ? [static::$auth] : [];
This results in $auth = [FALSE]
, which when it is passed to \Drupal\Tests\rest\Functional\ResourceTestBase::provisionResource()
and saved into a RestResourceConfig
entity just happens to be stored as 'authentication' => ['']
… because PHP bool-to-string casting…
(First described in #16.)
Proposed resolution
There are multiple ways to solve this:
- 1. The superficial/minimal solution
- Stop ignoring REST resource routes whose REST resource config entity did not specify any authentication providers (
supported_auth: []
). Therefore, also stop logging an error message. The end! - Rationale: it's valid to not list any authentication providers, because the
cookie
authentication provider is global, and is therefore always allowed, on any and every route, and there is no way to make it not global:user.authentication.cookie: class: Drupal\user\Authentication\Provider\Cookie arguments: ['@session_configuration', '@database'] tags: - { name: authentication_provider, provider_id: 'cookie', priority: 0, global: TRUE }
IOW: this solution is accepting that it's impossible to disallow
cookie
-based authentication and therefore also anonymous access. - Consequence: every REST resource plugin should be doing some level of access checking, relying only on a user being authenticated or not is insufficient. This is already enforced by default, because
\Drupal\rest\Plugin\ResourceBase::permissions()
automatically generates a permissions for every method for every REST resource.
- 2. The explicit solution
- This is what you probably prefer if you think solution 1 is too vague/implicit/misleading, and therefore dangerous.
Therefore this solution does not accept the status quo: it does not accept that a global authentication provider must be global; we must change that to mean
global by default
. What this means is that we add a mechanism to allow routes to effectively opt out from global route providers, and only allow the explicitly allowed authentication providers.Now, changing the routing system is a tall order, and will require extreme scrutiny to not cause BC breaks. A practical alternative could be:
- allow
'anon'
to be specified in the REST Resource config entity'sauthentication
section - this would not be an actual authentication provider, but would be an explicit signal to allow anonymous access
- in
\Drupal\rest\Routing\ResourceRoutes::getRoutesForResourceConfig()
, set_user_is_logged_in: 'TRUE'
unless this'anon'
authentication provider is explicitly mentioned - the end result would be that anonymous access is forbidden unless a REST resource's configuration explicitly allowed it — hence removing the requirement that if you don't want anonymous users to access REST resources, that they have some permission
- and if
'anon'
is the only item listed insupported_auth
, then we'd set_user_is_logged_in: 'FALSE'
, and we would not set_auth
, so that'd we'd effectively fall back to the default authentication providers (i.e.cookie
)
To retain BC (becaus Drupal has always allowed anonymous access to any REST resource due to all of the above), we could simply add
anon
to thesupported_auth
of every REST Resource config entity. That'd guarantee zero functional changes (because zero changes to actual route definitions), but would at least allow things to become clearer in the future.
- allow
- Rationale: Explicitness prevents unpleasant security surprises.
- Consequence: Upgrade path necessary. But we've done that in the past, in #2721595: Simplify REST configuration, so we've got a clear, battle-hardened example to look at.
Remaining tasks
- Decide which solution we want to go with.
- Test coverage
- Update https://www.drupal.org/docs/8/api/authentication-api
User interface changes
None.
API changes
None.
Data model changes
None.
Comment | File | Size | Author |
---|---|---|---|
#57 | 2817737-48.patch | 2.75 KB | kksandr |
#57 | 2817737-47.patch | 2.72 KB | kksandr |
#46 | 2817737-46.patch | 2.72 KB | PQ |
#40 | 2817737-40.patch | 3.18 KB | Wim Leers |
#38 | interdiff-2817737.txt | 594 bytes | dawehner |
Comments
Comment #2
Wim LeersComment #3
Wim LeersComment #4
Wim LeersComment #5
dawehnerEven if not, don't we even in this case fallback to the default authentication, which is cookie by default? So that would be a totally valid configuration?
Comment #6
Grayside CreditAttribution: Grayside at Phase2 for Norwegian Cruise Line commentedConfiguring an auth provider for anonymous is an explicit requirement that could be handled via a sensible default.
Right now, the problem is that Drupal will yell if your anonymous resource is insufficiently configured. At least it "yells". Removing this, and setting up authenticated resources will have an unmet, implicit dependency that will be confusing to troubleshoot. I don't think this should go forward as-is without a planned follow-up to make authenticated access to REST resources less touchy by default.
Comment #7
Wim LeersCorrect. Note that for that problem/aspect I have #2817745: Add test coverage to prove that REST resource's "auth" configuration is also not allowing global authentication providers like "cookie" when not listed.
If by that you mean that
supported_auth: []
is totally valid, then yes. If not, can you clarify?What are you suggesting exactly?
In what way does it "yell"? Logging to watchdog is very very easily missed.
I would argue it is confusing today.
This issue is just meant to make things logical for "authless" resources. For making it more clear for authenticated resources, there is #2817745: Add test coverage to prove that REST resource's "auth" configuration is also not allowing global authentication providers like "cookie" when not listed.
Comment #8
Grayside CreditAttribution: Grayside at Phase2 for Norwegian Cruise Line commentedThe other issue addresses my concerns with this, which I slightly conflated with how confusing this is to set up with the REST UI module enforcing the current expectations.
Comment #9
Wim LeersYep, that's exactly part of the problem. REST UI exposes the very same problems in the UI: the same problems that you have if you manually set up this YAML file.
Comment #10
dawehnerCould we make an explicit check of
supported_auth : []
vssupported_auth: ~
aka. a non existing value? You could read[]
as explicit decision to not provide authentication.Comment #11
Wim LeersThat sounds sensible!
Comment #12
dawehnerMaybe we even have tests for that alright, even I sadly doubt it ...
Comment #13
dawehnerDamnit it'll be tricky to update
\Drupal\rest\Tests\RESTTestBase::enableService
properly.Comment #14
Wim LeersWouldn't
empty($a) && $a !== NULL
be clearer?Comment #15
dawehnerOh totally!
Comment #16
Wim LeersSo, I worked on #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method, and that does also have explicit anonymous test coverage. Why does that work?
Two reasons:
\Drupal\rest\Tests\RESTTestBase::enableService()
has. It's slightly different though:\Drupal\rest\Tests\RESTTestBase::enableService
sets it to['cookie']
, if you don't specify allowed auth.\Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::provisionEntityResource()
just sets[FALSE]
if you don't specify allowed auth. And yes, everything accepts this: theRestResourceConfig
entity (because it doesn't do validation) and the routing system (because it maps this to the empty string). It only works thanks to the entire system being incredibly non-strict.By fixing
EntityResourceTestBase::provisionEntityResource()
, we automatically gain test coverage.Thinking about this more, and playing with this, I actually had interpreted
~
(i.e.NULL
) as the way to specify the intention to only allow anon access. Because~
is currently not yet allowed by the REST Resource config schema.But thinking about this again, I think it makes sense to just use the empty array for that. Because you can never choose to not allow anonymous access. Anonymous access to a resource is always allowed. That's how the routing system works.
Comment #17
Wim LeersThis is the direction I went with first:
~
(NULL
) means .Comment #18
Wim LeersAnd this is if I follow what @dawehner initially proposed. Note that the value can never be
NULL
. So this:does not make sense. Unless we of course again update the config schema to allow NULL, and then NULL becomes the way to truly specify "no authentication providers", but then that also doesn't actually make sense, because A) we don't allow that in the code anyway, so why allow it in the config schema?, B) not allowing any authentication providers would still allow anonymous access, and that's exactly what the empty array already signifies.
In other words: that logged error ceases to make sense, and that's all!
Comment #19
Wim LeersOf course, if some of you think
, then I don't disagree!But the thing is that we've always allowed anonymous access to any REST resource!
I personally think what would be clearest is if we would break BC (it'd be a soft BC break, because it's unlikely many sites are relying on anonymous access to REST resources), and do this:
'anon'
to be specified in the REST Resource config entity'sauthentication
section\Drupal\rest\Routing\ResourceRoutes::getRoutesForResourceConfig()
, set_user_is_logged_in: 'TRUE'
unless this'anon'
authentication provider is explicitly mentioned\Drupal\rest\Plugin\ResourceBase::permissions()
generates a permission for every method for every resource, unless you opt out from that)Comment #20
gabesullice@Wim Leers I like what you have to say in #19. I much prefer explicitly "allowing" anon to access the resource over presuming that anonymous is allowed simply because no authentication provider is configured (if we were to remove the error and log message).
My only concern is with CSRF tokens (I do not have deep enough knowledge of this to know if it's a legitimate concern). By not having cookie based auth, even for anonymous users, then we lose the anonymous session cookie, which I believe plays some role in creating CRSF tokens. Are we potentially enabling unexpected behavior with that? Will requests that should seem to work start getting blocked or will IPs start being put in the flood table? As I said, this may be a totally unfounded concern, but I want to raise it just in case it is not.
Comment #21
Wim LeersGlad you raised it, I've gotten confused by that myself too :)
CSRF tokens are only necessary for cookie auth. Basic auth, OAuth 2 etc are not vulnerable to CSRFs (because you can embed a URL that would be triggered by an unwitting user, but it would never include Basic Auth or OAuth 2 request headers — the same is not true for cookies).
Furthermore, allowing anonymous implies allowing anybody to access a resource, so CSRF is irrelevant: anybody can do these things. (So you'd probably only allow
GET
for anonymous users.)Comment #26
Wim LeersThe contrib module REST UI's #2332797: Add option to allow access to resource (or a particular method on a resource) without authentication (i.e. as anon) problem cannot be fixed without this being fixed.
Comment #27
Wim LeersActually, this means that
\Drupal\user\Plugin\rest\resource\UserRegistrationResource
which was added in #2291055: REST resources for anonymous users: register cannot be used at all in practice.If you set this up and comply with REST UI's demand to specify an authentication method and choose e.g.
basic_auth
, registration fails because:And if you manually modify the
user_resource
REST Resource Config entity, so that it has:(i.e. no authentication mechanisms) then the corresponding REST route is never created.
The only way to have it work is to associate the
cookie
authentication mechanism… and then not send a session cookie. Because\Drupal\user\Authentication\Provider\Cookie
is aglobal
authentication provider, this is accepted. This is a huge WTF.EDIT: and yes, we have test coverage:
\Drupal\user\Tests\RestRegisterUserTest
. But it only works due to chance: it doesn't explicitly enable thecookie
authentication provider, that just happens to be the default. inRESTTestBase::enableService()
. IOW: this test coverage only works by accident!Comment #28
Wim LeersImproved issue summary: explained how the current REST test coverage for the anonymous use (
NodeJsonAnonTest
,CommentHalJsonAnonTest
, and many dozens others) manage to work around this.Comment #29
Wim LeersRevamped IS.
There are now two proposed resolutions in the IS. Solution 1 == patch in #18. Solution 2 does not yet have a patch, but was proposed in #19.
This is blocked on a decision which solution is preferred. Hopefully we can get a framework manager to express a preference for either one, otherwise we may be wasting a lot of effort.
Comment #30
Wim LeersComment #31
dawehnerI totally agree that this should be explicit rather than implicit.
This statement is confusing. How is this dangerous, its really common to render stuff for the anonymous user.
I don't see a problem, it could be literal a anon provider, which returns an anonymous user as result of the authentication.
Comment #34
malcolm_p CreditAttribution: malcolm_p commentedNot sure if this issue is blocked in 8.4 but I wanted to weigh in in favor of option #1.
In the case of rest views, this can be particularly confusing because the requirement for authentication will supercede access controls based on permissions. I think that requiring authentication as a means of access rather than relying on the standard access control system via permisisons and roles. If the user can't be authenticated because no authentication providers are defined they should be considered anonymous.
Comment #35
Wim LeersI somehow never saw #31! :(
Sorry, I meant "allowing anon access by default".
+1!
I like where this patch is going, @dawehner++
Comment #36
Wim LeersRebased, let's see how many failures it has today. Even though the routing system now has fewer bugs, the number of failures is likely much higher, simply because there's far better REST test coverage now.
Comment #38
dawehnerThis might fix a good bunch of errors ...
Comment #40
Wim LeersHAH! Nice! 👌
All basic auth tests are failing like this:
Now they are 403s saying that the used authentication method is not allowed. But it used to be a 401, thanks to
\Drupal\Core\EventSubscriber\AuthenticationSubscriber::onExceptionSendChallenge()
converting the 403 exception (AccessDeniedHttpException
) into a 401.Now it's impossible in practice to ever reach that point. Because in
\Drupal\Core\EventSubscriber\AuthenticationSubscriber::onKernelRequestFilterProvider()
,$this->authenticationProvider->applies($request)
returns true forAnon
(which makes sense), which results in that 403 exception.But then because that exception is thrown, we reach
\Drupal\Core\EventSubscriber\AuthenticationSubscriber::onExceptionSendChallenge()
, we check!$this->authenticationProvider->applies($request)
(the inverse!), which can then of course never be true *also*.The problem is that
onExceptionSendChallenge()
never took into account that some authentication provider would always apply. It assumed it would only ever be called when access was denied because the user wasn't logged in. This is wrong.What we should do, is update
\Drupal\Core\EventSubscriber\AuthenticationSubscriber::onExceptionSendChallenge()
to iterate over only the authentication providers that allowed on the requested route, instead of iterating over all authentication providers.I don't yet see how to achieve that without making BC breaks. For now, here's a work-around that allows us to focus on fixing the other problems first. (I spent an hour trying.)
Comment #43
Wim LeersSomebody asked again about this WTF in #2949327-6: Help with REST authentication.3.
Comment #45
Solthun CreditAttribution: Solthun commentedRan into this issue when I found out a headless site in prod wasn't creating content since before summer.
First, thanks for the great work!
I wanted a rounded solution so went for the latest patch and I wanted to add some details here.
The app used the route '/entity/node ....' as a rest endpoint by posting for submissions to it and it worked back in the day.
Tried to fix the content creation by applying the patch and changing rest.resource.entity.node.yml by adding this to the end:
authentication:
- anon
Until now it did not work since the import itself failed without an authentication method set.
Big surprise, didn't work this time either.
After some debug, I can tell you this:
The 'new' rest endpoint '/node' works great with this patch, but the canonical path '/entity/{entity_type}' still fails.
Did not have the time to dig deeper and find the root cause, but still might help someone struggling with this.
Comment #46
PQ CreditAttribution: PQ commentedRerolled against 8.6.x (also applies to 8.5.x and 8.7.x)
Someone more clued up on this then me might check if the tests it adds in
BasicAuthResourceTestTrait->assertResponseWhenMissingAuthentication()
are now redundant with the interim changes to that file.Comment #48
oknate+1 on this.
I don't set a lot of these up, but when I do, I usually forget to set the permissions. I was thinking maybe there could be a checkbox (Allow for anonymous and authenticated users) on the form. But then I found this issue, and it makes more sense if you can just not select auth, and then there's no access check.
I went ahead a created a new issue for setting permissions:
#3062876: Allow setting resource permissions from config form
It would at least remove some of the pain of finding your resource is inaccessible to anonymous users on the QA site. And it wouldn't require an upgrade path.
Comment #49
Wim LeersComment #54
dafederWondering has anyone revisited this issue recently? I have some custom routes I'd like to re-implement as a rest plugin but I need the GETs to allow anonymous access. Have people discovered other workarounds in D9?
Comment #57
kksandr CreditAttribution: kksandr commentedPatch reroll