craftcms / cms

Build bespoke content experiences with Craft.
https://craftcms.com
Other
3.29k stars 638 forks source link

Authorizing users when in headless mode in frameworks like Next.js, Nuxt.js and create-react-app #5303

Closed aaronbushnell closed 3 years ago

aaronbushnell commented 4 years ago

Description

Now that Craft has fantastic support for running in a headless mode I’d love to have a built-in way to authenticate users when the front end is disconnected from Craft. For instance, if I’m running the front end on something like Next.js, Nuxt.js or create-react-app.

Additional info

aaronbushnell commented 4 years ago

FWIW Laravel’s Airlock add-on does a really good job of handling SPA authentication while still using sessions. https://github.com/laravel/airlock

Wanted to drop that here in case it’s relevant as this progresses!

steveDL commented 4 years ago

I am looking for the same functionality. It has so far been a pleasure using Craft and graphQL with nuxtJS for building a PWA with separate back and front ends, however, craft falls short when trying to implement user auth with this stack. In some cases, I really don't even need a user to be able to edit or do anything with the craft entries/assets etc, I just want an endpoint that I can POST data to from a frontend login form that will check that the credentials match a user in the craft DB and return me ok so I can show certain vue routes if the user login is correct.

As a long time user of craft and also of react/vue it would be so awesome if you guys could make craft play nice in this regard. I've always loved Craft and our clients do too but this is pushing me a bit towards using something like Strapi.

Thanks guys!

brandonkelly commented 4 years ago

I just want an endpoint that I can POST data to from a frontend login form that will check that the credentials match a user in the craft DB and return me ok so I can show certain vue routes if the user login is correct.

@steveDL Well that’s super easy for us to do. I just added basic authentication support for web requests, for Craft 3.5.

You can send a request to the users/session-info action to get some basic info about a user account.

curl "http://example.test/actions/users/session-info" \
     -H 'Accept: application/json' \
     -u 'admin:password'

Example response:

{
  "isGuest": false,
  "timeout": 0,
  "id": 1,
  "uid": "b66b2bfe-badb-478a-81ff-1fceb638a019",
  "username": "admin",
  "email": "admin@happylager.dev"
}

Or if the username/password are incorrect, you will get a 401 response:

{
  "error": "Your request was made with invalid credentials."
}

Note that that Accept: application/json header is required for the users/session-info controller action.

The user will only actually be “logged in” for the duration of that one request. They won’t get an actual user session this way.

If you want to test this out now, change your craftcms/cms requirement in composer.json to "3.5.x-dev", and then run composer update.

(I’d still like to add access tokens to user accounts, similar to Laravel Sanctum, at some point, so leaving this open.)

brandonkelly commented 4 years ago

I’ve decided to make basic HTTP auth something you have to opt into, as it was conflicting with server authorization (#6421). So if you want to use it going forward, you will need to add this to config/general.php:

'enableBasicHttpAuth' => true,
timkelty commented 4 years ago

@steveDL working for me w/o a CSRF token

steveDL commented 4 years ago

@brandonkelly Thanks for this I am calling this endpoint ok in my project now but I seem to be getting a 200 ok with isGuest: true instead of a 401 or the response with the user details.

Screenshot 2020-07-23 at 11 59 02

Screenshot 2020-07-23 at 11 59 51

That being said it seems to work as you describe in postman.

brandonkelly commented 4 years ago

@steveDL you should be getting a 401 response if both username & password are passed, but don’t match. Maybe you tested without passing any password at all? If so, the whole authentication logic would be skipped.

https://github.com/craftcms/cms/blob/549b73e6acb508a882a93609dc1932d5cf765a38/src/web/Application.php#L392-L394

pieterjandebruyne commented 4 years ago

@brandonkelly @steveDL I am struggling with the same thing. I would like routes within my vue app to be protected using the craft cms user managment, But I have no clue where to start... at first I thought I needed to get a csrf token as described in https://nystudio107.com/blog/using-the-craft-cms-graphql-api-on-the-frontend#controller-of-fury and then use that csrf to post to the user login endpoint but this does not seem to pass cors errors..

So now I am trying this new basic auth functionality When I submit this:

axios.get('http://domain.test/actions/users/session-info', {}, {
                    auth: {
                        username: 'admin',
                        password: 'password',
                    },
                    headers: {
                        'Content-Type': 'application/json',
                        Accept: 'application/json'
                    }
                }).then(response => {
                    let data = response.data;
                    console.log(data);
                })

The data returned is

csrfTokenValue: "MkpESOtwj38KfcOxdXXo5eECUlWnj7NMASYtgeOwVzcDYHo9PWdPQx8zKy-sMt4OZilxgCYmppKpUwdkkcblDzd_f-WC7xlWVDQtCRAVPAA="
isGuest: true
timeout: 0

even when I post the correct admin user cred. @steveDL did this return the userdata and isGuest: false for you? Also when I go to the backend CP and I login here the login spinner under the login button and the chrome refresh indicator keep spinning without redirecting me to the CP, if I refresh the page I am logged in and inside the CP.. I added these to my config:

'enableBasicHttpAuth' => true,
'enableCsrfProtection' => true,

I also tried this to pass the basic auth in the axios headers:

const token = Buffer.from(`${username}:${password}`, 'utf8').toString('base64')
axios.get('http://domain.test/actions/users/session-info', {}, {
    headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'Authorization': 'Basic ' + token
    }
}).then(response => {
    console.log(response);
})

