Skip to content

Commit

Permalink
add new react app and Slim tempaltes for 2fa enrollemnt
Browse files Browse the repository at this point in the history
  • Loading branch information
crossan007 committed Nov 10, 2019
1 parent ef7e8f8 commit 5ceddc7
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 1 deletion.
267 changes: 267 additions & 0 deletions react/components/UserSecurity/UserTwoFactorEnrollment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import * as React from 'react';
import CRMRoot from '../../window-context-service.jsx';

const TwoFAEnrollmentWelcome: React.FunctionComponent<{nextButtonEventHandler: Function}> = ({ nextButtonEventHandler}) => {
return (
<div>
<div className="col-lg-12">
<div className="box" id="TwoFAEnrollmentSteps">
<div className="box-body">
<p>{window.i18next.t("Enrolling your ChurchCRM user account in Two Factor Authention provides an additional layer of defense against bad actors trying to access your account.")}</p>
<p>{window.i18next.t("ChurchCRM Two factor supports any TOTP authenticator app, so you're free to choose between Microsoft Authenticator, Google Authenticator, Authy, LastPass, and others")}</p>
<hr/>
<div className="col-lg-4">
<i className="fa fa-id-card-o"></i>
<p>{window.i18next.t("When you sign in to ChurchCRM, you'll still enter your username and password like normal")}</p>
</div>
<div className="col-lg-4">
<i className="fa fa-key"></i>
<p>{window.i18next.t("However, you'll also need to supply a one-time code from your authenticator device to complete your login")}</p>
</div>
<div className="col-lg-4">
<i className="fa fa-check-square-o"></i>
<p>{window.i18next.t("After successfully entering both your credentials, and the one-time code, you'll be logged in as normal")}</p>
</div>
<div className="clearfix"></div>
<div className="callout callout-warning">
<p>{window.i18next.t("To prevent being locked out of your ChurchCRM account, please ensure you're ready to complete two factor enrollment before clicking begin")}</p>
</div>
<ul>
<li>{window.i18next.t("Beginning enrollment will invalidate any previously enrolled 2 factor devices and recovery codes.")}</li>
<li>{window.i18next.t("When you click next, you'll be prompted to scan a QR code to enroll your authenticator app.")}</li>
<li>{window.i18next.t("To confirm enrollment, you'll need to enter the code generated by your authenticator app")}</li>
<li>
{window.i18next.t("After confirming app enrollment, single-use recovery codes will be generated and displayed.")}
<ul>
<li>{window.i18next.t("Recovery codes can be used instead of a code generated from your authenticator app.")}</li>
<li>{window.i18next.t("Store these in a secure location")}</li>
</ul>
</li>
</ul>


<div className="clearfix"></div>
<button className="btn btn-success" onClick={() => {nextButtonEventHandler()}}>{window.i18next.t("Begin Two Factor Authentication Enrollment")}</button>
</div>
</div>
</div>
</div>

)
}

