NodeJS / ExpressJS / passport-saml Реализация ADFS SingleLogout - PullRequest
0 голосов
/ 05 апреля 2020

Я не знал, куда дальше go, поэтому я собираюсь опубликовать здесь свою проблему, так как я уже видел некоторые связанные проблемы по этому вопросу. К сожалению, предоставленные решения не сработали в моем случае, и я не знаю, что еще попробовать.

Итак, немного предыстории: у меня есть приложение NodeJS / ExpressJS / passport-saml, которое аутентифицируется в системе ADFS. Часть SSO в этом вопросе работает отлично, но я не могу заставить работать часть SLO.

Что происходит, когда я инициирую выход из системы, инициированный SP или IdP, он зависает на первом SP. Этот первый SP корректно выходит из системы, но затем он перенаправляется на страницу входа в систему первого SP и продолжает ждать ввода учетных данных, фактически останавливая цепочку перенаправления, которая должна произойти.

Что я Я уже много пробовал, включая использование привязок POST и HTTP-Redirect на моем сервере конечной точки SLFS ADFS / NodeJS, изменение маршрутов et c.

Текущая реализация выглядит следующим образом: SLO Конфигурация конечной точки (равная для каждого SP, затемненная часть содержит): endpoint

На сервере SP конфигурация passport-saml выглядит следующим образом:

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ IMPORTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//

// NodeJS native
const path = require('path');
const fs = require('fs');

// NodeJS packages
const SamlStrategy = require('passport-saml').Strategy;
const { Database } = require('../../Database');

// Custom imports
const { ApplicationConfiguration } = require('../../ApplicationConfiguration');

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ CONSTANTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//

let strategy = {};

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ INIT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//

/**
 * Initialise the passport saml strategy with the necessary configuration parameters.
 */
const initStrategy = () => {
  // Get additional required configuration
  const config = ApplicationConfiguration.getProperties([
    ['CGS_HOST'],
    ['AUTH_PORT'],
    ['SSO', 'host'],
    ['SSO', 'identifier'],
    ['SSO', 'cert'],
    ['SSO', 'algorithm'],
    ['HTTPS_CERT_PRIVATE_PATH'],
  ]);
  // Define the SAML strategy based on configuration
  strategy = new SamlStrategy(
    {
      // URL that should be configured inside the AD FS as return URL for authentication requests
      callbackUrl: `https://${<sp_host_name>}:${<sp_port_value>}/sso/callback`,
      // URL on which the AD FS should be reached
      entryPoint: <idp_host_name>,
      // Identifier for the CIR-COO application in the AD FS
      issuer: <sp_identifier_in_idp>,
      identifierFormat: null,
      // CIR-COO private certificate
      privateCert: fs.readFileSync(<sp_server_private_cert_path>, 'utf8'),
      // Identity Provider's public key
      cert: fs.readFileSync(<idp_server_public_cert_path>, 'utf8'),
      authnContext: ['urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'],
      // AD FS signature hash algorithm with which the response is encrypted
      signatureAlgorithm: <idp_signature_algorithm>,
      // Single Log Out URL AD FS
      logoutUrl: <idp_host_name>,
      // Single Log Out callback URL
      logoutCallbackUrl: `https://${<sp_host_name>}:${<sp_port_value>}/slo/callback`,
      // skew that is acceptable between client and server when checking validity timestamps
      acceptedClockSkewMs: -1,
    },
    async (profile, done) => {
      // Map ADFS groups to Group without ADFS\\ characters
      const roles = profile.Roles.map(role => role.replace('ADFS\\', ''));
      // Get id's from the roles
      const queryResult = await Database.executeQuery('auth-groups', 'select_group_ids_by_name', [roles]);
      // Map Query result to Array for example: [1,2]
      const groupIds = queryResult.map(group => group.id);
      done(null,
        {
          sessionIndex: profile.sessionIndex,
          nameID: profile.nameID,
          nameIDFormat: profile.nameIDFormat,
          id: profile.DistinguishedName,
          username: profile.DistinguishedName,
          displayName: profile.DisplayName,
          groups: profile.Roles,
          mail: profile.Emailaddress,
          groupIds,
        });
    },
  );
  // Return the passport strategy
  return strategy;
};


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PASSPORT CONFIG ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//

