-
Notifications
You must be signed in to change notification settings - Fork 13.8k
feat: Phising resistant MFA #39760
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: Phising resistant MFA #39760
Changes from all commits
Commits
Show all changes
54 commits
Select commit
Hold shift + click to select a range
2243b8a
add deprecation warning
yash-rajpal b0cd754
chore: Passport login UI (#39848)
yash-rajpal 6889133
feat: Configure Passport (#39604)
yash-rajpal a85ee03
merge develop and fix conflicts
yash-rajpal 9313bce
Merge branch 'develop' into feat/phishing-resistant-mfa
yash-rajpal b1b3d94
fix express user typing
yash-rajpal ae49afb
use site url instead of meteor absolute url
yash-rajpal ed170a8
fix async login token creation
yash-rajpal c558ae5
oops
yash-rajpal 5628302
merge develop and fix conflicts
yash-rajpal a01dcdb
failure debugging
yash-rajpal 3ad63c7
use flash for error debugging
yash-rajpal 55d5a48
merge develop
yash-rajpal be66f93
lint
yash-rajpal 4e185c8
chore: Passport Google OAuth (#40222)
yash-rajpal c874372
trust proxy
yash-rajpal 7294f3c
merge and conflicts
yash-rajpal a0b3c2c
merge develop and fix conflicts
yash-rajpal 500e008
feat: Custom OAuth using Passport (#40203)
yash-rajpal aa3602b
custom OAuth auto publish fields
yash-rajpal 6e42e87
chore: LinkedIn custom OAuth (#40594)
yash-rajpal 9a2d2b0
feat: Deeplink login for Mobile and Desktop clients (#40601)
yash-rajpal 9cda3cf
feat: Secret setting for express-sessions secret (#40602)
yash-rajpal 93af0a8
chore: Passport Nextcloud OAuth (#40591)
yash-rajpal 33e32b9
merge develop and fix conflict
yash-rajpal c7f7306
chore: Passport Meteor OAuth (#40595)
yash-rajpal 80a082b
me stupid
yash-rajpal afda09b
chore: Passport Github Enterprise OAuth (#40608)
yash-rajpal dc44d7c
chore: Passport Gitlab OAuth (#40609)
yash-rajpal f052ca8
chore: Passport Dolphin (#40611)
yash-rajpal 02fa86b
chore: Passport Drupal OAuth (#40610)
yash-rajpal f709064
chore: Passport Wordpress OAuth (#40593)
yash-rajpal 9509e9b
add rate limiter
yash-rajpal d394e16
exclude saml, cas, ldap from login flow
yash-rajpal df761f0
express sessions mongo store
yash-rajpal 30c5ee1
rate limit only oauth req
yash-rajpal 161212e
chore: Passport Twitter OAuth (#40563)
yash-rajpal 77de0c2
feat: New 2FA flow for passport OAuth (#40106)
yash-rajpal de91e8f
Merge branch 'develop' into feat/phishing-resistant-mfa
yash-rajpal 732fc12
chore: Passport apple oauth (#40481)
yash-rajpal 20db627
Merge branch 'develop' into feat/phishing-resistant-mfa
yash-rajpal bdb7a2e
handle undefined client secrets
yash-rajpal fb20370
Merge branch 'develop' into feat/phishing-resistant-mfa
yash-rajpal 540c2b6
fix oauth e2e tests
yash-rajpal 88a8099
isolate oauth middlewares
yash-rajpal 1bb9ad4
Merge branch 'develop' into feat/phishing-resistant-mfa
yash-rajpal 5f2b140
spread profile for all fields
yash-rajpal 8a9a965
restore /_oauth/provider callback structure
yash-rajpal c1525a9
fix review
yash-rajpal 873daad
add changeset
yash-rajpal d360cd2
fix unused tokenObject
yash-rajpal 0b99664
remove unused deps
yash-rajpal 396cf66
improve login redirect url handling
yash-rajpal 2700a46
check for email method before sending email code
yash-rajpal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| --- | ||
| '@rocket.chat/web-ui-registration': minor | ||
| '@rocket.chat/model-typings': minor | ||
| '@rocket.chat/core-typings': minor | ||
| '@rocket.chat/rest-typings': minor | ||
| '@rocket.chat/desktop-api': minor | ||
| '@rocket.chat/models': minor | ||
| '@rocket.chat/i18n': minor | ||
| '@rocket.chat/meteor': minor | ||
| --- | ||
|
|
||
| ## Phishing-Resistant Multi-Factor Authentication | ||
|
|
||
| Introduces a more secure and reliable server-side OAuth authentication flow. | ||
|
|
||
| ### What’s New | ||
|
|
||
| - **Improved OAuth login security** | ||
| OAuth authentication now happens fully on the server, reducing the risk of token theft, phishing attacks, and client-side credential interception. | ||
|
|
||
| - **Built-in CSRF, state validation, and PKCE protection** | ||
| OAuth logins now include stronger protection against CSRF attacks, request tampering, and authorization code interception through secure state validation and PKCE support. | ||
|
|
||
| - **Improved two-step verification with OAuth logins** | ||
| Users with email or TOTP two-factor authentication enabled will now be asked to complete 2FA even when signing in with providers like Google, GitHub, GitLab, and others. | ||
|
|
||
| - **Improved mobile & desktop app login** | ||
| Mobile and desktop apps now support a smoother and more secure deep-link OAuth login flow. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import type { IUser } from '@rocket.chat/core-typings'; | ||
| import { TwoFactorChallenges } from '@rocket.chat/models'; | ||
|
|
||
| import { EmailCheck } from './EmailCheck'; | ||
|
|
||
| export class EmailCheckForOAuth extends EmailCheck { | ||
| public override readonly name = 'email-oauth'; | ||
|
|
||
| public readonly method = 'email'; | ||
|
|
||
| public async sendTwoFactorChallenge(user: IUser): Promise<string> { | ||
| const challengeId = await TwoFactorChallenges.createTwoFactorChallenge(user._id, 'email'); | ||
| await this.sendEmailCode(user); | ||
| return challengeId; | ||
| } | ||
|
|
||
| public async verifyEmailTwoFactorChallenge(user: IUser, challengeId: string, code: string): Promise<boolean> { | ||
| const challenge = await TwoFactorChallenges.findOneByPendingChallengeId(challengeId); | ||
| if (!challenge) { | ||
| return false; | ||
| } | ||
|
|
||
| if (challenge.expireAt && challenge.expireAt < new Date()) { | ||
| throw new Meteor.Error('error-challenge-expired', 'challenge expired'); | ||
| } | ||
|
|
||
| const isCodeValid = await this.verify(user, code); | ||
|
|
||
| if (!isCodeValid) { | ||
| return false; | ||
| } | ||
|
|
||
| await TwoFactorChallenges.removeByPendingChallengeId(challengeId); | ||
|
|
||
| return true; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import type { IUser } from '@rocket.chat/core-typings'; | ||
| import { TwoFactorChallenges } from '@rocket.chat/models'; | ||
|
|
||
| import { TOTPCheck } from './TOTPCheck'; | ||
|
|
||
| export class TOTPCheckForOAuth extends TOTPCheck { | ||
| public override readonly name = 'totp-oauth'; | ||
|
|
||
| public readonly method = 'totp'; | ||
|
|
||
| public async sendTwoFactorChallenge(user: IUser): Promise<string> { | ||
| return TwoFactorChallenges.createTwoFactorChallenge(user._id, 'totp'); | ||
| } | ||
|
|
||
| public async verifyEmailTwoFactorChallenge(user: IUser, challengeId: string, code: string): Promise<boolean> { | ||
| const challenge = await TwoFactorChallenges.findOneByPendingChallengeId(challengeId); | ||
| if (!challenge) { | ||
| return false; | ||
| } | ||
|
|
||
| if (challenge.expireAt && challenge.expireAt < new Date()) { | ||
| throw new Meteor.Error('error-challenge-expired', 'challenge expired'); | ||
| } | ||
|
|
||
| const isCodeValid = await this.verify(user, code); | ||
|
|
||
| if (!isCodeValid) { | ||
| return false; | ||
| } | ||
|
|
||
| await TwoFactorChallenges.removeByPendingChallengeId(challengeId); | ||
|
|
||
| return true; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import type { IMethodConnection } from '@rocket.chat/core-typings'; | ||
| import { TwoFactorChallenges } from '@rocket.chat/models'; | ||
| import { isTwoFactorChallengesSendEmailCodeParamsPOST, isTwoFactorChallengesVerifyChallengeParamsPOST } from '@rocket.chat/rest-typings'; | ||
| import { Accounts } from 'meteor/accounts-base'; | ||
|
|
||
| import { emailCheckForOAuth, getTwoFAMethodForOAuth } from '../../../../server/lib/oauth/twoFactorAuth'; | ||
| import { getUserForCheck, rememberAuthorizationByToken } from '../../../2fa/server/code'; | ||
| import { generateConnection } from '../ApiClass'; | ||
| import { API } from '../api'; | ||
|
|
||
| API.v1.addRoute( | ||
| 'twoFactorChallenges.sendEmailCode', | ||
| { validateParams: isTwoFactorChallengesSendEmailCodeParamsPOST, rateLimiterOptions: { intervalTimeInMS: 60000, numRequestsAllowed: 5 } }, | ||
| { | ||
| async post() { | ||
| const { challengeId } = this.bodyParams; | ||
|
|
||
| if (!challengeId) { | ||
| throw new Meteor.Error('error-parameter-required', 'challengeId is required'); | ||
| } | ||
|
|
||
| const challenge = await TwoFactorChallenges.findOneByPendingChallengeId(challengeId); | ||
|
|
||
| if (!challenge) { | ||
| throw new Meteor.Error('error-challenge-not-found', 'challenge not found'); | ||
| } | ||
|
|
||
| if (challenge.expireAt && challenge.expireAt < new Date()) { | ||
| throw new Meteor.Error('error-challenge-expired', 'challenge expired'); | ||
| } | ||
|
|
||
| if (challenge.method !== 'email') { | ||
| throw new Meteor.Error('error-invalid-challenge-method', 'invalid challenge method'); | ||
| } | ||
|
|
||
| const { userId } = challenge; | ||
|
|
||
| const user = await getUserForCheck(userId); | ||
|
|
||
| if (!user) { | ||
| throw new Meteor.Error('error-user-not-found', 'user not found'); | ||
| } | ||
|
|
||
| await emailCheckForOAuth.sendEmailCode(user); | ||
|
coderabbitai[bot] marked this conversation as resolved.
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
|
|
||
| return API.v1.success(); | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| API.v1.addRoute( | ||
| 'twoFactorChallenges.verifyChallenge', | ||
| { | ||
| validateParams: isTwoFactorChallengesVerifyChallengeParamsPOST, | ||
| rateLimiterOptions: { intervalTimeInMS: 60000, numRequestsAllowed: 5 }, | ||
| }, | ||
| { | ||
| async post() { | ||
| const { challengeId, code } = this.bodyParams; | ||
|
|
||
| if (!challengeId || !code) { | ||
| throw new Meteor.Error('error-parameter-required', 'challengeId and code are required'); | ||
| } | ||
|
|
||
| const challenge = await TwoFactorChallenges.findOneByPendingChallengeId(challengeId); | ||
|
yash-rajpal marked this conversation as resolved.
|
||
|
|
||
| if (!challenge) { | ||
| throw new Meteor.Error('error-challenge-not-found', 'challenge not found'); | ||
| } | ||
|
|
||
| const { userId } = challenge; | ||
|
|
||
| const user = await getUserForCheck(userId); | ||
|
|
||
| if (!user) { | ||
| throw new Meteor.Error('error-user-not-found', 'user not found'); | ||
| } | ||
|
|
||
| const twoFAMethod = getTwoFAMethodForOAuth(challenge.method); | ||
|
|
||
| const isCodeValid = await twoFAMethod.verifyEmailTwoFactorChallenge(user, challengeId, code); | ||
|
|
||
| if (!isCodeValid) { | ||
| const tooManyAttempts = await twoFAMethod.maxFaildedAttemtpsReached(user); | ||
| if (tooManyAttempts) { | ||
| await TwoFactorChallenges.removeByPendingChallengeId(challengeId); | ||
| throw new Meteor.Error('totp-max-attempts', 'TOTP Maximun Failed Attempts Reached'); | ||
| } | ||
| return API.v1.failure('error-invalid-code', 'Invalid code'); | ||
| } | ||
|
|
||
| const stampedToken = Accounts._generateStampedLoginToken(); | ||
|
|
||
| await Accounts._insertLoginToken(user._id, stampedToken); | ||
|
|
||
| const hashedToken = Accounts._hashLoginToken(stampedToken.token); | ||
|
|
||
| const connection = { | ||
| ...generateConnection(this.requestIp, this.request.headers), | ||
| token: hashedToken, | ||
| } as unknown as IMethodConnection; | ||
|
|
||
| // remember the 2FA authorization for the next requests | ||
| await rememberAuthorizationByToken(hashedToken, user._id, connection); | ||
|
|
||
| return API.v1.success({ | ||
| loginToken: stampedToken.token, | ||
| userId: user._id, | ||
| }); | ||
| }, | ||
| }, | ||
| ); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.