Sfoglia il codice sorgente

Merge pull request #481 from weseek/feat/OAuth-google-mizobuchi

Feat/oauth google mizobuchi
Yuki Takei 7 anni fa
parent
commit
9daa103786

+ 2 - 1
lib/form/admin/securityPassportGoogle.js

@@ -7,5 +7,6 @@ var form = require('express-form')
 module.exports = form(
   field('settingForm[security:passport-google:isEnabled]').trim().toBooleanStrict().required(),
   field('settingForm[security:passport-google:clientId]').trim(),
-  field('settingForm[security:passport-google:clientSecret]').trim()
+  field('settingForm[security:passport-google:clientSecret]').trim(),
+  field('settingForm[security:passport-google:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict()
 );

+ 3 - 1
lib/locales/en-US/translation.json

@@ -365,7 +365,9 @@
       "group_search_user_DN_property_detail": "The property of user object to use in <code>&#123;&#123;dn&#125;&#125;</code> interpolation of <code>Group Search Filter</code>.",
       "test_config": "Test Saved Configuration"
     },
-    "Google OAuth": {
+    "Google_OAuth": {
+      "use_Google_OAuth": "Google OAuth",
+      "change_redirect_url": "Enter <code>%s</code> <br>(where <code>%s</code> is your host name) for \"Authorized redirect URIs\"."
     },
     "Facebook": {
     },

+ 5 - 2
lib/locales/ja/translation.json

@@ -379,9 +379,12 @@
       "group_search_filter_detail2": "ログイン対象ユーザーオブジェクトのプロパティーで置換する場合は <code>&#123;&#123;dn&#125;&#125;</code> を用いてください。",
       "group_search_filter_detail3": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> は <code>cn=group1</code> と、ユーザーの <code>uid</code> を含む <code>memberUid</code> を持つグループにヒットします(<code>ユーザーの DN プロパティー</code> がデフォルトから変更されていない場合)",
       "group_search_user_DN_property": "ユーザーの DN プロパティー",
-      "group_search_user_DN_property_detail": "<code>グループ検索フィルター</code> 内の <code>&#123;&#123;dn&#125;&#125;</code> で置換される、ユーザーオブジェクトのプロパティー"
+      "group_search_user_DN_property_detail": "<code>グループ検索フィルター</code> 内の <code>&#123;&#123;dn&#125;&#125;</code> で置換される、ユーザーオブジェクトのプロパティー",
+      "test_config": "ログインテスト"
     },
