Vue.js-Electron application with an Apollo client does not receive cookies from a remote API server - nginx

I try to build an application with a GraphQL backend (based on Node.js and graphql-yoga). The server is hosted on a Linux machine with nginx as a reverse proxy. The proxy configuration is listed below.
server {
listen 443 ssl;
listen [::]:443 ssl;
include snippets/self-signed.conf;
include snippets/ssl-params.conf;
server_name url.example.com;
charset utf-8;
# Always serve index.html for any request
location / {
root /srv/app/public;
try_files $uri /index.html;
}
location /graphql {
allow all;
proxy_pass http://localhost:3000$request_uri;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location /playground {
# Additional configuration
}
location /subscriptions {
# Additional configuration
}
error_log /var/log/nginx/vue-app-error.log;
access_log /var/log/nginx/vue-app-access.log;
}
server {
listen 80;
listen [::]:80;
server_name url.example.com;
return 302 https://$server_name$request_uri;
To avoid CORS problems, I had to add the following entry in my servers configuration and it works.
const options = {
[
...
]
/**
* Cross-Origin Resource Sharing (CORS)
*/
cors: {
credentials: true,
origin: ['http://localhost:8080', 'https://url.example.com'], // frontend url
},
};
The authentication is based on JWT tokens packed into http-only cookies for access and refresh tokens. This is working as expected when I access my server with a browser and the cookies are shown in the developer tools. Within the AuthData, I additionally provide the username and a JWT token as a return value.
{
"data":{
"login":{
"user":"admin",
"name":" Admin",
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJhZG1pbiIsIm5hbWUiOiIgQWRtaW5pc3RyYXRvciIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJpYXQiOjE2NDg0ODM2MDcsImV4cCI6MTY0ODQ4NzIwN30.Dol41LStkscXrlGn3GJotf83k_d2EImyvDU68Dg8Bvw"
}
},
"loading":false,
"networkStatus":7
}
Now I wanted to play around with Electron, because some use cases of the application would be much easier. The stack is Vue.js and Electron with Tailwind and the Apollo client (I use the Apollo UploadClient here) as listed below.
import {
ApolloClient,
ApolloLink,
DefaultOptions,
InMemoryCache,
} from '#apollo/client/core';
import { setContext } from '#apollo/client/link/context';
import { createUploadLink } from 'apollo-upload-client';
const backendServer = 'https://url.example.com/graphql';
const httpLink = createUploadLink({
uri: backendServer,
credentials: 'include',
});
// Cache implementation
const cache = new InMemoryCache({
addTypename: false,
}); //
const defaultOptions = {
watchQuery: {
fetchPolicy: 'no-cache',
},
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'all',
},
mutate: {
errorPolicy: 'all',
},
} as DefaultOptions;
const authLink = setContext((_, { headers }) => ({
headers: {
...headers,
/**
* use a custom header tag to classify the application
* for authentication handling in the backend
*/
'app-type': 'web',
},
}));
const apolloClient = new ApolloClient({
link: ApolloLink.from([authLink, httpLink]),
cache,
defaultOptions,
resolvers: {},
});
export default apolloClient;
This client is bound to my Vue.js instance via #vue/apollo-composable. So I think it is only available in the renderer process. React examples I found on the Internet were built the same way.
Now the problem: When I run server and client (Electron) application on my development machine, everything works as expected, and I can see the cookies in the application tab in the developer tools. I also can access the API without issues.
When I now bind my ApolloClient to the remote API server I do not receive any cookies. From the logs I can see that I receive the AuthData like above from the server, but not the cookies. So any further request results in a custom error message "unauthenticated access" provided by my API server.
{
"loading":false,
"networkStatus":8,
"error":{
"graphQLErrors":[
{
"message":"user is not authenticated...redirect to the login page",
"locations":[
{"line":2,"column":3}
],
"path":[
"users"
]
}
],
"clientErrors":[],
"networkError":null,
"message":"user is not authenticated...redirect to the login page"
},"errors":[
{
"message":"user is not authenticated...redirect to the login page",
"locations":[
{"line":2,"column":3}
],
"path":[
"users"
]
}
]
}
What am I missing or doing wrong, or what else could I test?
Additional tested direct access
I additionally tested direct accessing the gql API from the electron client without the nginx proxy but still no success. I get the login correctly but I'm not able to see the cookies provided by the server.
any suggestions?

Related

CORS issue with Server sent event with Fastify-sse

