Przeglądaj źródła

Merge pull request #498 from weseek/feat/OAuth

Feat/OAuth
Yuki Takei 7 lat temu
rodzic
commit
19ce021b78

+ 2 - 1
lib/crowi/index.js

@@ -263,7 +263,8 @@ Crowi.prototype.setupPassport = function() {
   // setup strategies
   this.passportService.setupLocalStrategy();
   this.passportService.setupLdapStrategy();
-
+  this.passportService.setupGoogleStrategy();
+  this.passportService.setupGitHubStrategy();
   return Promise.resolve();
 };
 

+ 12 - 0
lib/form/admin/securityPassportGitHub.js

@@ -0,0 +1,12 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field
+  ;
+
+module.exports = form(
+  field('settingForm[security:passport-github:isEnabled]').trim().toBooleanStrict().required(),
+  field('settingForm[security:passport-github:clientId]').trim(),
+  field('settingForm[security:passport-github:clientSecret]').trim(),
+  field('settingForm[security:passport-github:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict()
+);

+ 12 - 0
lib/form/admin/securityPassportGoogle.js

@@ -0,0 +1,12 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field
+  ;
+
+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:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict()
+);

+ 2 - 0
lib/form/index.js

@@ -19,6 +19,8 @@ module.exports = {
     securityGoogle: require('./admin/securityGoogle'),
     securityMechanism: require('./admin/securityMechanism'),
     securityPassportLdap: require('./admin/securityPassportLdap'),
+    securityPassportGoogle: require('./admin/securityPassportGoogle'),
+    securityPassportGitHub: require('./admin/securityPassportGitHub'),
     markdown: require('./admin/markdown'),
     customcss: require('./admin/customcss'),
     customscript: require('./admin/customscript'),

+ 31 - 14
lib/locales/en-US/translation.json

@@ -315,14 +315,14 @@
 		"server_on_passport_auth": "The server is running with Passport authentication mechanism.",
 		"server_on_crowi_auth": "The server is running with official Crowi authentication mechanism.",
 		"google_setting": "Google Setting",
-    "connect_api_manager": "You can use your Google account to sign up and login after creating OAuth2 ClientId at <a href=\"https://console.cloud.google.com/apis/credentials\">API Manager on Google Cloud Platform</a>",
-		"access_api_manager": "Access <a href=\"https://console.cloud.google.com/apis/credentials\">API Manager</a>",
+    "connect_api_manager": "You can use your Google account to sign up and login after creating OAuth2 ClientId at <a href=\"https://console.cloud.google.com/apis/credentials\" target=\"_blank\">API Manager on Google Cloud Platform</a>",
+		"access_api_manager": "Access <a href=\"%s\" target=\"_blank\">%s</a>",
 		"create_project": "Create Project if no projects have been created.",
 		"create_auth_to_oauth": "\"Create credentials\" -> \"OAuth clientID\"",
 		"select_webapp": "Select \"Web Application\"",
-    "change_redirect_url": "Enter <code>https://${crowi.host}/google/callback</code> <br>(where < code > $ {crowi.host} < /code> is your host name) for \"Authorized redirect URIs\".",
+    "change_redirect_url": "Enter <code>https://${crowi.host}/google/callback</code> <br>(where <code>${crowi.host}</code> is your host name) for \"Authorized redirect URIs\".",
     "clientID": "Client ID",
-    "client_secret": "クライアントシークレット",
+    "client_secret": "Client Secret",
     "guest_mode": {
       "deny": "Deny Unregistered Users",
       "readonly": "View Only"
@@ -332,8 +332,10 @@
       "restricted": "Reuqire Admin permission",
       "closed": "Invitation Only"
     },
-    "configuration": "Configuration",
+    "configuration": " Configuration",
     "optional": "Optional",
+    "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>%s</code> match",
+    "Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>%s</code>.",
     "ldap": {
       "server_url_detail": "The LDAP URL of the directory service in the format <code>ldap://host:port/DN</code> or <code>ldaps://host:port/DN</code>.",
       "bind_mode": "Binding Mode",
@@ -353,8 +355,6 @@
       "search_filter_example2": "Match with 'sAMAccountName' for Active Directory",
       "username_detail": "Specification of mappings for <code>username</code> when creating new users",
       "name_detail": "Specification of mappings for <code>name</code> when creating new users",
-      "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",
-  		"Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>.",
       "group_search_base_DN": "Group Search Base DN",
       "group_search_base_DN_detail": "The base DN from which to search for groups. If defined, also <code>Group Search Filter</code> must be defined for the search to work.",
       "group_search_filter": "Group Search Filter",
@@ -365,13 +365,30 @@
       "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": {
-    },
-    "Facebook": {
-    },
-    "Twitter": {
-    },
-    "Github": {
+    "OAuth": {
+      "register": "Register for %s",
+      "connect_api_manager": "Register your Growi at <a href=\"%s\" target=\"_blank\">%s</a>",
+      "change_redirect_url": "Enter <code>%s</code> <br>(where <code>%s</code> is your host name) for \"Authorized redirect URIs\".",
+      "Google": {
+        "name": "Google OAuth",
+        "register_1": "Access <a href=\"%s\" target=\"_blank\">%s</a>",
+        "register_2": "Create Project if no projects exist",
+        "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 below"
+      },
+      "Facebook": {
+        "name": "Facebook OAuth"
+      },
+      "Twitter": {
+        "name": "Twitter OAuth"
+      },
+      "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 below"
+      }
     }
 	},
 

+ 32 - 14
lib/locales/ja/translation.json

@@ -84,7 +84,7 @@
   "Table of Contents": "目次",
   "Management Wiki Home": "Wiki管理トップ",
   "App settings": "アプリ設定",
-  "Markdown settings": "Markdown設定",
+  "Markdown settings": "マークダウン設定",
   "Customize": "カスタマイズ",
   "Notification settings": "通知設定",
   "User management": "ユーザー管理",
@@ -332,8 +332,8 @@
     "server_on_passport_auth": "Passport 認証機構でサーバーが稼働しています。",
     "server_on_crowi_auth": "Crowi Classic 認証機構でサーバーが稼働しています。",
     "google_setting": "Google 設定",
-    "connect_api_manager": "Google Cloud Platform の <a href=\"https://console.cloud.google.com/apis/credentials\">API Manager</a>から OAuth2 Client ID を作成すると、Google アカウントにコネクトして登録やログインが可能になります。",
-    "access_api_manager": "<a href=\"https://console.cloud.google.com/apis/credentials\">API Manager</a> へアクセス",
+    "connect_api_manager": "Google Cloud Platform の <a href=\"https://console.cloud.google.com/apis/credentials\" target=\"_blank\">API Manager</a>から OAuth2 Client ID を作成すると、Google アカウントにコネクトして登録やログインが可能になります。",
+    "access_api_manager": "<a href=\"%s\" target=\"_blank\">%s</a> へアクセス",
     "create_project": "プロジェクトを作成していない場合は作成してください",
     "create_auth_to_oauth": "「認証情報を作成」-> OAuthクライアントID",
     "select_webapp": "「ウェブアプリケーション」を選択",
@@ -349,8 +349,10 @@
       "restricted": "制限 (登録完了には管理者の承認が必要)",
       "closed": "非公開 (登録には管理者による招待が必要)"
     },
