Cookies not sending in cross-origin react-flask app - reactjs

I am currently building a ReactJS (front at "https://www.example.com") Flask (back "https://server.example.com") app. The react client makes GET and POST requests to my flask server, but first, the requests must be authenticated. I am using Flask-CORS to only accept requests from "https://www.example.com". I have enabled "CORS_SUPPORTS_CREDENTIALS" on my server, and from the client side, I have set fetch requests to "credentials: 'include'".
server.py
import os
import functools
from flask import Flask, Blueprint, request, make_response
from flask_cors import CORS
from .config import Config
# Application factory pattern
def create_app():
config_object = Config()
app = Flask(__name__)
# Cross-Origin Config
CORS(app,
origins=[config_object.CORS_ALLOW_ORIGIN], # https://www.example.com
supports_credentials=config_object.CORS_SUPPORTS_CREDENTIALS # True
)
app.config.from_object(config_object)
app.register_blueprint(main)
return app
# Cookie authentication
def auth_required(view):
#functools.wraps(view)
def wrapped_view(**kwargs):
user_cookie = request.cookies.get('USER_ID_COOKIE')
session_cookie = request.cookies.get('USER_SESSION_COOKIE')
is_authenticated = verify_user_session(user_cookie, session_cookie) # T/F if this is a valid user
if is_authenticated:
# Continue to route...
return view(**kwargs)
else:
# Reject request...
response = make_response({"flash": "User not logged in."}, 403)
return response
return wrapped_view
bp = Blueprint('main', __name__)
#bp.get('/main/protected')
#auth_required
def get_protected_data():
response = make_response({"fakeData": "Do you believe in life after love?"}, 200)
return response
app = create_app()
if __name__ == "__main__":
app.run()
snippet_from_protected_client.js
const MainPage = ({ setFlashMessages }) => {
let statusOK;
let statusCode;
const isStatusOK = (res) => {
statusOK = res.ok;
statusCode = res.status;
return res.json();
}
const [data, setData] = useState(null);
useEffect(() => {
fetch(process.env.REACT_APP_SERVER + '/main/protected', { // REACT_APP_SERVER=https://server.example.com
credentials: 'include'
})
.then(isStatusOK)
.then(data => {
if (statusOK) {
setData(data.fakeData);
} else if (statusCode === 403) {
setFlashMessages(data.flash);
}
});
}, []);
return (
<main>
<div className="container-md">
<div className="row">
<div className="col-md-1"></div>
<div className="col-md-11 mt-5">
<h1 className="text-start">My Account</h1>
<p className="text-start">Just some filler text here.</p>
<p className="text-start">{ data }</p>
</div>
</div>
</div>
</main>
);
}
export default MainPage;
Hopefully you get the idea--I've abbreviated a lot but maintained the main features that are giving me the issue. So now, the issue:
My cookies, which are have been created and are held in the browser, have been set with the JS library js-cookie like so:
snippet_from_login_client.js
Cookies.set('USER_ID_COOKIE', '1', { sameSite: 'none', secure: true}) // again, example
Cookies.set('USER_SESSION_COOKIE', 'ARanDomStrInGSetDURingLoGiNANDSTOrEDinTHESERVERDB', { sameSite: 'none', secure: true}) // again, example
And I know that they are set because I can see them in the developer tools. However, on subsequent requests to protected routes, the server accepts the request (meaning it's not a CORS origin issue) but #auth_required throws 403 (as shown above). After checking the Request headers in my browser's development tools, I can see that the request did not send with the cookies!
REQUEST HEADER FROM BROWSER DEVELOPER TOOLS
* Accept: */*
* Accept-Encoding: gzip, deflate, br
* Accept-Language: en-US,en;q=0.9
* Connection: keep-alive
* Host: server.example.com
* Origin: https://www.example.com
* Referer: https://www.example.com/
* Sec-Fetch-Dest: empty
* Sec-Fetch-Mode: cors
* Sec-Fetch-Site: cross-site
* Sec-GPC: 1
* User-Agent: *DEVICE-DATA*
Notice no Cookie: -- header despite the cookies being set...
Why aren't my cookies sending? Any tips or leads would be incredibly helpful.

I figured out the problem! I should have set my cookie domain to '.example.com' so that the client would attach them to each request. Here is how you would do it with js-cookie:
Cookies.set('USER_ID_COOKIE', '1', { domain: '.example.com', sameSite: 'none', secure: true}) // again, example
I'm not sure how to send cookies between unrelated domains, but if your server and client are formatted as 'server.domain.com' and 'client.domain.com', then this solution should work for you. Hopefully, this helps someone out there!

Related

Access-Control-Allow-Credentials problems React

Here is a project to create a login system using react. However when it connects to the back-end flask to check whether the login is valid or not, it shows the error message below. Hope someone can help me. I spent so many days trying to fix this and search every related post but still couldn't fix it :(
I'm using the chrome browser.
Also, I have installed the Access-Control-Allow-Origin plugin on chrome.
error message
Access to fetch at 'http://localhost:5000/login' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.
front-end code (react)
fetch(domain+"/login", {
body: JSON.stringify(mydata),
credentials: "include",
headers: new Headers({
'Content-Type': 'application/json',
'Access-Control-Allow-Credentials': true
}),
method: 'POST', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, cors, *same-origin
redirect: 'follow', // manual, *follow, error
referrer: 'no-referrer', // *client, no-referrer
})
.then(response=>{
if(response.ok){
return response.json()
}
}
back-end(flask) include below code to allow cross origin
app = Flask(__name__)
CORS(app, supports_credentials=True)
#app.route('/login', methods=['GET', 'POST'])
def login():
data = request.json
print(data)
result = {}
login_info = {
"code": -1,
"message": ""
}
if data:
username = data['username']
password = data['password']
cur = database.cursor(dictionary=True)
cur.execute(
"SELECT username, password, role FROM user WHERE username = %s AND password = %s AND disabled = 0;", (username, password))
# cur.execute("SELECT uid, displayname, rank, disabled FROM user WHERE username = %s AND password = %s;", (username, password,))
# cur.execute("SELECT uid, displayname, rank, disabled FROM user WHERE username = %s AND password = AES_ENCRYPT(%s, UNHEX(SHA2('encryption_key', )));", (username, password,))
account = cur.fetchone()
if account:
# modify_info(1, "Login successfully!")
login_info["code"] = 1
login_info["message"] = "Login successfully!"
account_str = json.dumps(account, cls=MyEncoder)
account_json = json.loads(account_str)
session["username"] = account_json["username"]
result["username"] = username
result["role"] = account_json["role"]
result["isLogin"] = 1
else:
# modify_info(0, "Login not successful")
login_info["code"] = 0
login_info["message"] = "Login not successful"
cur.close()
return jsonify({"code": login_info["code"], "data": result, "message": login_info["message"]})
In the specification, the Access-Control-Allow-Credentials: true header is not allowed to use with
the Access-Control-Allow-Origin: * header.
However, * is the default value for the origin header in flask cors, you should set it to a specific value, for example:
app = Flask(__name__)
CORS(app, origins=['http://localhost:3000'], supports_credentials=True)
Also, here is link to the document about it: https://flask-cors.readthedocs.io/en/latest/api.html
Update
It seems like the samesite of the cookie problem, here's the code to set it:
from flask import Flask, make_response
from flask_cors import CORS
app = Flask(__name__)
CORS(app, supports_credentials=True)
#app.route("/", methods=['POST'])
def index():
res = make_response()
res.set_cookie("name", value="I am cookie", samesite='Lax')
return res, 500
app.run(debug=True)

React Laravel Sanctum session doesn't work on cross site access (Laravel new session every request)

My application is laravel 8.75.0 in the backend serving apis requests for a react frontend.
I am using session to authenticate users, and to store session variables.
On production, react is built, and served statically using apache. Links are:
http://123.123.123.123/api -> backend laravel routes
http://123.123.123.123/dashboard -> react build folder
The production really doesn't use a domain. It is just an ip inside a corporate network, with no ssl.
On development, I use docker for hosting the laravel backend. React is served on its own. As a result, the links are:
http://172.18.0.3/api -> backend laravel routes
http://localhost:3000 -> react dev server
On successful login, I save session info like this on laravel:
$request->session()->put('workplace_id', $workplace_id);
session(['workplace_id' => $workplace_id]);
and on subsequent requests, I am getting the value like this:
Log::info("check session:".session('workplace_id'));
On production, I am able to get the workplace_id value. On development, I am not able to get the value because, from what I see, on every request to the api endpoint, a new session file is generated.
This is my code on react:
import axios from "axios";
import { wrapper } from 'axios-cookiejar-support';
import { CookieJar } from 'tough-cookie';
const jar = new CookieJar();
window.axios_client = wrapper(axios.create({ jar }));
window.axios_client.defaults.withCredentials = true;
window.axios_client.defaults.credentials = true;
window.axios_client.get(`${API_URL}/api/csrf-cookie`).then(response => {
console.log("csrf")
})
window.axios_client.interceptors.request.use((c) => {
const state = store.getState();
if (c.url && !c.url.includes(`${API_URL}/api`)) {
c.url = `${API_URL}/api` + c.url;
}
if (state.loginReducer && state.loginReducer?.appUser) {
const { token } = state.loginReducer.appUser;
c.headers.common.Authorization = `Bearer ${token}`;
c.withCredentials = true;
c.credentials = true;
}
return c;
});
window.axios_client.interceptors.response.use(
(response) => {
//const cookie = response.headers["set-cookie"][0];
return response;
},
(error) => {
if (error && error.response && error.response.status === 401) {
localStorage.removeItem("state");
window.location.href = window.location.origin;
}
return Promise.reject(error);
}
);
In the code above, I tried to use cookie jar that was recommended in some SO solutions, but it doesn't help.
I got to a point where I can see cookies sent to the server from the browser:
From the image, you can see there is an alert on the "SameSite" response. It says that this attempt was blocked because it had the "samesite=lax" though it came from a cross site response, which is not top level navigation.
You can see that the request cookies samesite are None. Also see laravel code below, and you can see that the configuration for samesite is set to none, and it still responds with lax. I don't know why it is happening.
Even though request cookies (XSRF-TOKEN & laravel_session) are always the same, the corresponding response cookies are changing on each response.
To get to this point I had to install self signed ssl certificate with a .local domain on the docker apache development server. These are the laravel files:
(relevant parts only)
config/session.php:
'secure' => env('SESSION_SECURE_COOKIE',true),
'http_only' => true,
'same_site' => "none",
config/cors.php:
'paths' => ['api/*','/login', '/logout', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => ["set-cookie"],
'max_age' => 0,
'supports_credentials' => true,
sanctum.php:
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,localhost:3001,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),
'middleware' => [
'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
//'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
],
app/Http/Middleware/Cors.php:
return $next($request)
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
->header('Access-Control-Allow-Headers', $request->header('Access-Control-Request-Headers'));
Kernel.php:
'api' => [
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
There is a solution for this issue. It is explained in this medium article.
Copying here the required code changes:
React app.js:
axios.defaults.withCredentials = true;
Backend:
ini_set("session.cookie_domain", '.dev.local');
session_set_cookie_params(3600, '/', '.dev.local');
session_start();
$http_origin = $_SERVER['HTTP_ORIGIN'];
if ($http_origin == "http://dev.local:3000" || $http_origin == "http://localhost:3000"){
header("Access-Control-Allow-Origin: $http_origin");
}
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Headers: X-Requested-With, Origin, Content-Type, X-Auth-Token, Accept');
And you need to use a example.local domain (set it in your host file).

How to fix python flask cors issue using flask-cors library

I could really use some help. I can't figure out what I'm doing wrong. I keep getting
Edit : Frontend React application runs on localhost:3000, backend is running on localhost:5000
Access to XMLHttpRequest at 'http://localhost:5000/api/auth/login' 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.
def create_app(test_config=None):
logger = logging.getLogger(__name__)
logger.info("Flask App Starting")
# create and configure the app
app = Flask(__name__, instance_relative_config=True)
CORS(app)
cors = CORS(app, resources={r"/api/*": {"origins": "*"}})
logging.getLogger('flask_cors').level = logging.DEBUG
app.config.from_mapping(
SECRET_KEY="dev",
JWT_SECRET_KEY="super secret key",
JWT_ACCESS_TOKEN_EXPIRES=timedelta(hours=2),
)
if test_config is None:
# load the instance config, if it exists, when not testing
app.config.from_pyfile("config.py", silent=True)
else:
# load the test config if passed in
app.config.from_mapping(test_config)
jwt = JWTManager(app)
"""
Adding blueprints
"""
from app.routes import tester
from app.routes import auth
from app.routes import api_routes
from app.routes import similar_routes
app.register_blueprint(tester.bp)
app.register_blueprint(auth.bp)
app.register_blueprint(api_routes.bp)
app.register_blueprint(similar_routes.bp)
#app.before_request
def check_login():
"""Before each request check if token exist."""
pass
logger.info("Checking if token is required")
if (not getattr(app.view_functions[flask.request.endpoint], "is_public", False)):
logger.info("Token required")
try:
result = verify_jwt_in_request(locations="headers")
logger.debug(f"Identity sent in is {get_jwt_identity()}")
except Exception as e:
logger.error("Error occured during checking token")
logger.error(e)
return jsonify(msg="Token Expired"),401
#app.errorhandler(Exception)
def all_exception_handler(error):
logger.error("Error caught" + str(error) )
return jsonify(msg="Oh no! A Server error occured. :,( "), 500
return app
if __name__ == "__main__":
loggingSetup()
app = create_app()
logger.info("App Created")
app.run(debug=True)
logger.info("App Running")
I'm making API calls from my react frontend, using axios
axios.defaults.baseURL = "http://localhost:5000/api"
function getHeaders(token){
return {
'Accept': 'application/json',
'Content-Type': 'application/json;charset=UTF-8',
"Authorization": "Bearer " + token,
'Access-Control-Allow-Origin': '*'
}
}
async function createCustomObject(token) {
let url = "/ontology/create-object";
let options = {
method: "POST",
url: url,
headers: getHeaders(token),
};
let response = await axios(options).then((response) => {
let data = response.data
}).catch((error) => {
handleError(error.response)
})
return response;
What am I missing?
You would set your origin to http://localhost:3000:
cors = CORS(app, resources={r"/api": {"origins": "http://localhost:3000"}})
'Access-Control-Allow-Origin': 'http://localhost:3000'
I resolved my issue using proxy after trying a couple of failed attempts using CORS solution.
I simply put "proxy": "http://127.0.0.1:5000" in my package.json and therefore, I can then use
fetch(`/test`)
.then((res) => res.json())
.then((data) => {
//do something
});
easily in my app without actually providing the full url to the backend (http://127.0.0.1:5000).

What is the right way to specify headers using axios?

I'm wrestling with cross origin headers while testing my app:
react side:
const url = "http://localhost:5000/blog/posts";
const headers = { headers: "Access-Control-Allow-Origin" };
axios.post(url, data, headers).then( ...
Flask backend __init__.py :
...
...
from flask_cors import CORS
def create_app(script_info=None):
app = Flask(__name__)
from project.api.blog import blog_blueprint
from project.api.auth import auth_blueprint
CORS(blog_blueprint, resources={'origin': ['http://localhost:3000']})
app.register_blueprint(blog_blueprint)
app.register_blueprint(auth_blueprint)
return app
the above gives me an exception in the catch block of the try-catch statement:
TypeError: name.toUpperCase is not a function
using Flask's defaults which means exposing the endpoint to any domain:
from project.api.blog import blog_blueprint
from project.api.auth import auth_blueprint
CORS(blog_blueprint)
gives me Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/blog/posts. (Reason: CORS header 'Access-Control-Allow-Origin' missing)
I've also tried to use a decorator from Flask-CORS:
from flask_cors import cross_origin
class BlogPosts(Resource):
#cross_origin()
def post(self):
parser.add_argument('category', type=str)
parser.add_argument('title', type=str)
parser.add_argument('post', type=str)
args = parser.parse_args()
new_post = Posts(title=args.title, category=args.category, post=args.post)
db.session.add(new_post)
db.session.commit()
return {'status': 'success', 'message': 'post added'}, 201
Any help is much appreciated.
Strangely, I fixed my problem by refactoring my code into function-based view:
CORS(blog_blueprint)
#blog_blueprint.route('/posts', methods=['GET', 'POST'])
#cross_origin()
def blog_posts():
if request.method == 'POST':
post_data = request.get_json()
category = post_data.get('category')
title = post_data.get('title')
post = post_data.get('post')
new_post = Posts(title=title, category=category, post=post)
db.session.add(new_post)
db.session.commit()
return {'status': 'success', 'message': 'post added'}, 201
return {'status': 'success', 'message': [post.to_json() for post in Posts.query.filter_by(category=category)]}, 200
I'm not very happy with this because Flask's CORS library should work same regardless if I'm using class-based or function-based view for handling APIs.

Unable to set httpOnly cookie between Heroku and Netlify + cloudflare

I have the following problem. I am trying to set httpOnly cookie and nothing happens. I spent a few hours trying to solve this issue and I have no idea what is going on... My architecture is the following:
Backend: Python fast-api hosted on Heroku, available at https://api.mysuperdomain.com.
Frontend: GatsbyJs hosted on Netlify, available at https://mysuperdomain.com
When I call login request from React component:
const handleSubmit = async (e) => {
e.preventDefault()
const config = {
headers: {
crossDomain: true,
withCredentials: true,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
const requestBody = {
username: emailRef.current.value,
password: passwordRef.current.value
}
try {
const data = await axios.post('https://api.mysuperdomain.com/login', qs.stringify(requestBody), config)
I get response from my backend with headers, set-cookie:
set-cookie: Authorization="Bearer somethinghere"; Domain=.mysuperdomain.com; expires=Tue, 28 Jul 2020 20:40:32 GMT; Max-Age=1800; Path=/; SameSite=lax
unfortunately in browser storage I cannot see this cookie.
My backend(API) sets the cookie in the following way:
#app.post("/login")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=400, detail="Incorrect username or password")
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": form_data.username}, expires_delta=access_token_expires
)
token = jsonable_encoder(access_token)
response = JSONResponse({'status': 'authenticated'})
response.set_cookie(
key="Authorization",
value=f"Bearer {token}",
domain=".mysuperdomain.com",
httponly=True,
max_age=1800,
expires=1800,
)
return response
My DNS records are within the Cloudflare, and CNAME record for backend is proxied:
Typ Name Content TTL Proxy status
CNAME api limitless-starfish-something.herokudns.com Auto Proxied
SSL/TLS encryption mode is Flexible (Encrypts traffic between the browser and Cloudflare). Backend at Heroku has no SSL Certificate therefore I set flexible SSL/TLS encryption mode.
Maybe it is somehow related to above config?
I think this happens because you didn't add a CORS middleware to your app, in FastAPI, allow_credentials is set to bool = False in default. But you can change that easily.
First you need to import CORSMiddleware from fastapi.middlewares
from fastapi.middleware.cors import CORSMiddleware
Then we can add a middleware to our app
app.add_middleware(
CORSMiddleware,
allow_credentials=True,
)
Also you can add origins and all other stuff with CORSMiddleware, for more related info check FastAPI-CORS out.

Resources