Commit 3d25dffc authored by David Burke's avatar David Burke

Merge branch 'mfa-checking-required' into 'dev'

Store mfa required info

See merge request !223
parents d68c8aec 94fca523
Pipeline #64247186 passed with stages
in 10 minutes and 6 seconds
This diff is collapsed.
......@@ -309,10 +309,6 @@ export const getEnableMfaForm = createSelector(
selectManageMfaState,
fromManageMfa.getForm
);
export const getMfaEnabled = createSelector(
selectManageMfaState,
fromManageMfa.getMfaEnabled
);
export const getMfaStep = createSelector(
selectManageMfaState,
fromManageMfa.getStep
......
......@@ -74,7 +74,8 @@ describe("Change Password Component", () => {
publicKey: "",
rememberMe: false,
userId: 1,
userToken: ""
userToken: "",
mfaRequired: false
})
);
fixture.detectChanges();
......
......@@ -9,6 +9,9 @@ export enum ManageMfaActionTypes {
ACTIVATE_MFA = "[ENABLE MFA] Activate",
ACTIVATE_MFA_SUCCESS = "[ENABLE MFA] Activate Success",
ACTIVATE_MFA_FAILURE = "[ENABLE MFA] Activate Failure",
DEACTIVATE_MFA = "[ENABLE MFA] Deactivate",
DEACTIVATE_MFA_SUCCESS = "[ENABLE MFA] Deactivate Success",
DEACTIVATE_MFA_FAILURE = "[ENABLE MFA] Deactivate Failure",
RESET_FORM = "[Reset] = Reset Form"
}
......@@ -44,6 +47,18 @@ export class ActivateMfaFailure implements Action {
readonly type = ManageMfaActionTypes.ACTIVATE_MFA_FAILURE;
}
export class DeactivateMfa implements Action {
readonly type = ManageMfaActionTypes.DEACTIVATE_MFA;
}
export class DeactivateMfaSuccess implements Action {
readonly type = ManageMfaActionTypes.DEACTIVATE_MFA_SUCCESS;
}
export class DeactivateMfaFailure implements Action {
readonly type = ManageMfaActionTypes.DEACTIVATE_MFA_FAILURE;
}
export class ResetForm implements Action {
readonly type = ManageMfaActionTypes.RESET_FORM;
}
......@@ -56,4 +71,7 @@ export type ManageMfaActions =
| ActivateMfa
| ActivateMfaSuccess
| ActivateMfaFailure
| DeactivateMfa
| DeactivateMfaSuccess
| DeactivateMfaFailure
| ResetForm;
......@@ -10,7 +10,7 @@
Two-Factor Authentication
</h2>
<div *ngIf="!mfaEnabled" class="register-step__feedback">
<div *ngIf="!mfaRequired" class="register-step__feedback">
<div class="u-margin-b-15">
Make your account more secure by requiring a code after you log in.
</div>
......@@ -23,7 +23,7 @@
</div>
</div>
<div *ngIf="mfaEnabled" class="register-step__feedback">
<div *ngIf="mfaRequired" class="register-step__feedback">
<div class="u-margin-b-15">
Two-factor authentication is currently enabled on your account.
</div>
......@@ -33,21 +33,15 @@
</div>
</div>
<form *ngIf="mfaEnabled" (sumbit)="disableMfa()">
<form *ngIf="mfaRequired" (sumbit)="disableMfa.emit()">
<div class="form-field form-field--large">
<app-form-label [isLarge]="true">
Password
</app-form-label>
<div class="form-field form-field--large">
<input type="password" class="form-field__input" />
</div>
<button class="button button--primary" (click)="disableMfa()">
<button class="button button--primary" (click)="deactivateMfa.emit()">
Disable Two-Factor Authentication
</button>
</div>
</form>
<form *ngIf="!mfaEnabled" [ngrxFormState]="form" (submit)="verifyMfa.emit()">
<form *ngIf="!mfaRequired" [ngrxFormState]="form" (submit)="verifyMfa.emit()">
<div *ngIf="step === 0" class="manage-backup-code__actions">
<button class="button button--primary" (click)="forwardStep.emit()">
Get Started
......@@ -150,7 +144,7 @@
</span>
</div>
</div>
<button class="button button--primary" (click)="onSubmit()">
<button class="button button--primary" (click)="verifyMfa.emit()">
Enable Two-Factor Authentication
</button>
</div>
......
......@@ -20,6 +20,7 @@ export class ManageMfaComponent {
@Input() step: ActivateMFAStep;
@Input() form: FormGroupState<IEnableMfaForm>;
@Input() errors: string[] | null;
@Input() mfaRequired: boolean;
@Input()
set uri(value: string) {
this._uri = value;
......@@ -33,11 +34,11 @@ export class ManageMfaComponent {
return this._uri;
}
qrCode: string | null;
mfaEnabled = false; // TODO remove this
@Output() generateMfa = new EventEmitter();
@Output() verifyMfa = new EventEmitter();
@Output() forwardStep = new EventEmitter();
@Output() deactivateMfa = new EventEmitter();
constructor(private _sanitizer: DomSanitizer) {}
......
import { Component } from "@angular/core";
import { Store, select } from "@ngrx/store";
import { IState } from "../../app.reducers";
import { EnableMfa, ForwardStep, ActivateMfa } from "./manage-mfa.actions";
import { IState, getMfaRequired } from "../../app.reducers";
import {
EnableMfa,
ForwardStep,
ActivateMfa,
DeactivateMfa
} from "./manage-mfa.actions";
import {
getMfaProvisioningURI,
getMfaStep,
......@@ -16,9 +21,11 @@ import {
[step]="step$ | async"
[form]="form$ | async"
[errors]="errors$ | async"
[mfaRequired]="mfaRequired$ | async"
(forwardStep)="forwardStep()"
(generateMfa)="generateMfa()"
(verifyMfa)="activateMfa()"
(deactivateMfa)="deactivateMfa()"
></app-manage-mfa>
`
})
......@@ -27,6 +34,7 @@ export class ManageMfaContainer {
step$ = this.store.pipe(select(getMfaStep));
form$ = this.store.pipe(select(getEnableMfaForm));
errors$ = this.store.pipe(select(getMfaErrorMessage));
mfaRequired$ = this.store.pipe(select(getMfaRequired));
constructor(private store: Store<IState>) {}
forwardStep() {
......@@ -40,4 +48,8 @@ export class ManageMfaContainer {
activateMfa() {
this.store.dispatch(new ActivateMfa());
}
deactivateMfa() {
this.store.dispatch(new DeactivateMfa());
}
}
......@@ -6,7 +6,9 @@ import {
EnableMfaSuccess,
EnableMfaFailure,
ActivateMfaFailure,
ActivateMfaSuccess
ActivateMfaSuccess,
DeactivateMfaSuccess,
DeactivateMfaFailure
} from "./manage-mfa.actions";
import { exhaustMap, map, catchError, withLatestFrom } from "rxjs/operators";
import { of } from "rxjs";
......@@ -45,6 +47,17 @@ export class ManageMFAEffects {
})
);
@Effect()
deactivateMfa$ = this.actions$.pipe(
ofType(ManageMfaActionTypes.DEACTIVATE_MFA),
exhaustMap(action =>
this.service.deactivateMfa().pipe(
map(resp => new DeactivateMfaSuccess()),
catchError(resp => of(new DeactivateMfaFailure()))
)
)
);
constructor(
private actions$: Actions,
private store: Store<IState>,
......
......@@ -17,7 +17,6 @@ export interface IEnableMfaForm {
export interface IEnbleMfaState {
form: FormGroupState<IEnableMfaForm>;
generatedMFA: IGeneratedMFA | null;
mfaEnabled: boolean;
step: ActivateMFAStep;
errorMessage: string[] | null;
}
......@@ -36,8 +35,7 @@ export const initialState: IEnbleMfaState = {
form: initialFormState,
generatedMFA: null,
step: ActivateMFAStep.Start,
errorMessage: null,
mfaEnabled: false
errorMessage: null
};
export const formReducer = createFormStateReducerWithUpdate<IEnableMfaForm>(
......@@ -63,8 +61,7 @@ export function reducer(
return {
...state,
generatedMFA: action.payload,
errorMessage: null,
mfaEnabled: true
errorMessage: null
};
case ManageMfaActionTypes.ENABLE_MFA_FAILURE:
......@@ -92,7 +89,6 @@ export function reducer(
}
export const getForm = (state: IEnbleMfaState) => state.form;
export const getMfaEnabled = (state: IEnbleMfaState) => state.mfaEnabled;
export const getStep = (state: IEnbleMfaState) => state.step;
export const getErrorMessage = (state: IEnbleMfaState) => state.errorMessage;
export const getProvisioningURI = (state: IEnbleMfaState) =>
......
......@@ -6,6 +6,7 @@ export interface IAuthStore {
userToken: string;
rememberMe: boolean;
optInErrorReporting: boolean;
mfaRequired: boolean;
}
export interface IResetPasswordVerifyResponse {
......
......@@ -133,9 +133,9 @@ export class UserService {
email,
userToken: resp.token,
rememberMe,
optInErrorReporting: resp.user.opt_in_error_reporting
optInErrorReporting: resp.user.opt_in_error_reporting,
mfaRequired: resp.user.mfa_required
};
this.setUp(auth);
return auth;
}),
......@@ -206,7 +206,8 @@ export class UserService {
email,
userToken: resp.token,
rememberMe,
optInErrorReporting: resp.user.opt_in_error_reporting
optInErrorReporting: resp.user.opt_in_error_reporting,
mfaRequired: resp.user.mfa_required
};
this.setUp(auth);
......@@ -269,7 +270,8 @@ export class UserService {
publicKey: auth.publicKey,
userToken: auth.userToken,
rememberMe: auth.rememberMe,
optInErrorReporting: auth.optInErrorReporting
optInErrorReporting: auth.optInErrorReporting,
mfaRequired: auth.mfaRequired
};
this.setUp(authStore);
}
......@@ -340,7 +342,8 @@ export class UserService {
email,
userToken: resp.token,
rememberMe: false,
optInErrorReporting: resp.user.opt_in_error_reporting
optInErrorReporting: resp.user.opt_in_error_reporting,
mfaRequired: resp.user.mfa_required
};
return auth;
})
......@@ -443,6 +446,12 @@ export class UserService {
return this.http.post<null>(url, data);
}
/** Deactivate MFA, user will no longer need to enter MFA code to log in */
deactivateMfa() {
const url = this.sdk.formUrl("deactivate-mfa/");
return this.http.post<null>(url, null);
}
private setSdkUrl(url: string) {
this.store.dispatch(new SetUrlAction(url));
}
......
......@@ -3,6 +3,7 @@ import {
HttpClientTestingModule,
HttpTestingController
} from "@angular/common/http/testing";
import { RouterTestingModule } from "@angular/router/testing";
import { AuthInterceptor } from "./auth.interceptor";
import { HTTP_INTERCEPTORS, HttpClient } from "@angular/common/http";
import { StoreModule, Store } from "@ngrx/store";
......@@ -21,7 +22,8 @@ describe(`AuthHttpInterceptor`, () => {
imports: [
HttpClientTestingModule,
StoreModule.forRoot({}),
StoreModule.forFeature("account", reducers)
StoreModule.forFeature("account", reducers),
RouterTestingModule.withRoutes([])
],
providers: [
{
......
......@@ -18,6 +18,7 @@ export const routes: Routes = [
{
path: "account/login/verify-mfa",
component: VerifyMfaContainer,
canActivate: [LoggedInGuard],
data: {
title: "Verify MFA",
showNavBar: false
......
......@@ -42,7 +42,8 @@ describe("AccountReducer", () => {
userId: 1,
userToken: "aaa",
rememberMe: false,
optInErrorReporting: false
optInErrorReporting: false,
mfaRequired: false
};
const createAction = new LoginSuccessAction(user);
......@@ -55,7 +56,8 @@ describe("AccountReducer", () => {
url: fromAuth.initialState.url,
rememberMe: false,
optInErrorReporting: false,
forceSetPassword: false
forceSetPassword: false,
mfaRequired: false
};
const result = fromAuth.authReducer(fromAuth.initialState, createAction);
......@@ -92,7 +94,8 @@ describe("AccountReducer", () => {
userId: 1,
userToken: "aaa",
rememberMe: false,
optInErrorReporting: false
optInErrorReporting: false,
mfaRequired: false
};
store.dispatch(new LoginSuccessAction(user));
isLoggedInSelector
......
......@@ -163,3 +163,7 @@ export const getOptInErrorReporting = createSelector(
selectAuthState,
(state: fromAuth.IAuthState) => state.optInErrorReporting
);
export const getMfaRequired = createSelector(
selectAuthState,
(state: fromAuth.IAuthState) => state.mfaRequired
);
......@@ -22,6 +22,10 @@ import {
ChangePasswordActionTypes
} from "./account/change-password/change-password.actions";
import { AppActions, AppActionTypes } from "./app.actions";
import {
ManageMfaActions,
ManageMfaActionTypes
} from "./account/manage-mfa/manage-mfa.actions";
export interface IAuthState {
email: string | null;
......@@ -32,6 +36,7 @@ export interface IAuthState {
publicKey: string | null;
rememberMe: boolean;
optInErrorReporting: boolean;
mfaRequired: boolean;
/** Always redirect logged in user to set password page */
forceSetPassword: boolean;
}
......@@ -45,6 +50,7 @@ export const initialState: IAuthState = {
publicKey: null,
rememberMe: false,
optInErrorReporting: false,
mfaRequired: false,
forceSetPassword: false
};
......@@ -60,6 +66,7 @@ export function authReducer(
| SetPasswordSuccess
| ForceSetPassword
| ChangePasswordSubmitFormSuccess
| ManageMfaActions
): IAuthState {
switch (action.type) {
case AppActionTypes.LOGIN:
......@@ -78,7 +85,8 @@ export function authReducer(
privateKey: action.payload.privateKey,
publicKey: action.payload.publicKey,
rememberMe: action.payload.rememberMe,
optInErrorReporting: action.payload.optInErrorReporting
optInErrorReporting: action.payload.optInErrorReporting,
mfaRequired: action.payload.mfaRequired
};
case ChangePasswordActionTypes.SUBMIT_FORM_SUCCESS:
......@@ -113,6 +121,18 @@ export function authReducer(
forceSetPassword: true
};
case ManageMfaActionTypes.ACTIVATE_MFA_SUCCESS:
return {
...state,
mfaRequired: true
};
case ManageMfaActionTypes.DEACTIVATE_MFA_SUCCESS:
return {
...state,
mfaRequired: false
};
default:
return state;
}
......
......@@ -2,7 +2,7 @@
<div class="account-column__inner account-column__inner--right">
<div class="account-column__right-heading">
<h1 class="heading-medium">Log&nbsp;In</h1>
<div *ngIf="linkRoute && linkText" class="account-column__link">
<div *ngIf="linkText" class="account-column__link">
<app-text-link
caret="right"
id="btn-signup"
......
......@@ -28,7 +28,10 @@ import {
LoginSuccessAction,
LoginFailureAction
} from "../app.actions";
import { SetPasswordActionTypes } from "../account/reset-password/set-password/set-password.actions";
import {
SetPasswordActionTypes,
SetPasswordSuccess
} from "../account/reset-password/set-password/set-password.actions";
import { IS_EXTENSION } from "../constants";
@Injectable()
......@@ -96,14 +99,20 @@ export class LoginEffects {
@Effect({ dispatch: false })
loginSuccess$ = this.actions$.pipe(
ofType(
ofType<LoginSuccessAction | SetPasswordSuccess>(
AppActionTypes.LOGIN_SUCCESS,
SetPasswordActionTypes.SET_PASSWORD_SUCCESS
),
tap(() => {
tap(action => {
if (IS_EXTENSION) {
this.router.navigate(["/popup"]);
} else {
if (action.type === AppActionTypes.LOGIN_SUCCESS) {
if (action.payload.mfaRequired) {
this.router.navigate(["/account/login/verify-mfa"]);
return;
}
}
this.router.navigate(["/list"]);
}
})
......
......@@ -12,6 +12,7 @@ import { SharedModule } from "../shared/shared.module";
import { NgrxFormsModule } from "ngrx-forms";
import { ProgressIndicatorModule } from "../progress-indicator/progress-indicator.module";
import { VerifyMfaComponent } from "./verify-mfa/verify-mfa.component";
import { VerifyMfaContainer } from "./verify-mfa/verify-mfa.container";
import { LoginWrapperComponent } from "./login-wrapper/login-wrapper.component";
@NgModule({
......@@ -19,6 +20,7 @@ import { LoginWrapperComponent } from "./login-wrapper/login-wrapper.component";
LoginComponent,
LoginContainer,
VerifyMfaComponent,
VerifyMfaContainer,
LoginWrapperComponent
],
imports: [
......
<app-login-wrapper
[isPopup]="isPopup"
(clickLink)="goToLogin.emit()"
linkRoute="/account/login"
linkText="Return to Login"
>
<form
......
......@@ -11,6 +11,8 @@ import { VerifyMfa } from "./verify-mfa.actions";
import * as fromRoot from "../../app.reducers";
import { IS_EXTENSION } from "../../constants";
import { Router } from "@angular/router";
import { LogoutAction } from "../../account/account.actions";
import { ResetFormContainer } from "../../form";
@Component({
template: `
......@@ -28,7 +30,7 @@ import { Router } from "@angular/router";
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class VerifyMfaContainer {
export class VerifyMfaContainer extends ResetFormContainer {
form$ = this.store.pipe(select(selectVerifyMfaForm));
hasStarted$ = this.store.pipe(select(getVerifyMfaHasStarted));
hasFinished$ = this.store.pipe(select(getVerifyMfaFinished));
......@@ -37,13 +39,16 @@ export class VerifyMfaContainer {
isExtension = IS_EXTENSION;
isPopup = false;
constructor(private router: Router, private store: Store<IState>) {
constructor(private router: Router, public store: Store<IState>) {
super(store);
store
.pipe(select(fromRoot.getIsPopup))
.subscribe(isPopup => (this.isPopup = isPopup));
}
goToLogin() {
// Technically, the user is logged in during this stage. Log them out before redirecting.
this.store.dispatch(new LogoutAction());
if (this.isPopup) {
browser.tabs.create({
url: "/index.html#/account/login"
......
......@@ -9,6 +9,10 @@ import {
import { minLength, maxLength, required } from "ngrx-forms/validation";
import { IBaseFormState } from "../../utils/interfaces";
import { VerifyMfaActionTypes, VerifyMfaActions } from "./verify-mfa.actions";
import {
ResetFormActionTypes,
ResetFormActionsUnion
} from "../../form/reset-form.actions";
const FORM_ID = "Login Form";
......@@ -44,11 +48,14 @@ export const formReducer = createFormStateReducerWithUpdate<IVerifyMfaForm>(
export function reducer(
state = initialState,
action: VerifyMfaActions
action: VerifyMfaActions | ResetFormActionsUnion
): IVerifyMfaState {
const form = formReducer(state.form, action);
state = { ...state, form };
switch (action.type) {
case ResetFormActionTypes.RESET_FORMS:
return { ...initialState };
case VerifyMfaActionTypes.VERIFY_MFA:
return {
...state,
......
......@@ -5,6 +5,7 @@ export interface IUser {
private_key: string;
client_salt: string;
opt_in_error_reporting: boolean;
mfa_required: boolean;
}
export interface ILoginResonse {
......@@ -77,70 +78,70 @@ export interface ICreateGroupUser {
}
export interface IContact {
id: number;
email: string;
first_name: string;
last_name: string;
id: number;
email: string;
first_name: string;
last_name: string;
}
export interface IData {
[propName: string]: string | undefined;
[propName: string]: string | undefined;
}
export interface ICreateSecretThrough {
/** Encrypted data */
data: IData;
/** Group id. Null indicates it belongs to a user instead */
group?: number | null;
key_ciphertext: string;
/** Encrypted data */
data: IData;
/** Group id. Null indicates it belongs to a user instead */
group?: number | null;
key_ciphertext: string;
}
export interface ICreateSecretThroughGroup extends ICreateSecretThrough {
group: number;
group: number;
}
export interface ICreateSecret {
name: string;
/** Classification of secret, for example a website or note. */
type?: string;
/** Unencrypted data for less sensative infomation. */
data: IData;
/** Encrypted data */
secret_through_set: ICreateSecretThrough[];
name: string;
/** Classification of secret, for example a website or note. */
type?: string;
/** Unencrypted data for less sensative infomation. */
data: IData;
/** Encrypted data */
secret_through_set: ICreateSecretThrough[];
}
export interface ISecretThrough {
id: number;
/** Encrypted data */
data: IData;
/** Group id. Null indicates it belongs to a user instead */
group: number | null;
key_ciphertext: string;
public_key: string;
is_mine: boolean;
id: number;
/** Encrypted data */
data: IData;
/** Group id. Null indicates it belongs to a user instead */
group: number | null;
key_ciphertext: string;
public_key: string;
is_mine: boolean;
}
export interface ISecretThroughGroup extends ISecretThrough {
group: number;
group: number;
}
export interface ISecret {
id: number;
name: string;
/** Classification of secret, for example a website or note. */
type: string;
/** Unencrypted data for less sensative infomation. */
data: IData;
/** Encrypted data */
secret_through_set: ISecretThrough[];
id: number;
name: string;
/** Classification of secret, for example a website or note. */
type: string;
/** Unencrypted data for less sensative infomation. */
data: IData;
/** Encrypted data */
secret_through_set: ISecretThrough[];
}
export interface IUpdateSecret {
name?: string;
/** Classification of secret, for example a website or note. */
type?: string;
/** Unencrypted data for less sensative infomation. */
data?: IData;
name?: string;
/** Classification of secret, for example a website or note. */
type?: string;
/** Unencrypted data for less sensative infomation. */
data?: IData;
}
export interface IUpdateSecretThrough extends Partial<ICreateSecretThrough> {
......@@ -152,7 +153,7 @@ export interface IUpdateSecretWithThroughs extends IUpdateSecret {
}
export interface IChangePasswordSecretThrough extends ISecretThrough {
hash: string;
hash: string;
}
export interface IChangePasswordGroupUser extends IGroupUser {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment