Sending a cookie as a response with Firebase Callable Functions - node.js

I am trying to send a cookie with options set to it as a response using a Firebase callable cloud function (https.onCall). I see in the Firebase docs that this can be done with express:
(The below is taken directly form the Firebase docs)
app.post('/sessionLogin', (req, res) => {
// Get the ID token passed and the CSRF token.
const idToken = req.body.idToken.toString();
const csrfToken = req.body.csrfToken.toString();
// Guard against CSRF attacks.
if (csrfToken !== req.cookies.csrfToken) {
res.status(401).send('UNAUTHORIZED REQUEST!');
return;
}
// Set session expiration to 5 days.
const expiresIn = 60 * 60 * 24 * 5 * 1000;
// Create the session cookie. This will also verify the ID token in the process.
// The session cookie will have the same claims as the ID token.
// To only allow session cookie setting on recent sign-in, auth_time in ID token
// can be checked to ensure user was recently signed in before creating a session cookie.
getAuth()
.createSessionCookie(idToken, { expiresIn })
.then(
(sessionCookie) => {
// Set cookie policy for session cookie.
const options = { maxAge: expiresIn, httpOnly: true, secure: true };
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({ status: 'success' }));
},
(error) => {
res.status(401).send('UNAUTHORIZED REQUEST!');
}
);
});
I have implemented the callable function, but I do now know how to attach the options to my cookie string.
The below is my code:
// I want the return type to be a Promise of a cookie object, not a string
export const setCookie = https.onCall(async (context: https.CallableContext): Promise<string> => {
try {
console.log(context);
const auth: Auth = getAuth();
const idToken: DecodedIdToken = await auth.verifyIdToken(context.instanceIdToken!); // https://firebase.google.com/docs/auth/admin/verify-id-tokens#web
console.log("idToken: ", idToken);
const cookie: string = await auth.createSessionCookie(idToken.uid, { expiresIn: 300000 });
const options = {
maxAge: 300000,
httpOnly: true,
secure: true,
sameSite: "strict",
};
// res.cookie("session", cookie, options);
return cookie; // should be assigned to __session cookie with domain .web.app
// httpOnly=true, secure=true and sameSite=strict set.
} catch (error) {
console.log("ERROR FOUND: ", error);
throw new https.HttpsError("unknown", "Error found in setCookie");
}
});
Is there any way I can do this using a Callable Firebase Cloud Function? All the documentation and resources I have found require express to send an cookie with Node.
Thanks!

The documentation you're linking to assumes you are writing standard nodejs backend code using express. However, your code is using a callable type function. They are not the same and do not have the same capabilities. Callable functions don't let you set cookies in the response. You can only send a JSON payload back to the client; the SDK handles all of the HTTP headers and they are outside of your control.
Perhaps you should look into using a standard HTTP type function (onRequest), where you do have some control over the headers in the response.

Related

http request with status 200 but no response data

