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

Merge pull request #581 from weseek/feat/OAuth-Twitter

Feat/OAuth-Twitter
Yuki Takei 7 лет назад
Родитель
Сommit
6e02033ea3

+ 1 - 0
lib/crowi/index.js

@@ -276,6 +276,7 @@ Crowi.prototype.setupPassport = function() {
     this.passportService.setupLdapStrategy();
     this.passportService.setupGoogleStrategy();
     this.passportService.setupGitHubStrategy();
+    this.passportService.setupTwitterStrategy(); 
   }
   catch (err) {
     logger.error(err);

+ 13 - 0
lib/form/admin/securityPassportTwitter.js

@@ -0,0 +1,13 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field
+  ;
+
+module.exports = form(
+  field('settingForm[security:passport-twitter:isEnabled]').trim().toBooleanStrict().required(),
+  field('settingForm[security:passport-twitter:consumerKey]').trim(),
+  field('settingForm[security:passport-twitter:consumerSecret]').trim(),
+  field('settingForm[security:passport-twitter:callbackUrl]').trim(),
+  field('settingForm[security:passport-twitter:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
+);

+ 1 - 0
lib/form/index.js

@@ -21,6 +21,7 @@ module.exports = {
     securityPassportLdap: require('./admin/securityPassportLdap'),
     securityPassportGoogle: require('./admin/securityPassportGoogle'),
     securityPassportGitHub: require('./admin/securityPassportGitHub'),
+    securityPassportTwitter: require('./admin/securityPassportTwitter'),
     markdown: require('./admin/markdown'),
     markdownXss: require('./admin/markdownXss'),
     customcss: require('./admin/customcss'),

+ 12 - 2
lib/locales/en-US/translation.json

@@ -389,13 +389,23 @@
         "name": "Facebook OAuth"
       },
       "Twitter": {
-        "name": "Twitter OAuth"
+        "name": "Twitter OAuth",
+        "register_1": "Access <a href=\"%s\" target=\"_blank\">%s</a>",
+        "register_2": "Sign in Twitter",
+        "register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
+        "register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>%s</code> (where <code>%s</code> is your hostname)",
+        "register_5": "Copy and paste your ClientID and Client Secret above"
       },
       "GitHub": {
         "name": "GitHub OAuth",
         "register_1": "Access <a href=\"%s\" target=\"_blank\">%s</a>",
         "register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>%s</code> (where <code>%s</code> is your hostname)",
-        "register_3": "Copy and paste your ClientID and Client Secret above"
+        "register_3": "Copy and paste your ClientID and Client Secret above"     
+      },
+      "how_to": {
+        "google": "How to configure Google OAuth?",
+        "github": "How to configure GitHub OAuth?",
+        "twitter": "How to configure Twitter OAuth?"
       }
     }
 	},

+ 11 - 1
lib/locales/ja/translation.json

@@ -406,13 +406,23 @@
         "name": "Facebook OAuth認証"
       },
       "Twitter": {
-        "name": "Twitter OAuth認証"
+        "name": "Twitter OAuth認証",
+        "register_1": "<a href=\"%s\" target=\"_blank\">%s</a>へアクセス",
+        "register_2": "Twitterにサインイン",
+        "register_3": "Create New Appをクリック &rightarrow; Application Detailsの各項目を入力",
+        "register_4": "Create your Twitter Applicationで作成",
+        "register_5": "上記フォームにクライアントIDとクライアントシークレットを入力"
       },
       "GitHub": {
         "name": "GitHub OAuth認証",
         "register_1": "<a href=\"%s\" target=\"_blank\">%s</a>へアクセス",
         "register_2": "\"Authorization callback URL\"を<code>%s</code>としてGrowiを登録 (<code>%s</code>は環境に合わせて変更してください)",
         "register_3": "上記フォームにクライアントIDとクライアントシークレットを入力"
+      },
+      "how_to": {
+        "google": "Google OAuthの設定方法",
+        "github": "GitHub OAuthの設定方法",
+        "twitter": "Twitter OAuthの設定方法"
       }
     }
   },

+ 6 - 0
lib/models/config.js

