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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user