I use Graphql subscriptions with Apollo client on a Vue3 app using Django graphQL Channels and DjangoGraphqlJWT packages in my backend app.
I'm trying to pass a JWT token on the Apollo subscriptions via the connectionParams.
Following this solution. I implemented a Middleware. However Apollo is passing the connectionParams as a payload. I can't find a way to access the payload at the Middleware level, but only on the consumer.
I could access the query string property from the scope argument in the middleware. However, I can't find a way to pass a query argument after the subscription is initiated.
CLIENT SIDE:
import { setContext } from "apollo-link-context";
import { Storage } from "#capacitor/storage";
import {
ApolloClient,
createHttpLink,
InMemoryCache,
split,
} from "#apollo/client/core";
import { getMainDefinition } from "#apollo/client/utilities";
import { WebSocketLink } from "#apollo/client/link/ws";
const authLink = setContext(async (_: any, { headers }: any) => {
const { value: authStr } = await Storage.get({ key: "auth" });
let token;
if (authStr) {
const auth = JSON.parse(authStr);
token = auth.token;
}
// return the headers to the context so HTTP link can read them
return {
headers: {
...headers,
authorization: token ? `JWT ${token}` : null,
},
};
});
const httpLink = createHttpLink({
uri: process.env.VUE_APP_GRAPHQL_URL || "http://0.0.0.0:8000/graphql",
});
const wsLink = new WebSocketLink({
uri: process.env.VUE_APP_WS_GRAPHQL_URL || "ws://0.0.0.0:8000/ws/graphql/",
options: {
reconnect: true,
connectionParams: async () => {
const { value: authStr } = await Storage.get({ key: "auth" });
let token;
if (authStr) {
const auth = JSON.parse(authStr);
token = auth.token;
console.log(token); // So far so good the token is logged.
return {
token: token,
};
}
return {};
},
},
});
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
httpLink
);
const cache = new InMemoryCache();
export default new ApolloClient({
// #ts-ignore
link: authLink.concat(link),
cache,
});
BACKEND:
asgy.py
from tinga.routing import MyGraphqlWsConsumer
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from tinga.channels_middleware import JwtAuthMiddlewareStack
import os
from django.core.asgi import get_asgi_application
from django.conf.urls import url
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tinga.settings')
application = get_asgi_application()
# import websockets.routing
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": JwtAuthMiddlewareStack(
URLRouter([
url(r"^ws/graphql/$", MyGraphqlWsConsumer.as_asgi()),
])
),
})
channels_middleware.py
#database_sync_to_async
def get_user(email):
try:
user = User.objects.get(email=email)
return user
except User.DoesNotExist:
return AnonymousUser()
class JwtAuthMiddleware(BaseMiddleware):
def __init__(self, inner):
self.inner = inner
async def __call__(self, scope, receive, send):
# Close old database connections to prevent usage of timed out connections
close_old_connections()
# Either find a way to get the payload from Apollo in order to get the token.
# OR
# Pass pass the token in query string in apollo when subscription is initiated.
# print(scope) # query_string, headers, etc.
# Get the token
# decoded_data = jwt_decode(payload['token'])
# scope["user"] = await get_user(email=decoded_data['email'])
return await super().__call__(scope, receive, send)
def JwtAuthMiddlewareStack(inner):
return JwtAuthMiddleware(AuthMiddlewareStack(inner))
As far as I understand, I can only access query string / URL params in the Middleware and not the Apollo payload. Would it be possible to pass the token for now in the query string? However since the token might not exist when Apollo client is provided, it needs to be reevaluated like the connectionParams.
Any workaround?
I managed to get the token in the consumer payload and inject the user into the context.
from tinga.schema import schema
import channels_graphql_ws
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from graphql_jwt.utils import jwt_decode
from core.models import User
from channels_graphql_ws.scope_as_context import ScopeAsContext
#database_sync_to_async
def get_user(email):
try:
user = User.objects.get(email=email)
return user
except User.DoesNotExist:
return AnonymousUser()
class MyGraphqlWsConsumer(channels_graphql_ws.GraphqlWsConsumer):
"""Channels WebSocket consumer which provides GraphQL API."""
schema = schema
# Uncomment to send keepalive message every 42 seconds.
# send_keepalive_every = 42
# Uncomment to process requests sequentially (useful for tests).
# strict_ordering = True
async def on_connect(self, payload):
"""New client connection handler."""
# You can `raise` from here to reject the connection.
print("New client connected!")
# Create object-like context (like in `Query` or `Mutation`)
# from the dict-like one provided by the Channels.
context = ScopeAsContext(self.scope)
if 'token' in payload:
# Decode the token
decoded_data = jwt_decode(payload['token'])
# Inject the user
context.user = await get_user(email=decoded_data['email'])
else:
context.user = AnonymousUser
And then passing the token in the connectionParams
const wsLink = new WebSocketLink({
uri: process.env.VUE_APP_WS_GRAPHQL_URL || "ws://0.0.0.0:8000/ws/graphql/",
options: {
reconnect: true,
connectionParams: async () => {
const { value: authStr } = await Storage.get({ key: "auth" });
let token;
if (authStr) {
const auth = JSON.parse(authStr);
token = auth.token;
console.log(token); // So far so good the token is logged.
return {
token: token,
};
}
return {};
},
},
});
Related
I want to post a HTTP request like this:
http://localhost/context/{{name}}/{{age}}
And I want to bind these path variables to request body, if my request body is :
{
"name": "Frank",
"age": 18
}
the final request I want to send is:
http://localhost/context/Frank/18
so how to achieve this function in POSTMAN?
postman request
Provisioning your request in Postman (non-parametric url):
Parametric url
I don't think you need to pass variables in your route, since you're already passing them in the request-body. However, here's a brief.
If you're working with NodeJS (using Express) and any JS library, you can send the request as thus, (using axios):
const body = {
"name": "Frank",
"age": 18
}
const requestHandler = async() => {
const serverResponse = await axios.post(`http://localhost/context/${body.name}/${body.age}`, {data: body}, {
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${backend-token}`
}
};
Then, on your server-side, with a proper definition for your routes (for parametric and non-paramatric choice of url), you'd create a route to handle the request as:
Using Express
import {Router} from "express";
const yourServerRouter = Router();
yourServerRouter.post(`yourPrimaryDomain/:name/:age`, function-to-handle-request)
If you're working with a python framework (Flask or Django), you can do thus:
Flask (using flask_restful):
urls = [f"your_base_url/name/age"]
from flask_restful import Resource, request
import json
class BioData(Resource):
def post(self):
url = request.url
if "context" in url:
request_body = json.loads(request.data)
response = self.context_handler(request_data)
def context_handler(self, request_data):
name, age = request_data
....
....
flask_app = Flask(__name__)
flask_app.add_resource(BioData, *urls)
Django (Using DRF - viewsets)
from rest_framework import viewsets, routers
class BioDataViewsets(viewsets.ModelViewSets):
#action(methods=["POST"], detail=False, url_name="context", url_path="context")
def context(self, *args, **kwargs):
clients_request = json.loads(self.request.body)
**define your path as above (for flask)**
context_router = routers.DefaultRouter()
context_router.register("name/age/", company_viewsets.CompanyViewSets)
url_patterns = [path(f"your_base_url/context", include(context_router.urls())]
Eventually I got some clues from this page. The request body can be parsed through Pre-request-Script, and the attributes of interest can be set as variables and referenced in URL.
var r = JSON.parse(request.data);
pm.variables.set("name", r.name);
pm.variables.set("age", r.age);
And use below form to apply variables set in the Pre-request-Script:
http://localhost/context/{{name}}/{{age}}
the request body is :
{
"name": "Frank",
"age": 18
}
postman request
I am failing to send over my personal banking data via a flask webhook from the Nordigen API to Dialogflow via fulfilment as only null is being received within the Dialogflow payload:
{
"fulfillmentText": "Your text response",
"fulfillmentMessages": [
{
"payload": [
null
]
}
]
}
The webhook error message is: Webhook call failed. Error: Failed to parse webhook JSON response: Expect a map object but found: [null].
When I just send the data as a fulfillmentText I receive "fulfillmentText": null.
I have tested my webhook with postman and there - as well as locally and other webhook'esque tests - everything is fine as I receive my most recent banking data.
The overall flow is twofold and simple:
User gets the correct banking and user specific login link to a specified bank, copy & pastes it to perform banking login by query_text = 'login'.
After a successful banking login the user can fetch different kinds of banking data (like balance) by query_text = 'balance'.
I went crazy with overengineering the flask webhook as I tried out many different things like asynchronous functions, uploading my Flask app to Heroku or CORS. I have even implemented an OAuth2 process where the user would query_text = 'google auth' and initiate the OAuth2 process in a step 0) by creating OAuth2 credentials and the Flask-Dance Python package. (Even though I have hardcoded the OAuth2 redirect link but this shouldn't be an issue atm). I was even trying to trick Dialogflow by creating a small Sqlite3 db within my webhook to at least upload the data there and then use it but without success.
So my question is .. what am I missing here? Why do I receive my banking data everywhere else but not in Dialogflow. My intuition is telling me Google is blocking this data for whatever reason.
Honestly I just don't know how to continue and I would appreciate any helpful comments!
This is my Flask webhook:
from dialogflow_fulfillment import QuickReplies, WebhookClient, Payload
from flask import Flask, request, jsonify, make_response, session, render_template, redirect, url_for
from flask_cors import CORS, cross_origin
import json
from json import JSONEncoder
import os
import asyncio
import requests
import sqlite3
from app.src.handler_login import handler_login
from app.src.handler_balance import handler_balance
from app.banking_data.init_db import create_connection
from flask_dance.contrib.google import make_google_blueprint, google
from oauthlib.oauth2.rfc6749.errors import InvalidGrantError, TokenExpiredError, OAuth2Error
from google.cloud import dialogflow_v2beta1 as dialogflow
from google.oauth2 import service_account
from uuid import uuid4
from nordigen import NordigenClient
# NORDIGEN
# Credentials
secret_id="XXX"
secret_key="XXX"
# Configuration
institution_id = "XXX"
app = Flask(__name__)
# set Flask secret key
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "supersekrit")
# GOOGLE API & AUTHENTICATION
app.config["GOOGLE_OAUTH_CLIENT_ID"] = "XXX"
app.config["GOOGLE_OAUTH_CLIENT_SECRET"] = "XXX"
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = "1"
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = "1"
google_bp = make_google_blueprint(scope=["profile", "email"])
app.register_blueprint(google_bp, url_prefix="/login")
app.config['CORS_HEADERS'] = 'Content-Type'
cors = CORS(app, supports_credentials=True, resources={r"/webhook": {"origins": "*"}})
client = NordigenClient(
secret_id=secret_id,
secret_key=secret_key
)
client.generate_token()
# subclass JSONEncoder
class setEncoder(JSONEncoder):
def default(self, obj):
return list(obj)
#app.route("/")
def index():
if not google.authorized:
return redirect(url_for("google.login"))
try:
resp = google.get("/oauth2/v1/userinfo")
assert resp.ok, resp.text
return "You are {email} on Google".format(email=resp.json()["email"])
except (InvalidGrantError, TokenExpiredError) as e: # or maybe any OAuth2Error
return redirect(url_for("google.login"))
#app.route('/webhook', methods=['GET', 'POST', 'OPTION'])
async def webhook():
"""Handles webhook requests from Dialogflow."""
req = request.get_json(force=True)
query_text = req.get('queryResult').get('queryText')
if query_text:
if query_text == 'google auth':
if not google.authorized:
auth_link = 'MY HARD CODED GOOGLE AUTHENTICATION LINK HERE'
auth_link = {
"fulfillmentText": auth_link,
"source": 'webhook'
}
return auth_link
try:
resp = google.get("/oauth2/v1/userinfo")
assert resp.ok, resp.text
return "You are {email} on Google".format(email=resp.json()["email"])
except (InvalidGrantError, TokenExpiredError) as e: # or maybe any OAuth2Error
auth_link = 'MY HARD CODED GOOGLE AUTHENTICATION LINK HERE'
auth_link = {
"fulfillmentText": auth_link,
"source": 'webhook'
}
return auth_link
if query_text == 'login':
link = await handler_login(client, institution_id, session)
link = {
"fulfillmentText": link,
"source": 'webhook'
}
link = make_response(jsonify(link))
link.headers.add('Access-Control-Allow-Origin', '*')
return link
if query_text == 'balance':
balance = await handler_balance(client, session)
balance = {
"fulfillmentText": "Your text response",
"fulfillmentMessages": [
{
"text": {
"text": [
"Your text response"
]
}
},
{
"payload": {
balance
}
}
]
}
balance = json.dumps(balance, indent=4, cls=setEncoder)
balance = make_response(balance)
return balance
if __name__ == "__main__":
app.run(debug=True)
Here are two helper functions I have created that perform the creation of the login link the the fetching of my banking data via Nordigen:
from uuid import uuid4
async def handler_login(client, institution_id, session):
"""Handles the webhook request."""
# Initialize bank session
init = client.initialize_session(
# institution id
institution_id=institution_id,
# redirect url after successful authentication
redirect_uri="https://nordigen.com",
# additional layer of unique ID defined by you
reference_id=str(uuid4())
)
link = init.link
session["req_id"] = init.requisition_id
return link
async def handler_balance(client, session):
if "req_id" in session:
# Get account id after you have completed authorization with a bank
# requisition_id can be gathered from initialize_session response
#requisition_id = init.requisition_id
accounts = client.requisition.get_requisition_by_id(
requisition_id=session["req_id"]
)
# Get account id from the list.
account_id = accounts["accounts"][0]
#account_id = accounts["id"]
# Create account instance and provide your account id from previous step
account = client.account_api(id=account_id)
# Fetch account metadata
#meta_data = account.get_metadata()
# Fetch details
#details = account.get_details()
# Fetch balances
balance = account.get_balances()
balance = balance["balances"][0]
balance = balance["balanceAmount"]["amount"]
#balance = json.loads(balance)
# Fetch transactions
#transactions = account.get_transactions()
#agent.add(Payload({'balance': balance}))
return balance
Feel free to comment if you need any more input!
I am building a frontend with Vue cli and backend with Django with Graphene. The mutation works fine but not the queries.
When I run the same query from GraphiQL, works fine.
Frontend
#vue/cli 4.1.2
vue-apollo 3.0.2
Backend
python 3.8
django 3.0.2
graphene-django 2.8.0
django-graphql-jwt 0.3.0
queries.js
import gql from 'graphql-tag'
export const ME_QUERY = gql`
query me {
me {
username
is_active
}
}
`
Home.vue
<script>
import { ME_QUERY } from '#/graphql/queries'
export default {
name: 'home',
async mounted () {
await this.$apollo
.query({
query: ME_QUERY
})
.then((result) => {
console.log(result)
})
.catch(({ graphQLErrors }) => {
graphQLErrors.map(({ message, locations, path }) => console.log(`Error Message: ${message}, Location: ${locations}, Path: ${path}`))
})
}
}
</script>
schema.py
from django.contrib.auth import authenticate, login, get_user_model
import graphene
from graphene_django import DjangoObjectType
import graphql_jwt
from graphql_jwt.decorators import jwt_cookie
class UserType(DjangoObjectType):
class Meta:
model = get_user_model()
class ObtainJSONWebToken(graphql_jwt.JSONWebTokenMutation):
user = graphene.Field(UserType)
#classmethod
def resolve(cls, root, info, **kwargs):
return cls(user=info.context.user)
class Query(graphene.ObjectType):
me = graphene.Field(UserType)
def resolve_me(root, info):
user = info.context.user
if user.is_anonymous:
raise Exception('Authentication failure!!')
return user
class Mutation(graphene.ObjectType):
# token_auth = graphql_jwt.ObtainJSONWebToken.Field()
verify_token = graphql_jwt.Verify.Field()
refresh_token = graphql_jwt.Refresh.Field()
revoke_token = graphql_jwt.Revoke.Field()
log_in = ObtainJSONWebToken.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
Thanks for advance
after to install the extension Graphql in Firefox and found my mistake.
{
"errors": [
{
"message": "Cannot query field \"is_active\" on type \"UserType\". Did you mean \"isActive\"?",
"locations": [
{
"line": 4,
"column": 5
}
]
}
]
}
Changing to isActive the query worked fine.
The issue is while requesting for token, we are getting the "Invalid Grant" error (Response - 400).
Please find attached the python code which we are using for the same.
We also tried same with postman and getting the same error message.
We also added callback url on docusign panel
Please see below the code :-
import json
import requests
from django.core.mail import send_mail
from django.shortcuts import render, redirect
from django.http import HttpResponse, HttpResponseRedirect
import base64
from .utils import return_csv_values, np, get_current_day_month, return_csv_values_application
from .process_docments import embedded_signing_ceremony_contract, embedded_signing_ceremony_application
import pandas as pd
from .models import DocumentSigned
CLIENT_AUTH_ID = 'my integration id'
CLIENT_SECRET_ID = 'my secret id'
# Create your views here.
def get_access_token(request):
base_url = "https://account-d.docusign.com/oauth/auth"
auth_url = "{0}?response_type=code&scope=signature click.manage organization_read permission_read dtr.documents.read&client_id={1}&redirect_uri={2}" \
.format(base_url, CLIENT_AUTH_ID, "http://127.0.0.1:8000/auth_login")
# print(request.build_absolute_uri())
return HttpResponseRedirect(auth_url)
#callback url
def auth_login(request):
access_code = request.GET['code']
# return HttpResponse(access_code)
base_url = "https://account-d.docusign.com/oauth/token"
auth_code_string = '{0}:{1}'.format(CLIENT_AUTH_ID, CLIENT_SECRET_ID)
print(auth_code_string)
auth_token = base64.b64encode(auth_code_string.encode('utf-8'))
auth_token = auth_token.decode("utf-8")
print(auth_token)
req_headers = {"Authorization": "Basic {0}".format(auth_token), "Content-Type": "application/x-www-form-urlencoded"}
post_data = {'grant_type': 'authorization_code', 'code': access_code}
try:
r = requests.post(base_url, data=post_data, headers=req_headers)
print(r)
return HttpResponse(json.dumps(r))
except Exception as e:
print(str(e))
return HttpResponse(str(e))
this is the code I use, note a few differences from what you're doing:
string endpoint = string.Format("{0}/oauth/token", accountServerUrl);
var values = new Dictionary<string, string>();
values.Add("grant_type", "authorization_code");
values.Add("code", token);
if (redirectUri != null)
values.Add("redirect_uri", redirectUri);
var content = new FormUrlEncodedContent(values);
var auth = string.Format("{0}:{1}", clientId, clientSecret);
var bytes = System.Text.Encoding.ASCII.GetBytes(auth);
var encodedAuth = Convert.ToBase64String(bytes);
httpClient.DefaultRequestHeaders.Add("Authorization", string.Format("Basic {0}", encodedAuth));
I use vue-cli-plugin-apollo and I want to send language chosen by user from frontend to backend via cookie.
As a vue-apollo.js I use the next template
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client'
// Install the vue plugin
Vue.use(VueApollo)
// Name of the localStorage item
const AUTH_TOKEN = 'apollo-token'
// Http endpoint
const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:4000/graphql'
// Files URL root
export const filesRoot = process.env.VUE_APP_FILES_ROOT || httpEndpoint.substr(0, httpEndpoint.indexOf('/graphql'))
Vue.prototype.$filesRoot = filesRoot
// Config
const defaultOptions = {
// You can use `https` for secure connection (recommended in production)
httpEndpoint,
// You can use `wss` for secure connection (recommended in production)
// Use `null` to disable subscriptions
wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4000/graphql',
// LocalStorage token
tokenName: AUTH_TOKEN,
// Enable Automatic Query persisting with Apollo Engine
persisting: false,
// Use websockets for everything (no HTTP)
// You need to pass a `wsEndpoint` for this to work
websocketsOnly: false,
// Is being rendered on the server?
ssr: false,
// Override default apollo link
// note: don't override httpLink here, specify httpLink options in the
// httpLinkOptions property of defaultOptions.
// link: myLink
// Override default cache
// cache: myCache
// Override the way the Authorization header is set
// getAuth: (tokenName) => ...
// Additional ApolloClient options
// apollo: { ... }
// Client local data (see apollo-link-state)
// clientState: { resolvers: { ... }, defaults: { ... } }
}
// Call this in the Vue app file
export function createProvider (options = {}) {
// Create apollo client
const { apolloClient, wsClient } = createApolloClient({
...defaultOptions,
...options,
})
apolloClient.wsClient = wsClient
// Create vue apollo provider
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
defaultOptions: {
$query: {
// fetchPolicy: 'cache-and-network',
},
},
errorHandler (error) {
// eslint-disable-next-line no-console
console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message)
},
})
return apolloProvider
}
taken from here. All options are shown here.
I have seen in different github discussions that cookies must be placed inside headers, for example here. Then I found, that apollo-link-http has headers option, so at the end I tried different variations of ...:
httpLinkOptions: {
headers: {
// Tried something like:
cookie[s]: 'language=en; path=/;'
// and something like:
cookie[s]: {
language: 'en'
}
}
}
but no luck.
In case of cookieS I receive Error: Network error: Failed to fetch.
In case of cookie, request is sent without issues, but backend does not see language cookie.
I double-checked backend using Postman and in this case backend receives request with manually added language cookie.
Could anyone help me?
Found solution.
FRONT END SETTINGS
Create cookie:
export function languageCookieSet (lang) {
document.cookie = `language=${lang}; path=/;`
}
Add httpLinkOptions to defaultOptions of vue-apollo.js.
const defaultOptions = {
...
httpLinkOptions: {
credentials: 'include'
},
...
BACKEND SETTINGS
As a backend I use Django (currently v2.2.7).
For development we need to use django-cors-headers
My development.py now looks like:
from .production import *
CORS_ORIGIN_WHITELIST = (
'http://localhost:8080',
)
CORS_ALLOW_CREDENTIALS = True
INSTALLED_APPS += ['corsheaders']
MIDDLEWARE.insert(0, 'corsheaders.middleware.CorsMiddleware')
Add to production.py:
LANGUAGE_COOKIE_NAME = 'language'
The default value of LANGUAGE_COOKIE_NAME is django_language, so if it is suitable for you, change
document.cookie = `language=${lang}; path=/;`
to
document.cookie = `django_language=${lang}; path=/;`
Now in backend we can get frontend language:
import graphene
from django.contrib.auth import get_user_model
from django.utils.translation import gettext as _
from .views import user_activation__create_email_confirmation
User = get_user_model()
class UserRegister(graphene.Mutation):
"""
mutation {
userRegister(email: "test#domain.com", password: "TestPass") {
msg
}
}
"""
msg = graphene.String()
class Arguments:
email = graphene.String(required=True)
password = graphene.String(required=True)
def mutate(self, info, email, password):
request = info.context
# Here we get either language from our cookie or from
# header's "Accept-Language" added by Browser (taken
# from its settings)
lang = request.LANGUAGE_CODE
print('lang:', lang)
if User.objects.filter(email=email).exists():
# In our case Django translates this string based
# on the cookie's value (the same as "request.LANGUAGE_CODE")
# Details: https://docs.djangoproject.com/en/2.2/topics/i18n/translation/
msg = _('Email is already taken')
else:
msg = _('Activation link has been sent to your email.')
user = User(email=email)
user.set_password(password)
user.save()
user_activation__create_email_confirmation(info.context, user)
return UserRegister(msg=msg)
Note: I have not tested yet these changes in production, but in production I use just one server where frontend and backend live behind nGinx and this is the reason why CORS settings live in development.py instead of production.py. Also in production credentials: 'include' possibly could be changed to credentials: 'same-origin' (ie more strict).