const TwoFAEnrollmentGetQR: React.FunctionComponent<{TwoFAQRCodeDataUri: string, newQRCode:Function, remove2FA:Function, validationCodeChangeHandler:(event: React.ChangeEvent<HTMLInputElement>) => void, currentTwoFAPin?:string, currentTwoFAPinStatus:string }> = ({TwoFAQRCodeDataUri, newQRCode, remove2FA, validationCodeChangeHandler, currentTwoFAPin, currentTwoFAPinStatus}) => {
return (
<div>
<div className="col-lg-12">
<div className="box">
<div className="box-header">
<h4>{window.i18next.t("2 Factor Authentication Secret")}</h4>
</div>
<div className="box-body">
<div className="col-lg-6">
<img src={TwoFAQRCodeDataUri} />
</div>
<div className="col-lg-6">
<div className="row">
<div className="col-lg-6">
<button className="btn btn-warning" onClick={() => {newQRCode()}}>{window.i18next.t("Regenerate 2 Factor Authentication Secret")}</button>
</div>
<div className="col-lg-6">
<button className="btn btn-warning" onClick={() => {remove2FA()}}>{window.i18next.t("Remove 2 Factor Authentication Secret")}</button>
</div>
</div>
<div className="row">
<div className="col-lg-12">
<label>
{window.i18next.t("Enter TOTP code to confirm enrollment")}:
<input onChange={validationCodeChangeHandler} value={currentTwoFAPin} autoFocus />
</label>
<p>{currentTwoFAPinStatus}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

const TwoFAEnrollmentSuccess: React.FunctionComponent<{TwoFARecoveryCodes?: string[]}> = ({ TwoFARecoveryCodes}) => {
return (
<div className="col-lg-12">
<div className="box">
<div className="box-header">
<h4>{window.i18next.t("2 Factor Authentication Enrollment Success")}</h4>
</div>
<div className="box-body">
<p>{window.i18next.t("Please store these recovery codes in a safe location")}</p>
<p>{window.i18next.t("If you ever lose access to your newly enrolled authenticator app, you'll need to use a recovery code to gain access to your account")}</p>
<ul>{TwoFARecoveryCodes.length ? (TwoFARecoveryCodes.map((code) => <li>{code}</li>)) : <p>waiting</p>}</ul>
</div>
</div>
</div>
)
};


class UserTwoFactorEnrollment extends React.Component<TwoFactorEnrollmentProps, TwoFactorEnrollmentState> {
constructor(props: TwoFactorEnrollmentProps) {
super(props);

this.state = {
currentView: "intro",
TwoFARecoveryCodes: []
}

this.nextButtonEventHandler = this.nextButtonEventHandler.bind(this);
this.requestNew2FABarcode = this.requestNew2FABarcode.bind(this);
this.remove2FAForuser = this.remove2FAForuser.bind(this);
this.validationCodeChangeHandler = this.validationCodeChangeHandler.bind(this);
this.requestNew2FARecoveryCodes = this.requestNew2FARecoveryCodes.bind(this);
}

nextButtonEventHandler() {
this.requestNew2FABarcode();
this.setState({
currentView:"BeginEnroll"
});
}

requestNew2FABarcode() {
fetch(CRMRoot + '/api/user/current/refresh2fasecret', {
credentials: "include",
method: "POST",
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
this.setState({ TwoFAQRCodeDataUri: data.TwoFAQRCodeDataUri })
});
}

requestNew2FARecoveryCodes() {
fetch(CRMRoot + '/api/user/current/refresh2farecoverycodes', {
credentials: "include",
method: "POST",
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
this.setState({ TwoFARecoveryCodes: data.TwoFARecoveryCodes })
});
}

remove2FAForuser() {
fetch(CRMRoot + '/api/user/current/remove2fasecret', {
credentials: "include",
method: "POST",
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
this.setState({
TwoFAQRCodeDataUri: "",
currentView: "intro"
})
});
}

validationCodeChangeHandler(event: React.ChangeEvent<HTMLInputElement> ) {
this.setState({
currentTwoFAPin: event.currentTarget.value
});
if(event.currentTarget.value.length == 6) {
console.log("Checking for valid pin");
fetch(CRMRoot + "/api/user/current/test2FAEnrollmentCode", {
credentials: "include",
method: "POST",
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({enrollmentCode: event.currentTarget.value})
})
.then(response => response.json())
.then(data => {
if (data.IsEnrollmentCodeValid ) {
this.requestNew2FARecoveryCodes();
this.setState({
currentView: "success"
});
}
else{
this.setState({
currentTwoFAPinStatus: "invalid"
});
}

});
this.setState({
currentTwoFAPinStatus: "pending"
})

}
else{
this.setState({
currentTwoFAPinStatus: "incomplete"
})
}
}


render() {
if (this.state.currentView === "intro") {
return (
<div>
<div className="row">
<TwoFAEnrollmentWelcome nextButtonEventHandler = { this.nextButtonEventHandler} />
</div>
</div >
);
}
else if(this.state.currentView === "BeginEnroll") {
return (
<div>

<div className="row">
<TwoFAEnrollmentGetQR TwoFAQRCodeDataUri={this.state.TwoFAQRCodeDataUri} newQRCode={this.requestNew2FABarcode} remove2FA={this.remove2FAForuser} validationCodeChangeHandler={this.validationCodeChangeHandler } currentTwoFAPin={this.state.currentTwoFAPin} currentTwoFAPinStatus={this.state.currentTwoFAPinStatus} />
</div>
</div >
);
}
else if(this.state.currentView === "success") {
return (
<TwoFAEnrollmentSuccess TwoFARecoveryCodes={this.state.TwoFARecoveryCodes} />
)
}
else {
return (
<h4>Uh-oh</h4>
)
}
}
}

interface TwoFactorEnrollmentProps {

}

interface TwoFactorEnrollmentState {
currentView: string,
TwoFAQRCodeDataUri?: string,
currentTwoFAPin?: string,
currentTwoFAPinStatus?: string,
TwoFARecoveryCodes: string[]
}
export default UserTwoFactorEnrollment;
15 changes: 15 additions & 0 deletions react/two-factor-enrollment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import UserTwoFactorEnrollment from './components/UserSecurity/UserTwoFactorEnrollment';

declare global {
interface Window {
// React does have it's own i18next implementation, but for now, lets use the one that's already being loaded
i18next: {
t(string): string
}
}
}
$(document).ready( function() {
ReactDOM.render(<UserTwoFactorEnrollment />, document.getElementById('two-factor-enrollment-react-app'));
});
1 change: 1 addition & 0 deletions src/skin/churchcrm.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
@import 'scss/strike';
@import 'scss/calendars';
@import 'scss/react-datepicker';
@import 'scss/_two-factor-enrollment';

html, body {
font-size: 14px;
Expand Down
16 changes: 16 additions & 0 deletions src/skin/scss/_two-factor-enrollment.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#TwoFAEnrollmentSteps div.col-lg-4 {
text-align: center;
}

#TwoFAEnrollmentSteps div.col-lg-4 i{
font-size: 100px
}

#TwoFAEnrollmentSteps button.btn-success {
display: block;
margin-right: auto;
margin-left: auto;
width: 40%;
min-height: 60px;
}

