diff --git a/.env.example b/.env.example index fe924ba4d..e1d5a57a7 100644 --- a/.env.example +++ b/.env.example @@ -218,6 +218,14 @@ OPENID_IMAGE_URL= # Delay is in millisecond e.g. 7 days is 1000*60*60*24*7 SESSION_EXPIRY=(1000 * 60 * 60 * 24) * 7 +# Github: +# Get the Client ID and Secret from your Github Application +# Add your Github Client ID and Client Secret here: + +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GITHUB_CALLBACK_URL=/oauth/github/callback + ########################### # Application Domains ########################### diff --git a/api/models/User.js b/api/models/User.js index b7f42023e..011291c34 100644 --- a/api/models/User.js +++ b/api/models/User.js @@ -70,6 +70,11 @@ const userSchema = mongoose.Schema( unique: true, sparse: true }, + githubId: { + type: String, + unique: true, + sparse: true + }, plugins: { type: Array, default: [] diff --git a/api/server/index.js b/api/server/index.js index ec203703e..947edfdb8 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -42,6 +42,9 @@ config.validate(); // Validate the config if (process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET) { require('../strategies/facebookStrategy'); } + if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) { + require('../strategies/githubStrategy'); + } if (process.env.OPENID_CLIENT_ID && process.env.OPENID_CLIENT_SECRET && process.env.OPENID_ISSUER && process.env.OPENID_SCOPE && process.env.OPENID_SESSION_SECRET) { diff --git a/api/server/routes/__tests__/config.spec.js b/api/server/routes/__tests__/config.spec.js index 4bc751d27..928fc2208 100644 --- a/api/server/routes/__tests__/config.spec.js +++ b/api/server/routes/__tests__/config.spec.js @@ -14,6 +14,8 @@ afterEach(() => { delete process.env.OPENID_SESSION_SECRET; delete process.env.OPENID_BUTTON_LABEL; delete process.env.OPENID_AUTH_URL; + delete process.env.GITHUB_CLIENT_ID; + delete process.env.GITHUB_CLIENT_SECRET; delete process.env.DOMAIN_SERVER; delete process.env.ALLOW_REGISTRATION; }); @@ -32,6 +34,8 @@ describe.skip('GET /', () => { process.env.OPENID_SESSION_SECRET= 'Test Secret'; process.env.OPENID_BUTTON_LABEL= 'Test OpenID'; process.env.OPENID_AUTH_URL= 'http://test-server.com'; + process.env.GITHUB_CLIENT_ID = 'Test Github client Id'; + process.env.GITHUB_CLIENT_SECRET= 'Test Github client Secret'; process.env.DOMAIN_SERVER = 'http://test-server.com'; process.env.ALLOW_REGISTRATION = 'true'; @@ -44,6 +48,7 @@ describe.skip('GET /', () => { openidLoginEnabled: true, openidLabel: 'Test OpenID', openidImageUrl: 'http://test-server.com', + githubLoginEnabled: true, serverDomain: 'http://test-server.com', registrationEnabled: 'true', }); diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 99e3ce613..115fffa35 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -11,10 +11,11 @@ router.get('/', async function (req, res) { && !!process.env.OPENID_SESSION_SECRET; const openidLabel = process.env.OPENID_BUTTON_LABEL || 'Login with OpenID'; const openidImageUrl = process.env.OPENID_IMAGE_URL; + const githubLoginEnabled = !!process.env.GITHUB_CLIENT_ID && !!process.env.GITHUB_CLIENT_SECRET; const serverDomain = process.env.DOMAIN_SERVER || 'http://localhost:3080'; const registrationEnabled = process.env.ALLOW_REGISTRATION === 'true'; - return res.status(200).send({appTitle, googleLoginEnabled, openidLoginEnabled, openidLabel, openidImageUrl, serverDomain, registrationEnabled}); + return res.status(200).send({appTitle, googleLoginEnabled, openidLoginEnabled, openidLabel, openidImageUrl, githubLoginEnabled, serverDomain, registrationEnabled}); } catch (err) { console.error(err); return res.status(500).send({error: err.message}); diff --git a/api/server/routes/oauth.js b/api/server/routes/oauth.js index 0a1893457..8289232e2 100644 --- a/api/server/routes/oauth.js +++ b/api/server/routes/oauth.js @@ -87,4 +87,32 @@ router.get( } ); + +router.get( + '/github', + passport.authenticate('github', { + scope: ['user:email', 'read:user'], + session: false + }) +); + +router.get( + '/github/callback', + passport.authenticate('github', { + failureRedirect: `${domains.client}/login`, + failureMessage: true, + session: false, + scope: ['user:email', 'read:user'] + }), + (req, res) => { + const token = req.user.generateToken(); + res.cookie('token', token, { + expires: new Date(Date.now() + eval(process.env.SESSION_EXPIRY)), + httpOnly: false, + secure: isProduction + }); + res.redirect(domains.client); + } +); + module.exports = router; diff --git a/api/strategies/githubStrategy.js b/api/strategies/githubStrategy.js new file mode 100644 index 000000000..d377b6183 --- /dev/null +++ b/api/strategies/githubStrategy.js @@ -0,0 +1,47 @@ +const passport = require('passport'); +const { Strategy: GitHubStrategy } = require('passport-github2'); +const config = require('../../config/loader'); +const domains = config.domains; + +const User = require('../models/User'); + +// GitHub strategy +const githubLogin = new GitHubStrategy( + { + clientID: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackURL: `${domains.server}${process.env.GITHUB_CALLBACK_URL}`, + proxy: false, + scope: ['user:email'] // Request email scope + }, + async (accessToken, refreshToken, profile, cb) => { + try { + let email; + if (profile.emails && profile.emails.length > 0) { + email = profile.emails[0].value; + } + + const oldUser = await User.findOne({ email }); + if (oldUser) { + return cb(null, oldUser); + } + + const newUser = await new User({ + provider: 'github', + githubId: profile.id, + username: profile.username, + email, + emailVerified: profile.emails[0].verified, + name: profile.displayName, + avatar: profile.photos[0].value + }).save(); + + cb(null, newUser); + } catch (err) { + console.error(err); + cb(err); + } + } +); + +passport.use(githubLogin); diff --git a/client/src/components/Auth/Login.tsx b/client/src/components/Auth/Login.tsx index 2675f12dd..5d2b2625d 100644 --- a/client/src/components/Auth/Login.tsx +++ b/client/src/components/Auth/Login.tsx @@ -102,9 +102,38 @@ function Login() { > )} + {startupConfig?.githubLoginEnabled && ( + <> +