React App + Spring Boot - JWT auth token inside a cookie is not set in Chrome - reactjs

I'm trying to configure Spring Boot to set-cookie containing JWT auth token following a sign-in request from my react app, and then the expectation is that the browser will automatically set this cookie to all requests as specified by the cookie path. The behaviour is ok on my friend's environment - same code, Chrome browser, different machine. I tried clearing node_modules, mvn clean install, also tried different browsers Chrome and FireFox, no success.
Here is the all the relevant code (let me know if I'm missing something else important)
React is running on localhost:3000
Spring Boot is running on localhost:8080
There is a proxy in the package.json
"proxy": "http://localhost:8080",
To test the auth flow we are issuing a sign-in request from the sign-in form (react), the request is successfully proxied to port 8080 and the response from the server is successfully returning the JWT token as part of an auth cookie. The cookie is specified to the /api path. Network request as seen in Chrome below:
Immediately after the login, the react app is issuing a second HTTP request to the back-end, but a break-point on the server shows no cookies are passed from the browser as part of this request. The request is to http://localhost:3000/api/user.
In the front-end we are using fetch to make that request and it looks like this:
fetch("/api/user, {
credentials: "same-origin"
})
Just for additional context this is how we return the original cookie from the server, upon a successful login:
#PostMapping("/signin")
public ResponseEntity signin(#RequestBody AuthenticationRequest data, HttpServletResponse response) {
try {
String username = data.getUsername();
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, data.getPassword()));
User user = (User) authentication.getPrincipal();
String token = jwtTokenProvider.createToken(user);
final Cookie cookie = new Cookie("auth", token);
cookie.setSecure(!environment.acceptsProfiles(Profiles.of("dev")));
cookie.setHttpOnly(true);
cookie.setMaxAge(Integer.MAX_VALUE);
cookie.setPath("/api");
response.addCookie(cookie);
return ok(buildUserResponseObject(user));
} catch (AuthenticationException e) {
throw new BadCredentialsException("Invalid username/password supplied");
}
}
Is there anything wrong with our approach? What could be preventing my browser from passing on the auth cookie?

Oh this is embarrassing...
The issue was this line
cookie.setSecure(!environment.acceptsProfiles(Profiles.of("dev")));
!environment.acceptsProfiles(Profiles.of("dev")) was evaluating to true and it was causing the cookie to be only passed if the connection is secure, which it wasn't because it was localhost. Mystery solved.

Related

Why won't my React app send HTTP-only cookies in WebSocket upgrade requests in production?

