Skip to content

Commit

Permalink
Merge branch 'develop' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
vikhyat187 authored Dec 27, 2024
2 parents aece5e7 + cb683fb commit 9675144
Show file tree
Hide file tree
Showing 25 changed files with 1,578 additions and 67 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
28 changes: 28 additions & 0 deletions controllers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const config = require("config");
const { generateUniqueUsername } = require("../services/users");
const userService = require("../services/users");
const discordDeveloperRoleId = config.get("discordDeveloperRoleId");
const usersCollection = firestore.collection("users");

const verifyUser = async (req, res) => {
const userId = req.userData.id;
Expand Down Expand Up @@ -714,6 +715,12 @@ const updateUser = async (req, res) => {
const { id: profileDiffId, message } = req.body;
const devFeatureFlag = req.query.dev === "true";
let profileDiffData;

const userDoc = await usersCollection.doc(req.params.userId).get();
if (!userDoc.exists) {
return res.boom.notFound("The User doesn't exist.");
}

if (devFeatureFlag) {
profileDiffData = await profileDiffsQuery.fetchProfileDiffUnobfuscated(profileDiffId);
} else {
Expand Down Expand Up @@ -1096,6 +1103,26 @@ const updateUsernames = async (req, res) => {
}
};

const updateProfile = async (req, res) => {
try {
const { id: currentUserId, roles = {} } = req.userData;
const isSelf = req.params.userId === currentUserId;
const isSuperUser = roles[ROLES.SUPERUSER];
const profile = req.query.profile === "true";

if (isSelf && profile && req.query.dev === "true") {
return await updateSelf(req, res);
} else if (isSuperUser) {
return await updateUser(req, res);
}

return res.boom.badRequest("Invalid Request.");
} catch (err) {
logger.error(`Error in updateUserStatusController: ${err}`);
return res.boom.badImplementation("An unexpected error occurred.");
}
};

module.exports = {
verifyUser,
generateChaincode,
Expand Down Expand Up @@ -1128,4 +1155,5 @@ module.exports = {
isDeveloper,
getIdentityStats,
updateUsernames,
updateProfile,
};
10 changes: 10 additions & 0 deletions middlewares/conditionalMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const conditionalMiddleware = (validator) => {
return async (req, res, next) => {
if (req.params.userId === req.userData.id && req.query.profile === "true") {
return validator(req, res, next);
}
next();
};
};

module.exports = conditionalMiddleware;
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
Loading

0 comments on commit 9675144

Please sign in to comment.