Skip to content

Commit d34cf40

Browse files
committed
chore: merge branch '2022_40_MFA' into 'main'
2022 40 mfa See merge request team-backend/orestes/orestes-js!41
2 parents 20f2177 + df270ee commit d34cf40

File tree

14 files changed

+350
-22
lines changed

14 files changed

+350
-22
lines changed

lib/EntityManager.ts

Lines changed: 94 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,25 @@
11
import * as messages from './message';
22
import {
3-
FileFactory,
4-
UserFactory,
3+
DeviceFactory,
54
Entity,
6-
ManagedFactory,
75
EntityFactory,
8-
DeviceFactory,
9-
LoginOption, OAuthOptions,
6+
FileFactory,
7+
LoginOption,
8+
ManagedFactory,
9+
OAuthOptions,
10+
UserFactory,
1011
} from './binding';
11-
import {
12-
atob,
13-
Class, deprecated,
14-
isNode,
15-
JsonMap,
16-
Lockable,
17-
uuid,
18-
openWindow,
19-
} from './util';
20-
import {
21-
Message, StatusCode, Connector, OAuthMessage,
22-
} from './connector';
12+
import { atob, Class, deprecated, isNode, JsonMap, Lockable, openWindow, uuid, } from './util';
13+
import { Connector, Message, OAuthMessage, RestSpecification, StatusCode } from './connector';
2314
import { BloomFilter } from './caching';
2415
import { GeoPoint } from './GeoPoint';
2516
import type { ConnectData, EntityManagerFactory } from './EntityManagerFactory';
2617
import * as model from './model';
2718
import type { Metamodel } from './metamodel';
19+
import { EntityType, ManagedType, MapAttribute, PluralAttribute, } from './metamodel';
2820

2921
import { Builder } from './query';
3022
import { EntityExistsError, IllegalEntityError, PersistentError } from './error';
31-
import {
32-
MapAttribute, EntityType, ManagedType, PluralAttribute,
33-
} from './metamodel';
3423

