Skip to content

Configurable CORS and site headers#15397

Merged
brandonkelly merged 9 commits into4.11from
feature/cors
Jul 24, 2024
Merged

Configurable CORS and site headers#15397
brandonkelly merged 9 commits into4.11from
feature/cors

Conversation

@timkelty
Copy link
Copy Markdown
Contributor

@timkelty timkelty commented Jul 22, 2024

Description

  • Makes minor change to allow Yii2's CORS filter to work for actions requests
  • Deprecates allowedGraphqlOrigins in favor of using CORS filter
  • Deprecates permissionsPolicyHeader in favor of using headers filter
  • Adds 2 new filters: CorsFilter, HeadersFilter
    • These are both for site-only (non-cp) requests, and can be optionally filtered by specific site

Example config: config/app.web.php

<?php

return [
    // Attach the CORS filter to the application
    'as corsFilter' => [
        'class' => \craft\filters\Cors::class,

        // optionally filter by site(s)
        'site' => ['siteA', 'siteB'],

        // CORS defaults for all non-CP requests
        'cors' => [
            'Origin' => [
                'https://craft-4-project-cp.ddev.site',
                'https://craft-4-project.ddev.site'
            ],
            'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
            'Access-Control-Request-Headers' => ['*'],
            'Access-Control-Allow-Credentials' => true,
            'Access-Control-Max-Age' => 86400,
            'Access-Control-Expose-Headers' => [],
        ],

        // Optionally override any controller action's CORS settings.
        // This is an override only – if the setting is not present above, it will not be added.
        'actions' => [
            'graphql/api' => [
                'Origin' => ['*'],
                'Access-Control-Allow-Credentials' => false,
            ],
        ],
    ],

    // Attach the headers filter to the application
    'as headersFilter' => [
        'class' => \craft\filters\Headers::class,
        'site' => 1,
        'headers' => [
            'foo' => 'bar',
            'Permissions-Policy' => 'interest-cohort=()',
        ],
    ],
];

Related issues

Comment thread src/web/Application.php Outdated
Comment thread src/web/Application.php
@timkelty timkelty changed the base branch from 5.x to 4.x July 22, 2024 14:27
@brandonkelly brandonkelly changed the base branch from 4.x to 4.11 July 22, 2024 15:30
@timkelty timkelty marked this pull request as ready for review July 22, 2024 17:33
@timkelty timkelty changed the title Configurable CORS Configurable CORS and site headres Jul 23, 2024
@timkelty timkelty changed the title Configurable CORS and site headres Configurable CORS and site headers Jul 24, 2024
@timkelty timkelty requested a review from brandonkelly July 24, 2024 15:29
@vettndr
Copy link
Copy Markdown

vettndr commented Aug 12, 2024

Hi Brandon @brandonkelly,
I have a Nuxt.js app which is connected to a CraftCMS instance in a separated domain.

When I try to login from the Nuxt.js application using POST actions/users/login I'm getting back this error:
Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

Is there a way to handle the "preflight request"?

Best

@timkelty
Copy link
Copy Markdown
Contributor Author

timkelty commented Aug 12, 2024

@vettndr The \craft\filters\Cors handles OPTIONS preflight requests, as well as Cors-related headers.
Implement the filter in your config/app.web.php with something like this:

<?php

return [
    'as corsFilter' => [
        'class' => \craft\filters\Cors::class,
        'cors' => [
            'Origin' => [
                'https://my-nuxt-app.com',
            ],
            'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
            'Access-Control-Request-Headers' => ['*'],
            'Access-Control-Allow-Credentials' => true,
            'Access-Control-Max-Age' => 86400,
            'Access-Control-Expose-Headers' => [],
        ],
    ],
];

@vettndr
Copy link
Copy Markdown

vettndr commented Aug 12, 2024

@vettndr The \craft\filters\Cors handles OPTIONS preflight requests, as well as Cors-related headers. Implement the filter in your config/app.web.php with something like this:

<?php

return [
    'as corsFilter' => [
        'class' => \craft\filters\Cors::class,
        'cors' => [
            'Origin' => [
                'https://my-nuxt-app.com',
            ],
            'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
            'Access-Control-Request-Headers' => ['*'],
            'Access-Control-Allow-Credentials' => true,
            'Access-Control-Max-Age' => 86400,
            'Access-Control-Expose-Headers' => [],
        ],
    ],
];

Hey @timkelty
thank you for your message!

I tried as you suggested me.... and now I'm facing a 400 Bad Request issue, this is the response:

"name": "Bad Request",
"message": "Unable to verify your data submission.",
"code": 0,
"status": 400,
"exception": "yii\\web\\BadRequestHttpException",
"file": "/var/www/html/vendor/yiisoft/yii2/web/Controller.php",
"line": 221,

I'm pretty sure the credentials I'm using are working in the backend when I try to access on it.

Thanks in advance for your support.

Best

@timkelty
Copy link
Copy Markdown
Contributor Author

@vettndr your request is failing CSRF validation.

You need to fetch a CSRF token first, then include it along with your request.
See https://craftcms.com/docs/5.x/development/forms.html#ajax for examples

@vettndr
Copy link
Copy Markdown

vettndr commented Aug 12, 2024

@vettndr your request is failing CSRF validation.

