Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion development.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ S3_REGION=replace.me
APPLE_CLIENT_ID=com.replace.me
APP_SECRET=replace.me
GOOGLE_CLIENT_ID=replace.me.apps.googleusercontent.com
ANDROID_RELEASE_HASH=android:apk-key-hash:replace.me
# Comma-separated list: Play app-signing cert hash + upload-key cert hash (for locally-signed release builds)
ANDROID_RELEASE_HASH=android:apk-key-hash:replace.me,android:apk-key-hash:replace.me.too

# Only required to process webhook events from RevenueCat
REVENUECAT_HEADER=
Expand Down
41 changes: 40 additions & 1 deletion src/__tests__/services/PasskeyService.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
import moment from 'moment';
import { randomUUID, randomBytes } from 'crypto';
import { PasskeyService } from '../../services/PasskeyService';
Expand Down Expand Up @@ -585,4 +585,43 @@ describe('PasskeyService', () => {
});
});
});

describe('expectedOrigins', () => {
const originalHash = process.env.ANDROID_RELEASE_HASH;

afterEach(() => {
process.env.ANDROID_RELEASE_HASH = originalHash;
});

it('should allow the web origin plus a single android hash', () => {
process.env.ANDROID_RELEASE_HASH = 'android:apk-key-hash:playSigningHash';
const svc = new PasskeyService();
expect((svc as any).expectedOrigins).toEqual([
`https://${process.env.WEBAUTHN_RP_ID}`,
'android:apk-key-hash:playSigningHash',
]);
});

it('should split a comma-separated list into multiple android origins', () => {
// Play App Signing: store builds assert with Google's app-signing cert, locally-signed
// release builds assert with the upload cert — both hashes must be accepted.
process.env.ANDROID_RELEASE_HASH =
'android:apk-key-hash:playSigningHash, android:apk-key-hash:uploadKeyHash';
const svc = new PasskeyService();
expect((svc as any).expectedOrigins).toEqual([
`https://${process.env.WEBAUTHN_RP_ID}`,
'android:apk-key-hash:playSigningHash',
'android:apk-key-hash:uploadKeyHash',
]);
});

it('should ignore empty segments from trailing commas', () => {
process.env.ANDROID_RELEASE_HASH = 'android:apk-key-hash:playSigningHash,';
const svc = new PasskeyService();
expect((svc as any).expectedOrigins).toEqual([
`https://${process.env.WEBAUTHN_RP_ID}`,
'android:apk-key-hash:playSigningHash',
]);
});
});
});
13 changes: 9 additions & 4 deletions src/services/PasskeyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,15 @@ export class PasskeyService {
private readonly rpName = process.env.WEBAUTHN_RP_NAME;
private readonly androidReleaseHash = process.env.ANDROID_RELEASE_HASH;
private readonly origin = `https://${this.rpID}`;
// Origins allowed to complete WebAuthn ceremonies: the web origin plus the
// Android app's signing-key origin (android:apk-key-hash:...). ANDROID_RELEASE_HASH
// is validated as required at boot (config/envs.ts), so it's always present.
private readonly expectedOrigins = [this.origin, this.androidReleaseHash!];
// Origins allowed to complete WebAuthn ceremonies: the web origin plus the Android app's
// signing-key origins (android:apk-key-hash:...). ANDROID_RELEASE_HASH is validated as
// required at boot (config/envs.ts) and accepts a comma-separated list: with Play App
// Signing the store build asserts with Google's app-signing cert while locally-signed
// release builds assert with the upload cert — both need to be allowed.
private readonly expectedOrigins = [
this.origin,
...this.androidReleaseHash!.split(',').map((hash) => hash.trim()).filter(Boolean),
];
private readonly challengeTTL = 300;

// Registration
Expand Down
Loading