diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 840784ff..23109653 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,6 +35,7 @@ jobs: BROWSER_STACK_ACCESS_KEY: ${{ secrets.BROWSER_STACK_ACCESS_KEY }} BROWSER_STACK_USERNAME: ${{ secrets.BROWSER_STACK_USERNAME }} TANKER_APPD_URL: ${{ secrets.TANKER_APPD_URL }} + TANKER_FAKE_OIDC_URL: ${{ secrets.TANKER_FAKE_OIDC_URL }} TANKER_FILEKIT_BUCKET_NAME: ${{ secrets.TANKER_FILEKIT_BUCKET_NAME }} TANKER_FILEKIT_BUCKET_REGION: ${{ secrets.TANKER_FILEKIT_BUCKET_REGION }} TANKER_FILEKIT_CLIENT_ID: ${{ secrets.TANKER_FILEKIT_CLIENT_ID }} @@ -70,6 +71,7 @@ jobs: BROWSER_STACK_ACCESS_KEY: ${{ secrets.BROWSER_STACK_ACCESS_KEY }} BROWSER_STACK_USERNAME: ${{ secrets.BROWSER_STACK_USERNAME }} TANKER_APPD_URL: ${{ secrets.TANKER_APPD_URL }} + TANKER_FAKE_OIDC_URL: ${{ secrets.TANKER_FAKE_OIDC_URL }} TANKER_FILEKIT_BUCKET_NAME: ${{ secrets.TANKER_FILEKIT_BUCKET_NAME }} TANKER_FILEKIT_BUCKET_REGION: ${{ secrets.TANKER_FILEKIT_BUCKET_REGION }} TANKER_FILEKIT_CLIENT_ID: ${{ secrets.TANKER_FILEKIT_CLIENT_ID }} diff --git a/config/karma/tanker.test.config.js b/config/karma/tanker.test.config.js index 23f5a1ce..5e79f2bb 100644 --- a/config/karma/tanker.test.config.js +++ b/config/karma/tanker.test.config.js @@ -18,6 +18,8 @@ const plugin = new webpack.EnvironmentPlugin({ TANKER_OIDC_MARTINE_EMAIL: null, TANKER_OIDC_MARTINE_REFRESH_TOKEN: null, + TANKER_FAKE_OIDC_URL: null, + TANKER_FILEKIT_BUCKET_NAME: null, TANKER_FILEKIT_BUCKET_REGION: null, TANKER_FILEKIT_CLIENT_ID: null, diff --git a/package.json b/package.json index ed085473..0b92e61a 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "Firefox ESR", "node >= 18", "not IE 11", + "not op_mini all", "not dead" ], "nyc": { diff --git a/packages/client-browser/src/index.ts b/packages/client-browser/src/index.ts index 7bcb3e62..8579230a 100644 --- a/packages/client-browser/src/index.ts +++ b/packages/client-browser/src/index.ts @@ -11,6 +11,10 @@ class Tanker extends TankerCore { constructor(options: TankerOptions) { super(optionsWithDefaults(options, defaultOptions)); } + + // authenticateWithIdP() is only exposed in client-browser because it relies on Cookies + // and Cookies are not handled by node fetch + authenticateWithIdP = this._authenticateWithIdP; } export { errors, fromBase64, toBase64, prehashPassword, Padding } from '@tanker/core'; diff --git a/packages/core/src/LocalUser/Manager.ts b/packages/core/src/LocalUser/Manager.ts index 4d764dfd..220febc9 100644 --- a/packages/core/src/LocalUser/Manager.ts +++ b/packages/core/src/LocalUser/Manager.ts @@ -29,6 +29,7 @@ import type { PreverifiedVerification, RemoteVerificationWithToken, LegacyEmailVerificationMethod, + OidcAuthorizationCodeVerification, } from './types'; import { isE2eVerification } from './types'; import { generateUserCreation, generateDeviceFromGhostDevice, generateGhostDevice } from './UserCreation'; @@ -230,7 +231,7 @@ export class LocalUserManager extends EventEmitter { first_device_creation: firstDeviceBlock, }; - if ('email' in verification || 'passphrase' in verification || 'oidcIdToken' in verification || 'phoneNumber' in verification) { + if ('email' in verification || 'passphrase' in verification || 'oidcIdToken' in verification || 'oidcAuthorizationCode' in verification || 'phoneNumber' in verification) { request.v2_encrypted_verification_key = ghostDeviceToEncryptedVerificationKey(ghostDevice, this._localUser.userSecret); request.verification = await formatVerificationRequest(verification, this); request.verification.with_token = verification.withToken; // May be undefined @@ -388,4 +389,6 @@ export class LocalUserManager extends EventEmitter { await oidcNonceManage.removeOidcNonce(nonce); return res; }; + + createOidcAuthorizationCode = async (oidcProviderId: string): Promise => this._client.oidcSignIn(oidcProviderId); } diff --git a/packages/core/src/LocalUser/requests.ts b/packages/core/src/LocalUser/requests.ts index 61301b7d..0bc96e9a 100644 --- a/packages/core/src/LocalUser/requests.ts +++ b/packages/core/src/LocalUser/requests.ts @@ -23,6 +23,11 @@ type OidcIdTokenRequest = { oidc_challenge_signature: b64string; oidc_test_nonce: string | undefined; }; +type OidcAuthorizationCode = { + oidc_provider_id: string; + oidc_authorization_code: string; + oidc_state: string; +}; type PhoneNumberRequest = { phone_number: string; encrypted_phone_number: Uint8Array; @@ -41,6 +46,7 @@ export type PreverifiedVerificationRequest = Preverified | Preveri export type VerificationRequestWithToken = WithToken | WithVerificationCode +| WithToken | WithToken | WithVerificationCode | WithToken; @@ -121,6 +127,14 @@ export const formatVerificationRequest = async ( }; } + if ('oidcAuthorizationCode' in verification) { + return { + oidc_authorization_code: verification.oidcAuthorizationCode, + oidc_provider_id: verification.oidcProviderId, + oidc_state: verification.oidcState, + }; + } + if ('preverifiedEmail' in verification) { return { hashed_email: generichash(utils.fromString(verification.preverifiedEmail)), diff --git a/packages/core/src/LocalUser/types.ts b/packages/core/src/LocalUser/types.ts index 3fa14680..672574c7 100644 --- a/packages/core/src/LocalUser/types.ts +++ b/packages/core/src/LocalUser/types.ts @@ -18,6 +18,7 @@ export type EmailVerification = { email: string; verificationCode: string; }; export type PassphraseVerification = { passphrase: string; }; export type E2ePassphraseVerification = { e2ePassphrase: string; }; export type KeyVerification = { verificationKey: string; }; +export type OidcAuthorizationCodeVerification = { oidcProviderId: string; oidcAuthorizationCode: string; oidcState: string; }; export type OidcVerification = { oidcIdToken: string; }; export type PhoneNumberVerification = { phoneNumber: string; verificationCode: string; }; export type PreverifiedEmailVerification = { preverifiedEmail: string; }; @@ -30,6 +31,7 @@ export type E2eRemoteVerification = E2ePassphraseVerification; export type RemoteVerification = E2eRemoteVerification | EmailVerification | PassphraseVerification +| OidcAuthorizationCodeVerification | OidcVerification | PhoneNumberVerification | PreverifiedEmailVerification @@ -44,9 +46,9 @@ export type RemoteVerificationWithToken = RemoteVerification & WithTokenOptions; export type VerificationOptions = { withSessionToken?: boolean; allowE2eMethodSwitch?: boolean; }; const validE2eMethods = ['e2ePassphrase']; -const validNonE2eMethods = ['email', 'passphrase', 'verificationKey', 'oidcIdToken', 'phoneNumber', 'preverifiedEmail', 'preverifiedPhoneNumber', 'preverifiedOidcSubject']; +const validNonE2eMethods = ['email', 'passphrase', 'verificationKey', 'oidcIdToken', 'oidcAuthorizationCode', 'phoneNumber', 'preverifiedEmail', 'preverifiedPhoneNumber', 'preverifiedOidcSubject']; const validMethods = [...validE2eMethods, ...validNonE2eMethods]; -const validKeys = [...validMethods, 'verificationCode', 'oidcProviderId']; +const validKeys = [...validMethods, 'verificationCode', 'oidcProviderId', 'oidcState']; const validVerifOptionsKeys = ['withSessionToken', 'allowE2eMethodSwitch']; @@ -93,6 +95,10 @@ export const assertVerification = (verification: Verification) => { if ('testNonce' in verification) { console.warn("'testNonce' field should be used for tests purposes only. It will be rejected for non-test Tanker application"); } + } else if ('oidcAuthorizationCode' in verification) { + assertNotEmptyString(verification.oidcProviderId, 'verification.oidcProviderId'); + assertNotEmptyString(verification.oidcAuthorizationCode, 'verification.oidcAuthorizationCode'); + assertNotEmptyString(verification.oidcState, 'verification.oidcState'); } else if ('preverifiedEmail' in verification) { assertNotEmptyString(verification.preverifiedEmail, 'verification.preverifiedEmail'); } else if ('preverifiedPhoneNumber' in verification) { diff --git a/packages/core/src/Network/Client.ts b/packages/core/src/Network/Client.ts index 635ef68b..d1036a38 100644 --- a/packages/core/src/Network/Client.ts +++ b/packages/core/src/Network/Client.ts @@ -10,6 +10,7 @@ import { signChallenge } from './Authenticator'; import { genericErrorHandler } from './ErrorHandler'; import { b64RequestObject, urlize } from './utils'; import type { ProvisionalKeysRequest, SetVerificationMethodRequest, VerificationRequest } from '../LocalUser/requests'; +import type { OidcAuthorizationCodeVerification } from '../LocalUser/types'; import type { PublicProvisionalIdentityTarget } from '../Identity/identity'; import type { FileUploadURLResponse, FileDownloadURLResponse, @@ -103,7 +104,7 @@ export class Client { // Simple _fetch wrapper with: // - proper headers set (sdk info and authorization) // - generic error handling - _baseApiCall = async (path: string, init?: RequestInit): Promise => { + _baseApiCall = async (path: string, authenticated: boolean, init?: RequestInit): Promise => { try { if (!path || path[0] !== '/') { throw new InvalidArgument('"path" should be non empty and start with "/"'); @@ -114,7 +115,7 @@ export class Client { headers['X-Tanker-Sdktype'] = this._sdkType; headers['X-Tanker-Sdkversion'] = this._sdkVersion; - if (this._accessToken) { + if (authenticated && this._accessToken) { headers['Authorization'] = `Bearer ${this._accessToken}`; // eslint-disable-line dot-notation } @@ -196,7 +197,7 @@ export class Client { }, }; - return retry(() => this._baseApiCall(path, init), retryOptions); + return retry(() => this._baseApiCall(path, true, init), retryOptions); }); _authenticate = this._cancelable((): Promise => { @@ -216,14 +217,14 @@ export class Client { const auth = async () => { const { challenge } = await this._cancelable( - () => this._baseApiCall(`/devices/${urlize(deviceId)}/challenges`, { method: 'POST' }), + () => this._baseApiCall(`/devices/${urlize(deviceId)}/challenges`, false, { method: 'POST' }), )(); const signature = signChallenge(deviceSignatureKeyPair, challenge); const signaturePublicKey = deviceSignatureKeyPair.publicKey; const { access_token: accessToken } = await this._cancelable( - () => this._baseApiCall(`/devices/${urlize(deviceId)}/sessions`, { + () => this._baseApiCall(`/devices/${urlize(deviceId)}/sessions`, false, { method: 'POST', body: JSON.stringify(b64RequestObject({ signature, challenge, signature_public_key: signaturePublicKey })), headers: { 'Content-Type': 'application/json' }, @@ -446,6 +447,19 @@ export class Client { return challenge; }; + oidcSignIn = async (oidcProviderId: string): Promise => { + const { code, state } = await this._baseApiCall( + `/oidc/${oidcProviderId}/signin?user_id=${urlize(this._userId)}`, + false, + { credentials: 'include' }, + ); + return { + oidcProviderId, + oidcAuthorizationCode: code, + oidcState: state, + }; + }; + getResourceKey = async (resourceId: Uint8Array): Promise => { const query = `resource_ids[]=${urlize(resourceId)}`; const { resource_keys: resourceKeys } = await this._apiCall(`/resource-keys?${query}`); @@ -613,7 +627,7 @@ export class Client { // 204: session successfully deleted // 401: session already expired // other: something unexpected happened -> ignore and continue closing ¯\_(ツ)_/¯ - await this._baseApiCall(path, { method: 'DELETE' }).catch((error: TankerError) => { + await this._baseApiCall(path, true, { method: 'DELETE' }).catch((error: TankerError) => { if (error.httpStatus !== 401) { console.error('Error while closing the network client', error); } diff --git a/packages/core/src/Network/ErrorHandler.ts b/packages/core/src/Network/ErrorHandler.ts index 7cb2da45..56146065 100644 --- a/packages/core/src/Network/ErrorHandler.ts +++ b/packages/core/src/Network/ErrorHandler.ts @@ -7,6 +7,7 @@ const apiCodeErrorMap: Record> = { empty_user_group: InvalidArgument, feature_not_enabled: PreconditionFailed, group_too_big: GroupTooBig, + invalid_authorization_code: InvalidVerification, invalid_delegation_signature: InvalidVerification, invalid_oidc_id_token: InvalidVerification, invalid_passphrase: InvalidVerification, @@ -14,6 +15,9 @@ const apiCodeErrorMap: Record> = { invalid_verification_code: InvalidVerification, missing_user_group_members: InvalidArgument, not_a_user_group_member: InvalidArgument, + oidc_provider_interaction_required: PreconditionFailed, + oidc_provider_not_configured: PreconditionFailed, + oidc_provider_not_supported: PreconditionFailed, provisional_identity_already_attached: IdentityAlreadyAttached, too_many_attempts: TooManyAttempts, too_many_requests: TooManyRequests, @@ -22,7 +26,6 @@ const apiCodeErrorMap: Record> = { verification_code_not_found: InvalidVerification, verification_key_not_found: PreconditionFailed, verification_method_not_set: PreconditionFailed, - oidc_provider_not_configured: PreconditionFailed, }; export const genericErrorHandler = (apiMethod: string, apiRoute: string, error: Record) => { diff --git a/packages/core/src/Session/Session.ts b/packages/core/src/Session/Session.ts index 44fd3acc..a396d4da 100644 --- a/packages/core/src/Session/Session.ts +++ b/packages/core/src/Session/Session.ts @@ -186,6 +186,7 @@ export class Session extends EventEmitter { setVerificationMethod = this._forward(this._getLocalUserManager, 'setVerificationMethod'); getVerificationMethods = this._forward(this._getLocalUserManager, 'getVerificationMethods'); generateVerificationKey = this._forward(this._getLocalUserManager, 'generateVerificationKey'); + createOidcAuthorizationCode = this._forward(this._getLocalUserManager, 'createOidcAuthorizationCode'); getSessionToken = this._forward(this._getLocalUserManager, 'getSessionToken'); diff --git a/packages/core/src/Tanker.ts b/packages/core/src/Tanker.ts index 462d7800..ac479221 100644 --- a/packages/core/src/Tanker.ts +++ b/packages/core/src/Tanker.ts @@ -30,6 +30,7 @@ import type { PhoneNumberVerification, LegacyEmailVerificationMethod, PreverifiedVerification, + OidcAuthorizationCodeVerification, } from './LocalUser/types'; import { extractUserData } from './LocalUser/UserData'; @@ -632,4 +633,11 @@ export class Tanker extends EventEmitter { return this.session.createEncryptionSession(encryptionOptions); } + + // authenticateWithIdP() is only exposed in client-browser because it relies on Cookies + // and Cookies are not handled by node fetch + async _authenticateWithIdP(oidcProviderId: string): Promise { + assertStatus(this.status, [statuses.IDENTITY_REGISTRATION_NEEDED, statuses.IDENTITY_VERIFICATION_NEEDED, statuses.READY], 'authenticate with an Identity provider'); + return this.session.createOidcAuthorizationCode(oidcProviderId); + } } diff --git a/packages/core/src/__tests__/Tanker.spec.ts b/packages/core/src/__tests__/Tanker.spec.ts index 46b9be15..9bba00d5 100644 --- a/packages/core/src/__tests__/Tanker.spec.ts +++ b/packages/core/src/__tests__/Tanker.spec.ts @@ -40,6 +40,13 @@ describe('Tanker', () => { { passphrase: 12 }, { passphrase: new Uint8Array(12) }, { passphrase: '' }, + { oidcProviderId: '', oidcAuthorizationCode: 'code', oidcState: 'state' }, + { oidcProviderId: 'issuer', oidcAuthorizationCode: '', oidcState: 'state' }, + { oidcProviderId: 'issuer', oidcAuthorizationCode: 'code', oidcState: '' }, + { oidcProviderId: 'issuer', oidcAuthorizationCode: new Uint8Array(12), oidcState: 'state' }, + { oidcProviderId: 'issuer', oidcAuthorizationCode: 'code' }, + { oidcAuthorizationCode: 'code', oidcState: 'state' }, + { oidcProviderId: 'issuer', oidcState: 'state' }, { email: 'valid@tanker.io', verificationCode: '12345678', passphrase: 'valid_passphrase' }, // only one method at a time! ]; @@ -218,6 +225,14 @@ describe('Tanker', () => { }); }); + describe('authenticateWithIdP', () => { + it('throws when tanker is STOPPED', async () => { + // We are testing a private method (only public in client-browser) + // eslint-disable-next-line no-underscore-dangle + await expect(tanker._authenticateWithIdP('SomeBase64')).to.be.rejectedWith(PreconditionFailed); + }); + }); + describe('enrollUser', () => { it('throws when tanker is not STOPPED', async () => { const illegalStatuses = [ diff --git a/packages/functional-tests/src/enroll.ts b/packages/functional-tests/src/enroll.ts index 3dfb47c5..55432dbe 100644 --- a/packages/functional-tests/src/enroll.ts +++ b/packages/functional-tests/src/enroll.ts @@ -18,14 +18,14 @@ export const generateEnrollTests = (args: TestArgs) => { let emailVerification: PreverifiedEmailVerification; let phoneNumberVerification: PreverifiedPhoneNumberVerification; let oidcVerification: PreverifiedOidcVerification; - let providerID: string; + let providerId: string; before(async () => { server = args.makeTanker(); ({ appHelper } = args); const config = await appHelper.setOidc(); - providerID = config.app.oidc_providers[0]!.id; + providerId = config.app.oidc_providers[0]!.id; emailVerification = { preverifiedEmail: email, @@ -34,7 +34,7 @@ export const generateEnrollTests = (args: TestArgs) => { preverifiedPhoneNumber: phoneNumber, }; oidcVerification = { - oidcProviderId: providerID, + oidcProviderId: providerId, preverifiedOidcSubject: 'a subject', }; }); diff --git a/packages/functional-tests/src/helpers/AppHelper.ts b/packages/functional-tests/src/helpers/AppHelper.ts index d00dbe2c..a6ec4d15 100644 --- a/packages/functional-tests/src/helpers/AppHelper.ts +++ b/packages/functional-tests/src/helpers/AppHelper.ts @@ -54,16 +54,18 @@ export class AppHelper { }); } - async setOidc(provider: 'google' | 'pro-sante-bas' | 'pro-sante-bas-no-expiry' = 'google') { + async setOidc(provider: 'google' | 'pro-sante-bas' | 'pro-sante-bas-no-expiry' | 'fake-oidc' = 'google') { const providers = { google: oidcSettings.googleAuth.clientId, 'pro-sante-bas': 'doctolib-dev', 'pro-sante-bas-no-expiry': 'doctolib-dev', + 'fake-oidc': 'tanker', }; const providersIssuer = { google: 'https://accounts.google.com', 'pro-sante-bas': 'https://auth.bas.psc.esante.gouv.fr/auth/realms/esante-wallet', 'pro-sante-bas-no-expiry': 'https://auth.bas.psc.esante.gouv.fr/auth/realms/esante-wallet', + 'fake-oidc': `${oidcSettings.fakeOidc.url}/issuer`, }; return this._update({ diff --git a/packages/functional-tests/src/helpers/config.ts b/packages/functional-tests/src/helpers/config.ts index 710270c9..b690e66e 100644 --- a/packages/functional-tests/src/helpers/config.ts +++ b/packages/functional-tests/src/helpers/config.ts @@ -25,6 +25,9 @@ const oidcSettings = { }, }, }, + fakeOidc: { + url: process.env['TANKER_FAKE_OIDC_URL'] || '', + }, }; const storageSettings = { s3: { diff --git a/packages/functional-tests/src/verification.ts b/packages/functional-tests/src/verification.ts index de073986..8083633f 100644 --- a/packages/functional-tests/src/verification.ts +++ b/packages/functional-tests/src/verification.ts @@ -3,7 +3,7 @@ import { errors, statuses } from '@tanker/core'; import type { Tanker, b64string, Verification, VerificationMethod, LegacyEmailVerificationMethod } from '@tanker/core'; import { tcrypto, utils } from '@tanker/crypto'; import { fetch } from '@tanker/http-utils'; -import { expect, uuid } from '@tanker/test-utils'; +import { expect, isBrowser, uuid } from '@tanker/test-utils'; import { createProvisionalIdentity } from '@tanker/identity'; import type { AppHelper, TestArgs } from './helpers'; @@ -891,6 +891,95 @@ export const generateVerificationTests = (args: TestArgs) => { }); }); + const authenticateWithIdP = (tanker: Tanker, id: string) => { + // We are testing a private method (only public in client-browser) + // eslint-disable-next-line no-underscore-dangle + return tanker._authenticateWithIdP(id); + }; + + describe('authenticateWithIdP', () => { + it('is restricted to trusted OIDC providers', async () => { + const config = await appHelper.setOidc('google'); + const provider = config.app.oidc_providers[0]!; + + await expect(authenticateWithIdP(bobLaptop, provider.id)).to.be.rejectedWith(errors.PreconditionFailed); + }); + }); + + if (isBrowser()) { + describe('verification by oidc authorization code', () => { + let provider: { id: string, display_name: string }; + + const setFakeOidcSubjectCookie = async (subject: string) => fetch(`${oidcSettings.fakeOidc.url}/issuer/subject`, { + method: 'POST', + body: JSON.stringify({ subject }), + credentials: 'include', + }); + + before(async () => { + const config = await appHelper.setOidc('fake-oidc'); + provider = config.app.oidc_providers[0]!; + }); + + after(async () => { + appHelper.unsetOidc(); + }); + + it('registers and verifies with an oidc authorization code', async () => { + await setFakeOidcSubjectCookie('bob'); + + const verification1 = await authenticateWithIdP(bobLaptop, provider.id); + const verification2 = await authenticateWithIdP(bobLaptop, provider.id); + await expect(bobLaptop.registerIdentity(verification1)).to.be.fulfilled; + + await bobPhone.start(bobIdentity); + expect(bobPhone.status).to.equal(IDENTITY_VERIFICATION_NEEDED); + await expect(bobPhone.verifyIdentity(verification2)).to.be.fulfilled; + expect(bobPhone.status).to.equal(READY); + }); + + it('fails to verify an oidc authorization code twice', async () => { + await setFakeOidcSubjectCookie('bob'); + + const verification1 = await authenticateWithIdP(bobLaptop, provider.id); + await expect(bobLaptop.registerIdentity(verification1)).to.be.fulfilled; + + await bobPhone.start(bobIdentity); + expect(bobPhone.status).to.equal(IDENTITY_VERIFICATION_NEEDED); + await expect(bobPhone.verifyIdentity(verification1)).to.be.rejectedWith(errors.InvalidVerification); + }); + + it('fails to verify an oidc authorization code for the wrong user', async () => { + await setFakeOidcSubjectCookie('bob'); + + const verification1 = await authenticateWithIdP(bobLaptop, provider.id); + await expect(bobLaptop.registerIdentity(verification1)).to.be.fulfilled; + + await setFakeOidcSubjectCookie('alice'); + const verification2 = await authenticateWithIdP(bobLaptop, provider.id); + await bobPhone.start(bobIdentity); + expect(bobPhone.status).to.equal(IDENTITY_VERIFICATION_NEEDED); + await expect(bobPhone.verifyIdentity(verification2)).to.be.rejectedWith(errors.InvalidVerification); + }); + + it('updates and verifies with an oidc authorization code', async () => { + await expect(bobLaptop.registerIdentity({ passphrase: 'plain old passphrase' })).to.be.fulfilled; + await expect(bobLaptop.setVerificationMethod({ + oidcProviderId: provider.id, + preverifiedOidcSubject: 'bob', + })).to.be.fulfilled; + + await bobPhone.start(bobIdentity); + expect(bobPhone.status).to.equal(IDENTITY_VERIFICATION_NEEDED); + + await setFakeOidcSubjectCookie('bob'); + const verification = await authenticateWithIdP(bobPhone, provider.id); + await expect(bobPhone.verifyIdentity(verification)).to.be.fulfilled; + expect(bobPhone.status).to.equal(READY); + }); + }); + } + describe('verification by E2E passphrase', () => { it('can register a verification e2e passphrase and open a new device with it', async () => { await bobLaptop.registerIdentity({ e2ePassphrase: 'passphrase' });