I am trying to make an http request on refresh within my Angular frontend to a nodejs backend and expect to receive a token as response. Sometimes the request gets cancelled and even if its successful (200) i do not get send the token in the response.
When i make a postman request the request is always successful and sends the token, also when i make the request in Angular constructor without the refresh logic, so i suspect it has something to do with the usage of rxjs but can not figure out whats the problem.
here is the logic of refresh in app.component
constructor(
private router: Router,
private auth: AuthService
) {
// this.auth.requestServer(ServerMethod.GET, '/refresh').subscribe() // this request would work fine
router.events.pipe(
switchMap((event) => {
if (event instanceof NavigationStart) {
const browserRefresh = !router.navigated;
if (browserRefresh) {
this.auth.deleteAccessToken();
return this.auth.requestServer(ServerMethod.GET, '/refresh');
}
}
return EMPTY;
})
).subscribe();
}
here is deleteAccessToken()
deleteAccessToken() {
sessionStorage.removeItem('accessToken');
this.tokenSubject.next(null);
}
requestServer()
requestServer(type: ServerMethod, path?: string, data?: any): Observable<any> {
let httpOptions: httpOptions;
switch (type) {
case ServerMethod.POST:
return this.server.post(path, data).pipe(tap(res => this.handleAccessToken(res)));
case ServerMethod.GETALL:
return this.server.getAll(this.getAllPath);
case ServerMethod.GET:
return this.server.get(path).pipe(tap(res => this.handleAccessToken(res)));
default:
return EMPTY;
}
}
here is server get method
get(path: string): Observable<any> {
const url = this.serverAdress + path;
return this.http.get(url);
}
and in my nodejs backend here is the refresh logic:
module.exports.refresh_get = async (req, res) => {
if (req.cookies && req.cookies.refreshToken) {
// Destructuring refreshToken from cookie
const refreshToken = req.cookies.refreshToken;
// Verifying refresh token
jwt.verify(refreshToken, 'secret',
(err, decodedToken) => {
if (err) {
// Wrong Refesh Token
res.status(406).json({ message: 'wrong refresh token' });
}
else {
// create new accessToken
const accessToken = createToken(decodedToken.id);
// create new refreshToken and set it in cookie and delete old cookie
const newRefreshToken = jwt.sign({
id: decodedToken.id,
}, 'secret', { expiresIn: '1d' });
res.cookie('refreshToken', newRefreshToken, { httpOnly: true,
sameSite: 'None', secure: true,
maxAge: 24 * 60 * 60 * 1000 });
res.status(200).json({ accessToken });
}
})
} else {
res.status(406).json({ message: 'Unauthorized' });
}
}
request in network tab on refresh looks then like this:
but Response is empty, there should be an object { accessToken: '...' }
ChatGPT answered my question:
It's possible that the problem lies with the switchMap operator in the router.events observable. The switchMap operator cancels the previous inner observable when a new value is emitted, which could result in the HTTP request being cancelled if it takes too long to complete.
To ensure that the HTTP request is not cancelled, you can try using the concatMap operator instead of switchMap. concatMap will wait for the previous observable to complete before starting a new one, which will prevent the HTTP request from being cancelled prematurely.
Thanks ChatGPT.

Cannot destructure property 'token' of 'req.cookie' as it is undefined. Nodejs

This is my code where I save the token into a cookie
const sendToken = (user, statusCode, res) => {
const token = user.getJWTToken();
//options for cookie
const options ={
expires: new Date(
Date.now + process.env.COOKIE_EXPIRE * 24 * 60 * 60 * 1000
),
httpOnly: true
};
res.status(statusCode).cookie('token', token, options).json({
success: true,
user,
token
});
};
module.exports = sendToken;
I check in postman and the cookie has been saved
But later on when I try to get it in this function:
exports.isAuthenticatedUser = catchAsyncErrors( async(req,res,next) => {
const { token } = req.cookie;
if(!token){
return next(new ErrorHandler("Please Login to Access this Resource.", 401));
}
const decodedData = JsonWebTokenError.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decodedData.id);
next();
});
It given the error Cannot destructure property 'token' of 'req.cookie' as it is undefined.
I'm new to Nodejs so I was following a tutorial. So I'm not sure what I'm doing wrong.
If you are using cookieParser (from the way you are setting the cookie), try changing this:
const { token } = req.cookie;
To this:
const { token } = req.cookies['token']
Make sure you use something like cookie-parser and that your user is logged in.
You can also see cookies in the postman cookie section. See below - if the cookie is not there, then you have to generate a cookie first.

How to clear an set cookies with Apollo Server