You need to fetch a CSRF token first, then include it along with your request. See https://craftcms.com/docs/5.x/development/forms.html#ajax for examples

@timkelty yes, I’m fetching the CSRF token when the app is loaded for the first time and then I’m including it in the POST request.

I don’t know why it’s triggering that 400 exception.

@timkelty
Copy link
Copy Markdown
Contributor Author

How are you making the request (fetch, axios, etc)?
A common mistake is to not send credentials along with the request (eg withCredentials, credentials: include)

@vettndr
Copy link
Copy Markdown

vettndr commented Aug 13, 2024

How are you making the request (fetch, axios, etc)? A common mistake is to not send credentials along with the request (eg withCredentials, credentials: include)

Hi @timkelty
this is the login request:

const { user } = await $fetch(
        `${envs.public.craftUrl}/${authRoutes.login}`,
        {
          method: "POST",
          headers: {
            Accept: "application/json",
            "X-CSRF-Token": csrfToken.value as string,
            "X-Requested-With": "XMLHttpRequest",
          },
          body: {
            loginName: email,
            password: password,
          },
          credentials: "include",
        }
      )

as you can see I'm setting the credentialsparameter

best

@timkelty
Copy link
Copy Markdown
Contributor Author

timkelty commented Aug 13, 2024

What are the domains involved? Are they subdomains of the same root?

I'm not seeing anything jump out from your example, so if you could, please email [email protected] with as much of the project as possible, specifically:

  • composer.json/lock files
  • config directory
  • any custom modules or plugins
  • a DB dump if possible

@vettndr
Copy link
Copy Markdown

vettndr commented Aug 14, 2024

What are the domains involved? Are they subdomains of the same root?

I'm not seeing anything jump out from your example, so if you could, please email [email protected] with as much of the project as possible, specifically:

  • composer.json/lock files
  • config directory
  • any custom modules or plugins
  • a DB dump if possible

Hi @timkelty
thank you, I will send a request to the team soon.

Anyway I did a little debugging process and I found that in the validateCsrfToken($clientSuppliedToken = null) function
the $trueToken variable is printing different values comparing with $this->getCsrfTokenFromHeader().

It seems like it's comparing the token I passed in the header with another one.

I would really appreciate to read your thoughts about it.

Best

@vnali
Copy link
Copy Markdown
Contributor

vnali commented May 23, 2025

@vettndr @timkelty

Hi, any updates on this?
I'm also encountering the error "Unable to verify your data submission" when sending a request to the backend through a Vue app.

To investigate, I added some debug output and noticed that when validateCsrfToken() is called, Craft runs loadCsrfToken(), but it doesn't generate the same token as expected.

@vettndr
Copy link
Copy Markdown

vettndr commented May 25, 2025

@vettndr @timkelty

Hi, any updates on this? I'm also encountering the error "Unable to verify your data submission" when sending a request to the backend through a Vue app.

To investigate, I added some debug output and noticed that when validateCsrfToken() is called, Craft runs loadCsrfToken(), but it doesn't generate the same token as expected.

Hi @vnali
this is happening also in my application just on safari and incognito mode.
I've already asked to the Craft team a little support on this but nothing has changed.

Best

cc @timkelty

@timkelty
Copy link
Copy Markdown
Contributor Author

Thanks for the nudge, @vettndr.
I found your support ticket – I'll dig in this week and follow up in this thread.

@vnali
Copy link
Copy Markdown
Contributor

vnali commented May 30, 2025

@timkelty @vettndr
The mistake on my part was that I didn't include credentials: 'include' when fetching the session info. I had only added it to the subsequent requests.
I tested it with some available browsers on Windows, including in incognito mode, and it worked.

If I’m right, it might be worth mentioning this in the documentation here. @AugustMiller

        const response = await fetch('/actions/users/session-info', {
            credentials: 'include',
            headers: {
              'Accept': 'application/json',
            }
        }); // Or your custom endpoint
        const sessioninfo = await response.json();

@vettndr
Copy link
Copy Markdown

vettndr commented May 30, 2025

credentials: 'include',
headers: {
'Accept': 'application/json',
}

hey @vnali,
is your Vue app in a different domain comparing to the Craft instance?
I added the parameter credentials: 'include' ages ago...

@vnali
Copy link
Copy Markdown
Contributor

vnali commented May 30, 2025

@vettndr It's the same domain but a different subdomain. I can email you a link that you can check in your browser, if you'd like.

@vettndr
Copy link
Copy Markdown

vettndr commented May 30, 2025

@vettndr It's the same domain but a different subdomain. I can email you a link that you can check in your browser, if you'd like.

thank you for your answer @vnali
In my case, I have a different domain, that's why I was asking to the Craft guys a possible solution.

I can check internally to move the frontend under the same domain/subdomain to solve it.

@vnali
Copy link
Copy Markdown
Contributor

vnali commented May 30, 2025

@vettndr
I'm not an expert on this, but since you're seeing this issue in a specific browser, it might be related to how the SameSite attribute behaves differently across browsers:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#browser_compatibility

Also, here's how it's configured in Craft:
https://craftcms.com/docs/5.x/reference/config/general.html#samesitecookievalue

I'd also be interested to know if you've heard anything from the dev team about a possible solution.
Good luck with resolving this issue!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants