Persist session id in passport-saml login login callback

347 Views Asked by At

I'm using passport-saml and express-session. I login with my original session id but when the idp response reach the login callback handler, I have another sessionId. Also, since my browser has the session cookie with the original session id, it cannot use the new session id in the login callback, so I cannot authenticate.

interface SamlProvider {
  name: string;
  config: SamlConfig;
}

const providers: SamlProvider[] = [
  {
    name: process.env.SAML_ENTITY_ID_1!,
    config: {
      path: "/login/callback",
      entryPoint: process.env.SAML_SSO_ENDPOINT_1,
      issuer: process.env.SAML_ENTITY_ID_1,
      cert: process.env.SAML_CERT_1!,
      ...(process.env.NODE_ENV === "production" && { protocol: "https" }),
      disableRequestedAuthnContext: true,
    },
  },
  {
    name: process.env.SAML_ENTITY_ID_2!,
    config: {
      path: "/login/callback",
      entryPoint: process.env.SAML_SSO_ENDPOINT_2,
      issuer: process.env.SAML_ENTITY_ID_2,
      cert: process.env.SAML_CERT_2!,
      ...(process.env.NODE_ENV === "production" && { protocol: "https" }),
      disableRequestedAuthnContext: true,
    },
  },
];

export const samlStrategy = (sessionStore: session.Store) =>
  new MultiSamlStrategy(
    {
      passReqToCallback: true, // makes req available in callback
      getSamlOptions: function (request, done) {
        // Find the provider
        const relayState = request.query.RelayState || request.body.RelayState;
        const provider = providers.find((p) => p.name === relayState);
        if (!provider) {
          return done(Error("saml identity provider not found"));
        }
        return done(null, provider.config);
      },
    },
    async function (
      req: Request,
      profile: Profile | null | undefined,
      done: VerifiedCallback
    ) {
      if (profile && profile.nameID) {
        const { nameID, nameIDFormat } = profile;
        const email = profile[
          "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
        ] as string;
        const firstName = profile[
          "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
        ] as string;
        const lastName = profile[
          "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
        ] as string;

        // Check if user is in risk database
        const user = await myUserService.getByEmail(email);
        if (!user) return done(new UserNotFoundError());

        // If user has existing session, delete that existing session
        sessionStore.all!((err: any, obj: any) => {
          const sessions = obj as Array<{
            sid: string;
            passport?: { user?: { email?: string } };
          }>;
          const existingSess = sessions.find(
            (sess) =>
              sess.passport &&
              sess.passport.user &&
              sess.passport.user.email &&
              sess.passport.user.email === email
          );
          if (existingSess && existingSess.sid) {
            sessionStore.destroy(existingSess.sid, (err: any) => {
              console.error(err);
              return done(Error("failed to delete existing user session"));
            });
          }
        });

        return done(null, { nameID, nameIDFormat, email, firstName, lastName });
      }
      return done(Error("invalid saml response"));
    }
  );

Here's my login and login callback

app.post("/login/callback", async function (req, res, next) {
  passport.authenticate("saml", (err: any, user: ISessionUser) => {
    if (err) {
      // TODO: Handle specific errors
      logger.info({ label: "SAML Authenticate Error:", error: err });
      return next(err);
    } else {
      req.logIn(user, (err) => {
        if (err) {
          logger.info({ label: "Login Error:", data: err });
          return next(err);
        }
        res.redirect("/");
      });
    }
  })(req, res, next);
});

app.get(
  "/auth/saml/login",
  passport.authenticate("saml", { failureRedirect: "/", failureFlash: true }),
  function (req, res) {
    res.redirect("/");
  }
);
1

There are 1 best solutions below

0
Peter Talbot On

I experienced a similar issue using Microsoft 365 for authentication. The answer was to pass a randomly-generated nonce to the authentication request - this gets passed back to your app in the callback request. With SAML I think it depends on the provider whether they support such a flow, but it is good practice. You can also use a cookie to maintain state in your app, instead of, or additional to, the session id.