From 736c1cb4ce21273b564365f52ead4d66be3674b9 Mon Sep 17 00:00:00 2001 From: uwimana janet Date: Wed, 20 Nov 2024 10:46:56 +0200 Subject: [PATCH] fixing login with 2fa --- src/models/user.ts | 6 +-- src/resolvers/2fa.resolvers.ts | 77 +++++++++++++++++++++------------ src/resolvers/userResolver.ts | 64 ++++++++++++++------------- src/schema/index.ts | 7 +-- src/utils/2WayAuthentication.ts | 2 +- 5 files changed, 92 insertions(+), 64 deletions(-) diff --git a/src/models/user.ts b/src/models/user.ts index d7bf5c59..0c418a1a 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -103,10 +103,10 @@ const userSchema = new Schema( type: String, }, - oneTimeCode: { + TwoWayVerificationToken: { type: String, - code: String, - required: false + required: false, + default:null }, oneTimeCodeExpiresAt: { type: Date, diff --git a/src/resolvers/2fa.resolvers.ts b/src/resolvers/2fa.resolvers.ts index 8a850981..51eb512b 100644 --- a/src/resolvers/2fa.resolvers.ts +++ b/src/resolvers/2fa.resolvers.ts @@ -1,13 +1,13 @@ import { AuthenticationError } from 'apollo-server-errors' import mongoose from 'mongoose' -import jwt from 'jsonwebtoken'; -import { generateTokenUserExists } from '../helpers/user.helpers'; -import { sendEmail } from '../utils/sendEmail'; -import { verifyOtpToken } from '../utils/2WayAuthentication'; -import { GraphQLError } from 'graphql'; -import { User } from '../models/user'; -import { logGeoActivity, loginsCount } from './userResolver'; +import jwt from 'jsonwebtoken' +import { generateTokenUserExists } from '../helpers/user.helpers' +import { sendEmail } from '../utils/sendEmail' +import { verifyOtpToken } from '../utils/2WayAuthentication' +import { GraphQLError } from 'graphql' +import { User } from '../models/user' +import { logGeoActivity, loginsCount } from './userResolver' interface Enable2FAInput { email: string @@ -17,7 +17,7 @@ interface Disable2FAInput { email: string } -const SECRET: string = process.env.SECRET ?? 'test_secret' +const SECRET = (process.env.SECRET as string) || 'mysq_unique_secret' const resolvers = { Mutation: { enableTwoFactorAuth: async (_: any, { email }: Enable2FAInput) => { @@ -65,9 +65,17 @@ const resolvers = { // Disable 2FA by clearing the secret and one-time code user.twoFactorSecret = null user.twoFactorAuth = false - user.oneTimeCode = null + user.TwoWayVerificationToken = null await user.save() + await sendEmail( + email, + ' Two-Factor Authentication disabled ', + 'Two-Factor Authentication has been disabled on your account', + null, + process.env.ADMIN_EMAIL, + process.env.ADMIN_PASS + ) return 'Two-factor authentication disabled.' } catch (error) { @@ -77,27 +85,38 @@ const resolvers = { loginWithTwoFactorAuthentication: async ( _: any, - { id, email, otp, TwoWayVerificationToken }: { id?: string; email?: string; otp: string; TwoWayVerificationToken: string }, context: any + { + id, + email, + otp, + + }: { + id?: string + email?: string + otp: string + + }, + context: any ) => { - const { clientIpAdress } = context; - // Verify OTP - const isValidOtp = verifyOtpToken(TwoWayVerificationToken, otp); - - if (!isValidOtp) { - throw new GraphQLError('Invalid OTP. Please try again.'); - } + const { clientIpAdress } = context // Fetch user by either ID or email - let user: any; + let user: any if (id) { - user = await User.findById(id); + user = await User.findById(id) } else if (email) { - user = await User.findOne({ email }); + user = await User.findOne({ email }) } // Check if user was found if (!user) { - throw new GraphQLError('User not found.'); + throw new GraphQLError('User not found.') + } + // Verify OTP + const isValidOtp = verifyOtpToken(user.TwoWayVerificationToken, otp) + + if (!isValidOtp) { + throw new GraphQLError('Invalid OTP. Please try again.') } // Generate JWT token @@ -105,23 +124,27 @@ const resolvers = { { userId: user._id, role: user._doc?.role || 'user' }, SECRET, { expiresIn: '2h' } - ); + ) const geoData = await logGeoActivity(user, clientIpAdress) - const organizationName = user.organizations[0]; + const organizationName = user.organizations[0] if (organizationName) { - const location = geoData && geoData.city && geoData.country_name ? `${geoData.city}-${geoData.country_name}` : null; - await loginsCount(organizationName, location); + const location = + geoData.city && geoData.country_name + ? `${geoData.city}-${geoData.country_name}` + : null + await loginsCount(organizationName, location) } + user.TwoWayVerificationToken = null + await user.save() return { token, user: user.toJSON(), message: 'Logged in successfully', - }; + } }, - }, } diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index d0060208..a4d3e6b2 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -377,14 +377,14 @@ const resolvers: any = { context: any ) { // Check organization validity - const org = await checkLoggedInOrganization(orgToken) - const { clientIpAdress } = context + const org = await checkLoggedInOrganization(orgToken); + const { clientIpAdress } = context; if (!org) { throw new GraphQLError('Organization not found', { extensions: { code: 'InvalidOrganization' }, - }) + }); } - + // Find user with populated fields const user: any = await User.findOne({ email }).populate({ path: 'cohort', @@ -400,14 +400,15 @@ const resolvers: any = { strictPopulate: false, }, }, - }) - + }); + // Check if user exists if (!user) { throw new GraphQLError('Invalid credentials', { extensions: { code: 'AccountNotFound' }, - }) + }); } + // Check if account is active if (user.status?.status !== 'active') { throw new GraphQLError( @@ -415,14 +416,14 @@ const resolvers: any = { { extensions: { code: 'AccountInactive' }, } - ) + ); } - + // Check if two-factor authentication is enabled if (user.twoFactorAuth) { - const otp = generateOtp() // Generate OTP - const TwoWayVerificationToken = encodeOtpToToken(otp, email) // Encode OTP - + const otp = generateOtp(); // Generate OTP + const TwoWayVerificationToken = encodeOtpToToken(otp, email); // Encode OTP + // Send email with OTP await sendEmail( email, @@ -431,52 +432,55 @@ const resolvers: any = { null, process.env.ADMIN_EMAIL, process.env.ADMIN_PASS - ) - - // Return response with encoded OTP token and message + ); + + // Save the Two-Way Verification Token to the database + user.TwoWayVerificationToken = TwoWayVerificationToken; + await user.save(); + + // Return a response without exposing the token return { message: 'Check your email for the OTP code.', otpRequired: true, - TwoWayVerificationToken, user: { id: user._id }, - } + }; } else { // Verify password if 2FA is not enabled - const passwordMatch = await user?.checkPass(password) + const passwordMatch = await user?.checkPass(password); if (!passwordMatch) { throw new GraphQLError('Invalid credentials', { extensions: { code: 'InvalidCredential' }, - }) + }); } - + // Generate token for authenticated user const token = jwt.sign( { userId: user._id, role: user._doc?.role || 'user' }, SECRET, { expiresIn: '2h' } - ) - - - const geoData = await logGeoActivity(user, clientIpAdress) // Log activity - - const organizationName = user.organizations[0] + ); + + const geoData = await logGeoActivity(user, clientIpAdress); // Log activity + + const organizationName = user.organizations[0]; if (organizationName) { const location = geoData && geoData.city && geoData.country_name ? `${geoData.city}-${geoData.country_name}` - : null - await loginsCount(organizationName, location) + : null; + await loginsCount(organizationName, location); } - + // Return token and user data return { token, user: user.toJSON(), geoData, otpRequired: false, - } + }; } }, + async deleteUser(_: any, { input }: any, context: { userId: any }) { const requester = await User.findById(context.userId) if (!requester) { diff --git a/src/schema/index.ts b/src/schema/index.ts index 3c5c9ba0..203ffae9 100644 --- a/src/schema/index.ts +++ b/src/schema/index.ts @@ -81,6 +81,7 @@ const Schema = gql` status: StatusType ratings: [Rating] twoFactorAuth:Boolean! + TwoWayVerificationToken:String } input RegisterInput { email: String! @@ -157,7 +158,7 @@ const Schema = gql` user: User message:String otpRequired:Boolean - TwoWayVerificationToken:String + } type OrgLogin { token: String @@ -314,9 +315,9 @@ const Schema = gql` type Mutation { enableTwoFactorAuth(email: String!): String - oneTimeCode: String! + # //TwoWayVerificationToken: String! disableTwoFactorAuth(email: String!): String - loginWithTwoFactorAuthentication(email: String!, otp: String!, TwoWayVerificationToken: String!): LoginResponse! + loginWithTwoFactorAuthentication(email: String!, otp: String!): LoginResponse! createUserRole(name: String!): UserRole! uploadResume(userId: ID!, resume: String!): Profile dropTTLUser(email: String!, reason: String!): String! diff --git a/src/utils/2WayAuthentication.ts b/src/utils/2WayAuthentication.ts index 9249ff8c..aeb8fb87 100644 --- a/src/utils/2WayAuthentication.ts +++ b/src/utils/2WayAuthentication.ts @@ -8,7 +8,7 @@ export function generateOtp(length = 6): string { } return otp } -const SECRET: string = process.env.SECRET ?? 'test_secret' +const SECRET = (process.env.SECRET as string) || 'mysq_unique_secret' export function encodeOtpToToken(otp: string, email: string): string { const payload = { otp, email }