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

Multi-factor authentication #796

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open

Multi-factor authentication #796

wants to merge 20 commits into from

Conversation

ozsay
Copy link
Contributor

@ozsay ozsay commented Sep 4, 2019

an alternative way for #777 of doing MFA following the auth0 way.

example login flow of user:

  1. /loginWithService('password', { username, password })
    1. if the user has MFA enabled will return an MFA token & challenges (array of challenges: sms|email|...) to complete
    2. otherwise will return the login response as before.
  2. /performMfaChallenge('sms', mfaToken, { codeFromSms })
    1. upon success returns the mfaLoginToken.
  3. /loginWithService('mfa', { mfaLoginToken })
    1. validates the mfaLoginToken and returns login response.

db:

  • add database interface
  • implement database interface for mongo
  • implement database interface for manager
  • implement database interface for typeorm

server:

  • implement login process
  • add unit tests

rest api:

  • client
  • server

graphql api:

  • client
  • server

client:

  • add api of transport

@pradel
Copy link
Member

pradel commented Sep 4, 2019

@ozsay since it's pretty hard to have a generic loginWithService and performMfaChallenge mutation (you can pass an email + password or a sms code or whatever for other services) what do you think about adding a JSON scalar for graphql that would allow any inputs?

}

private async createMfaLoginProcess(user: User): Promise<MFALoginResult> {
const loginToken = generateRandomToken();
Copy link
Member

Choose a reason for hiding this comment

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

@ozsay I don't exactly understand why do we need this loginToken, can you explain a bit why is it really needed please? :)

Copy link
Contributor Author

@ozsay ozsay Sep 9, 2019

Choose a reason for hiding this comment

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

the user can only log in if he has the loginToken which is provided to him upon successfully completing the challenge.

Did I over-think this? :)

Copy link
Member

Choose a reason for hiding this comment

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

Since the mfaToken is already a random token I feel like having this loginToken does not add more security no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

following auth0, the last step of the mfa is to call oauth/token (in our case loginWithService) with a token. I can't use the mfaToken here because it's provided to the client before it does the challenge. that's why I have the extra token.

if we want, we can skip this step and have the performMfaChallenge create the session and return the access and refresh tokens.

@codecov
Copy link

codecov bot commented Sep 11, 2019

Codecov Report

❗ No coverage uploaded for pull request base (master@0470532). Click here to learn what that means.
The diff coverage is 99.23%.

Impacted file tree graph

@@           Coverage Diff            @@
##             master    #796   +/-   ##
========================================
  Coverage          ?   95.5%           
========================================
  Files             ?      83           
  Lines             ?    1937           
  Branches          ?     401           
========================================
  Hits              ?    1850           
  Misses            ?      79           
  Partials          ?       8
Impacted Files Coverage Δ
...raphql-api/src/modules/accounts/schema/mutation.ts 100% <ø> (ø)
...s/graphql-api/src/modules/accounts/schema/types.ts 66.66% <ø> (ø)
...ges/database-typeorm/src/entity/MfaLoginAttempt.ts 100% <100%> (ø)
packages/rest-express/src/express-middleware.ts 100% <100%> (ø)
packages/client/src/accounts-client.ts 98.9% <100%> (ø)
...t-express/src/endpoints/oauth/provider-callback.ts 95.23% <100%> (ø)
packages/client-password/src/client-password.ts 100% <100%> (ø)
...-api/src/modules/accounts/resolvers/loginResult.ts 100% <100%> (ø)
packages/database-mongo/src/mongo.ts 99.36% <100%> (ø)
...hql-api/src/modules/accounts/resolvers/mutation.ts 94.44% <100%> (ø)
... and 8 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0470532...06c8d67. Read the comment docs.

@@ -21,6 +21,7 @@ const start = async () => {
const accountsDb = new DatabaseManager({
sessionStorage: userStorage,
userStorage,
mfaLoginAttemptsStorage: userStorage,
Copy link
Member

Choose a reason for hiding this comment

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

Make sure mfaLoginAttemptsStorage is optional so that it would not break the current usages. Also no need for it when MFA is not configured

@ozsay ozsay changed the title [WIP] Multi-factor authentication Multi-factor authentication Sep 16, 2019
@ozsay ozsay requested a review from davidyaha September 16, 2019 12:38
@davidyaha davidyaha requested a review from pradel September 16, 2019 14:09
Copy link
Member

@pradel pradel left a comment

Choose a reason for hiding this comment

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

The pr looks good 👍
A few comments I think would be great before merging.

  • Do we want to have the ability to limit the sms code to 3 attempts for example?
  • I will try to see if I can bypass the loginToken to see if we really need it or not :)
  • should the authenticator api a part of this pull request or better to split in another one?
  • mfaChallenges as a an array of string sounds not really extendable, where will you store the number associated to the user or other?
  • on the client isn't it better to return the mfaToken to the user instead of setting it in the local storage?

@@ -1 +1,11 @@
export const LoginResult = {};

export const LoginWithServiceResult = {
__resolveType(obj: any) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we type this one with the generated types?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not sure what you mean

Copy link
Member

Choose a reason for hiding this comment

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

Instead of using any import the types generated by graphql-codegen :)

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 hope I understood you correctly :)
52ac287

@@ -25,7 +25,7 @@ export class AccountsClientPassword {
/**
* Log the user in with a password.
*/
public async login(user: any): Promise<LoginResult> {
public async login(user: any): Promise<LoginResult | Omit<MFALoginResult, 'mfaToken'>> {
Copy link
Member

Choose a reason for hiding this comment

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

Why don't we return the mfaToken there?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

mainly, to simplify the usage


import { Configuration } from './types/configuration';

export class DatabaseManager implements DatabaseInterface {
private userStorage: DatabaseInterface;
private sessionStorage: DatabaseInterface | DatabaseInterfaceSessions;
private mfaLoginAttemptsStorage?: DatabaseInterface | DatabaseInterfaceMfaLoginAttempts;
Copy link
Member

Choose a reason for hiding this comment

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

Should this be a required field?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@davidyaha pointed out that people might not want mfa at all, so I made it optional

@ozsay
Copy link
Contributor Author

ozsay commented Sep 23, 2019

@pradel

  • Do we want to have the ability to limit the sms code to 3 attempts for example?

I think it's related to the #776 PR. The mfa process is kinda agnostic of which authenticators are being used.

  • should the authenticator api a part of this pull request or better to split in another one?

I don't think any changes are required to the authenticators API. I will test an alpha version this week, but I believe that it should work.

  • mfaChallenges as a an array of string sounds not really extendable, where will you store the number associated to the user or other?

currently, it will allow authenticating with any of the challenges in the array (only 1 challenge completion is sufficient for login). We can extend it in the future for more complex scenarios. I think the basic scenarios are covered here.

  • on the client isn't it better to return the mfaToken to the user instead of setting it in the local storage?

I can return it but I don't see any client usage of it, that's why I've "hidden" it inside accounts

thanks for the comments @pradel :)

@pradel pradel mentioned this pull request Sep 23, 2019
2 tasks
@acomito
Copy link

acomito commented Oct 29, 2019

Will this allow for 2-step authentication through email, or just SMS?

Also is this in the docs yet 😈?

@davidyaha
Copy link
Member

@ozsay Are we missing something here? Could this be merged?

@pradel
Copy link
Member

pradel commented Nov 29, 2019

@davidyaha I proposed another design for the MFA implementation which will allow us to support more authenticators. I posted the architecture design on slack and I started an implementation on this pr #813

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants