SuperiorServers / nodebb-plugin-sso-steam-v2

GNU General Public License v2.0
3 stars 7 forks source link

Not working in v1.10.1 (csrf-invalid) #4

Open wadeira opened 6 years ago

wadeira commented 6 years ago

Upgraded from v1.5.3 and the plugin stopped working, whenever we try to login, the callback just says the session expired.

2018-07-06T12:30:18.250Z [11508] - error: /auth/steam/callback Error: [[error:csrf-invalid]] at router.get.req.session.registration (/opt/nodebb/src/routes/authentication.js:75:54) at Layer.handle [as handle_request] (/opt/nodebb/node_modules/express/lib/router/layer.js:95:5) at next (/opt/nodebb/node_modules/express/lib/router/route.js:137:13) at Route.dispatch (/opt/nodebb/node_modules/express/lib/router/route.js:112:3) at Layer.handle [as handle_request] (/opt/nodebb/node_modules/express/lib/router/layer.js:95:5) at /opt/nodebb/node_modules/express/lib/router/index.js:281:22 at Function.process_params (/opt/nodebb/node_modules/express/lib/router/index.js:335:12) at next (/opt/nodebb/node_modules/express/lib/router/index.js:275:10) at Function.handle (/opt/nodebb/node_modules/express/lib/router/index.js:174:3) at router (/opt/nodebb/node_modules/express/lib/router/index.js:47:12) at Layer.handle [as handle_request] (/opt/nodebb/node_modules/express/lib/router/layer.js:95:5) at trim_prefix (/opt/nodebb/node_modules/express/lib/router/index.js:317:13) at /opt/nodebb/node_modules/express/lib/router/index.js:284:7 at Function.process_params (/opt/nodebb/node_modules/express/lib/router/index.js:335:12) at next (/opt/nodebb/node_modules/express/lib/router/index.js:275:10) at /opt/nodebb/node_modules/express/lib/router/index.js:635:15

chelog commented 6 years ago

Same here, any fixes?

shadowndacorner commented 5 years ago

Grappled with this issue tonight and while I don't have a "correct" solution, I got it working. I say it's "not correct" because it requires modifying a file in nodebb which doesn't feel great to me. At some point I may try to work it into a pull request for both nodebb and this lib, but for now, here are the steps I took:

Disclaimer: This works by bypassing the csrf check. I am not super familiar with SSO implementation details, so while this does work, I don't know how much of a negative security impact it has. It could be none due to the nature of OIDC vs OAuth, or it could make this super insecure. Hopefully someone more knowledgeable than me will chime in.

1) Replace library.js with the following:

(function(module) {
    'use strict';

    var user = require.main.require('./src/user'),
        db = require.main.require('./src/database'),
        meta = require.main.require('./src/meta'),
        passport = require.main.require('passport'),
        passportSteam = require('passport-steam').Strategy,
        nconf = require.main.require('nconf'),
        utils = require('../../public/src/utils'),
        authenticationController = require.main.require('./src/controllers/authentication'),
        winston = require.main.require('winston'),
        async = require('async');

    var constants = Object.freeze({
        'name': 'Steam',
        'admin': {
            'route': '/plugins/sso-steam',
            'icon': 'fa-steam'
        }
    });

    var Steam = {};

    function profileurl(steamid) {
        return 'https://steamcommunity.com/profiles/' + steamid;
    }

    Steam.init = function(data, callback) {
        function render(req, res, next) {
            res.render('admin/plugins/sso-steam', {});
        }
        data.router.get('/admin/plugins/sso-steam', data.middleware.admin.buildHeader, render);
        data.router.get('/api/admin/plugins/sso-steam', render);
        callback();
    };

    Steam.linkAccount = function (uid, steamid) {
        user.setUserField(uid, 'steam-sso:steamid', steamid);
        user.setUserField(uid, 'steam-sso:profile', profileurl(steamid));

        db.setObjectField('steam-sso:uid-link', steamid, uid);
        db.setObjectField('steam-sso:steamid-link', uid, steamid);
    }

    Steam.getStrategy = function (strategies, callback) {
        meta.settings.get('sso-steam', function(err, settings) {
            if (!err && settings['key']) {
                passport.use(
                    new passportSteam(
                    {
                        returnURL: nconf.get('url') + '/auth/steam/callback',
                        realm: nconf.get('url'),
                        apiKey: settings['key'],
                        passReqToCallback: true
                    },

                    function (req, identifier, profile, done) {
                        if (req.hasOwnProperty('user') && req.user.hasOwnProperty('uid') && req.user.uid > 0) {
                            Steam.linkAccount(req.user.uid, profile.id);
                            return done(null, req.user);
                        }

                        Steam.login(profile.id, profile.displayName, profile._json.avatarfull, function(err, user) {
                            if (err) {
                                return done(new Error(err));
                            }

                            authenticationController.onSuccessfulLogin(req, user.uid);
                            done(null, user);
                        });
                    })
                );

                strategies.push({
                    name: 'steam',
                    url: '/auth/steam',
                    callbackURL: '/auth/steam/callback',
                    skipCSRFCheck: true,
                    icon: constants.admin.icon,
                    scope: 'user:username'
                });
            }

            callback(null, strategies);
        });
    };

    Steam.getAssociation = function (data, callback) {
        Steam.getSteamidByUid(data.uid, function (err, steamid) {
            if (err) {
                return callback(err, data);
            }

            if (steamid) {
                data.associations.push({
                    associated: true,
                    url: profileurl(steamid),
                    steamid: steamid,
                    name: constants.name,
                    icon: constants.admin.icon
                });
            } else {
                data.associations.push({
                    associated: false,
                    url: require.main.require('nconf').get('url') + '/auth/steam',
                    name: constants.name,
                    icon: constants.admin.icon
                });
            }

            callback(null, data);
        })
    };

    Steam.login = function(steamid, username, avatar, callback) {
        Steam.getUidBySteamid(steamid, function(err, uid) {
            if (err) {
                return callback(err);
            }

            if (uid !== null) { // Existing User
                callback(null, {
                    uid: uid
                });
            } else {// New User
                if (!utils.isUserNameValid(username)) {
                    return callback('Invalid username! Your username can only contain alphanumeric letters (a-z, numbers, spaces).');
                }

                user.create({username: username}, function(err, uid) {
                    if (err !== null) {
                        callback(err);
                    } else {
                        Steam.linkAccount(uid, steamid);

                        user.setUserField(uid, 'picture', avatar);

                        callback(null, {
                            uid: uid
                        });
                    }
                });
            }
        });
    };

    Steam.getUidBySteamid = function(steamid, callback) {
        db.getObjectField('steam-sso:uid-link', steamid, function(err, uid) {
            if (err !== null) {
                return callback(err);
            }
            callback(null, uid);
        });
    };

    Steam.getSteamidByUid = function(uid, callback) {
        db.getObjectField('steam-sso:steamid-link', uid, function(err, steamid) {
            if (err !== null) {
                return callback(err);
            }
            callback(null, steamid);
        });
    };

    Steam.deleteUserData = function (data, callback) {
        var uid = data.uid;
        Steam.getSteamidByUid(uid, function (err, steamid) {
            if (err !== null) {
                return callback(err);
            }

            user.auth.revokeAllSessions(uid, function() {
                db.deleteObjectField('steam-sso:uid-link', steamid);
                db.deleteObjectField('steam-sso:steamid-link', uid);

                callback(null, uid);
            });
        });
    }

    Steam.addMenuItem = function(custom_header, callback) {
        custom_header.authentication.push({
            'route': constants.admin.route,
            'icon': constants.admin.icon,
            'name': constants.name
        });

        callback(null, custom_header);
    };

    // Add some APIs for themes to use
    Steam.addPostUserData = function (data, callback) {
        Steam.getSteamidByUid(data.uid, function (err, steamid) {
            if ((err == null) && (steamid !== null)) {
                data['steam-sso:steamid'] = steamid;
                data['steam-sso:profile'] = profileurl(steamid);
            }
            callback(null, data);
        })
    }

    module.exports = Steam;
}(module));

There are three major differences here. Firstly, I replaced module.parent.require with require.main.require as the former is deprecated (at least, according to the logs). Then, I fixed the require paths to actually match up. Finally, I added skipCSRFCheck: true, to the passport strategy. This doesn't do anything by default, but when combined with the next steps, it actually gets it working.

2) Add var nconf = require('nconf'); to the beginning of nodebb/src/routes/authentication.js 3) In the async.waterfall block of authentication.js (around line 60), within the loginStrategies.forEach block (starting at line 66 in my version of nodebb), add the following:

                if (strategy.skipCSRFCheck)
                {
                    if (strategy.url) {
                        router.get(strategy.url, passport.authenticate(strategy.name, {
                            scope: strategy.scope,
                            prompt: strategy.prompt || undefined,
                        }));
                    }

                    router.get(strategy.callbackURL, passport.authenticate(strategy.name, {
                        successReturnToOrRedirect: nconf.get('relative_path') + (strategy.successUrl !== undefined ? strategy.successUrl : '/'),
                        failureRedirect: nconf.get('relative_path') + (strategy.failureUrl !== undefined ? strategy.failureUrl : '/login'),
                    }));
                }
                else
                {
                    // all of the code from the current version of nodebb w/ csrf checks
                }

This lets passport strategies use the logic from 1.5.4, when it used to work. And it maintains compatibility with the newer (more secure) auth.

If anyone can think of a better way of solving it, I'm absolutely open to it. But as far as I can tell, Steam's OIDC doesn't send the crsf token back, which makes it impossible to actually validate it. Hence this dirty hack.