Просмотр исходного кода

Merge pull request #5015 from weseek/feat/gw7634-retry-oidcissuer-discovery

feat: Implement retry OIDCIssuer discovery
Haku Mizuki 4 лет назад
Родитель
Сommit
07dd229fd5
1 измененных файлов с 92 добавлено и 59 удалено
  1. 92 59
      packages/app/src/server/service/passport.ts

+ 92 - 59
packages/app/src/server/service/passport.ts

@@ -13,6 +13,7 @@ import { BasicStrategy } from 'passport-http';
 
 import { IncomingMessage } from 'http';
 import got from 'got';
+import pRetry from 'p-retry';
 import loggerFactory from '~/utils/logger';
 
 import S2sMessage from '../models/vo/s2s-message';
@@ -628,70 +629,72 @@ class PassportService implements S2sMessageHandlable {
     const redirectUri = (configManager.getConfig('crowi', 'app:siteUrl') != null)
       ? urljoin(this.crowi.appService.getSiteUrl(), '/passport/oidc/callback')
       : configManager.getConfig('crowi', 'security:passport-oidc:callbackUrl'); // DEPRECATED: backward compatible with v3.2.3 and below
-    // Check and initialize connection to OIDC issuer host
-    // Prevent request timeout error on app init
-    const oidcHostReady = await this.isOidcHostReachable(issuerHost);
-    const oidcIssuer = oidcHostReady ? await OIDCIssuer.discover(issuerHost) : null;
-    logger.debug('Discovered issuer %s %O', oidcIssuer.issuer, oidcIssuer.metadata);
 
-    const authorizationEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:authorizationEndpoint');
-    if (authorizationEndpoint) {
-      oidcIssuer.metadata.authorization_endpoint = authorizationEndpoint;
-    }
-    const tokenEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:tokenEndpoint');
-    if (tokenEndpoint) {
-      oidcIssuer.metadata.token_endpoint = tokenEndpoint;
-    }
-    const revocationEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:revocationEndpoint');
-    if (revocationEndpoint) {
-      oidcIssuer.metadata.revocation_endpoint = revocationEndpoint;
-    }
-    const introspectionEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:introspectionEndpoint');
-    if (introspectionEndpoint) {
-      oidcIssuer.metadata.introspection_endpoint = introspectionEndpoint;
-    }
-    const userInfoEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:userInfoEndpoint');
-    if (userInfoEndpoint) {
-      oidcIssuer.metadata.userinfo_endpoint = userInfoEndpoint;
-    }
-    const endSessionEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:endSessionEndpoint');
-    if (endSessionEndpoint) {
-      oidcIssuer.metadata.end_session_endpoint = endSessionEndpoint;
-    }
-    const registrationEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:registrationEndpoint');
-    if (registrationEndpoint) {
-      oidcIssuer.metadata.registration_endpoint = registrationEndpoint;
-    }
-    const jwksUri = configManager.getConfig('crowi', 'security:passport-oidc:jwksUri');
-    if (jwksUri) {
-      oidcIssuer.metadata.jwks_uri = jwksUri;
-    }
-    logger.debug('Configured issuer %s %O', oidcIssuer.issuer, oidcIssuer.metadata);
+    // Prevent request timeout error on app init
+    const oidcIssuer = await this.getOIDCIssuerInstace(issuerHost);
+    if (oidcIssuer != null) {
+      logger.debug('Discovered issuer %s %O', oidcIssuer.issuer, oidcIssuer.metadata);
 
-    const client = new oidcIssuer.Client({
-      client_id: clientId,
-      client_secret: clientSecret,
-      redirect_uris: [redirectUri],
-      response_types: ['code'],
-    });
-    // prevent error AssertionError [ERR_ASSERTION]: id_token issued in the future
-    // Doc: https://github.com/panva/node-openid-client/tree/v2.x#allow-for-system-clock-skew
-    client.CLOCK_TOLERANCE = 5;
-    passport.use('oidc', new OidcStrategy({
-      client,
-      params: { scope: 'openid email profile' },
-    },
-    ((tokenset, userinfo, done) => {
-      if (userinfo) {
-        return done(null, userinfo);
+      const authorizationEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:authorizationEndpoint');
+      if (authorizationEndpoint) {
+        oidcIssuer.metadata.authorization_endpoint = authorizationEndpoint;
+      }
+      const tokenEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:tokenEndpoint');
+      if (tokenEndpoint) {
+        oidcIssuer.metadata.token_endpoint = tokenEndpoint;
+      }
+      const revocationEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:revocationEndpoint');
+      if (revocationEndpoint) {
+        oidcIssuer.metadata.revocation_endpoint = revocationEndpoint;
+      }
+      const introspectionEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:introspectionEndpoint');
+      if (introspectionEndpoint) {
+        oidcIssuer.metadata.introspection_endpoint = introspectionEndpoint;
+      }
+      const userInfoEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:userInfoEndpoint');
+      if (userInfoEndpoint) {
+        oidcIssuer.metadata.userinfo_endpoint = userInfoEndpoint;
+      }
+      const endSessionEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:endSessionEndpoint');
+      if (endSessionEndpoint) {
+        oidcIssuer.metadata.end_session_endpoint = endSessionEndpoint;
       }
+      const registrationEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:registrationEndpoint');
+      if (registrationEndpoint) {
+        oidcIssuer.metadata.registration_endpoint = registrationEndpoint;
+      }
+      const jwksUri = configManager.getConfig('crowi', 'security:passport-oidc:jwksUri');
+      if (jwksUri) {
+        oidcIssuer.metadata.jwks_uri = jwksUri;
+      }
+      logger.debug('Configured issuer %s %O', oidcIssuer.issuer, oidcIssuer.metadata);
 
-      return done(null, false);
+      const client = new oidcIssuer.Client({
+        client_id: clientId,
+        client_secret: clientSecret,
+        redirect_uris: [redirectUri],
+        response_types: ['code'],
+      });
+      // prevent error AssertionError [ERR_ASSERTION]: id_token issued in the future
+      // Doc: https://github.com/panva/node-openid-client/tree/v2.x#allow-for-system-clock-skew
+      client.CLOCK_TOLERANCE = 5;
+      passport.use('oidc', new OidcStrategy({
+        client,
+        params: { scope: 'openid email profile' },
+      },
+      ((tokenset, userinfo, done) => {
+        if (userinfo) {
+          return done(null, userinfo);
+        }
 
-    })));
+        return done(null, false);
+
+      })));
+
+      this.isOidcStrategySetup = true;
+      logger.debug('OidcStrategy: setup is done');
+    }
 
-    this.isOidcStrategySetup = true;
-    logger.debug('OidcStrategy: setup is done');
   }
 
   /**
@@ -719,10 +722,40 @@ class PassportService implements S2sMessageHandlable {
       return response.statusCode === 200;
     }
     catch (err) {
-      logger.debug('Issuer host unreachable:', err.code);
+      logger.error('OidcStrategy: issuer host unreachable:', err.code);
     }
   }
 
+  /**
+   * Get oidcIssuer object
+   * Utilize p-retry package to retry oidcIssuer initialization 3 times
+   *
+   * @param issuerHost
+   * @returns instance of OIDCIssuer
+   */
+  async getOIDCIssuerInstace(issuerHost) {
+    const oidcIssuerHostReady = await this.isOidcHostReachable(issuerHost);
+    if (!oidcIssuerHostReady) {
+      logger.error('OidcStrategy: setup failed: OIDC Issur host unreachable');
+      return;
+    }
+    const oidcIssuer = await pRetry(async() => {
+      return OIDCIssuer.discover(issuerHost);
+    }, {
+      onFailedAttempt: (error) => {
+        // get current OIDCIssuer.defaultHttpOptions.timeout
+        const oidcOptionTimeout = OIDCIssuer.defaultHttpOptions.timeout;
+        // Increases OIDCIssuer.defaultHttpOptions.timeout by squaring 1.5 with attempNumber on each attempt
+        OIDCIssuer.defaultHttpOptions = { timeout: oidcOptionTimeout * (1.5 ** error.attemptNumber) };
+        logger.debug(`OidcStrategy: setup attempt ${error.attemptNumber} failed with error: ${error}. Retrying ...`);
+      },
+      retries: 2,
+    }).catch((error) => {
+      logger.error(`OidcStrategy: setup failed with error: ${error} `);
+    });
+    return oidcIssuer;
+  }
+
   setupSamlStrategy() {
 
     this.resetSamlStrategy();