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

[CP-9405] app authorization token #109

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fa8e628
feat: add dummy service for testing
bferenc Nov 26, 2024
50a0585
feat: get appcheck token on service worker activation
bferenc Nov 26, 2024
287c94e
feat: add firebase service
bferenc Nov 27, 2024
9be7ae0
feat: add challenge helper utils
bferenc Nov 27, 2024
3ec4e63
feat: add challenge solver utils
bferenc Nov 27, 2024
9087120
Merge branch 'main' into feat/appcheck
bferenc Dec 5, 2024
dc4fdfc
feat: remove unsubscribe logic
bferenc Dec 5, 2024
27f6057
feat: log fcm init error
bferenc Dec 5, 2024
7efcbc7
feat: add app type and version headers
bferenc Dec 9, 2024
3333758
feat: handle feature flag state
bferenc Dec 9, 2024
4999dc4
chore: apply patches on build
bferenc Dec 9, 2024
6d022a8
feat: improve base challenge
bferenc Dec 10, 2024
16806f9
Merge branch 'main' into feat/appcheck
bferenc Dec 11, 2024
4d32f3e
chore: fix type deps
bferenc Dec 11, 2024
02a774d
test: add FirebaseService tests
bferenc Dec 11, 2024
0aca297
test: add AppcheckService tests
bferenc Dec 11, 2024
402f1eb
test: add getHashByAlgorithm tests
bferenc Dec 11, 2024
c0c3200
test: add registerForChallenge tests
bferenc Dec 11, 2024
60acb57
test: add solveChallenge tests
bferenc Dec 11, 2024
9762150
test: add verifyChallenge tests
bferenc Dec 11, 2024
0821588
test: add basic challenge tests
bferenc Dec 11, 2024
b9c8dbe
chore: update allowed scripts config
bferenc Dec 12, 2024
5bd5507
Merge branch 'main' into feat/appcheck
bferenc Dec 16, 2024
a872e40
chore: add reference to docs
bferenc Dec 16, 2024
220306f
chore: lint
bferenc Dec 16, 2024
0d5c181
fix: always create new subscription with correct options
bferenc Jan 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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=
10 changes: 6 additions & 4 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 -c ./.eslintrc.js \"src/**/*.ts*\"",
"typecheck": "yarn tsc --skipLibCheck --noEmit",
"postinstall": "husky install && patch-package",
Expand Down Expand Up @@ -75,6 +75,7 @@
"eth-rpc-errors": "4.0.3",
"ethers": "6.8.1",
"events": "3.3.0",
"firebase": "11.0.1",
"fireblocks-sdk": "5.20.0",
"i18next": "21.9.2",
"i18next-http-backend": "1.4.4",
Expand Down Expand Up @@ -259,7 +260,8 @@
"@avalabs/avalanche-module>@avalabs/core-wallets-sdk>hdkey>secp256k1": false,
"@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
"@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
}
}
}
13 changes: 13 additions & 0 deletions patches/@firebase+messaging+0.12.13.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
diff --git a/node_modules/@firebase/messaging/dist/esm/index.esm2017.js b/node_modules/@firebase/messaging/dist/esm/index.esm2017.js
index 612f735..070ea56 100644
--- a/node_modules/@firebase/messaging/dist/esm/index.esm2017.js
+++ b/node_modules/@firebase/messaging/dist/esm/index.esm2017.js
@@ -565,7 +565,7 @@ async function getPushSubscription(swRegistration, vapidKey) {
return subscription;
}
return swRegistration.pushManager.subscribe({
- userVisibleOnly: true,
+ userVisibleOnly: false,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Chrome allows it since 121, but the SDK doesn't support it yet.

// 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)
5 changes: 4 additions & 1 deletion 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 @@ -15,7 +16,8 @@ export class BackgroundRuntime {
private onboardingService: OnboardingService,
// we try to fetch the bridge configs as soon as possible
private bridgeService: BridgeService,
private moduleManager: ModuleManager
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
187 changes: 187 additions & 0 deletions src/background/services/appcheck/AppCheckService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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('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();

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
Loading