-    "Google OAuth": {
+    "Google_OAuth": {
+      "use_Google_OAuth": "Google OAuth認証",
+      "change_redirect_url": "承認済みのリダイレクトURLに、 <code>%s</code> を入力<br>(<code>%s</code>は環境に合わせて変更してください)"
     },
     "Facebook": {
     },

+ 99 - 94
lib/routes/login-passport.js

@@ -66,7 +66,7 @@ module.exports = function(crowi, app) {
    * @param {*} res
    * @param {*} next
    */
-  const loginWithLdap = (req, res, next) => {
+  const loginWithLdap = async(req, res, next) => {
     if (!passportService.isLdapStrategySetup) {
       debug('LdapStrategy has not been set up');
       return next();
@@ -78,77 +78,39 @@ module.exports = function(crowi, app) {
       });
     }
 
-    passport.authenticate('ldapauth', (err, ldapAccountInfo, info) => {
-      if (res.headersSent) {  // dirty hack -- 2017.09.25
-        return;               // cz: somehow passport.authenticate called twice when ECONNREFUSED error occurred
-      }
-
-      debug('--- authenticate with LdapStrategy ---');
-      debug('ldapAccountInfo', ldapAccountInfo);
-      debug('info', info);
-
-      if (err) {  // DB Error
-        logger.error('LDAP Server Error: ', err);
-        req.flash('warningMessage', 'LDAP Server Error occured.');
-        return next(); // pass and the flash message is displayed when all of authentications are failed.
-      }
-
-      // authentication failure
-      if (!ldapAccountInfo) { return next() }
-      // check groups
-      if (!isValidLdapUserByGroupFilter(ldapAccountInfo)) {
-        return loginFailure(req, res, next);
-      }
+    const providerId = 'ldap';
+    const strategyName = 'ldapauth';
+    const ldapAccountInfo = await promisifiedPassportAuthentication(req, res, next, strategyName);
 
-      /*
-       * authentication success
-       */
-      // it is guaranteed that username that is input from form can be acquired
-      // because this processes after authentication
-      const ldapAccountId = passportService.getLdapAccountIdFromReq(req);
+    // check groups for LDAP
+    if (!isValidLdapUserByGroupFilter(ldapAccountInfo)) {
+      return loginFailure(req, res, next);
+    }
 
-      const attrMapUsername = passportService.getLdapAttrNameMappedToUsername();
-      const attrMapName = passportService.getLdapAttrNameMappedToName();
-      const usernameToBeRegistered = ldapAccountInfo[attrMapUsername];
-      const nameToBeRegistered = ldapAccountInfo[attrMapName];
+    /*
+      * authentication success
+      */
+    // it is guaranteed that username that is input from form can be acquired
+    // because this processes after authentication
+    const ldapAccountId = passportService.getLdapAccountIdFromReq(req);
+    const attrMapUsername = passportService.getLdapAttrNameMappedToUsername();
+    const attrMapName = passportService.getLdapAttrNameMappedToName();
+    const usernameToBeRegistered = ldapAccountInfo[attrMapUsername];
+    const nameToBeRegistered = ldapAccountInfo[attrMapName];
+    const userInfo = {
+      'id': ldapAccountId,
+      'username': usernameToBeRegistered,
+      'name': nameToBeRegistered
+    }
 
-      // find or register(create) user
-      ExternalAccount.findOrRegister('ldap', ldapAccountId, usernameToBeRegistered, nameToBeRegistered)
-        .catch((err) => {
-          if (err.name === 'DuplicatedUsernameException') {
-            // get option
-            const isSameUsernameTreatedAsIdenticalUser = Config.isSameUsernameTreatedAsIdenticalUser(config, 'ldap');
-            if (isSameUsernameTreatedAsIdenticalUser) {
-              // associate to existing user
-              debug(`ExternalAccount '${ldapAccountId}' will be created and bound to the exisiting User account`);
-              return ExternalAccount.associate('ldap', ldapAccountId, err.user);
-            }
-          }
-          throw err;  // throw again
-        })
-        .then((externalAccount) => {
-          return externalAccount.getPopulatedUser();
-        })
-        .then((user) => {
-          // login
-          req.logIn(user, (err) => {
-            if (err) { return next() }
-            else {
-              return loginSuccess(req, res, user);
-            }
-          });
-        })
-        .catch((err) => {
-          if (err.name === 'DuplicatedUsernameException') {
-            req.flash('isDuplicatedUsernameExceptionOccured', true);
-            return next();
-          }
-          else {
-            return next(err);
-          }
-        });
+    const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
+    const user = await externalAccount.getPopulatedUser();
 
-    })(req, res, next);
+    // login
+    await req.logIn(user, err => {
+      if (err) { return next(err) };
+      return loginSuccess(req, res, user);
+    });
   };
 
   /**
@@ -250,34 +212,77 @@ module.exports = function(crowi, app) {
     })(req, res);
   };
 
-  const loginPassportGoogleCallback = function(req, res, next) {
-    passport.authenticate('google', {failureRedirect: '/login'}, (request, response) => {
-      ExternalAccount.findOrRegister(
-        'google',
-        response.id,
-        response.displayName,
-        `${response.name.givenName} ${response.name.familyName}`
-      )
-      .catch((err) => {
-        throw err;
-      })
-      .then((externalAccount) => {
-        return externalAccount.getPopulatedUser();
-      })
-      .then((user) => {
-        // login
-        req.logIn(user, err => {
-          if (err) {
-            return next();
-          }
-          else {
-            return loginSuccess(req, res, user);
-          }
-        });
-      });
-    })(req, res, next);
+  const loginPassportGoogleCallback = async(req, res, next) => {
+    const providerId = 'google';
+    const strategyName = 'google';
+    const response = await promisifiedPassportAuthentication(req, res, next, strategyName);
+    const userInfo = {
+      'id': response.id,
+      'username': response.displayName,
+      'name': `${response.name.givenName} ${response.name.familyName}`
+    }
+    const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
+    const user = await externalAccount.getPopulatedUser();
+
+    // login
+    await 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) => {
+        if (res.headersSent) {  // dirty hack -- 2017.09.25
+          return;               // cz: somehow passport.authenticate called twice when ECONNREFUSED error occurred
+        }
+
+        if (err) {
+          logger.error(`'${strategyName}' passport authentication error: `, err);
+          req.flash('warningMessage', `Error occured in '${strategyName}' passport authentication`);
+          return next(); // pass and the flash message is displayed when all of authentications are failed.
+        }
+
+        // authentication failure
+        if (!response) {
+          return next();
+        }
+
+        resolve(response)
+      })(req, res, next);
+    });
+  };
+
+  const getOrCreateUser = async(req, res, next, userInfo, providerId) => {
+    try {
+      // find or register(create) user
+      const externalAccount = await ExternalAccount.findOrRegister(
+        providerId,
+        userInfo.id,
+        userInfo.username,
+        userInfo.name
+      );
+      return externalAccount;
+    }
+    catch (err) {
+      if (err.name === 'DuplicatedUsernameException') {
+        // get option
+        const isSameUsernameTreatedAsIdenticalUser = Config.isSameUsernameTreatedAsIdenticalUser(config, providerId);
+        if (isSameUsernameTreatedAsIdenticalUser) {
+          // associate to existing user
+          debug(`ExternalAccount '${userInfo.username}' will be created and bound to the exisiting User account`);
+          return ExternalAccount.associate(providerId, userInfo.id, err.user);
+        }
+        else {
+          req.flash('provider-DuplicatedUsernameException', providerId);
+          return loginFailure(req, res, next);
+        }
+      }
+      throw err;  // throw again
+    }
+  }
+
   return {
     loginFailure,
     loginWithLdap,

+ 5 - 0
lib/util/swigFunctions.js

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

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

@@ -5,7 +5,7 @@
   {% set nameForIsGoogleEnabled = "settingForm[security:passport-google:isEnabled]" %}
   {% set isGoogleEnabled = settingForm['security:passport-google:isEnabled'] %}
   <div class="form-group">
-    <label for="{{nameForIsGoogleEnabled}}" class="col-xs-3 control-label">Use Google OAuth</label>
+    <label for="{{nameForIsGoogleEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.Google_OAuth.use_Google_OAuth") }}</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 isGoogleEnabled %}active{% endif %}" data-active-class="primary">
@@ -20,16 +20,21 @@
     </div>
   </div>
   <fieldset id="passport-google-hide-when-disabled" {%if !isGoogleEnabled %}style="display: none;"{% endif %}>
-    <h4>{{ t("security_setting.google_setting") }}</h4>
-    <ol class="help-block">
-      <li>{{ t("security_setting.access_api_manager") }}</li>
-      <li>{{ t("security_setting.create_project") }}</li>
-      <li>{{ t("security_setting.create_auth_to_oauth") }}</li>
-      <ol>
-        <li>{{ t("security_setting.select_webapp") }}</li>
-        <li>{{ t("security_setting.change_redirect_url") }}</li>
-      </ol>
-    </ol>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-google:clientId]" class="col-xs-3 control-label">{{ t("security_setting.google_setting") }}</label>
+      <div class="col-xs-6">
+        <ol class="help-block">
+          <li>{{ t("security_setting.access_api_manager") }}</li>
+          <li>{{ t("security_setting.create_project") }}</li>
+          <li>{{ t("security_setting.create_auth_to_oauth") }}</li>
+          <ol>
+            <li>{{ t("security_setting.select_webapp") }}</li>
+            <li>{{ t("security_setting.Google_OAuth.change_redirect_url", "https://${growi.host}/passport/google/callback", "${growi.host}") }}</li>
+          </ol>
+        </ol>
+      </div>
+    </div>
 
     <div class="form-group">
       <label for="settingForm[security:passport-google:clientId]" class="col-xs-3 control-label">{{ t("security_setting.clientID") }}</label>
@@ -44,19 +49,29 @@
         <input class="form-control" type="text" name="settingForm[security:passport-google:clientSecret]" value="{{ settingForm['security:passport-google:clientSecret'] || '' }}">
       </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-Google" name="settingForm[security:passport-google:isSameUsernameTreatedAsIdenticalUser]" value="1"
+              {% if settingForm['security:passport-google:isSameUsernameTreatedAsIdenticalUser'] %}checked{% endif %} />
+          <label for="bindByUserName-Google">
+            {{ t("security_setting.ldap.Treat username matching as identical") }}
+          </label>
+          <p class="help-block">
+            <small>
+              {{ t("security_setting.ldap.Treat username matching as identical_warn") }}
+            </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>
-      <button type="button"
-            class="btn btn-default passport-google-hide-when-disabled"
-            data-target="#test-google-account" data-toggle="modal"
-            {%if !isGoogleEnabled %}style="display: none;"{% endif %}>
-
-          {{ t("security_setting.ldap.test_config") }}
-        </button>
     </div>
   </div>
 
@@ -137,25 +152,6 @@
 </ol>
 {% endif %}
 
-<div class="modal test-google-account" id="test-google-account">
-  <div class="modal-dialog">
-    <div class="modal-content">
-
-      <div class="modal-header">
-        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-        <div class="modal-title">Test Google Account</div>
-      </div>
-
-      <div class="modal-body">
-
-        {% include '../../../widget/passport/ldap-association-tester.html' with { showLog: true } %}
-
-      </div><!-- /.modal-body -->
-
-    </div><!-- /.modal-content -->
-  </div><!-- /.modal-dialog -->
-</div><!-- /.modal -->
-
 <script>
   $('input[name="settingForm[security:passport-google:isEnabled]"]').change(function() {
       const isEnabled = ($(this).val() === "true");

+ 13 - 11
lib/views/login.html

@@ -42,12 +42,12 @@
         # The case that there already exists a user whose username matches ID of the newly created LDAP user
         # https://github.com/weseek/growi/issues/193
         #}
-        {% set isDuplicatedUsernameExceptionOccured = req.flash('isDuplicatedUsernameExceptionOccured') %}
-        {% if isDuplicatedUsernameExceptionOccured != null %}
+        {% set failedProviderForDuplicatedUsernameException = req.flash('provider-DuplicatedUsernameException') %}
+        {% if failedProviderForDuplicatedUsernameException != null %}
         <div class="alert alert-warning small">
           <p><strong><i class="icon-fw icon-ban"></i>DuplicatedUsernameException occured</strong></p>
           <p>
-            Your LDAP authentication was succeess, but a new user could not be created.
+            Your {{ failedProviderForDuplicatedUsernameException }} authentication was succeess, but a new user could not be created.
             See the issue <a href="https://github.com/weseek/growi/issues/193">#193</a>.
           </p>
         </div>
@@ -145,9 +145,10 @@
         </div>
         {% endif %}
 
+        {% if passportGoogleLoginEnabled() || passportGithubLoginEnabled() || passportFacebookLoginEnabled() || passportTwitterLoginEnabled() %}
         <hr>
         <div class="input-group m-t-15 m-b-10 mx-auto d-flex flex-row justify-content-around flex-wrap">
-          {#% if passportGoogleLoginEnabled() %#}
+          {% if passportGoogleLoginEnabled() %}
           <form role="form" action="/passport/google" method="get">
             <button type="submit" class="fcbtn btn btn-danger btn-1b btn-login-google">
               <span class="btn-label"><i class="icon-social-google"></i></span>
@@ -155,8 +156,8 @@
             </button>
             <div class="small text-right">by Google Account</div>
           </form>
-          {#% endif %#}
-          {#% if passportGithubLoginEnabled() %#}
+          {% endif %}
+          {% if passportGithubLoginEnabled() %}
           <form role="form" action="/auth/passport/github" method="get">
             <input type="hidden" name="_csrf" value="{{ csrf() }}">
             <button type="submit" class="fcbtn btn btn-danger btn-1b btn-login-github">
@@ -165,8 +166,8 @@
             </button>
             <div class="small text-right">by Github Account</div>
           </form>
-          {#% endif %#}
-          {#% if passportFacebookLoginEnabled() %#}
+          {% endif %}
+          {% if passportFacebookLoginEnabled() %}
           <form role="form" action="/auth/passport/facebook" method="get">
             <input type="hidden" name="_csrf" value="{{ csrf() }}">
             <button type="submit" class="fcbtn btn btn-danger btn-1b btn-login-facebook">
@@ -175,8 +176,8 @@
             </button>
             <div class="small text-right">by Facebook Account</div>
           </form>
-          {#% endif %#}
-          {#% if passportTwitterLoginEnabled() %#}
+          {% endif %}
+          {% if passportTwitterLoginEnabled() %}
           <form role="form" action="/auth/passport/twitter" method="get">
             <input type="hidden" name="_csrf" value="{{ csrf() }}">
             <button type="submit" class="fcbtn btn btn-danger btn-1b btn-login-twitter">
@@ -185,8 +186,9 @@
             </button>
             <div class="small text-right">by Twitter Account</div>
           </form>
-          {#% endif %#}
+          {% endif %}
         </div>
+        {% endif %}
 
         <hr>