-    "configuration": "コンフィギュレーション",
+    "configuration": "設定",
     "optional": "オプション",
+    "Treat username matching as identical": "新規ログイン時、<code>%s</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
+    "Treat username matching as identical_warn": "警告: <code>%s</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
     "ldap": {
       "server_url_detail": "LDAP URLを <code>ldap://host:port/DN</code> または <code>ldaps://host:port/DN</code> の形式で入力してください。",
       "bind_mode": "Bind モード",
@@ -370,8 +372,6 @@
       "search_filter_example2": "'sAMAccountName' に一致 (Active Directory)",
       "username_detail": "新規ユーザーのアカウント名(<code>username</code>)に関連付ける属性",
       "name_detail": "新規ユーザーの表示名(<code>name</code>)に関連付ける属性",
-      "Treat username matching as identical": "新規ログイン時、<code>username</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
-      "Treat username matching as identical_warn": "WARNING: <code>username</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
       "group_search_base_DN": "グループ検索ベース DN",
       "group_search_base_DN_detail": "グループ検索を実行するベース DN。利用する場合は <code>グループ検索フィルター</code> も入力する必要があります。",
       "group_search_filter": "グループ検索フィルター",
@@ -379,15 +379,33 @@
       "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": {
-    },
-    "Facebook": {
-    },
-    "Twitter": {
-    },
-    "Github": {
+    "OAuth": {
+      "register": "%sに登録",
+      "connect_api_manager": "あなたのGrowiを<a href=\"%s\" target=\"_blank\">%s</a>で登録してください。",
+      "change_redirect_url": "承認済みのリダイレクトURLに、 <code>%s</code> を入力<br>(<code>%s</code>は環境に合わせて変更してください)",
+      "Google": {
+        "name": "Google OAuth認証",
+        "register_1": "<a href=\"%s\" target=\"_blank\">%s</a>へアクセス",
+        "register_2": "プロジェクトがない場合はプロジェクトを作成",
+        "register_3": "認証情報を作成 &rightarrow; OAuthクライアントID &rightarrow; ウェブアプリケーションを選択",
+        "register_4": "承認済みのリダイレクトURIを<code>%s</code>としてGrowiを登録 (<code>%s</code>は環境に合わせて変更してください)",
+        "register_5": "以下にクライアントIDとクライアントシークレットを貼り付ける"
+      },
+      "Facebook": {
+        "name": "Facebook OAuth認証"
+      },
+      "Twitter": {
+        "name": "Twitter OAuth認証"
+      },
+      "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とクライアントシークレットを貼り付ける"
+      }
     }
   },
   "markdown_setting": {

+ 12 - 0
lib/models/config.js

@@ -64,6 +64,8 @@ module.exports = function(crowi) {
       'security:passport-ldap:groupSearchFilter' : undefined,
       'security:passport-ldap:groupDnProperty' : undefined,
       'security:passport-ldap:isSameUsernameTreatedAsIdenticalUser': false,
+      'security:passport-google:isEnabled' : false,
+      'security:passport-github:isEnabled' : false,
 
       'aws:bucket'          : 'growi',
       'aws:region'          : 'ap-northeast-1',
@@ -267,6 +269,16 @@ module.exports = function(crowi) {
     return getValueForCrowiNS(config, key);
   };
 
+  configSchema.statics.isEnabledPassportGoogle = function(config) {
+    const key = 'security:passport-google:isEnabled';
+    return getValueForCrowiNS(config, key);
+  };
+
+  configSchema.statics.isEnabledPassportGitHub = function(config) {
+    const key = 'security:passport-github:isEnabled';
+    return getValueForCrowiNS(config, key);
+  };
+
   configSchema.statics.isSameUsernameTreatedAsIdenticalUser = function(config, providerType) {
     const key = `security:passport-${providerType}:isSameUsernameTreatedAsIdenticalUser`;
     return getValueForCrowiNS(config, key);

+ 54 - 0
lib/routes/admin.js

@@ -914,6 +914,60 @@ module.exports = function(crowi, app) {
       });
   };
 
+  actions.api.securityPassportGoogleSetting = async(req, res) => {
+    var 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.resetGoogleStrategy();
+    // setup strategy
+    if (Config.isEnabledPassportGoogle(config)) {
+      await crowi.passportService.setupGoogleStrategy(true);
+    }
+
+    return res.json({status: true});
+  };
+
+  actions.api.securityPassportGitHubSetting = async(req, res) => {
+    var 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.resetGitHubStrategy();
+    // setup strategy
+    if (Config.isEnabledPassportGoogle(config)) {
+      await crowi.passportService.setupGitHubStrategy(true);
+    }
+
+    return res.json({status: true});
+  };
+
+  actions.api.customizeSetting = function(req, res) {
+    var form = req.form.settingForm;
+
+    if (req.form.isValid) {
+      debug('form content', form);
+      return saveSetting(req, res, form);
+    }
+    else {
+      return res.json({status: false, message: req.form.errors.join('\n')});
+    }
+  };
+
   actions.api.customizeSetting = function(req, res) {
     var form = req.form.settingForm;
 

+ 8 - 0
lib/routes/index.js

@@ -67,6 +67,14 @@ 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);
 
+  // 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.get('/passport/google'                      , loginPassport.loginPassportGoogle);
+  app.get('/passport/github'                      , loginPassport.loginPassportGitHub);
+  app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback);
+  app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback);
+
   // markdown admin
   app.get('/admin/markdown'                   , loginRequired(crowi, app) , middleware.adminRequired() , admin.markdown.index);
   app.post('/admin/markdown/lineBreaksSetting', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.markdown, admin.markdown.lineBreaksSetting);

+ 155 - 67
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,43 @@ 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);
+    const providerId = 'ldap';
+    const strategyName = 'ldapauth';
+    const ldapAccountInfo = await promisifiedPassportAuthentication(req, res, next, strategyName);
 
-      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);
-      }
+    // check groups for LDAP
+    if (!isValidLdapUserByGroupFilter(ldapAccountInfo)) {
+      return loginFailure(req, res, next);
+    }
 
-      /*
-       * 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);
+    /*
+      * 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
+    }
 
-      const attrMapUsername = passportService.getLdapAttrNameMappedToUsername();
-      const attrMapName = passportService.getLdapAttrNameMappedToName();
-      const usernameToBeRegistered = ldapAccountInfo[attrMapUsername];
-      const nameToBeRegistered = ldapAccountInfo[attrMapName];
+    const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
+    if (!externalAccount) {
+      return loginFailure(req, res, next);
+    }
 
-      // 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 user = await externalAccount.getPopulatedUser();
 
-    })(req, res, next);
+    // login
+    await req.logIn(user, err => {
+      if (err) { return next(err) };
+      return loginSuccess(req, res, user);
+    });
   };
 
   /**
@@ -239,10 +205,132 @@ module.exports = function(crowi, app) {
     })(req, res, next);
   };
 
+  const loginPassportGoogle = function(req, res) {
+    if (!passportService.isGoogleStrategySetup) {
+      debug('GoogleStrategy has not been set up');
+      return;
+    }
+
+    passport.authenticate('google', {
+      scope: ['profile', 'email'],
+    })(req, res);
+  };
+
+  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);
+    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 loginPassportGitHub = function(req, res) {
+    if (!passportService.isGitHubStrategySetup) {
+      debug('GitHubStrategy has not been set up');
+      return;
+    }
+
+    passport.authenticate('github')(req, res);
+  };
+
+  const loginPassportGitHubCallback = async(req, res, next) => {
+    const providerId = 'github';
+    const strategyName = 'github';
+    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) => {
+        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;
+        }
+      }
+    }
+  }
+
   return {
     loginFailure,
     loginWithLdap,
     testLdapCredentials,
     loginWithLocal,
+    loginPassportGoogle,
+    loginPassportGitHub,
+    loginPassportGoogleCallback,
+    loginPassportGitHubCallback,
   };
 };

+ 104 - 0
lib/service/passport.js

@@ -2,6 +2,8 @@ const debug = require('debug')('growi:service:PassportService');
 const passport = require('passport');
 const LocalStrategy = require('passport-local').Strategy;
 const LdapStrategy = require('passport-ldapauth');
+const GoogleStrategy = require('passport-google-auth').Strategy;
+const GitHubStrategy = require('passport-github').Strategy;
 
 /**
  * the service class of Passport
@@ -25,6 +27,11 @@ class PassportService {
      */
     this.isLdapStrategySetup = false;
 
