From 4826e5bdb16b457bd33af0a30758ee9438141bd0 Mon Sep 17 00:00:00 2001 From: AndrewPoppe Date: Sun, 12 Nov 2023 16:54:53 -0500 Subject: [PATCH 01/22] progress --- REDCapPRO.php | 3 - config.json | 16 +- lib/jQuery/jquery-3.7.1.min.js | 2 + .../jquery-3.7.1.min.js:Zone.Identifier | 4 + src/classes/Auth.php | 78 ++++++-- src/classes/ProjectSettings.php | 22 ++- src/classes/UI.php | 1 + src/css/rcpro.php | 4 + src/home.php | 15 ++ src/js/mfa.js | 16 ++ src/mfa.php | 166 ++++++++++++++---- src/settings.php | 63 +++++-- 12 files changed, 317 insertions(+), 73 deletions(-) create mode 100644 lib/jQuery/jquery-3.7.1.min.js create mode 100644 lib/jQuery/jquery-3.7.1.min.js:Zone.Identifier create mode 100644 src/js/mfa.js diff --git a/REDCapPRO.php b/REDCapPRO.php index b2f3bef..aeb4cdb 100644 --- a/REDCapPRO.php +++ b/REDCapPRO.php @@ -213,9 +213,6 @@ public function redcap_survey_page_top( // Check MFA Token if ( $settings->mfaEnabled((int) $project_id) && !$auth->is_mfa_verified() ) { - $code = $auth->get_mfa_code(); - $participantEmail = $participantHelper->getEmail($auth->get_participant_id()); - $this->sendMfaTokenEmail($participantEmail, $code); header("location: " . $this->framework->getUrl("src/mfa.php", true)); return; } diff --git a/config.json b/config.json index 8a68c63..c579632 100644 --- a/config.json +++ b/config.json @@ -50,11 +50,25 @@ "type": "text", "validation": "integer" }, + { + "key": "mfa-descriptive", + "name": "
Multi-Factor Authentication Settings

These settings control the use of multi-factor authentication (MFA). Please take care to understand how MFA may be used before enabling it in the system.

", + "type": "descriptive" + }, { "key": "mfa", - "name": "Multi-Factor Authentication:
Should participants be required to use multi-factor authentication (MFA) when logging in? If so, they will be required to enter a code sent to their email address after entering their username and password. This is an additional security measure to prevent unauthorized access.
Note: this setting enables the MFA option globally, but it still must be enabled in the project settings to take effect", + "name": "
Multi-Factor Authentication:

Should participants be required to use multi-factor authentication (MFA) when logging in? If so, they will be required to enter a code after entering their username and password. This is an additional security measure to prevent unauthorized access.
Note: this setting enables the MFA option globally, but it still must be enabled in the project settings to take effect", "type": "checkbox" }, + { + "key": "mfa-authenticator-app", + "name": "
Multi-Factor Authentication: Authenticator App:

Should participants be allowed to use an authenticator app (such as Google Authenticator or Microsoft Authenticator) to generate their MFA code? If not, they will be required to use the code sent to their email address.
Note: this setting only applies if MFA is enabled globally, but it still must be enabled in the project settings to take effect", + "type": "checkbox", + "branchingLogic": { + "field": "mfa", + "value": "1" + } + }, { "key": "self-registration-descriptive", "name": "
Self-Registration and Enrollment Settings

These settings control the ability for participants to register and/or enroll themselves. Please take care to understand the implications of this before enabling it in the system.

", diff --git a/lib/jQuery/jquery-3.7.1.min.js b/lib/jQuery/jquery-3.7.1.min.js new file mode 100644 index 0000000..7f37b5d --- /dev/null +++ b/lib/jQuery/jquery-3.7.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},M=function(){V()},R=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&U(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&R(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function X(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function U(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",M),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="
",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return M(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return M(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0APPTITLE . "_mfa_code"] = $code; - $_SESSION[$this->APPTITLE . "_mfa_code_time"] = time(); - return $code; + return $_SESSION[$this->APPTITLE . "_mfa_code_verified"]; } - public function get_mfa_code() + + // --- Email MFA --- \\ + + public function generate_email_mfa_code() { - return $_SESSION[$this->APPTITLE . "_mfa_code"] ?? $this->generate_mfa_code(); + $code = random_int(100000, 999999); + $_SESSION[$this->APPTITLE . "_email_mfa_code"] = $code; + $_SESSION[$this->APPTITLE . "_email_mfa_code_time"] = time(); + return $code; } - public function get_mfa_code_time() + public function get_email_mfa_code() { - return $_SESSION[$this->APPTITLE . "_mfa_code_time"]; + return $_SESSION[$this->APPTITLE . "_email_mfa_code"] ?? $this->generate_email_mfa_code(); } - public function clear_mfa_code() + public function get_email_mfa_code_time() { - unset($_SESSION[$this->APPTITLE . "_mfa_code"]); - unset($_SESSION[$this->APPTITLE . "_mfa_code_time"]); + return $_SESSION[$this->APPTITLE . "_email_mfa_code_time"]; } - public function is_mfa_verified() + public function clear_email_mfa_code() { - return $_SESSION[$this->APPTITLE . "_mfa_code_verified"]; + unset($_SESSION[$this->APPTITLE . "_email_mfa_code"]); + unset($_SESSION[$this->APPTITLE . "_email_mfa_code_time"]); } - public function check_mfa_code(int $code) + public function check_email_mfa_code(int $code) { - $codeMatches = $code === $this->get_mfa_code(); - $codeExpired = time() - $this->get_mfa_code_time() > $this->mfa_duration; + $codeMatches = $code === $this->get_email_mfa_code(); + $codeExpired = time() - $this->get_email_mfa_code_time() > $this->mfa_duration; if ( $codeExpired ) { - $this->clear_mfa_code(); + $this->clear_email_mfa_code(); } $success = $codeMatches && !$codeExpired; if ( $success ) { @@ -248,4 +257,35 @@ public function check_mfa_code(int $code) } return $success; } + + // --- TOTP Authenticator MFA --- \\ + + public function get_totp_mfa_qr_url(string $otpauth) + { + return APP_PATH_WEBROOT_FULL . "redcap_v" . REDCAP_VERSION . "/Authentication/generate_qrcode.php?value=" . urlencode($otpauth); + } + + public function create_totp_mfa_secret() { + $ga = new \GoogleAuthenticator(); + $secret = $ga->createSecret(); + return $secret; + } + + public function create_totp_mfa_otpauth(string $email, string $secret) { + $scheme = 'otpauth'; + $type = 'totp'; + $issuer = urlencode($this->APPTITLE . ' (' . SERVER_NAME . ')'); + $accountName = urlencode($email); + $otpauth = $scheme.'://'.$type.'/'.$issuer.':'.$accountName.'?secret='.$secret.'&issuer='.$issuer; + return $otpauth; + } + + public function check_totp_mfa_code(string $code, REDCapPRO $module) { + $ga = new \GoogleAuthenticator(); + $secret = $this->get_totp_mfa_secret($module); + $codeIsCorrect = $ga->verifyCode($secret, $code, 2); + return $codeIsCorrect; + } + + } \ No newline at end of file diff --git a/src/classes/ProjectSettings.php b/src/classes/ProjectSettings.php index 6ce2530..46fb6c0 100644 --- a/src/classes/ProjectSettings.php +++ b/src/classes/ProjectSettings.php @@ -88,8 +88,8 @@ public function getLanguageFiles() $files = glob($path . "*.{i,I}{n,N}{i,I}", GLOB_BRACE); foreach ( $files as $filename ) { if ( is_file($filename) ) { - $lang = pathinfo($filename, PATHINFO_FILENAME); - $langs[$lang] = $filename; + $thisLang = pathinfo($filename, PATHINFO_FILENAME); + $langs[$thisLang] = $filename; } } } @@ -126,6 +126,24 @@ public function mfaEnabled(int $pid) return $mfaEnabledSystem === true && $mfaEnabledProject === true; } + /** + * Checks whether an authenticator app is enabled for this project + * + * @param int $pid The redcap project ID + * + * @return bool Whether an authenticator app is enabled for this project + */ + public function mfaAuthenticatorAppEnabled(int $pid) + { + $mfaAuthenticatorAppEnabledSystem = $this->module->getSystemSetting("mfa-authenticator-app"); + $mfaAuthenticatorAppEnabledProject = $this->module->getProjectSetting("mfa-authenticator-app", $pid); + + return $mfaAuthenticatorAppEnabledSystem === true && $mfaAuthenticatorAppEnabledProject === true; + } + + + + public function shouldAllowSelfRegistration(int $pid) : bool { $allowSelfRegistrationSystem = $this->module->getSystemSetting("allow-self-registration-system"); diff --git a/src/classes/UI.php b/src/classes/UI.php index 9746e2e..9ac5eae 100644 --- a/src/classes/UI.php +++ b/src/classes/UI.php @@ -26,6 +26,7 @@ public function ShowParticipantHeader(string $title) + + + EndParticipantPage(); ?> \ No newline at end of file diff --git a/src/settings.php b/src/settings.php index ae8299b..bfb333c 100644 --- a/src/settings.php +++ b/src/settings.php @@ -59,6 +59,7 @@ // Validate MFA $new_settings["mfa"] = $post_settings["mfa"] === "true"; + $new_settings["mfa-authenticator-app"] = $post_settings["mfa-authenticator-app"] === "on"; // Validate API $new_settings["api"] = $post_settings["api"] === "true"; @@ -115,6 +116,8 @@ $settings = $module->getProjectSettings(); $preventEmailLoginSystem = $module->getSystemSetting("prevent-email-login-system"); $allowMfa = $module->getSystemSetting("mfa"); +$allowMfaAuthenticatorApp = $module->getSystemSetting("mfa-authenticator-app"); +$mfaAuthenticatorAppEnabled = $projectSettings->mfaAuthenticatorAppEnabled($project_id); $project_id = (int) $module->framework->getProjectId(); $allowSelfRegistrationSystem = $module->framework->getSystemSetting("allow-self-registration-system"); $allowAutoEnrollSystem = (bool) $module->framework->getSystemSetting("allow-auto-enroll-upon-self-registration-system"); @@ -186,7 +189,7 @@ onclick="(function(){ - $('#prevent-email-login').val($('#prevent-email-login-check')[0].checked); + $('#prevent-email-login').val($('#prevent-email-login-check').get(0).checked); })()">