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.
Related
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.
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 am new to node.js, express and JWT. I found similar questions here but they didn't help.
I am able to login and store the token in local storage but when I try set Authorization header for another request with the same token, it fails verification in the server. I checked the token both from the server and client, they are exactly the same but the verification is failing, please help !
Here is the code I am using to verify the token.
exports.verify = function(req, res, next) {
let accessToken = req.headers.authorization
if (!accessToken){
return res.status(403).send()
}
let payload
try{
// Never makes it through this
payload = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET)
next()
}
catch(e){
return res.status(401).json({success: false, message: "token expired or invalid"})
}
}
Here in app.js file I use the verify function like this for another route.
const { verify } = require('./controllers/auth')
const userRoutes = require('./routes/userRoutes')
app.use('/user', verify, userRoutes)
Where am I going wrong here ?
EDIT:
I added console.log(e) in the verify function, inside the catch() and got the the below result.
TokenExpiredError: jwt expired
at /home/shashank/Documents/sms/server/node_modules/jsonwebtoken/verify.js:152:21
at getSecret (/home/shashank/Documents/sms/server/node_modules/jsonwebtoken/verify.js:90:14)
at Object.module.exports [as verify] (/home/shashank/Documents/sms/server/node_modules/jsonwebtoken/verify.js:94:10)
at exports.verify (/home/shashank/Documents/sms/server/controllers/auth.js:62:23)
at Layer.handle [as handle_request] (/home/shashank/Documents/sms/server/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/home/shashank/Documents/sms/server/node_modules/express/lib/router/index.js:317:13)
at /home/shashank/Documents/sms/server/node_modules/express/lib/router/index.js:284:7
at Function.process_params (/home/shashank/Documents/sms/server/node_modules/express/lib/router/index.js:335:12)
at next (/home/shashank/Documents/sms/server/node_modules/express/lib/router/index.js:275:10)
at cookieParser (/home/shashank/Documents/sms/server/node_modules/cookie-parser/index.js:57:14)
{ expiredAt: 2020-10-29T17:30:31.000Z }
Let me show my .env file where I store the secret key info
ACCESS_TOKEN_SECRET=swsh23hjddnns
ACCESS_TOKEN_LIFE=3600
REFRESH_TOKEN_SECRET=dhw782wujnd99ahmmakhanjkajikhiwn2n
REFRESH_TOKEN_LIFE=86400
So, the access token has to last an hour right ?
The token is created like below
const jwt = require('jsonwebtoken')
// Not using a database right now.
let users = {
email: 'myemail#gmail.com',
password: 'password'
}
exports.login = function(req, res) {
let email = req.body.email
let password = req.body.password
// Simple validation !
if (!email || !password || users.email !== email || users.password !== password){
return res.status(401).json({success: false, message: "Incorrect username or password"})
}
//use the payload to store information about the user such as username, user role, etc.
let payload = {email: email}
//create the access token with the shorter lifespan
let accessToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, {
algorithm: "HS256",
expiresIn: process.env.ACCESS_TOKEN_LIFE
})
//create the refresh token with the longer lifespan
let refreshToken = jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET, {
algorithm: "HS256",
expiresIn: process.env.REFRESH_TOKEN_LIFE
})
//store the refresh token in the user array
users.refreshToken = refreshToken
//send the access token to the client inside a cookie
// res.cookie("jwt", accessToken, { httpOnly: true}) //secure: false, use this along with httpOnly: true in production
// res.setHeader('Authorization', accessToken);
res.json({
accessToken: accessToken,
success: true, message: "Authentication success"
});
res.send()
}
It looks like the problem you are experiencing is because of how you are storing the time out period.
From the Documentation for node-jsonwebtoken
expiresIn: expressed in seconds or a string describing a time span
zeit/ms. Eg: 60, "2 days", "10h", "7d". A numeric value is interpreted
as a seconds count. If you use a string be sure you provide the time
units (days, hours, etc), otherwise milliseconds unit is used by
default ("120" is equal to "120ms").
Because you are storing in your process.env, it looks like it is translating it to a string, instead of maintaining the integer value.
Test Code:
const jwt = require('jsonwebtoken')
require('dotenv').config();
let payload = {email: 'email'}
let accessToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, {
algorithm: "HS256",
expiresIn: process.env.ACCESS_TOKEN_LIFE
})
console.log(accessToken);
console.log('waiting 4 seconds');
setTimeout(function() {
let val = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET);
console.log(val);
}, 4000);
With the following process.env values, it fails
ACCESS_TOKEN_SECRET=swsh23hjddnns
ACCESS_TOKEN_LIFE=3600
REFRESH_TOKEN_SECRET=dhw782wujnd99ahmmakhanjkajikhiwn2n
REFRESH_TOKEN_LIFE=86400
But if we change the ACCESS_TOKEN_LIFE to
ACCESS_TOKEN_LIFE=3600S
It succeeds
Without the time unit, any request that is delayed by more than 3.6 Seconds will error out.
I'm having trouble with Jwt and especially an error "Invalid Signature".
I'm generating a token after the user logs in (jsonwebtoken).
userSchema.methods.generateJwt = function() {
var expiry = new Date();
//expiry.setDate(expiry.getDate() + 7);
expiry.setDate(expiry.getDate() + 2);
return jwt.sign({
_id: this._id,
username: this.username,
name: this.lastname,
exp: parseInt(expiry.getTime() / 1000),
}, process.env.SRCT, {
algorithm: 'HS256'
});
}
Then I'm creating an express-jwt middleware to add it to routes :
var auth = jwt({
secret: process.env.SRCT,
userProperty: 'payload'
});
Used like this :
router.get('/', auth, ctrlUser.slash);
My JWT created is passed in the front end request (Authorization bearer) and is the same as the one created right after the login, according to the debugger.
But unfortunatly, I'm still having the error {"message":"UnauthorizedError: invalid signature"} after each request to the nodejs backend.
Could someone tell me what I am doing wrong to have an invalid signature?
Thanks in advance
Where is your verify function ? You need to check on every request made to a protected area that token is really valid, jwt provides a function verify to do that.
You don't seem to be parsing the request headers for the token, nor using verify() function of the JWT library for that. your auth middleware should look something like this
module.exports = (req, res, next) => {
try {
//parse the token from Authorization header (value of "bearer <token>")
let token = req.headers.authorization.split(" ")[1];
//verify the token against your secret key to parse the payload
const tokenData = jwt.verify(token, process.env.JWT_SECRET_KEY);
//add the data to the request body if you wish
req.user = tokenData;
next();
} catch (err) {
res.status(401).json({
message: "Unauthorized access error!",
});
}
};
I notice that the docs specify that I can create a token to expire up to 3600 seconds later[1] But I don't see how to do that with auth().createCustomToken ... I can manually do it with jsonwektoken, but it seems like this should be addressable directly with firebase-admin library.
Another question is, what is the secret I need to verify my own token generated in this way, the uid ?
index.js
// demo server generating custom auth for firebase
import Koa from 'koa'
import Koajwt from 'koa-jwt'
import Token from './token'
const app = new Koa()
// Custom 401 handling if you don't want to expose koa-jwt errors to users
app.use(function(ctx, next){
return next().catch((err) => {
if (401 == err.status) {
ctx.status = 401
ctx.body = 'Protected resource, use Authorization header to get access\n'
} else {
throw err
}
})
})
// Unprotected middleware
app.use(function(ctx, next){
if (ctx.url.match(/^\/login/)) {
// use router , post, https to securely send an id
const conf = {
uid: 'sample-user-uid',
claims: {
// Optional custom claims to include in the Security Rules auth / request.auth variables
appid: 'sample-app-uid'
}
}
ctx.body = {
token: Token.generateJWT(conf)
}
} else {
return next();
}
});
// Middleware below this line is only reached if JWT token is valid
app.use(Koajwt({ secret: 'shared-secret' }))
// Protected middleware
app.use(function(ctx){
if (ctx.url.match(/^\/api/)) {
ctx.body = 'protected\n'
}
})
app.listen(3000);
token.js
//import jwt from 'jsonwebtoken'
import FirebaseAdmin from 'firebase-admin'
import serviceAccount from 'demo-admin-firebase-adminsdk-$$$$-$$$$$$.json'
export default {
isInitialized: false,
init() {
FirebaseAdmin.credential.cert(serviceAccount)
isInitialized = true
},
/* generateJWTprimiative (payload, signature, conf) {
// like: jwt.sign({ data: 'foobar' }, 'secret', { expiresIn: '15m' })
jwt.sign(payload, signature, conf)
} */
generateJWT (conf) {
if(! this.isInitialized)
init()
FirebaseAdmin.auth().createCustomToken(conf.uid, conf.claims)
.then(token => {
return token
})
.catch(err => {
console.log('no token generate because', err)
})
}
}
[1] https://firebase.google.com/docs/auth/admin/create-custom-tokens
You can't change the token expiration. The docs you found includes the words:
Firebase tokens comply with the OpenID Connect JWT spec, which means
the following claims are reserved and cannot be specified within the
additional claims:
... exp ...
This is further backed up by inspecting the Firebase Admin SDK source code on GitHub.
In this section:
public createCustomToken(uid: string, developerClaims?: {[key: string]: any}): Promise<string> {
// .... cut for length ....
const header: JWTHeader = {
alg: ALGORITHM_RS256,
typ: 'JWT',
};
const iat = Math.floor(Date.now() / 1000);
const body: JWTBody = {
aud: FIREBASE_AUDIENCE,
iat,
exp: iat + ONE_HOUR_IN_SECONDS,
iss: account,
sub: account,
uid,
};
if (Object.keys(claims).length > 0) {
body.claims = claims;
}
// .... cut for length ....
You can see the exp property is hard coded to be iat + ONE_HOUR_IN_SECONDS where the constant is defined elsewhere in the code as 60 * 60...
If you want to customize the expiration time, you will HAVE to create your own token via a 3rd party JWT package.
To your 2nd question, a secret is typically stored in the server environment variables, and is a pre-set string or password. Technically you could use the UID as the secret, but that would be a TERRIBLE idea security wise - please don't do this. Your secret should be like your password, keep it secure and don't upload it with your source code to GitHub. You can read more about setting and retrieving environment variables in Firebase in these docs here