But that also never returns the correct info for this user. Passing invalid passwords or even no Auth object at all results in the same response (isGuest: true etc)

If I use postman and add the username/pw under Basic Auth it does return the isGuest false and userinfo

With the axios post I noticed the XMLHttpRequest.withCredentials is false, is this the problem? I'm rather new to cross domain requests

(using Craft CMS 3.5.0-RC1.1 )

steveDL commented 4 years ago

@brandonkelly @pieter-janDB - We are experiencing the same thing expect I am able to login to the control panel fine.

At the moment, with my axios setup I posted above, with any user details I am getting the response I posted above which has only isGuest: true, the csrf and timeout: 0. If I use postman then it works exactly as you describe so I am wondering if this is possibly to do with the axios request.

From my understanding using the auth param with axios will add the Authorization header.

pieterjandebruyne commented 4 years ago

@steveDL I think we need the following config with axios..

On the backend server add these headers (for me this was inside /usr/local/etc/nginx/valet/valet.conf for valet, I should make this a different config file for that domain only but have not done this before)

add_header Access-Control-Allow-Origin http://vueappdomain.test;
add_header Access-Control-Allow-Credentials true;

In the vue app add this to the axios instance: window.axios.defaults.withCredentials = true;

It now returns the user data but only if I am logged into the backend CMS in another tab. When I log out from the backend cms and then send the session-info request I just get isGuest true no matter what auth data I pass

pieterjandebruyne commented 4 years ago

https://craftcms.stackexchange.com/questions/35808/js-login-form-on-different-domain-and-handling-the-csrf-token/ is trying the same thing

pieterjandebruyne commented 4 years ago

I have been trying many things without success.. I only get returned the userdata when there is an active session (login on the backend via CP login form so there is an active session) no matter what username or password I pass.. It seems like it is skipping the basic auth check..

Also I added the get-csrf function in a module as described here : https://nystudio107.com/blog/using-the-craft-cms-graphql-api-on-the-frontend#controller-of-fury When I pass this token inside the X-CSRF-TOKEN header within the session-info call I get errors on CORS Access to XMLHttpRequest at 'http://domain.test/actions/users/session-info' from origin 'http://vueapp.test' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. But when I remove the X-CSRF-TOKEN header the Access-Control-Allow-Origin is set as should.

pieterjandebruyne commented 4 years ago

I started up xDebug to debug /src/web/Application.php within the authenticate function on line 390 list($username, $password) = $this->getRequest()->getAuthCredentials(); returns null for both username and password. When I use postman these params indeed included my user and password.. So I'm guessing it is still a problem with axios ? I am not sure how postman handles these requests, does it send request from the same domain so cors is not an issue ?

steveDL commented 4 years ago

@timkelty Please could you shed some light on how you have this working? Are you using axios?

timkelty commented 4 years ago

@steveDL I was just testing via curl (with 'enableBasicHttpAuth' => true,):

