7

I am following this documentation: Manage Session Cookies

My app.js looks like this basically sign in the user on the client side on a button click.

(function() {
// Initialize Firebase
var config = {
  //...
};

firebase.initializeApp(config);

// no local persistence because of the httpOnly flag
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);

const emailField = document.getElementById("email");
const passwordField = document.getElementById("password");
const loginButton = document.getElementById("loginButton");

loginButton.addEventListener("click", e => {
    const email = emailField.value;
    const password = passwordField.value;

    const signInPromise = firebase.auth().signInWithEmailAndPassword(email, password);
    signInPromise.catch(e => {
        console.log("Login Error: " + e.message);
    })
    return signInPromise.then(() => {
        console.log("Signed in + " + firebase.auth().currentUser.uid);
        return firebase.auth().currentUser.getIdToken().then(idToken => {
            // Session login endpoint is queried and the session cookie is set.
            // CSRF protection should be taken into account.
            // ...
            // const csrfToken = getCookie('csrfToken')
            console.log("User ID Token: " + idToken);
            return sendToken(idToken);
            //return postIdTokenToSessionLogin('/sessionLogin', idToken, csrfToken);
        });
    })
});

firebase.auth().onAuthStateChanged(user => {
    if (user) {
        document.getElementById('loginSuccess').innerHTML = `Signed in as ${user.uid}`;
        document.getElementById('loginError').innerHTML = "";
    } else {
        document.getElementById('loginSuccess').innerHTML = "";
        document.getElementById('loginError').innerHTML = `Not signed in`;
    }
}); 
})();

The sendToken function looks like this:

function sendToken(idToken) {
   console.log("Posting " + idToken);
   var xhr = new XMLHttpRequest();
   var params = `token=${idToken}`;
   xhr.open('POST', "/admin/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);
   });
}

On the server side I use an express app with hosting and firebase functions The /admin/login post looks like this:

adminApp.post("/login", (request, response) => {
   console.log("Got login post request");
   if (request.body.token) {
      const idToken = request.body.token.toString();
      console.log("idToken = " + idToken);
      // Set session expiration to 5 days.
      const expiresIn = 60 * 60 * 24 * 5 * 1000;
      return adminFirebase.auth().createSessionCookie(idToken, {expiresIn}).then((sessionCookie) => {
        const options = {maxAge: expiresIn, httpOnly: true, secure: true};
        response.cookie('session', sessionCookie, options);
        response.end(JSON.stringify({status: 'success'}));
    }, error => {
        response.status(401).send('UNAUTHORIZED REQUEST!');
    });
   }
   return response.status(400).send("MISSING TOKEN");
});

So after posting sendToken I should have a cookie called 'session', which contains information. So now I wrote a little middleware to check that token:

const validateLogin = function (req, res, next) {
   const sessionCookie = req.cookies.session || '';
   console.log(JSON.stringify(req.headers));
   console.log("Verifying " + sessionCookie);
   return adminFirebase.auth().verifySessionCookie(sessionCookie, true).then((decodedClaims) => {
     console.log("decoded claims: " + decodedClaims);
     next();
   }).catch(error => {
      res.redirect('/admin/login');
   });
};

Last but not least, I have a admin/secret get that is using this middleware:

adminApp.get("/secret/", validateLogin, (request, response) => {
   return response.send("This is secret!");
});

I, however, am constantly sent back to the login page. What am I missing for cookies to work?

I found out Firebase Hosting according to this only allows one cookie (otherwise they are stripped). This cookies is __session, however setting this cookies also does not seem to work for me...

I was able to set the __session cookie at the client side:

document.cookie = "__session=TOKEN"

and then verify the token on the server side, however the cookie only works for the local / path and not /a/b

Janosch Hübner
  • 1,584
  • 25
  • 44

2 Answers2

6

If somebody else is getting to this page (like I did an hour ago) here is the code in frontend that deals with this problem:

// Sign in with email and pass.
firebase.auth().signInWithEmailAndPassword(email, password)
    .then(user => {
    // Get the user's ID token and save it in the session cookie.
        return firebase.auth().currentUser.getIdToken(true).then(function (token) {
                // set the __session cookie
                document.cookie = '__session=' + token + ';max-age=3600';
                })
        })
        .catch(function (error) {//... code for error catching

I hope it helps.

4b0
  • 21,981
  • 30
  • 95
  • 142
SharpBCD
  • 547
  • 1
  • 7
  • 25
  • You may want to remove the last part. Your answer is fine, but please don't ask new questions with an answer. – Pika Supports Ukraine Jan 27 '19 at 02:32
  • I know what you mean but since this ask for "opinionated answer", I can't ask it separately. I thought replies as comments would be good enough. – SharpBCD Jan 27 '19 at 19:37
  • Users tend not to answer questions in comments. While this question might not be on-topic here on Stack Overflow, there's probably a site [here](https://stackexchange.com/sites#traffic) where it is on-topic and you can get an answer. – Pika Supports Ukraine Jan 27 '19 at 22:32
  • You are not setting the session cookie but instead using the idToken as a cookie. It is not optimal as IdTokens are short lived. Its better to make your code work. What is the error you catch when you redirect to login? – Jens Feb 01 '21 at 18:31
-2

@Janosch, This is how I set up my app. I recommend that you go through this GitHub Repo that I followed.

My client side is:

function signIn(){
var email = document.getElementById("username").value;
var password = document.getElementById("password").value;
// As httpOnly cookies are to be used, do not persist any state client side.
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
// When the user signs in with email and password.
firebase.auth().signInWithEmailAndPassword(email, password).then(user => {
  // Get the user's ID token as it is needed to exchange for a session cookie.
  return firebase.auth().currentUser.getIdToken().then(idToken => {
  // Session login endpoint is queried and the session cookie is set.
  // CSRF protection should be taken into account.
  // ...
  var csrfToken = getCookie('_csrf')
  return postIdTokenToSessionLogin('/sessionLogin', idToken, csrfToken);
  });
 }).then(() => {
 // A page redirect would suffice as the persistence is set to NONE.
 return firebase.auth().signOut();
 }).then(() => {
   window.location.assign('/profile');
 });
}


function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i <ca.length; i++) {
    var c = ca[i];
    while (c.charAt(0) == ' ') {
        c = c.substring(1);
    }
    if (c.indexOf(name) == 0) {
        return c.substring(name.length, c.length);
    }
}
return "";
}

var postIdTokenToSessionLogin = function(url, idToken, csrfToken) {
// POST to session login endpoint.
return $.ajax({
  type:'POST',
  url: url,
  dataType:"json",
  data: {idToken: idToken, csrfToken: csrfToken},
  contentType: 'application/x-www-form-urlencoded',
  xhrFields: {
    withCredentials: true
  },
  crossDomain: true
 });
};

Here is my server side code

app.post("/sessionLogin", (req, res) => {

// Get ID token and CSRF token.
var idToken = req.body.idToken.toString();
var csrfToken = req.body.csrfToken.toString();
// Guard against CSRF attacks.
if (!req.cookies || csrfToken !== req.cookies._csrf) {
 res.status(401).send('UNAUTHORIZED REQUEST!');
 return;
}
  // Set session expiration to 5 days.
var 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.
 // We could also choose to enforce that the ID token auth_time is recent.
 firebase.auth().verifyIdToken(idToken).then(function(decodedClaims) {
  // In this case, we are enforcing that the user signed in in the last 5 
 minutes.
  if (new Date().getTime() / 1000 - decodedClaims.auth_time < 5 * 60) {
   return firebase.auth().createSessionCookie(idToken, {expiresIn: 
 expiresIn});
 }
 throw new Error('UNAUTHORIZED REQUEST!');
 })
 .then(function(sessionCookie) {
 // Note httpOnly cookie will not be accessible from javascript.
 // secure flag should be set to true in production.
 var options = {maxAge: expiresIn, path: "/", httpOnly: false, secure: true 
  /** to test in localhost */};
 res.cookie('session', sessionCookie, options);
 res.end(JSON.stringify({status: 'success'}));
 })
 .catch(function(error) {
   res.status(401).send('UNAUTHORIZED REQUEST!');
 });
 });

app.get("/profile", (req, res) => {
  console.log('Cookies: ', req.cookies); //Empty object, 'Cookies: {}' 
  res.render("profile");

});

app.post("/profile", (req, res) => {
  res.send(req.body.name);
  console.log('Cookies: ', req.cookies); //Cookies object with csrf and 
   session token
});

This is working fine and I'm able to pass the cookies to the server with every POST request. An unauthenticated user cannot send POST requests.

Please Note: 1. In the POST request httpOnly: false **while development to check if the session is recorded at the client side. It should be true to hide from the client side. 2. This only works for the POST requests and not for the GET requests for some reason. I've raised this issue here (comments might be helpful). 3. I'm using csurf npm package to assign the CSRF cookie. Below is the code that assign a user csrf token in the cookie when they visit the app. Visit the link for more info on usage.

CSRF usage:

app.get("/", csrfProtection, (req, res) => {
  res.render("home");
});
  1. Last but not least. To identify users on a GET request, since I'm unable to receive Cookie Session Token, I'm planning on persisting the Auth State at the client side for that SESSION and use the user info to show them the user-specific information. Not sure if that's the best way to do it, but I will update this post as and when I implement it.

Let me know if you think of anything that might be better.

Himanshu Pal
  • 43
  • 1
  • 8
  • 2
    This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/low-quality-posts/21112184) – Kos Oct 12 '18 at 10:53
  • @Kos Thanks for correcting me. I'm new here and thought it was okay to add it in answers. Anyways, I was able to find the answer to this question and posted the same again. Please let me know what you think. – Himanshu Pal Oct 18 '18 at 16:16
  • I guess now you need feedback from OP, to see if it answers his question – Kos Oct 18 '18 at 18:06
  • @Kos, of course! I was just asking for your feedback on the way I’ve tried to include as much info as I could. Also, if there is anything I could change. Thanks again! – Himanshu Pal Oct 18 '18 at 19:38