Restangular fails with promise and cross-origin requests - django

I am reading about Restangular and everywhere is mentioned that Restangular promises and Angular works smart and the template is updating in situations like this:
As Angular supports setting promises to scope variables as soon as we
get the information from the server, it will be shown in our template
$scope.something = Restagular.one('something').get();
I am trying to do the same thing but the Restangular is in a service because I want to keep my controllers clean. When I make a request to my REST API the Angular template is not updating and I receive this error:
XMLHttpRequest cannot load http://localhost:3000/api/template/1. The request was redirected to 'http://localhost:3000/api/template/1/', which is disallowed for cross-origin requests that require preflight.
Here is my code:
in the service...
myAppServices.service('TemplateService', ['$http', '$cookies', '$cookieStore', Restangular',
function($http, $cookies, $cookieStore, Restangular) {
Restangular.setBaseUrl(constants.serverAddress);
var getTemplate = function(templateId) {
// Check the input
if (!isValidId(templateId))
return;
return Restangular.one('api/template', templateId).get();
};
// Public getters
this.getTemplate = getTemplate;
}]);
in the controller..
$scope.currentCard = TemplateService.getTemplate(1);
So where is the problem in this case - on the client or on the server. For my API I am using django-rest-framework but I don't have problems when I am getting list with all templates (without a specific id).
I know that I can try to return a promise from the service and from its .then() to set my scope variable but in the official repo of Restangular is mentioned this and I want to use it because the code remains clean.

The issue that you are having is because Django will automatically redirect urls without a slash to urls with a slash. This isn't framework specific, as I recently discovered it is an issue for ExtJS as well.
Because you are requesting the url api/templates/1 without the trailing slash and the API is being served at api/templates/1/, Django is automatically redirecting requests from one location to the other. Normally this issue an issue, you just see the redirect happening in the console and nobody cares, but CORS requires you to have permission for the url you requested, which means it can't redirect.
You can fix this two different ways: on the client side or on the server side.
If you want to fix this on the client side, and keep the server requiring slashes, you need to tell restangular to add a slash to the end. Restangular allows you to do this by setting
RestangularProvider.setRequestSuffix('/');
In your code, which will tell restangular to always add the trailing slash.
If you want to fix this on the server side, you are going to need to stop requiring slashes in your API. This has the unpleasant side effect of not allowing any requests with a slash, and may break existing applications which are working as expected. Django REST Framework allows you to do this on the router level by setting trailing_slash=False when initializing the router.
router = SimpleRouter(trailing_slash=False)
This will tell Django REST Framework to register all of the urls without a trailing slash.

Related

Keystone session cookie only working on localhost