3524
import {
3625
Code,
@@ -43,6 +32,9 @@ import {
4332
Validator,
4433
} from './intersection';
4534
import { appendQueryParams, CACHE_REPLACEMENT_SUPPORTED } from './connector/Message';
35+
import { MFAError } from './error/MFAError';
36+
import { Base64 } from './util/Base64';
37+
import { MFAResponse } from './util/Mfa';
4638

4739
const DB_PREFIX = '/db/';
4840

@@ -958,6 +950,79 @@ export class EntityManager extends Lockable {
958950
return this.withLock(() => this.send(new messages.Logout()).then(this._logout.bind(this)));
959951
}
960952

953+
/**
954+
* Starts the MFA initiate process - note you must be logged in, to start the mfa setup process
955+
*
956+
* @returns A promise that resolves to an object with the following properties:
957+
* - qrCode: A Base64 representation of the QR code for MFA setup.
958+
* - keyUri: The URI for the MFA secret key.
959+
* @example
960+
* const { qrCode, keyUri } = await db.initMFA();
961+
* const code = await setupMFADevice(qrCode, keyUri);
962+
* const user = await db.finishMFA(code);
963+
*/
964+
async initMFA(): Promise<MFAResponse> {
965+
return this.send(new messages.MFAInitChallenge()).then((resp) => {
966+
return {
967+
qrCode: resp.entity.qrCode as Base64<'png'>,
968+
keyUri: resp.entity.keyUri as string
969+
};
970+
});
971+
}
972+
973+
/**
974+
* Finishes the MFA (Multi-Factor Authentication) initiation process.
975+
*
976+
* @param code - The verification code for MFA.
977+
* @returns A promise that resolves with the user object of the logged-in user.
978+
*/
979+
public finishMFA(code: number): Promise<model.User> {
980+
return this.send(new messages.MFAInitFinish({ code })).then((resp) => {
981+
return this.User.me!; // to be here user is already logged in;
982+
});
983+
}
984+
985+
/**
986+
* Submit a verification code after a login
987+
*
988+
* @param code - A 6 digit verification code
989+
* @param token - An MFA token obtained during the login process
990+
* @return The logged-in user object
991+
*/
992+
async submitMFACode(code: number, token: string): Promise<model.User > {
993+
const loginType = this.tokenStorage.temporary ? LoginOption.SESSION_LOGIN : LoginOption.PERSIST_LOGIN;
994+
const msg = new messages.MFAToken({
995+
authToken: token,
996+
code,
997+
global: loginType === LoginOption.PERSIST_LOGIN,
998+
});
999+
return this.withLock(() => this._userRequest(msg, loginType)) as Promise< model.User>;
1000+
}
1001+
1002+
/**
1003+
* Disables Multi-Factor Authentication for the currently logged in user.
1004+
*
1005+
* @throws {PersistentError} - Thrown when the user is not logged in.
1006+
* @return A promise that resolves when Multi-Factor Authentication is successfully disabled.
1007+
*/
1008+
disableMFA(): Promise<any> {
1009+
if (!this.User.me)
1010+
throw new PersistentError('User not Logged in');
1011+
1012+
return this.send(new messages.MFADelete());
1013+
}
1014+
1015+
/**
1016+
* Returns the current MFA status of the user
1017+
*
1018+
* @returns A promise that resolves to the MFA status of the user.
1019+
* Possible values are 'ENABLED' if MFA is enabled, 'DISABLED' if MFA is
1020+
* disabled, or 'PENDING' if MFA status is pending.
1021+
*/
1022+
getMFAStatus(): Promise<'ENABLED' | 'DISABLED' | 'PENDING'> {
1023+
return this.send(new messages.MFAStatus()).then((resp) => resp.entity);
1024+
}
1025+
9611026
loginWithOAuth(provider: string, options: OAuthOptions): any | string | Promise<model.User | null> {
9621027
if (!this.connection) {
9631028
throw new Error('This EntityManager is not connected.');
@@ -1028,7 +1093,7 @@ export class EntityManager extends Lockable {
10281093
private _loginOAuthDevice(provider: string, opt: OAuthOptions): Promise<model.User | null> {
10291094
return this._userRequest(new messages.OAuth2(provider, opt.deviceCode), opt.loginOption)
10301095
.catch(() => new Promise((resolve) => setTimeout(resolve, 5000))
1031-
.then(() => this._loginOAuthDevice(provider, opt)));
1096+
.then(() => this._loginOAuthDevice(provider, opt))) as Promise<model.User | null>;
10321097
}
10331098

10341099
renew(loginOption?: LoginOption | boolean) {
@@ -1092,7 +1157,9 @@ export class EntityManager extends Lockable {
10921157

10931158
return this.send(msg, !login)
10941159
.then(
1095-
(response) => (response.entity ? this._updateUser(response.entity, login) : null),
1160+
(response) => {
1161+
return response.entity ? this._updateUser(response.entity, login) : null;
1162+
},
10961163
(e) => {
10971164
if (e.status === StatusCode.OBJECT_NOT_FOUND) {
10981165
if (login) {
@@ -1101,6 +1168,11 @@ export class EntityManager extends Lockable {
11011168
return null;
11021169
}
11031170

1171+
if (e.status === StatusCode.FORBIDDEN) {
1172+
const { data } = e;
1173+
throw new MFAError(data['baqend-mfa-auth-token']); // If MFA is required: throw an error containing the auth token
1174+
}
1175+
11041176
throw e;
11051177
},
11061178
);

lib/connector/Connector.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export abstract class Connector {
3333
'last-modified',
3434
'baqend-created-at',
3535
'baqend-custom-headers',
36+
'Baqend-MFA-Auth-Token',
3637
];
3738

3839
/**

lib/connector/Message.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const StatusCode = {
4343
BUCKET_NOT_FOUND: 461,
4444
INVALID_PERMISSION_MODIFICATION: 462,
4545
INVALID_TYPE_VALUE: 463,
46+
FORBIDDEN: 403,
4647
OBJECT_NOT_FOUND: 404,
4748
OBJECT_OUT_OF_DATE: 412,
4849
PERMISSION_DENIED: 466,

lib/error/MFAError.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { PersistentError } from './PersistentError';
2+
3+
export class MFAError extends PersistentError {
4+
/**
5+
* The Verification Token for the MFA Message
6+
*/
7+
public readonly token: string;
8+
9+
constructor(token: string) {
10+
super('MFA Required');
11+
this.token = token;
12+
}
13+
}

lib/message.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,72 @@ export const RevokeUserToken = Message.create<RevokeUserToken>({
905905
status: [204],
906906
});
907907

908+
interface MFAInitChallenge {
909+
/**
910+
* Starts MFA initialization
911+
* Method to create a MFA
912+
*/
913+
new(): Message;
914+
}
915+
export const MFAInitChallenge = Message.create<MFAInitChallenge>({
916+
method: 'GET',
917+
path: '/db/User/mfa/init',
918+
status: [200],
919+
});
920+
921+
interface MFAInitFinish {
922+
/**
923+
* Finishes MFA initialization
924+
* Method to create a MFA
925+
*
926+
* @param body The massage Content
927+
*/
928+
new(body?: json): Message;
929+
}
930+
export const MFAInitFinish = Message.create<MFAInitFinish>({
931+
method: 'POST',
932+
path: '/db/User/mfa/init',
933+
status: [200],
934+
});
935+
936+
interface MFAToken {
937+
/**
938+
* Finalize the generation of the shared secret for MFA
939+
*
940+
* @param body The massage Content
941+
*/
942+
new(body?: json): Message;
943+
}
944+
export const MFAToken = Message.create<MFAToken>({
945+
method: 'POST',
946+
path: '/db/User/mfa/token',
947+
status: [200],
948+
});
949+
950+
interface MFADelete {
951+
/**
952+
* Deletes the users mfaSecret and mfaService
953+
*/
954+
new(): Message;
955+
}
956+
export const MFADelete = Message.create<MFADelete>({
957+
method: 'DELETE',
958+
path: '/db/User/mfa',
959+
status: [204],
960+
});
961+
962+
interface MFAStatus {
963+
/**
964+
* Returns the current state of MFA
965+
*/
966+
new(): Message;
967+
}
968+
export const MFAStatus = Message.create<MFAStatus>({
969+
method: 'GET',
970+
path: '/db/User/mfa/status',
971+
status: [200],
972+
});
973+
908974
interface AssumeRole {
909975
/**
910976
* Assumes a role

lib/model/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export interface Role extends binding.Role {}
1414
* Devices are connected to the app to be contactable.
1515
*/
1616
export interface Device extends binding.Entity {}
17+

lib/util/Base64.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type Base64<ImageType extends string> = `data:image/${ImageType};base64${string}`;

lib/util/Mfa.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Base64 } from './Base64';
2+
3+
export type MFAResponse = {
4+
qrCode: Base64<'png'>
5+
keyUri: string
6+
};

lib/util/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ export { Class } from './Class';
1010
export { deprecated } from './deprecated';
1111
export { trailingSlashIt } from './trailingSlashIt';
1212
export { openWindow, OpenWindowHandler } from './openWindow';
13+
export { Base64 } from './Base64';
14+
export { MFAResponse } from './Mfa';

package-lock.json

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)