@@ -68,6 +68,7 @@ module.exports = function(crowi) {
       'security:passport-ldap:isSameUsernameTreatedAsIdenticalUser': false,
       'security:passport-google:isEnabled' : false,
       'security:passport-github:isEnabled' : false,
+      'security:passport-twitter:isEnabled' : false,
 
       'aws:bucket'          : 'growi',
       'aws:region'          : 'ap-northeast-1',
@@ -285,6 +286,11 @@ module.exports = function(crowi) {
     return getValueForCrowiNS(config, key);
   };
 
+   configSchema.statics.isEnabledPassportTwitter = function(config) {
+    const key = 'security:passport-twitter:isEnabled';
+    return getValueForCrowiNS(config, key);
+  };
+
   configSchema.statics.isSameUsernameTreatedAsIdenticalUser = function(config, providerType) {
     const key = `security:passport-${providerType}:isSameUsernameTreatedAsIdenticalUser`;
     return getValueForCrowiNS(config, key);

+ 28 - 0
lib/routes/admin.js

@@ -1089,6 +1089,34 @@ module.exports = function(crowi, app) {
     return res.json({status: true});
   };
 
+  actions.api.securityPassportTwitterSetting = 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.resetTwitterStrategy();
+    // setup strategy
+    if (Config.isEnabledPassportTwitter(config)) {
+      try {
+        await crowi.passportService.setupTwitterStrategy(true);
+      }
+      catch (err) {
+        // reset
+        await crowi.passportService.resetTwitterStrategy();
+        return res.json({status: false, message: err.message});
+      }
+    }
+
+    return res.json({status: true});
+  };
   actions.api.customizeSetting = function(req, res) {
     const form = req.form.settingForm;
 

+ 3 - 0
lib/routes/index.js

@@ -71,10 +71,13 @@ module.exports = function(crowi, app) {
   // OAuth
   app.post('/_api/admin/security/passport-google' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportGoogle, admin.api.securityPassportGoogleSetting);
   app.post('/_api/admin/security/passport-github' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportGitHub, admin.api.securityPassportGitHubSetting);
+  app.post('/_api/admin/security/passport-twitter' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportTwitter, admin.api.securityPassportTwitterSetting);
   app.get('/passport/google'                      , loginPassport.loginWithGoogle);
   app.get('/passport/github'                      , loginPassport.loginWithGitHub);
+  app.get('/passport/twitter'                     , loginPassport.loginWithTwitter);
   app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback);
   app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback);
+  app.get('/passport/twitter/callback'             , loginPassport.loginPassportTwitterCallback); 
 
   // markdown admin
   app.get('/admin/markdown'                   , loginRequired(crowi, app) , middleware.adminRequired() , admin.markdown.index);

+ 37 - 0
lib/routes/login-passport.js

@@ -277,6 +277,41 @@ module.exports = function(crowi, app) {
     });
   };
 
