Skip to content

Commit

Permalink
Merge branch 'develop' into include-userData-in-progress-response
Browse files Browse the repository at this point in the history
  • Loading branch information
Achintya-Chatterjee authored Dec 27, 2024
2 parents 9cb2489 + 7269b1f commit e5fb0a2
Show file tree
Hide file tree
Showing 14 changed files with 591 additions and 44 deletions.
5 changes: 5 additions & 0 deletions config/custom-environment-variables.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ module.exports = {
clientSecret: "GITHUB_CLIENT_SECRET",
},

googleOauth: {
clientId: "GOOGLE_CLIENT_ID",
clientSecret: "GOOGLE_CLIENT_SECRET",
},

githubAccessToken: "GITHUB_PERSONAL_ACCESS_TOKEN",

firestore: "FIRESTORE_CONFIG",
Expand Down
5 changes: 5 additions & 0 deletions config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ module.exports = {
clientSecret: "<clientSecret>",
},

googleOauth: {
clientId: "<clientId>",
clientSecret: "<clientSecret>",
},

emailServiceConfig: {
email: "<RDS_EMAIL>",
password: "<EMAIL PASSWORD GENERATED AFTER 2FA>",
Expand Down
168 changes: 134 additions & 34 deletions controllers/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,120 @@ const {
USER_DOES_NOT_EXIST_ERROR,
} = require("../constants/errorMessages");

const googleAuthLogin = (req, res, next) => {
const { redirectURL } = req.query;
return passport.authenticate("google", {
scope: ["email"],
state: redirectURL,
})(req, res, next);
};

function handleRedirectUrl(req) {
const rdsUiUrl = new URL(config.get("services.rdsUi.baseUrl"));
let authRedirectionUrl = rdsUiUrl;
let isMobileApp = false;
let isV2FlagPresent = false;
let devMode = false;

if ("state" in req.query) {
try {
const redirectUrl = new URL(req.query.state);
if (redirectUrl.searchParams.get("isMobileApp") === "true") {
isMobileApp = true;
redirectUrl.searchParams.delete("isMobileApp");
}

if (`.${redirectUrl.hostname}`.endsWith(`.${rdsUiUrl.hostname}`)) {
// Matching *.realdevsquad.com
authRedirectionUrl = redirectUrl;
devMode = Boolean(redirectUrl.searchParams.get("dev"));
} else {
logger.error(`Malicious redirect URL provided URL: ${redirectUrl}, Will redirect to RDS`);
}
if (redirectUrl.searchParams.get("v2") === "true") {
isV2FlagPresent = true;
}
} catch (error) {
logger.error("Invalid redirect URL provided", error);
}
}
return {
authRedirectionUrl,
isMobileApp,
isV2FlagPresent,
devMode,
};
}

const getAuthCookieOptions = () => {
const rdsUiUrl = new URL(config.get("services.rdsUi.baseUrl"));
return {
domain: rdsUiUrl.hostname,
expires: new Date(Date.now() + config.get("userToken.ttl") * 1000),
httpOnly: true,
secure: true,
sameSite: "lax",
};
};

async function handleGoogleLogin(req, res, user, authRedirectionUrl) {
try {
if (!user.emails || user.emails.length === 0) {
logger.error("Google login failed: No emails found in user data");
return res.boom.unauthorized("No email found in Google account");
}
const primaryEmail = user.emails.find((email) => email.verified === true);
if (!primaryEmail) {
logger.error("Google login failed: No verified email found");
return res.boom.unauthorized("No verified email found in Google account");
}

const userData = {
email: primaryEmail.value,
created_at: Date.now(),
updated_at: null,
};

const userDataFromDB = await users.fetchUser({ email: userData.email });

if (userDataFromDB.userExists) {
if (userDataFromDB.user.roles?.developer) {
const errorMessage = encodeURIComponent("Google login is restricted for developer role.");
const separator = authRedirectionUrl.search ? "&" : "?";
return res.redirect(`${authRedirectionUrl}${separator}error=${errorMessage}`);
}
}

const { userId, incompleteUserDetails } = await users.addOrUpdate(userData);

const token = authService.generateAuthToken({ userId });

const cookieOptions = getAuthCookieOptions();

res.cookie(config.get("userToken.cookieName"), token, cookieOptions);

if (incompleteUserDetails) {
authRedirectionUrl = "https://my.realdevsquad.com/new-signup";
}

return res.redirect(authRedirectionUrl);
} catch (err) {
logger.error("Unexpected error during Google login", err);
return res.boom.unauthorized("User cannot be authenticated");
}
}

const googleAuthCallback = (req, res, next) => {
const { authRedirectionUrl } = handleRedirectUrl(req);
return passport.authenticate("google", { session: false }, async (err, accessToken, user) => {
if (err) {
logger.error(err);
return res.boom.unauthorized("User cannot be authenticated");
}
return await handleGoogleLogin(req, res, user, authRedirectionUrl);
})(req, res, next);
};

/**
* Makes authentication call to GitHub statergy
*
Expand Down Expand Up @@ -41,33 +155,7 @@ const githubAuthLogin = (req, res, next) => {
*/
const githubAuthCallback = (req, res, next) => {
let userData;
let isMobileApp = false;
const rdsUiUrl = new URL(config.get("services.rdsUi.baseUrl"));
let authRedirectionUrl = rdsUiUrl;
let devMode = false;
let isV2FlagPresent = false;

if ("state" in req.query) {
try {
const redirectUrl = new URL(req.query.state);
if (redirectUrl.searchParams.get("isMobileApp") === "true") {
isMobileApp = true;
redirectUrl.searchParams.delete("isMobileApp");
}

if (redirectUrl.searchParams.get("v2") === "true") isV2FlagPresent = true;

if (`.${redirectUrl.hostname}`.endsWith(`.${rdsUiUrl.hostname}`)) {
// Matching *.realdevsquad.com
authRedirectionUrl = redirectUrl;
devMode = Boolean(redirectUrl.searchParams.get("dev"));
} else {
logger.error(`Malicious redirect URL provided URL: ${redirectUrl}, Will redirect to RDS`);
}
} catch (error) {
logger.error("Invalid redirect URL provided", error);
}
}
let { authRedirectionUrl, isMobileApp, isV2FlagPresent, devMode } = handleRedirectUrl(req);
try {
return passport.authenticate("github", { session: false }, async (err, accessToken, user) => {
if (err) {
Expand All @@ -77,23 +165,33 @@ const githubAuthCallback = (req, res, next) => {
userData = {
github_id: user.username,
github_display_name: user.displayName,
email: user._json.email,
github_created_at: Number(new Date(user._json.created_at).getTime()),
github_user_id: user.id,
created_at: Date.now(),
updated_at: null,
};

if (!userData.email) {
const githubBaseUrl = config.get("githubApi.baseUrl");
const res = await fetch(`${githubBaseUrl}/user/emails`, {
headers: {
Authorization: `token ${accessToken}`,
},
});
const emails = await res.json();
const primaryEmails = emails.filter((item) => item.primary);

if (primaryEmails.length > 0) {
userData.email = primaryEmails[0].email;
}
}

const { userId, incompleteUserDetails, role } = await users.addOrUpdate(userData);

const token = authService.generateAuthToken({ userId });

const cookieOptions = {
domain: rdsUiUrl.hostname,
expires: new Date(Date.now() + config.get("userToken.ttl") * 1000),
httpOnly: true,
secure: true,
sameSite: "lax",
};
const cookieOptions = getAuthCookieOptions();
// respond with a cookie
res.cookie(config.get("userToken.cookieName"), token, cookieOptions);

Expand Down Expand Up @@ -232,6 +330,8 @@ const fetchDeviceDetails = async (req, res) => {
module.exports = {
githubAuthLogin,
githubAuthCallback,
googleAuthLogin,
googleAuthCallback,
signout,
storeUserDeviceInfo,
updateAuthStatus,
Expand Down
13 changes: 13 additions & 0 deletions middlewares/passport.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const passport = require("passport");
const GitHubStrategy = require("passport-github2").Strategy;
const GoogleStrategy = require("passport-google-oauth20").Strategy;

try {
passport.use(
Expand All @@ -14,6 +15,18 @@ try {
}
)
);
passport.use(
new GoogleStrategy(
{
clientID: config.get("googleOauth.clientId"),
clientSecret: config.get("googleOauth.clientSecret"),
callbackURL: `${config.get("services.rdsApi.baseUrl")}/auth/google/callback`,
},
(accessToken, refreshToken, profile, done) => {
return done(null, accessToken, profile);
}
)
);
} catch (err) {
logger.error("Error initialising passport:", err);
}
23 changes: 18 additions & 5 deletions models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,23 @@ const addOrUpdate = async (userData, userId = null, devFeatureFlag) => {
}

// userId is null, Add or Update user
let user;
let user = null;

if (userData.github_user_id) {
user = await userModel.where("github_user_id", "==", userData.github_user_id).limit(1).get();
}
if (!user || (user && user.empty)) {

if (userData.github_id && (!user || user.empty)) {
user = await userModel.where("github_id", "==", userData.github_id).limit(1).get();
}

if (userData.email && (!user || user.empty)) {
user = await userModel.where("email", "==", userData.email).limit(1).get();
}

if (user && !user.empty && user.docs !== null) {
await userModel.doc(user.docs[0].id).set({ ...userData, updated_at: Date.now() }, { merge: true });
const { created_at: createdAt, ...updatedUserData } = userData;
await userModel.doc(user.docs[0].id).set({ ...updatedUserData, updated_at: Date.now() }, { merge: true });

const logData = {
type: logType.USER_DETAILS_UPDATED,
Expand All @@ -153,7 +161,6 @@ const addOrUpdate = async (userData, userId = null, devFeatureFlag) => {
role: Object.values(AUTHORITIES).find((role) => data.roles[role]) || AUTHORITIES.USER,
};
}

// Add new user
/*
Adding default archived role enables us to query for only
Expand Down Expand Up @@ -377,7 +384,7 @@ const fetchUsers = async (usernames = []) => {
* @param { Object }: Object with username and userId, any of the two can be used
* @return {Promise<{userExists: boolean, user: <userModel>}|{userExists: boolean, user: <userModel>}>}
*/
const fetchUser = async ({ userId = null, username = null, githubUsername = null, discordId = null }) => {
const fetchUser = async ({ userId = null, username = null, githubUsername = null, discordId = null, email = null }) => {
try {
let userData, id;
if (username) {
Expand All @@ -402,6 +409,12 @@ const fetchUser = async ({ userId = null, username = null, githubUsername = null
id = doc.id;
userData = doc.data();
});
} else if (email) {
const user = await userModel.where("email", "==", email).limit(1).get();
user.forEach((doc) => {
id = doc.id;
userData = doc.data();
});
}

if (userData && userData.disabled_roles !== undefined) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"nodemailer-mock": "^2.0.6",
"passport": "0.7.0",
"passport-github2": "0.1.12",
"passport-google-oauth20": "^2.0.0",
"rate-limiter-flexible": "5.0.3",
"winston": "3.13.0"
},
Expand Down
5 changes: 5 additions & 0 deletions routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import auth from "../controllers/auth";
import authenticate from "../middlewares/authenticate";
import userDeviceInfoValidator from "../middlewares/validators/qrCodeAuth";
import qrCodeAuthValidator from "../middlewares/validators/qrCodeAuth";
import { devFlagMiddleware } from "../middlewares/devFlag";

router.get("/github/login", auth.githubAuthLogin);

router.get("/github/callback", auth.githubAuthCallback);

router.get("/google/login", devFlagMiddleware, auth.googleAuthLogin);

router.get("/google/callback", auth.googleAuthCallback);

router.get("/signout", auth.signout);

router.get("/qr-code-auth", userDeviceInfoValidator.validateFetchingUserDocument, auth.fetchUserDeviceInfo);
Expand Down
4 changes: 4 additions & 0 deletions test/config/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ module.exports = {
identity_store_id: "test-identity-store-id",
},

googleOauth: {
clientId: "cliendId",
clientSecret: "clientSecret",
},
firestore: `{
"type": "service_account",
"project_id": "test-project-id-for-emulator",
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/auth/githubUserInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ module.exports = () => {
company: null,
blog: "",
location: null,
email: null,
email: "[email protected]",
hireable: null,
bio: null,
twitter_username: null,
Expand Down
Loading

0 comments on commit e5fb0a2

Please sign in to comment.