I'm using a Fastify server to send SSE events to a React front-end.
While everything worked well locally, I'm having issues once deployed behind Nginx. The front-end and the server aren't on the same domain and although I set the cors origin to be "*" on the server, and the other call resolve without issue, for the server-sent-events endpoint only I get
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://example.com/events. (Reason: CORS request did not succeed). Status code: (null).
Here's how Fastify is configured, using #fastify/cors and fastify-sse-v2
import fastifyCors from "#fastify/cors";
import FastifySSEPlugin from "fastify-sse-v2";
// ...
await this.instance.register(FastifySSEPlugin, { logLevel: "debug" });
await this.instance.after();
await this.instance.register(fastifyCors, { origin: "*" });
Then sending events based on postgres pubsub with:
await pubsub.addChannel(TxUpdateChannel);
reply.sse(
(async function* () {
for await (const [event] of on(pubsub, TxUpdateChannel)) {
yield {
event: event.name,
data: JSON.stringify(event.data),
};
}
})()
);
On the front-end I use eventsource so that I can add Authorization headers:
import EventSource from "eventsource";
// ...
const source = new EventSource(`${SERVER_URL}/transaction/events`, {
headers: {
'Authorization': `Bearer ${jwt}`,
},
});
source.onmessage = (event) => {
console.log('got message', event)
getUserData()
}
source.onopen = (event) => {
console.log('---> open', event)
}
source.onerror = (event) => {
console.error('Event error', event)
}
The problem was in the nginx configuration. Thanks to EventSource / Server-Sent Events through Nginx I solved it using the following placed into the location of the nginx conf :
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;

Why is Nginx truncating the gRPC streaming response?