Edit:
After investigating this further, it seems cookies are sent correctly on most API requests. However something happens in the specific request that checks if the user is logged in and it always returns null. When refreshing the browser a successful preflight request is sent and nothing else, even though there is a session and a valid session cookie.
Original question:
I have a NextJS frontend authenticating against a Keystone backend.
When running on localhost, I can log in and then refresh the browser without getting logged out, i.e. the browser reads the cookie correctly.
When the application is deployed on an external server, I can still log in, but when refreshing the browser it seems no cookie is found and it is as if I'm logged out. However if I then go to the Keystone admin UI, I am still logged in.
In the browser settings, I can see that for localhost there is a "keystonejs-session" cookie being created. This is not the case for the external server.
Here are the session settings from the Keystone config file.
The value of process.env.DOMAIN on the external server would be for example example.com when Keystone is deployed to admin.example.com. I have also tried .example.com, with a leading dot, with the same result. (I believe the leading dot is ignored in newer specifications.)
const sessionConfig = {
maxAge: 60 * 60 * 24 * 30,
secret: process.env.COOKIE_SECRET,
sameSite: 'lax',
secure: true,
domain: process.env.DOMAIN,
path: "/",
};
const session = statelessSessions(sessionConfig);
(The session object is then passed to the config function from #keystone-6/core.)
Current workaround:
I'm currently using a workaround which involves routing all API requests to '/api/graphql' and rewriting that request to the real URL using Next's own rewrites. Someone recommended this might work and it does, sort of. When refreshing the browser window the application is still in a logged-out state, but after a second or two the session is validated.
To use this workaround, add the following rewrite directive to next.config.js
rewrites: () => [
{
source: '/api/graphql',
destination:
process.env.NODE_ENV === 'development'
? `http://localhost:3000/api/graphql`
: process.env.NEXT_PUBLIC_BACKEND_ENDPOINT,
},
],
Then make sure you use this URL for queries. In my case that's the URL I feed to createUploadLink().
This workaround still means constant error messages in the logs since relative URLs are not supposed to work. I would love to see a proper solution!
It's hard to know what's happening for sure without knowing more about your setup. Inspecting the requests and responses your browser is making may help figure this out. Look in the "network" tab in your browser dev tools. When you make make the request to sign in, you should see the cookie being set in the headers of the response.
Some educated guesses:
Are you accessing your external server over HTTPS?
They Keystone docs for the session API mention that, when setting secure to true...
[...] the cookie is only sent to the server when a request is made with the https: scheme (except on localhost)
So, if you're running your deployed env over plain HTTP, the cookie is never set, creating the behaviour you're describing. Somewhat confusingly, in development the flag is ignored, allowing it to work.
A similar thing can happen if you're deploying behind a proxy, like nginx:
In this scenario, a lot of people choose to have the proxy terminate the TLS connection, so requests are forwarded to the backend over HTTP (but on a private network, so still relatively secure). In that case, you need to do two things:
Ensure the proxy is configured to forward the X-Forwarded-Proto header, which informs the backend which protocol was used originally request
Tell express to trust what the proxy is saying by configuring the trust proxy setting
I did a write up of this proxy issue a while back. It's for Keystone 5 (so some of the details are off) but, if you're using a reverse proxy, most of it's still relevant.
Update
From Simons comment, the above guesses missed the mark 😭 but I'll leave them here in case they help others.
Since posting about this issue a month ago I was actually able to work around it by routing API requests via a relative path like '/api/graphql' and then forwarding that request to the real API on a separate subdomain. For some mysterious reason it works this way.
This is starting to sound like a CORS or issue
If you want to serve your front end from a different origin (domain) than the API, the API needs to return a specific header to allow this. Read up on CORS and the Access-Control-Allow-Origin header. You can configure this setting the cors option in the Keystone server config which Keystone uses to configure the cors package.
Alternatively, the solution of proxying API requests via the Next app should also work. It's not obvious to me why your proxying "workaround" is experiencing problems.

Why can't I see my (localhost) cookie being stored in Electron app?

I have an Angular app using Electron as the desktop wrapper. And there's a separate Django backend which provides HTTP APIs to the Electron client.
So normally when I call the login API the response header will have a Set-Cookie field containing the sessionId. And I can clearly see that sessionId in Postman, however, I can't see this cookie in my Angular app (Dev tools of Electron).
After some further debugging I noticed a warning sign beside my Set-Cookie in dev tools. It said that the cookie is blocked due to the SameSite being set to Lax. So I found a way to modify the server code to return a None samesite (together with a Secure property; I'm using HTTP):
# settings.py
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = 'None'
which did work (and the warning sign is gone) but the cookie is still not visible.
So what's the problem here? Why can't (and How can) I see that cookie so as to make sure that the login works in the actual client, not just Postman?
(btw, now both ends are being developed in localhost.)
There's no need to worry. A good way to check if it works is to actually make a request that requires login (after the API has been Postman tested) and see if the desired data are returned. If so, you are good to go (especially when the warning is gone).
If the sessionId cookie is saved it should automatically be included in the request. Unless there's something wrong with the cookie's path; but a / path would be fine.
Why is the cookie not visible: it's probably due to the separation of front and back ends. In Electron, the pages are typically some local HTML files, as one common step during configuration is to probably modify loadURL or something like that in main.js, for instance:
mainWindow.loadURL(`file://${__dirname}/dist/your-project/index.html`);
So the "site" you are accessing from Electron can be considered as local filesystem (which has no domain and hence no cookie at all), and you should see an empty file:// entry in dev tools -> application -> storage -> cookie. It doesn't mean a local path containing all cookies of the Electron app. Although your backend may be on the same local machine, you are accessing as http:// instead of file:// so the browser (Electron) will treat it as an actual web server.
Therefore, your cookies should be stored in another entry like http(s)://localhost and you can't see it in Electron. (Note that the same cookie will work in both HTTP and HTTPS)
If you use Chrome instead to test, you may be able to see it in all cookies. In some cases where the frontend and backend are deployed to the same host you may see the cookie in dev tools. But I guess there're always some reasons why you need Electron to create a desktop app (e.g. Python scripts).
Further reading
Using HTTPS
Although moving to HTTPS does not necessarily solve the original problem, it may be worth doing in order to prevent potential problems and get ready for the publish.
In your case, for the backend, you can use django-sslserver as a temporary solution before getting your SSL, but it uses a self-signed certificate and may make your frontend complain.
To fix this, consider adding the following code to the main process:
# const { app } = require('electron');
if (!app.isPackaged) {
app.commandLine.appendSwitch('ignore-certificate-errors');
}
Now it provides a good way to distinguish between development (unpacked) and production (packed) and only disables certificate check in development in order to make the code work.
Assuming that SESSION_COOKIE_SECURE in your config refers to cookie's secure flag, You ll have to set
SESSION_COOKIE_SECURE = False
because if this flag is set to True the browser will allow this cookie to be set only if you are using an https connection.
PS: This is just for your localhost. Hopefully you ll be using an Https connection in other environments.

Redirecting API requests in Django Rest Framework

I have a two-layer backend architecture:
a "front" server, which serves web clients. This server's codebase is shared with a 3rd party developer
a "back" server, which holds top-secret-proprietary-kick-ass-algorithms, and has a single endpoint to do its calculation
When a client sends a request to a specific endpoint in the "front" server, the server should pass the request to the "back" server. The back server then crunches some numbers, and returns the result.
One way of achieving it is to use the requests library. A simpler way would be to have the "front" server simply redirect the request to the "back" server. I'm using DRF throughout both servers.
Is redirecting an ajax request possible using DRF?
You don't even need the DRF to add a redirection to urlconf. All you need to redirect is a simple rule:
urlconf = [
url("^secret-computation/$",
RedirectView.as_view(url=settings.BACKEND_SECRET_COMPUTATION_URL))),
url("^", include(your_drf_router.urls)),
]
Of course, you may extend this to a proper DRF view, register it with the DRF's router (instead of directly adding url to urlconf), etc etc - but there isn't much sense in doing so to just return a redirect response.
However, the code above would only work for GET requests. You may subclass HttpResponseRedirect to return HTTP 307 (replacing RedirectView with your own simple view class or function), and depending on your clients, things may or may not work. If your clients are web browsers and those may include IE9 (or worse) then 307 won't help.
So, unless your clients are known to be all well-behaving (and on non-hostile networks without any weird way-too-smart proxies - you'll never believe what kinds of insanity those may do to HTTP requests), I'd suggest to actually proxy the request.
Proxying can be done either in Django - write a GenericViewSet subclass that uses requests library - or by using something in front of it, e.g. nginx or Caddy (or any other HTTP server/load balancer that you know best).
For production purposes, as you probably have a fronting webserver, I suggest to use that. This would save implementation time and also a little bit of server resources, as your "front" Django project won't even have to handle the request and keep the worker busy as it waits for the response.
For development purposes, your options may vary. If you use bare runserver then a proxy view may be your best option. If you use e.g. Docker, you may just throw in an HTTP server container in front of your Django container.
For example, I currently have a two-project setup (legacy Django 1.6 project and newer Django 1.11 project, sharing the same database) and a Caddy server in front of those, routing on per-URL basis. With a simple 9-line Caddyfile things just work:
:80
tls off
log / stdout "{common}"
proxy /foo project1:8000 {
transparent
}
proxy / project2:8000 {
transparent
}
(This is a development-mode config.) If you can have something similar, then, I guess, that would be the simplest option.

Django + CloudFront caching without HTTP_REFERER

We use django 1.8. on backend with default authorization that runs on elasticbeanstalk and AWS CloudFront to cache app pages on CDN level. The problem is that we want to whitelist as few http headers as possible to minimize keys amount and maximize hit rate. One of the headers that varies a lot and we would prefer to exclude is HTTP_REFERER. On business level we are fine with resolving referer through js by using document.referer and that sending it to server with ajax calls.
The problem: csrf login, registration and other default django authentification apps require HTTP_REFERER when used on website on https protocol.
One of the solutions I found is to move all auth pages to separate behaviours and proxy all headers there. Are there any other ways to make such set up work?
I don't think there's another solution then the one you mention, use a separate behavior.
I usually use rest api calls from frontend to backend so I don't need the Referer header there.
Then add an extra behavior for /admin/ where I do forward Referer.

Transition users from unmatched route with HistoryLocation

With the new implementation of HistoryLocation in 1.5.1, the rootURL parameter now requires a trailing slash. This is now causing issues for users that are trying to access the application without a trailing slash as ember thinks that it cannot find a route.
For example my application is located at /path/to/app, this is what existing users have bookmarked and is linked by default from our serverside framework (which creates in app urls with trimmed trailing slashes).
Is there a way to either,
1.Allow no trailing slash in the HistoryLocation rootURL
2. Transition the users using the router when trying to access the app without a trailing slash?
From my server I have a window.rootURL with the application root URL without a trailing slash(there are dynamic segments based on the accessed resource).
In my router I have the following:
App.Router.reopen({
location: 'auto',
rootURL: window.rootURL + '/'
});
I could run something like
if (location.pathName == window.rootURL) {
window.location.assign(window.rootURL + '/');
}
But this would force a reload of the page which seems like it shouldn't be needed since I already have a full Ember App running.
Since this application shares components with a few Ember applications we have in our code base and there are dynamic segments in the rootURL, creating routes just to redirect wouldn't make much sense (at least IMO).
So, what would be the best solution for this problem?
Without a ton of thought, you could use replaceState which wouldn't reload the page, but would update the url to hit your ember app.
window.history.replaceState({},"", pathWithSlash);