feat(auth): implement mobile Google authentication using Passport strategy

- Added a new Passport strategy for mobile Google authentication.
- Introduced `googleMobileAuthPassport` method in `AuthController` to handle authentication flow.
- Updated routes to utilize the new Passport strategy for mobile sign-in.
- Added `passport-custom` dependency for custom authentication strategy.
- Updated `package.json` and `package-lock.json` to reflect new dependencies and version requirements.
This commit is contained in:
Chamika J
2025-08-05 17:12:29 +05:30
parent 84f96b7db2
commit 7bb020d448
6 changed files with 189 additions and 441 deletions

View File

@@ -1,4 +1,6 @@
import bcrypt from "bcrypt";
import passport from "passport";
import {NextFunction} from "express";
import {sendResetEmail, sendResetSuccessEmail} from "../shared/email-templates";
@@ -182,6 +184,54 @@ export default class AuthController extends WorklenzControllerBase {
}
}
public static googleMobileAuthPassport(req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction) {
const mobileOptions = {
session: true,
failureFlash: true,
failWithError: false
};
passport.authenticate("google-mobile", mobileOptions, (err: any, user: any, info: any) => {
if (err) {
return res.status(500).send({
done: false,
message: "Authentication failed",
body: null
});
}
if (!user) {
return res.status(400).send({
done: false,
message: info?.message || "Authentication failed",
body: null
});
}
// Log the user in (create session)
req.login(user, (loginErr) => {
if (loginErr) {
return res.status(500).send({
done: false,
message: "Session creation failed",
body: null
});
}
// Add build version
user.build_v = FileConstants.getRelease();
return res.status(200).send({
done: true,
message: "Login successful",
user,
authenticated: true
});
});
})(req, res, next);
}
@HandleExceptions({logWithError: "body"})
public static async googleMobileAuth(req: IWorkLenzRequest, res: IWorkLenzResponse) {
const {idToken} = req.body;

View File

@@ -4,6 +4,7 @@ import {deserialize} from "./deserialize";
import {serialize} from "./serialize";
import GoogleLogin from "./passport-strategies/passport-google";
import GoogleMobileLogin from "./passport-strategies/passport-google-mobile";
import LocalLogin from "./passport-strategies/passport-local-login";
import LocalSignup from "./passport-strategies/passport-local-signup";
@@ -15,6 +16,7 @@ export default (passport: PassportStatic) => {
passport.use("local-login", LocalLogin);
passport.use("local-signup", LocalSignup);
passport.use(GoogleLogin);
passport.use("google-mobile", GoogleMobileLogin);
passport.serializeUser(serialize);
passport.deserializeUser(deserialize);
};

View File

@@ -0,0 +1,110 @@
import { Strategy as CustomStrategy } from "passport-custom";
import axios from "axios";
import { Request } from "express";
import db from "../../config/db";
import { log_error } from "../../shared/utils";
import { ERROR_KEY } from "./passport-constants";
interface GoogleTokenProfile {
sub: string;
email: string;
name: string;
picture: string;
email_verified: boolean;
aud: string;
iss: string;
exp: number;
}
async function handleMobileGoogleAuth(req: Request, done: any) {
try {
const { idToken } = req.body;
if (!idToken) {
return done(null, false, { message: "ID token is required" });
}
// Verify Google ID token
const response = await axios.get(
`https://oauth2.googleapis.com/tokeninfo?id_token=${idToken}`
);
const profile: GoogleTokenProfile = response.data;
// Validate token audience (client ID)
const allowedClientIds = [
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_ANDROID_CLIENT_ID,
process.env.GOOGLE_IOS_CLIENT_ID,
].filter(Boolean);
if (!allowedClientIds.includes(profile.aud)) {
return done(null, false, { message: "Invalid token audience" });
}
// Validate token issuer
if (
!["https://accounts.google.com", "accounts.google.com"].includes(
profile.iss
)
) {
return done(null, false, { message: "Invalid token issuer" });
}
// Check token expiry
if (Date.now() >= profile.exp * 1000) {
return done(null, false, { message: "Token expired" });
}
if (!profile.email_verified) {
return done(null, false, { message: "Email not verified" });
}
// Check for existing local account
const localAccountResult = await db.query(
"SELECT 1 FROM users WHERE email = $1 AND password IS NOT NULL AND is_deleted IS FALSE;",
[profile.email]
);
if (localAccountResult.rowCount) {
const message = `No Google account exists for email ${profile.email}.`;
return done(null, false, { message });
}
// Check if user exists
const userResult = await db.query(
"SELECT id, google_id, name, email, active_team FROM users WHERE google_id = $1 OR email = $2;",
[profile.sub, profile.email]
);
if (userResult.rowCount) {
// Existing user - login
const user = userResult.rows[0];
return done(null, user, { message: "User successfully logged in" });
}
// New user - register
const googleUserData = {
id: profile.sub,
displayName: profile.name,
email: profile.email,
picture: profile.picture,
};
const registerResult = await db.query(
"SELECT register_google_user($1) AS user;",
[JSON.stringify(googleUserData)]
);
const { user } = registerResult.rows[0];
return done(null, user, {
message: "User successfully registered and logged in",
});
} catch (error: any) {
log_error(error);
if (error.response?.status === 400) {
return done(null, false, { message: "Invalid ID token" });
}
return done(error);
}
}
export default new CustomStrategy(handleMobileGoogleAuth);

View File

@@ -8,6 +8,7 @@ import resetEmailValidator from "../../middlewares/validators/reset-email-valida
import updatePasswordValidator from "../../middlewares/validators/update-password-validator";
import passwordValidator from "../../middlewares/validators/password-validator";
import safeControllerFunction from "../../shared/safe-controller-function";
import FileConstants from "../../shared/file-constants";
const authRouter = express.Router();
@@ -55,8 +56,8 @@ authRouter.get("/google/verify", (req, res) => {
})(req, res);
});
// Mobile Google Sign-In
authRouter.post("/google/mobile", safeControllerFunction(AuthController.googleMobileAuth));
// Mobile Google Sign-In using Passport strategy
authRouter.post("/google/mobile", AuthController.googleMobileAuthPassport);
// Passport logout
authRouter.get("/logout", AuthController.logout);