Skip to content

Commit fe60e3f

Browse files
authored
[CP-9405] app authorization token (#109)
1 parent 1eba870 commit fe60e3f

26 files changed

+1670
-6
lines changed

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,10 @@ NEWSLETTER_PORTAL_ID=
9898

9999
# Optional
100100
NEWSLETTER_FORM_ID=
101+
102+
# Base64 encoded Firebase config
103+
FIREBASE_CONFIG=
104+
105+
# Required for ID token registration
106+
# ID service URL
107+
ID_SERVICE_URL=

.github/workflows/create_release.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ jobs:
3939
echo NEWSLETTER_BASE_URL=${{ secrets.NEWSLETTER_BASE_URL }} >> .env.production
4040
echo NEWSLETTER_PORTAL_ID=${{ secrets.NEWSLETTER_PORTAL_ID }} >> .env.production
4141
echo NEWSLETTER_FORM_ID=${{ secrets.NEWSLETTER_FORM_ID }} >> .env.production
42+
echo FIREBASE_CONFIG=${{ secrets.FIREBASE_CONFIG }} >> .env.production
43+
echo ID_SERVICE_URL=${{ secrets.ID_SERVICE_URL }} >> .env.production
4244
- name: Install dependencies
4345
run: yarn setup
4446
- name: Build library

.github/workflows/e2e_testing.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ jobs:
4848
echo NEWSLETTER_BASE_URL=${{ secrets.NEWSLETTER_BASE_URL }} >> .env.production
4949
echo NEWSLETTER_PORTAL_ID=${{ secrets.NEWSLETTER_PORTAL_ID }} >> .env.production
5050
echo NEWSLETTER_FORM_ID=${{ secrets.NEWSLETTER_FORM_ID }} >> .env.production
51+
echo FIREBASE_CONFIG=${{ secrets.FIREBASE_CONFIG }} >> .env.production
52+
echo ID_SERVICE_URL=${{ secrets.ID_SERVICE_URL }} >> .env.production
5153
- name: Install dependencies
5254
run: yarn setup
5355
- name: Build library

.github/workflows/main_branch.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ jobs:
4242
echo NEWSLETTER_BASE_URL=${{ secrets.NEWSLETTER_BASE_URL }} >> .env.production
4343
echo NEWSLETTER_PORTAL_ID=${{ secrets.NEWSLETTER_PORTAL_ID }} >> .env.production
4444
echo NEWSLETTER_FORM_ID=${{ secrets.NEWSLETTER_FORM_ID }} >> .env.production
45+
echo FIREBASE_CONFIG=${{ secrets.FIREBASE_CONFIG }} >> .env.production
46+
echo ID_SERVICE_URL=${{ secrets.ID_SERVICE_URL }} >> .env.production
4547
- name: Install dependencies
4648
run: yarn setup
4749
- name: Run tests

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
"start": "yarn dev",
88
"build:inpage": "webpack --config webpack.inpage.js",
99
"dev:inpage": "webpack -w --config webpack.inpage.js",
10-
"build": "yarn run build:inpage --mode=production && webpack --config webpack.prod.js",
11-
"build:alpha": "yarn run build:inpage --mode=production && webpack --config webpack.alpha.js",
12-
"dev": "yarn run build:inpage && webpack -w --config webpack.dev.js",
10+
"build": "yarn run patch-package && yarn run build:inpage --mode=production && webpack --config webpack.prod.js",
11+
"build:alpha": "yarn run patch-package && yarn run build:inpage --mode=production && webpack --config webpack.alpha.js",
12+
"dev": "yarn run patch-package && yarn run build:inpage && webpack -w --config webpack.dev.js",
1313
"lint": "eslint --fix \"src/**/*.ts*\"",
1414
"typecheck": "yarn tsc --skipLibCheck --noEmit",
1515
"postinstall": "husky install && patch-package",
@@ -77,6 +77,7 @@
7777
"eth-rpc-errors": "4.0.3",
7878
"ethers": "6.8.1",
7979
"events": "3.3.0",
80+
"firebase": "11.1.0",
8081
"fireblocks-sdk": "5.20.0",
8182
"hypersdk-client": "0.4.17",
8283
"i18next": "21.9.2",
@@ -267,6 +268,7 @@
267268
"@avalabs/avalanche-module>@avalabs/vm-module-types>@avalabs/core-wallets-sdk>@avalabs/hw-app-avalanche>@ledgerhq/hw-app-eth>@ledgerhq/domain-service>eip55>keccak": false,
268269
"@avalabs/avalanche-module>@avalabs/vm-module-types>@avalabs/core-wallets-sdk>@ledgerhq/hw-app-btc>bitcoinjs-lib>bip32>tiny-secp256k1": false,
269270
"@avalabs/avalanche-module>@avalabs/vm-module-types>@avalabs/core-wallets-sdk>hdkey>secp256k1": false,
271+
"firebase>@firebase/firestore>@grpc/grpc-js>@grpc/proto-loader>protobufjs": false,
270272
"@avalabs/core-bridge-sdk>@avalabs/core-wallets-sdk>@metamask/eth-sig-util>@metamask/utils>@ethereumjs/tx>@ethereumjs/common>ethereumjs-util>ethereum-cryptography>keccak": false,
271273
"@avalabs/avalanche-module>@avalabs/vm-module-types>hypersdk-client>@metamask/sdk>@metamask/sdk-communication-layer>bufferutil": false,
272274
"@avalabs/avalanche-module>@avalabs/vm-module-types>hypersdk-client>@metamask/sdk>@metamask/sdk-communication-layer>utf-8-validate": false,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
diff --git a/node_modules/@firebase/messaging/dist/esm/index.esm2017.js b/node_modules/@firebase/messaging/dist/esm/index.esm2017.js
2+
index b4c53c4..71498b8 100644
3+
--- a/node_modules/@firebase/messaging/dist/esm/index.esm2017.js
4+
+++ b/node_modules/@firebase/messaging/dist/esm/index.esm2017.js
5+
@@ -562,11 +562,17 @@ async function getNewToken(firebaseDependencies, subscriptionOptions) {
6+
*/
7+
async function getPushSubscription(swRegistration, vapidKey) {
8+
const subscription = await swRegistration.pushManager.getSubscription();
9+
+
10+
if (subscription) {
11+
- return subscription;
12+
+ if(!subscription.options.userVisibleOnly) {
13+
+ return subscription;
14+
+ }
15+
+
16+
+ await subscription.unsubscribe()
17+
}
18+
+
19+
return swRegistration.pushManager.subscribe({
20+
- userVisibleOnly: true,
21+
+ userVisibleOnly: false,
22+
// Chrome <= 75 doesn't support base64-encoded VAPID key. For backward compatibility, VAPID key
23+
// submitted to pushManager#subscribe must be of type Uint8Array.
24+
applicationServerKey: base64ToArray(vapidKey)
25+
diff --git a/node_modules/@firebase/messaging/dist/esm/index.sw.esm2017.js b/node_modules/@firebase/messaging/dist/esm/index.sw.esm2017.js
26+
index 88ac597..82ee9bc 100644
27+
--- a/node_modules/@firebase/messaging/dist/esm/index.sw.esm2017.js
28+
+++ b/node_modules/@firebase/messaging/dist/esm/index.sw.esm2017.js
29+
@@ -560,11 +560,17 @@ async function getNewToken(firebaseDependencies, subscriptionOptions) {
30+
*/
31+
async function getPushSubscription(swRegistration, vapidKey) {
32+
const subscription = await swRegistration.pushManager.getSubscription();
33+
+
34+
if (subscription) {
35+
- return subscription;
36+
+ if(!subscription.options.userVisibleOnly) {
37+
+ return subscription;
38+
+ }
39+
+
40+
+ await subscription.unsubscribe()
41+
}
42+
+
43+
return swRegistration.pushManager.subscribe({
44+
- userVisibleOnly: true,
45+
+ userVisibleOnly: false,
46+
// Chrome <= 75 doesn't support base64-encoded VAPID key. For backward compatibility, VAPID key
47+
// submitted to pushManager#subscribe must be of type Uint8Array.
48+
applicationServerKey: base64ToArray(vapidKey)

src/background/runtime/BackgroundRuntime.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { LockService } from '@src/background/services/lock/LockService';
66
import { OnboardingService } from '@src/background/services/onboarding/OnboardingService';
77
import { ModuleManager } from '../vmModules/ModuleManager';
88
import { BridgeService } from '../services/bridge/BridgeService';
9+
import { AppCheckService } from '@src/background/services/appcheck/AppCheckService';
910

1011
@singleton()
1112
export class BackgroundRuntime {
@@ -16,6 +17,7 @@ export class BackgroundRuntime {
1617
// we try to fetch the bridge configs as soon as possible
1718
private bridgeService: BridgeService,
1819
private moduleManager: ModuleManager,
20+
private appCheckService: AppCheckService,
1921
) {}
2022

2123
activate() {
@@ -28,6 +30,7 @@ export class BackgroundRuntime {
2830
this.lockService.activate();
2931
this.onboardingService.activate();
3032
this.moduleManager.activate();
33+
this.appCheckService.activate();
3134
}
3235

3336
private onInstalled() {
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import * as Sentry from '@sentry/browser';
2+
import {
3+
AppCheck,
4+
CustomProvider,
5+
initializeAppCheck,
6+
setTokenAutoRefreshEnabled,
7+
} from 'firebase/app-check';
8+
import { FirebaseService } from '../firebase/FirebaseService';
9+
import { FcmMessageEvents, FirebaseEvents } from '../firebase/models';
10+
import {
11+
AppCheckService,
12+
WAIT_FOR_CHALLENGE_ATTEMPT_COUNT,
13+
WAIT_FOR_CHALLENGE_DELAY_MS,
14+
} from './AppCheckService';
15+
import registerForChallenge from './utils/registerForChallenge';
16+
import { ChallengeTypes } from './models';
17+
import { MessagePayload } from 'firebase/messaging/sw';
18+
import solveChallenge from './utils/solveChallenge';
19+
import verifyChallenge from './utils/verifyChallenge';
20+
21+
jest.mock('@sentry/browser');
22+
jest.mock('firebase/app-check');
23+
jest.mock('./utils/registerForChallenge');
24+
jest.mock('./utils/verifyChallenge');
25+
jest.mock('./utils/solveChallenge');
26+
27+
describe('AppCheckService', () => {
28+
let appCheckService: AppCheckService;
29+
let firebaseService: FirebaseService;
30+
31+
beforeEach(() => {
32+
jest.resetAllMocks();
33+
34+
(Sentry.startTransaction as jest.Mock).mockReturnValue({
35+
finish: jest.fn(),
36+
setStatus: jest.fn(),
37+
startChild: jest.fn(() => ({
38+
finish: jest.fn(),
39+
})),
40+
});
41+
42+
firebaseService = {
43+
isFcmInitialized: true,
44+
getFirebaseApp: () => ({ name: 'test' }),
45+
getFcmToken: jest.fn().mockReturnValue('fcmToken'),
46+
addFcmMessageListener: jest.fn(),
47+
addFirebaseEventListener: jest.fn(),
48+
} as unknown as FirebaseService;
49+
50+
appCheckService = new AppCheckService(firebaseService);
51+
appCheckService.activate();
52+
});
53+
54+
it('subscribes for events on activation correctly', () => {
55+
expect(firebaseService.addFcmMessageListener).toHaveBeenCalledWith(
56+
FcmMessageEvents.ID_CHALLENGE,
57+
expect.any(Function),
58+
);
59+
60+
expect(firebaseService.addFirebaseEventListener).toHaveBeenCalledTimes(2);
61+
expect(firebaseService.addFirebaseEventListener).toHaveBeenNthCalledWith(
62+
1,
63+
FirebaseEvents.FCM_INITIALIZED,
64+
expect.any(Function),
65+
);
66+
expect(firebaseService.addFirebaseEventListener).toHaveBeenNthCalledWith(
67+
2,
68+
FirebaseEvents.FCM_TERMINATED,
69+
expect.any(Function),
70+
);
71+
});
72+
73+
const appCheckMock = { app: { name: 'test' } } as AppCheck;
74+
75+
beforeEach(() => {
76+
jest.useFakeTimers();
77+
jest.mocked(initializeAppCheck).mockReturnValue(appCheckMock);
78+
79+
// simulate FCM_INITIALIZED event
80+
jest.mocked(firebaseService.addFirebaseEventListener).mock.calls[0]?.[1]();
81+
});
82+
83+
afterEach(() => {
84+
jest.useRealTimers();
85+
});
86+
87+
it('initializes appcheck correctly', () => {
88+
expect(setTokenAutoRefreshEnabled).not.toHaveBeenCalled();
89+
expect(initializeAppCheck).toHaveBeenCalledWith(
90+
{ name: 'test' },
91+
{
92+
provider: expect.any(CustomProvider),
93+
isTokenAutoRefreshEnabled: true,
94+
},
95+
);
96+
97+
// simulate FCM_INITIALIZED event (second time)
98+
jest.mocked(firebaseService.addFirebaseEventListener).mock.calls[0]?.[1]();
99+
100+
expect(initializeAppCheck).toHaveBeenCalledTimes(1);
101+
expect(setTokenAutoRefreshEnabled).toHaveBeenCalledWith(appCheckMock, true);
102+
});
103+
104+
it('terminates appcheck correctly', () => {
105+
expect(setTokenAutoRefreshEnabled).not.toHaveBeenCalled();
106+
expect(initializeAppCheck).toHaveBeenCalledWith(
107+
{ name: 'test' },
108+
{
109+
provider: expect.any(CustomProvider),
110+
isTokenAutoRefreshEnabled: true,
111+
},
112+
);
113+
114+
// simulate FCM_TERMINATED event
115+
jest.mocked(firebaseService.addFirebaseEventListener).mock.calls[1]?.[1]();
116+
117+
expect(setTokenAutoRefreshEnabled).toHaveBeenCalledWith(
118+
appCheckMock,
119+
false,
120+
);
121+
});
122+
123+
describe('getToken', () => {
124+
it('throws when FCM is not initialized', async () => {
125+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
126+
// @ts-ignore
127+
firebaseService.isFcmInitialized = false;
128+
await expect(
129+
jest.mocked(CustomProvider).mock.calls[0]?.[0].getToken(),
130+
).rejects.toThrow('fcm is not initialized');
131+
});
132+
133+
it('throws when FCM token is missing', async () => {
134+
jest.mocked(firebaseService.getFcmToken).mockReturnValueOnce(undefined);
135+
await expect(
136+
jest.mocked(CustomProvider).mock.calls[0]?.[0].getToken(),
137+
).rejects.toThrow('fcm token is missing');
138+
});
139+
140+
it('throws a timeout error when challenge is not received in time', async () => {
141+
jest
142+
.mocked(CustomProvider)
143+
.mock.calls[0]?.[0].getToken()
144+
.catch((err) => {
145+
expect(err).toBe('timeout');
146+
});
147+
148+
for (let i = 0; i <= WAIT_FOR_CHALLENGE_ATTEMPT_COUNT; i++) {
149+
jest.advanceTimersByTime(WAIT_FOR_CHALLENGE_DELAY_MS);
150+
await Promise.resolve();
151+
}
152+
});
153+
154+
it('generates a token correctly', async () => {
155+
jest.mocked(crypto.randomUUID).mockReturnValue('1-2-3-4-5');
156+
jest.mocked(solveChallenge).mockResolvedValueOnce('solution');
157+
jest
158+
.mocked(verifyChallenge)
159+
.mockResolvedValueOnce({ token: 'token', exp: 1234 });
160+
161+
const promise = jest.mocked(CustomProvider).mock.calls[0]?.[0].getToken();
162+
163+
// trigger ID_CHALLENGE event
164+
jest.mocked(firebaseService.addFcmMessageListener).mock.calls[0]?.[1]({
165+
data: {
166+
requestId: crypto.randomUUID(),
167+
registrationId: 'registrationId',
168+
type: ChallengeTypes.BASIC,
169+
event: FcmMessageEvents.ID_CHALLENGE,
170+
details: '{}',
171+
},
172+
} as unknown as MessagePayload);
173+
174+
await Promise.resolve();
175+
jest.advanceTimersByTime(1000);
176+
await Promise.resolve();
177+
178+
await expect(promise).resolves.toStrictEqual({
179+
token: 'token',
180+
expireTimeMillis: 1234,
181+
});
182+
183+
expect(registerForChallenge).toHaveBeenCalledWith({
184+
token: 'fcmToken',
185+
requestId: crypto.randomUUID(),
186+
});
187+
expect(solveChallenge).toHaveBeenCalledWith({
188+
type: ChallengeTypes.BASIC,
189+
challengeDetails: '{}',
190+
});
191+
expect(verifyChallenge).toHaveBeenCalledWith({
192+
registrationId: 'registrationId',
193+
solution: 'solution',
194+
});
195+
});
196+
});
197+
});

0 commit comments

Comments
 (0)