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

Merge pull request #1049 from yaodingyd/auth

Add Passport HTTP basic auth strategy
Yuki Takei 6 лет назад
Родитель
Сommit
dbd2c02c8f

+ 1 - 0
package.json

@@ -114,6 +114,7 @@
     "passport": "^0.4.0",
     "passport-github": "^1.1.0",
     "passport-google-auth": "^1.0.2",
+    "passport-http": "^0.3.0",
     "passport-ldapauth": "^2.0.0",
     "passport-local": "^1.0.0",
     "passport-saml": "^1.0.0",

+ 2 - 1
resource/locales/en-US/translation.json

@@ -439,7 +439,8 @@
   },
 
   "security_setting": {
-		"Basic authentication": "Basic authentication",
+		"Basic authentication": "Basic Authentication",
+		"Security settings": "Security settings",
 		"Guest users access": "Guest users access",
 		"Register limitation": "Register limitation",
 		"The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",

+ 1 - 0
src/server/crowi/index.js

@@ -277,6 +277,7 @@ Crowi.prototype.setupPassport = function() {
     this.passportService.setupTwitterStrategy();
     this.passportService.setupOidcStrategy();
     this.passportService.setupSamlStrategy();
+    this.passportService.setupBasicStrategy();
   }
   catch (err) {
     logger.error(err);

+ 9 - 0
src/server/form/admin/securityPassportBasic.js

@@ -0,0 +1,9 @@
+const form = require('express-form');
+
+const field = form.field;
+
+module.exports = form(
+  field('settingForm[security:passport-basic:isEnabled]').trim().toBooleanStrict().required(),
+  field('settingForm[security:passport-basic:id]').trim(),
+  field('settingForm[security:passport-basic:password]').trim(),
+);

+ 1 - 0
src/server/form/index.js

@@ -23,6 +23,7 @@ module.exports = {
     securityMechanism: require('./admin/securityMechanism'),
     securityPassportLdap: require('./admin/securityPassportLdap'),
     securityPassportSaml: require('./admin/securityPassportSaml'),
+    securityPassportBasic: require('./admin/securityPassportBasic'),
     securityPassportGoogle: require('./admin/securityPassportGoogle'),
     securityPassportGitHub: require('./admin/securityPassportGitHub'),
     securityPassportTwitter: require('./admin/securityPassportTwitter'),

+ 6 - 0
src/server/models/config.js

@@ -86,6 +86,7 @@ module.exports = function(crowi) {
       'security:passport-github:isEnabled' : false,
       'security:passport-twitter:isEnabled' : false,
       'security:passport-oidc:isEnabled' : false,
+      'security:passport-basic:isEnabled' : false,
 
       'aws:bucket'          : 'growi',
       'aws:region'          : 'ap-northeast-1',
@@ -348,6 +349,11 @@ module.exports = function(crowi) {
     return getValueForCrowiNS(config, key);
   };
 
+  configSchema.statics.isEnabledPassportBasic = function(config) {
+    const key = 'security:passport-basic:isEnabled';
+    return getValueForCrowiNS(config, key);
+  };
+
   configSchema.statics.isUploadable = function(config) {
     const method = process.env.FILE_UPLOAD || 'aws';
 

+ 28 - 0
src/server/routes/admin.js

@@ -991,6 +991,34 @@ module.exports = function(crowi, app) {
     return res.json({ status: true });
   };
 
+  actions.api.securityPassportBasicSetting = async(req, res) => {
+    const form = req.form.settingForm;
+
+    if (!req.form.isValid) {
+      return res.json({ status: false, message: req.form.errors.join('\n') });
+    }
+
+    debug('form content', form);
+    await saveSettingAsync(form);
+    const config = await crowi.getConfig();
+
+    // reset strategy
+    await crowi.passportService.resetBasicStrategy();
+    // setup strategy
+    if (Config.isEnabledPassportBasic(config)) {
+      try {
+        await crowi.passportService.setupBasicStrategy(true);
+      }
+      catch (err) {
+        // reset
+        await crowi.passportService.resetBasicStrategy();
+        return res.json({ status: false, message: err.message });
+      }
+    }
+
+    return res.json({ status: true });
+  };
+
   actions.api.securityPassportGoogleSetting = async(req, res) => {
     const form = req.form.settingForm;
 

+ 2 - 0
src/server/routes/index.js

@@ -71,6 +71,7 @@ module.exports = function(crowi, app) {
   app.post('/_api/admin/security/mechanism'     , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityMechanism, admin.api.securitySetting);
   app.post('/_api/admin/security/passport-ldap' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportLdap, admin.api.securityPassportLdapSetting);
   app.post('/_api/admin/security/passport-saml' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportSaml, admin.api.securityPassportSamlSetting);
+  app.post('/_api/admin/security/passport-basic', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportBasic, admin.api.securityPassportBasicSetting);
 
   // OAuth
   app.post('/_api/admin/security/passport-google' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportGoogle, admin.api.securityPassportGoogleSetting);
@@ -82,6 +83,7 @@ module.exports = function(crowi, app) {
   app.get('/passport/twitter'                     , loginPassport.loginWithTwitter);
   app.get('/passport/oidc'                        , loginPassport.loginWithOidc);
   app.get('/passport/saml'                        , loginPassport.loginWithSaml);
+  app.get('/passport/basic'                       , loginPassport.loginWithBasic);
   app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback);
   app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback);
   app.get('/passport/twitter/callback'            , loginPassport.loginPassportTwitterCallback);

+ 46 - 0
src/server/routes/login-passport.js

@@ -455,6 +455,51 @@ module.exports = function(crowi, app) {
     });
   };
 
+  /**
+   * middleware that login with BasicStrategy
+   * @param {*} req
+   * @param {*} res
+   * @param {*} next
+   */
+  const loginWithBasic = async(req, res, next) => {
+    if (!passportService.isBasicStrategySetup) {
+      debug('BasicStrategy has not been set up');
+      req.flash('warningMessage', 'Basic has not been set up');
+      return next();
+    }
+
+    const providerId = 'basic';
+    const strategyName = 'basic';
+    let userId;
+
+    try {
+      userId = await promisifiedPassportAuthentication(strategyName, req, res);
+    }
+    catch (err) {
+      // display prompt in browser
+      res.setHeader('WWW-Authenticate', 'Basic realm="Users"');
+      res.sendStatus(401).end();
+      return;
+    }
+
+    const userInfo = {
+      id: userId,
+      username: userId,
+      name: userId,
+    };
+
+    const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
+    if (!externalAccount) {
+      return loginFailure(req, res, next);
+    }
+
+    const user = await externalAccount.getPopulatedUser();
+    await req.logIn(user, (err) => {
+      if (err) { return next() }
+      return loginSuccess(req, res, user);
+    });
+  };
+
   const promisifiedPassportAuthentication = (strategyName, req, res) => {
     return new Promise((resolve, reject) => {
       passport.authenticate(strategyName, (err, response, info) => {
@@ -530,6 +575,7 @@ module.exports = function(crowi, app) {
     loginWithTwitter,
     loginWithOidc,
     loginWithSaml,
+    loginWithBasic,
     loginPassportGoogleCallback,
     loginPassportGitHubCallback,
     loginPassportTwitterCallback,

+ 54 - 0
src/server/service/passport.js

@@ -9,6 +9,7 @@ const TwitterStrategy = require('passport-twitter').Strategy;
 const OidcStrategy = require('openid-client').Strategy;
 const SamlStrategy = require('passport-saml').Strategy;
 const OIDCIssuer = require('openid-client').Issuer;
+const BasicStrategy = require('passport-http').BasicStrategy;
 
 /**
  * the service class of Passport
@@ -58,6 +59,11 @@ class PassportService {
      */
     this.isSamlStrategySetup = false;
 
+    /**
+     * the flag whether BasicStrategy is set up successfully
+     */
+    this.isBasicStrategySetup = false;
+
     /**
      * the flag whether serializer/deserializer are set up successfully
      */
@@ -588,6 +594,54 @@ class PassportService {
     return missingRequireds;
   }
 
+  /**
+   * reset BasicStrategy
+   *
+   * @memberof PassportService
+   */
+  resetBasicStrategy() {
+    debug('BasicStrategy: reset');
+    passport.unuse('basic');
+    this.isBasicStrategySetup = false;
+  }
+
+  /**
+   * setup BasicStrategy
+   *
+   * @memberof PassportService
+   */
+  setupBasicStrategy() {
+    // check whether the strategy has already been set up
+    if (this.isBasicStrategySetup) {
+      throw new Error('BasicStrategy has already been set up');
+    }
+
+    const configManager = this.crowi.configManager;
+    const isBasicEnabled = configManager.getConfig('crowi', 'security:passport-basic:isEnabled');
+
+    // when disabled
+    if (!isBasicEnabled) {
+      return;
+    }
+
+    debug('BasicStrategy: setting up..');
+
+    const configId = configManager.getConfig('crowi', 'security:passport-basic:id');
+    const configPassword = configManager.getConfig('crowi', 'security:passport-basic:password');
+
+    passport.use(new BasicStrategy(
+      (userId, password, done) => {
+        if (userId !== configId || password !== configPassword) {
+          return done(null, false, { message: 'Incorrect credentials.' });
+        }
+        return done(null, userId);
+      },
+    ));
+
+    this.isBasicStrategySetup = true;
+    debug('BasicStrategy: setup is done');
+  }
+
   /**
    * setup serializer and deserializer
    *

+ 5 - 0
src/server/util/swigFunctions.js

@@ -178,6 +178,11 @@ module.exports = function(crowi, app, req, locals) {
     return locals.isEnabledPassport() && config.crowi['security:passport-oidc:isEnabled'];
   };
 
+  locals.passportBasicLoginEnabled = function() {
+    const config = crowi.getConfig();
+    return locals.isEnabledPassport() && config.crowi['security:passport-basic:isEnabled'];
+  };
+
   locals.searchConfigured = function() {
     if (crowi.getSearcher()) {
       return true;

+ 7 - 0
src/server/views/admin/security.html

@@ -324,6 +324,9 @@
             <li>
               <a href="#passport-oidc" data-toggle="tab" role="tab"><i class="fa fa-openid"></i> OIDC</a>
             </li>
+            <li>
+              <a href="#passport-basic" data-toggle="tab" role="tab"><i class="fa fa-sign-in"></i> Basic</a>
+            </li>
             <li class="tbd">
               <a href="#passport-facebook" data-toggle="tab" role="tab"><i class="fa fa-facebook"></i> (TBD) Facebook</a>
             </li>
@@ -358,6 +361,10 @@
               {% include './widget/passport/github.html' %}
             </div>
 
+            <div id="passport-basic" class="tab-pane" role="tabpanel">
+              {% include './widget/passport/basic.html' %}
+            </div>
+
           </div><!-- /.tab-content -->
         </div>
 

+ 62 - 0
src/server/views/admin/widget/passport/basic.html

@@ -0,0 +1,62 @@
+<form action="/_api/admin/security/passport-basic" method="post" class="form-horizontal passportStrategy" id="basicSetting" role="form"
+    {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
+  <legend class="alert-anchor">{{ t("security_setting.Basic authentication") }} {{ t("security_setting.configuration") }}</legend>
+
+  {% set nameForIsbasicEnabled = "settingForm[security:passport-basic:isEnabled]" %}
+  {% set isbasicEnabled = settingForm['security:passport-basic:isEnabled'] %}
+
+  <div class="form-group">
+    <label for="{{nameForIsbasicEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.Basic authentication") }}</label>
+    <div class="col-xs-6">
+      <div class="btn-group btn-toggle" data-toggle="buttons">
+        <label class="btn btn-default btn-rounded btn-outline {% if isbasicEnabled %}active{% endif %}" data-active-class="primary">
+          <input name="{{nameForIsbasicEnabled}}" value="true" type="radio"
+              {% if true === isbasicEnabled %}checked{% endif %}> ON
+        </label>
+        <label class="btn btn-default btn-rounded btn-outline {% if !isbasicEnabled %}active{% endif %}" data-active-class="default">
+          <input name="{{nameForIsbasicEnabled}}" value="false" type="radio"
+              {% if !isbasicEnabled %}checked{% endif %}> OFF
+        </label>
+      </div>
+    </div>
+  </div>
+  <fieldset id="passport-basic-hide-when-disabled" {%if !isbasicEnabled %}style="display: none;"{% endif %}>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-basic:id]" class="col-xs-3 control-label">ID</label>
+      <div class="col-xs-6">
+        <input class="form-control" type="text" name="settingForm[security:passport-basic:id]" value="{{ settingForm['security:passport-basic:id'] || '' }}">
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-basic:password]" class="col-xs-3 control-label">{{ t("Password") }}</label>
+      <div class="col-xs-6">
+        <input class="form-control" type="text" name="settingForm[security:passport-basic:password]" value="{{ settingForm['security:passport-basic:password'] || '' }}">
+      </div>
+    </div>
+
+  </fieldset>
+
+  <div class="form-group" id="btn-update">
+    <div class="col-xs-offset-3 col-xs-6">
+      <input type="hidden" name="_csrf" value="{{ csrf() }}">
+      <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
+    </div>
+  </div>
+
+</form>
+
+<script>
+  $('input[name="settingForm[security:passport-basic:isEnabled]"]').change(function() {
+    const isEnabled = ($(this).val() === "true");
+
+    if (isEnabled) {
+      $('#passport-basic-hide-when-disabled').show(400);
+    }
+    else {
+      $('#passport-basic-hide-when-disabled').hide(400);
+    }
+  });
+</script>
+

+ 11 - 1
src/server/views/login.html

@@ -144,7 +144,7 @@
           </form>
         </div>
         {% endif %}
-        {% if passportGoogleLoginEnabled() || passportGitHubLoginEnabled() || passportFacebookLoginEnabled() || passportTwitterLoginEnabled() || passportOidcLoginEnabled() || passportSamlLoginEnabled() %}
+        {% if passportGoogleLoginEnabled() || passportGitHubLoginEnabled() || passportFacebookLoginEnabled() || passportTwitterLoginEnabled() || passportOidcLoginEnabled() || passportSamlLoginEnabled() || passportBasicLoginEnabled() %}
         <hr class="mb-1">
         <div class="collapse collapse-oauth collapse-anchor">
           <div class="spacer"></div>
@@ -208,6 +208,16 @@
               <div class="small text-right">with SAML</div>
             </form>
             {% endif %}
+            {% if passportBasicLoginEnabled() %}
+            <form role="form" action="/passport/basic" class="d-inline-flex flex-column">
+              <input type="hidden" name="_csrf" value="{{ csrf() }}">
+              <button type="submit" class="fcbtn btn btn-1b btn-login-oauth d-inline-flex" id="basic">
+                <span class="btn-label"><i class="fa fa-key"></i></span>
+                <span class="btn-label-text">{{ t('Sign in') }}</span>
+              </button>
+              <div class="small text-right">with HTTP Basic</div>
+            </form>
+            {% endif %}
           </div>{# ./d-flex flex-row flex-wrap #}
           <div class="spacer"></div>
         </div>

+ 7 - 0
yarn.lock

@@ -7943,6 +7943,13 @@ passport-google-auth@^1.0.2:
     googleapis "^16.0.0"
     passport-strategy "1.x"
 
+passport-http@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/passport-http/-/passport-http-0.3.0.tgz#8ee53d4380be9c60df2151925029826f77115603"
+  integrity sha1-juU9Q4C+nGDfIVGSUCmCb3cRVgM=
+  dependencies:
+    passport-strategy "1.x.x"
+
 passport-ldapauth@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/passport-ldapauth/-/passport-ldapauth-2.0.0.tgz#42dff004417185d0a4d9f776a3eed8d4731fd689"