Skip to content

Commit

Permalink
[CP-9405] app authorization token (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
bferenc authored Feb 6, 2025
1 parent 1eba870 commit fe60e3f
Show file tree
Hide file tree
Showing 26 changed files with 1,670 additions and 6 deletions.
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,10 @@ NEWSLETTER_PORTAL_ID=

# Optional
NEWSLETTER_FORM_ID=

# Base64 encoded Firebase config
FIREBASE_CONFIG=

# Required for ID token registration
# ID service URL
ID_SERVICE_URL=
2 changes: 2 additions & 0 deletions .github/workflows/create_release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ jobs:
echo NEWSLETTER_BASE_URL=${{ secrets.NEWSLETTER_BASE_URL }} >> .env.production
echo NEWSLETTER_PORTAL_ID=${{ secrets.NEWSLETTER_PORTAL_ID }} >> .env.production
echo NEWSLETTER_FORM_ID=${{ secrets.NEWSLETTER_FORM_ID }} >> .env.production
echo FIREBASE_CONFIG=${{ secrets.FIREBASE_CONFIG }} >> .env.production
echo ID_SERVICE_URL=${{ secrets.ID_SERVICE_URL }} >> .env.production
- name: Install dependencies
run: yarn setup
- name: Build library
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/e2e_testing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ jobs:
echo NEWSLETTER_BASE_URL=${{ secrets.NEWSLETTER_BASE_URL }} >> .env.production
echo NEWSLETTER_PORTAL_ID=${{ secrets.NEWSLETTER_PORTAL_ID }} >> .env.production
echo NEWSLETTER_FORM_ID=${{ secrets.NEWSLETTER_FORM_ID }} >> .env.production
echo FIREBASE_CONFIG=${{ secrets.FIREBASE_CONFIG }} >> .env.production
echo ID_SERVICE_URL=${{ secrets.ID_SERVICE_URL }} >> .env.production
- name: Install dependencies
run: yarn setup
- name: Build library
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/main_branch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ jobs:
echo NEWSLETTER_BASE_URL=${{ secrets.NEWSLETTER_BASE_URL }} >> .env.production
echo NEWSLETTER_PORTAL_ID=${{ secrets.NEWSLETTER_PORTAL_ID }} >> .env.production
echo NEWSLETTER_FORM_ID=${{ secrets.NEWSLETTER_FORM_ID }} >> .env.production
echo FIREBASE_CONFIG=${{ secrets.FIREBASE_CONFIG }} >> .env.production
echo ID_SERVICE_URL=${{ secrets.ID_SERVICE_URL }} >> .env.production
- name: Install dependencies
run: yarn setup
- name: Run tests
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
"start": "yarn dev",
"build:inpage": "webpack --config webpack.inpage.js",
"dev:inpage": "webpack -w --config webpack.inpage.js",
"build": "yarn run build:inpage --mode=production && webpack --config webpack.prod.js",
"build:alpha": "yarn run build:inpage --mode=production && webpack --config webpack.alpha.js",
"dev": "yarn run build:inpage && webpack -w --config webpack.dev.js",
"build": "yarn run patch-package && yarn run build:inpage --mode=production && webpack --config webpack.prod.js",
"build:alpha": "yarn run patch-package && yarn run build:inpage --mode=production && webpack --config webpack.alpha.js",
"dev": "yarn run patch-package && yarn run build:inpage && webpack -w --config webpack.dev.js",
"lint": "eslint --fix \"src/**/*.ts*\"",
"typecheck": "yarn tsc --skipLibCheck --noEmit",
"postinstall": "husky install && patch-package",
Expand Down Expand Up @@ -77,6 +77,7 @@
"eth-rpc-errors": "4.0.3",
"ethers": "6.8.1",
"events": "3.3.0",
"firebase": "11.1.0",
"fireblocks-sdk": "5.20.0",
"hypersdk-client": "0.4.17",
"i18next": "21.9.2",
Expand Down Expand Up @@ -267,6 +268,7 @@
"@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,
"@avalabs/avalanche-module>@avalabs/vm-module-types>@avalabs/core-wallets-sdk>@ledgerhq/hw-app-btc>bitcoinjs-lib>bip32>tiny-secp256k1": false,
"@avalabs/avalanche-module>@avalabs/vm-module-types>@avalabs/core-wallets-sdk>hdkey>secp256k1": false,
"firebase>@firebase/firestore>@grpc/grpc-js>@grpc/proto-loader>protobufjs": false,
"@avalabs/core-bridge-sdk>@avalabs/core-wallets-sdk>@metamask/eth-sig-util>@metamask/utils>@ethereumjs/tx>@ethereumjs/common>ethereumjs-util>ethereum-cryptography>keccak": false,
"@avalabs/avalanche-module>@avalabs/vm-module-types>hypersdk-client>@metamask/sdk>@metamask/sdk-communication-layer>bufferutil": false,
"@avalabs/avalanche-module>@avalabs/vm-module-types>hypersdk-client>@metamask/sdk>@metamask/sdk-communication-layer>utf-8-validate": false,
Expand Down
48 changes: 48 additions & 0 deletions patches/@firebase+messaging+0.12.15.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
diff --git a/node_modules/@firebase/messaging/dist/esm/index.esm2017.js b/node_modules/@firebase/messaging/dist/esm/index.esm2017.js
index b4c53c4..71498b8 100644
--- a/node_modules/@firebase/messaging/dist/esm/index.esm2017.js
+++ b/node_modules/@firebase/messaging/dist/esm/index.esm2017.js
@@ -562,11 +562,17 @@ async function getNewToken(firebaseDependencies, subscriptionOptions) {
*/
async function getPushSubscription(swRegistration, vapidKey) {
const subscription = await swRegistration.pushManager.getSubscription();
+
if (subscription) {
- return subscription;
+ if(!subscription.options.userVisibleOnly) {
+ return subscription;
+ }
+
+ await subscription.unsubscribe()
}
+
return swRegistration.pushManager.subscribe({
- userVisibleOnly: true,
+ userVisibleOnly: false,
// Chrome <= 75 doesn't support base64-encoded VAPID key. For backward compatibility, VAPID key
// submitted to pushManager#subscribe must be of type Uint8Array.
applicationServerKey: base64ToArray(vapidKey)
diff --git a/node_modules/@firebase/messaging/dist/esm/index.sw.esm2017.js b/node_modules/@firebase/messaging/dist/esm/index.sw.esm2017.js
index 88ac597..82ee9bc 100644
--- a/node_modules/@firebase/messaging/dist/esm/index.sw.esm2017.js
+++ b/node_modules/@firebase/messaging/dist/esm/index.sw.esm2017.js
@@ -560,11 +560,17 @@ async function getNewToken(firebaseDependencies, subscriptionOptions) {
*/
async function getPushSubscription(swRegistration, vapidKey) {
const subscription = await swRegistration.pushManager.getSubscription();
+
if (subscription) {
- return subscription;
+ if(!subscription.options.userVisibleOnly) {
+ return subscription;
+ }
+
+ await subscription.unsubscribe()
}
+
return swRegistration.pushManager.subscribe({
- userVisibleOnly: true,
+ userVisibleOnly: false,
// Chrome <= 75 doesn't support base64-encoded VAPID key. For backward compatibility, VAPID key
// submitted to pushManager#subscribe must be of type Uint8Array.
applicationServerKey: base64ToArray(vapidKey)
3 changes: 3 additions & 0 deletions src/background/runtime/BackgroundRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { LockService } from '@src/background/services/lock/LockService';
import { OnboardingService } from '@src/background/services/onboarding/OnboardingService';
import { ModuleManager } from '../vmModules/ModuleManager';
import { BridgeService } from '../services/bridge/BridgeService';
import { AppCheckService } from '@src/background/services/appcheck/AppCheckService';

@singleton()
export class BackgroundRuntime {
Expand All @@ -16,6 +17,7 @@ export class BackgroundRuntime {
// we try to fetch the bridge configs as soon as possible
private bridgeService: BridgeService,
private moduleManager: ModuleManager,
private appCheckService: AppCheckService,
) {}

activate() {
Expand All @@ -28,6 +30,7 @@ export class BackgroundRuntime {
this.lockService.activate();
this.onboardingService.activate();
this.moduleManager.activate();
this.appCheckService.activate();
}

private onInstalled() {
Expand Down
197 changes: 197 additions & 0 deletions src/background/services/appcheck/AppCheckService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import * as Sentry from '@sentry/browser';
import {
AppCheck,
CustomProvider,
initializeAppCheck,
setTokenAutoRefreshEnabled,
} from 'firebase/app-check';
import { FirebaseService } from '../firebase/FirebaseService';
import { FcmMessageEvents, FirebaseEvents } from '../firebase/models';
import {
AppCheckService,
WAIT_FOR_CHALLENGE_ATTEMPT_COUNT,
WAIT_FOR_CHALLENGE_DELAY_MS,
} from './AppCheckService';
import registerForChallenge from './utils/registerForChallenge';
import { ChallengeTypes } from './models';
import { MessagePayload } from 'firebase/messaging/sw';
import solveChallenge from './utils/solveChallenge';
import verifyChallenge from './utils/verifyChallenge';

jest.mock('@sentry/browser');
jest.mock('firebase/app-check');
jest.mock('./utils/registerForChallenge');
jest.mock('./utils/verifyChallenge');
jest.mock('./utils/solveChallenge');

describe('AppCheckService', () => {
let appCheckService: AppCheckService;
let firebaseService: FirebaseService;

beforeEach(() => {
jest.resetAllMocks();

(Sentry.startTransaction as jest.Mock).mockReturnValue({
finish: jest.fn(),
setStatus: jest.fn(),
startChild: jest.fn(() => ({
finish: jest.fn(),
})),
});

firebaseService = {
isFcmInitialized: true,
getFirebaseApp: () => ({ name: 'test' }),
getFcmToken: jest.fn().mockReturnValue('fcmToken'),
addFcmMessageListener: jest.fn(),
addFirebaseEventListener: jest.fn(),
} as unknown as FirebaseService;

appCheckService = new AppCheckService(firebaseService);
appCheckService.activate();
});

it('subscribes for events on activation correctly', () => {
expect(firebaseService.addFcmMessageListener).toHaveBeenCalledWith(
FcmMessageEvents.ID_CHALLENGE,
expect.any(Function),
);

expect(firebaseService.addFirebaseEventListener).toHaveBeenCalledTimes(2);
expect(firebaseService.addFirebaseEventListener).toHaveBeenNthCalledWith(
1,
FirebaseEvents.FCM_INITIALIZED,
expect.any(Function),
);
expect(firebaseService.addFirebaseEventListener).toHaveBeenNthCalledWith(
2,
FirebaseEvents.FCM_TERMINATED,
expect.any(Function),
);
});

const appCheckMock = { app: { name: 'test' } } as AppCheck;

beforeEach(() => {
jest.useFakeTimers();
jest.mocked(initializeAppCheck).mockReturnValue(appCheckMock);

// simulate FCM_INITIALIZED event
jest.mocked(firebaseService.addFirebaseEventListener).mock.calls[0]?.[1]();
});

afterEach(() => {
jest.useRealTimers();
});

it('initializes appcheck correctly', () => {
expect(setTokenAutoRefreshEnabled).not.toHaveBeenCalled();
expect(initializeAppCheck).toHaveBeenCalledWith(
{ name: 'test' },
{
provider: expect.any(CustomProvider),
isTokenAutoRefreshEnabled: true,
},
);

// simulate FCM_INITIALIZED event (second time)
jest.mocked(firebaseService.addFirebaseEventListener).mock.calls[0]?.[1]();

expect(initializeAppCheck).toHaveBeenCalledTimes(1);
expect(setTokenAutoRefreshEnabled).toHaveBeenCalledWith(appCheckMock, true);
});

it('terminates appcheck correctly', () => {
expect(setTokenAutoRefreshEnabled).not.toHaveBeenCalled();
expect(initializeAppCheck).toHaveBeenCalledWith(
{ name: 'test' },
{
provider: expect.any(CustomProvider),
isTokenAutoRefreshEnabled: true,
},
);

// simulate FCM_TERMINATED event
jest.mocked(firebaseService.addFirebaseEventListener).mock.calls[1]?.[1]();

expect(setTokenAutoRefreshEnabled).toHaveBeenCalledWith(
appCheckMock,
false,
);
});

describe('getToken', () => {
it('throws when FCM is not initialized', async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
firebaseService.isFcmInitialized = false;
await expect(
jest.mocked(CustomProvider).mock.calls[0]?.[0].getToken(),
).rejects.toThrow('fcm is not initialized');
});

it('throws when FCM token is missing', async () => {
jest.mocked(firebaseService.getFcmToken).mockReturnValueOnce(undefined);
await expect(
jest.mocked(CustomProvider).mock.calls[0]?.[0].getToken(),
).rejects.toThrow('fcm token is missing');
});

it('throws a timeout error when challenge is not received in time', async () => {
jest
.mocked(CustomProvider)
.mock.calls[0]?.[0].getToken()
.catch((err) => {
expect(err).toBe('timeout');
});

for (let i = 0; i <= WAIT_FOR_CHALLENGE_ATTEMPT_COUNT; i++) {
jest.advanceTimersByTime(WAIT_FOR_CHALLENGE_DELAY_MS);
await Promise.resolve();
}
});

it('generates a token correctly', async () => {
jest.mocked(crypto.randomUUID).mockReturnValue('1-2-3-4-5');
jest.mocked(solveChallenge).mockResolvedValueOnce('solution');
jest
.mocked(verifyChallenge)
.mockResolvedValueOnce({ token: 'token', exp: 1234 });

const promise = jest.mocked(CustomProvider).mock.calls[0]?.[0].getToken();

// trigger ID_CHALLENGE event
jest.mocked(firebaseService.addFcmMessageListener).mock.calls[0]?.[1]({
data: {
requestId: crypto.randomUUID(),
registrationId: 'registrationId',
type: ChallengeTypes.BASIC,
event: FcmMessageEvents.ID_CHALLENGE,
details: '{}',
},
} as unknown as MessagePayload);

await Promise.resolve();
jest.advanceTimersByTime(1000);
await Promise.resolve();

await expect(promise).resolves.toStrictEqual({
token: 'token',
expireTimeMillis: 1234,
});

expect(registerForChallenge).toHaveBeenCalledWith({
token: 'fcmToken',
requestId: crypto.randomUUID(),
});
expect(solveChallenge).toHaveBeenCalledWith({
type: ChallengeTypes.BASIC,
challengeDetails: '{}',
});
expect(verifyChallenge).toHaveBeenCalledWith({
registrationId: 'registrationId',
solution: 'solution',
});
});
});
});
Loading

0 comments on commit fe60e3f

Please sign in to comment.