I've asked this question before but decided to delete that old question and reformulate it along with a minimum reproducible example. The issue is that when I deploy my gunicorn webserver on nginx, my streamed responses from my go server via gRPC get truncated. All details can be found in the repository. My nginx configuration for this site looks like this:
server {
listen 80 default_server;
server_name example.com;
location / {
#include proxy_params;
proxy_pass http://localhost:5000;
proxy_buffering off;
chunked_transfer_encoding off;
}
}
The code receiving and parsing the response on the front end looks like this:
<script>
(async function(){
const response = await fetch("{{ url_for('realtimedata') }}");
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
while (true) {
const {done, value} = await reader.read();
if (done) break;
try {
console.log('Received', value);
const rtd = JSON.parse(value);
console.log('Parsed', rtd);
} catch(err) {
console.log(err);
}
}
})()
</script>
Something to note regarding the data from the go server, one service is providing a data object with 96 fields and another service is providing data with 200 fields. Which makes the incoming stream response have variable length (in terms of bytes).
I want to use gunicorn because I may have multiple listeners at the same time. Using gunicorn solved an issue where all the responses were making it to the webserver but they were being distributed among the active clients. So each client would get a different response but not all of them.
EDIT:
I've tried changing the response object size on the goserver to be the same from both services but the truncating still happened. Having variable length doesn't seem to be the issue. I've also tried doing this with uWSGI instead of gunicorn and the issue persists. I even set uwsgi_buffering off; and the issue persists.
UPDATE:
I've ran the minimum reproducible example with Apache2 instead of Nginx and I'm getting the same issue. Maybe the issue is with something else.
Looking at your python code, it seems like pushing the data from the backend to the frontend would be done better with websockets. I've rewritten your backend to use FastAPI instead of Flask and modified the nginx configuration.
main.py
import asyncio
import dependencies.rpc_pb2 as r
import dependencies.rpc_pb2_grpc as rpc
from fastapi import FastAPI, WebSocket, Request
from fastapi.templating import Jinja2Templates
import grpc
import json
import os
os.environ["GRPC_SSL_CIPHER_SUITES"] = 'HIGH+ECDSA'
app = FastAPI()
templates = Jinja2Templates(directory="templates")
server_addr = "localhost"
server_port = 3567
#app.get("/")
def read_root(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
def parseRtd(rtd):
rtdDict = {}
rtdDict["source"] = rtd.source
rtdDict["is_scanning"] = rtd.is_scanning
rtdDict["timestamp"] = int(rtd.timestamp)
rtdDict["data"] = {}
for key, v in rtd.data.items():
rtdDict["data"][int(key)] = {"name": v.name, "value": v.value}
return rtdDict
def get_rtd():
channel = grpc.insecure_channel(f"{server_addr}:{server_port}")
stub = rpc.RpcServiceStub(channel)
for rtd in stub.SubscribeDataStream(r.SubscribeDataRequest()):
yield parseRtd(rtd)
#app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
await websocket.send_json({"test": "this is a test"})
it = get_rtd()
while True:
await asyncio.sleep(0.1)
payload = next(it)
await websocket.send_json(payload)
index.html
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.4.0/socket.io.js" integrity="sha512-nYuHvSAhY5lFZ4ixSViOwsEKFvlxHMU2NHts1ILuJgOS6ptUmAGt/0i5czIgMOahKZ6JN84YFDA+mCdky7dD8A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<script>
var ws = new WebSocket("ws://localhost:5000/ws");
ws.onopen = function () {
console.log("websocket was open");
};
ws.onclose = () => {
console.log("Websocket was closed!");
}
ws.onerror = (error) =>{
console.error("Websocket error: " + JSON.stringify(error));
};
ws.onmessage = (message) => {
console.log("MSG: " + message.data );
};
</script>
</body>
</html>
webserver.conf
server {
listen 80 default_server;
server_name example.com;
location / {
include proxy_params;
proxy_pass http://localhost:5000;
}
location /ws {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass http://localhost:5000;
}
}

getting net::ERR_CONNECTION_REFUSED error on production

I have deployed my react app which also has a backend. I have started both my front end and backend from my server using npm start command. The front end is working correctly but the backend is not. (Works perfectly from localhost)
The frontend is open from port 3000 and backend from port 9000.
This is my nginx config
server {
server_name myservername.com www.myservername.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
server {
if($host = www.myservername.com){
return 301 https://$host$request_uri;
}
if($host = myservername.com){
return 301 https://$host$request_uri;
}
listen 80 default_server;
listen [::]:80 default_server;
server_name myservername.com www.myservername.com;
return 404;
}
ports that are in use
The error i'm getting in console
POST http://localhost:9000/bot net::ERR_CONNECTION_REFUSED
How can i fix this issue?
The issue is trying to do this fetch
fetch("http://localhost:9000/bot", {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
}).then(res => res.text())
.then(res => {
res = JSON.parse(res);
this.setState({ apiResponse: res.response, apiIntent: res.intent}, () => {this.setBotMsg()});
console.log(`This is the response: ${res.response}, ${res.intent}`);
;
});
This will issue a call to your local loopback adapter on your computer port 9000. I don't think this is what you want. Use your server IP or domainname inseat of localhost.
fetch("http://myservername.com:9000/bot", {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
}).then(res => res.text())
.then(res => {
res = JSON.parse(res);
this.setState({ apiResponse: res.response, apiIntent: res.intent}, () => {this.setBotMsg()});
console.log(`This is the response: ${res.response}, ${res.intent}`);
;
});
I would use NGINX to proxy the request to your backend and not expose port 9000 directly.
fetch("https://myservername.com/bot", {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
}).then(res => res.text())
.then(res => {
res = JSON.parse(res);
this.setState({ apiResponse: res.response, apiIntent: res.intent}, () => {this.setBotMsg()});
console.log(`This is the response: ${res.response}, ${res.intent}`);
;
});
The nginx configuration depends on what you are want to do. Subdomain or location? A subdomain could be something like bot.myserver.com. A location myserver.com/bot.
server {
listen 443
.....
location /bot {
proxy_pass http://localhost:9000;
proxy_set_header Host $host;
....
}
}
Additional note: I have updated the configuration to use https from the frontend.

Nginx Reverse Proxy With SSL and Asp.NET Core API

I am currently trying to build an API for a client interface-server interaction. I have decided to use ASP.NET Core for the API with Nginx as the hosting platform (On Ubuntu 18.04). Since ASP.NET uses Kestrel, we have set up a reverse proxy to forward requests from Nginx to Kestrel-- what is hosting the API. We have SSL set up on the NGINX server, however it is not set up on the Kestrel Server.
Simply put, I do not know how to set up SSL on the Kestrel Server with another layer of SSL on the NGINX side. How can I do this?
Model:
Client --> GET Request over HTTPS --> NGINX with SSL --> HTTP Kestrel Server and vice versa
Output: SSL_PROTOCOL_ERROR
Temporary Solution: Use HTTP with port 5000 in the link.-- No error, however, data is not secure.
Optimal Solution: Use HTTPS without port 5000 in the link. Data is secure.
NGINX Config:
if ($host = api.OURSITENAME.co) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name api.OURSITENAME.co;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
include /etc/nginx/proxy_params;
server_name api.OURSITENAME.co;
access_log /var/log/nginx/api.access.log;
error_log /var/log/nginx/api.error.log error;
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/api.OURSITENAME.co/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/api.OURSITENAME.co/privkey.pem; # managed by Certbot
proxy_http_version 1.1;
proxy_cache_bypass $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header Upgrade $http_upgrade;
location / {
proxy_pass http://172.18.0.2:5000; <-- Docker Container. Can easily be switched out with localhost if we want to run on dotnet directly.
}
}
As I understand the problem, when you use HTTP to access your application directly on port 5000 you get an SSL error. Even though you don't use HTTPS.
If you have app.UseHsts(); and/or app.UseHttpsRedirection(); in your Startup code then it will use HTTPS.
If you are letting nginx handle the SSL then you can remove code from your app Startup.cs
Typical startup code:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else // Production
{
app.UseExceptionHandler("/Error");
// Remove to use HTTP only
app.UseHsts(); // HTTPS Strict mode
}
// Remove to use HTTP only
app.UseHttpsRedirection(); // Redirects HTTP to HTTPS
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc();
}
Documentation on enforcing SSL in dotnet core
This simply happens because Kestrel is not configured to handle HTTPS requests. A quick look at MS Docs shows you how.
You can use listenOptions and its various extensions to specify your SSL certificate and other configurations
webBuilder.ConfigureKestrel(serverOptions =>
{
serverOptions.ConfigureHttpsDefaults(listenOptions =>
{
// certificate is an X509Certificate2
listenOptions.ServerCertificate = certificate;
});
});
Also, if you used CreateDefaultBuilder from your Program.cs, then you can configure your SSL/HTTPS from appsettings.json because CreateDefaultBuilder calls Configure(context.Configuration.GetSection("Kestrel")) by default to load Kestrel configurations. A sample config file highlighting Kestrel config (from MS Docs) is shown below:
{
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://localhost:5000"
},
"HttpsInlineCertFile": {
"Url": "https://localhost:5001",
"Certificate": {
"Path": "<path to .pfx file>",
"Password": "<certificate password>"
}
},
"HttpsInlineCertStore": {
"Url": "https://localhost:5002",
"Certificate": {
"Subject": "<subject; required>",
"Store": "<certificate store; required>",
"Location": "<location; defaults to CurrentUser>",
"AllowInvalid": "<true or false; defaults to false>"
}
},
"HttpsDefaultCert": {
"Url": "https://localhost:5003"
},
"Https": {
"Url": "https://*:5004",
"Certificate": {
"Path": "<path to .pfx file>",
"Password": "<certificate password>"
}
}
},
"Certificates": {
"Default": {
"Path": "<path to .pfx file>",
"Password": "<certificate password>"
}
}
}
}
Make sure to visit the docs if you need more info.
Nevertheless, in my opinion, I don't see much advantage with a double layer of SSL protection. One downside I can immediately notice is some delay as a result of encryption/decryption. A single layer of SSL on your reverse proxy should be more than enough.

