Skip to content

Commit

Permalink
runfix: Fix certificate renewal process [WPB-6795] (#16878)
Browse files Browse the repository at this point in the history
  • Loading branch information
atomrc authored Feb 22, 2024
1 parent b7bf902 commit efee5aa
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 129 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"@peculiar/x509": "1.9.6",
"@wireapp/avs": "9.6.11",
"@wireapp/commons": "5.2.4",
"@wireapp/core": "44.0.13",
"@wireapp/core": "45.0.0",
"@wireapp/react-ui-kit": "9.15.4",
"@wireapp/store-engine-dexie": "2.1.7",
"@wireapp/webapp-events": "0.20.1",
Expand Down Expand Up @@ -36,7 +36,7 @@
"long": "5.2.3",
"markdown-it": "14.0.0",
"murmurhash": "2.0.1",
"oidc-client-ts": "^2.4.0",
"oidc-client-ts": "3.0.1",
"platform": "1.3.6",
"react": "18.2.0",
"react-dom": "18.2.0",
Expand Down
26 changes: 3 additions & 23 deletions src/script/E2EIdentity/E2EIdentityEnrollment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {UserState} from 'src/script/user/UserState';
import {getCertificateDetails} from 'Util/certificateDetails';
import * as util from 'Util/util';

import {E2EIHandler, E2EIHandlerStep} from './E2EIdentityEnrollment';
import {E2EIHandler} from './E2EIdentityEnrollment';
import {hasActiveCertificate} from './E2EIdentityVerification';
import {getModalOptions, ModalType} from './Modals';
import {OIDCServiceStore} from './OIDCService/OIDCServiceStorage';
Expand Down Expand Up @@ -122,26 +122,6 @@ describe('E2EIHandler', () => {
void instance.attemptEnrollment();
await wait(1);
expect(container.resolve(Core).service?.e2eIdentity?.initialize).toHaveBeenCalled();
expect(instance['currentStep']).toBe(E2EIHandlerStep.INITIALIZED);
});

it('should set currentStep to SUCCESS when enrollE2EI is called and enrollment succeeds', async () => {
jest.spyOn(container.resolve(Core), 'enrollE2EI').mockResolvedValueOnce({status: 'successful'});

const instance = await E2EIHandler.getInstance().initialize(params);
void instance['enroll']();
await wait(1);
expect(instance['currentStep']).toBe(E2EIHandlerStep.SUCCESS);
});

it('should set currentStep to ERROR when enrolE2EI is called and enrolment fails', async () => {
// Mock the Core service to return an error
jest.spyOn(container.resolve(Core), 'enrollE2EI').mockImplementationOnce(jest.fn(() => Promise.reject()));

const instance = await E2EIHandler.getInstance().initialize(params);
void instance['enroll']();
await wait(1);
expect(instance['currentStep']).toBe(E2EIHandlerStep.ERROR);
});

it('should display user info message when initialized', async () => {
Expand All @@ -156,7 +136,7 @@ describe('E2EIHandler', () => {
});

it('should throw error if trying to enroll with no config given', async () => {
await expect(E2EIHandler.getInstance().enroll()).rejects.toEqual(
await expect(E2EIHandler.getInstance()['enroll']()).rejects.toEqual(
new Error('Trying to enroll for E2EI without initializing the E2EIHandler'),
);
});
Expand Down Expand Up @@ -229,7 +209,7 @@ describe('E2EIHandler', () => {
jest.spyOn(container.resolve(Core).service!.e2eIdentity!, 'isEnrollmentInProgress').mockResolvedValue(true);

// Spy on enroll to check if it's called
const enrollSpy = jest.spyOn(handler, 'enroll');
const enrollSpy = jest.spyOn(handler as any, 'enroll');

// Initialize E2EI
await handler.initialize(params);
Expand Down
90 changes: 34 additions & 56 deletions src/script/E2EIdentity/E2EIdentityEnrollment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import {TimeInMillis} from '@wireapp/commons/lib/util/TimeUtil';
import {amplify} from 'amplify';
import {User} from 'oidc-client-ts';
import {container} from 'tsyringe';

import {TypedEventEmitter} from '@wireapp/commons';
Expand Down Expand Up @@ -72,7 +71,6 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
private readonly core = container.resolve(Core);
private readonly userState = container.resolve(UserState);
private config?: EnrollmentConfig;
private currentStep: E2EIHandlerStep = E2EIHandlerStep.UNINITIALIZED;
private oidcService?: OIDCService;
public certificateTtl?: number;

Expand Down Expand Up @@ -119,7 +117,7 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
* @returns
*/
public isE2EIEnabled() {
return this.currentStep !== E2EIHandlerStep.UNINITIALIZED;
return !!this.config;
}

public async initialize({discoveryUrl, gracePeriodInSeconds}: E2EIHandlerParams) {
Expand All @@ -136,7 +134,6 @@ export class E2EIHandler extends TypedEventEmitter<Events> {

await this.coreE2EIService.initialize(discoveryUrl);

this.currentStep = E2EIHandlerStep.INITIALIZED;
this.emit('initialized', {enrollmentConfig: this.config});
return this;
}
Expand Down Expand Up @@ -200,15 +197,7 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
*/
private async renewCertificate(): Promise<void> {
try {
this.oidcService = this.createOIDCService();
// Use the oidc service to get the user data via silent authentication (refresh token)
const userData = await this.oidcService.handleSilentAuthentication();

if (!userData) {
throw new Error('Received no user data from OIDC service');
}
// renew without user action
await this.enroll(userData);
await this.enroll(true);
} catch (error) {
this.logger.error('Silent authentication with refresh token failed', error);

Expand Down Expand Up @@ -252,66 +241,68 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
await this.coreE2EIService.clearAllProgress();
}

public async enroll(userData?: User) {
private async getOAuthToken(
silent: boolean,
challengeData?: {keyAuth: string; challenge: {url: string; target: string}},
) {
let userData;

if (challengeData) {
// If a challengeData is provided, that means we are at the beginning of the enrollment process
// We need to first authenticate the user (either silently if we are renewing the certificate, or by redirection if it an initial enrollment)
const {challenge, keyAuth} = challengeData;
OIDCServiceStore.store.targetURL(challenge.target);
const oidcService = this.createOIDCService();
userData = await oidcService.authenticate(keyAuth, challenge.url, silent);
} else {
const oidcService = this.createOIDCService();
// If there is no challengeData, that means we have already authenticated the user and we just need to get the userdata
userData = await oidcService.getUser();
}
if (!userData) {
throw new Error('No user data received');
}
return userData.id_token;
}

private async enroll(attemptSilentAuth = false) {
if (!this.config) {
throw new Error('Trying to enroll for E2EI without initializing the E2EIHandler');
}
try {
// Notify user about E2EI enrolment in progress
this.currentStep = E2EIHandlerStep.ENROLL;
const isCertificateRenewal = await hasActiveCertificate();
this.showLoadingMessage();

if (!userData) {
// If the enrolment is in progress, we need to get the id token from the oidc service, since oauth should have already been completed
if (await this.coreE2EIService.isEnrollmentInProgress()) {
// The redirect-url which is needed inside the OIDCService is stored in the OIDCServiceStore previously
this.oidcService = this.createOIDCService();
userData = await this.oidcService.handleAuthentication();
if (!userData) {
throw new Error('Received no user data from OIDC service');
}
}
}
const oAuthIdToken = userData?.id_token;

const displayName = this.userState.self()?.name();
const handle = this.userState.self()?.username();
const teamId = this.userState.self()?.teamId;
// If the user has no username or handle, we cannot enroll
if (!displayName || !handle || !teamId) {
throw new Error('Username, handle or teamId not found');
}
const enrollmentState = await this.core.enrollE2EI({

await this.core.enrollE2EI({
discoveryUrl: this.config.discoveryUrl,
displayName,
handle,
teamId,
oAuthIdToken,
getOAuthToken: async authenticationChallenge => {
return this.getOAuthToken(attemptSilentAuth, authenticationChallenge);
},
certificateTtl: this.certificateTtl,
});
// If the data is false or we dont get the ACMEChallenge, enrolment failed

if (enrollmentState.status === 'authentication') {
// If the data is authentication flow data, we need to kick off the oauth flow to get an oauth token
const {challenge, keyAuth} = enrollmentState.authenticationChallenge;
OIDCServiceStore.store.targetURL(challenge.target);
this.oidcService = this.createOIDCService();
await this.oidcService.authenticate(keyAuth, challenge.url);
}

// Notify user about E2EI enrolment success
// This setTimeout is needed because there was a timing with the success modal and the loading modal
setTimeout(removeCurrentModal, 0);

this.currentStep = E2EIHandlerStep.SUCCESS;
// clear the oidc service progress/data and successful enrolment
await this.cleanUp(false);

await this.showSuccessMessage(isCertificateRenewal);
this.emit('identityUpdated', {enrollmentConfig: this.config!});
} catch (error) {
this.currentStep = E2EIHandlerStep.ERROR;
this.logger.error('E2EI enrollment failed', error);

setTimeout(removeCurrentModal, 0);
Expand All @@ -320,10 +311,6 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
}

private showLoadingMessage(isCertificateRenewal = false): void {
if (this.currentStep !== E2EIHandlerStep.ENROLL) {
return;
}

const {modalOptions, modalType} = getModalOptions({
type: ModalType.LOADING,
hideClose: true,
Expand All @@ -335,10 +322,6 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
}

private async showSuccessMessage(isCertificateRenewal = false) {
if (this.currentStep !== E2EIHandlerStep.SUCCESS) {
return;
}

return new Promise<void>(resolve => {
const {modalOptions, modalType} = getModalOptions({
type: ModalType.SUCCESS,
Expand All @@ -357,10 +340,6 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
}

private async showErrorMessage(): Promise<void> {
if (this.currentStep !== E2EIHandlerStep.ERROR) {
return;
}

// Remove the url parameters of the failed enrolment
removeUrlParameters();
// Clear the oidc service progress
Expand All @@ -376,12 +355,12 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
hideClose: true,
hideSecondary: disableSnooze,
primaryActionFn: async () => {
this.currentStep = E2EIHandlerStep.INITIALIZED;
await this.enroll();
resolve();
},
secondaryActionFn: async () => {
await this.startEnrollment(ModalType.ENROLL);
this.config?.timer.snooze();
this.showSnoozeConfirmationModal();
resolve();
},
extraParams: {
Expand All @@ -407,7 +386,6 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
resolve();
},
secondaryActionFn: () => {
this.currentStep = E2EIHandlerStep.SNOOZE;
this.config?.timer.snooze();
this.showSnoozeConfirmationModal();
resolve();
Expand Down
26 changes: 8 additions & 18 deletions src/script/E2EIdentity/OIDCService/OIDCService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import {KeyAuth} from '@wireapp/core/lib/messagingProtocols/mls';
import {UserManager, User, UserManagerSettings, WebStorageStateStore} from 'oidc-client-ts';
import {UserManager, UserManagerSettings, WebStorageStateStore} from 'oidc-client-ts';

import {clearKeysStartingWith} from 'Util/localStorage';

Expand Down Expand Up @@ -61,7 +61,7 @@ export class OIDCService {
this.userManager = new UserManager(dexioConfig);
}

public async authenticate(keyAuth: KeyAuth, challengeUrl: string): Promise<void> {
public async authenticate(keyAuth: KeyAuth, challengeUrl: string, silent: boolean) {
// New claims value for keycloak
const claims = {
id_token: {
Expand All @@ -70,22 +70,16 @@ export class OIDCService {
},
};

await this.userManager.signinRedirect({
extraQueryParams: {shouldBeRedirectedByProxy: true, claims: JSON.stringify(claims)},
});
const params = {shouldBeRedirectedByProxy: true, claims: JSON.stringify(claims)};
return silent
? this.userManager.signinSilent({extraTokenParams: params})
: this.userManager.signinRedirect({extraQueryParams: params});
}

public async handleAuthentication(): Promise<User | undefined> {
public getUser() {
// Remove the hash (hash router) from the url before processing
const url = window.location.href.replace('/#', '');

const user = await this.userManager.signinCallback(url);

if (!user) {
return undefined;
}

return user;
return this.userManager.signinCallback(url);
}

public clearProgress(includeUserData: boolean = false): Promise<void> {
Expand All @@ -97,8 +91,4 @@ export class OIDCService {
}
return this.userManager.clearStaleState();
}

public async handleSilentAuthentication() {
return this.userManager.signinSilent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const E2EIVerificationMessage = ({message, conversation}: E2EIVerificatio

const getCertificate = async () => {
try {
await E2EIHandler.getInstance().enroll();
await E2EIHandler.getInstance().attemptEnrollment();
} catch (error) {
logger.error('Failed to enroll user certificate: ', error);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const E2EICertificateDetails = ({identity, isCurrentDevice}: E2EICertific

const getCertificate = async () => {
try {
await E2EIHandler.getInstance().enroll();
await E2EIHandler.getInstance().attemptEnrollment();
} catch (error) {
logger.error('Cannot get E2EI instance: ', error);
}
Expand Down
Loading

0 comments on commit efee5aa

Please sign in to comment.