+  const loginWithTwitter = function(req, res, next) {
+    if (!passportService.isTwitterStrategySetup) {
+      debug('TwitterStrategy has not been set up');
+      req.flash('warningMessage', 'TwitterStrategy has not been set up');
+      return next();
+    }
+
+    passport.authenticate('twitter')(req, res);
+  };
+
+  const loginPassportTwitterCallback = async(req, res, next) => {
+    const providerId = 'twitter';
+    const strategyName = 'twitter';
+    const response = await promisifiedPassportAuthentication(req, res, next, strategyName);
+    const userInfo = {
+      'id': response.id,
+      'username': response.username,
+      'name': response.displayName
+    };
+
+    const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
+    if (!externalAccount) {
+      return loginFailure(req, res, next);
+    }
+
+    const user = await externalAccount.getPopulatedUser();
+
+    // login
+    req.logIn(user, err => {
+      if (err) { return next(err) }
+      return loginSuccess(req, res, user);
+    });
+  };
+
+
   const promisifiedPassportAuthentication = (req, res, next, strategyName) => {
     return new Promise((resolve, reject) => {
       passport.authenticate(strategyName, (err, response, info) => {
@@ -336,7 +371,9 @@ module.exports = function(crowi, app) {
     loginWithLocal,
     loginWithGoogle,
     loginWithGitHub,
+    loginWithTwitter,
     loginPassportGoogleCallback,
     loginPassportGitHubCallback,
+    loginPassportTwitterCallback,
   };
 };

+ 47 - 1
lib/service/passport.js

@@ -4,6 +4,7 @@ const LocalStrategy = require('passport-local').Strategy;
 const LdapStrategy = require('passport-ldapauth');
 const GoogleStrategy = require('passport-google-auth').Strategy;
 const GitHubStrategy = require('passport-github').Strategy;
+const TwitterStrategy = require('passport-twitter').Strategy;
 
 /**
  * the service class of Passport
@@ -342,7 +343,6 @@ class PassportService {
     this.isGitHubStrategySetup = true;
     debug('GitHubStrategy: setup is done');
   }
-
   /**
    * reset GoogleStrategy
    *
@@ -354,6 +354,52 @@ class PassportService {
     this.isGitHubStrategySetup = false;
   }
 
+  setupTwitterStrategy() {
+    // check whether the strategy has already been set up
+    if (this.isTwitterStrategySetup) {
+      throw new Error('TwitterStrategy has already been set up');
+    }
+
+    const config = this.crowi.config;
+    const Config = this.crowi.model('Config');
+    //this
+    const isTwitterEnabled = Config.isEnabledPassportTwitter(config);
+
+    // when disabled
+    if (!isTwitterEnabled) {
+      return;
+    }
+
+    debug('TwitterStrategy: setting up..');
+    passport.use(new TwitterStrategy({
+      consumerKey: config.crowi['security:passport-twitter:consumerKey'] || process.env.OAUTH_TWITTER_CONSUMER_KEY,
+      consumerSecret: config.crowi['security:passport-twitter:consumerSecret'] || process.env.OAUTH_TWITTER_CONSUMER_SECRET,
+      callbackURL: config.crowi['security:passport-twitter:callbackUrl'] || process.env.OAUTH_TWITTER_CALLBACK_URI,
+      skipUserProfile: false,
+    }, function(accessToken, refreshToken, profile, done) {
+      if (profile) {
+        return done(null, profile);
+      }
+      else {
+        return done(null, false);
+      }
+    }));
+
+    this.isTwitterStrategySetup = true;
+    debug('TwitterStrategy: setup is done');
+  }
+
+  /**
+   * reset GoogleStrategy
+   *
+   * @memberof PassportService
+   */
+  resetTwitterStrategy() {
+    debug('TwitterStrategy: reset');
+    passport.unuse('twitter');
+    this.isTwitterStrategySetup = false;
+  }
+
   /**
    * setup serializer and deserializer
    *

+ 5 - 0
lib/util/swigFunctions.js

@@ -102,6 +102,11 @@ module.exports = function(crowi, app, req, locals) {
     return locals.isEnabledPassport() && config.crowi['security:passport-github:isEnabled'];
   };
 
+  locals.passportTwitterLoginEnabled = function() {
+    var config = crowi.getConfig();
+    return locals.isEnabledPassport() && config.crowi['security:passport-twitter:isEnabled'];
+  };
+
   locals.searchConfigured = function() {
     if (crowi.getSearcher()) {
       return true;

+ 3 - 3
lib/views/admin/security.html

@@ -252,10 +252,10 @@
               <a href="#passport-github" data-toggle="tab" role="tab"><i class="fa fa-github"></i> GitHub</a>
             </li>
             <li class="tbd">
-              <a href="#passport-facebook" data-toggle="tab" role="tab"><i class="fa fa-facebook"></i> (TBD) Facebook</a>
+              <a href="#passport-twitter" data-toggle="tab" role="tab"><i class="fa fa-twitter"></i> Twitter</a>
             </li>
             <li class="tbd">
-              <a href="#passport-twitter" data-toggle="tab" role="tab"><i class="fa fa-twitter"></i> (TBD) Twitter</a>
+              <a href="#passport-facebook" data-toggle="tab" role="tab"><i class="fa fa-facebook"></i> (TBD) Facebook</a>
             </li>
           </ul>
 
@@ -288,7 +288,7 @@
   </div>
 
   <script>
-    $('#generalSetting, #googleSetting, #mechanismSetting, #githubSetting').each(function() {
+    $('#generalSetting, #googleSetting, #mechanismSetting, #githubSetting, #twitterSetting').each(function() {
       $(this).submit(function()
       {
         function showMessage(formId, msg, status) {

+ 1 - 1
lib/views/admin/widget/passport/github.html

@@ -92,7 +92,7 @@
 <hr>
 <h4>
   <i class="fa fa-question-circle" aria-hidden="true"></i>
-  <a href="#collapseHelpForGithubOauth" data-toggle="collapse">How to configure GitHub OAuth?</a>
+  <a href="#collapseHelpForGithubOauth" data-toggle="collapse">{{ t("security_setting.OAuth.how_to.github") }}</a>
 </h4>
 <ol id="collapseHelpForGithubOauth" class="collapse">
   <li>{{ t("security_setting.OAuth.GitHub.register_1", "https://github.com/settings/developers", "GitHub Developer Settings") }}</li>

+ 1 - 1
lib/views/admin/widget/passport/google-oauth.html

@@ -92,7 +92,7 @@
 <hr>
 <h4>
   <i class="fa fa-question-circle" aria-hidden="true"></i>
-  <a href="#collapseHelpForGoogleOauth" data-toggle="collapse">How to configure Google OAuth?</a>
+  <a href="#collapseHelpForGoogleOauth" data-toggle="collapse">{{ t("security_setting.OAuth.how_to.google") }}</a>
 </h4>
 <ol id="collapseHelpForGoogleOauth" class="collapse">
   <li>{{ t("security_setting.OAuth.Google.register_1", "https://console.cloud.google.com/apis/credentials", "Google Cloud Platform API Manager") }}</li>

+ 115 - 4
lib/views/admin/widget/passport/twitter.html

@@ -1,6 +1,117 @@
-<form action="" method="post" class="form-horizontal passportStrategy" id="twitterOauthSetting" role="form">
-  <fieldset>
-    <legend>Twitter OAuth {{ t("security_setting.configuration") }}</legend>
-    <p class="well">(TBD)</p>
+<form action="/_api/admin/security/passport-twitter" method="post" class="form-horizontal passportStrategy" id="twitterSetting" role="form"
+    {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
+  <legend class="alert-anchor">{{ t("security_setting.OAuth.Twitter.name") }}{{ t("security_setting.configuration") }}</legend>
+
+  {% set nameForIsTwitterEnabled = "settingForm[security:passport-twitter:isEnabled]" %}
+  {% set isTwitterEnabled = settingForm['security:passport-twitter:isEnabled'] %}
+  
+  <div class="form-group">
+    <label for="{{nameForIsTwitterEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.OAuth.Twitter.name") }}</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 isTwitterEnabled %}active{% endif %}" data-active-class="primary">
+          <input name="{{nameForIsTwitterEnabled}}" value="true" type="radio"
+              {% if true === isTwitterEnabled %}checked{% endif %}> ON
+        </label>
+        <label class="btn btn-default btn-rounded btn-outline {% if !isTwitterEnabled %}active{% endif %}" data-active-class="default">
+          <input name="{{nameForIsTwitterEnabled}}" value="false" type="radio"
+              {% if !isTwitterEnabled %}checked{% endif %}> OFF
+        </label>
+      </div>
+    </div>
+  </div>
+  <fieldset id="passport-twitter-hide-when-disabled" {%if !isTwitterEnabled %}style="display: none;"{% endif %}>
+
+
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-twitter:consumerKey]" class="col-xs-3 control-label">{{ t("security_setting.clientID") }}</label>
+      <div class="col-xs-6">
+        <input class="form-control" type="text" name="settingForm[security:passport-twitter:consumerKey]" value="{{ settingForm['security:passport-twitter:consumerKey'] || '' }}">
+        <p class="help-block">
+          <small>
+                {{ t("security_setting.Use env var if empty", "OAUTH_TWITTER_CONSUMER_KEY") }}
+          </small>
+        </p>
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-twitter:consumerSecret]" class="col-xs-3 control-label">{{ t("security_setting.client_secret") }}</label>
+      <div class="col-xs-6">
+        <input class="form-control" type="text" name="settingForm[security:passport-twitter:consumerSecret]" value="{{ settingForm['security:passport-twitter:consumerSecret'] || '' }}">
+        <p class="help-block">
+          <small>
+             {{ t("security_setting.Use env var if empty", "OAUTH_TWITTER_CONSUMER_SECRET") }}
+          </small>
+        </p>
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-twitter:callbackUrl]" class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
+      <div class="col-xs-6">
+        <input class="form-control" type="text" name="settingForm[security:passport-twitter:callbackUrl]" value="{{ settingForm['security:passport-twitter:callbackUrl'] || '' }}">
+        <p class="help-block">
+          <small>
+            {{ t("security_setting.Use env var if empty", "OAUTH_TWITTER_CALLBACK_URL") }}
+          </small>
+        </p>
+      </div>
+    </div>
+
+    
+    <div class="form-group">
+      <div class="col-xs-6 col-xs-offset-3">
+        <div class="checkbox checkbox-info">
+          <input type="checkbox" id="bindByUserName-Twitter" name="settingForm[security:passport-twitter:isSameUsernameTreatedAsIdenticalUser]" value="1"
+              {% if settingForm['security:passport-twitter:isSameUsernameTreatedAsIdenticalUser'] %}checked{% endif %} />
+          <label for="bindByUserName-Twitter">
+            {{ t("security_setting.Treat username matching as identical", "username") }}
+          </label>
+          <p class="help-block">
+            <small>
+              {{ t("security_setting.Treat username matching as identical_warn", "username") }}
+            </small>
+          </p>
+        </div>
+      </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>
+
+{# Help Section #}
+<hr>
+<h4>
+  <i class="fa fa-question-circle" aria-hidden="true"></i>
+  <a href="#collapseHelpForTwitterOauth" data-toggle="collapse">{{ t("security_setting.OAuth.how_to.twitter") }}</a>
+</h4>
+<ol id="collapseHelpForTwitterOauth" class="collapse">
+  <li>{{ t("security_setting.OAuth.Twitter.register_1", "https://apps.twitter.com/", "Twitter Application Management") }}</li>
+  <li>{{ t("security_setting.OAuth.Twitter.register_2") }}</li>
+  <li>{{ t("security_setting.OAuth.Twitter.register_3") }}</li>
+  <li>{{ t("security_setting.OAuth.Twitter.register_4", "https://${growi.host}/passport/twitter/callback", "${growi.host}") }}</li>
+</ol>
+
+<script>
+  $('input[name="settingForm[security:passport-twitter:isEnabled]"]').change(function() {
+      const isEnabled = ($(this).val() === "true");
+
+      if (isEnabled) {
+        $('#passport-twitter-hide-when-disabled').show(400);
+      }
+      else {
+        $('#passport-twitter-hide-when-disabled').hide(400);
+      }
+    });
+</script>
+

+ 1 - 0
package.json

@@ -102,6 +102,7 @@
     "passport-google-auth": "^1.0.2",
     "passport-ldapauth": "^2.0.0",
     "passport-local": "^1.0.0",
+    "passport-twitter": "^1.0.4",
     "rimraf": "^2.6.1",
     "slack-node": "^0.1.8",
     "socket.io": "^2.0.3",

+ 25 - 0
yarn.lock

@@ -6227,6 +6227,14 @@ passport-local@^1.0.0:
   dependencies:
     passport-strategy "1.x.x"
 
+passport-oauth1@1.x.x:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/passport-oauth1/-/passport-oauth1-1.1.0.tgz#a7de988a211f9cf4687377130ea74df32730c918"
+  dependencies:
+    oauth "0.9.x"
+    passport-strategy "1.x.x"
+    utils-merge "1.x.x"
+
 passport-oauth2@1.x.x:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.4.0.tgz#f62f81583cbe12609be7ce6f160b9395a27b86ad"
@@ -6240,6 +6248,13 @@ passport-strategy@1.x, passport-strategy@1.x.x, passport-strategy@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
 
+passport-twitter@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/passport-twitter/-/passport-twitter-1.0.4.tgz#01a799e1f760bf2de49f2ba5fba32282f18932d7"
+  dependencies:
+    passport-oauth1 "1.x.x"
+    xtraverse "0.1.x"
+
 passport@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.0.tgz#c5095691347bd5ad3b5e180238c3914d16f05811"
@@ -8968,6 +8983,10 @@ xmlbuilder@4.2.1, xmlbuilder@^4.1.0:
   dependencies:
     lodash "^4.0.0"
 
+xmldom@0.1.x:
+  version "0.1.27"
+  resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
+
 xmlhttprequest-ssl@~1.5.4:
   version "1.5.4"
   resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.4.tgz#04f560915724b389088715cc0ed7813e9677bf57"
@@ -8996,6 +9015,12 @@ xtend@~2.1.1:
   dependencies:
     object-keys "~0.4.0"
 
+xtraverse@0.1.x:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/xtraverse/-/xtraverse-0.1.0.tgz#b741bad018ef78d8a9d2e83ade007b3f7959c732"
+  dependencies:
+    xmldom "0.1.x"
+
 y18n@^3.2.0, y18n@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"