How i can update single filed of array in state in redux?

I need update one field in a state => books => (one indicated book) => (one of book field).
I try do this that
export default function(state = initialState, action){
case EDIT_BOOK:
return {
...state,
books :{
...state.books,
title: action.title
}
}
My state looks like
books:[
{
"_id": "5cfa9698361a8427b85dc79f",
"title": "Krzyżacy",
"author": "Henryk Sienkiewicz",
"__v": 0,
"state": "toReads"
},
{
"_id": "5cfa9bd1cb5c152ee4269a28",
"title": "Quo Vadis",
"author": "Henryk Sienkiewicz",
"state": "toReads",
"__v": 0
}
]
//Action
export const editBook = (id, title) => dispatch => {
axios
.put(`http://localhost:5000/books/${id}`, title)
.then(res => dispatch({
type: EDIT_BOOK,
payload: id,
newTitle: title
}))
My second problem is thus
Access to XMLHttpRequest at 'http://localhost:5000/books/5cfa9698361a8427b85dc79f' from origin 'http://localhost:3000' 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.
You are updating array incorrectly. See this pattern from redux documentation:
https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns#updating-an-item-in-an-array
Be carefull and do not change your books from array into object (use output of function updateObjectInArray from above link and put it into your books property)
case EDIT_BOOK:
return {
...state,
// here you are putting object instead of array into books
books :{
...state.books,
title: action.title
}
}
Solution for your second problem is using e.g. nginx in your local development environment for passing requests to your frontend application and backend api to the right address/port.
Nginx configuration will look something like this (simplified, just for giving a right direction):
server {
# port where nginx will run (you will debug your web application on this port from now)
listen 8081;
location / {
# address of your frontend application (e.g. webpack dev server)
proxy_pass http://127.0.0.1:3000;
}
location /backend {
# address of your backennd application
proxy_pass http://127.0.0.1:5000;
}
}
As I wrote above, after that configuration you need to debug application on nginx proxy and not on frontend server (e.g. webpack dev server), but the server should be still running. The nginx proxy application url according to the above configuration will be: http://localhost:8081
And whenever your application would like to call backend api, it will do it this way:
// your old address will change into ...
http://localhost:5000/books/${id}
// ... this new address
/backend/books/${id}

Resources