13 changes: 13 additions & 0 deletions src/v2/templates/user/manage-2fa.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php


use ChurchCRM\dto\SystemConfig;
use ChurchCRM\dto\SystemURLs;

//Set the page title
$sPageTitle = $user->getFullName() . gettext("2 Factor Authentication enrollment");
include SystemURLs::getDocumentRoot() . '/Include/Header.php';
?>
<div id="two-factor-enrollment-react-app"> </div>
<script src="<?= SystemURLs::getRootPath() ?>/skin/js-react/two-factor-enrollment-app.js"></script>
<?php include SystemURLs::getDocumentRoot() . '/Include/Footer.php'; ?>
24 changes: 24 additions & 0 deletions src/v2/templates/user/unsupported-2fa.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

use ChurchCRM\dto\SystemURLs;

//Set the page title
$sPageTitle = gettext("Unsupported Two Factor Authentication Configuration");
require SystemURLs::getDocumentRoot() . '/Include/Header.php';
?>

<div class="box">
<div class="box-body">

<div class="box-body">
<h3><i class="fa fa-warning text-yellow"></i> <?= gettext("Unable To Begin Two Factor Authentication Enrollment") ?></h3>

<p><?= gettext("Two factor authentication requires ChurchCRM administrators to configure a few parameters").":" ?></p>
<ul>
<li><?= gettext("System configuration ") . " bEnable2FA " . gettext("Must be set to 'true'") ?></li>
<li><?= gettext("Include/Config.php must define an encryption key for storing 2FA secret keys in the database by setting a value for") ?>: $TwoFASecretKey</li>
</ul>
</div>
</div>

<?php require SystemURLs::getDocumentRoot() . '/Include/Footer.php'; ?>
3 changes: 2 additions & 1 deletion webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ const path = require('path');
module.exports = {
mode: "development",
entry: {
'calendar-event-editor' : './react/calendar-event-editor.tsx'
'calendar-event-editor' : './react/calendar-event-editor.tsx',
'two-factor-enrollment' : './react/two-factor-enrollment.tsx'
},
output: {
path:path.resolve('./src/skin/js-react'),
Expand Down

0 comments on commit 5ceddc7

Please sign in to comment.