SSO with Firebase


(Adam Beers) #1

Is it possible to use Firebase as the SSO for Discourse? I’ve tried several times to get it working, but to no avail.

Has anyone been successful in using Firebase with Discourse?


(Vinoth Kannan) #2

Can you share your code?


(Adam Beers) #3

There’s no real code yet. I configured Discourse with the sso secret and url, but it didn’t work. I don’t really know what the url should be. Thanks the issue.


(Saurek) #4

Also curious about how to do this.


#5

Bump. Is this possible?


(Gaurav Negi) #6

Did anyone figure out, how to use authentication from firebase for discourse?


(Adam Beers) #7

Nobody found anyway to do it that I know of. I stopped looking when I didn’t get any real help. But I did figure out an alternative. All authentication is done through Firebase. If a user forgets a password or wants to change it, it is handled by Firebase. Then, if a user is able to authenticate through Firebase, I created an account in Discourse with the same user name, but with a global, secure password. Worked for my solution. You can check out the app I made for a client on Android. Search for WWJDNow in the Android Play Store. I haven’t had to do anything similar for another client yet, so I didn’t look into if further and don’t have any other example. Hope that helps.


(Support) #8

I wanted to bump this because I’m running into the issue as well. Have there been any updates or has anyone figured it out? Otherwise I’m unsure how to pass a users Firebase info to the server for SSO.


(Support) #9

As a follow up to myself and pinging @adam_beers and @vinothkannans, I’ve been doing a lot of research today and it seems like it may be possible using Firebase’s new ability to create and then check session cookies.

I think you would be able to store a user’s session login as a cookie when they initially login (and redirect to your main login page when they don’t have a session), and then verify that cookie inside your SSO handling if it’s passes from the request. If the cookie is verified, you could then extract the DecodedIdToken and properly send back a response for logging in.

Any thoughts on this method?


(Support) #10

I got it working!

Hey everyone! Extremely happy to report I’ve gotten Firebase working as an SSO option for Discourse. It was as I had suggested in my last post that the key to making this work was to leverage the new ability for the Firebase SDK to set cookies on the browser after a login session.

On client-side you’ll need to do something like this:

auth.setPersistence(firebase.auth.Auth.Persistence.SESSION);

Which first sets the persistence state of your Firebase session.

Then you’ll need to tie into your users login flow with Firebase’s token logic. The implementation on our site is like this:

auth.signInWithEmailAndPassword(email, password).then(function(credential) {
    credential.user.getIdToken(true).then(function(token) {
    var FD = new FormData();
    FD.append("idToken", token);
    $.ajax({
        url: 'ENDPOINTURL',
        xhrFields: { withCredentials: true },
        data: FD,
        cache: false,
        contentType: false,
        processData: false,
        type: 'POST'
    })
    .done(function(data) {
         // redirect to wherever you want a logged in user to go to
          location.href = "/account";
    })
    .fail(function(xhr) {
         log('error', xhr);
    });
});

This hits an endpoint on our (node/express) server, which looks like this:

app.post('ENDPOINTURL',formidable(),function(req,res) {
  // Get the ID token passed
  var idToken = req.fields.idToken;
  // 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.
  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 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, domain: 'depthkit.tv', httpOnly: false, secure: true /** false to test in localhost */};
        res.cookie('session', sessionCookie, options);
        res.end(JSON.stringify({status: 'success'}));
  })
  .catch(function(error) {
        res.status(401).send('UNAUTHORIZED REQUEST!');
  });
});

Note that this is mostly a reference implementation from Google’s sample here. Also, auth is our firebase admin account Auth object.

This sets the session cookie in the client browser. It’s worth noting here as well that we are using subdomains, so the intended and implemented ability is that you can login/register at depthkit.tv and then login to our forums at a subdomain using SSO.

Lastly, Discourse has to ping our server for SSO auth (triggered on clicking Login), which looks like this.

app.get('SSOENDPOINT', cookieParser(), function(req,res) {
    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.
    auth.verifySessionCookie(sessionCookie, true /** checkRevoked */)
        .then((decodedClaims) => {
        //once we are here the user cookie is known to be valid and we can extract the uid
        var uid = decodedClaims.uid;
        var payload = req.query.sso; // fetch from incoming request
        var sig = req.query.sig; // fetch from incoming request
        if(sso.validate(payload, sig)) 
        {
            //valid sso, make sure the user is valid
            auth.getUser(uid)
            .then(function(userRecord) {
                // Successfully fetched user data
                var nonce = sso.getNonce(payload);
                var userparams = {
                // Required, will throw exception otherwise
                "nonce": nonce,
                "external_id": uid,
                "email": userRecord.email,
                // Optional - could pull these from DB values
                "username": userRecord.displayName,
                "name": userRecord.displayName
                };
                var q = sso.buildLoginString(userparams);
                console.log("user authed and signed in through SSO, redirecting");
                res.redirect('http://FORUMURL/session/sso_login?' + q);
            })
            .catch(function(error) {
                console.log("Error fetching user data:", error);
                console.log("redirecting to login because of invalid user data");
                res.redirect('https://www.depthkit.tv/login');
            });
        }
        else
        {
            console.log("unable to validate discourse payload, redirecting to login");
            res.redirect('https://www.depthkit.tv/login');
        }
        }).catch(error => {
        // Session cookie is unavailable or invalid. Force user to login.
        console.log("invalied session cookie, redirecting to login");
        res.redirect('https://www.depthkit.tv/login');
    });
});

This is also largely pulled from Firebase’s sample implementation as well as the reference code for the discourse-sso node (sso in the code) package (thank you to the creator!!).

Learned a lot about working with the web in general when implementing this, so hopefully other people can find it useful as well!


(Curtis Cooper) #11

Just wanted to thank you for sharing your code! Worked great!

Some related things in case anyone else walks down this road:

  • be sure to add your domain to Authorized domains in Firebase Console (Authentication -> Sign-in Method)

  • pay careful attention to this comment in the code above, during testing it might catch you off guard // In this case, we are enforcing that the user signed in in the last 5 minutes.

  • if you need to use CORS to access from a different domain you can use multiple middlewares by passing in an array: [cors(corsOptions),formidable()]

Thanks again for sharing!