❯ curl "http://localhost:8089/actions/users/session-info" \
     -H 'Accept: application/json' \
     -u 'myusername:mypassword'
{"isGuest":false,"timeout":0,"csrfTokenValue":"xxxx","id":2,"uid":"xxxx","myusername":"username","email":"myusername@foo.com"}

You're using Axios' auth option?

steveDL commented 4 years ago

Ah thank you, I have tried using the axios auth option and also with setting the Authorization basic header manually and passing it a base64 string. Curl is working absolutely fine for me and so is postman. I have a feeling that axios could be omitting the auth credentials and so the whole authentication logic is being skipped altogether but I am not sure why it would be omitting them.

I know that using Access-Control-Allow-Origin with the wildcard * means doesn't allow the use of credentials so I have tried changing it to localhost but to no avail

pieterjandebruyne commented 4 years ago

Ok, I did more axios tests.. Using no config at all and posting with :

axios.get('http://xxx.test/actions/users/session-info', {
                    }).then(response => {
                        console.log(response);
                    })

Returns:

Screenshot 2020-07-24 at 13 56 28

With these headers:

Screenshot 2020-07-24 at 13 54 49

And I think this is what @steveDL is posting aswell, looking at your axios.get you pass an url and then an empty object which should be the options object.

But now when I use the options within the axios call like this:

let username = 'admin';
let password = 'adminpw';
 axios.get('http://xxx.test/actions/users/session-info', {
                        headers: {
                            'Content-Type': 'application/json',
                        },
                        auth: {
                            username: username,
                            password: password
                        },
                    }).then(response => {
                        console.log(response);
                    })

It will now do 2 calls (which I understand is needed for basic auth? it will first fire an OPTIONS call?) but they will fail with a CORS error:

Screenshot 2020-07-24 at 13 56 24 Screenshot 2020-07-24 at 14 08 32

1st call:

Screenshot 2020-07-24 at 13 55 03

2nd:

Screenshot 2020-07-24 at 13 55 11

Maybe someone has a little more experience with request/response headers and can spot the mistake..

timkelty commented 4 years ago

@pieter-janDB Axios is doing a preflight CORS request because you are (presumably) requesting across different domains. See https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests

To configure CORS, you're going to need to add a header, e.g.

Craft::$app->getResponse()->getHeaders()->add('Access-Control-Allow-Origin', '*');

…ideally replacing * with your other domain.

pieterjandebruyne commented 4 years ago

@timkelty Thanks for the response but I think I am already doing this. I have these headers setup in my valet nginx config:

add_header Access-Control-Allow-Origin http://vueappdomain.test;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With";

And if you take a look at the response from the server without options it says: Access-Control-Allow-Origin: http//vueappdomain.test ... (but this doesn't seem to be included when using the Basic Auth options?) Also I read that '*' doesn't work when using Basic Auth thats why I hardcoded the frontend domain.. Also this session-info is inside craft app code so I can't really add that line anyway..

steveDL commented 4 years ago

I am also using nginx, currently reading through this https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/

Ah that is no help to be honest.

I have noticed that in safari I get no response headers

Screenshot 2020-07-24 at 14 30 15

timkelty commented 4 years ago

@pieter-janDB @steveDL Shot in the dark, but can you try just adding the auth to the url instead of via the auth option?

let username = 'admin';
let password = 'adminpw';
 axios.get(`http://${username}:${password}@xxx.test/actions/users/session-info`, {
                        headers: {
                            'Content-Type': 'application/json',
                        },
                    }).then(response => {
                        console.log(response);
                    })

Assuming that doesn't do anything, I'll spin something up and try via Axios and get back to you.

timkelty commented 4 years ago

Also, you should be setting a Accept: application/json header. Content-Type isn't necessary here since we arent passing any data:

                        headers: {
                            'Accept': 'application/json',
                        },
pieterjandebruyne commented 4 years ago

Oh yes I see I copied the axios call without Accept header in my example.. But I tried it with Accept, both, in quotes, not in quotes.. The response stays the same as in the screenshot. I changed it in above post to show Accept instead of Content-Type doing the call with username:pw@ in front returns the same as when I use no config (1 call and csrf, guest true, timeout 0)

