如何在启动时将持久会话初始化为护照/会话文件存储?

How to initialize persisted sessions into passport / session-file-store at startup?

此快速应用程序使用护照通过 Azure 进行身份验证。当用户进行身份验证时,用户数据将保存到内存存储中,然后写入会话存储。

当 express 应用程序重新启动时,它会从会话存储中加载之前保存的用户数据。但是,passport 无法识别之前经过身份验证的用户,需要他们重新进行身份验证。这是一个问题,因为该应用程序仍在开发中,需要经常重新启动。

我希望应用程序在应用程序重新启动后识别之前经过身份验证的用户。


更新: 经过更多研究,我发现会话由 express-session 包管理。我已将 session-file-store 包添加到代码中并将其配置为保存会话。会话仍然没有持续。


此应用程序由此 ms graph tutorial 开始。它将用户数据序列化和反序列化到内存存储中,我添加了代码来保存数据。但是存储的用户数据和存储在浏览器中的会话密钥之间存在脱节。

应用程序通过使用 oid 作为键来存储用户,如下所示:

{
    "1aa31bba-xxxx-xxxx-xxxx-xxxx": {
        "profile": {
            "sub": "_v3k8L5PkIREMis1in9IF3SucjEQqxRxThsoK35rBZQ",
            "oid": "1aa31bba-xxxx-xxxx-xxxx-xxxx",
            ...
        }
    }
}

但是,浏览器有一个不是 oid 的会话 cookie:

"connect.sid":"s:cd957624-72ba-4552-893d-8fd59bbce31e.Gt87eHx0WjkKXOKeq6oDvsw35BZZ+6ZQ6p0c3dwjRTE"

cookie connect.sid 是浏览器中唯一存储的 cookie。每次用户重新验证时,cookie 的值都会更改。此 cookie 不会写入用户文件或会话文件。这是我不知道如何解决的断开连接。

启动时初始化 session-file-store 的正确方法是什么?

这是应用程序的护照代码。

var passport = require('passport');
var OIDCStrategy = require('passport-azure-ad').OIDCStrategy;
var FileStore = require('session-file-store')(session);  // added to test FileStore

require('dotenv').config();

var users = {};
var userStore = require('userStore');
users = userStore.load();

// Passport calls serializeUser and deserializeUser to manage users
passport.serializeUser(function (user, done) {
    // Use the OID property of the user as a key
    users[user.profile.oid] = user;
    userStore.save(users);
    done(null, user.profile.oid);
});

passport.deserializeUser(function (id, done) {
    done(null, users[id]);
});

const oauth2 = require('simple-oauth2').create({
    client: {
        id: process.env.OAUTH_APP_ID,
        secret: process.env.OAUTH_APP_PASSWORD
    },
    auth: {
        tokenHost: process.env.OAUTH_AUTHORITY,
        authorizePath: process.env.OAUTH_AUTHORIZE_ENDPOINT,
        tokenPath: process.env.OAUTH_TOKEN_ENDPOINT,
    }
});

async function signInComplete(iss, sub, profile, accessToken, refreshToken, params, done) {
    if (!profile.oid) {
        return done(new Error("No OID found in user profile."));
    }
    try {
        const user = await graph.getUserDetails(accessToken);
        if (user) {
            // Add properties to profile
            profile['email'] = user.mail ? user.mail : user.userPrincipalName;
        }
    } catch (err) {
        return done(err);
    }
    // Create a simple-oauth2 token from raw tokens
    let oauthToken = oauth2.accessToken.create(params);
    // Save the profile and tokens in user storage
    users[profile.oid] = { profile, oauthToken };
    return done(null, users[profile.oid]);
}

passport.use(new OIDCStrategy(
    {
        identityMetadata: `${process.env.OAUTH_AUTHORITY}${process.env.OAUTH_ID_METADATA}`,
        clientID: process.env.OAUTH_APP_ID,
        responseType: 'code id_token',
        responseMode: 'form_post',
        redirectUrl: process.env.OAUTH_REDIRECT_URI,
        allowHttpForRedirectUrl: true,
        clientSecret: process.env.OAUTH_APP_PASSWORD,
        validateIssuer: false,
        passReqToCallback: false,
        scope: process.env.OAUTH_SCOPES.split(' ')
    },
    signInComplete
));

app.use(session({
    secret: process.env.SESSION_SECRET_KEY,
    store: new FileStore({}), // added to test FileStore
    genid: uuid,
    resave: true, // false,  // changed to test FileStore
    saveUninitialized: true, // false,  // changed to test FileStore
    unset: 'destroy'
}));

