Here is my configuration:
SERVER
Laravel 8 application that uses Sanctum to provide access to REST API. It is configured to run on http://b8/
CLIENT
A separate Vue CLI 3.0 SPA that connects to the API using session-based cookie authentication method. I have configured Vue CLI Serve to run it at http://app.b8/.
PROBLEM
Chrome does not allow Set-Cookie operation even when both server and client are on the same domain. After going through several posts and articles, I have successfully run it in Postman, but the real Vue application that runs in the browser can't set Sanctum cookie and thus cannot login.
Here is my configuration:
.env
SESSION_DRIVER=cookie
SESSION_DOMAIN=.b8
SANCTUM_STATEFUL_DOMAINS=localhost,localhost:8080,127.0.0.1,127.0.0.1:8000,::1,b8,app.b8,app.b8:8080
vue.config.js
...
devServer: {
proxy: "http://b8/api/v1",
host: "app.b8"
},
Login function in Vue application
async login(context, credentials) {
axios.get('http://b8/sanctum/csrf-cookie').then(response => {
axios.post('http://b8/login', credentials).then(response2 => {
//successfully logged in.
});
});
},
Error msg
The outer call for /sanctum/csrf-cookie route returns successfully and brings Sanctum cookie. However, Chrome thinks that the cookie is invalid for the current domain and therefore doesn't set it. Here is the Dev Tools pane's Cookies tab after the Sanctum token call returns (tooltip showing Chrome's complaint):
Since the cookie is not set, the following login call fails with a 419 error.
What am I missing here?
Finally got it working. For any future reader, here are the things:
The first mistake that I was doing was to use a single word host name. It looks like Chrome (or Sanctum, not sure) wants you to have at least one period in the host name. So I changed it from b8 to b8.cod.
You should keep same_site attribute set to lax. none is a rather dangerous value. Chrome now shows none as a potential issue.
SESSION_DOMAIN should be set to .b8.cod with a leading period to support front-end subdomain, as correctly suggested by the documentation.
All other things should be done as suggested in the question. axios must use withCredentials set to true.
I've recently set up the exact same thing (Laravel 8 backend with Sanctum + separate Vue Frontend) and it's working.
Your server side setup looks good. Please double-check that the frontend's IP is actually included in your SANCTUM_STATEFUL_DOMAINS - that caused the 419 error for me sometimes.
On the Vue side, make sure to that you enable the withCredentials in your axios instance, e.g.
const baseDomain = "http://localhost"
const baseURL = `${baseDomain}/api`
export default axios.create({
baseURL,
withCredentials: true,
})
If the provided suggestion does not help, I can further extend my answer by showing you more of my working backend code.
Related
I have a nextjs app I have deployed on vercel. It is paired with a nestjs back-end which is deployed in heroku.
I implemented login via github and, while developing the app, I used the following code to set a jwt header after the user successfully authenticated:
#Get('redirect')
#UseGuards(AuthGuard('github'))
async githubAuthRedirect(#Req() req, #Res() res) {
const accessToken = await this.authService.generateJWT(req.user);
res.cookie('jwt', accessToken);
res.redirect(`http://localhost:3000/dashboard`);
}
That is the endpoint github calls once the user has approved access to their account.
That works while I run everything on localhost, but once I deployed the front-end app to vercel, the back-end app to heroku (and configured a CNAME alias for my api) then it doesn't work.
My server gets the request from github and the user is effectively redirected to the dashboard, but no cookie is set.
Am I doing something wrong? did I miss some vercel/nextjs configuration?
After some digging around and testing all the possible configurations, turns out this is not a problem with next or with nest (or with heroku or vercel).
Cookies are by default set on the same domain which your request originates from, including sub domains.
In my case this meant, my API was responding from api.[DOMAIN].com and the cookie was not reaching [DOMAIN].com.
When setting the cookie I had to explicitely pass the parent domain:
res.cookie('jwt', accessToken, {
domain: 'scripthunt.sh',
sameSite: 'none',
secure: req.secure || req.headers['x-forwarded-proto'] === 'https',
});
You might need to change other settings on express like:
app.enable('trust proxy');
app.enableCors({
origin: 'https://[MY DOMAIN].com',
credentials: true,
});
Which might also be necessary for Heroku, not sure, all I know is my app now correctly sets the cookie.
Cheers!
I'm currently trying to test an SPA using Laravel 8.19.0 and Postman 7.36.1 but I keep getting an "Unauthenticated" response from a route that's guarded by "auth:sanctum", even though I have logged in correctly.
As far as I can understand, I've followed the documentation fully at https://laravel.com/docs/8.x/sanctum
in order to set Sanctum up to be used for SPA so I've done the following:
Installed Sanctum.
Published the Sanctum config.
Performed a migration.
Included the EnsureFrontendRequestsAreStateful middleware and 'EnsureFrontendRequestsAreStateful::class' to the Http Kernal.
Added my local domains (same top-level domain but 1 with the "test" sub domain and another with "api") to the "stateful domains" option in the Sanctum config file.
Set the "supports_credentials" option in the cors config to "true".
Set my top level domain, prefixed with a "." for the "domain" option in the session config.
Then, I've set Postman up using the guide at https://blog.codecourse.com/laravel-sanctum-airlock-with-postman/
so I've written a script to get the CSRF token from "/sanctum/csrf-cookie" then used said token as the value for the "X-XSRF-TOKEN" in the request header and I can succesfully log in. however, when I try to access a route afterwards that's guarded by the "auth:sanctum" guard, even with the referrer and 'X-XSRF-TOKEN' being set up in the request header I cannot access the route.
After debugging, I can see that $this->auth->guard($guard)->check() is returning false in the authenticate($request, array $guards) method where $guard = "sanctum" in \vendor\laravel\framework\src\Illuminate\Auth\Middleware\Authenticate.php on line 63 because $this->user() is null for the Illuminate\Auth\RequestGuard instance.
Any help or even ideas on things to check would be greatly appreciated as I'm unsure on what to do from here, short of spending a day digging deeper into the request guard object and its instantiation!
Thanks.
The issue a lot folk are seeing when using Postman with Sanctum SPA authentication is that you simply need to add an additional header to your requests, This can be "Referrer" or "Origin" and the value must match the domains set in the sanctum.php config file. e.g. localhost or mysite.test etc.
vendor/laravel/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStatefull.php in the fromFrontEnd() method is where you can see this requirement. Laravel V8.x and I believe also in Laravel V7.x
Issue has since been resolved and was caused by Postman only saving the "XSRF-TOKEN" and "laravel_session" cookies to the "test" subdomain after logging in (the login URL used this sub domain) and thus not passing them to the "api" subdomain when trying to access the route which was protected by "auth:sanctum". By adding the same cookies to the "api" subdomain via the "Manage Cookies" menu in Postman, the route can now be accessed as intended.
I have a simple web app using regular authentication for all of my web routes. There are just a few places in my app where I want to be able to call a few API routes from Javascript. Can I setup Sanctum's SPA authentication to work without doing an SPA-style login?
I have followed the instructions server-side, and on my login page I am doing a CSRF cookie request using the axios library before the user logs-in using the standard routes. But when I try to then hit a Sanctum protected route I just get redirected to the home page.
Is it expected that Sanctum session-based auth should work with a regular app login?
Edit
I located the problem. In EnsureFrontendRequestsAreStateful is this function:
public static function fromFrontend($request)
{
$referer = Str::replaceFirst('https://', '', $request->headers->get('referer'));
$referer = Str::replaceFirst('http://', '', $referer);
return Str::startsWith($referer, config('sanctum.stateful', [])) ||
Str::is(config('sanctum.stateful', []), $referer);
}
$referer is null on my requests, so this function cannot return true. If 'referer' is changed to 'host' it works. Is this acceptable? Does it satisfy the point of the function still, that the "given request is from the first-party application frontend" or are there security implications I haven't considered?
'Referer' works when hitting the API endpoint from Javascript, but when hitting it from Postman or in a browser window the headers non-existence causes a problem. So in simple GET testing it is redirecting to login, but is working fine when called from the Axios library in a Vue component.
'Referer' works in postman too, you need only add header to your request Referer -> {{host}}. host it's postman variable or you can type it like localhost. Sanctum by default checking this referers localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1, but you can specify your own referer by adding variable to env file SANCTUM_STATEFUL_DOMAINS=your-app-domain.com here.
I believe I successfully implemented the JWT middleware for Gin Gonic by following the example in the readme.
It is my understanding that upon retrieving an access token, I should also retrieve a refresh token that is being stored in a http only cookie.
There is an option for this:
SendCookie: true,
SecureCookie: false, //non HTTPS dev environments
CookieHTTPOnly: true, // JS can't modify
So I was under the impression that after logging in (and thereby getting the access token), a cookie with the refresh token is stored simultaneously. It appears if this is not the case, because I can't see any cookie in the browser's dev tools. What am I missing here?
It was actually set, one can check when looking at the response in the network tab of the browser's developer tools.
I could, however, not see it in the cookies section (dev tools > application) because it had the wrong domain. Apparently there is a bug in the middleware that causes troubles when the domain is set to localhost with a specific port.
In an application I implemented an javascript chat with long polling. Since there is just one Ajax Request per domain allowed I wanted to move the poll request to a subdomain.
So I have two domains:
dev.site.com
poll.dev.site.com
In my config.yml I entered the following:
framework:
session:
domain: .dev.site.com
cookie_domain: .dev.site.com
But Symfony does not keep me logged in if I try to poll on the sub-domain via Ajax.
Any idea on how to keep the session on the sub-domains?
I'm using the FOSUserBundle
First, the two applications need to share the fos_user table so they can reload the user when. As you have "one app and the two domains pointing to the same app." this should already be correct.
Next is to set the session cookie to be shared between the domain and the subdomain. The config in your question is correct. However for FOSUserBundle to be able to reload the user when you change from dev.site.com to poll.dev.site.com you need to share the session storage between the two domain.
The easiest way I can suggest is to store the session in a database. This is achieved by using the PdoSessionStorage available in Symfony. The official documentation covers how to setup the session storage to do that.
If all above is done correct you should not able to login to an secure area on dev.site.com, and then change the URL to an other secure area on poll.dev.site.com without any need provide login credentials again. Notice that the user credentials are only loaded in an secure area.
When it works to open poll.dev.site.com directly in the browser with any need to enter the credentials again. You need to do some additional work to get the Ajax request to work.
According to these two questions: Setting a cookie on a subdomain from an ajax request, multi-sub-domain cookies and ajax problems the problem is likely the http://en.wikipedia.org/wiki/Same_origin_policy.
The first suggests setting the following header fields on dev.site.com:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://poll.dev.site.com
And then passing withCredentials on the ajax request.
$.ajax({
url: 'http://poll.dev.site.com/some/ajax/endpoint.json',
xhrFields: {
withCredentials: true
}
});
I've tested it using a dummy file that would just set the cookie and try and ajax request. I got it to worked if I had withCredentials on the ajax request, but I could not see any difference when I tried with/without the Access-Control-Allow-* headers.
The other answer suggested using document.domain but I dodn't test that.
I used using Opera's Dragonfly to inspect the network trafic if the Cookie header was sent to the server when I tested. You can use Firebug, Chrome or probably IE too.