I'm currently building a full-stack TypeScript live chat app with React + Vite on the frontend and Node on the backend. I have two separate servers running: one is a REST API and OAuth2 auth server built with Express and Passport.js and the other one is a WebSockets server built with the ws package. They run independently (no interprocess communication whatsoever) and use stateless auth in the form of JWTs.
Here's how my current flow works: users first log in with either their Google or GitHub account, and once the first server has verified their identity, it sends an HTTP-only cookie down to the client. This cookie is send back to the server on all subsequent requests and I have some middleware that runs on the REST API to parse and verify the JWTs on protected routes. Once it has the cookie, the client then initiates a WS connection with the second server, which also checks for the JWT cookie in the incoming HTTP Upgrade request and verifies its signature before allowing the new client to continue exchanging messages:
import { WebSocket, WebSocketServer } from "ws";
import { baseDataSchema } from "./zod/schemas";
import prisma from "./prisma";
import { asyncJWTverify } from "./misc/jwt";
import { UserJwtReceived } from "../types/jwt";
import { handleJoinGroup } from "./websockets-handlers/join-group";
// Websockets server setup
const wss = new WebSocketServer({ port: Number(process.env.WS_PORT) });
const userSocketMap = new Map<string, WebSocket>();
wss.on("listening", () => {
console.log(`WebSockets server started on port ${process.env.WS_PORT}`);
});
wss.on("connection", async function connection(ws, req) {
// authenticate incoming websocket connection
const cookies = req.headers.cookie;
if (!cookies) return ws.close();
let currentUser: UserJwtReceived = { id: "", iat: 0 };
try {
// Decode auth JWT
const token = cookies.split("=")[1];
currentUser = (await asyncJWTverify(
token,
process.env.JWT_SECRET as string
)) as UserJwtReceived;
} catch (err) {
console.error(err);
return ws.close();
}
// check for JWT expiry
const expiryTime = Number(process.env.JWT_EXPIRY);
if (Math.round(Date.now() / 1000) - currentUser.iat > expiryTime) {
return ws.close();
}
// Bind user ID to WebSocket, add it to map
// TypeScript doesn't complain about this because I've extended ws's WebSocket interface
ws.userId = currentUser.id;
userSocketMap.set(currentUser.id, ws);
console.log(`User ID ${ws.userId} connected`);
ws.on("message", async function message(rawData) => {
// ... actual app logic goes here
})
ws.on("close", function () {
if (!ws.userId) return;
console.log(`User ID ${ws.userId} has disconnected`);
userSocketMap.delete(ws.userId);
});
})
Both servers and the React frontend app run on different URLs, both on local dev and prod, so all requests are cross-origin, but CORS is enabled on the REST API/auth server and as far as I know the WebSockets protocol doesn't implement any CORS policies...
The problem I'm currently facing is that in my local dev environment, the cookie that contains the JWT is sent along with Upgrade request no problem, but after deploying my app to AWS Lightsail (it's a VPS service similar to EC2) and setting up NGINX, my React frontend is no longer able to include the cookie with the upgrade request.
After spending literally the whole day debugging, I've been able to rule out a faulty NGINX config as the root of the problem, since I can use wscat to connect (and most importantly, successfully authenticate) to my production WS server by manually including the Cookie header.
I still have no idea why my React app won't properly send the HTTP-only auth cookie to my WS server. Does anyone have any clue as to why this is happening?
I expected the HTTP-only cookie containing the JWT to be sent along with the HTTP Upgrade request, just like I've been able to do in my local dev environment, but no luck.

Google Oauth2 giving "redirect_uri_mismatch" error in production when redirect uri setup correctly

I have google oauth set up in a react project. It was working fine in development, locally. Once i promoted the oauth client to "production" and modified the JS origin and redirect URIs to production values, yet it gives this error
Error 400: redirect_uri_mismatch
You can't sign in to this app because it doesn't comply with Google's
OAuth 2.0 policy.
If you're the app developer, register the redirect URI in the Google
Cloud Console. Request details:
redirect_uri=http://super-server.herokuapp.com/v1/auth/google/callback
Related developer documentation
These are the authorized redirect URIs within google cloud console:
https://super-server.herokuapp.com/v1/auth/google/callback
https://super-server.herokuapp.com/v1/auth/google/callback/
https://www.super-server.herokuapp.com/v1/auth/google/callback
https://www.super-server.herokuapp.com/v1/auth/google/callback/
As you can see, there are no authorized uri's with an HTTP schema. Theyre not even allowed in production mode. So im not sure where this is coming from, because the server is HTTPS
server:
Any advice?
the redirect uri must exactly match the one you are adding to Google developer console
If you check the error message your app is running with
http://super-server.herokuapp.com/v1/auth/google/callback
All the ones you have added are https
May i suggest fixing your app so that it runs https. I dont think that you will be able to add http as a production redirect uri endpoint.
documentation
Obtaining OAuth 2.0 access tokens
authorization-errors-redirect-uri-mismatch
Node.js
I dont know enough about react.js but with node you should be able to do something like this
const http = require('http');
const https = require('https');
In my case (MERN + passport.js), i had a configuration that looked like:
const googleOptions = {
clientID: config.google.id,
clientSecret: config.google.secret,
passReqToCallback: true,
callbackURL: '/v1/auth/google/callback',
scope: ['profile', 'email']
};
Even though the server, the client && the configuration of the oauth client within google api console were all in production with everything setup for https, for some reason, the callbackURL kept firing with google oauth as http://my-domain.com/v1/auth/google/callback
so this fix may be hacky, but it did fix my oauth issues:
//Google Strategy
const googleOptions = {
clientID: config.google.id,
clientSecret: config.google.secret,
passReqToCallback: true,
callbackURL: config.environment == 'production' ? 'https://super-server.herokuapp.com/v1/auth/google/callback' : '/v1/auth/google/callback',
scope: ['profile', 'email']
};
Looks like the redirect_uri on the client side (React side) is set to http://super-server.herokuapp.com/v1/auth/google/callback
Change the redirect_uri on the client side from (http) http://super-server.herokuapp.com/v1/auth/google/callback to (https) https://super-server.herokuapp.com/v1/auth/google/callback.

