My Setup
I have a server with a REST API that runs on Symfony with API Platform. The GET requests for my resources do not require authorization, however the other operations do. Authorization is handled with a JWT Bearer token.
The client uses React-admin with API Platform Admin. I added this code to send the JWT token along with the operations:
// dataProvider.js
import React from "react";
import { hydraDataProvider, fetchHydra as baseFetchHydra } from "#api-platform/admin";
export default entrypoint => {
const fetchHeaders = { Authorization: `Bearer ${localStorage.getItem("token")}` };
const fetchHydra = (url, options = {}) => baseFetchHydra(url, {
...options,
headers: new Headers(fetchHeaders),
});
return hydraDataProvider(entrypoint, fetchHydra);
};
The Problem
When I log in to my admin interface now, I get a 401 Unauthorized response, because the server did not expect a token for a GET request.
Request Headers:
Host: localhost:8000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0
Accept: application/ld+json
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost:3000/
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1ODQ2NzcwMTYsImV4cCI6MTU4NDY4MDYxNiwicm9sZXMiOlsiUk9MRV9BRE1JTiJdLCJ1c2VybmFtZSI6IlNvbWVib2R5In0.O_StagfEJy5VQS-5s-DjuwzOlUgrl3MTmxPfZUU0J1go06tKOpLjiBrEIJpjo5AK67w93SfsUaIBop8apoacHQ
Content-Type: application/ld+json
Origin: http://localhost:3000
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
TE: Trailers
Response Headers:
HTTP/2 401 Unauthorized
access-control-allow-origin: http://localhost:3000
access-control-expose-headers: link
cache-control: no-cache, private
content-type: application/json
date: Fri, 20 Mar 2020 04:40:39 GMT
link: <https://localhost:8000/api/docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"
www-authenticate: Bearer
x-debug-token: 720652
x-debug-token-link: https://localhost:8000/_profiler/720652
x-powered-by: PHP/7.4.1
x-robots-tag: noindex
content-length: 282
X-Firefox-Spdy: h2
When I manually remove the Authorization line from the request headers in the browser and retry it, it works.
My Questions:
Is this even expected behavior?
Should the client always send the token?
If the token should always be sent, how do I tell API Platform to accept it even if it isn't needed?
If the token should only be sent when it's required, how do I let the hydraDataProvider know?
After many many hours of trying different solutions, I finally fixed this problem.
Answers to my Questions
No, sending a valid token should not result in a 401 response.
The token can be sent on every request.
My Solution
The problem was that the JWT authentication was configured incorrectly on my server. None of the guides I followed actually covered the following case:
I have the user email as identifier, not the user name.
So what ended up happening is that the token contained the encoded user name, which is not a unique identifier in my case. To tell JWT to use the email instead, I had to set the user_identity_field to email.
// config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'
user_identity_field: email
Related
I'm trying to implement Discord OAuth2 in my React app with Spring Boot REST API. How it should work:
In React app (localhost on port1) I manually set window.location.href to my backend application oauth2 authorization endpoint. (localhost port 2 /api/oauth2/authorization/discord)
async function onSubmit() {
try {
window.location.href =
"http test.local port2/api/oauth2/authorization/discord";
} catch (error) {
console.log(error)
}
}
GET request is sent to test.local, which responds with HTTP 302 to https discordapp /oauth2/authorize?... along with Set-Cookie: ... Http header (this is important)
Response headers:
HTTP/1.1 302
Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers
Set-Cookie: oauth2_auth_request=long_cookie_value; Path=/; Max-Age=180; Expires=Sun, 29 Jan 2023 14:13:47 GMT; SameSite=None
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Location: https discordapp com/oauth2/authorize?response_type=code&client_id=...&redirect_uri=http://localhost:5173/login
Content-Length: 0
Date: Sun, 29 Jan 2023 14:10:47 GMT
Keep-Alive: timeout=60
Connection: keep-alive
User sees the discord login page
Now if I open http test local port2 in the browser I can see that the cookie was set correctly
cookie screenshot
User logs in to discord, discord redirects to http localhost port1/login?code=...&state=...
In react app I fetch GET http test.local port2/api/login/oauth2/code/discord?code=...&state=...
Code:
fetch(
`http test local port2/api/login/oauth2/code/discord?code=${searchParams.get("code")}&state=${searchParams.get("state")}`,
{
credentials: "include",
}
)
.then((res) => res.json())
.then((data) => console.log(data))
Request headers:
GET /api/login/oauth2/code/discord?code=...&state=... HTTP/1.1
Host: sss.test:8081
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:108.0) Gecko/20100101 Firefox/108.0
Accept: */*
Accept-Language: pl,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://localhost:5173/
Origin: http://localhost:5173
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Expected result:
The request is sent together with the cookie set in step 2. (required to confirm that both requests in authorization flow are sent from the same origin)
Actual result:
Cookie is not set.
Observations:
If I open http test.local port2/api/login/oauth2/code/discord?code=...&state=... in the browser, the cookie is set
Request headers:
GET /api/login/oauth2/code/discord?code=...&stat=... HTTP/1.1
Host: sss.test:8081
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:108.0) Gecko/20100101 Firefox/108.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: pl,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Cookie: oauth2_auth_request=long_cookie_value
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache
Works on the same domain for FE and BE
Notes:
Using http for development purpose only. I will switch to https and BE available on external server soon, but in the meantime I want to make it work for http and local. BE and FE will be hosted on the same server with proxy for /api requests that will direct api requests to BE port. But I will need cross-origin cookie for local FE development with external BE API anyway.
Chrome blocks non-secure, sameSite: none cookies, so it will not work there. Firefox allows it (for now), so I'm testing on firefox.
Spring Security configuration:
...
.cors()
.configurationSource(request -> {
final CorsConfiguration config = new CorsConfiguration().applyPermitDefaultValues();
config.setAllowCredentials(true);
config.setAllowedOrigins(List.of("http://localhost:5173"));
return config;
})
...
FE should receive JWT token in the response of this request for further authentication.
Sorry for weird URLs sometimes but apparently this question is SPAM -.-
My setup is the following:
(http://localhost:39500) ASP.NET Core backend
(http://localhost:3000) React frontend
I am sending an API request from my frontend to backend. The backend responds with a Set-Cookie header but the cookie is not being set in the browser.
Raw headers:
Response headers
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Vary: Origin
Server: Microsoft-IIS/10.0
Set-Cookie: PT=longstringhere; expires=Tue, 27 Sep 2022 04:56:03 GMT; path=/; httponly
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Credentials: true
X-Powered-By: ASP.NET
Date: Tue, 27 Sep 2022 03:56:03 GMT
Request headers
POST /account/login HTTP/1.1
Host: localhost:39500
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost:3000/
content-type: application/json
credentials: include
Content-Length: 46
Origin: http://localhost:3000
Connection: keep-alive
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
When inspecting my browser cookie storage (Firefox and Chrome) I have no cookies being set, additionally no cookies are being sent to my backend as well.
Any ideas or pointers why this is happening?
Frontend and backend run at different hosts. Set-Cookie saves the cookie for the given host, i.e. localhost:39500, but your frontend sits at host localhost:3000. Try inspecting cookies for localhost:39500 (for example in Chrome>Settings>Cookies and other site data>See all cookies and site data or with Postman), you will see that there is a cookie set. In production, you could serve your frontend from your backend, which will both be the same host. You could also put your frontend or backend under a subdomain, which can also be set as a cookie. See here for more info: Share cookie between subdomain and domain
EDIT: For development, you can use a proxy (as described in https://create-react-app.dev/docs/proxying-api-requests-in-development/)
In my frontend I was including into my headers "credentials": "include" which is not the same as setting the credentials to include in fetch.
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
TLDR:
The following response header doesn't set the cookie in browser:
Access-Control-Allow-Origin: *
Allow: GET, HEAD, OPTIONS
Content-Length: 7
Content-Type: application/json
Date: Tue, 27 Apr 2021 15:58:02 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.9.4
Set-Cookie: csrftoken=r5r2YcZZvJKs79cbLd24VSyNscpUsxJB6UuWiWO2TXriy6B4r8KDZrwSDyI091K1; expires=Tue, 26 Apr 2022 15:58:02 GMT; Max-Age=31449600; Path=/; SameSite=Lax
Vary: Accept, Cookie, Origin
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
My request headers:
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cache-Control: no-cache
Connection: keep-alive
Host: 127.0.0.1:8000
Origin: http://localhost:3000
Pragma: no-cache
Referer: http://localhost:3000/
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"
sec-ch-ua-mobile: ?0
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36
I am new to Django, react and "http header" related stuff.
My django dev server runs at:
http://127.0.0.1:8000/
and my react dev server runs at:
http://127.0.0.1:3000
In order to access the website, login is required. So, all unauthorized requests are 1st redirected to login page, by configuring react-router and following this template. So, till now, no api calls are made.
In order to post login data, i need to have csrf token set by the server. But since i have not made any api calls, i created an endpoint /api/csrf/ explicitly, to set the csrf token.
# URL: /api/csrf/
class CSRFGet(APIView):
"""
Explicitly set csrf cookie
"""
#method_decorator(ensure_csrf_cookie)
def get(self, request):
return Response('hello')
I call this endpoint, when useProvideAuth hook is mounted.
function useProvideAuth() {
const [token, setToken] = useState(null);
const login = (username, password) => {
return axios.post(
'/auth/',
{
username: username,
password: password
})
.then(response => {
setToken(response.token)
})
}
useEffect(()=> {
axios.get(
'/csrf/'
)
},[])
return {
token,
login,
}
}
To retrieve and set this cookie, i followed the official Django docs. I also enabled CORS policy using django-CORS-headers allow all origins.
Now, when i make a request to any page, it redirects to login page, and i can see api/csrf/ responds with:
Set-Cookie: csrftoken=LgHo2Y7R1BshM4iPisi5qCXhdHyAQK7hD0LxYwESZGcUh3dXwDu03lORdDq02pzG; expires=Tue, 26 Apr 2022 06:29:23 GMT; Max-Age=31449600; Path=/; SameSite=Lax
But, the cookie is not set at all. Why is it so?
Is my approach for getting csrf cookie correct? Please let me know, if i am making any security vulnerability with this approach.
Could you try adding the following to the django-cors-headers configuration and retry?
CORS_ALLOW_CREDENTIALS = True
Also, please note that the above configuration would probably not work if you are allowing all origins. See this Mozilla documentation: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’
If you face such error, I suggest setting:
CORS_ALLOWED_ORIGINS = [
"http://127.0.0.1:3000",
]
or something fancier like:
CORS_ALLOWED_ORIGIN_REGEXES = [
r"^http://127.0.0.1:[0-9]{1,4}$",
]
Finally, make sure that you are using a django-cors-headers version >= 3.5 since the 2 above configuration had different aliases back then.
Let me know if it works, I am very curious.
Turns out the issue was, i was using http://127.0.0.1:8000 to make api calls, where as my server was on http://localhost:8000. Because of this, host and origin, in my request headers didn't match the same domain.
Cookies can be allowed to be used under same domain, with different ports and subdomains, unlike Same-Origin policy, but cannot be used cross-domains.
In my case, I guess http://127.0.0.1:8000 & http://localhost:8000 were considered different domains, and thus the browser was not setting my cookie.
Thanks Anas Tiour, for stating about Allow-Credentials. I had tried that too, but still had no luck until i found out the actual reason.
I've got quite random occurrence with this common error:
OPTIONS https://api.cloudfunctions.net/api/graphql 404
Access to fetch at 'https://api.cloudfunctions.net/api/graphql' from origin 'https://website.com' 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. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
What I have is a graphql endpoint with apollo server deployed on Google Cloud Functions and a react client. At some points the client will throw the error on browser but if I try refresh or send the request again 2 or 3 times later it will work.
The preflight request headers being sent:
:authority: api.cloudfunctions.net
:method: OPTIONS
:path: /api/graphql
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9,id;q=0.8,ms;q=0.7
access-control-request-headers: content-type
access-control-request-method: POST
origin: https://website.com
referer: https://website.com/
sec-fetch-mode: cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36
Expected response
access-control-allow-credentials: true
access-control-allow-headers: content-type
access-control-allow-methods: POST,OPTIONS
access-control-allow-origin: https://website.com
alt-svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000
content-length: 0
content-type: text/html
date: Wed, 08 Jan 2020 00:38:16 GMT
function-execution-id: 84et92k6mvd9
server: Google Frontend
status: 200
vary: Origin, Access-Control-Request-Headers
x-cloud-trace-context: 95d25375171148a66bc629cc41a79d05
x-powered-by: Express
Random failed response
alt-svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000
cache-control: private
content-encoding: gzip
content-length: 140
content-security-policy: default-src 'none'
content-type: text/html; charset=utf-8
date: Wed, 08 Jan 2020 00:38:05 GMT
function-execution-id: 84etgky3im1k
server: Google Frontend
status: 404
x-cloud-trace-context: 77040d2c72304cad0d645480b6814f7f;o=1
x-content-type-options: nosniff
x-powered-by: Express
Looking at the failed response above kinda make sense that it's missing the access-control-allow-* headers compared to success one, but again I am not sure how that happened.
Here's my cors config:
const corsConfig = {
origin: ['https://website.com', 'http://localhost:3000'],
methods: ['POST', 'OPTIONS'],
credentials: true,
optionsSuccessStatus: 200,
}
const app = express()
app
.use(cors(corsConfig))
.use(...)
...
apolloServer.applyMiddleware({ app, cors: corsConfig })
Based on few suggestions around I have tried different setup but still sometimes the error happens:
set cors: false in applyMiddleware
remove cors
repeat cors as shown above
add app.options('*', cors()) as per doc says
All and all it happens like 1 in 10, sometimes on first request after the user open the site the other times after the user browsing around the site for a while.
I think there might be other middleware that messes up your cors settings.
You can try use a different path for your graphql endpoint, and apply cors only to that path.
apolloServer.applyMiddleware({ app, path: '/graphql', cors: corsConfig });
Alternatively, you can try the express cors middleware and disable the cors from apollo server
I used the apollo-server-cloud-functions package to solve this problem. Just follow the instructions here (https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-cloud-functions) but instead of using exports.handler = server.createHandler() swap it out for your own function, like this:
exports.api = functions.https.onRequest(
server.createHandler({
cors: {
origin: true,
credentials: true
}
})
);
That solved it for me!
I have a Flask backend and a React front-end. The Flask backend is an API that will communicate with other microservices. In development I have my React front-end running on localhost:3000 and the Flask app running on localhost:5000.
Clearly these are different ports which will throw a CORS error by default. So I added Flask_CORS and allowed traffic from localhost:3000. This works and I can now serve GET and POST requests.
I then add my Firebase authentication to the front-end. I receive a JWT and then I want to send the JWT with ech API request to ensure that the user is allowed to make certain requests, which will be validated on the Flask backend.
I added the token_id to the headers in the Axios request to my back-end, but now I am getting the following error:
localhost/:1 Access to XMLHttpRequest at 'http://localhost:5000/items' 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.
When I examine the network tab I notice that without the JWT the request passes through fine. See below for header content:
General:
Request URL: http://localhost:5000/items
Request Method: GET
Status Code: 200 OK
Remote Address: 127.0.0.1:5000
Referrer Policy: no-referrer-when-downgrade
Response Headers:
Access-Control-Allow-Origin: http://localhost:3000 <--This line is my concern
Content-Length: 37
Content-Type: application/json
Date: Fri, 06 Sep 2019 09:10:10 GMT
Server: Werkzeug/0.15.5 Python/3.7.4
Vary: Origin
Request Headers:
Provisional headers are shown
Accept: application/json, text/plain, /
Origin: http://localhost:3000
Referer: http://localhost:3000/
Sec-Fetch-Mode: cors
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36
I then only add the JWT to the Authorization header config of an Axios interceptor and now the request fails with the following headers present in the Network tab:
General:
Request URL: http://localhost:5000/items
Request Method: OPTIONS
Status Code: 200 OK
Remote Address: 127.0.0.1:5000
Referrer Policy: no-referrer-when-downgrade
Response Headers:
Allow: POST, OPTIONS, HEAD, GET
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Fri, 06 Sep 2019 09:14:46 GMT
Server: Werkzeug/0.15.5 Python/3.7.4
Request Headers:
Provisional headers are shown
Access-Control-Request-Headers: authorization
Access-Control-Request-Method: GET
Origin: http://localhost:3000
Referer: http://localhost:3000/
Sec-Fetch-Mode: no-cors
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36
I noticed that the Access-Control-Allow-Origin disappears when the Authorization header is added and instead an Access-Control_Request-Headers is present.
The code of interest in the front end is below
import axios from 'axios';
import * as firebase from "firebase/app";
import 'firebase/auth';
const instance = axios.create({
baseURL: 'http://localhost:5000/',
});
instance.interceptors.request.use(config => {
const id_token = firebase.auth().currentUser.getIdToken();
config.headers = { Authorization: id_token}; <---Commenting out this line works
return config
}, error => {
return Promise.reject(error);
})
export default instance;
I do not know how or why the CORS fails to work as soon as a JWT is added. I suspect it is because once the JWT is added there is a pre-flight request. But I have changed nothing on the server side so I am puzzled as to why the server would not provide a suitablelCORS response just because a JWT is added.
Any pointers would be appreciated.
You can define CORS at the top of the main file, it will allow all the CORS requests
from flask import Flask
from flask_cors import CORS, cross_origin
app = Flask(__name__)
CORS(app)
#app.route("/api/test")
def test_cors():
return "CORS allowed"