/**
 * Initialise the passport instance and add the saml passport strategy to it for authentication
 * @param {Object} passport - Passport object
 */
const initPassport = (passport) => {
  // (De)serialising
  passport.serializeUser((user, done) => {
    done(null, user);
  });
  passport.deserializeUser((user, done) => {
    done(null, user);
  });
  // Initialise the strategy
  const passportStrategy = initStrategy();
  // Addition strategy to passport
  passport.use('saml', passportStrategy);
};

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ HELPERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//

/**
 * Get the metadata from the Service Provider (this server).
 * @param {String} publicPath - Path to public certificate
 * @return {Promise<any>} - Metadata object for this application
 */
const getMetaData = publicPath => new Promise((resolve) => {
  const metaData = strategy.generateServiceProviderMetadata({}, fs.readFileSync(path.join(publicPath), 'utf8'));
  resolve(metaData);
});

/**
 * Construct a Single Logout Request and send it to the IdP.
 * @param {Object} req - Default request object
 * @param {Object} res - Default response object
 */
const logout = (req, res) => {
  // Construct SLO request for IdP
  strategy.logout(req, (err, url) => {
    req.logOut();
    // Redirect to SLO callback URL and send logout request.
    return res.redirect(url);
  });
};

const getStrategy = () => strategy;

module.exports = {
  initPassport,
  getStrategy,
  getMetaData,
  logout,
};

И соответствующие маршруты и функции следующие:

const logOutLocalSession = sid => new Promise(((resolve, reject) => {
    log.info(`Received request to destroy session with sid ${sid}.`);
    // Destroy local session
    store.destroy(sid, (err) => {
      if (err) {
        log.error(`Error occurred while logging out local session with SID ${sid}: ${err}`);
        reject('Onbekende fout opgetreden bij uitloggen lokaal.');
      }
      log.info(`Successfully logged out user locally with SID ${sid}.`);
      resolve();
    });
}));

const logOutAllSessions = async (req, res) => {
    // Extract username to get all sessions
    const { username } = req.session.passport.user;
    log.info(`Received request to log user ${username} out of all sessions.`);
    const sessionIdsRes = await Database.executeQuery('sessions', 'select_sids_by_user_id', [username]);
    // Loop over all sessions and destroy them
    const destroyPromises = [];
    sessionIdsRes.forEach((sessionIdRes) => {
      destroyPromises.push(logOutLocalSession(sessionIdRes.sid));
    });
    await Promise.all(destroyPromises);
    // Remove local session from request
    req.session = null;
    log.info(`User ${username} logged out successfully from all known sessions.`);
};

const logOutIdp = (req, res) => {
    const { username } = req.session.passport.user;
    log.info(`Received request to log out user ${username} on Identity Provider.`);
    const strategy = passportImpl.getStrategy();
    // Create logout request for IdP
    strategy.logout(req, async (err, url) => {
      // Destroy local sessions
      logOutAllSessions(req, res);
      // Redirect to SLO callback URL and send logout request.
      return res.redirect(url);
    });
};

// SP initiated logout sequence  
app.get('/auth/logout', (req, res) => {
    const { username } = req.session.passport.user;
    // If user not logged in, redirect to login
    if (!req.user) {
      return res.redirect('/saml/login');
    }

    if (username === 'Administrator' || username === 'Support user') {
      logOutLocalSession(req.session.id);
    } else {
      logOutIdp(req, res);
    }
});

// IdP initiated logout sequence or from other SP
app.post('/slo/callback', logOutAllSessions);

Если какая-то информация отсутствует, я смогу предоставить. Я надеюсь, что смогу узнать, что делать дальше! Заранее спасибо!

1 Ответ

1 голос
/ 05 апреля 2020

С точки зрения конфигурации ADFS:

«Доверенный URL-адрес» должен быть конечной точкой выхода из ADFS - вы можете видеть это в метаданных, чтобы ADFS могла очищать файлы cookie.

«Ответ URL "должен быть конечной точкой в ​​вашем приложении. ожидает ответа SLO, чтобы очистить куки-файлы клиента.

...