React native Axios Django - Csrf failed referer checking failed no referer

I am calling a Django back-end api from React front-end using axios.
For that api which is login api, I am using Django Knox package in logic.
React.js - I am calling axios.request(method, url, data) and the api call is working correctly.
When I went to Developer tools>Network, I can see Referer header set to React.js website in request header and no other csrf-related header. In Response headers I can see two set-cookie headers, csrftoken and sessionid.
React Native - same way I am calling api but api returns error csrf failed referer checking failed - no referer . When I checked response.config, Referer header is not set unlike React.js
Curl - works fine
httpie - works fine
How can I get rid of this error.
Note 1 - My Django back-end is based on api token logic and not csrf in any way.
Note 2 - React.js and Django are hosted on different domains. I am facing error in React Native which is in debug mode.
Update 1 - After disabling CSRF middleware in Django settings.py, now I am getting only one setCookie header (csrftoken is no longer obtained) but same error still persists.
Django Rest api need a Referer header.
In case of React.js it is automatically set (maybe by browser) and its value is current website.
But in case of React Native, it is not set. so we have to manually set it.
From this link, i set Referer header in axios. see below code
export const axiosAPICall = (method,url,data,headers) => {
let request = {
method: method,
url: url,
};
if (data) {
request['data'] = data;
}
if (headers) {
request['headers'] = headers;
}
// Referer is auto-set in react.js as same website value.
// for react-native we have to set it manually to target api:port
request['headers'] = {
...request['headers'],
Referer: url
}
return axios.request(request)
.then(res => res.data)
.catch(error => {throw error});
};
In Django settings.py, I commented CSRF middleware
In Django settings.py, I added only TokenAuthentication class to remove SessionAuthentication.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
]
}
Note - Please do steps 2 and 3 at your risk after knowing your requirements properly. I removed CSRF middleware because my API was completely dependent on token for auth. I did not need CSRF in any way.

CORS errors when react gets redirected to login page

I am getting the following error messages:
-Fetch API cannot load http://localhost:8080/oauth2/authorization/google due to access control checks
-Failed to load resource: Cross-origin redirection to https://accounts.google... denied by CORS policy: Origin http://localhost:3000 is not allowed by Access-Control-Allow-Origin
I made a Kotlin-SpringBoot API and I'm in the process of developing a front end in React-TypeScript.
I used SwaggerCodegen to import the functions to interact with my API with OpenAPI docs.
I want my users to be authenticated with google OAUTH, so I decided to use Spring Security to make all my endpoints need authentication. I did it using this class:
#Configuration
class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http
.antMatcher("/**").authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login()
}
}
Now, the api is in port 8080 and the frontend is on 3000
So if I access localhost:8080/restricted I get prompted to login and it works as expected.
But if my frontend on localhost:3000 tries to retrieve any data from the API I thought It'd just redirect me anyways, but I get the CORS error above.
I have looked everywhere and just want to know if my approach is conceptually wrong or how I can authorize this type of redirecting (or if I even should).
Thanks for any help!

Waterlock, Sails, Angular JWT token gives 403 from browser

backend: Sails app w/ Waterlock, Waterlock-local-auth
frontend: An angular app
I'm running into a issue where postman works fine, gets JWT from /user/jwt all good and dandy but whenever I login through the browser and then attempt to hit /user/jwt it gives 403 forbidden. Any clues?
I have CORs enabled for all origins on all routes.
Any help would be appriciated!
To save some time for novices like me:
$http won't be passing the session data with the request, so Sails/express server doesn't know which 'session' the /user/jwt request is being made from and hence giving 403, as it should. I guess Postman handles this for you automagically.
Added this to my angular config:
config(['$httpProvider', function($httpProvider) {
$httpProvider.defaults.withCredentials = true;
}]).

Resources