I'm trying to assign a generate a session cookie in exchange for the provided ID token. Here are the docs I'm following.
Here is my client side sign-in code:
firebase.auth().signInWithEmailAndPassword(email, password)
.then(function(user) {
firebase.auth().currentUser.getIdToken(/* forceRefresh */ true).then(function(idToken) {
return sendToken(idToken);
});
})
// .then(() => {
// return firebase.auth().signOut();
// })
.then(() => {
window.location.assign('/member');
})
.catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
alert(errorMessage);
});
My send sendToken() posts the idToken to the server:
function sendToken(idToken) {
console.log("Posting " + idToken);
var xhr = new XMLHttpRequest();
var params = `token=${idToken}`;
xhr.open('POST', "/login", true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
return new Promise(function(resolve, reject) {
xhr.onreadystatechange = function() {//Call a function when the state changes.
if (xhr.readyState == 4 && xhr.status == 200) {
resolve();
} else if (xhr.readyState == 4 && xhr.status != 200) {
reject("Invalid http return status");
}
}
return xhr.send(params);
});
}
And at the server, I'm returning a session cookie:
app.post('/login', (req, res) => {
if (req.body.token) {
const idToken = req.body.token.toString();
// Set session expiration to 1 day.
const expiresIn = 60 * 60 * 24 * 1 * 1000;
return firebase.auth().createSessionCookie(idToken, {expiresIn}).then((sessionCookie) => {
const options = {maxAge: expiresIn, httpOnly: true, secure: true};
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({status: 'success'}));
})
.catch((error) => {
res.status(401).send('UNAUTHORIZED REQUEST!');
});
}
return res.status(400).send("MISSING TOKEN");
});
I've also set up a middleware to verify the session cookie before the server serves the member info:
function authVerification(req, res, next){
const sessionCookie = req.cookies.session || '';
// Verify the session cookie. In this case an additional check is added to
detect
// if the user's Firebase session was revoked, user deleted/disabled, etc.
return firebase.auth().verifySessionCookie(
sessionCookie, true /** checkRevoked */).then((decodedClaims) => {
console.log("decoded claims: " + decodedClaims);
next();
// serveContentForUser('/profile', req, res, decodedClaims);
}).catch(error => {
// Session cookie is unavailable or invalid. Force user to login.
res.send(error);
});
}
But, when I try to get a member's page after signing in:
app.get("/member", authVerification, (req, res) => {
res.send("member page");
});
I keep getting error from authVerification:
code: "auth/argument-error",
message: "Decoding Firebase session cookie failed. Make sure you passed the entire string JWT which represents a session cookie. See https://firebase.google.com/docs/auth/admin/manage-cookies for details on how to retrieve a session cookie."
Can anyone please point me in the right direction. Thank you in advance!
Solution in the quickstart-nodejs repo.
enter link description here
You have to add cookie-parser and body-parser. Here is how I solved it with Firebase Cloud Functions:
const admin = require("firebase-admin");
const functions = require("firebase-functions");
const next = require("next");
const cors = require("cors");
const express = require("express");
const cookieParser = require("cookie-parser");
const bodyParser = require("body-parser");
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev, conf: { distDir: "next" } });
const handle = app.getRequestHandler();
admin.initializeApp({
credential: admin.credential.cert("Service account key"),
databaseURL: "Path to database"
});
const server = express();
server.use(cors({ origin: true }));
server.use(bodyParser.json());
// Support URL-encoded bodies.
server.use(bodyParser.urlencoded({
extended: true
}));
// Support cookie manipulation.
server.use(cookieParser());
// Attach CSRF token on each request.
server.use(attachCsrfToken('/', 'csrfToken', (Math.random()* 100000000000000000).toString()));
function attachCsrfToken(url, cookie, value) {
return function(req, res, next) {
if (req.url === url) {
res.cookie(cookie, value);
}
next();
}
}
server.post("/login", (req, res) => {
if (req.body && req.body.idToken) {
const idToken = `${req.body.idToken}`;
const expiresIn = 60 * 60 * 24 * 5 * 1000;
admin.auth().createSessionCookie(idToken, { expiresIn }).then((sessionCookie) => {
const options = { maxAge: expiresIn, httpOnly: true, secure: true };
res.cookie("session", sessionCookie, options);
res.end(JSON.stringify({ sessionCookie }));
}, error => {
res.status(401).send(error);
});
} else {
res.status(401).send("Token empty");
}
});
server.post("/profile", (req, res) => {
if (req.cookies && req.cookies.session) {
const sessionCookie = `${req.cookies.session}`;
admin.auth().verifySessionCookie(
sessionCookie, true /** checkRevoked */).then((decodedClaims) => {
res.end(JSON.stringify({ decodedClaims }));
}).catch(error => {
res.status(401).send(error);
});
} else {
res.status(401).send("Session empty");
}
});
exports.next = functions.https.onRequest((req, res) => {
if (req.method === "POST") {
if (!req.path) req.url = `/${request.url}`;
return server(req, res);
}
return app.prepare().then(() => handle(req, res));
});
Nothing special on the client side. You can use isomorphic-unfetch, axios or jQuery.
Related
Using thunderclient (similar to postman) i cant access this employee api which requires a jwt, even though i confirm i am already authorized. Here is my code:
authController:
const usersDB = {
users: require("../model/users.json"),
setUsers: function (data) {
this.users = data;
},
};
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
require("dotenv").config();
const fsPromises = require("fs").promises;
const path = require("path");
const handleLogin = async (req, res) => {
const { user, pwd } = req.body;
if (!user || !pwd)
return res
.status(400)
.json({ message: "Username and password are required." });
const foundUser = usersDB.users.find((person) => person.username === user);
console.log(foundUser);
if (!foundUser) return res.sendStatus(401);
const match = await bcrypt.compare(pwd, foundUser.password);
if (match) {
const accessToken = jwt.sign(
{ username: foundUser.username },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: "60s" }
);
const refreshToken = jwt.sign(
{ username: foundUser.username },
// we need our secret from env file as well to make our jwt
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: "1d" }
);
const otherUsers = usersDB.users.filter(
(person) => person.username !== foundUser.username
);
const currentUser = { ...foundUser, refreshToken };
usersDB.setUsers([...otherUsers, currentUser]);
await fsPromises.writeFile(
path.join(__dirname, "..", "model", "users.json"),
JSON.stringify(usersDB.users)
);
res.cookie("jwt", refreshToken, {
httpOnly: true,
ameSite: "None",
secure: true,
maxAge: 24 * 60 * 60 * 1000,
});
res.json({ accessToken });
} else {
res.sendStatus(401);
}
};
module.exports = { handleLogin };
sever.js:
const express = require("express");
const app = express();
const path = require("path");
const cors = require("cors");
const corsOptions = require("./config/corsOptions");
const { logger } = require("./middleware/logEvents");
const errorHandler = require("./middleware/errorHandler");
const cookieParser = require("cookie-parser");
const verifyJWT = require("./middleware/verifyJWT");
const PORT = process.env.PORT || 3500;
app.use(logger);
app.use(cors(corsOptions));
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "./public")));
// routes
app.use("/", require("./routes/root"));
app.use("/register", require("./routes/register"));
app.use("/auth", require("./routes/auth"));
app.use("/refresh", require("./routes/refresh"));
app.use(verifyJWT);
app.use("/employees", require("./routes/api/employees"));
app.all("/*", (req, res) => {
res.status(404);
if (req.accepts("html")) {
res.sendFile(path.join(__dirname, "views", "404.html"));
} else if (req.accepts("json")) {
res.json({ error: "404 Not Found" });
} else {
res.type("txt").send("404 not found");
}
});
app.use(errorHandler);
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
verifyJWT middleware:
const jwt = require("jsonwebtoken");
require("dotenv").config();
const verifyJWT = (req, res, next) => {
const authHeader = req.headers["authorization"];
if (!authHeader) return res.sendStatus(401);
console.log(authHeader);
// bearer token, hence bearer space 1, 1 is the token
const token = authHeader.split("")[1];
// decoded info from the jwt
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, decoded) => {
// 403 is forbidden
if (err) return res.sendStatus(403);
req.user = decoded.username;
next();
});
};
module.exports = verifyJWT;
so if i for example
http://localhost:3500/auth (post) and login with a user and pwd, my res does log an access token, and if i try to use that inside
http://localhost:3500/employees (get) i get forbidden. not sure what i am missing here
i tried console.logging to see if i had foundUser, which i did, so not sure why i cant get into this route
You are splitting by empty string, it would divide every character, try spliting by space:
const token = authHeader.split(" ")[1];
I'm trying to make and API call to a QuickBooks endpoint from my express app, but I keep getting the following error:
"Missing required parameter: access_token"
I checked the Intuit documentation and it said the following:
Access tokens let your app connect and send requests to our APIs.
Pass the access_token value in the authorization header of every API request your app makes. The value should always be: Authorization: Bearer {AccessToken}
So what I've done is tried to extract the access token, then pass it into my API call:
app.get("/callback", function (req, res) {
console.log('it got here')
oauthClient
.createToken(req.url)
.then(function (authResponse) {
oauth2_token_json = JSON.stringify(authResponse.getJson(), null, 2);
const newToken = JSON.parse(oauth2_token_json)
access_token = newToken.access_token **TOKEN EXTRACTED HERE**
})
.catch(function (e) {
console.error(e);
});
res.redirect("http://localhost:3000/companyInfo" );
});
app.get("/getCompanyInfo", (req, res) => {
let companyID = oauthClient.getToken().realmId;
let url = oauthClient.environment == "sandbox" ? OAuthClient.environment.sandbox : OAuthClient.environment.production;
oauthClient
.makeApiCall({
url: url + "v3/company/" + companyID + "/companyinfo/" + companyID,
headers:{
'Authorization':{
'Bearer': access_token //**PASSED INTO API CALL HERE**
}
}
})
.then(function (authResponse) {
console.log(
"The response for API call is :" + JSON.stringify(authResponse)
);
res.send(JSON.parse(authResponse.text()));
})
.catch(function (e) {
console.error(e);
});
});
..however it's still not working. This part of OAuth authorization is still confusing me - any suggestions on how to make this work?
Here's the whole express part of the app:
const express = require("express");
const OAuthClient = require("intuit-oauth");
const bodyParser = require("body-parser");
const axios = require('axios')
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const port = process.env.PORT || 3001;
let urlencodedParser = bodyParser.urlencoded({ extended: true });
let oauth2_token_json = null;
let access_token = null;
let oauthClient = null;
app.get("/authUri", urlencodedParser, (req, res) => {
oauthClient = new OAuthClient({
clientId: "******",
clientSecret: "******",
environment: "sandbox",
redirectUri: "http://localhost:3001/callback",
});
let authUri = oauthClient.authorizeUri({
scope: [OAuthClient.scopes.Accounting],
state: "testState",
});
res.send(authUri);
});
app.get("/callback", function (req, res) {
console.log('it got here')
oauthClient
.createToken(req.url)
.then(function (authResponse) {
oauth2_token_json = JSON.stringify(authResponse.getJson(), null, 2);
const newToken = JSON.parse(oauth2_token_json)
access_token = newToken.access_token
})
.catch(function (e) {
console.error(e);
});
res.redirect("http://localhost:3000/companyInfo" );
});
app.get("/getCompanyInfo", (req, res) => {
let companyID = oauthClient.getToken().realmId;
let url = oauthClient.environment == "sandbox" ? OAuthClient.environment.sandbox : OAuthClient.environment.production;
oauthClient
.makeApiCall({
url: url + "v3/company/" + companyID + "/companyinfo/" + companyID,
headers:{
'Authorization':{
'Bearer': access_token
}
}
})
.then(function (authResponse) {
console.log(
"The response for API call is :" + JSON.stringify(authResponse)
);
res.send(JSON.parse(authResponse.text()));
})
.catch(function (e) {
console.error(e);
});
});
app.get("/", (req, res) => {
res.send("hello from server");
});
app.listen(port, () => {
console.log(`Server is listening on port: ${port}`);
});
I'm developing a register/login website which includes all features to make it work in an efficient and secure way using reactJS, NodeJS and Mysql.
Everything was working fine until I used express-session. In fact, when a user logs in, he will be redirected to a home page (obviously a session will be created) but when the user refreshes the page, It is expected to stay on the home page but the behavior I got is losing the session, thus being redirected to login page.
I looked for a fix and I already tried enabling credentials with Axios in the frontEnd and Cors in the backEnd but the problem is persisting.
This is my code:
server.js
const express = require('express');
const app = express();
const mysql = require('mysql2');
const cors = require('cors');
const validator = require('validator');
const {body, validationResult} = require('express-validator');
const session = require('express-session');
const cookieParser = require('cookie-parser');
app.use(express.json());
app.use(cors({
origin: ['http://localhost:3000'],
methods: ['GET', 'POST'],
credentials: true,
}
));
app.use(express.urlencoded({extended: true}));
app.use(cookieParser());
app.use(session({
name: 'session',
secret: 'crud',
resave: false,
saveUninitialized: false,
cookie: {
expires: 60 * 30,
sameSite: 'strict',
}
}
app.post('/login', (req, res) => {
const mail = validator.escape(req.body.mail);
const pass = validator.escape(req.body.pass);
const sqlSelect = 'SELECT * FROM login WHERE mail = ? AND pass = ?';
db.query(sqlSelect, [mail, pass], (err, result) => {
if (err) {
console.log(err);
}
if (result.length > 0) {
req.session.user = result;
req.session.loggedIn = true;
console.log(req.session.user);
res.send({message: 'success', session: req.session});
}
else {
res.send({message: 'Wrong combination Email/Password !'});
}
})
});
app.get('/login', (req, res) => {
console.log(req.session.user);
if (req.session.user){
res.send({
session: req.session,
message: 'logged'
});
}
else {
res.send({
message: 'Not logged'
});
}
});
app.js (login page)
Axios.defaults.withCredentials = true;
const onSubmit = () => {
Axios.post('http://localhost:9001/login', {
mail,
pass,
}).then((response) => {
console.log(response.data.message);
if (response.data.message === 'success') {
history.push('/home');
}
else {
setMessage(response.data.message);
}
});
};
home.js
export default function Home() {
const [user, setUser] = useState('');
const history = useHistory();
useEffect(() => {
Axios.get('http://localhost:9001/login', {withCredentials: true}).then((response) => {
console.log(response.data.message);
if (response.data.message === 'logged'){
setUser(response.data.session.user[0].mail);
}
else {
history.push('/');
}
})
//eslint-disable-next-line
}, []);
return (
<div>
<p>{user}</p>
</div>
)
}
I hope someone is able to suggest some fix to this. I know I can use localStorage but I want to use the session instead.
I am using an Express App for the backend and VueJs with Nuxt (Server Side Rendering). My problem is that the cookies are not getting saved when the session is getting refreshed.
Server:
const express = require('express')
const cookieParser = require('cookie-parser')
const { loadNuxt } = require('nuxt')
const app = express()
app.use(cookieParser())
// Middleware
app.use(async (req, res, next) => {
// ...
if (sessionExpired && refreshTokenIsValid) {
// Generate new session
// ...
res.cookie('sessionToken', token, { maxAge: 86400000, path: '/' })
res.cookie('sessionId', id, { maxAge: 86400000, path: '/' })
res.cookie('refreshToken', refreshToken, { maxAge: 86400000, path: '/' })
return next()
}
})
...
Login route
router.get('/login', async (req, res, next) => {
// ...
res.cookie('sessionToken', token, { maxAge: 86400000, path: '/' })
res.cookie('sessionId', id, { maxAge: 86400000, path: '/' })
res.cookie('refreshToken', refreshToken, { maxAge: 86400000, path: '/' })
res.status(200).redirect('/')
})
Client:
async asyncData({ $axios }) {
const data = await $axios.get('/something')
},
methods: {
async someMethod() {
let data = await this.$axios.$get('/something')
}
}
The cookies are not getting saved when sending a request from asyncData().
I solved the problem by using an axios helper.
Solution source: proxy cookies
// plugins/ssr-cookie-proxy.js
import { parse as parseCookie } from 'cookie';
function parseSetCookies(cookies) {
return cookies
.map(cookie => cookie.split(';')[0])
.reduce((obj, cookie) => ({
...obj,
...parseCookie(cookie),
}), {});
}
function serializeCookies(cookies) {
return Object
.entries(cookies)
.map(([name, value]) => `${name}=${encodeURIComponent(value)}`)
.join('; ');
}
function mergeSetCookies(oldCookies, newCookies) {
const cookies = new Map();
function add(setCookie) {
const cookie = setCookie.split(';')[0];
const name = Object.keys(parseCookie(cookie))[0];
cookies.set(name, cookie);
}
oldCookies.forEach(add);
newCookies.forEach(add);
return [...cookies.values()];
}
export default function ({ $axios, res }) {
$axios.onResponse((response) => {
const setCookies = response.headers['set-cookie'];
if (setCookies) {
// Combine the cookies set on axios with the new cookies and serialize them
const cookie = serializeCookies({
...parseCookie($axios.defaults.headers.common.cookie),
...parseSetCookies(setCookies),
});
$axios.defaults.headers.common.cookie = cookie; // eslint-disable-line no-param-reassign
// If the res already has a Set-Cookie header it should be merged
if (res.getHeader('Set-Cookie')) {
const newCookies = mergeSetCookies(
res.getHeader('Set-Cookie'),
setCookies,
);
res.setHeader('Set-Cookie', newCookies);
} else {
res.setHeader('Set-Cookie', setCookies);
}
}
});
}
I am using JWT to generate a token for access control. I can hit /api/auth/login and get back the token, however, when attempting to hit /api/protected with a GET request, I get 401 Unauthorized.
I've looked through SO and haven't found anything specific although it seems like a routine issue, maybe. I have tried moving the route around in the server.js file to see if that is the issue . I have removed the preceeding slash from the route (from /api/protected to api/protected) and using the latter I get back a bunch of html due to, I think, the app.use(express.static....
I am using Postman to test it but i'm not sure what I'm missing here. I have also made sure to set the authorization to Bearer Token in Postman.
'use strict';
const { Strategy: LocalStrategy } = require('passport-local');
// Assigns the Strategy export to the name JwtStrategy using object destructuring
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const { User } = require('../users/models');
const { JWT_SECRET } = require('../config');
const localStrategy = new LocalStrategy((username, password, callback) => {
let user;
User.findOne({ username })
.then(_user => {
user = _user;
if (!user) {
// Return a rejected promise so we break out of the chain of .thens.
// Any errors like this will be handled in the catch block.
return Promise.reject({
reason: 'LoginError',
message: 'Incorrect username or password'
});
}
return user.validatePassword(password);
})
.then(isValid => {
if (!isValid) {
return Promise.reject({
reason: 'LoginError',
message: 'Incorrect username or password'
});
}
return callback(null, user);
})
.catch(err => {
if (err.reason === 'LoginError') {
return callback(null, false, err);
}
return callback(err, false);
});
});
const jwtStrategy = new JwtStrategy(
{
secretOrKey: JWT_SECRET,
// Look for the JWT as a Bearer auth header
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('Bearer'),
// Only allow HS256 tokens - the same as the ones we issue
algorithms: ['HS256']
},
(payload, done) => {
done(null, payload.user);
}
);
module.exports = { localStrategy, jwtStrategy };
'use strict';
//How does order of code affect how it works?
// YES
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const morgan = require('morgan');
const passport = require('passport');
const path = require('path');
const { router: usersRouter } = require('./users');
const { router: authRouter, localStrategy, jwtStrategy } = require('./auth');
mongoose.Promise = global.Promise;
// Is this needed if dotenv is in this file also?
const { PORT, DATABASE_URL } = require('./config');
const app = express();
// Logging
app.use(morgan("common"));
// const logRequest = (req, res, next) => {
// const now = new Date();
// console.log(
// `local log - ${now.toLocaleDateString()} ${now.toLocaleTimeString()} ${req.method} ${req.url}`
// );
// next();
// }
app.use(function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE');
if (req.method === 'OPTIONS') {
return res.send(204);
}
next();
});
passport.use(localStrategy);
passport.use(jwtStrategy);
//app.use(logRequest);
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use('/api/users/', usersRouter);
app.use('/api/auth/', authRouter);
app.use("/api/items", require('./routes/api/items'));
// protected route that needs a valid JWT for access
const jwtAuth = passport.authenticate("jwt", { session: false });
// route to handle static content ie.e *.jpg
app.use(express.static(path.join(__dirname, "client", "build")));
app.get('/api/protected', jwtAuth, (req, res) => {
return res.json({
data: 'Hello World'
});
});
// have react client handle all additional routes
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "client", "build", "index.html"));
});
let server;
function runServer(DATABASE_URL, port = PORT) {
return new Promise((resolve, reject) => {
// How is DATABASE_URL used? What is the value? Is it referencing
// DATABASE_URL?
mongoose.connect(DATABASE_URL, { useNewUrlParser: true, useFindAndModify: false }, (err) => {
console.log("Success");
if (err) {
return reject(err);
}
server = app.listen(port, () => {
console.log(`Your app is listening on port ${PORT}`);
resolve();
})
.on('error', (err) => {
mongoose.disconnect();
reject(err);
});
});
});
}
function closeServer() {
return mongoose.disconnect()
.then(() => new Promise((resolve, reject) => {
console.log("Closing server");
server.close((err) => {
if (err) {
return reject(err);
}
resolve();
});
}));
}
if (require.main === module) {
runServer(DATABASE_URL)
.catch(err => console.error(err));
}
module.exports = { app, runServer, closeServer };
enter code hereI am expecting to get back a string that says "Hello World" just to make sure i'm hitting the endpoint correctly. Instead I get the 401 error, GET /api/protected HTTP/1.1" 401enter code here