I recently just switched from using express with apollo server to just using apollo server since the subscriptions setup seemed more current and easier to setup. The problem I'm having now is I was saving a cookie with our refresh token for login and clearing the cookie on logout. This worked when I was using express.
const token = context.req.cookies[process.env.REFRESH_TOKEN_NAME!];
context.res.status(401);
Since switching from express/apollo to just apollo server I don't have access to req.cookies even when i expose the req/res context on apollo server.
I ended up switching to this (which is hacky) to get the cookie.
const header = context.req.headers.cookie
var cookies = header.split(/[;] */).reduce(function(result: any, pairStr: any) {
var arr = pairStr.split('=');
if (arr.length === 2) { result[arr[0]] = arr[1]; }
return result;
}, {});
This works but now I can't figure out how to delete the cookies. With express I was doing
context.res.clearCookie(process.env.REFRESH_TOKEN_NAME!);
Not sure how I can clear cookies now since res.clearCookie doesn't exist.
You do not have to specifically clear the cookies. The expiresIn cookie key does that for you. Here is the snippet which i used to set cookies in browser from apollo-server-lambda. Once the expiresIn date values has passed the current date time then the cookies wont be valid for that host/domain. You need to revoke access token for the user again or logout the user from the application
import { ApolloServer, AuthenticationError } from "apollo-server-lambda";
import resolvers from "./src/graphql/resolvers";
import typeDefs from "./src/graphql/types";
const { initConnection } = require("./src/database/connection");
const { validateAccessToken, hasPublicEndpoint } = require("./src/bll/user-adapter");
const { addHoursToDate } = require("./src/helpers/utility");
const corsConfig = {
cors: {
origin: "http://localhost:3001",
credentials: true,
allowedHeaders: [
"Content-Type",
"Authorization"
],
},
};
// creating the server
const server = new ApolloServer({
// passing types and resolvers to the server
typeDefs,
resolvers,
context: async ({ event, context, express }) => {
const cookies = event.headers.Cookie;
const accessToken = ("; " + cookies).split(`; accessToken=`).pop().split(";")[0];
const accessLevel = ("; " + cookies).split(`; accessLevel=`).pop().split(";")[0];
const expiresIn = ("; " + cookies).split(`; expiresIn=`).pop().split(";")[0];
const { req, res } = express;
const operationName = JSON.parse(event.body).operationName;
if (await hasPublicEndpoint(operationName)) {
console.info(operationName, " Is a public endpoint");
} else {
if (accessToken) {
const jwtToken = accessToken.split(" ")[1];
try {
const verifiedUser = await validateAccessToken(jwtToken);
console.log("verifiedUser", verifiedUser);
if (verifiedUser) {
return {
userId: verifiedUser,
};
} else {
console.log();
throw new AuthenticationError("Your token does not verify!");
}
} catch (err) {
console.log("error", err);
throw new AuthenticationError("Your token does not verify!");
}
}
}
return {
headers: event.headers,
functionName: context.functionName,
event,
context,
res,
};
},
cors: corsConfig,
formatResponse: (response, requestContext) => {
if (response.data?.authenticateUser || response.data?.revokeAccessToken) {
// console.log(requestContext.context);
const { access_token, user_type, access_token_generated_on, email } =
response.data.authenticateUser || response.data.revokeAccessToken;
const expiresIn = addHoursToDate(new Date(access_token_generated_on), 12);
requestContext.context.res.set("set-cookie", [
`accessToken=Bearer ${access_token}`,
`accessLevel=${user_type}`,
`expiresIn=${new Date(access_token_generated_on)}`,
`erUser=${email}`,
]);
}
if (response.data?.logoutUser) {
console.log("Logging out user");
}
return response;
},
});
Simply send back the exact same cookie to the client with an Expires attribute set to some date in the past. Note that everything about the rest of the cookie has to be exactly the same, so be sure to keep all the original cookie attributes, too.
And, here's a link to the RFC itself on this topic:
Finally, to remove a cookie, the server returns a Set-Cookie header
with an expiration date in the past. The server will be successful
in removing the cookie only if the Path and the Domain attribute in
the Set-Cookie header match the values used when the cookie was
created.
As to how to do this, if you're using Node's http module, you can just use something like this (assuming you have a response coming from the callback passed to http.createServer):
context.response.writeHead(200, {'Set-Cookie': '<Your Cookie Here>', 'Content-Type': 'text/plain'});
This is assuming that your context has access to that http response it can write to.
For the record, you can see how Express does it here and here for clarity.

i want to get my jwt value from cookies in browser