+    /**
+     * the flag whether LdapStrategy is set up successfully
+     */
+    this.isGoogleStrategySetup = false;
+
     /**
      * the flag whether serializer/deserializer are set up successfully
      */
@@ -235,6 +242,103 @@ class PassportService {
     };
   }
 
+  /**
+   * Asynchronous configuration retrieval
+   *
+   * @memberof PassportService
+   */
+  setupGoogleStrategy() {
+    // check whether the strategy has already been set up
+    if (this.isGoogleStrategySetup) {
+      throw new Error('GoogleStrategy has already been set up');
+    }
+
+    const config = this.crowi.config;
+    const Config = this.crowi.model('Config');
+    //this
+    const isGoogleEnabled = Config.isEnabledPassportGoogle(config);
+
+    // when disabled
+    if (!isGoogleEnabled) {
+      return;
+    }
+
+    debug('GoogleStrategy: setting up..');
+    passport.use(new GoogleStrategy({
+      clientId: config.crowi['security:passport-google:clientId'],
+      clientSecret: config.crowi['security:passport-google:clientSecret'],
+      callbackURL: 'http://localhost:3000/passport/google/callback',  //change this
+      skipUserProfile: false,
+    }, function(accessToken, refreshToken, profile, done) {
+      if (profile) {
+        return done(null, profile);
+      }
+      else {
+        return done(null, false);
+      }
+    }));
+
+    this.isGoogleStrategySetup = true;
+    debug('GoogleStrategy: setup is done');
+  }
+
+  /**
+   * reset GoogleStrategy
+   *
+   * @memberof PassportService
+   */
+  resetGoogleStrategy() {
+    debug('GoogleStrategy: reset');
+    passport.unuse('google');
+    this.isGoogleStrategySetup = false;
+  }
+
+  setupGitHubStrategy() {
+    // check whether the strategy has already been set up
+    if (this.isGitHubStrategySetup) {
+      throw new Error('GitHubStrategy has already been set up');
+    }
+
+    const config = this.crowi.config;
+    const Config = this.crowi.model('Config');
+    //this
+    const isGitHubEnabled = Config.isEnabledPassportGitHub(config);
+
+    // when disabled
+    if (!isGitHubEnabled) {
+      return;
+    }
+
+    debug('GitHubStrategy: setting up..');
+    passport.use(new GitHubStrategy({
+      clientID: config.crowi['security:passport-github:clientId'],
+      clientSecret: config.crowi['security:passport-github:clientSecret'],
+      callbackURL: 'http://localhost:3000/passport/github/callback',  //change this
+      skipUserProfile: false,
+    }, function(accessToken, refreshToken, profile, done) {
+      if (profile) {
+        return done(null, profile);
+      }
+      else {
+        return done(null, false);
+      }
+    }));
+
+    this.isGitHubStrategySetup = true;
+    debug('GitHubStrategy: setup is done');
+  }
+
+  /**
+   * reset GoogleStrategy
+   *
+   * @memberof PassportService
+   */
+  resetGitHubStrategy() {
+    debug('GitHubStrategy: reset');
+    passport.unuse('github');
+    this.isGitHubStrategySetup = false;
+  }
+
   /**
    * setup serializer and deserializer
    *

+ 10 - 0
lib/util/swigFunctions.js

@@ -92,6 +92,16 @@ 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.passportGitHubLoginEnabled = function() {
+    var config = crowi.getConfig();
+    return locals.isEnabledPassport() && config.crowi['security:passport-github:isEnabled'];
+  };
+
   locals.searchConfigured = function() {
     if (crowi.getSearcher()) {
       return true;

+ 11 - 12
lib/views/admin/security.html

@@ -111,7 +111,7 @@
                   <input type="radio" id="radioPassportAuthMech" name="settingForm[security:isEnabledPassport]" value="true"
                       {% if true === settingForm['security:isEnabledPassport'] %}checked="checked"{% endif %}>
                   <label for="radioPassportAuthMech">
-                    <a href="http://passportjs.org/">
+                    <a href="http://passportjs.org/" target="_blank">
                       <img src="/images/admin/security/passport-logo.svg" class="passport-logo"> Passport
                     </a> {{ t("security_setting.auth_mechanism") }} <small class="text-success">({{ t("security_setting.recommended") }})</small>
                   </label>
@@ -120,10 +120,10 @@
               <ul>
                 <li>{{ t("security_setting.username_email_password") }}</li>
                 <li>{{ t("security_setting.ldap_auth") }}</li>
-                <li class="text-muted tbd">(TBD) <del>{{ t("security_setting.google_auth2") }}</del></li>
-                <li class="text-muted tbd">(TBD) <del>{{ t("security_setting.facebook_auth2") }}</del></li>
-                <li class="text-muted tbd">(TBD) <del>{{ t("security_setting.twitter_auth2") }}</del></li>
-                <li class="text-muted tbd">(TBD) <del>{{ t("security_setting.github_auth2") }}</del></li>
+                <li>{{ t("security_setting.google_auth2") }}</li>
+                <li class="text-muted">(TBD) <del>{{ t("security_setting.facebook_auth2") }}</del></li>
+                <li class="text-muted">(TBD) <del>{{ t("security_setting.twitter_auth2") }}</del></li>
+                <li>{{ t("security_setting.github_auth2") }}</li>
               </ul>
             </div>
             <div class="col-xs-6">
@@ -179,7 +179,7 @@
               </p>
 
               <ol class="help-block">
-                <li>{{ t("security_setting.access_api_manager") }}</li>
+                <li>{{ t("security_setting.access_api_manager", "https://console.cloud.google.com/apis/credentials", "API Manager") }}</li>
                 <li>{{ t("security_setting.create_project") }}</li>
                 <li>{{ t("security_setting.create_auth_to_oauth") }}</li>
                 <ol>
@@ -232,8 +232,8 @@
             <li class="active">
               <a href="#passport-ldap" data-toggle="tab" role="tab"><i class="fa fa-sitemap"></i> LDAP</a>
             </li>
-            <li class="tbd">
-              <a href="#passport-google-oauth" data-toggle="tab" role="tab"><i class="fa fa-google"></i> (TBD) Google OAuth</a>
+            <li>
+              <a href="#passport-google-oauth" data-toggle="tab" role="tab"><i class="icon-social-google"></i> Google</a>
             </li>
             <li class="tbd">
               <a href="#passport-facebook" data-toggle="tab" role="tab"><i class="fa fa-facebook"></i> (TBD) Facebook</a>
@@ -241,9 +241,8 @@
             <li class="tbd">
               <a href="#passport-twitter" data-toggle="tab" role="tab"><i class="fa fa-twitter"></i> (TBD) Twitter</a>
             </li>
-            <li class="tbd">
-              <a href="#passport-github" data-toggle="tab" role="tab"><i class="fa fa-github"></i> (TBD) Github</a>
-            </li>
+            <li>
+              <a href="#passport-github" data-toggle="tab" role="tab"><i class="icon-social-github"></i> GitHub</a>
           </ul>
 
           <div class="tab-content p-t-10" {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
@@ -276,7 +275,7 @@
   </div>
 
   <script>
-    $('#generalSetting, #googleSetting, #mechanismSetting').each(function() {
+    $('#generalSetting, #googleSetting, #mechanismSetting, #githubSetting').each(function() {
       $(this).submit(function()
       {
         function showMessage(formId, msg, status) {

+ 86 - 4
lib/views/admin/widget/passport/github.html

@@ -1,6 +1,88 @@
-<form action="" method="post" class="form-horizontal passportStrategy" id="githubOauthSetting" role="form">
-  <fieldset>
-    <legend>Github OAuth {{ t("security_setting.configuration") }}</legend>
-    <p class="well">(TBD)</p>
+<form action="/_api/admin/security/passport-github" method="post" class="form-horizontal passportStrategy" id="githubSetting" role="form"
+    {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
+  <legend>{{ t("security_setting.OAuth.GitHub.name") }}{{ t("security_setting.configuration") }}</legend>
+  <p class="well alert-anchor">{{ t("security_setting.OAuth.connect_api_manager", "https://github.com/settings/developers", "GitHub Developer Settings") }}</p>
+  {% set nameForIsGitHubEnabled = "settingForm[security:passport-github:isEnabled]" %}
+  {% set isGitHubEnabled = settingForm['security:passport-github:isEnabled'] %}
+  <div class="form-group">
+    <label for="{{nameForIsGitHubEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.OAuth.GitHub.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 isGitHubEnabled %}active{% endif %}" data-active-class="primary">
+          <input name="{{nameForIsGitHubEnabled}}" value="true" type="radio"
+              {% if true === isGitHubEnabled %}checked{% endif %}> ON
+        </label>
+        <label class="btn btn-default btn-rounded btn-outline {% if !isGitHubEnabled %}active{% endif %}" data-active-class="default">
+          <input name="{{nameForIsGitHubEnabled}}" value="false" type="radio"
+              {% if !isGitHubEnabled %}checked{% endif %}> OFF
+        </label>
+      </div>
+    </div>
+  </div>
+  <fieldset id="passport-github-hide-when-disabled" {%if !isGitHubEnabled %}style="display: none;"{% endif %}>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-github:clientId]" class="col-xs-3 control-label">{{ t("security_setting.OAuth.register", t("security_setting.OAuth.GitHub.name") ) }}</label>
+      <div class="col-xs-6">
+        <ol class="help-block">
+          <li>{{ t("security_setting.OAuth.GitHub.register_1", "https://github.com/settings/developers", "GitHub Developer Settings") }}</li>
+          <li>{{ t("security_setting.OAuth.GitHub.register_2", "https://${growi.host}/passport/github/callback", "${growi.host}") }}</li>
+          <li>{{ t("security_setting.OAuth.GitHub.register_3") }}</li>
+        </ol>
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-github:clientId]" 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-github:clientId]" value="{{ settingForm['security:passport-github:clientId'] || '' }}">
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-github:clientSecret]" 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-github:clientSecret]" value="{{ settingForm['security:passport-github: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-GitHub" name="settingForm[security:passport-github:isSameUsernameTreatedAsIdenticalUser]" value="1"
+              {% if settingForm['security:passport-github:isSameUsernameTreatedAsIdenticalUser'] %}checked{% endif %} />
+          <label for="bindByUserName-GitHub">
+            {{ 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>
+
+<script>
+  $('input[name="settingForm[security:passport-github:isEnabled]"]').change(function() {
+      const isEnabled = ($(this).val() === "true");
+
+      if (isEnabled) {
+        $('#passport-github-hide-when-disabled').show(400);
+      }
+      else {
+        $('#passport-github-hide-when-disabled').hide(400);
+      }
+    });
+</script>
+

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

@@ -1,10 +1,79 @@
-<form action="" method="post" class="form-horizontal passportStrategy" id="googleOauthSetting" role="form">
-  <fieldset>
-    <legend>Google OAuth {{ t("security_setting.configuration") }}</legend>
-    <p class="well">(TBD)</p>
+<form action="/_api/admin/security/passport-google" method="post" class="form-horizontal passportStrategy" id="googleSetting" role="form"
+    {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
+  <legend>{{ t("security_setting.OAuth.Google.name") }}{{ t("security_setting.configuration") }}</legend>
+  <p class="well alert-anchor">{{ t("security_setting.OAuth.connect_api_manager", "https://console.cloud.google.com/apis/credentials", "Google Cloud Platform API Manager") }}</p>
+  {% 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">{{ t("security_setting.OAuth.Google.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 isGoogleEnabled %}active{% endif %}" data-active-class="primary">
+          <input name="{{nameForIsGoogleEnabled}}" value="true" type="radio"
+              {% if true === isGoogleEnabled %}checked{% endif %}> ON
+        </label>
+        <label class="btn btn-default btn-rounded btn-outline {% if !isGoogleEnabled %}active{% endif %}" data-active-class="default">
+          <input name="{{nameForIsGoogleEnabled}}" value="false" type="radio"
+              {% if !isGoogleEnabled %}checked{% endif %}> OFF
+        </label>
+      </div>
+    </div>
+  </div>
+  <fieldset id="passport-google-hide-when-disabled" {%if !isGoogleEnabled %}style="display: none;"{% endif %}>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-google:clientId]" class="col-xs-3 control-label">{{ t("security_setting.OAuth.register", t("security_setting.OAuth.Google.name") ) }}</label>
+      <div class="col-xs-6">
+        <ol class="help-block">
+          <li>{{ t("security_setting.OAuth.Google.register_1", "https://console.cloud.google.com/apis/credentials", "Google Cloud Platform API Manager") }}</li>
+          <li>{{ t("security_setting.OAuth.Google.register_2") }}</li>
+          <li>{{ t("security_setting.OAuth.Google.register_3") }}</li>
+          <li>{{ t("security_setting.OAuth.Google.register_4", "https://${growi.host}/passport/google/callback", "${growi.host}") }}</li>
+          <li>{{ t("security_setting.OAuth.Google.register_5") }}</li>
+        </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>
+      <div class="col-xs-6">
+        <input class="form-control" type="text" name="settingForm[security:passport-google:clientId]" value="{{ settingForm['security:passport-google:clientId'] || '' }}">
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-google:clientSecret]" 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-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.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>
-</form>
 
+  <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>
 {% if false %}
 <hr>
 <h4>
@@ -80,3 +149,16 @@
   </li>
 </ol>
 {% endif %}
+
+<script>
+  $('input[name="settingForm[security:passport-google:isEnabled]"]').change(function() {
+      const isEnabled = ($(this).val() === "true");
+
+      if (isEnabled) {
+        $('#passport-google-hide-when-disabled').show(400);
+      }
+      else {
+        $('#passport-google-hide-when-disabled').hide(400);
+      }
+    });
+</script>

+ 2 - 2
lib/views/admin/widget/passport/ldap.html

@@ -139,11 +139,11 @@
             <input type="checkbox" id="cbSameUsernameTreatedAsIdenticalUser" name="settingForm[security:passport-ldap:isSameUsernameTreatedAsIdenticalUser]" value="1"
                 {% if settingForm['security:passport-ldap:isSameUsernameTreatedAsIdenticalUser'] %}checked{% endif %} />
             <label for="cbSameUsernameTreatedAsIdenticalUser">
-              {{ t("security_setting.ldap.Treat username matching as identical") }}
+              {{ t("security_setting.Treat username matching as identical", "username") }}
             </label>
             <p class="help-block">
               <small>
-                {{ t("security_setting.ldap.Treat username matching as identical_warn") }}
+                {{ t("security_setting.Treat username matching as identical_warn", "username") }}
               </small>
             </p>
           </div>

+ 49 - 4
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>
@@ -136,7 +136,7 @@
         <div class="input-group m-t-15 m-b-10 mx-auto">
           <form role="form" action="/login/google" method="get">
             <input type="hidden" name="_csrf" value="{{ csrf() }}">
-            <button type="submit" class="fcbtn btn btn-danger btn-1b btn-login-google">
+            <button type="submit" class="fcbtn btn btn-danger btn-1b btn-login-oauth" id="google">
               <span class="btn-label"><i class="icon-social-google"></i></span>
               {{ t('Sign in') }}
             </button>
@@ -145,6 +145,51 @@
         </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() %}
+          <form role="form" action="/passport/google" method="get">
+            <button type="submit" class="fcbtn btn btn-1b btn-login-oauth" id="google">
+              <span class="btn-label"><i class="icon-social-google"></i></span>
+              {{ t('Sign in') }}
+            </button>
+            <div class="small text-right">by Google Account</div>
+          </form>
+          {% endif %}
+          {% if passportGitHubLoginEnabled() %}
+          <form role="form" action="/passport/github" method="get">
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
+            <button type="submit" class="fcbtn btn btn-1b btn-login-oauth" id="github">
+              <span class="btn-label"><i class="icon-social-github"></i></span>
+              {{ t('Sign in') }}
+            </button>
+            <div class="small text-right">by Github Account</div>
+          </form>
+          {% endif %}
+          {% if passportFacebookLoginEnabled() %}
+          <form role="form" action="/passport/facebook" method="get">
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
+            <button type="submit" class="fcbtn btn btn-1b btn-login-oauth" id="facebook">
+              <span class="btn-label"><i class="icon-social-facebook"></i></span>
+              {{ t('Sign in') }}
+            </button>
+            <div class="small text-right">by Facebook Account</div>
+          </form>
+          {% endif %}
+          {% if passportTwitterLoginEnabled() %}
+          <form role="form" action="/passport/twitter" method="get">
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
+            <button type="submit" class="fcbtn btn btn-1b btn-login-oauth" id="twitter">
+              <span class="btn-label"><i class="icon-social-twitter"></i></span>
+              {{ t('Sign in') }}
+            </button>
+            <div class="small text-right">by Twitter Account</div>
+          </form>
+          {% endif %}
+        </div>
+        {% endif %}
+
         <hr>
 
         <div class="row">

+ 2 - 0
package.json

@@ -98,6 +98,8 @@
     "nodemailer-ses-transport": "~1.5.0",
     "npm-run-all": "^4.1.2",
     "passport": "^0.4.0",
+    "passport-github": "^1.1.0",
+    "passport-google-auth": "^1.0.2",
     "passport-ldapauth": "^2.0.0",
     "passport-local": "^1.0.0",
     "rimraf": "^2.6.1",

+ 27 - 3
resource/styles/scss/_login.scss

@@ -99,7 +99,7 @@
   }
 
   // button style
-  .btn-login.fcbtn, .btn-register.fcbtn, .btn-login-google.fcbtn {
+  .btn-login.fcbtn, .btn-register.fcbtn, .btn-login-oauth.fcbtn {
     border: none;
     color: white;
     background-color: rgba(lighten(black, 20%), 0.4);
@@ -120,9 +120,33 @@
       background-color: #7e4153;
     }
   }
-  .btn-login-google.fcbtn {
+  .btn-login-oauth.fcbtn#google {
     .btn-label {
-      background-color: rgba(#444, 0.4);
+      background: linear-gradient(to bottom right, darken(#db3236, 20%), darken(#f4c20d, 20%), darken(#3cba54, 20%), darken(#4885ed, 20%));
+    }
+    &:after {
+      background-color: #555;
+    }
+  }
+  .btn-login-oauth.fcbtn#github {
+    .btn-label {
+      background-color: rgba(#24292e, 0.4);
+    }
+    &:after {
+      background-color: #555;
+    }
+  }
+  .btn-login-oauth.fcbtn#facebook {
+    .btn-label {
+      background-color: rgba(#29487d, 0.4);
+    }
+    &:after {
+      background-color: #555;
+    }
+  }
+  .btn-login-oauth.fcbtn#twitter {
+    .btn-label {
+      background-color: rgba(#1da1f2, 0.4);
     }
     &:after {
       background-color: #555;

+ 128 - 2
yarn.lock

@@ -556,6 +556,12 @@ async@~0.2.6:
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
 
+async@~2.1.4:
+  version "2.1.5"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc"
+  dependencies:
+    lodash "^4.14.0"
+
 asynckit@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -2646,6 +2652,12 @@ ecc-jsbn@~0.1.1:
   dependencies:
     jsbn "~0.1.0"
 
+ecdsa-sig-formatter@1.0.10:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3"
+  dependencies:
+    safe-buffer "^5.0.1"
+
 ecdsa-sig-formatter@1.0.9:
   version "1.0.9"
   resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1"
@@ -3599,6 +3611,21 @@ google-auth-library@^1.4.0:
     lru-cache "^4.1.2"
     retry-axios "^0.3.2"
 
+google-auth-library@~0.10.0:
+  version "0.10.0"
+  resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.10.0.tgz#6e15babee85fd1dd14d8d128a295b6838d52136e"
+  dependencies:
+    gtoken "^1.2.1"
+    jws "^3.1.4"
+    lodash.noop "^3.0.1"
+    request "^2.74.0"
+
+google-p12-pem@^0.1.0:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-0.1.2.tgz#33c46ab021aa734fa0332b3960a9a3ffcb2f3177"
+  dependencies:
+    node-forge "^0.7.1"
+
 google-p12-pem@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-1.0.0.tgz#375cc4e977a311908d365b47ed3519e7207c1f77"
@@ -3606,6 +3633,14 @@ google-p12-pem@^1.0.0:
     node-forge "^0.7.1"
     pify "^3.0.0"
 
+googleapis@^16.0.0:
+  version "16.1.0"
+  resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-16.1.0.tgz#0f19f2d70572d918881a0f626e3b1a2fa8629576"
+  dependencies:
+    async "~2.1.4"
+    google-auth-library "~0.10.0"
+    string-template "~1.0.0"
+
 googleapis@^32.0.0:
   version "32.0.0"
   resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-32.0.0.tgz#04795d1956568546bb3e3cca3cae29a759326493"
@@ -3632,6 +3667,15 @@ growly@^1.2.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
 
+gtoken@^1.2.1:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-1.2.3.tgz#5509571b8afd4322e124cf66cf68115284c476d8"
+  dependencies:
+    google-p12-pem "^0.1.0"
+    jws "^3.0.0"
+    mime "^1.4.1"
+    request "^2.72.0"
+
 gtoken@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-2.3.0.tgz#4e0ffc16432d7041a1b3dbc1d97aac17a5dc964a"
@@ -4511,6 +4555,21 @@ jwa@^1.1.4:
     ecdsa-sig-formatter "1.0.9"
     safe-buffer "^5.0.1"
 
+jwa@^1.1.5:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6"
+  dependencies:
+    buffer-equal-constant-time "1.0.1"
+    ecdsa-sig-formatter "1.0.10"
+    safe-buffer "^5.0.1"
+
+jws@^3.0.0:
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f"
+  dependencies:
+    jwa "^1.1.5"
+    safe-buffer "^5.0.1"
+
 jws@^3.1.4:
   version "3.1.4"
   resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2"
@@ -4787,6 +4846,10 @@ lodash.mergewith@^4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
 
+lodash.noop@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c"
+
 lodash.set@^4.3.2:
   version "4.3.2"
   resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
@@ -5120,6 +5183,10 @@ mime@1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
 
+mime@^1.4.1:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+
 mime@^2.2.0:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
@@ -5705,6 +5772,10 @@ oauth-sign@~0.8.1, oauth-sign@~0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
 
+oauth@0.9.x:
+  version "0.9.15"
+  resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
+
 object-additions@^0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/object-additions/-/object-additions-0.5.1.tgz#ac624e0995e696c94cc69b41f316462b16a3bda4"
@@ -5960,6 +6031,19 @@ parseurl@~1.3.1, parseurl@~1.3.2:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
 
+passport-github@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/passport-github/-/passport-github-1.1.0.tgz#8ce1e3fcd61ad7578eb1df595839e4aea12355d4"
+  dependencies:
+    passport-oauth2 "1.x.x"
+
+passport-google-auth@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/passport-google-auth/-/passport-google-auth-1.0.2.tgz#8b300b5aa442ef433de1d832ed3112877d0b2938"
+  dependencies:
+    googleapis "^16.0.0"
+    passport-strategy "1.x"
+
 pascalcase@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
@@ -5979,7 +6063,16 @@ passport-local@^1.0.0:
   dependencies:
     passport-strategy "1.x.x"
 
-passport-strategy@1.x.x, passport-strategy@^1.0.0:
+passport-oauth2@1.x.x:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.4.0.tgz#f62f81583cbe12609be7ce6f160b9395a27b86ad"
+  dependencies:
+    oauth "0.9.x"
+    passport-strategy "1.x.x"
+    uid2 "0.0.x"
+    utils-merge "1.x.x"
+
+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"
 
@@ -7001,6 +7094,31 @@ request@2.81.0:
     tunnel-agent "^0.6.0"
     uuid "^3.0.0"
 
+request@^2.72.0:
+  version "2.87.0"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
+  dependencies:
+    aws-sign2 "~0.7.0"
+    aws4 "^1.6.0"
+    caseless "~0.12.0"
+    combined-stream "~1.0.5"
+    extend "~3.0.1"
+    forever-agent "~0.6.1"
+    form-data "~2.3.1"
+    har-validator "~5.0.3"
+    http-signature "~1.2.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.17"
+    oauth-sign "~0.8.2"
+    performance-now "^2.1.0"
+    qs "~6.5.1"
+    safe-buffer "^5.1.1"
+    tough-cookie "~2.3.3"
+    tunnel-agent "^0.6.0"
+    uuid "^3.1.0"
+
 request@^2.74.0:
   version "2.85.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa"
@@ -7758,6 +7876,10 @@ strict-uri-encode@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
 
+string-template@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
+
 string-width@^1.0.1, string-width@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@@ -8185,6 +8307,10 @@ uid-safe@~2.1.5:
   dependencies:
     random-bytes "~1.0.0"
 
+uid2@0.0.x:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82"
+
 ultron@~1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
@@ -8320,7 +8446,7 @@ utils-merge@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8"
 
-utils-merge@1.0.1:
+utils-merge@1.0.1, utils-merge@1.x.x:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"