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

Merge pull request #495 from weseek/feat/OAuth-github

Feat/OAuth GitHub
Sou Mizobuchi 7 лет назад
Родитель
Сommit
c0c3dd7fa2

+ 1 - 0
lib/crowi/index.js

@@ -264,6 +264,7 @@ Crowi.prototype.setupPassport = function() {
   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()
+);

+ 1 - 0
lib/form/index.js

@@ -20,6 +20,7 @@ module.exports = {
     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'),

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

@@ -315,8 +315,8 @@
 		"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\"",
@@ -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,15 +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": {
-      "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": {
-    },
-    "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"
+      }
     }
 	},
 

+ 30 - 15
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": "グループ検索フィルター",
@@ -382,15 +382,30 @@
       "group_search_user_DN_property_detail": "<code>グループ検索フィルター</code> 内の <code>&#123;&#123;dn&#125;&#125;</code> で置換される、ユーザーオブジェクトのプロパティー",
       "test_config": "ログインテスト"
     },
-    "Google_OAuth": {
-      "use_Google_OAuth": "Google OAuth認証",
-      "change_redirect_url": "承認済みのリダイレクトURLに、 <code>%s</code> を入力<br>(<code>%s</code>は環境に合わせて変更してください)"
-    },
-    "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": {

+ 6 - 0
lib/models/config.js

@@ -65,6 +65,7 @@ module.exports = function(crowi) {
       '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',
@@ -273,6 +274,11 @@ module.exports = function(crowi) {
     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);

+ 22 - 1
lib/routes/admin.js

@@ -903,7 +903,7 @@ module.exports = function(crowi, app) {
       });
   };
 
-  actions.api.securityPassportGoogleSetting = async function(req, res) {
+  actions.api.securityPassportGoogleSetting = async(req, res) => {
     var form = req.form.settingForm;
 
     if (!req.form.isValid) {
@@ -924,6 +924,27 @@ module.exports = function(crowi, app) {
     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;
 

+ 3 - 0
lib/routes/index.js

@@ -69,8 +69,11 @@ 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.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);

+ 45 - 3
lib/routes/login-passport.js

@@ -104,6 +104,10 @@ module.exports = function(crowi, app) {
     }
 
     const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
+    if (!externalAccount) {
+      return loginFailure(req, res, next);
+    }
+
     const user = await externalAccount.getPopulatedUser();
 
     // login
@@ -222,10 +226,47 @@ module.exports = function(crowi, app) {
       '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
-    await req.logIn(user, err => {
+    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);
     });
@@ -276,10 +317,9 @@ module.exports = function(crowi, app) {
         }
         else {
           req.flash('provider-DuplicatedUsernameException', providerId);
-          return loginFailure(req, res, next);
+          return;
         }
       }
-      throw err;  // throw again
     }
   }
 
@@ -289,6 +329,8 @@ module.exports = function(crowi, app) {
     testLdapCredentials,
     loginWithLocal,
     loginPassportGoogle,
+    loginPassportGitHub,
     loginPassportGoogleCallback,
+    loginPassportGitHubCallback,
   };
 };

+ 47 - 0
lib/service/passport.js

@@ -3,6 +3,7 @@ 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
@@ -292,6 +293,52 @@ class PassportService {
     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
    *

+ 5 - 0
lib/util/swigFunctions.js

@@ -97,6 +97,11 @@ module.exports = function(crowi, app, req, locals) {
     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;

+ 7 - 7
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) <del>{{ t("security_setting.google_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 class="text-muted">(TBD) <del>{{ t("security_setting.github_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>
@@ -233,7 +233,7 @@
               <a href="#passport-ldap" data-toggle="tab" role="tab"><i class="icon-organization"></i> LDAP</a>
             </li>
             <li>
-              <a href="#passport-google-oauth" data-toggle="tab" role="tab"><i class="icon-social-google"></i> Google OAuth</a>
+              <a href="#passport-google-oauth" data-toggle="tab" role="tab"><i class="icon-social-google"></i> Google</a>
             </li>
             <li>
               <a href="#passport-facebook" data-toggle="tab" role="tab"><i class="icon-social-facebook"></i> Facebook</a>
@@ -242,7 +242,7 @@
               <a href="#passport-twitter" data-toggle="tab" role="tab"><i class="icon-social-twitter"></i> Twitter</a>
             </li>
             <li>
-              <a href="#passport-github" data-toggle="tab" role="tab"><i class="icon-social-github"></i> Github</a>
+              <a href="#passport-github" data-toggle="tab" role="tab"><i class="icon-social-github"></i> GitHub</a>
             </li>
           </ul>
 
@@ -276,7 +276,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>
+

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

@@ -1,11 +1,11 @@
 <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>Google OAuth {{ t("security_setting.configuration") }}</legend>
-  <p class="well alert-anchor">{{ t("security_setting.connect_api_manager") }}</p>
+  <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.Google_OAuth.use_Google_OAuth") }}</label>
+    <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">
@@ -22,16 +22,14 @@
   <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.google_setting") }}</label>
+      <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.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>
+          <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>
@@ -55,11 +53,11 @@
           <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") }}
+            {{ 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>

+ 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>

+ 10 - 10
lib/views/login.html

@@ -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,22 +145,22 @@
         </div>
         {% endif %}
 
-        {% if passportGoogleLoginEnabled() || passportGithubLoginEnabled() || passportFacebookLoginEnabled() || passportTwitterLoginEnabled() %}
+        {% 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-danger btn-1b btn-login-google">
+            <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="/auth/passport/github" method="get">
+          {% if passportGitHubLoginEnabled() %}
+          <form role="form" action="/passport/github" method="get">
             <input type="hidden" name="_csrf" value="{{ csrf() }}">
-            <button type="submit" class="fcbtn btn btn-danger btn-1b btn-login-github">
+            <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>
@@ -168,9 +168,9 @@
           </form>
           {% endif %}
           {% if passportFacebookLoginEnabled() %}
-          <form role="form" action="/auth/passport/facebook" method="get">
+          <form role="form" action="/passport/facebook" method="get">
             <input type="hidden" name="_csrf" value="{{ csrf() }}">
-            <button type="submit" class="fcbtn btn btn-danger btn-1b btn-login-facebook">
+            <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>
@@ -178,9 +178,9 @@
           </form>
           {% endif %}
           {% if passportTwitterLoginEnabled() %}
-          <form role="form" action="/auth/passport/twitter" method="get">
+          <form role="form" action="/passport/twitter" method="get">
             <input type="hidden" name="_csrf" value="{{ csrf() }}">
-            <button type="submit" class="fcbtn btn btn-danger btn-1b btn-login-twitter">
+            <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>

+ 1 - 0
package.json

@@ -95,6 +95,7 @@
     "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",

+ 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;

+ 24 - 1
yarn.lock

@@ -5114,6 +5114,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"
@@ -5351,6 +5355,12 @@ 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"
@@ -5373,6 +5383,15 @@ passport-local@^1.0.0:
   dependencies:
     passport-strategy "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"
+  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"
@@ -7378,6 +7397,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"
@@ -7471,7 +7494,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"