I did notice that when using the configs with headers & auth:L The first request (that tests the auth?) shows request header 'Accept: application/json'but the second one shows Accept: */*

steveDL commented 4 years ago

Have you guys managed to solve this issue?

pieterjandebruyne commented 4 years ago

@brandonkelly Have you successfully gotten the right response when fetching from JS, if so mind sharing the code? I can only get curl and postman working.. If I send the request without any Authorization I get the csrf, isGuest:true,timeout:0 (and when I am logged into the cms in another tab I also get the user info) But if I add the Authorization data it gives CORS errors..

Access to XMLHttpRequest at 'http://cmsdomain.test/actions/users/session-info' from origin 'http://vueappdomain.test' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

So it only gets blocked when I pass the Auth and the browser does an extra preflight request.. When I try it from another domain it also gives CORS errors on the first request without auth.. so this makes me think my server nginx config is ok.. you can see screenshots above with my config and the responses I get

brandonkelly commented 4 years ago

@pieter-janDB sounds like you need to add an Access-Control-Allow-Origin header to the responses. You’ll likely need to send an Access-Control-Allow-Credentials header as well. You can do that from your nginx config:

add_header Access-Control-Allow-Origin <JS-origin>;
add_header Access-Control-Allow-Credentials true;

There are a number of things that cause browsers to become more strict with CORS requirements, and sending an Authorization header is one of those.

pieterjandebruyne commented 4 years ago

@brandonkelly This is my nginx config (via Valet) If I change the domain to * it gives me errors saying I need to specify one, and since it is not giving me cors errors when I try to fetch without auth I think everything is set up allright? Both the vue app and craft cms run via valet now. if I run the app on localhost:8080 it gives me cors errors because it is not the http://vueappdomain.test so that seems ok.

server {
    listen 127.0.0.1:80 default_server;
    root /;
    charset utf-8;
    client_max_body_size 128M;

    location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
        # CORS Rules
        add_header Access-Control-Allow-Origin http://vueappdomain.test;
        add_header Access-Control-Allow-Credentials true;
        add_header Access-Control-Allow-Methods "GET, POST, PATCH, PUT, DELETE, OPTIONS";
        add_header Access-Control-Allow-Headers "Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, X-Auth-Token, Content-Range, X-CustomHeader, Range,Authorization";
        # END of CORS Rules #
        internal;
        alias /;
        try_files $uri $uri/;
    }

    location / {
        # CORS Rules
        add_header Access-Control-Allow-Origin http://vueappdomain.test;
        add_header Access-Control-Allow-Credentials true;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
        add_header Access-Control-Allow-Headers "Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, X-Auth-Token, Content-Range, X-CustomHeader, Range,Authorization";
        # END of CORS Rules #
        rewrite ^ "/Users/pieterjandb/.composer/vendor/laravel/valet/server.php" last;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    access_log off;
    error_log "/Users/pieterjandb/.config/valet/Log/nginx-error.log";

    error_page 404 "/Users/pieterjandb/.composer/vendor/laravel/valet/server.php";

    location ~ [^/]\.php(/|$) {
        # CORS Rules
        add_header Access-Control-Allow-Origin http://vueappdomain.test;
        add_header Access-Control-Allow-Credentials true;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
        add_header Access-Control-Allow-Headers "Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, X-Auth-Token, Content-Range, X-CustomHeader, Range,Authorization";
        # END of CORS Rules #
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass "unix:/Users/pieterjandb/.config/valet/valet.sock";
        fastcgi_index "/Users/pieterjandb/.composer/vendor/laravel/valet/server.php";
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME "/Users/pieterjandb/.composer/vendor/laravel/valet/server.php";
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }

    location ~ /\.ht {
        deny all;
    }
}

js:

window.axios.get('http://ibccms.test/actions/users/session-info', {
                        headers: {
                            'Accept': 'application/json',
                        },
                        auth: {
                            username: username,
                            password: password
                        },
                    }).then(response => {
                        console.log(response);
                    })

Above works as long as I do not pass the auth (same with fetch and Authorization header

Is there anything special about the preflight requests ? I feel like those don't get the right headers but I have no experience with them.

brandonkelly commented 4 years ago

since it is not giving me cors errors when I try to fetch without auth I think everything is set up allright

As said, things get more strict when you pass an Authorization header. So the fact that it works without one doesn’t help.

I’m not sure what’s going wrong exactly with your nginx config, but you can trust the error the browser is giving you. If it’s saying a header needs to be set, then it’s not getting set properly, at least for the OPTIONS (preflight) request.

timkelty commented 4 years ago

I did a test with Axios with a same-origin request, and all worked fine.

However, there are some issues with cross-origin…

As already mentioned, you need to have Access-Control-Allow-Origin "mydomain" and Access-Control-Allow-Header: Authorization set.

@brandonkelly The problem is coming from requireAcceptsJson in the controller. When the request is cross-origin, it dies here with the OPTIONS request, (Response to preflight request doesn't pass access control check: It does not have HTTP ok status.)

This seems like this might be a problem with any controllers with OPTIONS preflights and requireAcceptsJson or similar methods.

If I comment that out and ensure proper CORS headers, it works cross-origin as expected.

brandonkelly commented 4 years ago

@timkelty Just updated that function to stop throwing an exception for preflight requests.

pieterjandebruyne commented 4 years ago

That was it !!! got it working aswel after upgrading to aa7d700 commit, Thanks for the help @timkelty @brandonkelly . Hope it works for you too @steveDL , if it doesn't try using the config 4 posts up.(or pieces of it, my nginx setup is with valet) Cheers!

steveDL commented 4 years ago

Great work guys, it sounds like this is resolved. Which craft version has this updated controller?

brandonkelly commented 4 years ago

@steveDL Not released yet, but will be part of 3.5.0-RC5.

timkelty commented 4 years ago

For anyone else looking for this, I just PR'd to allow for email or usernames in the creds: https://github.com/craftcms/cms/pull/6487

@brandonkelly not a huge deal, but with an invalid login, i'm not seeing the asErrorJson response you show here: https://github.com/craftcms/cms/issues/5303#issuecomment-657918281 – it just throws UnauthorizedHttpException and you always get back html.

brandonkelly commented 4 years ago

@timkelty merged!

Ah yeah, I ended up changing the location of the authentication logic in 1e736e893b0b8801f60b47596fa50260d08c1f73, and now it happens before we’ve had a chance to worry about returning error info in the right format. The response code will still be 401 though, and that’s the most important thing.

timkelty commented 4 years ago

Yup no biggie since the 401 is really what matters. Thanks!

pieterjandebruyne commented 4 years ago

After completing this request I get some warning in chrome: A cookie associated with a cross-site resource at http://cmsdomain.test/ was set without the 'SameSite' attribute. A future release of Chrome will only deliver cookies with cross-site requests if they are set with 'SameSite=None' and 'Secure'. You can review cookies in developer tools under Application>Storage>Cookies

Even though the warning says was set I don't see any of them when I inspect in applications>cookies..

Craft uses PHP session cookies to maintain sessions across web requests so looking at the changes from chrome cookies ( https://blog.chromium.org/2020/02/samesite-cookie-changes-in-february.html ) I think there needs to be a SameSite=None; Secure attached to the craftSessionId cookie in order to maintain the php session so I do not have to keep passing basic auth data ?

Screenshot 2020-07-30 at 17 47 34 This is what the cookies I receive from that request look like now.

Firefox says something simular that attribute sameSite is misused becuase it has the value none without secure

Is this something I can do, making sure those get attached to the cookie ? or is it inside the craft code? or is it because I am serving test domains over http?

Also maybe this new basic auth functionality is a perfect topic for a new CraftQuest livestream so it gets a bit documented how to do user management with craft as headless CMS

pieterjandebruyne commented 4 years ago

Ok I found that we can set secure and samesite from the config. but this requires php 7.3 so I will need to upgrade first before I can use this I guess. ( sameSiteCookieValue -> This setting requires PHP 7.3 or later. )

brandonkelly commented 4 years ago

@pieter-janDB that’s probably only happening for you, because you have been directly visiting that domain on your browser. If your end users will never login to Craft directly (not including these authenticated Ajax requests), then they will never get any cookies sent for that domain.

But still, you could resolve this by creating a subdomain that is responsible for handling the Ajax requests. It can share the same webroot as your main Craft site – so just talking about adding a new vhost/server record on your web server. Then as long as you never login from that subdomain directly in your browser, you won’t get these warnings either.

pieterjandebruyne commented 4 years ago

@brandonkelly I tried alot of things today but couldn't get this to work... some endusers will also login to the CP so I will need to use the subdomain to query my api requests.

I started with switching to php7.4 local. I then got the vue app and craft cms install working on a secure valet domain (both served over https) For the cms I created a new domainname in valet (this is the same as a new vhost/server record right?) and I never visited this domain in my browser ( https://chapi.test ).

I added these configs :

        'enableBasicHttpAuth' => true,
        'enableCsrfProtection' => true,
        'sameSiteCookieValue' => null,
        'useSecureCookies'     => true

I get the right user info returned and validated but still no cookies I still get the error:

A cookie associated with a cross-site resource at https://chapi.test/ was set without the `SameSite` attribute. A future release of Chrome will only deliver cookies with cross-site requests if they are set with `SameSite=None` and `Secure`. You can review cookies in developer tools under Application>Storage>Cookies and see more details at https://www.chromestatus.com/feature/5088147346030592 and https://www.chromestatus.com/feature/5633521622188032.

But when I inspect the network request I see Secure is checked

Screenshot 2020-07-31 at 15 01 12

But under application>Cookies>https://vueappdomain.test there is not 1 cookie

I also tried changing some samesite config @ chrome://flags/ but nothing seems to be doing the trick to place these cookies

Could this have something to do with the 'httponly' cookie config?

@timkelty & @steveDL are you experiencing something similar?

pieterjandebruyne commented 4 years ago

@brandonkelly @timkelty @steveDL Did any of you get this to work with CORS ?

I am still struggling.. I also tried hosting them online without any success. I made a new sub-domain A-record to make sure I have not yet logged in from the craft cms api domainname via CP (as mentioned by @brandonkelly ).

I have ssl certificates active for my api domain and vue app domain

I use apache online so I added this to my craft cms domains public folder .htaccess file:

<IfModule mod_headers.c>
    Header always set Access-Control-Allow-Origin https://[frontenddomain].com
    Header always set Access-Control-Allow-Credentials true
    Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE, PUT, PATCH"
    Header always set Access-Control-Allow-Headers "X-CSRF-TOKEN, Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, X-Auth-Token, Content-Range, X-CustomHeader, Range"
</IfModule>

I am not sure if these allow-methods and headers are needed but I tested it without them as well without success.

I added these configs to craft config:

'enableBasicHttpAuth' => true,
'enableCsrfProtection' => true,

Using postman with basicauth + header ['Accept'] = application/json it does not return the data for the auth passed:

{
    "isGuest": true,
    "timeout": 0,
    "csrfTokenValue": "j4rCbxXR1iQEGDixH62t7r6NvgA4EyryAmILvGQG-pMQkskhO4N37ezmqh9yt5pNcm9t1kD-6vrh69puUF4au24HYY0-T5TKe6SvTAjMJrI="
}

I get the same responses with axios but I am 100% sure the auth data passed is right and it is not a guest..

Screenshot 2020-08-17 at 14 10 34 Screenshot 2020-08-17 at 14 10 29
webvader commented 4 years ago

Hi,

A little follow-up on the issues above. @pieter-janDB and I debugged further and came to the conclusion that the Apache webserver did not support the necessary allowed methods e.g. OPTIONS. After changing the config and allowing the OPTIONS method everything is working fine.

Case closed!

steveDL commented 4 years ago

@pieter-janDB I did manage to get this working, however adding headers for this basic auth caused another issue, that @brandonkelly might be able to shed some light on, as I am using graphql. Adding the headers for basic auth broke my graphql API as the headers were then duplicated becasue, I believe, craft already adds the headers for graphql. So I got login working but no graphql queries are able to execute. We've been looking into our nginx config and locking down headers to specific calls like /actions/users/session-info but as soon as I let it serve the php file the headers inside of location gets overridden.

By adding the following to the NGINX Server level block we got the login working

# CORS SOLUTION
add_header Access-Control-Allow-Origin ttps://my-api.staging.mycompany.com;
add_header Access-Control-Allow-Credentials true;

But when we try to make these headers apply only to a specific URI path it fails and it seems that Craft Overrides

# CORS SOLUTION
        location '/actions/users/session-info' {
                add_header Access-Control-Allow-Origin ttps://my-api.staging.mycompany.com;
                add_header Access-Control-Allow-Credentials true;
                try_files $uri $uri/ /index.php$is_args$args;
     }
brandonkelly commented 4 years ago

@steveDL The graphql/api action is only concerned with the GraphQL token; it’s not affected by whether someone is logged in. So for now probably best to just stop sending the user auth header for those requests.

steveDL commented 4 years ago

@brandonkelly It's not so much that it is affected by someone being logged in. Adding headers to the NGINX Server level block fixes the CORS issue so I can make a call to the /actions/users/session-info route and get the repsonse back perfectly however adding these headers also causes duplicate headers whenever a gql call is made.

So we tried using our nginx config to make these headers (for the login) apply only to a specific URI path (location '/actions/users/session-info' ) but it seems these headers get overridden by craft and they are not added for that specific route. This one is becoming quite critical for us with a go live date approaching. 😬

brandonkelly commented 4 years ago

@steveDL Sorry, I read your comment too quickly and assumed you were referring to the Authentication request header, rather than the CORS response headers.

Craft will only set Access-Control-Allow-Origin and Access-Control-Allow-Credentials headers in two cases: GraphQL API requests and Live Preview requests. It won’t set them for the users/session-info action. So I suspect this is just an issue with your Nginx config.

Maybe this is just a typo from posting on GitHub, but your example Access-Control-Allow-Origin header has an invalid URL (missing the h in https://).

If that wasn’t the culprit, try creating a test.json file in your webroot (contents set to {}) , and change your try_files line to:

try_files /test.json;

Then test the /actions/users/session-info request and see if you’re getting the expected response headers back. If you are, then you may be right that Craft is somehow at fault here, and we can look into it further.

steveDL commented 4 years ago

@brandonkelly My apologies you are correct it was nginx config related turns out we have to conditionally wrap the method of the request and apply headers for each also like so. Thank you for your time!

 location '/actions/users/session-info' {
                add_header Access-Control-Allow-Origin https://sanroque-fe.staging.doodle.je;
                add_header Access-Control-Allow-Credentials true;
                add_header Access-Control-Allow-Headers Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modifi$
                add_header Access-Control-Allow-Methods GET,POST,OPTIONS,PUT,DELETE,PATCH;

                if ($request_method = 'OPTIONS') {
                        add_header Access-Control-Allow-Origin https://sanroque-fe.staging.doodle.je;
                        add_header Access-Control-Allow-Credentials true;
                        add_header Access-Control-Allow-Methods GET,POST,OPTIONS,PUT,DELETE,PATCH;
                        add_header Access-Control-Allow-Headers Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,I$
                        return 204;
                }
                if ($request_method = 'GET') {
                        add_header Access-Control-Allow-Origin https://sanroque-fe.staging.doodle.je;
                        add_header Access-Control-Allow-Credentials true;
                        add_header Access-Control-Allow-Methods GET,POST,OPTIONS,PUT,DELETE,PATCH;
                        add_header Access-Control-Allow-Headers Authorization,Accept,Origin;
                        return 200;
                }
                try_files $uri $uri/ /index.php$is_args$args;
        }

on a side note I am trying to now submit contact forms with this setup, are there any easier ways to do this? I think I may have to use the element API to get a csrf token and sumbit the form?

brandonkelly commented 4 years ago

@steveDL You can get the CSRF token via that /actions/users/session-info endpoint. Element API won’t help you with that.

jamesedmonston commented 4 years ago

For anyone still looking for a GraphQL authentication solution, I've just released a plugin which (hopefully!) handles everything you need: https://plugins.craftcms.com/graphql-authentication

mcclaskiem commented 3 years ago

I was wondering if anyone could provide insight on how this works in the case of Commerce. When sending requests to things like the Cart, Addresses, Payments, Add to Cart, etc. This was previously handled via cookies and I believe the controllers for these respective actions used the "currentUser" helper function to determine who to associate those requests with via the cookies.

Are these cookies still being set when accessing the "/actions/users/session-info" with the appropriate credentials or do we now need to include the userId that is returned when making requests to Commerce? Conditionally, if the cookies are not being set, does Commerce even accept a userId param out of the box on the normal routes that Commerce uses?