Skip to content
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

Feat/2fa #9634

Merged
merged 6 commits into from
Jan 24, 2025
Merged

Feat/2fa #9634

merged 6 commits into from
Jan 24, 2025

Conversation

nicolasrouanne
Copy link
Contributor

Description

Closes #7003
Implements 2FA with TOTP.

Warning

This is a draft PR, with only partial changes, made as a mean of discussion about #7003 (it's easier to reason about real code)

Behaviour

  • a totpSecret is stored for each user
  • use otplib to create a QR code and to validate an otp against an totpSecret (great demo website by otplib)
  • OTP is asked upon each login attempt

Source

Inspired by:

Remaining

  • encrypt totpSecret at rest using a symetric algorithm

Comment on lines 68 to 72
@Field(() => [TwoFactorMethod], { nullable: true })
@OneToMany(
() => TwoFactorMethod,
(twoFactorMethod) => twoFactorMethod.userWorkspace,
)
twoFactorMethods: Relation<TwoFactorMethod[]>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the twoFactorMethods relation in UserWorkspace, but for some reason, typeorm can't seem to find the association

/Users/nicolasrouanne/dev/twenty/src/metadata-builder/EntityMetadataBuilder.ts:1111
        entityMetadata.relations.forEach((relation) => {
                                 ^
TypeORMError: Entity metadata for UserWorkspace#twoFactorMethods was not found. Check if you specified a correct entity object and if it's connected in the connection options.
    at /Users/nicolasrouanne/dev/twenty/src/metadata-builder/EntityMetadataBuilder.ts:1121:23

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a quick chat with @FelixMalfait on Discord, it's probably a module-wiring issue. We are going to add the TwoFactorModule to AuthModule, to ensure it's properly loaded

Copy link
Contributor Author

@nicolasrouanne nicolasrouanne Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually fixed by @FelixMalfait on 59f84ba 🙏🏻

- also add a `is2FAEnabled` column to `userWorkspace` entity
@FelixMalfait FelixMalfait marked this pull request as ready for review January 24, 2025 09:43
Copy link
Member

@FelixMalfait FelixMalfait left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good first step! Thank you!!!

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary

This PR introduces the initial database and service layer for Two-Factor Authentication (2FA) using TOTP (Time-based One-Time Password).

  • Missing critical totpSecret column in TwoFactorMethod entity and migration for storing TOTP secrets
  • TwoFactorMethodService needs additional methods for TOTP validation and secret generation using the otplib package
  • Consider adding status/type fields to TwoFactorMethod entity to track enabled/disabled state and authentication method type
  • Security concern: Need to implement encryption for TOTP secrets at rest as noted in PR description
  • UserWorkspace to TwoFactorMethod relationship allows multiple 2FA methods per user, but business logic for handling multiple methods isn't implemented yet

💡 (3/5) Reply to the bot's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!

8 file(s) reviewed, 8 comment(s)
Edit PR Review Bot Settings | Greptile

Comment on lines +25 to +27
async findAll(): Promise<TwoFactorMethod[]> {
return this.twoFactorMethodRepository.find();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: findAll() without any filters could expose all 2FA methods. Consider adding workspace-level filtering for security.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole service file was added as a proof of concept. It might be nice to clean it a bit. Can do it this afternoon.

Comment on lines +33 to +35
async remove(id: string): Promise<void> {
await this.twoFactorMethodRepository.delete(id);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: No verification that the caller has permission to remove the 2FA method. Add authorization check.

Comment on lines +64 to +68
@OneToMany(
() => TwoFactorMethod,
(twoFactorMethod) => twoFactorMethod.userWorkspace,
)
twoFactorMethods: Relation<TwoFactorMethod[]>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: missing @field decorator for GraphQL exposure of twoFactorMethods - this field won't be queryable via GraphQL

Comment on lines +64 to +68
@OneToMany(
() => TwoFactorMethod,
(twoFactorMethod) => twoFactorMethod.userWorkspace,
)
twoFactorMethods: Relation<TwoFactorMethod[]>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: consider adding cascade option to automatically handle deletion of associated TwoFactorMethod records


@Entity({ name: 'twoFactorMethod', schema: 'core' })
@ObjectType('TwoFactorMethod')
export class TwoFactorMethod {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused, this table does provide any information? no type, token, etc. why is it useful then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because this PR is heavily WIP, nothing is really done. But:

  • the token will be derived from the Primary Key of this table
  • we only have one 2FA type which is TOTP, that's why we don't store it

Does it make sense?


import { Repository } from 'typeorm';

import { TwoFactorMethod } from './two-factor-method.entity';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

general about the naming: shouldn't we use two-factor-authentication instead of two-factor-method which seems more standard?

  • for data model: two-factor-method makes sense as a shortcut for two-factor-authentication-method
  • but for the module I would name it two-factor-authentication

return this.twoFactorMethodRepository.save(twoFactorMethod);
}

async findAll(): Promise<TwoFactorMethod[]> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't exist IMO:

  • if this endpoint is meant to be part of the "engine api" long term (usable by business modules), I don't think we should authorize such a getter (findAll)
  • if this endpoint is meant to be used within the engine, it's not useful as we can just call the ORM direclty and there is no additional business logic to wrap

}

async findOne(id: string): Promise<TwoFactorMethod | null> {
return this.twoFactorMethodRepository.findOne({ where: { id } });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

}

async remove(id: string): Promise<void> {
await this.twoFactorMethodRepository.delete(id);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

@nicolasrouanne
Copy link
Contributor Author

@charlesBochet I see your review and I'm sorry about a few things:

  • this PR wasn't really meant for review, apart from the general direction of creating a TwoFactorMethod entity and link it to the UserWorkspace
  • I haven't pushed it through because I was blocked by a module-wiring issue, and @FelixMalfait fixed it in 59f84ba
  • I added some dummy generated service methods and will only leave the relevant ones: create; Maybe findOne is relevant, or you prefer calling the ORM directly?

Now that I'm unblocked, I can add unit tests, auth logic in the resolvers, and remove all the cruft before merging. Frontend can be done in a separate task, but this is probably too raw right now to merge as is.

@FelixMalfait
Copy link
Member

Sorry @nicolasrouanne I think there was a miscommunication between me and @charlesBochet - agree with your points and keep the discussed approach which is to do partial merges along the way

@charlesBochet
Copy link
Member

Ok, let's merge it, my commentaries are non blocking. More than happy with the iterative paths as long as the comments are addressed or discussed in upcoming PRs

@charlesBochet charlesBochet merged commit 17def22 into twentyhq:main Jan 24, 2025
30 checks passed
Copy link
Contributor

Fails
🚫

node failed.

Log

�[31mError: �[39m SyntaxError: Unexpected token C in JSON at position 0
    at JSON.parse (<anonymous>)
�[90m    at parseJSONFromBytes (node:internal/deps/undici/undici:5584:19)�[39m
�[90m    at successSteps (node:internal/deps/undici/undici:5555:27)�[39m
�[90m    at fullyReadBody (node:internal/deps/undici/undici:1665:9)�[39m
�[90m    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)�[39m
�[90m    at async specConsumeBody (node:internal/deps/undici/undici:5564:7)�[39m
danger-results://tmp/danger-results-717cd744.json

Generated by 🚫 dangerJS against 0d6291d

DeepaPrasanna pushed a commit to DeepaPrasanna/twenty that referenced this pull request Jan 27, 2025
# Description
Closes twentyhq#7003 
Implements 2FA with TOTP. 

>[!WARNING]
> This is a draft PR, with only partial changes, made as a mean of
discussion about twentyhq#7003 (it's easier to reason about real code)

## Behaviour
- a `totpSecret` is stored for each user
- use [`otplib`](https://github.com/yeojz/otplib/tree/master) to create
a QR code and to validate an `otp` against an `totpSecret` (great [demo
website](https://otplib.yeojz.dev/) by `otplib`)
- OTP is asked upon each login attempt

## Source
Inspired by:
- [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238)
- Cal.com's implementation of 2FA, namely
- [raising a
401](https://github.com/calcom/cal.com/blob/c21ba636d2bec4ed55775f0b058f70fdc371c410/packages/features/auth/lib/next-auth-options.ts#L188-L190)
when missing OTP and 2FA is enabled, with a [specific error
code](https://github.com/calcom/cal.com/blob/c21ba636d2bec4ed55775f0b058f70fdc371c410/packages/features/auth/lib/ErrorCode.ts#L9)
- [catching the
401](https://github.com/calcom/cal.com/blob/c21ba636d2bec4ed55775f0b058f70fdc371c410/apps/web/modules/auth/login-view.tsx#L160)
in the frontend and
[displaying](https://github.com/calcom/cal.com/blob/c21ba636d2bec4ed55775f0b058f70fdc371c410/apps/web/modules/auth/login-view.tsx#L276)
the OTP input

## Remaining
- [ ] encrypt `totpSecret` at rest using a symetric algorithm

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2FA
3 participants