i have now stored my jwt in cookies when user sign in or sign up but the data don't stay so i made a function to handle this but i need the value of the token to make it work
this is the function that i need token value for
const setAuthToken = (token) => {
if (token) {
axios.defaults.headers.common['x-auth-token'] = token;
} else {
delete axios.defaults.headers.common['x-auth-token'];
}
};
and this is my action that i use in react to send the token value to this function i tried to use js-cookies for that but it give me undefined
import Cookies from 'js-cookie';
//load user
export const loadUser = () => async (dispatch) => {
const token = Cookies.get('access_token');
console.log(token);
// if (cookie.access_token) {
// setAuthToken(cookie.access_token);
// }
try {
const res = await axios.get('/user/me');
dispatch({
type: USER_LOADED,
payload: res.data,
});
} catch (err) {
dispatch({
type: AUTH_ERROR,
});
}
};
and this is my recieved cookie in browser
If you take a close look at your screenshot, you can see that the cookie is sent by the server as HttpOnly. This is a security measure, and therefore the cookie isn't accessible to any JavaScript code by design.
See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Restrict_access_to_cookies
If you are in control of the server, you could change it accordingly, if not you will have to make a deal :-)
res.cookie('x-auth-token',token,{
maxAge: 3600,
httpOnly: true,
secure:true
})

How to secure JWT token and refresh token in nodejs/angular

i am new in node js. I am building a simple notes taking app and wanted to use JWT tokens for authentication and to secure my API's. On research i came to know that i need to create two tokens:
access token (short expire time like 10 minutes)
refresh token (longer expire time 30 days)
My config file
"secret": "*************",
"refreshTokenSecret": "*************",
"port": 5000,
"tokenLife": 900,
"refreshTokenLife": 86400
Code for middleware
const jwt = require('jsonwebtoken')
const config = require('./config')
module.exports = (req,res,next) => {
const token = req.body.token || req.query.token || req.headers['x-access-token']
// decode token
if (token) {
// verifies secret and checks exp
jwt.verify(token, config.secret, function(err, decoded) {
if (err) {
return res.status(401).json({"error": true, "message": 'Unauthorized access.' });
}
req.decoded = decoded;
next();
});
} else {
// if there is no token
// return an error
return res.status(403).send({
"error": true,
"message": 'No token provided.'
});
}
}
Here is the response
access token can be saved in local storage. but articles said save refresh token as http-only cookie.
i need the answer of following points (Keeping in mind that i am just a beginner):
How to store refresh token as http-only cookie (any node-js code
example would be a great help)?
How to secure it on client side and should I save refresh token to database?
Is there any other better solution to secure my API's?
You can use an http-only cookie using the following:
public authenticateUser(user: User, res: Response) {
const authJwtToken = this.generateJWT({
email: user.email,
uuid: user.uuid
});
const cookieOptions = {
maxAge: 3600000,
secure: true,
httpOnly: true
};
res.cookie('access_token', authJwtToken, cookieOptions);
}
// you can then res.send({...}) or wtv
Not that there is nothing from preventing you to store more than one cookie so I can't see a reason why not storing both of them in the same manner.
Now whether you will store it on the database depends on what you want to achieve.
Generally it is not required but note that in that case the server cannot in any way invalidate a single JWT. (You could in theory change the signing key but this would invalidate all of them).
In case you want to be able to achieve functionality such as 'log me out of all devices' you would need to store the JWTs issued for each user in a database (preferably an in-memory one such as Redis or Memcached) and do a second check with the extra information on whether they have been invalidated or not - even though such functionality is typically achieved using sessions instead of JWT
See this example how i secured my getByRefId api in nodjs :
In routes file :
router.get("/refId/:refId", helper.auth, groupController.getByRefId);
helper.auth is function :
auth: (req, res, next) => {
var token = req.body.token || req.headers['authorization'] || req.headers['Authorization'];
if (token.startsWith('Bearer ')) {
// Remove Bearer from string
token = token.slice(7, token.length).trimLeft();
}
if (token) {
jwt.verify(token, 'MY_SECRET', function (err, decoded) {
if (err) {
console.error('JWT Verification Error', err);
return res.status(403).send(err);
} else {
req.payload = decoded;
return next();
}
});
} else {
res.status(403).send('Token not provided');
}
}
This use jwt = require('jsonwebtoken') library you can install it in nodejs project

Resources