Handling Sessions with Httponly Cookies in React - reactjs

What's the best practice handling user session when you get your token from HttpOnly cookies in react?
My login endpoint looks like this and as you can see token is set on cookies:
#Post('login')
#HttpCode(HttpStatus.OK)
async login(#Ip() ipAddress, #Request() req, #Res() res: Response) {
const auth = await this.basicAuthService.login(req.user, ipAddress);
const cookieOptions = setTokenCookie();
res.cookie('token', auth.token, { httpOnly: true });
res.cookie('refreshToken', auth.refreshToken, { httpOnly: true });
res.send(auth);
}
And also I have another endpoint which decodes a token in order to get user Data
#Get('user-data')
async getTokenPayload(#Request() req) {
if (!('token' in req.cookies)) {
throw new HttpException('Token was not provided', HttpStatus.NOT_FOUND);
}
const { token } = req.cookies;
return this.basicAuthService.getTokenPayload(token);
}
On FrontEnd I'm using API Context from React like this, and as you can see I'm fetching data from the /user-data endpoint:
export const UserContext = createContext<UserContextState>(userContextValue);
export const UserProvider:FC<UserProviderProps> = ({ children }) => {
const [user, setUser] = useState<User>(userInitialValue);
useEffect(() => {
const getData = async () => {
const tokenDecoded = await getUserData();
setUser(tokenDecoded.user);
};
getData();
}, []);
return (
<UserContext.Provider value={{ user, setUser }}>
{ children }
</UserContext.Provider>
);
};
It's working ok, the problem is a request is made every time the browser refreshes in order to get the users data and set it on the react state. I'm not sure whether this is a good practice, since sometimes user is not authenticated and obviously that /user-data request returns an error. I don't want to store the token on localStorage or set HttpOnly as false. Is there a better way to do it?

From what I understand is your having server side session lets say for example express-session that which I know of and can explain but I believe that concept is the same with others.
So from what I understand is if when the user is logged in and a session is made that cookie is to be set in browser and will only be removed only if the expiration date has been met besides that then that cookie will stay there. Meaning that even on page reload that cookie will never go anywhere.
So I am to highly believe from what you saying that the cookie is not getting set in browser or maybe you just mis-explained, cause if the cookie is getting set and not yet expired even on page reload should be there
So if you are using NodeJS as your back-end below is an implementation on how you can handle express-session with react app and getting that cookie set in browser once user logged in and saving that session in mongodb the instance a session is made
Firstly you will need the following packages
npm i express-session connect-mongodb-session or yarn add express-session connect-mongodb-session
Now that we have packages that we need to setup our mongoStore and express-session middleware:
//Code in server.js/index.js (Depending on your server entry point)
import expressSession from "express-session";
import MongoDBStore from "connect-mongodb-session";
import cors from "cors";
const mongoStore = MongoDBStore(expressSession);
const store = new mongoStore({
collection: "userSessions",
uri: process.env.mongoURI,
expires: 1000,
});
app.use(
expressSession({
name: "SESS_NAME",
secret: "SESS_SECRET",
store: store,
saveUninitialized: false,
resave: false,
cookie: {
sameSite: false,
secure: process.env.NODE_ENV === "production",
maxAge: 1000,
httpOnly: true,
},
})
);
Now the session middleware is ready but now you have to setup cors to accept your ReactApp so to pass down the cookie and have it set in there by server
//Still you index.js/server.js (Server entry point)
app.use(
cors({
origin: "http://localhost:3000",
methods: ["POST", "PUT", "GET", "OPTIONS", "HEAD"],
credentials: true,
})
);
Now our middlewares are all setup now lets look at your login route
router.post('/api/login', (req, res)=>{
//Do all your logic and now below is how you would send down the cooki
//Note that "user" is the retrieved user when you were validating in logic
// So now you want to add user info to cookie so to validate in future
const sessionUser = {
id: user._id,
username: user.username,
email: user.email,
};
//Saving the info req session and this will automatically save in your mongoDB as configured up in sever.js(Server entry point)
request.session.user = sessionUser;
//Now we send down the session cookie to client
response.send(request.session.sessionID);
})
Now our server is ready but now we have to fix how we make request in client so that this flow can work 100%:
Code below: React App/ whatever fron-tend that your using where you handling logging in
//So you will have all your form logic and validation and below
//You will have a function that will send request to server
const login = () => {
const data = new FormData();
data.append("username", username);
data.append("password", password);
axios.post("http://localhost:5000/api/user-login", data, {
withCredentials: true, // Now this is was the missing piece in the client side
});
};
Now with all this you have now server sessions cookies as httpOnly

