diff --git a/backend/config/custom-environment-variables.json b/backend/config/custom-environment-variables.json index 509f4f312b..7fe50e8789 100644 --- a/backend/config/custom-environment-variables.json +++ b/backend/config/custom-environment-variables.json @@ -129,6 +129,7 @@ "appId": "CROWD_GITHUB_APP_ID", "clientId": "CROWD_GITHUB_CLIENT_ID", "clientSecret": "CROWD_GITHUB_CLIENT_SECRET", + "callbackUrl": "CROWD_GITHUB_CALLBACK_URL", "privateKey": "CROWD_GITHUB_PRIVATE_KEY", "webhookSecret": "CROWD_GITHUB_WEBHOOK_SECRET", "isCommitDataEnabled": "CROWD_GITHUB_IS_COMMIT_DATA_ENABLED" diff --git a/backend/package-lock.json b/backend/package-lock.json index dce59e879c..6f1aeae992 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -74,6 +74,7 @@ "openapi-comment-parser": "^1.0.0", "passport": "0.6.0", "passport-facebook": "3.0.0", + "passport-github2": "^0.1.12", "passport-google-oauth": "2.0.0", "passport-google-oauth20": "^2.0.0", "passport-slack": "0.0.7", @@ -28864,6 +28865,17 @@ "node": ">= 0.4.0" } }, + "node_modules/passport-github2": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz", + "integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/passport-google-oauth": { "version": "2.0.0", "license": "MIT", @@ -63970,6 +63982,14 @@ "passport-oauth2": "1.x.x" } }, + "passport-github2": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz", + "integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==", + "requires": { + "passport-oauth2": "1.x.x" + } + }, "passport-google-oauth": { "version": "2.0.0", "requires": { diff --git a/backend/package.json b/backend/package.json index 14f4e4ffce..02c6e09ba6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -108,6 +108,7 @@ "openapi-comment-parser": "^1.0.0", "passport": "0.6.0", "passport-facebook": "3.0.0", + "passport-github2": "^0.1.12", "passport-google-oauth": "2.0.0", "passport-google-oauth20": "^2.0.0", "passport-slack": "0.0.7", diff --git a/backend/src/api/auth/authSocial.ts b/backend/src/api/auth/authSocial.ts index 77951211e3..97d9b29f0f 100644 --- a/backend/src/api/auth/authSocial.ts +++ b/backend/src/api/auth/authSocial.ts @@ -1,6 +1,6 @@ import passport from 'passport' import { getServiceChildLogger } from '@crowd/logging' -import { API_CONFIG, GOOGLE_CONFIG } from '../../conf' +import { API_CONFIG, GITHUB_CONFIG, GOOGLE_CONFIG } from '../../conf' import AuthService from '../../services/auth/authService' const log = getServiceChildLogger('AuthSocial') @@ -46,6 +46,26 @@ export default (app, routes) => { })(req, res) }) } + + if (GITHUB_CONFIG.clientId) { + routes.get( + '/auth/social/github', + passport.authenticate('github', { + scope: ['user:email', 'read:user'], + session: false, + }), + () => { + // The request will be redirected for authentication, so this + // function will not be called. + }, + ) + + routes.get('/auth/social/github/callback', (req, res) => { + passport.authenticate('github', (err, jwtToken) => { + handleCallback(res, err, jwtToken) + })(req, res) + }) + } } function handleCallback(res, err, jwtToken) { diff --git a/backend/src/conf/configTypes.ts b/backend/src/conf/configTypes.ts index a79c3e84d7..ea49e7f8cc 100644 --- a/backend/src/conf/configTypes.ts +++ b/backend/src/conf/configTypes.ts @@ -144,6 +144,7 @@ export interface GithubConfiguration { webhookSecret: string isCommitDataEnabled: string globalLimit?: number + callbackUrl: string } export interface SendgridConfiguration { diff --git a/backend/src/middlewares/passportStrategyMiddleware.ts b/backend/src/middlewares/passportStrategyMiddleware.ts index b9016d4c5d..3df0fd792f 100644 --- a/backend/src/middlewares/passportStrategyMiddleware.ts +++ b/backend/src/middlewares/passportStrategyMiddleware.ts @@ -1,8 +1,9 @@ import { getServiceLogger } from '@crowd/logging' import passport from 'passport' -import { GOOGLE_CONFIG, SLACK_CONFIG } from '../conf' +import { GOOGLE_CONFIG, SLACK_CONFIG, GITHUB_CONFIG } from '../conf' import { getGoogleStrategy } from '../services/auth/passportStrategies/googleStrategy' import { getSlackStrategy } from '../services/auth/passportStrategies/slackStrategy' +import { getGithubStrategy } from '../services/auth/passportStrategies/githubStrategy' const log = getServiceLogger() @@ -19,6 +20,10 @@ export async function passportStrategyMiddleware(req, res, next) { if (GOOGLE_CONFIG.clientId) { passport.use(getGoogleStrategy()) } + + if (GITHUB_CONFIG.clientId) { + passport.use(getGithubStrategy()) + } } catch (error) { log.error(error, 'Error getting some passport strategies!') } finally { diff --git a/backend/src/services/auth/passportStrategies/githubStrategy.ts b/backend/src/services/auth/passportStrategies/githubStrategy.ts new file mode 100644 index 0000000000..bfb2576c00 --- /dev/null +++ b/backend/src/services/auth/passportStrategies/githubStrategy.ts @@ -0,0 +1,50 @@ +import { get } from 'lodash' +import GithubStrategy from 'passport-github2' +import { getServiceChildLogger } from '@crowd/logging' +import { GITHUB_CONFIG } from '../../../conf' +import { databaseInit } from '../../../database/databaseConnection' +import AuthService from '../authService' +import { splitFullName } from '../../../utils/splitName' +import { AuthProvider } from '../../../types/common' + +const log = getServiceChildLogger('AuthSocial') + +export function getGithubStrategy(): GithubStrategy { + return new GithubStrategy( + { + clientID: GITHUB_CONFIG.clientId, + clientSecret: GITHUB_CONFIG.clientSecret, + callbackURL: GITHUB_CONFIG.callbackUrl, + scope: ['user:email'], // Request email scope + }, + (accessToken, refreshToken, profile, done) => { + databaseInit() + .then((database) => { + const email = get(profile, 'emails[0].value') + // GitHub user's profile doesn't include 'verified' field + // However, GitHub accounts require email verification for activation + const emailVerified = !!email + const displayName = get(profile, 'displayName') + const { firstName, lastName } = splitFullName(displayName) + + return AuthService.signinFromSocial( + AuthProvider.GITHUB, + profile.id, + email, + emailVerified, + firstName, + lastName, + displayName, + { database }, + ) + }) + .then((jwtToken) => { + done(null, jwtToken) + }) + .catch((error) => { + log.error(error, 'Error while handling github auth!') + done(error, null) + }) + }, + ) +} diff --git a/backend/src/services/auth/passportStrategies/googleStrategy.ts b/backend/src/services/auth/passportStrategies/googleStrategy.ts index 38ac46b6b7..1b9e6636f6 100644 --- a/backend/src/services/auth/passportStrategies/googleStrategy.ts +++ b/backend/src/services/auth/passportStrategies/googleStrategy.ts @@ -4,6 +4,8 @@ import { getServiceChildLogger } from '@crowd/logging' import { GOOGLE_CONFIG } from '../../../conf' import { databaseInit } from '../../../database/databaseConnection' import AuthService from '../authService' +import { splitFullName } from '../../../utils/splitName' +import { AuthProvider } from '../../../types/common' const log = getServiceChildLogger('AuthSocial') @@ -23,7 +25,7 @@ export function getGoogleStrategy(): GoogleStrategy { const { firstName, lastName } = splitFullName(displayName) return AuthService.signinFromSocial( - 'google', + AuthProvider.GOOGLE, profile.id, email, emailVerified, @@ -43,19 +45,3 @@ export function getGoogleStrategy(): GoogleStrategy { }, ) } - -function splitFullName(fullName) { - let firstName - let lastName - - if (fullName && fullName.split(' ').length > 1) { - const [firstNameArray, ...lastNameArray] = fullName.split(' ') - firstName = firstNameArray - lastName = lastNameArray.join(' ') - } else { - firstName = fullName || null - lastName = null - } - - return { firstName, lastName } -} diff --git a/backend/src/types/common.ts b/backend/src/types/common.ts index c78f57c281..a88a4c5a57 100644 --- a/backend/src/types/common.ts +++ b/backend/src/types/common.ts @@ -33,3 +33,8 @@ export enum FeatureFlagRedisKey { MEMBER_ENRICHMENT_COUNT = 'memberEnrichmentCount', ORGANIZATION_ENRICHMENT_COUNT = 'organizationEnrichmentCount', } + +export enum AuthProvider { + GOOGLE = 'google', + GITHUB = 'github', +} diff --git a/backend/src/utils/splitName.ts b/backend/src/utils/splitName.ts new file mode 100644 index 0000000000..809ac1a13c --- /dev/null +++ b/backend/src/utils/splitName.ts @@ -0,0 +1,15 @@ +export function splitFullName(fullName) { + let firstName + let lastName + + if (fullName && fullName.split(' ').length > 1) { + const [firstNameArray, ...lastNameArray] = fullName.split(' ') + firstName = firstNameArray + lastName = lastNameArray.join(' ') + } else { + firstName = fullName || null + lastName = null + } + + return { firstName, lastName } +} diff --git a/frontend/src/modules/auth/pages/signin-page.vue b/frontend/src/modules/auth/pages/signin-page.vue index 2bc966eb72..bb4bfac6c9 100644 --- a/frontend/src/modules/auth/pages/signin-page.vue +++ b/frontend/src/modules/auth/pages/signin-page.vue @@ -117,7 +117,7 @@
-diff --git a/frontend/src/modules/auth/pages/signup-page.vue b/frontend/src/modules/auth/pages/signup-page.vue index 9047018930..c02da342ef 100644 --- a/frontend/src/modules/auth/pages/signup-page.vue +++ b/frontend/src/modules/auth/pages/signup-page.vue @@ -194,7 +194,7 @@