app.use(passport.initialize());
app.use(passport.session());

下面的更改应该会有所帮助,因为我不确定“sessionStore”,我做到了 in-memory,一旦您重新启动应用程序,用户列表将被删除并再次注册。

var passport = require('passport');
var OIDCStrategy = require('passport-azure-ad').OIDCStrategy;
require('dotenv').config();

// Ideally this needs to be replaced with DB
var users = [];

// This is not required if DB is present
let getUser = (id) => {
    for (let index = 0; index < users.length; index++) {
        if (users[index].id === id) {
            return users[index];
            break;
        }
    }
};

// This is not required if DB is present
let checkIfUserExists = (id) => {
    for (let index = 0; index < users.length; index++) {
        if (users[index].id === id) {
            return true;
            break;
        }
    }
}

passport.serializeUser(function(user, done){
    done(null, user.id);
});

passport.deserializeUser(function(id, done){
    // This is not required if DB is present, just get the user from DB
    let user = getUser(id);

    done(null, user);
});

const oauth2 = require('simple-oauth2').create({
    client: {
        id: process.env.OAUTH_APP_ID,
        secret: process.env.OAUTH_APP_PASSWORD
    },
    auth: {
        tokenHost: process.env.OAUTH_AUTHORITY,
        authorizePath: process.env.OAUTH_AUTHORIZE_ENDPOINT,
        tokenPath: process.env.OAUTH_TOKEN_ENDPOINT,
    }
});

function signInComplete(iss, sub, profile, accessToken, refreshToken, params, done) {
    // Encapsulating the logic in nexttick function helps manage event loop
    process.nextTick(async () => {
        if (profile.oid) {
            if (checkIfUserExists(profile.oid)) {
                // getUser and return user, if DB this part will change
                let user = getUser(profile.oid);
                let userDetails = {};
                try {
                    userDetails = await graph.getUserDetails(accessToken);
                } catch (error) {
                    return done(error);
                }
                user.userDetails = userDetails;
                user.oauthToken = oauth2.accessToken.create(params);
                return done(null, user);
            } else {
                // Create a new user and return user, will change when db comes to place
                let userDetails = {};
                try {
                    userDetails = await graph.getUserDetails(accessToken);
                } catch (error) {
                    return done(error);
                }
                // User structure have retained my best based on the current code
                let newUser = {
                    id: profile.oid,
                    userDetails: userDetails, 
                    profile: profile,
                    oauthToken: oauth2.accessToken.create(params)
                };
                users.push(newUser);
                return done(null, newUser)
            }
        } else {
            return done(new Error("No OID found in user profile."));    
        }
    });
}

passport.use(new OIDCStrategy(
    {
        identityMetadata: `${process.env.OAUTH_AUTHORITY}${process.env.OAUTH_ID_METADATA}`,
        clientID: process.env.OAUTH_APP_ID,
        responseType: 'code id_token',
        responseMode: 'form_post',
        redirectUrl: process.env.OAUTH_REDIRECT_URI,
        allowHttpForRedirectUrl: true,
        clientSecret: process.env.OAUTH_APP_PASSWORD,
        validateIssuer: false,
        passReqToCallback: false,
        scope: process.env.OAUTH_SCOPES.split(' ')
    },
    signInComplete
));

// See the passport documentation for this part
app.use(session({
    secret: process.env.SESSION_SECRET_KEY,
    genid: uuid,
    resave: false, // Change to true once you have a store
    saveUninitialized: false // Change to true once you have a store
}));

app.use(passport.initialize());
app.use(passport.session());

供参考passportjs,

根本原因原来是user.oauthtoken对象是classAuthToken的一个实例。当用户序列化存储时,json只记录对象属性(因为json)。因此,当用户在应用程序重新启动后 de-serialized 时,user.oauthtoken 只是一个 Object 而不是 AuthToken.

解决方法很简单。我从这里更改了 de-serialize 函数:

passport.deserializeUser(function (id, done) {
    done(null, users[id]);
});

对此:

passport.deserializeUser(function (id, done) {
    let user = users[id];
    if( user.oauthToken.constructor.name !== "AccessToken" ) {
        user.oauthToken = oauth2.accessToken.create(user.oauthToken.token);
    }
    done(null, user);
});

通过这个简单的更改,应用程序的其余部分能够调用访问令牌的方法并且一切正常。