diff --git a/android/src/main/java/com/tanker/clientreactnative/Verification.kt b/android/src/main/java/com/tanker/clientreactnative/Verification.kt index 0520a57..9e0f82b 100644 --- a/android/src/main/java/com/tanker/clientreactnative/Verification.kt +++ b/android/src/main/java/com/tanker/clientreactnative/Verification.kt @@ -36,6 +36,9 @@ fun Verification(json: ReadableMap): Verification { json.getString("preverifiedPhoneNumber")?.let { return PreverifiedPhoneNumberVerification(it) } + json.getString("preverifiedOidcSubject")?.let { + return PreverifiedOIDCVerification(it, json.getString("oidcProviderId")!!) + } json.getString("e2ePassphrase")?.let { return E2ePassphraseVerification(it) } diff --git a/example/src/test_verify.ts b/example/src/test_verify.ts index 82e3b6b..aebec36 100644 --- a/example/src/test_verify.ts +++ b/example/src/test_verify.ts @@ -16,6 +16,20 @@ import { import { createTanker, clearTankerDataDirs } from './tests'; import base64 from 'react-native-base64'; +function extractOidcSubject(jwt: string): string { + const b64UrlNoPadBody = jwt.split('.')[1]!; + const b64NoPadBody = b64UrlNoPadBody + .replaceAll('-', '+') + .replaceAll('_', '/'); + + const remainder = b64NoPadBody.length % 4; + const paddingBytes = (4 - remainder) % 4; + const b64Body = b64NoPadBody + '='.repeat(paddingBytes); + + const body = base64.decode(b64Body); + return JSON.parse(body).sub; +} + export const verifyTests = () => { describe('Verify tests', () => { let tanker: Tanker; @@ -142,6 +156,22 @@ export const verifyTests = () => { ).is.rejectedWith(InvalidArgument); }); + it('fails to register with preverified oidc', async () => { + const oidcConfig = await getOidcConfig(); + const appResponse = await setAppOidcConfig(oidcConfig); + const oidcProviderResponse = appResponse.oidc_providers[0]!!; + + await tanker.start(identity); + await expect( + tanker.registerIdentity({ + preverifiedOidcSubject: 'martine', + oidcProviderId: oidcProviderResponse.id, + }) + ).is.rejectedWith(InvalidArgument); + + await setAppOidcConfig(undefined); // Cleanup + }); + it('fails to verify with preverified email', async () => { const email = 'bob@burger.io'; await tanker.start(identity); @@ -174,6 +204,35 @@ export const verifyTests = () => { await secondDevice.stop(); }); + it('fails to verify with preverified oidc', async () => { + const oidcConfig = await getOidcConfig(); + const appResponse = await setAppOidcConfig(oidcConfig); + const oidcProviderResponse = appResponse.oidc_providers[0]!!; + + await setAppOidcConfig(oidcConfig); + const oidcToken = await getGoogleIdToken( + oidcConfig, + oidcConfig.users.martine!! + ); + + await tanker.start(identity); + await tanker.setOidcTestNonce(await tanker.createOidcNonce()); + await tanker.registerIdentity({ oidcIdToken: oidcToken }); + await tanker.stop(); + + let secondDevice = await createTanker(); + await secondDevice.start(identity); + await expect( + secondDevice.verifyIdentity({ + preverifiedOidcSubject: 'martine', + oidcProviderId: oidcProviderResponse.id, + }) + ).is.rejectedWith(InvalidArgument); + + await secondDevice.stop(); + await setAppOidcConfig(undefined); // Cleanup + }); + it('can use set verification method with preverified email', async () => { const email = 'bob@burger.io'; const pass = { passphrase: 'Shame, dring dring' }; @@ -250,6 +309,49 @@ export const verifyTests = () => { await secondDevice.stop(); }); + it('can use set verification method with preverified oidc', async () => { + const oidcConfig = await getOidcConfig(); + const appResponse = await setAppOidcConfig(oidcConfig); + const oidcProviderResponse = appResponse.oidc_providers[0]!!; + + await setAppOidcConfig(oidcConfig); + const oidcToken = await getGoogleIdToken( + oidcConfig, + oidcConfig.users.martine!! + ); + const oidcSubject = extractOidcSubject(oidcToken); + + const pass = { passphrase: 'Malotru' }; + await tanker.start(identity); + await tanker.registerIdentity(pass); + await tanker.setVerificationMethod({ + preverifiedOidcSubject: oidcSubject, + oidcProviderId: oidcProviderResponse.id, + }); + expect(await tanker.getVerificationMethods()).to.have.deep.members([ + { + type: 'passphrase', + }, + { + type: 'oidcIdToken', + providerId: oidcProviderResponse.id, + providerDisplayName: oidcProviderResponse.display_name, + }, + ]); + await tanker.stop(); + + let secondDevice = await createTanker(); + await secondDevice.start(identity); + await secondDevice.setOidcTestNonce(await secondDevice.createOidcNonce()); + await secondDevice.verifyIdentity({ + oidcIdToken: oidcToken, + }); + expect(secondDevice.status).eq(Tanker.statuses.READY); + + await secondDevice.stop(); + await setAppOidcConfig(undefined); // Cleanup + }); + it('can register an e2e passphrase', async () => { const e2ePassphrase = 'So we all are agreed'; await tanker.start(identity); diff --git a/ios/Utils.swift b/ios/Utils.swift index e83ed52..3db84fc 100644 --- a/ios/Utils.swift +++ b/ios/Utils.swift @@ -30,6 +30,9 @@ public class Utils: NSObject { if let preverifiedPhoneNumber = dict["preverifiedPhoneNumber"] as? String { return Verification(preverifiedPhoneNumber: preverifiedPhoneNumber) } + if let preverifiedOIDCSubject = dict["preverifiedOidcSubject"] as? String, let providerID = dict["oidcProviderId"] as? String { + return Verification(preverifiedOIDCSubject: preverifiedOIDCSubject, providerID: providerID) + } if let e2ePassphrase = dict["e2ePassphrase"] as? String { return Verification(e2ePassphrase: e2ePassphrase) } diff --git a/src/verification.ts b/src/verification.ts index c417bd9..4a61c50 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -50,6 +50,10 @@ export type PreverifiedEmailVerification = { preverifiedEmail: string }; export type PreverifiedPhoneNumberVerification = { preverifiedPhoneNumber: string; }; +export type PreverifiedOidcVerification = { + preverifiedOidcSubject: string; + oidcProviderId: string; +}; export type E2ePassphraseVerification = { e2ePassphrase: string }; export type Verification = @@ -61,6 +65,7 @@ export type Verification = | PhoneNumberVerification | PreverifiedEmailVerification | PreverifiedPhoneNumberVerification + | PreverifiedOidcVerification | E2ePassphraseVerification; export type VerificationOptions = { @@ -77,6 +82,7 @@ const validMethods = [ 'phoneNumber', 'preverifiedEmail', 'preverifiedPhoneNumber', + 'preverifiedOidcSubject', 'e2ePassphrase', ]; const validKeys = [ @@ -167,6 +173,22 @@ export const assertVerification = (verification: Verification) => { verification.preverifiedPhoneNumber, 'verification.preverifiedPhoneNumber' ); + } else if ('preverifiedOidcSubject' in verification) { + assertNotEmptyString( + verification.preverifiedOidcSubject, + 'verification.preverifiedOidcSubject' + ); + if (!('oidcProviderId' in verification)) { + throw new InvalidArgument( + 'verification', + 'oidc pre-verification should also have a oidcProviderId', + verification + ); + } + assertNotEmptyString( + verification.oidcProviderId, + 'verification.oidcProviderId' + ); } else if ('e2ePassphrase' in verification) { assertNotEmptyString( verification.e2ePassphrase,