|
|
@@ -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();
|