Related

How to bypass NextAuth when given an external access token

This is not a common use case and even less a good practice, but for the purposes of my project, if an access token is passed as a parameter in the url (e.g.http://localhost:3000?accessToken=myAccessToken), I need to use it in my API calls and "disable" authentication with next auth.
The authentication process is just a fallback in case an accessToken is not passed.
My current implementation is:
storing the accessToken in a cookie in _app.tsx, before the auth
kicks and redirects to the login page :
_app.tsx
...
// Retrieving the callbackURL query params.
const { callbackUrl } = router.query;
// Retrieving the accessToken from the callbackURL.
const params = new URL(callbackUrl as string, 'https://example').searchParams;
const accessToken = params.get('accessToken');
// Storing it in a cookie.
if (storeNumber) {
document.cookie = `storeNumber=${storeNumber}`;
}
...
in my _middleware.ts file, trying to get this cookie, and authorize
the login if the token is present.
_middleware.ts :
export default withAuth({
pages: {
signIn: '/auth/signin',
},
callbacks: {
authorized: ({ req, token }) => {
const accessToken = getCookie('accessToken'); // => null
return !!accessToken;
},
},
});
I'm not even sure I can access the cookie from the _middleware.ts file, or if it's the right way to do this.
Any help would really be appreciated. Thank you guys.
If anyone wants the solution (doubt it), I managed to retrieve the cookie in the middleware like this :
callbacks: {
authorized: ({ req }) => {
const cookie = req.headers.get('cookie');
const accessToken = cookie.split('accessToken=')[1].split(';')[0];
console.log(accessToken);
// Do your logic
return !!accessToken
},
},

Logout from next-auth with keycloak provider not works

I have a nextjs application with next-auth to manage the authentication.
Here my configuration
....
export default NextAuth({
// Configure one or more authentication providers
providers: [
KeycloakProvider({
id: 'my-keycloack-2',
name: 'my-keycloack-2',
clientId: process.env.NEXTAUTH_CLIENT_ID,
clientSecret: process.env.NEXTAUTH_CLIENT_SECRET,
issuer: process.env.NEXTAUTH_CLIENT_ISSUER,
profile: (profile) => ({
...profile,
id: profile.sub
})
})
],
....
Authentication works as expected, but when i try to logout using the next-auth signOut function it doesn't works. Next-auth session is destroyed but keycloak mantain his session.
After some research i found a reddit conversation https://www.reddit.com/r/nextjs/comments/redv1r/nextauth_signout_does_not_end_keycloak_session/ that describe the same problem.
Here my solution.
I write a custom function to logout
const logout = async (): Promise<void> => {
const {
data: { path }
} = await axios.get('/api/auth/logout');
await signOut({ redirect: false });
window.location.href = path;
};
And i define an api path to obtain the path to destroy the session on keycloak /api/auth/logout
export default (req, res) => {
const path = `${process.env.NEXTAUTH_CLIENT_ISSUER}/protocol/openid-connect/logout?
redirect_uri=${encodeURIComponent(process.env.NEXTAUTH_URL)}`;
res.status(200).json({ path });
};
UPDATE
In the latest versions of keycloak (at time of this post update is 19.*.* -> https://github.com/keycloak/keycloak-documentation/blob/main/securing_apps/topics/oidc/java/logout.adoc) the redirect uri becomes a bit more complex
export default (req, res) => {
const session = await getSession({ req });
let path = `${process.env.NEXTAUTH_CLIENT_ISSUER}/protocol/openid-connect/logout?
post_logout_redirect_uri=${encodeURIComponent(process.env.NEXTAUTH_URL)}`;
if(session?.id_token) {
path = path + `&id_token_hint=${session.id_token}`
} else {
path = path + `&client_id=${process.env.NEXTAUTH_CLIENT_ID}`
}
res.status(200).json({ path });
};
Note that you need to include either the client_id or id_token_hint parameter in case that post_logout_redirect_uri is included.
So, I had a slightly different approach building upon this thread here.
I didn't really like all the redirects happening in my application, nor did I like adding a new endpoint to my application just for dealing with the "post-logout handshake"
Instead, I added the id_token directly into the initial JWT token generated, then attached a method called doFinalSignoutHandshake to the events.signOut which automatically performs a GET request to the keycloak service endpoint and terminates the session on behalf of the user.
This technique allows me to maintain all of the current flows in the application and still use the standard signOut method exposed by next-auth without any special customizations on the front-end.
This is written in typescript, so I extended the JWT definition to include the new values (shouldn't be necessary in vanilla JS
// exists under /types/next-auth.d.ts in your project
// Typescript will merge the definitions in most
// editors
declare module "next-auth/jwt" {
interface JWT {
provider: string;
id_token: string;
}
}
Following is my implementation of /pages/api/[...nextauth.ts]
import axios, { AxiosError } from "axios";
import NextAuth from "next-auth";
import { JWT } from "next-auth/jwt";
import KeycloakProvider from "next-auth/providers/keycloak";
// I defined this outside of the initial setup so
// that I wouldn't need to keep copying the
// process.env.KEYCLOAK_* values everywhere
const keycloak = KeycloakProvider({
clientId: process.env.KEYCLOAK_CLIENT_ID,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
issuer: process.env.KEYCLOAK_ISSUER,
});
// this performs the final handshake for the keycloak
// provider, the way it's written could also potentially
// perform the action for other providers as well
async function doFinalSignoutHandshake(jwt: JWT) {
const { provider, id_token } = jwt;
if (provider == keycloak.id) {
try {
// Add the id_token_hint to the query string
const params = new URLSearchParams();
params.append('id_token_hint', id_token);
const { status, statusText } = await axios.get(`${keycloak.options.issuer}/protocol/openid-connect/logout?${params.toString()}`);
// The response body should contain a confirmation that the user has been logged out
console.log("Completed post-logout handshake", status, statusText);
}
catch (e: any) {
console.error("Unable to perform post-logout handshake", (e as AxiosError)?.code || e)
}
}
}
export default NextAuth({
secret: process.env.NEXTAUTH_SECRET,
providers: [
keycloak
],
callbacks: {
jwt: async ({ token, user, account, profile, isNewUser }) => {
if (account) {
// copy the expiry from the original keycloak token
// overrides the settings in NextAuth.session
token.exp = account.expires_at;
token.id_token = account.id_token;
}
return token;
}
},
events: {
signOut: ({ session, token }) => doFinalSignoutHandshake(token)
}
});
signOut only clears session cookies without destroying user's session on the provider.
Year 2023 Solution:
hit GET /logout endpoint of the provider to destroy user's session
do signOut() to clear session cookies, only if step 1 was successful
Implementation:
Assumption: you are storing user's idToken in the session object returned by useSession/getSession/getServerSession
create an idempotent endpoint (PUT) on server side to make this GET call to the provider
create file: pages/api/auth/signoutprovider.js
import { authOptions } from "./[...nextauth]";
import { getServerSession } from "next-auth";
export default async function signOutProvider(req, res) {
if (req.method === "PUT") {
const session = await getServerSession(req, res, authOptions);
if (session?.idToken) {
try {
// destroy user's session on the provider
await axios.get("<your-issuer>/protocol/openid-connect/logout", { params: id_token_hint: session.idToken });
res.status(200).json(null);
}
catch (error) {
res.status(500).json(null);
}
} else {
// if user is not signed in, give 200
res.status(200).json(null);
}
}
}
wrap signOut by a function, use this function to sign a user out throughout your app
import { signOut } from "next-auth/react";
export async function theRealSignOut(args) {
try {
await axios.put("/api/auth/signoutprovider", null);
// signOut only if PUT was successful
return await signOut(args);
} catch (error) {
// <show some notification to user asking to retry signout>
throw error;
}
}
Note: theRealSignOut can be used on client side only as it is using signOut internally.
Keycloak docs logout

How to refresh Firebase IdToken after it expires?

I'm using express as my backend and I'm using the Firebase Admin SDK to send back the token to the client.
At the moment the token is expired after 1 Hour. I read on the firebase that there isn't any way to change the expiration property because the way it works - the user will get a refreshed token every hour. Is that correct? If so, how I supposed to implement it?
Here is my login route:
exports.login = async (req, res) => {
const user = {
email: req.body.email,
password: req.body.password,
}
// Validate Data
const { valid, errors } = validateLogin(user)
if (!valid) return res.status(400).json(errors)
try {
const data = await firebase
.auth()
.signInWithEmailAndPassword(user.email, user.password)
const token = await data.user.getIdToken()
const cookieOptions = {
httpOnly: true,
secure: false,
}
res.cookie('jwt', token, cookieOptions)
return res.status(201).json({ token })
} catch (err) {
errors.general = 'Wrong credentials, please try again'
return res.status(403).json(errors)
}
}
There is nothing to do on the backend to implement token refreshing. The client app will do that automatically using the Firebase Auth SDK.
Anyway, backend apps aren't supposed to sign in the user - that won't work out the way you expect. When using Firebase Auth, client apps are supposed to sign in using the client SDK. The client app signs in, gets a token, then passes that token to the backend to be verified using the Firebase Admin SDK, then acted on.

Google-OAuth REST Api to react

I have successfully created an API that uses passport-google-oauth to return a JWT. Currently when I go through the process using my API routes it returns a json object with a JWT Bearer token.
I am attempting to use Reactjs on the front end however am running into a couple issues.
In my signin button component I am just trying to retrieve the result with the bearer token to pass it into a reducer
When using Axios -> I am running into a CORS issue when using exios and cant return a result, when adding CORS into my build and a proxy to my react project I recieve the following error No 'Access-Control-Allow-Origin' header is present on the requested resource.
When I use a anchor tag with href link the authentication successfully works however it redirects to the /api/auth/google/callback link itself instead of allowing me to catch the bearer token and then run it through my reducers to save it into local storage and update my state.
Am I missing a step? Ive looked for a few hours at various resources online and cant seem to find the solution im looking for
React
(for simplicity at the moment I am just trying to catch the response, which should be returned bearer token, however I am unable to do this)
googleAuth = (e) => {
e.preventDefault()
axios.get('/api/auth/google')
.then(res => console.log(res))
.catch(err => console.log(err))
}
render() {
return (
<button onClick={this.googleAuth}>Signin With Google</button>
)
}
API
Routes
router.get('/google', passport.authenticate('google', {
session: false,
scope: ['profile', 'email']
}))
router.get('/google/callback', passport.authenticate('google', { session: false }), generateUserToken)
Strategy
passport.use(new passportGoogle.OAuth2Strategy(googleConfig, async (request, accessToken, refreshToken, profile, done) => {
// Check for existing user
const existingUser = await User.findOne({
providers: {
$elemMatch: {
provider: 'Google',
providerId: profile.id
}
}
})
// If user exists return done
if (existingUser) return done(null, existingUser)
// If user does not exist create a new user
const newUser = await new User({
name: profile.displayName,
providers: [
{
provider: 'Google',
providerId: profile.id
}
]
}).save()
// Create profile with new user information
const newProfile = await new Profile({
userId: newUser.id
}).save()
return done(null, newUser)
}))
I've looked a bit at your code and haven't seen any serializing/deserializing going on. Usually you'd have to go through this process to be able to connect with whatever authentication strategy you are using.
Here is a snippet of code that you could use in the file you keep your strategies:
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findById(id).then(user => {
done(null, user);
});
});
Maybe you could look it up in more detail in the documentation. Here is a link http://www.passportjs.org/docs/ go to the sessions part. Plus, make sure to look at how the app.use is put together with the .session() func.

How to refresh Cognito session in async function that needs to return valid credentials

I have a function that runs right after I configure my Redux store:
function configureApollo(store) {
return new AWSAppSyncClient({
url: AppSync.graphqlEndpoint,
region: AppSync.region,
auth: {
credentials: async () => {//...code that returns valid credentials}
}
})
}
I'm using react-cognito to manage credentials and it stores them in the redux store under store.getState().cognito.creds. My problem is when credentials are expired and need to be refreshed, react-cognito exports a function performLogin which returns a promise for a "COGNITO_LOGIN" action which has a payload with refreshed credentials.
I feel like I should be dispatching the returned action and pulling the new credentials from the store after it's updated, but dispatch has no callback.
I think I can read the credentials out of the COGNITO_LOGIN action, dispatch the action, and never worry about reading the credentials from store.getState()
const resultAction = await performLogin(user, config, group);
store.dispatch(resultAction);
return new AWSCredentials(
{
accessKeyId: resultAction.creds.accessKeyId,
secretAccessKey: resultAction.creds.secretAccessKey,
sessionToken: resultAction.creds.sessionToken
})
...but this seems wrong. What if there is an error in the reducer saving the credentials to the store? Then my AWSAppSyncClient has credentials that are out of sync with the rest of my app.
Is there a better approach here?
If you use the aws-amplify libraries, it's a lot easier. Take a look at the section on authentication in the README.md at https://github.com/aws/aws-amplify#2-add-authentication-to-your-app to use it to add AWS Cognito authentication to your app. After that, you can configure the credentials for AWS AppSync via the following
import { Auth } from 'aws-amplify';
import AWSAppSyncClient from 'aws-appsync';
import { AUTH_TYPE } from 'aws-appsync/lib/link/auth-link'
import AppSync from './AppSync.js';
const client = new AWSAppSyncClient({
url: AppSync.graphqlEndpoint,
region: AppSync.region,
auth: {
type: AUTH_TYPE.AWS_IAM,
credentials: () => Auth.currentCredentials()
},
complexObjectsCredentials: () => Auth.currentCredentials(),
});

Resources