Skip to content

Commit a9d890c

Browse files
authored
feat(graphql-api): create mfa operations (#1048)
1 parent 8738f6e commit a9d890c

File tree

16 files changed

+399
-20
lines changed

16 files changed

+399
-20
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { AccountsMfa } from '@accounts/mfa';
2+
import { Mutation } from '../../../../src/modules/accounts-mfa/resolvers/mutation';
3+
4+
describe('accounts-mfa resolvers mutations', () => {
5+
const accountsMfaMock = {
6+
challenge: jest.fn(),
7+
associate: jest.fn(),
8+
associateByMfaToken: jest.fn(),
9+
};
10+
const injector = {
11+
get: jest.fn(() => accountsMfaMock),
12+
};
13+
const user = { id: 'idTest' };
14+
const infos = {
15+
ip: 'ipTest',
16+
userAgent: 'userAgentTest',
17+
};
18+
19+
beforeEach(() => {
20+
jest.clearAllMocks();
21+
});
22+
23+
describe('challenge', () => {
24+
it('should call associateByMfaToken', async () => {
25+
const mfaToken = 'mfaTokenTest';
26+
const authenticatorId = 'authenticatorIdTest';
27+
await Mutation.challenge!(
28+
{},
29+
{ mfaToken, authenticatorId },
30+
{ injector, infos } as any,
31+
{} as any
32+
);
33+
expect(injector.get).toHaveBeenCalledWith(AccountsMfa);
34+
expect(accountsMfaMock.challenge).toHaveBeenCalledWith(mfaToken, authenticatorId, infos);
35+
});
36+
});
37+
38+
describe('associate', () => {
39+
it('should throw if no user in context', async () => {
40+
await expect(Mutation.associate!({}, {} as any, {} as any, {} as any)).rejects.toThrowError(
41+
'Unauthorized'
42+
);
43+
});
44+
45+
it('should call associate', async () => {
46+
const type = 'typeTest';
47+
const params = 'paramsTest';
48+
await Mutation.associate!(
49+
{},
50+
{ type, params: params as any },
51+
{ user, injector, infos } as any,
52+
{} as any
53+
);
54+
expect(injector.get).toHaveBeenCalledWith(AccountsMfa);
55+
expect(accountsMfaMock.associate).toHaveBeenCalledWith(user.id, type, params, infos);
56+
});
57+
});
58+
59+
describe('associateByMfaToken', () => {
60+
it('should call associateByMfaToken', async () => {
61+
const mfaToken = 'mfaTokenTest';
62+
const type = 'typeTest';
63+
const params = 'paramsTest';
64+
await Mutation.associateByMfaToken!(
65+
{},
66+
{ mfaToken, type, params: params as any },
67+
{ injector, infos } as any,
68+
{} as any
69+
);
70+
expect(injector.get).toHaveBeenCalledWith(AccountsMfa);
71+
expect(accountsMfaMock.associateByMfaToken).toHaveBeenCalledWith(
72+
mfaToken,
73+
type,
74+
params,
75+
infos
76+
);
77+
});
78+
});
79+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { AccountsMfa } from '@accounts/mfa';
2+
import { Query } from '../../../../src/modules/accounts-mfa/resolvers/query';
3+
4+
describe('accounts-mfa resolvers query', () => {
5+
const accountsMfaMock = {
6+
findUserAuthenticators: jest.fn(),
7+
findUserAuthenticatorsByMfaToken: jest.fn(),
8+
};
9+
const injector = {
10+
get: jest.fn(() => accountsMfaMock),
11+
};
12+
const user = { id: 'idTest' };
13+
14+
beforeEach(() => {
15+
jest.clearAllMocks();
16+
});
17+
18+
describe('authenticators', () => {
19+
it('should throw if no user in context', async () => {
20+
await expect(Query.authenticators!({}, {}, {} as any, {} as any)).rejects.toThrowError(
21+
'Unauthorized'
22+
);
23+
});
24+
25+
it('should call findUserAuthenticators', async () => {
26+
await Query.authenticators!({}, {}, { injector, user } as any, {} as any);
27+
expect(injector.get).toHaveBeenCalledWith(AccountsMfa);
28+
expect(accountsMfaMock.findUserAuthenticators).toHaveBeenCalledWith(user.id);
29+
});
30+
});
31+
32+
describe('authenticatorsByMfaToken', () => {
33+
it('should call findUserAuthenticators', async () => {
34+
const mfaToken = 'mfaTokenTest';
35+
await Query.authenticatorsByMfaToken!({}, { mfaToken }, { injector } as any, {} as any);
36+
expect(injector.get).toHaveBeenCalledWith(AccountsMfa);
37+
expect(accountsMfaMock.findUserAuthenticatorsByMfaToken).toHaveBeenCalledWith(mfaToken);
38+
});
39+
});
40+
});

packages/graphql-api/__tests__/modules/accounts/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import { print } from 'graphql';
22
import { AccountsModule } from '../../../src/modules/accounts/index';
33

44
const accountsServer = {
5-
getServices: () => ({
6-
password: {},
7-
}),
5+
getService: () => ({}),
86
};
97

108
describe('AccountsModule', () => {

packages/graphql-api/introspection.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/graphql-api/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@
3333
},
3434
"homepage": "https://github.com/js-accounts/graphql-api",
3535
"peerDependencies": {
36-
"@accounts/password": "^0.28.0",
37-
"@accounts/server": "^0.28.0",
38-
"@accounts/types": "^0.28.0",
36+
"@accounts/mfa": "^0.29.0",
37+
"@accounts/password": "^0.29.0",
38+
"@accounts/server": "^0.29.0",
39+
"@accounts/types": "^0.29.0",
3940
"@graphql-modules/core": "0.7.17",
4041
"graphql-tag": "^2.10.0",
4142
"graphql-tools": "^5.0.0"
@@ -46,6 +47,7 @@
4647
"tslib": "2.0.1"
4748
},
4849
"devDependencies": {
50+
"@accounts/mfa": "^0.29.0",
4951
"@accounts/password": "^0.29.0",
5052
"@accounts/server": "^0.29.0",
5153
"@accounts/types": "^0.29.0",

packages/graphql-api/src/models.ts

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ export type Scalars = {
1414
};
1515

1616

17+
export type AssociateParamsInput = {
18+
_?: Maybe<Scalars['String']>;
19+
};
20+
21+
export type AssociationResult = OtpAssociationResult;
22+
1723
export type AuthenticateParamsInput = {
1824
access_token?: Maybe<Scalars['String']>;
1925
access_token_secret?: Maybe<Scalars['String']>;
@@ -25,6 +31,16 @@ export type AuthenticateParamsInput = {
2531

2632
export type AuthenticationResult = LoginResult | MultiFactorResult;
2733

34+
export type Authenticator = {
35+
__typename?: 'Authenticator';
36+
id?: Maybe<Scalars['ID']>;
37+
type?: Maybe<Scalars['String']>;
38+
active?: Maybe<Scalars['Boolean']>;
39+
activatedAt?: Maybe<Scalars['String']>;
40+
};
41+
42+
export type ChallengeResult = DefaultChallengeResult;
43+
2844
export type CreateUserInput = {
2945
username?: Maybe<Scalars['String']>;
3046
email?: Maybe<Scalars['String']>;
@@ -37,6 +53,12 @@ export type CreateUserResult = {
3753
loginResult?: Maybe<LoginResult>;
3854
};
3955

56+
export type DefaultChallengeResult = {
57+
__typename?: 'DefaultChallengeResult';
58+
mfaToken?: Maybe<Scalars['String']>;
59+
authenticatorId?: Maybe<Scalars['String']>;
60+
};
61+
4062
export type EmailRecord = {
4163
__typename?: 'EmailRecord';
4264
address?: Maybe<Scalars['String']>;
@@ -79,6 +101,9 @@ export type Mutation = {
79101
changePassword?: Maybe<Scalars['Boolean']>;
80102
twoFactorSet?: Maybe<Scalars['Boolean']>;
81103
twoFactorUnset?: Maybe<Scalars['Boolean']>;
104+
challenge?: Maybe<ChallengeResult>;
105+
associate?: Maybe<AssociationResult>;
106+
associateByMfaToken?: Maybe<AssociationResult>;
82107
impersonate?: Maybe<ImpersonateReturn>;
83108
refreshTokens?: Maybe<LoginResult>;
84109
logout?: Maybe<Scalars['Boolean']>;
@@ -135,6 +160,25 @@ export type MutationTwoFactorUnsetArgs = {
135160
};
136161

137162

163+
export type MutationChallengeArgs = {
164+
mfaToken: Scalars['String'];
165+
authenticatorId: Scalars['String'];
166+
};
167+
168+
169+
export type MutationAssociateArgs = {
170+
type: Scalars['String'];
171+
params?: Maybe<AssociateParamsInput>;
172+
};
173+
174+
175+
export type MutationAssociateByMfaTokenArgs = {
176+
mfaToken: Scalars['String'];
177+
type: Scalars['String'];
178+
params?: Maybe<AssociateParamsInput>;
179+
};
180+
181+
138182
export type MutationImpersonateArgs = {
139183
accessToken: Scalars['String'];
140184
impersonated: ImpersonationUserIdentityInput;
@@ -158,12 +202,25 @@ export type MutationVerifyAuthenticationArgs = {
158202
params: AuthenticateParamsInput;
159203
};
160204

205+
export type OtpAssociationResult = {
206+
__typename?: 'OTPAssociationResult';
207+
mfaToken?: Maybe<Scalars['String']>;
208+
authenticatorId?: Maybe<Scalars['String']>;
209+
};
210+
161211
export type Query = {
162212
__typename?: 'Query';
163213
twoFactorSecret?: Maybe<TwoFactorSecretKey>;
214+
authenticators?: Maybe<Array<Maybe<Authenticator>>>;
215+
authenticatorsByMfaToken?: Maybe<Array<Maybe<Authenticator>>>;
164216
getUser?: Maybe<User>;
165217
};
166218

219+
220+
export type QueryAuthenticatorsByMfaTokenArgs = {
221+
mfaToken: Scalars['String'];
222+
};
223+
167224
export type Tokens = {
168225
__typename?: 'Tokens';
169226
refreshToken?: Maybe<Scalars['String']>;
@@ -274,16 +331,22 @@ export type ResolversTypes = {
274331
Query: ResolverTypeWrapper<{}>;
275332
TwoFactorSecretKey: ResolverTypeWrapper<TwoFactorSecretKey>;
276333
String: ResolverTypeWrapper<Scalars['String']>;
277-
User: ResolverTypeWrapper<User>;
334+
Authenticator: ResolverTypeWrapper<Authenticator>;
278335
ID: ResolverTypeWrapper<Scalars['ID']>;
279-
EmailRecord: ResolverTypeWrapper<EmailRecord>;
280336
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
337+
User: ResolverTypeWrapper<User>;
338+
EmailRecord: ResolverTypeWrapper<EmailRecord>;
281339
Mutation: ResolverTypeWrapper<{}>;
282340
CreateUserInput: CreateUserInput;
283341
CreateUserResult: ResolverTypeWrapper<CreateUserResult>;
284342
LoginResult: ResolverTypeWrapper<LoginResult>;
285343
Tokens: ResolverTypeWrapper<Tokens>;
286344
TwoFactorSecretKeyInput: TwoFactorSecretKeyInput;
345+
ChallengeResult: ResolversTypes['DefaultChallengeResult'];
346+
DefaultChallengeResult: ResolverTypeWrapper<DefaultChallengeResult>;
347+
AssociateParamsInput: AssociateParamsInput;
348+
AssociationResult: ResolversTypes['OTPAssociationResult'];
349+
OTPAssociationResult: ResolverTypeWrapper<OtpAssociationResult>;
287350
ImpersonationUserIdentityInput: ImpersonationUserIdentityInput;
288351
ImpersonateReturn: ResolverTypeWrapper<ImpersonateReturn>;
289352
AuthenticateParamsInput: AuthenticateParamsInput;
@@ -297,16 +360,22 @@ export type ResolversParentTypes = {
297360
Query: {};
298361
TwoFactorSecretKey: TwoFactorSecretKey;
299362
String: Scalars['String'];
300-
User: User;
363+
Authenticator: Authenticator;
301364
ID: Scalars['ID'];
302-
EmailRecord: EmailRecord;
303365
Boolean: Scalars['Boolean'];
366+
User: User;
367+
EmailRecord: EmailRecord;
304368
Mutation: {};
305369
CreateUserInput: CreateUserInput;
306370
CreateUserResult: CreateUserResult;
307371
LoginResult: LoginResult;
308372
Tokens: Tokens;
309373
TwoFactorSecretKeyInput: TwoFactorSecretKeyInput;
374+
ChallengeResult: ResolversParentTypes['DefaultChallengeResult'];
375+
DefaultChallengeResult: DefaultChallengeResult;
376+
AssociateParamsInput: AssociateParamsInput;
377+
AssociationResult: ResolversParentTypes['OTPAssociationResult'];
378+
OTPAssociationResult: OtpAssociationResult;
310379
ImpersonationUserIdentityInput: ImpersonationUserIdentityInput;
311380
ImpersonateReturn: ImpersonateReturn;
312381
AuthenticateParamsInput: AuthenticateParamsInput;
@@ -319,16 +388,38 @@ export type AuthDirectiveArgs = { };
319388

320389
export type AuthDirectiveResolver<Result, Parent, ContextType = any, Args = AuthDirectiveArgs> = DirectiveResolverFn<Result, Parent, ContextType, Args>;
321390

391+
export type AssociationResultResolvers<ContextType = any, ParentType extends ResolversParentTypes['AssociationResult'] = ResolversParentTypes['AssociationResult']> = {
392+
__resolveType: TypeResolveFn<'OTPAssociationResult', ParentType, ContextType>;
393+
};
394+
322395
export type AuthenticationResultResolvers<ContextType = any, ParentType extends ResolversParentTypes['AuthenticationResult'] = ResolversParentTypes['AuthenticationResult']> = {
323396
__resolveType: TypeResolveFn<'LoginResult' | 'MultiFactorResult', ParentType, ContextType>;
324397
};
325398

399+
export type AuthenticatorResolvers<ContextType = any, ParentType extends ResolversParentTypes['Authenticator'] = ResolversParentTypes['Authenticator']> = {
400+
id?: Resolver<Maybe<ResolversTypes['ID']>, ParentType, ContextType>;
401+
type?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
402+
active?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
403+
activatedAt?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
404+
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
405+
};
406+
407+
export type ChallengeResultResolvers<ContextType = any, ParentType extends ResolversParentTypes['ChallengeResult'] = ResolversParentTypes['ChallengeResult']> = {
408+
__resolveType: TypeResolveFn<'DefaultChallengeResult', ParentType, ContextType>;
409+
};
410+
326411
export type CreateUserResultResolvers<ContextType = any, ParentType extends ResolversParentTypes['CreateUserResult'] = ResolversParentTypes['CreateUserResult']> = {
327412
userId?: Resolver<Maybe<ResolversTypes['ID']>, ParentType, ContextType>;
328413
loginResult?: Resolver<Maybe<ResolversTypes['LoginResult']>, ParentType, ContextType>;
329414
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
330415
};
331416

417+
export type DefaultChallengeResultResolvers<ContextType = any, ParentType extends ResolversParentTypes['DefaultChallengeResult'] = ResolversParentTypes['DefaultChallengeResult']> = {
418+
mfaToken?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
419+
authenticatorId?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
420+
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
421+
};
422+
332423
export type EmailRecordResolvers<ContextType = any, ParentType extends ResolversParentTypes['EmailRecord'] = ResolversParentTypes['EmailRecord']> = {
333424
address?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
334425
verified?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
@@ -364,15 +455,26 @@ export type MutationResolvers<ContextType = any, ParentType extends ResolversPar
364455
changePassword?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType, RequireFields<MutationChangePasswordArgs, 'oldPassword' | 'newPassword'>>;
365456
twoFactorSet?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType, RequireFields<MutationTwoFactorSetArgs, 'secret' | 'code'>>;
366457
twoFactorUnset?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType, RequireFields<MutationTwoFactorUnsetArgs, 'code'>>;
458+
challenge?: Resolver<Maybe<ResolversTypes['ChallengeResult']>, ParentType, ContextType, RequireFields<MutationChallengeArgs, 'mfaToken' | 'authenticatorId'>>;
459+
associate?: Resolver<Maybe<ResolversTypes['AssociationResult']>, ParentType, ContextType, RequireFields<MutationAssociateArgs, 'type'>>;
460+
associateByMfaToken?: Resolver<Maybe<ResolversTypes['AssociationResult']>, ParentType, ContextType, RequireFields<MutationAssociateByMfaTokenArgs, 'mfaToken' | 'type'>>;
367461
impersonate?: Resolver<Maybe<ResolversTypes['ImpersonateReturn']>, ParentType, ContextType, RequireFields<MutationImpersonateArgs, 'accessToken' | 'impersonated'>>;
368462
refreshTokens?: Resolver<Maybe<ResolversTypes['LoginResult']>, ParentType, ContextType, RequireFields<MutationRefreshTokensArgs, 'accessToken' | 'refreshToken'>>;
369463
logout?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
370464
authenticate?: Resolver<Maybe<ResolversTypes['AuthenticationResult']>, ParentType, ContextType, RequireFields<MutationAuthenticateArgs, 'serviceName' | 'params'>>;
371465
verifyAuthentication?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType, RequireFields<MutationVerifyAuthenticationArgs, 'serviceName' | 'params'>>;
372466
};
373467

468+
export type OtpAssociationResultResolvers<ContextType = any, ParentType extends ResolversParentTypes['OTPAssociationResult'] = ResolversParentTypes['OTPAssociationResult']> = {
469+
mfaToken?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
470+
authenticatorId?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
471+
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
472+
};
473+
374474
export type QueryResolvers<ContextType = any, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']> = {
375475
twoFactorSecret?: Resolver<Maybe<ResolversTypes['TwoFactorSecretKey']>, ParentType, ContextType>;
476+
authenticators?: Resolver<Maybe<Array<Maybe<ResolversTypes['Authenticator']>>>, ParentType, ContextType>;
477+
authenticatorsByMfaToken?: Resolver<Maybe<Array<Maybe<ResolversTypes['Authenticator']>>>, ParentType, ContextType, RequireFields<QueryAuthenticatorsByMfaTokenArgs, 'mfaToken'>>;
376478
getUser?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>;
377479
};
378480

@@ -402,13 +504,18 @@ export type UserResolvers<ContextType = any, ParentType extends ResolversParentT
402504
};
403505

404506
export type Resolvers<ContextType = any> = {
507+
AssociationResult?: AssociationResultResolvers<ContextType>;
405508
AuthenticationResult?: AuthenticationResultResolvers<ContextType>;
509+
Authenticator?: AuthenticatorResolvers<ContextType>;
510+
ChallengeResult?: ChallengeResultResolvers<ContextType>;
406511
CreateUserResult?: CreateUserResultResolvers<ContextType>;
512+
DefaultChallengeResult?: DefaultChallengeResultResolvers<ContextType>;
407513
EmailRecord?: EmailRecordResolvers<ContextType>;
408514
ImpersonateReturn?: ImpersonateReturnResolvers<ContextType>;
409515
LoginResult?: LoginResultResolvers<ContextType>;
410516
MultiFactorResult?: MultiFactorResultResolvers<ContextType>;
411517
Mutation?: MutationResolvers<ContextType>;
518+
OTPAssociationResult?: OtpAssociationResultResolvers<ContextType>;
412519
Query?: QueryResolvers<ContextType>;
413520
Tokens?: TokensResolvers<ContextType>;
414521
TwoFactorSecretKey?: TwoFactorSecretKeyResolvers<ContextType>;

0 commit comments

Comments
 (0)