Skip to content

Commit d900ec0

Browse files
authored
fixing login with 2fa (#437)
1 parent 5f5a2d1 commit d900ec0

File tree

5 files changed

+92
-64
lines changed

5 files changed

+92
-64
lines changed

src/models/user.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,10 @@ const userSchema = new Schema(
103103
type: String,
104104

105105
},
106-
oneTimeCode: {
106+
TwoWayVerificationToken: {
107107
type: String,
108-
code: String,
109-
required: false
108+
required: false,
109+
default:null
110110
},
111111
oneTimeCodeExpiresAt: {
112112
type: Date,

src/resolvers/2fa.resolvers.ts

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { AuthenticationError } from 'apollo-server-errors'
22
import mongoose from 'mongoose'
33

4-
import jwt from 'jsonwebtoken';
5-
import { generateTokenUserExists } from '../helpers/user.helpers';
6-
import { sendEmail } from '../utils/sendEmail';
7-
import { verifyOtpToken } from '../utils/2WayAuthentication';
8-
import { GraphQLError } from 'graphql';
9-
import { User } from '../models/user';
10-
import { logGeoActivity, loginsCount } from './userResolver';
4+
import jwt from 'jsonwebtoken'
5+
import { generateTokenUserExists } from '../helpers/user.helpers'
6+
import { sendEmail } from '../utils/sendEmail'
7+
import { verifyOtpToken } from '../utils/2WayAuthentication'
8+
import { GraphQLError } from 'graphql'
9+
import { User } from '../models/user'
10+
import { logGeoActivity, loginsCount } from './userResolver'
1111

1212
interface Enable2FAInput {
1313
email: string
@@ -17,7 +17,7 @@ interface Disable2FAInput {
1717
email: string
1818
}
1919

20-
const SECRET: string = process.env.SECRET ?? 'test_secret'
20+
const SECRET = (process.env.SECRET as string) || 'mysq_unique_secret'
2121
const resolvers = {
2222
Mutation: {
2323
enableTwoFactorAuth: async (_: any, { email }: Enable2FAInput) => {
@@ -65,9 +65,17 @@ const resolvers = {
6565
// Disable 2FA by clearing the secret and one-time code
6666
user.twoFactorSecret = null
6767
user.twoFactorAuth = false
68-
user.oneTimeCode = null
68+
user.TwoWayVerificationToken = null
6969

7070
await user.save()
71+
await sendEmail(
72+
email,
73+
' Two-Factor Authentication disabled ',
74+
'Two-Factor Authentication has been disabled on your account',
75+
null,
76+
process.env.ADMIN_EMAIL,
77+
process.env.ADMIN_PASS
78+
)
7179

7280
return 'Two-factor authentication disabled.'
7381
} catch (error) {
@@ -77,51 +85,66 @@ const resolvers = {
7785

7886
loginWithTwoFactorAuthentication: async (
7987
_: any,
80-
{ id, email, otp, TwoWayVerificationToken }: { id?: string; email?: string; otp: string; TwoWayVerificationToken: string }, context: any
88+
{
89+
id,
90+
email,
91+
otp,
92+
93+
}: {
94+
id?: string
95+
email?: string
96+
otp: string
97+
98+
},
99+
context: any
81100
) => {
82-
const { clientIpAdress } = context;
83-
// Verify OTP
84-
const isValidOtp = verifyOtpToken(TwoWayVerificationToken, otp);
85-
86-
if (!isValidOtp) {
87-
throw new GraphQLError('Invalid OTP. Please try again.');
88-
}
101+
const { clientIpAdress } = context
89102

90103
// Fetch user by either ID or email
91-
let user: any;
104+
let user: any
92105
if (id) {
93-
user = await User.findById(id);
106+
user = await User.findById(id)
94107
} else if (email) {
95-
user = await User.findOne({ email });
108+
user = await User.findOne({ email })
96109
}
97110

98111
// Check if user was found
99112
if (!user) {
100-
throw new GraphQLError('User not found.');
113+
throw new GraphQLError('User not found.')
114+
}
115+
// Verify OTP
116+
const isValidOtp = verifyOtpToken(user.TwoWayVerificationToken, otp)
117+
118+
if (!isValidOtp) {
119+
throw new GraphQLError('Invalid OTP. Please try again.')
101120
}
102121

103122
// Generate JWT token
104123
const token = jwt.sign(
105124
{ userId: user._id, role: user._doc?.role || 'user' },
106125
SECRET,
107126
{ expiresIn: '2h' }
108-
);
127+
)
109128

110129
const geoData = await logGeoActivity(user, clientIpAdress)
111-
const organizationName = user.organizations[0];
130+
const organizationName = user.organizations[0]
112131
if (organizationName) {
113-
const location = geoData && geoData.city && geoData.country_name ? `${geoData.city}-${geoData.country_name}` : null;
114-
await loginsCount(organizationName, location);
132+
const location =
133+
geoData.city && geoData.country_name
134+
? `${geoData.city}-${geoData.country_name}`
135+
: null
136+
await loginsCount(organizationName, location)
115137
}
138+
user.TwoWayVerificationToken = null
139+
await user.save()
116140

117141
return {
118142
token,
119143
user: user.toJSON(),
120144

121145
message: 'Logged in successfully',
122-
};
146+
}
123147
},
124-
125148
},
126149
}
127150

src/resolvers/userResolver.ts

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -377,14 +377,14 @@ const resolvers: any = {
377377
context: any
378378
) {
379379
// Check organization validity
380-
const org = await checkLoggedInOrganization(orgToken)
381-
const { clientIpAdress } = context
380+
const org = await checkLoggedInOrganization(orgToken);
381+
const { clientIpAdress } = context;
382382
if (!org) {
383383
throw new GraphQLError('Organization not found', {
384384
extensions: { code: 'InvalidOrganization' },
385-
})
385+
});
386386
}
387-
387+
388388
// Find user with populated fields
389389
const user: any = await User.findOne({ email }).populate({
390390
path: 'cohort',
@@ -400,29 +400,30 @@ const resolvers: any = {
400400
strictPopulate: false,
401401
},
402402
},
403-
})
404-
403+
});
404+
405405
// Check if user exists
406406
if (!user) {
407407
throw new GraphQLError('Invalid credentials', {
408408
extensions: { code: 'AccountNotFound' },
409-
})
409+
});
410410
}
411+
411412
// Check if account is active
412413
if (user.status?.status !== 'active') {
413414
throw new GraphQLError(
414415
`Account is ${user.status?.status}. Contact admin.`,
415416
{
416417
extensions: { code: 'AccountInactive' },
417418
}
418-
)
419+
);
419420
}
420-
421+
421422
// Check if two-factor authentication is enabled
422423
if (user.twoFactorAuth) {
423-
const otp = generateOtp() // Generate OTP
424-
const TwoWayVerificationToken = encodeOtpToToken(otp, email) // Encode OTP
425-
424+
const otp = generateOtp(); // Generate OTP
425+
const TwoWayVerificationToken = encodeOtpToToken(otp, email); // Encode OTP
426+
426427
// Send email with OTP
427428
await sendEmail(
428429
email,
@@ -431,52 +432,55 @@ const resolvers: any = {
431432
null,
432433
process.env.ADMIN_EMAIL,
433434
process.env.ADMIN_PASS
434-
)
435-
436-
// Return response with encoded OTP token and message
435+
);
436+
437+
// Save the Two-Way Verification Token to the database
438+
user.TwoWayVerificationToken = TwoWayVerificationToken;
439+
await user.save();
440+
441+
// Return a response without exposing the token
437442
return {
438443
message: 'Check your email for the OTP code.',
439444
otpRequired: true,
440-
TwoWayVerificationToken,
441445
user: { id: user._id },
442-
}
446+
};
443447
} else {
444448
// Verify password if 2FA is not enabled
445-
const passwordMatch = await user?.checkPass(password)
449+
const passwordMatch = await user?.checkPass(password);
446450
if (!passwordMatch) {
447451
throw new GraphQLError('Invalid credentials', {
448452
extensions: { code: 'InvalidCredential' },
449-
})
453+
});
450454
}
451-
455+
452456
// Generate token for authenticated user
453457
const token = jwt.sign(
454458
{ userId: user._id, role: user._doc?.role || 'user' },
455459
SECRET,
456460
{ expiresIn: '2h' }
457-
)
458-
459-
460-
const geoData = await logGeoActivity(user, clientIpAdress) // Log activity
461-
462-
const organizationName = user.organizations[0]
461+
);
462+
463+
const geoData = await logGeoActivity(user, clientIpAdress); // Log activity
464+
465+
const organizationName = user.organizations[0];
463466
if (organizationName) {
464467
const location =
465468
geoData && geoData.city && geoData.country_name
466469
? `${geoData.city}-${geoData.country_name}`
467-
: null
468-
await loginsCount(organizationName, location)
470+
: null;
471+
await loginsCount(organizationName, location);
469472
}
470-
473+
471474
// Return token and user data
472475
return {
473476
token,
474477
user: user.toJSON(),
475478
geoData,
476479
otpRequired: false,
477-
}
480+
};
478481
}
479482
},
483+
480484
async deleteUser(_: any, { input }: any, context: { userId: any }) {
481485
const requester = await User.findById(context.userId)
482486
if (!requester) {

src/schema/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ const Schema = gql`
8181
status: StatusType
8282
ratings: [Rating]
8383
twoFactorAuth:Boolean!
84+
TwoWayVerificationToken:String
8485
}
8586
input RegisterInput {
8687
email: String!
@@ -157,7 +158,7 @@ const Schema = gql`
157158
user: User
158159
message:String
159160
otpRequired:Boolean
160-
TwoWayVerificationToken:String
161+
161162
}
162163
type OrgLogin {
163164
token: String
@@ -314,9 +315,9 @@ const Schema = gql`
314315
315316
type Mutation {
316317
enableTwoFactorAuth(email: String!): String
317-
oneTimeCode: String!
318+
# //TwoWayVerificationToken: String!
318319
disableTwoFactorAuth(email: String!): String
319-
loginWithTwoFactorAuthentication(email: String!, otp: String!, TwoWayVerificationToken: String!): LoginResponse!
320+
loginWithTwoFactorAuthentication(email: String!, otp: String!): LoginResponse!
320321
createUserRole(name: String!): UserRole!
321322
uploadResume(userId: ID!, resume: String!): Profile
322323
dropTTLUser(email: String!, reason: String!): String!

src/utils/2WayAuthentication.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export function generateOtp(length = 6): string {
88
}
99
return otp
1010
}
11-
const SECRET: string = process.env.SECRET ?? 'test_secret'
11+
const SECRET = (process.env.SECRET as string) || 'mysq_unique_secret'
1212

1313
export function encodeOtpToToken(otp: string, email: string): string {
1414
const payload = { otp, email }

0 commit comments

Comments
 (0)