├── .gitignore ├── Application.cfc ├── LICENSE ├── README.md ├── admin ├── controllers │ ├── main.cfc │ └── security.cfc ├── layouts │ └── default.cfm └── views │ └── main │ ├── dashboard.cfm │ ├── default.cfm │ └── twofactor.cfm ├── blocked └── Application.cfc ├── box.json ├── data └── top_100000_hacked_passwords.txt ├── database ├── smsProviders_data.xls └── users_and_smsprovider_tables.sql ├── framework ├── Application.cfc ├── MyApplication.cfc ├── WireBoxAdapter.cfc ├── aop.cfc ├── beanProxy.cfc ├── facade.cfc ├── ioc.cfc ├── methodProxy.cfc ├── nullObject.cfc └── one.cfc ├── home ├── controllers │ └── main.cfc ├── layouts │ └── default.cfm └── views │ └── main │ ├── default.cfm │ ├── error.cfm │ ├── process.cfm │ ├── register.cfm │ └── reset.cfm ├── index.cfm ├── ipBlocked.html ├── ipFlagged.html ├── keyrings └── move_keyrings_folder_outside_webroot.txt ├── libs └── scrypt-1.4.0.jar └── model ├── beans ├── BaseBean.cfc ├── Session.cfc ├── SmsProvider.cfc ├── User.cfc └── instant.cfc └── services ├── MailService.cfc ├── SecurityService.cfc ├── SmsProviderService.cfc ├── UserService.cfc └── formatter.cfc /.gitignore: -------------------------------------------------------------------------------- 1 | keyrings/BF34677F9BEBB7C3D076C08B817C80E3.bin -------------------------------------------------------------------------------- /Application.cfc: -------------------------------------------------------------------------------- 1 | component extends="framework.one" { 2 | 3 | this.name = 'secure_auth_combined'; 4 | this.applicationTimeout = createTimeSpan( 30, 0, 0, 0 ); // 30 days 5 | this.sessionManagement = true; 6 | this.sessionTimeout = createTimeSpan( 0, 0, 30, 0 ); // 30 minutes 7 | this.datasource = 'twofactorauth'; 8 | this.scriptprotect = 'all'; 9 | 10 | this.javaSettings = { 11 | loadPaths = [ expandPath( 'libs/' ) ], 12 | reloadOnChange=true, 13 | watchInterval=600 14 | }; 15 | // CF10+ uncomment the following line to make your cfid/cftoken cookies httpOnly 16 | // this.sessioncookie.httpOnly; 17 | 18 | // set application specific variables 19 | variables.framework = { 20 | usingSubsystems = true 21 | }; 22 | 23 | // set environment variables - one of 'dev' (default), 'test' or 'prod' 24 | // The 'prod' (production) environment is the only one that executes IP watching and blocking. 25 | // This helps prevent being added to the watched or blocked IP list while 26 | // in development or testing. NOTE: You can specify any other environments here, 27 | // such as QA, or rename any other environment except 'prod' as needed 28 | // You can also specify environment specific framework settings here. 29 | // See the Environment Control section of the FW/1 Developing Applications Manual: 30 | // https://github.com/framework-one/fw1/wiki/Developing-Applications-Manual 31 | variables.framework.environments = { 32 | dev = {}, 33 | test = {}, 34 | prod = {} 35 | }; 36 | 37 | // delegation of lifecycle methods to FW/1: 38 | function onApplicationStart() { 39 | 40 | // Lucee 5+ added the function generatePBKDFKey() which is a much 41 | // more secure way of handling the keyring master key than the 42 | // legacy hashing routine this code previously used. To maintain 43 | // backwards compatibility with Lucee 4.5, a few hoops have been 44 | // introduced to determine which version of the Lucee engine is in 45 | // use, and select the appropriate routine (PBKDF or legacy) based 46 | // on the version information. 47 | 48 | // If you'll be using Lucee 5+, then you can remove the code below 49 | // that applies only if Lucee is running v4.5. Likewise, if you're 50 | // using Lucee 4.5 then you can remove the code below that applies 51 | // only if Lucee is running v5+. 52 | 53 | // NOTE: toBase64(), toBinary() and toString() are used in lieu if using 54 | // charsetDecode() and charsetEncode() to aid in obfuscating 55 | // sensitive data in case of accidental code disclosure 56 | // This is not security, it is simply obfuscation that would 57 | // confuse only those who are not programmers themselves 58 | 59 | // set use of PBKDF master key to false 60 | application.usePBKDF = false; 61 | 62 | // define a password for the application's master key 63 | // defined using toBase64( 'secure_auth_master_key', 'UTF-8' ) 64 | // which provides some obfuscation if this code ever leaks 65 | // define your own password using the same technique 66 | application.password = toBinary( 'c2VjdXJlX2F1dGhfbWFzdGVyX2tleQ==' ); 67 | 68 | // define the salt to use with PBKDF 69 | // defined using toBase64( 'RtTpPAKXNBh0zoWb', 'UTF-8' ) 70 | // which provides some obfuscation if this code ever leaks 71 | // define your own salt using the same technique 72 | // salt should be a minimum of 16 chars (128 bits) long 73 | application.salt = toBinary( 'UnRUcFBBS1hOQmgwem9XYg==' ); 74 | 75 | // define the keyring filename to use 76 | // defined using toBase64( 'secure_auth_keyring', 'UTF-8' ) 77 | // which provides some obfuscation if this code ever leaks 78 | // define your own keyring filename using the same technique 79 | application.keyRingFilename = toBinary( 'c2VjdXJlX2F1dGhfa2V5cmluZw==' ); 80 | 81 | // set the path to the keyring file location on disk 82 | // NOTE: The keyRingPath should be placed in a secure directory *outside* of 83 | // your web root to prevent key disclosure over the internet. 84 | // this path should be accessible *only* to the user the CFML application server is 85 | // running under and to root/Administrator users 86 | // you can change the number of hash iterations (173 by default) to further 87 | // distinguish this application from others using this framework example 88 | // ex: keyRingPath = expandPath( '/opt/secure/keyrings/' ) & hash( 'toString( application.keyRingFilename, 'UTF-8' ), 'MD5', 'UTF-8', 420 ) & '.bin' 89 | application.keyRingPath = expandPath( 'keyrings/' ) & hash( toString( application.keyRingFilename, 'UTF-8' ), 'MD5', 'UTF-8', 173 ) & '.bin'; 90 | 91 | // get the engine we're currently deployed on 92 | application.engine = server.coldfusion.productname; 93 | 94 | // check if we're using Lucee 95 | if( findNoCase( 'lucee', application.engine ) ) { 96 | // we are, get the version of lucee we're running 97 | application.engineVersion = server.lucee.version; 98 | // check if it is version 5 or above 99 | if( listFirst( application.engineVersion, '.' ) gte 5 ) { 100 | // it is, we can use a PBKDF master key 101 | application.usePBKDF = true; 102 | } 103 | // otherwise, check if we're running Railo 104 | } else if( findNoCase( 'railo', application.engine ) ) { 105 | // we are, get the version of Railo we're running 106 | application.engineVersion = server.railo.version; 107 | // otherwise, assume we're running ACF 108 | } else { 109 | // get the version of ACF we're running 110 | application.engineVersion = listFirst( server.coldfusion.productversion ); 111 | // check if it is version 11 or above 112 | if( listFirst( application.engineVersion ) gte 11 ) { 113 | // it is, we can use a PBKDF master key 114 | application.usePBKDF = true; 115 | } 116 | } 117 | 118 | // NOTE: If upgrading from a previous release that already has 119 | // a generated and used keyring file, and you are running Lucee 5+ 120 | // or ACF 11+, then you risk either generating a new keyring file, 121 | // or throwing a decryption error, as this version will try to use 122 | // PBKDF for the master key instead of legacy hashing of previous versions. 123 | // You can either first rekey your keyring using the new PBKDF master 124 | // key and then proceed (see function rekeyKeyRing() in model/services/SecurityService.cfc), 125 | // or you can uncomment the following line to prevent these conditions 126 | // by forcing the use of the legacy master key 127 | 128 | // application.usePBKDF = false; 129 | 130 | // check if we can use a PBKDF master key 131 | if( application.usePBKDF ) { 132 | // we can, generate the master key using PBKDF 133 | // in addition to differences in passwords and salts used 134 | // you can change the algorithm (PBKDF2WithHmacSHA1 by default) 135 | // and the number of iterations (2048 by default) to futher distinguish 136 | // this application from others using this framework example 137 | application.masterKey = generatePBKDFKey( 'PBKDF2WithHmacSHA1', toString( application.password, 'UTF-8' ), toString( application.salt, 'UTF-8' ), 2048, 128 ); 138 | // otherwise 139 | } else { 140 | // we cannot, generate the master key using legacy hashing 141 | // in addition to differences in passwords used 142 | // you can change the hash algorithm (SHA-512 by default) 143 | // the number of iterations (512 by default) and the 144 | // starting position of the mid() statement (38 by default - range from 1 to 106 with SHA-512) 145 | // to further distinguish this application from others using this 146 | // framework example 147 | application.masterKey = mid( lCase( hash( toString( application.password, 'UTF-8' ), 'SHA-512', 'UTF-8', 512 ) ), 38, 22 ) & '=='; 148 | } 149 | 150 | // provide a static HMAC key using generateSecretKey( 'HMACSHA512' ) 151 | // to be used in development environments where application reload 152 | // forcing re-login is undesireable (currently any environment other than 'prod') 153 | application.developmentHmacKey = '1Srai7KJK/oUD/pNHvaCJdb5JLJfyPOOjIyYSLvttJs0PaA9HskfJlz2YsXjyokh4fDTC0utupQ4SREklCCZ4w=='; 154 | 155 | // load and initialize the SecurityService with keyring path and master key 156 | application.securityService = new model.services.SecurityService( 157 | keyRingPath = application.keyRingPath, 158 | masterKey = application.masterKey 159 | ); 160 | 161 | // use the SecurityService to read the encryption keys from disk 162 | application.keyRing = application.securityService.readKeyRingFromDisk(); 163 | 164 | // check if the keyring is a valid array of keys 165 | if( !isArray( application.keyRing ) or !arrayLen( application.keyRing ) ) { 166 | // it isn't, try 167 | try { 168 | // to generate a new keyring file (for new application launch only) 169 | // you should throw an error instead of attempting to generate a new 170 | // keyring once a keyring has already been established 171 | // ex: throw( 'The keyring file could not be found' ); 172 | application.keyRing = application.securityService.generateKeyRing(); 173 | // catch any errors 174 | } catch ( any e ) { 175 | // and dump the error 176 | // writeDump( e ); 177 | // or throw a new error 178 | // throw( 'The keyring file could not be found' ); 179 | // or otherwise log, etc. and abort 180 | abort; 181 | } 182 | } 183 | 184 | // (re)initialize the SecurityService with the keyring 185 | // NOTE: To avoid being forced to login every time the framework 186 | // is reloaded (reload=true), the 'hmacKey' value below is set to a 187 | // static HMAC key instead of creating a new one each time. 188 | // This is randomized (using 'generateSecretKey( 'HMACSHA512' )') 189 | // in production for increased security, but can be static in development 190 | // to avoid the cookies being improperly signed on reload. 191 | // You should change this HMAC key in your environment. 192 | application.securityService = application.securityService.init( 193 | encryptionKey1 = application.keyRing[1].key, 194 | encryptionAlgorithm1 = application.keyRing[1].alg, 195 | encryptionEncoding1 = application.keyRing[1].enc, 196 | encryptionIV1 = binaryDecode( application.keyRing[1].iv, 'BASE64' ), 197 | encryptionKey2 = application.keyRing[2].key, 198 | encryptionAlgorithm2 = application.keyRing[2].alg, 199 | encryptionEncoding2 = application.keyRing[2].enc, 200 | encryptionIV2 = binaryDecode( application.keyRing[2].iv, 'BASE64' ), 201 | encryptionKey3 = application.keyRing[3].key, 202 | encryptionAlgorithm3 = application.keyRing[3].alg, 203 | encryptionEncoding3 = application.keyRing[3].enc, 204 | encryptionIV3 = binaryDecode( application.keyRing[3].iv, 'BASE64' ), 205 | hmacKey = ( ( application.securityService.getEnvironment() eq 'prod' ) ? generateSecretKey( 'HMACSHA512' ) : application.developmentHmacKey ), 206 | hmacAlgorithm = 'HMACSHA512', 207 | hmacEncoding = 'UTF-8', 208 | scN = 16384, 209 | scR = 16, 210 | scP = 1 211 | ); 212 | 213 | // clear out temp keys from the application scope 214 | for( item in [ 'password', 'salt', 'keyRingFilename', 'keyRingPath', 'keyRing', 'masterKey', 'developmentHmacKey', 'usePBKDF' ] ) { 215 | structDelete( application, item ); 216 | } 217 | 218 | // set the name of the cookie to use for session management 219 | // (*DO NOT USE* cfid, cftoken or jsessionid) 220 | // Obscuring your cookie name using common tracker names 221 | // can help throw a would-be hacker off course 222 | // ex: __ga_utm_source, __imgur_ref_id, __fb_beacon_token, etc. 223 | application.cookieName = '__ga_utm_source'; 224 | 225 | // set the name of the dummy cookies to use to help 226 | // obfuscate the actual session cookie 227 | // (*DO NOT USE* cfid, cftoken or jsessionid) 228 | // Using obscure and/or common session cookie names 229 | // here can help throw a would-be hacker off course 230 | // ex: __secure_auth_id, session_id, _fb__beacon__token, etc. 231 | application.dummyCookieOne = '__secure_auth_id'; 232 | application.dummyCookieTwo = 'session_id'; 233 | application.dummyCookieThree = '_fb__beacon__token_'; 234 | 235 | // set number of minutes before a session is timed out 236 | application.timeoutMinutes = 30; // 30 minutes 237 | 238 | // set the directory where the blocked ip json file is stored 239 | // if this file is web accessible you can share your blocked ip's 240 | // with other sites more easily when using the 241 | // importBlockedIPFileFromUrl() function of the security service 242 | application.blockedIpDir = '/blocked/'; 243 | 244 | // set the number of times an ip address can attempt 245 | // hacker like activity before being automatically added 246 | // to the blocked ip list. 247 | application.blockIpThreshold = 15; 248 | 249 | // choose the way an ip address that is in the blocklist 250 | // is handled by the application. One of two modes are 251 | // available: 252 | // * 253 | // 'abort' - this simply aborts all further processing 254 | // 'redirect' - this redirects to the ipBlocked.html file (default) 255 | // * 256 | application.blockMode = 'redirect'; 257 | 258 | // configure if this application will use two factor authentication 259 | // two factor authentication uses the users SMS Provider and telephone 260 | // number to send an authorization code as a second security factor 261 | // for logging into the application. 262 | // NOTE: Registration depends on this being set to either true or false 263 | // If set to false, the providerId will be set to zero (0) and the 264 | // phone number will be blank. If you turn on TFA after users have 265 | // registered, they will not be able to login until these values 266 | // are assigned in the database. 267 | application.use2FA = false; 268 | 269 | // configure if the application will reject hacked passwords on 270 | // system password generation and password changes by the user 271 | // This is set to 'false' by default to maintain backwards compatibility 272 | // however it is recommended that you turn this feature on by setting 273 | // this value to 'true' instead. 274 | application.rejectHackedPasswords = false; 275 | 276 | // set the path to the top 100,000 hacked password list 277 | // you can replace this list with any list you choose. The format 278 | // of the file should be a single password entry per line. Lines 279 | // can be terminated with a cariage-return and linefeed combined, a linefeed 280 | // only or a carriage-return only. 281 | // Additional password files can be downloaded from: 282 | // https://github.com/danielmiessler/SecLists/tree/master/Passwords 283 | application.passwordFilePath = expandPath( 'data/top_100000_hacked_passwords.txt' ); 284 | 285 | // fire off framework one's method 286 | super.onApplicationStart(); 287 | } 288 | 289 | function onError( exception, event ) { 290 | // fire off framework one's method 291 | super.onError( arguments.exception, arguments.event ); 292 | } 293 | 294 | function onRequest( targetPath ) { 295 | // fire off framework one's method 296 | super.onRequest( arguments.targetPath ); 297 | } 298 | 299 | function onRequestEnd() { 300 | // fire off framework one's method 301 | super.onRequestEnd(); 302 | } 303 | 304 | function onRequestStart( targetPath ) { 305 | // fire off framework one's method 306 | super.onRequestStart( arguments.targetPath ); 307 | } 308 | 309 | function onSessionStart() { 310 | // fire off framework one's method 311 | super.onSessionStart(); 312 | } 313 | 314 | /** 315 | * @displayname setupApplication 316 | * @description I'm run by fw/1 during onApplicationStart() to configure application level settings 317 | */ 318 | function setupApplication() {} 319 | 320 | /** 321 | * @displayname setupSession 322 | * @description I'm run by fw/1 during onSessiontart() to configure session level settings 323 | */ 324 | function setupSession() { 325 | 326 | // check if we're in the 'admin' subsystem 327 | if( getSubsystem() eq 'admin' ) { 328 | // we are, call the security controller's session action to configure the session 329 | controller( 'admin:security.session' ); 330 | } 331 | 332 | } 333 | 334 | /** 335 | * @displayname setupRequest 336 | * @description I'm run by fw/1 during onRequestStart() to configure request level settings 337 | */ 338 | function setupRequest() { 339 | 340 | // get the http request headers 341 | var headers = getHTTPRequestData().headers; 342 | var ipAddress = ''; 343 | 344 | // check if we're in the production environment 345 | if( findNoCase( 'prod', getEnvironment() ) ) { 346 | 347 | // we are, check if this server sits behind a load balancer, proxy or firewall 348 | if( structKeyExists( headers, 'x-forwarded-for' ) ) { 349 | // it does, get the ip address this request has been forwarded for 350 | ipAddress = headers[ 'x-forwarded-for' ]; 351 | // otherwise 352 | } else { 353 | // it doesn't, get the ip address of the remote client 354 | ipAddress = CGI.REMOTE_ADDR; 355 | } 356 | 357 | // check if this ip address is blocked 358 | if( application.securityService.isBlockedIP( ipAddress ) ) { 359 | 360 | // switch on the block mode 361 | switch( application.blockMode ) { 362 | // redirect 363 | case 'redirect': 364 | // redirect the browser to an html page for notification 365 | location( '/ipBlocked.html', 'false', '302' ); 366 | break; 367 | 368 | // abort 369 | default: 370 | abort; 371 | break; 372 | } 373 | 374 | } 375 | 376 | // check if the query string contains SQL injection attempts 377 | // if sql injection is detected an error is thrown and caught 378 | // by home.main.error 379 | application.securityService.checkSqlInjectionAttempt( CGI.QUERY_STRING ); 380 | 381 | } 382 | 383 | // check if we're in the 'admin' subsystem 384 | if( getSubsystem() eq 'admin' ) { 385 | // we are, call the security controller's authorize action to perform session management 386 | controller( 'admin:security.authorize' ); 387 | // set HTTP headers to disallow caching of admin pages 388 | getPageContext().getResponse().addHeader( 'Cache-Control', 'no-cache, no-store, must-revalidate' ); 389 | getPageContext().getResponse().addHeader( 'Pragma', 'no-cache' ); 390 | // otherwise 391 | } else { 392 | // we aren't in the admin subsystem, set a practical age for cache control for performance 393 | // in seconds (86400 = 1 day) 394 | getPageContext().getResponse().addHeader( 'Cache-Control', 'max-age=86400' ); 395 | } 396 | 397 | // use HTTP headers to help protect against common attack vectors 398 | getPageContext().getResponse().addHeader( 'X-Frame-Options', 'deny' ); 399 | getPageContext().getResponse().addHeader( 'X-XSS-Protection', '1; mode=block' ); 400 | getPageContext().getResponse().addHeader( 'X-Content-Type-Options', 'nosniff' ); 401 | getPageContext().getResponse().addHeader( 'Strict-Transport-Security', 'max-age=31536000; includeSubDomains' ); 402 | getPageContext().getResponse().addHeader( 'Expires', '-1' ); 403 | getPageContext().getResponse().addHeader( 'X-Permitted-Cross-Domain-Policies', 'master-only' ); 404 | 405 | // check if there is a url variable for flushing the page cache 406 | if( structKeyExists( url, 'flushCache') ) { 407 | // there is, flush the page cache 408 | cfcache( action='flush' ); 409 | } 410 | 411 | } 412 | 413 | /** 414 | * @displayname getEnvironment 415 | * @description I'm run by fw/1 during onRequest() to configure environment specific settings 416 | */ 417 | public function getEnvironment() { 418 | 419 | return application.securityService.getEnvironment(); 420 | 421 | } 422 | 423 | } 424 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FW/1 Secure Authentication Example 2 | 3 | This project is an example [fw/1](https://github.com/framework-one/fw1) application with secure single and two-factor (2FA) authentication and session management functions. This code was originally put together for the `ColdFusion: Code Security Best Practices` presentation by Denard Springle at [NCDevCon 2015](http://www.ncdevcon.com) and has since been transformed into a concise starting point for developers who need to create a secure application using the [fw/1](https://github.com/framework-one/fw1) CFML MVC framework. 4 | 5 | This code has been expanded multiple times to include additional functionality not shown during the initial presentation. More details on how (and why) these security functions work and are important can be gleaned from reading the ColdFusion Code Security guides on the bottom half of [CFDocs](http://cfdocs.org/security) and from reviewing the SecurityService.cfc in /model/services/ which has been expanded with comments to help aid in understanding how and why security features have been implemented and should be easy to pick up and run with for anyone with a passing familiarity of CFML and [fw/1](https://github.com/framework-one/fw1). 6 | 7 | ## Features and Notes 8 | 9 | * Based on basic example [fw/1](https://github.com/framework-one/fw1) application 10 | * Uses subsystems for separation of concerns, securing only the `admin` subsystem 11 | * Includes a SecurityService component that has encryption, decryption, hashing, password generation and session management code 12 | * Includes a security controller for managing session and request scope session management within the `admin` subsystem 13 | * Uses cookies and object cache for session management 14 | * Includes HMAC protection for session cookies to help prevent tampering 15 | * Rotates the session id on each request and utilizes form tokenization to help prevent CSRF 16 | * Federates the login with a cookie and referrer requirement 17 | * Protects the users password from disclosure with SHA-384 hashing during login 18 | * Stores user data in encrypted format in the database 19 | * Default CBC/PKCS5Padding defined for encryption algorithms 20 | * Includes HTTP security headers designed to reduce attack surface 21 | * Uses keyring stored on disk to load encryption keys instead of hard-coded in the `Application.cfc` 22 | * Includes functions for reading, writing and generating a random keyring file 23 | * Includes functions for checking for, adding, removing and importing blocked IP's 24 | * Includes functions for checking for, adding, removing and importing watched IP's 25 | * Includes functions for managing watched/blocked IPs by catching common parameter tampering/sql injection attacks 26 | * Includes optional `addDate` true/false parameter to uberHash function to append the current date to the input value on hash 27 | * Includes 'dummy' cookies for the purpose of further obfuscating which cookie is used for session management 28 | * Includes repeatable form encryption for ajax populated and javascript selected form fields 29 | * Includes BaseBean with convenience functions for populating primary key data and CSRF fields in urls and forms (respectively) 30 | * Includes page caching and flushing capabilities added for static views (for [NVCFUG Preso](https://www.meetup.com/nvcfug/events/236791823/)) - use url param `flushCache` to flush 31 | * Includes fw1 environment control and check for the `prod` (production) environment before running IP watching or blocking routines 32 | * Includes configurable block mode - one of abort or redirect. Abort simply aborts further processing for blocked IP's. Redirect works as it did before this release, redirecting to the `ipBlocked.html` file. 33 | * Migrated to new `Application.cfc` FW/1 initialization model 34 | * Improved HMAC key management to prevent development reloads from forcing the user to re-login (for non-production environments) 35 | * **BREAKING CHANGE** The two factor (2FA) authentication code from our two-factor example has been rolled into this code as of 7/24/2017. You can turn on 2FA in the `Application.cfc` (off by default to maintain backwards compatibility). Code prior to this release has been moved to the `legacy` branch. 36 | * **BREAKING CHANGE** As of 9/12/2017 the keyring master key now uses a PBKDF key on Lucee 5+ and ACF 11+ engines by default instead of legacy hashing to further enhance the security of the keyring. A new function `rekeyKeyRing()` has been added to the SecurityService to aid in rekeying your keyring for this change (and rekeying it in general) if upgrading from a previous release. You may alternatively uncomment a line in `Application.cfc` to force legacy master key usage. Please see additional notes in the `Application.cfc` for further details. Lucee 4.5 will continue to use the legacy hashing of the master key. 37 | * **BREAKING CHANGE** The keyring path and the master key are now defined in their own variables in the application scope instead of being hard-coded in the initialization of the security service. These are now BASE64 encoded to aid in obfuscating the key and filename in case of code disclosure. If upgrading from a previous release you will need to BASE64 encode your master keyphrase and filename and replace the new default one in `Application.cfc`. Please see additional notes in the `Application.cfc` for further details. 38 | * **BREAKING CHANGE** The dashboard controller has removed the `rc.product` and `rc.version` variables definitions and the dashboard view now uses the engine and engine version information derived from the application scope 39 | * There is now an option to use a hacked password list to prevent the system from generating, or user from choosing, a password from the top 100,000 known passwords. This is turned off (`false`) by default in `Application.cfc` for backwards compatibility. To use this new function you should set `application.rejectHackedPasswords` to `true`. 40 | * **BREAKING CHANGE** The `SecurityService.cfc` has been enhanced with additional functionality to randomly generate (when creating a new keyring) and use initialization vectors with all encryption and decryption. This will break existing code that is using a keyring without an initialization vector (will return an error about the length of the initialization vector). 41 | * NEW! **BREAKING CHANGE** As of 6/26/2024, the `SecurityService.cfc` has been modified for compatibility with JDK17+ as it relates to the master key encryption and decryption block mode being utilized. Prior to these changes the master key encryption and decryption relied on the CTR block mode of encryption (BLOWFISH/CTR/PKCS5Padding). This has been modified to instead utilize CBC block mode (BLOWFISH/CBC/PKCS5Padding) for greater compatibility with JDK17+. This will break existing code that is using a keyring encrypted with the old CTR block mode. It is recommended to decrypt your existing keyring with the CTR block mode and then re-encrypt using the CBC block mode if you are upgrading from a previous version of this repository. The following code will help you accomplish this safely: 42 | 43 | ``` 44 | 45 | if( !structKeyExists( variables, 'rc' ) ) { 46 | variables.rc = {}; 47 | structAppend( rc, url ); 48 | structAppend( rc, form, true ); 49 | } 50 | 51 | // set a keyring path 52 | rc.keyRingPath = expandPath( './keyrings/[ABCDEF0123456789].bin' ); 53 | // set a keyring backup path 54 | rc.backupPath = rc.keyRingPath & '_BACKUP_' & dateTimeFormat( now(), 'yyyymmddhhnnss' ); 55 | 56 | // validate the keyring file exists 57 | if( !fileExists( rc.keyRingPath ) ) { 58 | throw( rc.keyRingPath & ': keyring path does not exist!' ); 59 | } 60 | 61 | // backup the existing keyring file 62 | fileCopy( rc.keyRingPath, rc.backupPath ); 63 | 64 | // validate the backup file exists 65 | if( !fileExists( rc.backupPath ) ) { 66 | throw( rc.backupPath & ': backup path does not exist!' ); 67 | } 68 | 69 | // load the CTR encrypted keyring from the file 70 | rc.keyring = charsetEncode( fileReadBinary( rc.keyRingPath ), 'utf-8' ); 71 | 72 | // decrypt the keyring with the master key and BLOWFISH/CTR block mode 73 | rc.roundOne = decrypt( rc.keyring, rc.masterKey, 'BLOWFISH/CTR/PKCS5Padding', 'HEX' ); 74 | rc.roundTwo = decrypt( roundOne, rc.masterKey, 'AES/CBC/PKCS5Padding', 'HEX' ); 75 | 76 | // re-encrypt the keyring with the master key and BLOWFISH/CBC block mode 77 | rc.roundOne = encrypt( rc.roundTwo, rc.masterKey, 'AES/CBC/PKCS5Padding', 'HEX' ); 78 | rc.roundTwo = encrypt( roundOne, variables.masterKey, 'BLOWFISH/CBC/PKCS5Padding', 'HEX' ); 79 | 80 | // write the keyring back to disk 81 | fileWrite( rc.keyRingPath, charsetDecode( rc.roundTwo, 'utf-8' ) ); 82 | 83 | ``` 84 | * NEW! The scrypt JAR has been added to the repository and initialized for use (with 32MB/64MB RAM used for hashing). It has been added to the `uberHash()` method of `SecurityService.cfc` and can be utilized by passing the flag `useScrypt` as `true` (default is `false`). e.g. `application.securityService.uberHash( input = 'mY$7R0nGP@$$w0R6', useScrypt = true )` 85 | 86 | * NEW! The scrypt JAR has been added to the repository and initialized for use (with 32MB/64MB RAM used for hashing). It has been added to the `uberHash()` method of `SecurityService.cfc` and can be utilized by passing the flag `useScrypt` as `true` (default is `false`). e.g. `application.securityService.uberHash( input = 'mY$7R0nGP@$$w0R6', useScrypt = true )` 87 | * NEW! The missing link... a new function, `checkScrypt()` has been added to the `SecurityService.cfc` to check for values hased using `useScript=true` with `uberHash()` 88 | 89 | ## Compatibility 90 | 91 | * Lucee 4.5+ 92 | 93 | * Adobe ColdFusion 2021+ 94 | 95 | ## Installing 96 | 97 | 1. Drop the code into your favorite CFML engine's webroot OR install using [CommandBox](https://www.ortussolutions.com/products/commandbox) using the command `box install fw1-sa` 98 | 2. Create a database and generate the users and smsProviders database tables (MSSQL SQL and Excel data provided in the 'database' folder) 99 | 3. Create a datasource called `twofactorauth` for your database in your CFML engine's admin (or change in `Application.cfc`) 100 | 4. Configure an object cache, if one is not already defined (or, optionally, add it to `Application.cfc` if running Lucee 5.x+) 101 | 5. Configure a mail server in your CFML engine's admin 102 | 6. Move the `keyrings` folder to a location outside your webroot 103 | 7. Modify the default `developmentHmacKey` value in `Application.cfc` (use `generateSecretKey( 'HMACSHA512' )`) 104 | 8. Change the `keyRingPath` location to where you moved the `keyrings` folder to in `Application.cfc` 105 | 9. Change the hash iterations for the hashed keyring file name from the default value of `173` to some other integer number of iterations in `Application.cfc` 106 | 10. Provide a unique BASE64 encoded value for the application password in `Application.cfc` (instead of `c2VjdXJlX2F1dGhfbWFzdGVyX2tleQ==`) 107 | 11. Provide a unique BASE64 encoded value for the application salt in `Application.cfc` (instead of `UnRUcFBBS1hOQmgwem9XYg==`) 108 | 12. Provide a unique BASE64 encoded value for the keyring filename in `Application.cfc` (instead of `c2VjdXJlX2F1dGhfa2V5cmluZw==`) 109 | 13. Change the hash iterations for the hashed master key from the default value of `512` to some other integer number of iterations in `Application.cfc` 110 | 14. Change the starting location for the `mid()` function of the hashed master key to start at a position other than `38` in a range from `1` to `106` 111 | 15. Provide unique values for the `cookieName` and `dummyCookieOne`, `dummyCookieTwo` and `dummyCookieThree` values in `Application.cfc` 112 | 16. Modify remaining application variables in `Application.cfc` as needed (see notes in `Application.cfc`) 113 | 17. Browse to webroot to launch the application and generate a unique set of encryption keys in your keyring 114 | 18. Modify the `check if the keyring is a valid array of keys` statement in `Application.cfc` to prevent regeneration of a new keyring file after initial launch. See notes in `Application.cfc`. 115 | 19. Register an account, login and enjoy! 116 | 117 | ## Upgrading 118 | 119 | **NOTE** If you are currently running a version of fw1-sa without the 2FA integration, then you'll need to complete the following steps before updating to the latest master branch: 120 | 121 | _If **not using** 2FA_: 122 | 123 | 1. Preserve a copy of your existing `Application.cfc` (or `MyApplication.cfc` if included in your distribution) so you can copy values for keyring and other application variables as needed. 124 | 2. Modify your users table to include `providerId` and `phone` as additional fields before updating 125 | 126 | _If **using** 2FA_: 127 | 128 | 1. Preserve a copy of your existing `Application.cfc` (or `MyApplication.cfc` if included in your distribution) so you can copy values for keyring and other application variables as needed. 129 | 2. Modify your users table as above 130 | 3. Add the smsProviders table and import the included data 131 | 4. Assign sms provider id's and phone numbers to existing users *(this must be done before switching 2FA on else users will not be able to authenticate)* 132 | 133 | ## Bugs and Feature Requests 134 | 135 | If you find any bugs or have a feature you'd like to see implemented in this code, please use the issues area here on GitHub to log them. 136 | 137 | ## Contributing 138 | 139 | This project is actively being maintained and monitored by Denard Springle. If you would like to contribute to this example please feel free to fork, modify and send a pull request! 140 | 141 | ## Attribution 142 | 143 | This project utilizes the free open source MVC CFML (ColdFusion) framework [Framework One (fw/1)](https://github.com/framework-one/fw1) by [Sean Corfield](https://twitter.com/seancorfield). 144 | 145 | ## License 146 | 147 | The use and distribution terms for this software are covered by the Apache Software License 2.0 (http://www.apache.org/licenses/LICENSE-2.0). 148 | -------------------------------------------------------------------------------- /admin/controllers/main.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @file admin/controllers/main.cfc 4 | * @author Denard Springle ( denard.springle@gmail.com ) 5 | * @description I am the controller for the admin:main section 6 | * 7 | */ 8 | 9 | component accessors="true" { 10 | 11 | property userService; 12 | property smsProviderService; 13 | property mailService; 14 | 15 | /** 16 | * @displayname init 17 | * @description I am the constructor method for main 18 | * @return this 19 | */ 20 | public any function init( fw ) { 21 | variables.fw = fw; 22 | return this; 23 | } 24 | 25 | /** 26 | * @displayname default 27 | * @description I clear session data and present the login view 28 | */ 29 | public void function default( rc ) { 30 | 31 | // disable the admin layout since the login page has it's own html 32 | variables.fw.disableLayout(); 33 | 34 | // set a zero session cookie when hitting the login page (federate the login) 35 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.cookieName#=0;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 36 | 37 | // send a zero primary dummy cookie 38 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.dummyCookieOne#=0;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 39 | 40 | // send a zero secondary dummy cookie 41 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.dummyCookieTwo#=0;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 42 | 43 | // send a zero tertiary dummy cookie 44 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.dummyCookieThree#=0;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 45 | 46 | // lock and clear the sessionObj 47 | lock scope='session' timeout='10' { 48 | session.sessionObj = new model.beans.Session(); 49 | } 50 | 51 | // check for the existence of the 'msg' url paramter 52 | if( structKeyExists( rc, 'msg' ) ) { 53 | // and generate a message to be displayed 54 | if( rc.msg eq 500 ) { 55 | rc.message = 'Both Email and Password fields are required to login.'; 56 | } else if( rc.msg eq 404 or rc.msg eq 403 ) { 57 | rc.message = 'Account not found with provided credentials. Please try again.'; 58 | } else if( rc.msg eq 555 ) { 59 | rc.message = 'Account is disabled. Please contact your system administrator.'; 60 | } else if( rc.msg eq 200 ) { 61 | rc.message = "You have been successfully logged out."; 62 | } else if( rc.msg eq 410 ) { 63 | rc.message = 'Second factor was not provided. Please login again.'; 64 | } else if( rc.msg eq 411 ) { 65 | rc.message = 'Second factor does not match. Please login again.'; 66 | } else { 67 | rc.message = 'Your session has timed out. Please log in again to continue.'; 68 | } 69 | // if it doesn't exist 70 | } else { 71 | // create it 72 | rc.msg = 0; 73 | // and set a null message string 74 | rc.message = ''; 75 | } 76 | 77 | // set a title for the login page to render 78 | rc.title = 'Secure Authentication Sign In'; 79 | 80 | } 81 | 82 | /** 83 | * @displayname dashboard 84 | * @description I present the dashbaord view 85 | */ 86 | public void function dashboard( rc ) {} 87 | 88 | /** 89 | * @displayname authenticate 90 | * @description I authenticate a user login and redirect to the dashboard view if valid 91 | */ 92 | public void function authenticate( rc ) { 93 | 94 | var qGetUser = ''; 95 | var hashedPwd = ''; 96 | 97 | // check if the host and referrer match (federate the login) 98 | if( !findNoCase( CGI.HTTP_HOST, CGI.HTTP_REFERER ) ) { 99 | // they don't, redirect to the logout page 100 | variables.fw.redirect( action = 'main.logout', queryString = 'msg=503' ); 101 | } 102 | 103 | // check if the session cookie exists (federate the login) 104 | if( !structKeyExists( cookie, application.cookieName ) ) { 105 | // it doesn't, redirect to the logout page 106 | variables.fw.redirect( action = 'main.logout', queryString = 'msg=504' ); 107 | } 108 | 109 | // ensure a username and password were sent 110 | if( !len( rc.username ) OR !len( rc.password ) ) { 111 | // they weren't, redirect to the logout page 112 | variables.fw.redirect( action = 'main.logout', queryString = 'msg=500' ); 113 | } 114 | 115 | // ensure the CSRF token is provided and valid 116 | if( !structKeyExists( rc, 'f' & application.securityService.uberHash( 'token', 'SHA-512', 150 ) ) OR !CSRFVerifyToken( rc[ 'f' & application.securityService.uberHash( 'token', 'SHA-512', 150 ) ] ) ) { 117 | // it doesn't, redirect to the logout page 118 | variables.fw.redirect( action = 'main.logout', queryString = 'msg=505' ); 119 | } 120 | 121 | // get the user from the database by encrypted username 122 | qGetUser = userService.filter( username = application.securityService.dataEnc( rc.username, 'repeatable' ) ); 123 | 124 | // check if there is a record for the passed username 125 | if( !qGetUser.recordCount ) { 126 | // there isn't, redirect to the logout page 127 | variables.fw.redirect( action = 'main.logout', queryString = 'msg=404' ); 128 | } 129 | 130 | // check to be sure this user has an active account 131 | if( !qGetUser.isActive ) { 132 | // they don't, redirect to the logout page 133 | variables.fw.redirect( action = 'main.logout', queryString = 'msg=555' ); 134 | } 135 | 136 | // hash the users stored password with the passed heartbeat for comparison 137 | hashedPwd = hash( lcase( application.securityService.dataDec( qGetUser.password, 'db' ) ) & rc.heartbeat, 'SHA-384' ); 138 | 139 | // compare the hashed stored password with the passed password 140 | if( !findNoCase( hashedPwd, rc.password ) ) { 141 | // they don't match, redirect to the logout page 142 | variables.fw.redirect( action = 'main.logout', queryString = 'msg=403' ); 143 | } 144 | 145 | // lock the session scope and create a sessionObj for this user 146 | lock scope='session' timeout='10' { 147 | session.sessionObj = application.securityService.createUserSession( 148 | userId = qGetUser.userId, 149 | role = qGetUser.role, 150 | firstName = application.securityService.dataDec( qGetUser.firstName, 'db' ), 151 | lastName = application.securityService.dataDec( qGetUser.lastName, 'db' ) 152 | ); 153 | } 154 | 155 | // check if we're using two factor authentication 156 | if( application.use2FA ) { 157 | // we are, send the mfa code to this user for this session 158 | mailService.sendMfaCode( phone = application.securityService.dataDec( qGetUser.phone, 'db' ), providerEmail = variables.smsProviderService.getSmsProviderById( qGetUser.providerId ).getEmail(), mfaCode = session.sessionObj.getMfaCode() ); 159 | } 160 | 161 | // set the session cookie with the new encrypted session id 162 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.cookieName#=#application.securityService.setSessionIdForCookie( session.sessionObj.getSessionId() )#;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 163 | 164 | // send a new primary dummy cookie 165 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.dummyCookieOne#=#application.securityService.generateDummyCookieValue( 'BASE64' )#;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 166 | 167 | // send a new secondary dummy cookie 168 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.dummyCookieTwo#=#application.securityService.generateDummyCookieValue( 'UU' )#;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 169 | 170 | // send a new tertiary dummy cookie 171 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.dummyCookieThree#=#application.securityService.generateDummyCookieValue( 'HEX' )#;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 172 | 173 | // rotate the cfid/cftoken session to prevent session fixation 174 | // NOTE: This does *not* work with J2EE (jsessionid) sessions 175 | sessionRotate(); 176 | // check if we're using two factor authentication 177 | if( application.use2FA ) { 178 | // we are, go to the twofactor view 179 | variables.fw.redirect( 'main.twofactor' ); 180 | // otherwise 181 | } else { 182 | // we're not, go to the dashboard view 183 | variables.fw.redirect( 'main.dashboard' ); 184 | } 185 | 186 | } 187 | 188 | /** 189 | * @displayname twofactor 190 | * @description I present the two-factor view 191 | */ 192 | public void function twofactor( rc ) { 193 | 194 | // disable the admin layout since the two-factor page has it's own html 195 | variables.fw.disableLayout(); 196 | 197 | // set a title for the login page to render 198 | rc.title = 'Secure Authentication Sign In » Second Factor'; 199 | 200 | } 201 | 202 | /** 203 | * @displayname authfactor 204 | * @description I authenticate the second factor 205 | */ 206 | public void function authfactor( rc ) { 207 | 208 | if( !structKeyExists( rc, 'twofactor' ) OR !len( rc.twofactor ) ) { 209 | // they don't match, redirect to the logout page 210 | variables.fw.redirect( action = 'main.logout', queryString = 'msg=410' ); 211 | } 212 | 213 | // ensure the CSRF token is provided and valid 214 | if( !structKeyExists( rc, 'f' & application.securityService.uberHash( 'token', 'SHA-512', 1700 ) ) OR !CSRFVerifyToken( rc[ 'f' & application.securityService.uberHash( 'token', 'SHA-512', 1700 ) ] ) ) { 215 | // it doesn't, redirect to the logout page 216 | variables.fw.redirect( action = 'main.logout', queryString = 'msg=510' ); 217 | } 218 | 219 | if( compareNoCase( rc.twofactor, session.sessionObj.getMfaCode() ) NEQ 0 ) { 220 | variables.fw.redirect( action = 'main.logout', queryString = 'msg=411' ); 221 | } 222 | 223 | // lock the session scope and create a sessionObj for this user 224 | lock scope='session' timeout='10' { 225 | session.sessionObj.setMfaCode( '' ); 226 | session.sessionObj.setIsAuthenticated( true ); 227 | } 228 | 229 | // and go to the dashboard view 230 | variables.fw.redirect( 'main.dashboard' ); 231 | 232 | } 233 | /** 234 | * @displayname logout 235 | * @description I clear session data and present the login view 236 | */ 237 | public void function logout( rc ) { 238 | 239 | // check if we have a session object to clear 240 | if( structKeyExists( session, 'sessionObj' ) ) { 241 | 242 | // we do, clear the users session object from cache 243 | application.securityService.clearUserSession( session.sessionObj ); 244 | 245 | } 246 | 247 | // lock and clear the sessionObj 248 | lock scope='session' timeout='10' { 249 | session.sessionObj = new model.beans.Session(); 250 | } 251 | 252 | // set a zero session cookie when logging out (clear the session cookie) 253 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.cookieName#=0;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 254 | 255 | // send a zero primary dummy cookie 256 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.dummyCookieOne#=0;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 257 | 258 | // send a zero secondary dummy cookie 259 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.dummyCookieTwo#=0;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 260 | 261 | // send a zero tertiary dummy cookie 262 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.dummyCookieThree#=0;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 263 | 264 | // invalidate the cfid/cftoken session 265 | // NOTE: This does *not* work with J2EE (jsessionid) sessions 266 | sessionInvalidate(); 267 | 268 | // check if a message was passed into the logout function 269 | if( !structKeyExists( rc, 'msg') ) { 270 | // it wasn't, regular logout by the user, set the msg to 200 271 | rc.msg = 200; 272 | } 273 | 274 | // go to the login page 275 | variables.fw.redirect( action = 'main.default', queryString = 'msg=' & rc.msg ); 276 | 277 | } 278 | 279 | } 280 | -------------------------------------------------------------------------------- /admin/controllers/security.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @file admin\controllers\security.cfc 4 | * @author Denard Springle ( denard.springle@gmail.com ) 5 | * @description I am the security controller for session management for the admin subsystem 6 | * 7 | */ 8 | 9 | component { 10 | 11 | /** 12 | * @displayname init 13 | * @description I am the constructor method for security 14 | * @return this 15 | */ 16 | function init( fw ) { 17 | variables.fw = fw; 18 | } 19 | 20 | /** 21 | * @displayname session 22 | * @description I setup a sessionObj for this Session 23 | */ 24 | function session( rc ) { 25 | 26 | // lock and clear the sessionObj 27 | lock scope='session' timeout='10' { 28 | session.sessionObj = new model.beans.Session(); 29 | } 30 | 31 | } 32 | 33 | /** 34 | * @displayname authorize 35 | * @description I authenticate and rotate a session on each request 36 | */ 37 | function authorize( rc ) { 38 | 39 | var actionArr = [ 'admin:main.default', 'admin:main.authenticate', 'admin:main.twofactor', 'admin:main.authfactor', 'admin:main.logout' ]; 40 | 41 | // check if we're already logging in 42 | if( !arrayFind( actionArr, rc.action )) { 43 | 44 | // we're not, check if the session cookie is defined 45 | if( !structKeyExists( cookie, application.cookieName ) ) { 46 | // it isn't, redirect to the logout page 47 | variables.fw.redirect( action = 'main.logout', queryString = "msg=501" ); 48 | } 49 | 50 | // try 51 | try { 52 | // decrypt the cookie 53 | rc.sessionId = application.securityService.getSessionIdFromCookie( cookie[ application.cookieName ] ); 54 | // catch any decryption errors 55 | } catch ( any e ) { 56 | // decryption failed (invalid cookie value), redirect to the logout page 57 | variables.fw.redirect( action = 'main.logout', queryString = "msg=501" ); 58 | } 59 | 60 | // lock the session and get the sessionObj from the cache 61 | lock scope='session' timeout='10' { 62 | session.sessionObj = application.securityService.checkUserSession( rc.sessionId ); 63 | } 64 | 65 | // check if the sessionObj returned is valid 66 | if( session.sessionObj.getUserId() EQ 0 ) { 67 | // it isn't, redirect to the logout page 68 | variables.fw.redirect( action = 'main.logout', queryString = "msg=502" ); 69 | } 70 | 71 | // check if the second factor is required and has been completed 72 | if( application.use2FA and !session.sessionObj.getIsAuthenticated() ) { 73 | // it hasn't, redirect to the logout page 74 | variables.fw.redirect( action = 'main.logout', queryString = "msg=507" ); 75 | } 76 | 77 | // lock the session and rotate the session id (for every request) 78 | // NOTE: This rotation can cause decryption errors in some browsers when the back button is used 79 | // due to the browser sending the cookie associated with that request. If this is an issue for 80 | // your code, simply comment out the following three lines and sessions will not be rotated. 81 | lock scope='session' timeout='10' { 82 | session.sessionObj = application.securityService.rotateUserSession( session.sessionObj ); 83 | } 84 | 85 | // Update session's last action datetime and save session 86 | lock scope='session' timeout='10' { 87 | session.sessionObj = application.securityService.updateUserSession( session.sessionObj ); 88 | } 89 | 90 | // send a new cookie with the new encrypted session id 91 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.cookieName#=#application.securityService.setSessionIdForCookie( session.sessionObj.getSessionId() )#;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 92 | 93 | // send a new primary dummy cookie 94 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.dummyCookieOne#=#application.securityService.generateDummyCookieValue( 'BASE64' )#;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 95 | 96 | // send a new secondary dummy cookie 97 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.dummyCookieTwo#=#application.securityService.generateDummyCookieValue( 'UU' )#;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 98 | 99 | // send a new tertiary dummy cookie 100 | getPageContext().getResponse().addHeader("Set-Cookie", "#application.dummyCookieThree#=#application.securityService.generateDummyCookieValue( 'HEX' )#;path=/;domain=#listFirst( CGI.HTTP_HOST, ':' )#;HTTPOnly"); 101 | 102 | // rotate the cfid/cftoken session to prevent session fixation 103 | // NOTE: This does *not* work with J2EE (jsessionid) sessions 104 | sessionRotate(); 105 | 106 | } 107 | 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /admin/layouts/default.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Secure Auth 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | #body# 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /admin/views/main/dashboard.cfm: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Secure Auth Example » Dashboard

4 |
5 |
6 |
 
7 |
8 |
9 |
10 |

Welcome #session.sessionObj.getFirstName()# #session.sessionObj.getLastName()#!

11 |

You have successfully logged into the Secure Auth Example Dashboard.

12 |

Nothing to see here... this is just an example, after all :P

13 |
14 |
15 |
16 |
17 |
18 |
 
19 |
20 |
21 | 22 |
23 |
24 |
 
25 |
26 |
27 |
Running #encodeForHtml( application.engine )# #encodeForHtml( application.engineVersion )#
28 |
29 |
-------------------------------------------------------------------------------- /admin/views/main/default.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | <cfoutput>#encodeForHtml( rc.title )#</cfoutput> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 48 | 49 | 50 | 51 | 52 | 53 |
54 | 55 | 56 |
57 |
58 | 59 |
60 | 61 | #encodeForHtml( rc.message )# 62 |
63 | 64 |
65 | 66 | #encodeForHtml( rc.message )# 67 |
68 |
69 |
70 |
71 | 72 |
73 | 74 |
75 |
76 |
77 |
78 | #encodeForHtml( rc.title )# 79 |
80 |
81 |
82 | 83 | 84 | 85 | 86 |
87 |
88 |
89 | 90 |
91 |
92 |
93 |
94 |
95 |
96 | 97 | 98 | 99 | 100 |
101 |
102 |
103 |
104 | 105 | 106 | 107 | 108 |
109 |
110 |
111 | 112 |
113 |
114 |
115 |
116 |
117 |
118 | 121 |
122 |
123 |
124 |
125 | 126 | 127 | 128 | 129 | 130 | 131 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /admin/views/main/twofactor.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | <cfoutput>#encodeForHtml( rc.title )#</cfoutput> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 48 | 49 | 50 | 51 | 52 | 53 |
54 | 55 |
56 |
57 |
58 |
59 | #encodeForHtml( rc.title )# 60 |
61 |
62 | 63 |
64 | 65 | 66 |
67 |
68 |

Please enter your authorization code below.

69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | 77 | 78 | 79 | 80 |
81 |
82 |
83 | 84 |
85 |
86 |
87 |
88 |
89 |
90 | 92 |
93 |
94 |
95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /blocked/Application.cfc: -------------------------------------------------------------------------------- 1 | // minimal Application.cfc to prevent fw/1 from triggering 2 | // during http calls to blocked_ips.json 3 | component { 4 | this.name = hash( getBaseTemplatePath() ); 5 | } -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "framework-one-secure-auth", 3 | "slug" : "fw1-sa", 4 | "version" : "3.0.1", 5 | "author" : "Denard Springle ", 6 | "location" : "https://github.com/ddspringle/framework-one-secure-auth/archive/master.zip", 7 | "Homepage" : "https://github.com/ddspringle/framework-one-secure-auth", 8 | "Documentation" : "https://github.com/ddspringle/framework-one-secure-auth", 9 | "Repository" : { 10 | "type" : "git", "URL" : "https://github.com/ddspringle/framework-one-secure-auth.git" 11 | }, 12 | "Bugs" : "https://github.com/ddspringle/framework-one-secure-auth/issues", 13 | "shortDescription" : "An example fw/1 application with secure authentication and session management functions", 14 | "changelog" : "https://github.com/ddspringle/framework-one-secure-auth/commits/master", 15 | "type" : "projects", 16 | "keywords" : [ "fw1", "framework one", "mvc", "authentication" ], 17 | "engines" : [ 18 | { "type" : "railo", "version" : ">=4.1.x" }, 19 | { "type" : "lucee", "version" : ">=4.5.x" }, 20 | { "type" : "adobe", "version" : ">=11.0.0" } 21 | ], 22 | "defaultEngine" : "lucee", 23 | "License" : [ 24 | { "type" : "Apache 2.0", "URL" : "http://www.apache.org/licenses/LICENSE-2.0" } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /database/smsProviders_data.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddspringle/framework-one-secure-auth/195c1f1fe673e52091cce289cd2aedb3e26c66db/database/smsProviders_data.xls -------------------------------------------------------------------------------- /database/users_and_smsprovider_tables.sql: -------------------------------------------------------------------------------- 1 | USE [twofactorauth] 2 | GO 3 | 4 | SET ANSI_NULLS ON 5 | GO 6 | 7 | SET QUOTED_IDENTIFIER ON 8 | GO 9 | 10 | SET ANSI_PADDING ON 11 | GO 12 | 13 | CREATE TABLE [dbo].[smsProviders]( 14 | [providerId] [int] IDENTITY(1,1) NOT NULL, 15 | [provider] [varchar](255) NOT NULL, 16 | [email] [varchar](255) NOT NULL, 17 | [isActive] [bit] NOT NULL 18 | ) ON [PRIMARY] 19 | 20 | GO 21 | 22 | CREATE TABLE [dbo].[users]( 23 | [userId] [int] IDENTITY(1,1) NOT NULL, 24 | [providerId] [int] NOT NULL, 25 | [username] [varchar](max) NOT NULL, 26 | [password] [varchar](max) NOT NULL, 27 | [firstName] [varchar](max) NOT NULL, 28 | [lastName] [varchar](max) NOT NULL, 29 | [phone] [varchar](max) NOT NULL, 30 | [role] [int] NOT NULL, 31 | [isActive] [bit] NOT NULL, 32 | CONSTRAINT [PK_users] PRIMARY KEY CLUSTERED 33 | ( 34 | [userId] ASC 35 | )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 36 | ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] 37 | GO 38 | 39 | SET ANSI_PADDING OFF 40 | GO 41 | 42 | ALTER TABLE [dbo].[smsProviders] ADD CONSTRAINT [DF_smsProviders_isActive] DEFAULT ((1)) FOR [isActive] 43 | GO 44 | 45 | ALTER TABLE [dbo].[users] ADD CONSTRAINT [DF_users_role] DEFAULT ((0)) FOR [role] 46 | GO 47 | 48 | ALTER TABLE [dbo].[users] ADD CONSTRAINT [DF_users_isActive] DEFAULT ((1)) FOR [isActive] 49 | GO 50 | 51 | -------------------------------------------------------------------------------- /framework/Application.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | // Version: FW/1 4.3.0 3 | 4 | // copy this to your application root to use as your Application.cfc 5 | // or incorporate the logic below into your existing Application.cfc 6 | 7 | // you can provide a specific application name if you want: 8 | this.name = hash( getBaseTemplatePath() ); 9 | 10 | // any other application settings: 11 | this.sessionManagement = true; 12 | 13 | // set up per-application mappings as needed: 14 | // this.mappings[ '/framework' ] = expandPath( '../path/to/framework' ); 15 | // this.mappings[ '/app' ] expandPath( '../path/to/app' ); 16 | 17 | function _get_framework_one() { 18 | if ( !structKeyExists( request, '_framework_one' ) ) { 19 | 20 | // create your FW/1 application: 21 | request._framework_one = new framework.one(); 22 | 23 | // you can specify FW/1 configuration as an argument: 24 | // request._framework_one = new framework.one({ 25 | // base : '/app', 26 | // trace : true 27 | // }); 28 | 29 | // if you need to override extension points, use 30 | // MyApplication.cfc for those and then do: 31 | // request._framework_one = new MyApplication({ 32 | // base : '/app', 33 | // trace : true 34 | // }); 35 | 36 | } 37 | return request._framework_one; 38 | } 39 | 40 | // delegation of lifecycle methods to FW/1: 41 | function onApplicationStart() { 42 | return _get_framework_one().onApplicationStart(); 43 | } 44 | function onError( exception, event ) { 45 | return _get_framework_one().onError( exception, event ); 46 | } 47 | function onRequest( targetPath ) { 48 | return _get_framework_one().onRequest( targetPath ); 49 | } 50 | function onRequestEnd() { 51 | return _get_framework_one().onRequestEnd(); 52 | } 53 | function onRequestStart( targetPath ) { 54 | return _get_framework_one().onRequestStart( targetPath ); 55 | } 56 | function onSessionStart() { 57 | return _get_framework_one().onSessionStart(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /framework/MyApplication.cfc: -------------------------------------------------------------------------------- 1 | component extends="framework.one" { 2 | // Version: FW/1 4.3.0-SNAPSHOT 3 | 4 | // if you need to provide extension points, copy this to 5 | // your web root, next to your Application.cfc, and add 6 | // functions to it, then in Application.cfc use: 7 | // request._framework_one = new MyApplication( config ); 8 | // instead of: 9 | // request._framework_one = new framework.one( config ); 10 | // in the _get_framework_one() function. 11 | // 12 | // if you do not need extension points, you can ignore this 13 | 14 | function setupApplication() { } 15 | 16 | function setupEnvironment( env ) { } 17 | 18 | function setupSession() { } 19 | 20 | function setupRequest() { } 21 | 22 | function setupResponse( rc ) { } 23 | 24 | function setupSubsystem( module ) { } 25 | 26 | function setupView( rc ) { } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /framework/WireBoxAdapter.cfc: -------------------------------------------------------------------------------- 1 | component extends="wirebox.system.ioc.Injector" { 2 | variables._fw1_version = "4.3.0"; 3 | /* 4 | Copyright (c) 2010-2018, Sean Corfield 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // the FW/1 requirements for a bean factory are very simple: 20 | 21 | public boolean function containsBean( string beanName ) { 22 | return super.containsInstance( beanName ); 23 | } 24 | 25 | public any function getBean( string beanName, struct constructorArgs ) { 26 | if ( structKeyExists( arguments, "constructorArgs" ) ) { 27 | return super.getInstance( name=beanName, initArguments=constructorArgs ); 28 | } 29 | return super.getInstance( beanName ); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /framework/aop.cfc: -------------------------------------------------------------------------------- 1 | component extends="framework.ioc" { 2 | variables._fw1_version = "4.3.0"; 3 | variables._aop1_version = variables._fw1_version; 4 | /* 5 | Copyright (c) 2013-2018, Mark Drew, Sean Corfield, Daniel Budde 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | // Internal cache of interceptor definitions. 20 | variables.interceptorCache = {regex = [], name = {}, type = []}; 21 | 22 | 23 | 24 | 25 | // -------------- // 26 | // PUBLIC METHODS // 27 | // -------------- // 28 | 29 | /** Constructor. */ 30 | public any function init(any folders, struct config = {}) 31 | { 32 | super.init(argumentCollection = arguments); 33 | 34 | if (structKeyExists(arguments.config, "interceptors") && isArray(arguments.config.interceptors) && arrayLen(arguments.config.interceptors)) 35 | { 36 | loadInterceptors(arguments.config.interceptors); 37 | } 38 | } 39 | 40 | 41 | /** Adds an interceptor definition to the definition cache. */ 42 | public any function intercept(string beanName, string interceptorName, string methods = "") 43 | { 44 | var interceptDefinition = 45 | { 46 | name = arguments.interceptorName, 47 | methods = arguments.methods 48 | }; 49 | 50 | 51 | arguments.beanName = trim(arguments.beanName); 52 | 53 | 54 | // Determine if this is a name match or regex match. 55 | if (len(arguments.beanName) && left(arguments.beanName, 1) == "/" && right(arguments.beanName, 1) == "/") 56 | { 57 | // Store regex without the forward slashes. 58 | interceptDefinition.regex = mid(arguments.beanName, 2, len(arguments.beanName) - 2); 59 | 60 | arrayAppend(variables.interceptorCache.regex, interceptDefinition); 61 | } 62 | else 63 | { 64 | if (!structKeyExists(variables.interceptorCache.name, arguments.beanName)) 65 | { 66 | variables.interceptorCache.name[arguments.beanName] = []; 67 | } 68 | 69 | arrayAppend(variables.interceptorCache.name[arguments.beanName], interceptDefinition); 70 | } 71 | 72 | 73 | return this; 74 | } 75 | 76 | 77 | /** Adds an interceptor definition to the definition cache. */ 78 | public any function interceptByType(string type, string interceptorName, string methods = "") 79 | { 80 | var interceptDefinition = 81 | { 82 | type = arguments.type, 83 | name = arguments.interceptorName, 84 | methods = arguments.methods 85 | }; 86 | 87 | arrayAppend(variables.interceptorCache.type, interceptDefinition); 88 | } 89 | 90 | 91 | 92 | 93 | // --------------- // 94 | // PRIVATE METHODS // 95 | // --------------- // 96 | 97 | /** Hook point to wrap bean with proxy. */ 98 | private any function construct(string dottedPath) 99 | { 100 | var bean = super.construct(arguments.dottedPath); 101 | var beanProxy = ""; 102 | 103 | // if it doesn't have a dotted path for us to create a new instance 104 | // or it has no interceptors, we have to leave it alone 105 | if (!hasInterceptors(arguments.dottedPath)) 106 | { 107 | return bean; 108 | } 109 | 110 | // Create and return a proxy wrapping the bean. 111 | beanProxy = new framework.beanProxy(bean, getInterceptorsForBean(arguments.dottedPath), variables.config); 112 | 113 | return beanProxy; 114 | } 115 | 116 | 117 | /** Gets the associated interceptor definitions for a specific bean. */ 118 | private array function getInterceptorsForBean(string dottedPath) 119 | { 120 | // build the interceptor array: 121 | var beanName = listLast(arguments.dottedPath, "."); 122 | var beanNames = getAliases(arguments.dottedPath); 123 | var beanTypes = ""; 124 | var interceptDefinition = ""; 125 | var interceptedBeanName = ""; 126 | var interceptors = []; 127 | 128 | 129 | arrayPrepend(beanNames, beanName); 130 | 131 | // Removing duplicate beanNames 132 | beanNames = listToArray(listRemoveDuplicates(arrayToList(beanNames),",",true) ); 133 | 134 | // Grab all name based interceptors that match. 135 | for (interceptedBeanName in beanNames) 136 | { 137 | // Match on name. 138 | if (structKeyExists(variables.interceptorCache.name, interceptedBeanName)) 139 | { 140 | for (interceptDefinition in variables.interceptorCache.name[interceptedBeanName]) 141 | { 142 | arrayAppend(interceptors, {bean = getBean(interceptDefinition.name), methods = interceptDefinition.methods}); 143 | } 144 | } 145 | } 146 | 147 | 148 | // Match on regex. Ensure we only attach each one time. 149 | if (arrayLen(variables.interceptorCache.regex)) 150 | { 151 | for (interceptDefinition in variables.interceptorCache.regex) 152 | { 153 | for (interceptedBeanName in beanNames) 154 | { 155 | if (reFindNoCase(interceptDefinition.regex, interceptedBeanName)) 156 | { 157 | arrayAppend(interceptors, {bean = getBean(interceptDefinition.name), methods = interceptDefinition.methods}); 158 | break; 159 | } 160 | } 161 | } 162 | } 163 | 164 | 165 | // Grab all type based interceptors that match. 166 | if (arrayLen(variables.interceptorCache.type)) 167 | { 168 | beanTypes = getBeanTypes(arguments.dottedPath); 169 | 170 | for (interceptDefinition in variables.interceptorCache.type) 171 | { 172 | if (listFindNoCase(beanTypes, interceptDefinition.type)) 173 | { 174 | arrayAppend(interceptors, {bean = getBean(interceptDefinition.name), methods = interceptDefinition.methods}); 175 | } 176 | } 177 | } 178 | 179 | 180 | return interceptors; 181 | } 182 | 183 | 184 | /** Determines if the bean has interceptor definitions associated with it. */ 185 | private boolean function hasInterceptors(string dottedPath) 186 | { 187 | var interceptedBeanName = ""; 188 | var interceptorDefinition = {}; 189 | var beanName = listLast(arguments.dottedPath, "."); 190 | var beanNames = getAliases(arguments.dottedPath); 191 | var beanTypes = ""; 192 | 193 | 194 | arrayPrepend(beanNames, beanName); 195 | 196 | 197 | for (interceptedBeanName in beanNames) 198 | { 199 | // Look for matches on name first. 200 | if (structKeyExists(variables.interceptorCache.name, interceptedBeanName)) 201 | { 202 | return true; 203 | } 204 | 205 | 206 | // Look for matches on regex. 207 | if (arrayLen(variables.interceptorCache.regex)) 208 | { 209 | for (interceptorDefinition in variables.interceptorCache.regex) 210 | { 211 | if (reFindNoCase(interceptorDefinition.regex, interceptedBeanName)) 212 | { 213 | return true; 214 | } 215 | } 216 | } 217 | 218 | 219 | // Look for matches by bean type. 220 | if (arrayLen(variables.interceptorCache.type)) 221 | { 222 | beanTypes = getBeanTypes(arguments.dottedPath); 223 | 224 | for (interceptorDefinition in variables.interceptorCache.type) 225 | { 226 | if (listFindNoCase(beanTypes, interceptorDefinition.type)) 227 | { 228 | return true; 229 | } 230 | } 231 | } 232 | } 233 | 234 | 235 | return false; 236 | } 237 | 238 | 239 | /** Finds all aliases for the given dottedPath. */ 240 | private array function getAliases(string dottedPath) 241 | { 242 | var aliases = []; 243 | var beanData = ""; 244 | var key = ""; 245 | 246 | for (key in variables.beanInfo) 247 | { 248 | // Same cfc dotted path, must be an alias. 249 | if ( 250 | structKeyExists(variables.beanInfo[key], "cfc") && 251 | variables.beanInfo[key].cfc == arguments.dottedPath) 252 | { 253 | arrayAppend(aliases, key); 254 | } 255 | } 256 | 257 | return aliases; 258 | } 259 | 260 | 261 | /** Returns a list of bean types (both name and dotted path) for a given bean. */ 262 | private string function getBeanTypes(string dottedPath) 263 | { 264 | var beanTypes = ""; 265 | var metadata = getComponentMetadata(arguments.dottedPath); 266 | 267 | while (!len(beanTypes) || structKeyExists(metadata, "extends")) 268 | { 269 | beanTypes = listAppend(beanTypes, listLast(metadata.name, ".")); 270 | beanTypes = listAppend(beanTypes, metadata.name); 271 | 272 | if (structKeyExists(metadata, "extends")) 273 | { 274 | metadata = metadata.extends; 275 | } 276 | } 277 | 278 | return beanTypes; 279 | } 280 | 281 | 282 | /** Loads an array of interceptor definitions into the interceptor definition cache. */ 283 | private void function loadInterceptors(array interceptors) 284 | { 285 | var interceptor = false; 286 | 287 | for (interceptor in interceptors) 288 | { 289 | if (structKeyExists(interceptor, "beanName")) 290 | { 291 | intercept(argumentCollection = interceptor); 292 | } 293 | else 294 | { 295 | interceptByType(argumentCollection = interceptor); 296 | } 297 | } 298 | } 299 | 300 | 301 | private void function setupFrameworkDefaults() 302 | { 303 | super.setupFrameworkDefaults(); 304 | variables.config.version = variables._fw1_version; 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /framework/beanProxy.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | variables._fw1_version = "4.3.0"; 3 | variables._aop1_version = variables._fw1_version; 4 | /* 5 | Copyright (c) 2013-2018, Mark Drew, Sean Corfield, Daniel Budde 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | 21 | 22 | variables.afterInterceptors = []; 23 | variables.aroundInterceptors = []; 24 | variables.beforeInterceptors = []; 25 | variables.errorInterceptors = []; 26 | variables.interceptedMethods = ""; 27 | variables.interceptID = createUUID(); 28 | variables.preName = "___"; 29 | variables.targetBean = ""; 30 | variables.targetBeanPath = ""; 31 | 32 | 33 | 34 | 35 | // -------------- // 36 | // PUBLIC METHODS // 37 | // -------------- // 38 | 39 | /** Constructor. */ 40 | public any function init(required any bean, required array interceptors, required struct config) 41 | { 42 | variables.targetBean = arguments.bean; 43 | 44 | populateInterceptorCache(arguments.interceptors); 45 | morphTargetBean(arguments.config); 46 | morphProxy(arguments.config); 47 | cleanVarScope(); 48 | 49 | return this; 50 | } 51 | 52 | 53 | /** Entry point for all publically accessible intercepted methods. */ 54 | public any function onMissingMethod(string missingMethodName, struct missingMethodArguments = {}) 55 | { 56 | // Prevent infinite loop and make sure the method is publically accessible. 57 | if ( !structKeyExists(variables.targetBean, arguments.missingMethodName) && 58 | !structKeyExists(variables.targetBean, variables.preName & arguments.missingMethodName) && 59 | !structKeyExists(variables.targetBean, "onMissingMethod") && 60 | !structKeyExists(variables.targetBean, variables.preName & "onMissingMethod") ) 61 | { 62 | var objectName = listLast(getMetadata(variables.targetBean).name, "."); 63 | var stdout = createObject("java","java.lang.System").out; 64 | stdout.println("Unable to locate method in (" & objectName & "). " & 65 | "The method (" & arguments.missingMethodName & ") could not be found. Please verify the method exists and is publically accessible."); 66 | } 67 | else 68 | { 69 | local.result = runStacks(arguments.missingMethodName, arguments.missingMethodArguments); 70 | 71 | if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; 72 | } 73 | } 74 | 75 | 76 | /** Runs all the interceptor stacks. */ 77 | public any function runStacks(string methodName, struct args = {}) 78 | { 79 | var objectName = ""; 80 | 81 | 82 | // Prevent infinite loop and make sure the method exists (public or private) 83 | if (!variables.targetBean.$methodExists(arguments.methodName) && !variables.targetBean.$methodExists(variables.preName & arguments.methodName)) 84 | { 85 | objectName = listLast(getMetadata(this).name, "."); 86 | throw(message="Unable to locate method in (" & objectName & ").", detail="The method (" & arguments.methodName & ") could not be found."); 87 | } 88 | 89 | 90 | try 91 | { 92 | // Intercepted method call 93 | if (variables.interceptedMethods == "*" || listFindNoCase(variables.interceptedMethods, arguments.methodName)) 94 | { 95 | runBeforeStack(arguments.methodName, arguments.args); 96 | local.result = runAroundStack(arguments.methodName, arguments.args); 97 | local.result = runAfterStack(arguments.methodName, arguments.args, !structKeyExists(local, "result") || isNull(local.result) ? javacast("null", 0) : local.result); 98 | } 99 | 100 | // Non-intercepted method call 101 | else 102 | { 103 | local.result = variables.targetBean.$call(arguments.methodName, arguments.args); 104 | } 105 | } 106 | catch (any exception) 107 | { 108 | if (arrayLen(variables.errorInterceptors)) 109 | { 110 | runOnErrorStack(arguments.methodName, arguments.args, exception); 111 | } 112 | else 113 | { 114 | rethrow; 115 | } 116 | } 117 | 118 | 119 | if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; 120 | } 121 | 122 | 123 | 124 | 125 | // --------------- // 126 | // PRIVATE METHODS // 127 | // --------------- // 128 | 129 | // --- Interceptor Augmentation Methods --- // 130 | 131 | /** Used to setup intercepted method lists on a per bean basis. */ 132 | public any function _addInterceptedMethods(required string interceptID, required string methods) 133 | { 134 | var interceptedMethods = ""; 135 | var methodName = ""; 136 | 137 | 138 | if (!structKeyExists(variables, "interceptedMethods")) 139 | { 140 | variables.interceptedMethods = {}; 141 | } 142 | 143 | if (!structKeyExists(variables.interceptedMethods, arguments.interceptID)) 144 | { 145 | variables.interceptedMethods[arguments.interceptID] = ""; 146 | } 147 | 148 | interceptedMethods = variables.interceptedMethods[arguments.interceptID]; 149 | 150 | 151 | if (interceptedMethods != "*") 152 | { 153 | if (arguments.methods == "" || arguments.methods == "*") 154 | { 155 | variables.interceptedMethods[arguments.interceptID] = "*"; 156 | } 157 | else 158 | { 159 | for (methodName in listToArray(arguments.methods)) 160 | { 161 | if (!listFindNoCase(variables.interceptedMethods[arguments.interceptID], methodName)) 162 | { 163 | interceptedMethods = listAppend(interceptedMethods, methodName); 164 | } 165 | } 166 | 167 | 168 | interceptedMethods = listSort(interceptedMethods, "textnocase"); 169 | variables.interceptedMethods[arguments.interceptID] = interceptedMethods; 170 | } 171 | } 172 | } 173 | 174 | 175 | /** Used to setup intercepted method lists on a per bean basis. */ 176 | public any function _getInterceptedMethods(string interceptID) 177 | { 178 | var methods = {}; 179 | 180 | if (structKeyExists(variables, "interceptedMethods")) 181 | { 182 | methods = variables.interceptedMethods; 183 | } 184 | 185 | if (!structKeyExists(arguments, "interceptID")) 186 | { 187 | return methods; 188 | } 189 | 190 | 191 | if (structKeyExists(methods, arguments.interceptID)) 192 | { 193 | return methods[arguments.interceptID]; 194 | } 195 | 196 | return ""; 197 | } 198 | 199 | 200 | /** Used to inject methods and data. */ 201 | public any function _inject(required string key, required any value, required string access="public") 202 | { 203 | if (arguments.access == "public") 204 | { 205 | this[arguments.key] = arguments.value; 206 | } 207 | 208 | variables[arguments.key] = arguments.value; 209 | } 210 | 211 | 212 | /** Determines if an around interceptor is the last in the call chain. */ 213 | public boolean function _isLast() 214 | { 215 | return isSimpleValue(variables.nextInterceptor); 216 | } 217 | 218 | 219 | /** Runs the 'Around' method, skips to the next interceptor in the chain if the 'Around' should not be run, or calls the actual method. */ 220 | public any function _preAround(required any targetBean, required string methodName, struct args = {}) 221 | { 222 | var interceptedMethods = getInterceptedMethods(arguments.targetBean.interceptID); 223 | 224 | // Match if method is to be intercepted by this interceptor. 225 | if (interceptedMethods == "*" || listFindNoCase(interceptedMethods, arguments.methodName)) 226 | { 227 | local.result = around(arguments.targetBean, arguments.methodName, arguments.args); 228 | } 229 | else 230 | { 231 | local.result = proceed(arguments.targetBean, arguments.methodName, arguments.args); 232 | } 233 | 234 | if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; 235 | } 236 | 237 | 238 | /** Runs the next around interceptor or processes the method if it is the final interceptor in the call chain. */ 239 | public any function _proceed(required any targetBean, required string methodName, struct args = {}) 240 | { 241 | if (isLast()) 242 | { 243 | local.result = arguments.targetBean.$call(arguments.methodName, arguments.args, true); 244 | } 245 | else 246 | { 247 | local.result = variables.nextInterceptor.preAround(arguments.targetBean, arguments.methodName, arguments.args); 248 | } 249 | 250 | 251 | if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; 252 | } 253 | 254 | 255 | /** Helper method for use inside of (after, around, before) to translate position based 'args' into name based. */ 256 | private any function _translateArgs(required any targetBean, required string methodName, required struct args, boolean replace = false) 257 | { 258 | var i = 1; 259 | var key = ""; 260 | var argumentInfo = arguments.targetBean.$getArgumentInfo(arguments.methodName); 261 | var resultArgs = {}; 262 | 263 | if (structIsEmpty(arguments.args) || !structKeyExists(arguments.args, "1")) 264 | { 265 | return arguments.args; 266 | } 267 | 268 | for (i = 1; i <= arrayLen(argumentInfo); i++) 269 | { 270 | resultArgs[argumentInfo[i].name] = arguments.args[i]; 271 | } 272 | 273 | if (arguments.replace) 274 | { 275 | structAppend(arguments.args, resultArgs, true); 276 | 277 | for (key in arguments.args) 278 | { 279 | if (isNumeric(key)) 280 | { 281 | structDelete(arguments.args, key); 282 | } 283 | } 284 | } 285 | 286 | return resultArgs; 287 | } 288 | 289 | 290 | 291 | 292 | // --- Target Bean and Proxy Bean Augmentation Methods --- // 293 | 294 | /** Runs the appropriate method on the target bean. */ 295 | private any function $call(required string methodName, struct args = {}, boolean original = false) 296 | { 297 | if (arguments.original) 298 | { 299 | local.result = invoke( variables, variables.preName & arguments.methodName, arguments.args ); 300 | } 301 | else 302 | { 303 | local.result = invoke( variables, arguments.methodName, arguments.args ); 304 | } 305 | 306 | if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; 307 | } 308 | 309 | 310 | /** Used to replace any 'private' methods on the target bean that are being intercepted. Creates an intercept point. */ 311 | public any function $callPrivateMethod() 312 | { 313 | local.methodName = getFunctionCalledName(); 314 | local.result = $callStacks(local.methodName, arguments); 315 | 316 | if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; 317 | } 318 | 319 | 320 | /** Used to replace any 'public' methods on the target bean that are being intercepted. Creates an intercept point. */ 321 | public any function $callPublicMethod() 322 | { 323 | local.methodName = getFunctionCalledName(); 324 | local.result = $callStacks(local.methodName, arguments); 325 | 326 | if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; 327 | } 328 | 329 | 330 | /** Method called by the intercept points to start the stack run if needed. */ 331 | private any function $callStacks(string methodName, struct args = {}) 332 | { 333 | local.result = variables.beanProxy.runStacks(arguments.methodName, arguments.args); 334 | 335 | if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; 336 | } 337 | 338 | 339 | /** Gets arguments information for a method. */ 340 | private array function $getArgumentInfo(required string methodName) 341 | { 342 | var method = ""; 343 | var methodMetadata = ""; 344 | 345 | 346 | if (structKeyExists(this, variables.preName & arguments.methodName)) 347 | { 348 | method = this[variables.preName & arguments.methodName]; 349 | } 350 | else if (structKeyExists(this, arguments.methodName)) 351 | { 352 | method = this[arguments.methodName]; 353 | } 354 | else if (structKeyExists(variables, variables.preName & arguments.methodName)) 355 | { 356 | method = variables[variables.preName & arguments.methodName]; 357 | } 358 | else if (structKeyExists(variables, arguments.methodName)) 359 | { 360 | method = variables[arguments.methodName]; 361 | } 362 | 363 | 364 | if (!isSimpleValue(method)) 365 | { 366 | methodMetadata = getMetadata(method); 367 | 368 | if (structKeyExists(methodMetadata, "parameters") && arrayLen(methodMetadata.parameters)) 369 | { 370 | return methodMetadata.parameters; 371 | } 372 | } 373 | 374 | 375 | return []; 376 | } 377 | 378 | 379 | /** Runs the appropriate method on the target bean. */ 380 | private boolean function $methodExists(required string methodName) 381 | { 382 | return structKeyExists(this, arguments.methodName) || structKeyExists(variables, arguments.methodName); 383 | } 384 | 385 | 386 | /** A pass through method placed on the proxy bean (used primarily for 'init', 'set..', and 'initMethod' on target bean). */ 387 | private any function $passThrough() 388 | { 389 | local.methodName = getFunctionCalledName(); 390 | local.result = invoke( variables.targetBean, local.methodName, arguments ); 391 | 392 | if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; 393 | } 394 | 395 | 396 | /** Used to inject methods on the target bean. */ 397 | public any function $replaceMethod(required string methodName, required any implementedMethod, required string access="public") 398 | { 399 | var method = ""; 400 | 401 | if (arguments.access == "public") 402 | { 403 | method = this[arguments.methodName]; 404 | 405 | if (isCustomFunction(method)) 406 | { 407 | this[variables.preName & arguments.methodName] = this[arguments.methodName]; 408 | this[arguments.methodName] = arguments.implementedMethod; 409 | } 410 | } 411 | 412 | method = variables[arguments.methodName]; 413 | 414 | if (isCustomFunction(method)) 415 | { 416 | variables[variables.preName & arguments.methodName] = variables[arguments.methodName]; 417 | variables[arguments.methodName] = arguments.implementedMethod; 418 | } 419 | } 420 | 421 | 422 | 423 | 424 | // --- Local Private Methods --- // 425 | 426 | /** Adds an interceptor definition and bean to the interceptor cache for the proxied bean. */ 427 | private void function addInterceptor(required any interceptor) 428 | { 429 | // If someone decides to have an interceptor handle multiple interceptor types, go for it. 430 | 431 | if (hasAfterMethod(arguments.interceptor)) 432 | { 433 | arrayAppend(variables.afterInterceptors, arguments.interceptor); 434 | } 435 | 436 | 437 | if (hasAroundMethod(arguments.interceptor)) 438 | { 439 | arrayAppend(variables.aroundInterceptors, arguments.interceptor); 440 | } 441 | 442 | 443 | if (hasBeforeMethod(arguments.interceptor)) 444 | { 445 | arrayAppend(variables.beforeInterceptors, arguments.interceptor); 446 | } 447 | 448 | 449 | if (hasOnErrorMethod(arguments.interceptor)) 450 | { 451 | arrayAppend(variables.errorInterceptors, arguments.interceptor); 452 | } 453 | 454 | 455 | if (!structKeyExists(arguments.interceptor.bean, "interceptorAugmented")) 456 | { 457 | augmentInterceptor(arguments.interceptor); 458 | } 459 | 460 | 461 | // Maintain the list of intercepted methods. '*' and blank means all. 462 | if (variables.interceptedMethods != "*") 463 | { 464 | if (!len(arguments.interceptor.methods) || arguments.interceptor.methods == "*") 465 | { 466 | variables.interceptedMethods = "*"; 467 | } 468 | else 469 | { 470 | variables.interceptedMethods = listSort(listAppend(variables.interceptedMethods, arguments.interceptor.methods), "textnocase"); 471 | } 472 | } 473 | 474 | 475 | // Update the interceptor itself. 476 | arguments.interceptor.bean.addInterceptedMethods(variables.interceptID, arguments.interceptor.methods); 477 | } 478 | 479 | 480 | /** Adds variables and methods needed by Around interceptors. */ 481 | private void function augmentAroundInterceptor(required any interceptor) 482 | { 483 | var prevInterceptor = ""; 484 | 485 | 486 | // Add additional methods for an around interceptor. 487 | arguments.interceptor.bean._inject("isLast", _isLast); 488 | arguments.interceptor.bean._inject("preAround", _preAround); 489 | arguments.interceptor.bean._inject("proceed", _proceed); 490 | arguments.interceptor.bean._inject("nextInterceptor", "", "private"); 491 | 492 | 493 | // Add a link in the call chain from the previous interceptor to the one just added. 494 | if (1 < arrayLen(variables.aroundInterceptors)) 495 | { 496 | prevInterceptor = variables.aroundInterceptors[arrayLen(variables.aroundInterceptors) - 1]; 497 | 498 | prevInterceptor.bean._inject = _inject; 499 | 500 | prevInterceptor.bean._inject("nextInterceptor", arguments.interceptor.bean, "private"); 501 | 502 | structDelete(prevInterceptor.bean, "_inject"); 503 | } 504 | } 505 | 506 | 507 | /** Adds variables and methods needed by all interceptors. */ 508 | private void function augmentInterceptor(required any interceptor) 509 | { 510 | var interceptorVarScope = ""; 511 | 512 | 513 | if (!structKeyExists(arguments.interceptor, "methods") || !len(arguments.interceptor.methods)) 514 | { 515 | arguments.interceptor.methods = "*"; 516 | } 517 | 518 | arguments.interceptor.bean._inject = _inject; 519 | 520 | arguments.interceptor.bean._inject("interceptorAugmented", true); 521 | arguments.interceptor.bean._inject("addInterceptedMethods", _addInterceptedMethods); 522 | arguments.interceptor.bean._inject("getInterceptedMethods", _getInterceptedMethods); 523 | arguments.interceptor.bean._inject("translateArgs", _translateArgs, "private"); 524 | 525 | if (hasAroundMethod(arguments.interceptor)) 526 | { 527 | augmentAroundInterceptor(arguments.interceptor); 528 | } 529 | 530 | structDelete(arguments.interceptor.bean, "_inject"); 531 | } 532 | 533 | 534 | /** Cleans up temporary methods from the variables scope. */ 535 | private void function cleanVarScope() 536 | { 537 | var key = ""; 538 | 539 | for (key in variables) 540 | { 541 | if (left(key, 1) == "_" || left(key, 1) == "$") 542 | { 543 | structDelete(variables, key); 544 | } 545 | } 546 | } 547 | 548 | 549 | /** Returns whether a method's access is public or private. */ 550 | private string function getMethodAccess(any method) 551 | { 552 | var access = "public"; 553 | var methodMetadata = getMetadata(method); 554 | 555 | if (structKeyExists(methodMetadata, "access") && methodMetadata.access == "private") 556 | { 557 | access = "private"; 558 | } 559 | 560 | return access; 561 | } 562 | 563 | 564 | /** Retrieves property and method info about the targetBean. */ 565 | private struct function getTargetBeanMetadata(any beanMetadata) 566 | { 567 | var beanInfo = {accessors = false, methods = {}, name = "", properties = {}}; 568 | var i = 0; 569 | var method = {}; 570 | var property = {}; 571 | var tmpBeanInfo = {}; 572 | 573 | 574 | if (isObject(arguments.beanMetadata)) 575 | { 576 | arguments.beanMetadata = getMetadata(arguments.beanMetadata); 577 | } 578 | 579 | 580 | if (structKeyExists(arguments.beanMetadata, "accessors")) 581 | { 582 | beanInfo.accessors = arguments.beanMetadata.accessors; 583 | } 584 | 585 | 586 | if (structKeyExists(arguments.beanMetadata, "name")) 587 | { 588 | beanInfo.name = arguments.beanMetadata.name; 589 | } 590 | 591 | 592 | // Gather method information. 593 | if (structKeyExists(arguments.beanMetadata, "functions")) 594 | { 595 | // ACF 9 did NOT like using a for-in loop here. 596 | for (i = 1; i <= arrayLen(arguments.beanMetadata.functions); i++) 597 | { 598 | method = arguments.beanMetadata.functions[i]; 599 | beanInfo.methods[method.name] = {}; 600 | 601 | if (structKeyExists(method, "access")) 602 | { 603 | beanInfo.methods[method.name]["access"] = method.access; 604 | } 605 | } 606 | } 607 | 608 | 609 | // Gather property information. 610 | if (structKeyExists(arguments.beanMetadata, "properties")) 611 | { 612 | // ACF 9 did NOT like using a for-in loop here. 613 | for (i = 1; i <= arrayLen(arguments.beanMetadata.properties); i++) 614 | { 615 | property = arguments.beanMetadata.properties[i]; 616 | beanInfo.properties[property.name] = {}; 617 | 618 | if (structKeyExists(property, "access")) 619 | { 620 | beanInfo.properties[property.name]["access"] = property.access; 621 | } 622 | } 623 | } 624 | 625 | 626 | // Handle 'extends' hierarchy info. 627 | if (structKeyExists(arguments.beanMetadata, "extends")) 628 | { 629 | tmpBeanInfo = getTargetBeanMetadata(arguments.beanMetadata.extends); 630 | structAppend(beanInfo.properties, tmpBeanInfo.properties); 631 | structAppend(beanInfo.methods, tmpBeanInfo.methods); 632 | } 633 | 634 | 635 | return beanInfo; 636 | } 637 | 638 | 639 | /** Gathers all the method information for the targetBean. */ 640 | private struct function getTargetBeanMethodInfo() 641 | { 642 | var beanInfo = getTargetBeanMetadata(variables.targetBean); 643 | var key = ""; 644 | var methodInfo = {}; 645 | 646 | 647 | variables.targetBeanPath = beanInfo.name; 648 | 649 | 650 | // Locate methods in 'this' scope of targetBean. 651 | for (key in variables.targetBean) 652 | { 653 | if (!structKeyExists(methodInfo, key) && isCustomFunction(variables.targetBean[key])) 654 | { 655 | methodInfo[key] = {access = "public", discoveredIn = "this", isPropertyAccessor = false}; 656 | } 657 | } 658 | 659 | 660 | // Locate any missing 'set' and 'get' methods only present in the metadata. 661 | if (beanInfo.accessors) 662 | { 663 | for (key in beanInfo.methods) 664 | { 665 | if (!structKeyExists(methodInfo, key)) 666 | { 667 | methodInfo[key] = {access = beanInfo.methods[key].access, discoveredIn = "metadata", isPropertyAccessor = false}; 668 | } 669 | } 670 | } 671 | 672 | 673 | // Determine if any of the 'set' or 'get' methods match a property. 674 | if (beanInfo.accessors) 675 | { 676 | for (key in beanInfo.properties) 677 | { 678 | if (structKeyExists(methodInfo, "set" & key)) 679 | { 680 | methodInfo["set" & key].isPropertyAccessor = true; 681 | } 682 | 683 | 684 | if (structKeyExists(methodInfo, "get" & key)) 685 | { 686 | methodInfo["get" & key].isPropertyAccessor = true; 687 | } 688 | } 689 | } 690 | 691 | 692 | return methodInfo; 693 | } 694 | 695 | 696 | /** Determines if an interceptor has an After method. */ 697 | private boolean function hasAfterMethod(required any interceptor) 698 | { 699 | return structKeyExists(arguments.interceptor.bean, "after"); 700 | } 701 | 702 | 703 | /** Determines if an interceptor has an Around method. */ 704 | private boolean function hasAroundMethod(required any interceptor) 705 | { 706 | return structKeyExists(arguments.interceptor.bean, "around"); 707 | } 708 | 709 | 710 | /** Determines if an interceptor has a Before method. */ 711 | private boolean function hasBeforeMethod(required any interceptor) 712 | { 713 | return structKeyExists(arguments.interceptor.bean, "before"); 714 | } 715 | 716 | 717 | /** Determines if an interceptor has an onError method. */ 718 | private boolean function hasOnErrorMethod(required any interceptor) 719 | { 720 | return structKeyExists(arguments.interceptor.bean, "onError"); 721 | } 722 | 723 | 724 | /** Determines if a 'methodName' is in a list of methods. A blank list of method matches will be an automatic match. */ 725 | private boolean function methodMatches(string methodName, string matchers) 726 | { 727 | // Match on: 1) No matches provided 2) Method name in matchers 728 | return !listLen(arguments.matchers) || arguments.matchers == "*" || listFindNoCase(arguments.matchers, arguments.methodName); 729 | } 730 | 731 | 732 | /** Alters the proxy bean so the factory still sees the set..(), init(), and initMethod() and so these methods get called on the target bean. */ 733 | private void function morphProxy(required struct config) 734 | { 735 | var key = ""; 736 | 737 | // Handle the 'set...' methods. 738 | for (key in variables.targetBean) 739 | { 740 | if (left(key, 3) == "set") 741 | { 742 | this[key] = $passThrough; 743 | } 744 | } 745 | 746 | 747 | // Checks to see if the 'initMethod' was defined in the config and handles if it exists on the target bean. 748 | if (structKeyExists(arguments.config, "initMethod") && len(arguments.config.initMethod) && structKeyExists(variables.targetBean, arguments.config.initMethod)) 749 | { 750 | this[arguments.config.initMethod] = $passThrough; 751 | } 752 | 753 | 754 | // Passes the init() if it exists, otherwise removes it. 755 | if (structKeyExists(variables.targetBean, "init")) 756 | { 757 | this["init"] = $passThrough; 758 | } 759 | else 760 | { 761 | structDelete(this, "init"); 762 | } 763 | } 764 | 765 | 766 | /** Alters the target bean by adding intercept points. */ 767 | private void function morphTargetBean(required struct config) 768 | { 769 | var access = ""; 770 | var beanMethodInfo = getTargetBeanMethodInfo(); 771 | var initMethod = ""; 772 | var key = ""; 773 | var method = ""; 774 | var methodInfo = ""; 775 | 776 | 777 | if (structKeyExists(arguments.config, "initMethod")) 778 | { 779 | initMethod = arguments.config.initMethod; 780 | } 781 | 782 | 783 | variables.targetBean.$inject = _inject; 784 | variables.targetBean.$replaceMethod = $replaceMethod; 785 | 786 | 787 | // Setup internal variables and methods on the target bean. 788 | variables.targetBean.$inject("beanProxy", this, "private"); 789 | variables.targetBean.$inject("preName", variables.preName, "private"); 790 | variables.targetBean.$inject("$callStacks", $callStacks, "private"); 791 | variables.targetBean.$inject("$call", $call); 792 | variables.targetBean.$inject("$methodExists", $methodExists); 793 | variables.targetBean.$inject("$getArgumentInfo", $getArgumentInfo); 794 | variables.targetBean.$inject("interceptID", variables.interceptID); 795 | 796 | 797 | for (key in beanMethodInfo) 798 | { 799 | methodInfo = beanMethodInfo[key]; 800 | 801 | 802 | // Only alter methods that should be intercepted. 'init()', accessors, and 'initMethod' are ignored unless specified in the methods list. 803 | if ( 804 | (variables.interceptedMethods == "*" && key != "init" && key != initMethod && !methodInfo.isPropertyAccessor) || 805 | listFindNoCase(variables.interceptedMethods, key) 806 | ) 807 | { 808 | // Handle methods listed in a scope. 809 | if (listFindNoCase("this,variables", methodInfo.discoveredIn)) 810 | { 811 | // Handle methods found in 'this' scope. 812 | if (methodInfo.access == "public") 813 | { 814 | variables.targetBean.$replaceMethod(key, $callPublicMethod); 815 | } 816 | 817 | // Handle methods in the variables scope. 818 | else 819 | { 820 | variables.targetBean.$replaceMethod(key, $callPublicMethod, "private"); 821 | } 822 | } 823 | 824 | 825 | // Handle methods found only in the metadata. 826 | else 827 | { 828 | try 829 | { 830 | if (methodInfo.access == "public") 831 | { 832 | variables.targetBean.$replaceMethod(key, $callPublicMethod); 833 | } 834 | else 835 | { 836 | variables.targetBean.$replaceMethod(key, $callPublicMethod, "private"); 837 | } 838 | } 839 | catch (any exception) 840 | { 841 | throw(message="Unable to locate the method (" & key & ") on target bean (" & variables.targetBeanPath & ")."); 842 | } 843 | } 844 | } 845 | } 846 | 847 | 848 | structDelete(variables.targetBean, "$inject"); 849 | structDelete(variables.targetBean, "$replaceMethod"); 850 | } 851 | 852 | 853 | /** Adds an array of interceptor definitions to the interceptor definition cache. */ 854 | private void function populateInterceptorCache(required array interceptors) 855 | { 856 | var interceptor = ""; 857 | 858 | for (interceptor in arguments.interceptors) 859 | { 860 | addInterceptor(interceptor); 861 | } 862 | } 863 | 864 | 865 | private any function runAfterStack(string methodName, struct args, any result) 866 | { 867 | if (structKeyExists(arguments, "result") && !isNull(arguments.result)) 868 | { 869 | local.result = arguments.result; 870 | } 871 | 872 | 873 | for (local.interceptor in variables.afterInterceptors) 874 | { 875 | if (methodMatches(methodName, local.interceptor.methods)) 876 | { 877 | local.tempResult = local.interceptor.bean.after(variables.targetBean, arguments.methodName, args, isNull(arguments.result) ? javacast("null", 0) : arguments.result); 878 | } 879 | 880 | if (structKeyExists(local, "tempResult")) 881 | { 882 | if (!isNull(local.tempResult)) 883 | { 884 | local.result = local.tempResult; 885 | } 886 | 887 | structDelete(local, "tempResult"); 888 | } 889 | } 890 | 891 | 892 | if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; 893 | } 894 | 895 | 896 | private any function runAroundStack(string methodName, struct args) 897 | { 898 | if (arrayLen(variables.aroundInterceptors)) 899 | { 900 | // Only need to call the first one in the chain to start the process. 901 | local.result = variables.aroundInterceptors[1].bean.preAround(variables.targetBean, arguments.methodName, arguments.args); 902 | } 903 | else 904 | { 905 | local.result = variables.targetBean.$call(arguments.methodName, arguments.args, true); 906 | } 907 | 908 | if (structKeyExists(local, "result") and !isNull(local.result)) return local.result; 909 | } 910 | 911 | 912 | private void function runBeforeStack(string methodName, struct args) 913 | { 914 | var inteceptor = ""; 915 | 916 | for (inteceptor in variables.beforeInterceptors) 917 | { 918 | if (structKeyExists(inteceptor.bean, "before")) 919 | { 920 | if (methodMatches(arguments.methodName, inteceptor.methods)) 921 | { 922 | inteceptor.bean.before(variables.targetBean, arguments.methodName, arguments.args); 923 | } 924 | } 925 | } 926 | } 927 | 928 | 929 | private void function runOnErrorStack(string methodName, struct args, any exception) 930 | { 931 | var interceptor = ""; 932 | 933 | for (interceptor in variables.errorInterceptors) 934 | { 935 | if (methodMatches(arguments.methodName, interceptor.methods)) 936 | { 937 | interceptor.bean.onError(variables.targetBean, arguments.methodName, arguments.args, arguments.exception); 938 | } 939 | } 940 | } 941 | } 942 | -------------------------------------------------------------------------------- /framework/facade.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | variables._fw1_version = "4.3.0"; 3 | /* 4 | Copyright (c) 2016-2018, Sean Corfield 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | function init() { 20 | try { 21 | return request._fw1.theFramework; 22 | } catch ( any e ) { 23 | throw( 24 | type = "FW1.FacadeException", message = "Unable to locate FW/1 for this request", 25 | detail = "It appears that you asked for the facade in a request that did not originate in FW/1?" 26 | ); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /framework/methodProxy.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | variables._fw1_version = "4.3.0"; 3 | /* 4 | Copyright (c) 2018, Sean Corfield 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | function init( fw, method ) { 20 | variables.fw = fw; 21 | variables.method = method; 22 | return this; 23 | } 24 | 25 | // implements Java 8 Function interface 26 | function apply( arg ) { 27 | return invoke( variables.fw, method, [ arg ] ); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /framework/nullObject.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | function onMissingMethod( string missingMethodName, struct missingMethodArguments ) { 3 | return this; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /home/controllers/main.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @file home/controllers/main.cfc 4 | * @author Denard Springle ( denard.springle@gmail.com ) 5 | * @description I am the controller for the home:main section 6 | * 7 | */ 8 | 9 | component accessors="true" { 10 | 11 | property beanFactory; 12 | property formatterService; 13 | property userService; 14 | property smsProviderService; 15 | //property mailService; 16 | 17 | /** 18 | * @displayname init 19 | * @description I am the constructor method for main 20 | * @return this 21 | */ 22 | public any function init( fw ) { 23 | variables.fw = fw; 24 | return this; 25 | } 26 | 27 | /** 28 | * @displayname default 29 | * @description I use the existing fw/1 example code 30 | */ 31 | public void function default( rc ) { 32 | // keep existing basic example fw/1 code 33 | var instant = variables.beanFactory.getBean( "instant" ); 34 | rc.today = variables.formatterService.longdate( instant.created() ); 35 | } 36 | 37 | /** 38 | * @displayname register 39 | * @description I present the registration view 40 | */ 41 | public void function register( rc ) { 42 | 43 | // check for the existence of the 'msg' url paramter 44 | if( structKeyExists( rc, 'msg') ) { 45 | // and generate a message to be displayed 46 | if( rc.msg eq 501 ) { 47 | rc.message = 'You must provide a valid value for all fields to register.'; 48 | } else if( rc.msg eq 502 ) { 49 | rc.message = 'Your password and confirmation password do not match. Please try again.'; 50 | } else if( rc.msg eq 503 ) { 51 | rc.message = 'A user account already exists for this email address. Please log in.'; 52 | } else if( rc.msg eq 504 ) { 53 | rc.message = 'Your password cannot be the same or similar to your email address. Please try another password.'; 54 | } else if( rc.msg eq 505 ) { 55 | rc.message = 'The password you have chosen is known to be a disclosed password available to hackers. If you are using this password with any other services then we strongly suggest you change your password with those services. Please try another password.'; 56 | } else { 57 | rc.message = ''; 58 | } 59 | // if it doesn't exist 60 | } else { 61 | // create it 62 | rc.msg = 0; 63 | // and set a null message string 64 | rc.message = ''; 65 | } 66 | 67 | // check if we're using two factor authentication 68 | if( application.use2FA ) { 69 | // we are, get the list of SMS providers to select from 70 | rc.qGetSmsProviders = smsProviderService.filter( isActive = true, orderby = 'provider', cache = true, cacheTime = CreateTimeSpan( 1, 0, 0, 0 ) ); 71 | } 72 | 73 | } 74 | 75 | /** 76 | * @displayname process 77 | * @description I process registration requests and display the process view 78 | */ 79 | public void function process( rc ) { 80 | 81 | var qGetUser = ''; 82 | var fieldList = 'username,password,confirm,firstName,lastName'; 83 | var ix = 0; 84 | 85 | // check if we're using two factor authentication 86 | if( application.use2FA ) { 87 | // we are, add to the list of required fields 88 | fieldList &= ',providerId,phone'; 89 | } 90 | 91 | // loop through fields 92 | for( ix = 1; ix <= listLen( fieldList ); ix++ ) { 93 | // ensure the username, password, confirm, firstName and lastName have been passed in 94 | if( !structKeyExists( rc, listGetAt( fieldList, ix ) ) OR !len( rc[ listGetAt( fieldList, ix ) ] ) ) { 95 | // missing something, redirect to registration page 96 | variables.fw.redirect( action = 'main.register', queryString = "msg=501" ); 97 | } 98 | } 99 | 100 | // check if the password and confirmation are the same 101 | if( compareNoCase( rc.password, rc.confirm ) NEQ 0 ) { 102 | // password mismatch, redirect to registration page 103 | variables.fw.redirect( action = 'main.register', queryString = "msg=502" ); 104 | } 105 | 106 | // get the user from the database by encrypted username passed in 107 | qGetUser = userService.filter( username = application.securityService.dataEnc( rc.username, 'repeatable' ) ); 108 | 109 | // check if there is a record for the passed username 110 | if( qGetUser.recordCount ) { 111 | // user exists, redirect to register page 112 | variables.fw.redirect( action = 'main.register', queryString = "msg=503" ); 113 | } 114 | 115 | // ensure the password is not found in the username 116 | if( findNoCase( rc.password, rc.username ) ) { 117 | // password found in username, redirect to register page 118 | variables.fw.redirect( action = 'main.register', queryString = "msg=504" ); 119 | } 120 | 121 | // if hacked password checking is enabled, and the 122 | // chosen password is found in the hacked password list 123 | if( application.rejectHackedPasswords and application.securityService.isPasswordHacked( rc.password ) ) { 124 | // password hacked, redirect to registration page 125 | variables.fw.redirect( action = 'main.register', queryString = "msg=505" ); 126 | } 127 | 128 | // get a user object to populate 129 | rc.userObj = userService.getUserById( 0 ); 130 | 131 | // populate the user object encrypting and hashing as needed 132 | rc.userObj.setProviderId( rc.providerId ); 133 | rc.userObj.setUsername( application.securityService.dataEnc( rc.username, 'repeatable' ) ); 134 | rc.userObj.setPassword( application.securityService.dataEnc( hash( rc.password, 'SHA-384' ), 'db' ) ); 135 | rc.userObj.setFirstName( application.securityService.dataEnc( encodeForHTML( rc.firstName ), 'db' ) ); 136 | rc.userObj.setLastName( application.securityService.dataEnc( encodeForHTML( rc.lastName ), 'db' ) ); 137 | rc.userObj.setPhone( application.securityService.dataEnc( reReplace( rc.phone, '[^0-9]', '', 'ALL' ), 'db' ) ); 138 | rc.userObj.setRole( 0 ); 139 | rc.userObj.setIsActive( 1 ); 140 | 141 | // save the user object 142 | userService.saveUser( rc.userObj ); 143 | 144 | } 145 | 146 | /** 147 | * @displayname reset 148 | * @description I present the reset view 149 | */ 150 | public void function reset( rc ) { 151 | 152 | // check for the existence of the 'msg' url paramter 153 | if( structKeyExists( rc, 'msg') ) { 154 | // and generate a message to be displayed 155 | if( rc.msg eq 403 ) { 156 | rc.message = 'A user account could not be located for this email address. Please register for an account.'; 157 | } else if( rc.msg eq 200 ) { 158 | rc.message = 'An email has been sent with your new password. Please check your email and login with the new password provided.'; 159 | } else { 160 | rc.message = ''; 161 | } 162 | // if it doesn't exist 163 | } else { 164 | // create it 165 | rc.msg = 0; 166 | // and set a null message string 167 | rc.message = ''; 168 | } 169 | } 170 | 171 | /** 172 | * @displayname resetpass 173 | * @description I reset the users password 174 | */ 175 | public void function resetpass( rc ) { 176 | 177 | // disabled until you write a mailService to handle emailing the user their new password 178 | abort; 179 | 180 | var qGetUser = userService.filter( username = application.securityService.dataEnc( rc.username, 'repeatable' ) ); 181 | var randomPass = application.securityService.getRandomPassword(); 182 | 183 | // check if there isn't a record for the passed username 184 | if( !qGetUser.recordCount ) { 185 | // user does not exist, redirect to reset page 186 | variables.fw.redirect( action = 'main.reset', queryString = "msg=403" ); 187 | } 188 | 189 | // get a user object to modify 190 | rc.userObj = userService.getUserById( qGetUser.userId ); 191 | 192 | rc.userObj.setPassword( application.securityService.dataEnc( hash( randomPass, 'SHA-384' ), 'db' ) ); 193 | 194 | // save the user object 195 | userService.saveUser( rc.userObj ); 196 | 197 | // email the customer their new password 198 | //mailService.sendPasswordResetEmail( rc.userObj, randomPass ); 199 | 200 | // password reset, redirect to reset page 201 | variables.fw.redirect( action = 'main.reset', queryString = "msg=200" ); 202 | } 203 | 204 | public void function error( rc ) { 205 | 206 | // check if we're in a production environment 207 | if( findNoCase( 'prod', application.securityService.getEnvironment() ) ) { 208 | 209 | // we are, get the http request headers 210 | rc.headers = getHTTPRequestData().headers; 211 | 212 | // check if this server sits behind a load balancer, proxy or firewall 213 | if( structKeyExists( rc.headers, 'x-forwarded-for' ) ) { 214 | // it does, get the ip address this request has been forwarded for 215 | rc.ipAddress = rc.headers[ 'x-forwarded-for' ]; 216 | // otherwise 217 | } else { 218 | // it doesn't, get the ip address of the remote client 219 | rc.ipAddress = CGI.REMOTE_ADDR; 220 | } 221 | 222 | // check if the user is requesting a view not found (when onMissingView() isn't being used) 223 | if( request.exception.cause.type eq 'FW1.viewNotFound' ) { 224 | // and redirect to the root of the site 225 | location( '/', 'false', '302' ); 226 | } 227 | 228 | // check for sql injection errors 229 | if( findNoCase( 'SQLInjection', request.exception.cause.type ) ) { 230 | // sql injection attempt detected, add this ip address to the blocked ip list 231 | application.securityService.addBlockedIP( ipAddress = rc.ipAddress, reason = request.exception.cause.message ); 232 | 233 | // switch on the block mode 234 | switch( application.blockMode ) { 235 | // redirect 236 | case 'redirect': 237 | // redirect the browser to an html page for notification 238 | location( '/ipBlocked.html', 'false', '302' ); 239 | break; 240 | 241 | // abort 242 | default: 243 | abort; 244 | break; 245 | } 246 | 247 | } 248 | 249 | // check for parameter tampering 250 | if( 251 | ( findNoCase( 'key', request.exception.cause.message ) and ( findNoCase( "doesn't exist", request.exception.cause.message ) or findNoCase( "does not exist" , request.exception.cause.message ) ) ) 252 | or findNoCase( 'invalid hexadecimal string', request.exception.cause.message ) 253 | or findNoCase( 'given final block not properly padded', request.exception.cause.message ) 254 | ) { 255 | 256 | // parameter tampering likely, get this ip's record from the watched ip list 257 | watchedIp = application.securityService.getWatchedIp( rc.ipAddress ); 258 | 259 | // check if the ip is currently being watched 260 | if( watchedIp.isWatched ) { 261 | // it is, check if the total number of times the ip has been flagged 262 | // exceeds the total set in the Application.cfc 263 | if( watchedIp.totalCount gt application.blockIpThreshold ) { 264 | // it has, add this ip address to the blocked ip list 265 | application.securityService.addBlockedIP( ipAddress = rc.ipAddress, reason = 'parameter tampering more than #application.blockIpThreshold# times' ); 266 | // and remove it from the watched ip list 267 | application.securityService.removeWatchedIP( rc.ipAddress ); 268 | // otherwise 269 | } else { 270 | // the ip has not exceeded the total flags required to be blocked 271 | // increase the total times this ip has been flagged 272 | application.securityService.increaseWatchedIpCount( ipAddress = rc.ipAddress, reason = request.exception.cause.message ); 273 | } 274 | // otherwise 275 | } else { 276 | // this ip is not currently being watched, so add it to the watch list 277 | application.securityService.addWatchedIP( ipAddress = rc.ipAddress, reason = request.exception.cause.message ); 278 | } 279 | // redirect the browser to an html page for notification 280 | location( '/ipFlagged.html', 'false', '302' ); 281 | } 282 | 283 | } // end checking if we're in a production environment 284 | 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /home/layouts/default.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Secure Auth 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | #body# 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /home/views/main/default.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |

Secure Auth Example

6 |
7 |
8 |
 
9 |
10 |
11 |
12 |
13 |
14 | Register 15 |
16 |
17 | Login 18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
 
26 |
27 |
28 |

This page was last generated on #encodeForHtml( rc.today )#.

29 |
30 |
31 | 32 |
-------------------------------------------------------------------------------- /home/views/main/error.cfm: -------------------------------------------------------------------------------- 1 |  2 | 3 |
4 |

ERROR!

5 |
6 |

An error occurred!

7 | 8 | 9 | 10 | Action: #replace( request.failedAction, "<", "<", "all" )#
11 | 12 | Action: unknown
13 |
14 | Error: #request.exception.cause.message#
15 | Type: #request.exception.cause.type#
16 | Details: #request.exception.cause.detail#
17 |
18 |
19 |
-------------------------------------------------------------------------------- /home/views/main/process.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |

Secure Auth Example

6 |
7 |
8 |
 
9 |
10 |
11 |
12 |
13 | 14 | Your account has been successfully created. Please Login Now! 15 |
16 |
17 |
18 |
19 |
20 |
21 | 22 |
-------------------------------------------------------------------------------- /home/views/main/register.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Secure Auth Example

5 |
6 |
7 |
 
8 |
9 |
10 |
11 | 12 | 13 |
14 | 15 | #encodeForHtml( rc.message )# 16 |
17 | 18 |
19 | 20 | #encodeForHtml( rc.message )# 21 |
22 |
23 |
24 |
25 |
26 |

27 | Register for a Secure Auth account 28 |

29 |
30 |
31 |
32 |
33 | 36 | 37 |
38 |
39 | 42 | 43 |
44 |
45 | 48 | 49 |
50 |
51 |   52 |
53 |
54 | 57 | 58 |
59 |
60 | 63 | 64 |
65 | 66 |
67 |   68 |
69 |
70 | 73 | 74 |
75 |
76 | 79 | 85 |
86 | 87 | 88 | 89 |
90 | 93 |    94 | Cancel 95 |
96 |
97 | 100 |
101 |
102 |
103 |
104 |
105 | 106 |
-------------------------------------------------------------------------------- /home/views/main/reset.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Secure Auth Example

5 |
6 |
7 |
 
8 |
9 |
10 |
11 | 12 | 13 |
14 | 15 | #encodeForHtml( rc.message )# 16 |
17 | 18 |
19 | 20 | #encodeForHtml( rc.message )# 21 |
22 |
23 |
24 |
25 |
26 |

27 | Reset your password 28 |

29 |
30 |
31 |

If you have forgotten your password, use this form to have a new system generated password emailed to you.

32 |
33 |
34 | 37 | 38 |
39 | 42 |    43 | Cancel 44 |
45 |
46 | 49 |
50 |
51 |
52 |
53 |
54 | 55 |
-------------------------------------------------------------------------------- /index.cfm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ipBlocked.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IP Blocked 6 | 7 | 8 |

IP Blocked!

9 |

Your IP address has been blocked for suspicious activity.

10 |

11 | Continued attempts to access this system will result in
12 | your IP address being reported to your ISP's abuse department. 13 |

14 |

15 | If you feel you have been blocked in error, please contact our
16 | abuse department to determine why your ip address has been blocked. 17 | 18 | -------------------------------------------------------------------------------- /ipFlagged.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IP Flagged 6 | 7 | 8 |

IP Flagged!

9 |

Your IP address has been flagged for suspicious activity.

10 |

11 | Continued abuse of our system will get your IP banned from this site
12 | and will result in your IP address being reported to your ISP's abuse department. 13 |

14 |

15 | If you feel you have received this warning in error, please contact our
16 | abuse department to determine why your ip address has been flagged. 17 | 18 | -------------------------------------------------------------------------------- /keyrings/move_keyrings_folder_outside_webroot.txt: -------------------------------------------------------------------------------- 1 | You *MUST* move the keyrings folder outside of the webroot 2 | so it is not accessible to the internet 3 | 4 | ex: /opt/secure/keyrings/ 5 | 6 | this path should be accessible *only* to the user the CFML 7 | application server is running under and to 8 | root/Administrator users -------------------------------------------------------------------------------- /libs/scrypt-1.4.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddspringle/framework-one-secure-auth/195c1f1fe673e52091cce289cd2aedb3e26c66db/libs/scrypt-1.4.0.jar -------------------------------------------------------------------------------- /model/beans/BaseBean.cfc: -------------------------------------------------------------------------------- 1 | component output="false" displayname="" { 2 | 3 | /** 4 | * @displayname getUidHash 5 | * @description I return a hashed string version of the primary key of the bean that extends this bean 6 | * @param format {String} I am the format of the hash - one of 'url' (default) or 'form' 7 | * @return string 8 | */ 9 | public string function getUidHash( string format = 'url' ) { 10 | 11 | // get the cached uid hash for this bean and format 12 | var hashUid = cacheGet( 'hash_uid_' & arguments.format & '_' & hash( getCurrentTemplatePath() ) ); 13 | 14 | // check if the cached version exists 15 | if( isNull( hashUid ) ) { 16 | 17 | // it doesn't, check which format the hash should be in 18 | if( arguments.format eq 'form' ) { 19 | // it should be 'form' format, generate the form based uid hash 20 | // NOTE: Adjust the iterations of this hash to make it unique to your application 21 | // e.g. '84' iterations instead of '30' 22 | hashUid = 'f' & application.securityService.uberHash( getPrimaryKey(), 'SHA-512', 30 ); 23 | // otherwise 24 | } else { 25 | // it should be 'url' format, generate the url based uid hash 26 | // NOTE: Adjust the iterations of this hash to make it unique to your application 27 | // e.g. '49' iterations instead of '15' 28 | hashUid = 'v' & application.securityService.uberHash( getPrimaryKey(), 'SHA-384', 15 ); 29 | } 30 | 31 | // and put this format hash uid into the cache for 30 mins, 15 mins idle 32 | // NOTE: adjust the cache times for longer durations in production 33 | cachePut( 'hash_uid_' & arguments.format & '_' & hash( getCurrentTemplatePath() ), hashUid, createTimeSpan( 0, 0, 30, 0 ), createTimeSpan( 0, 0, 15, 0 ) ); 34 | } 35 | 36 | return hashUid; 37 | } 38 | 39 | /** 40 | * @displayname getTokenKeyHash 41 | * @description I return a standard hashed value for the CSRF token key variable 42 | * @return string 43 | */ 44 | public string function getTokenKeyHash() { 45 | // get the cached token key hash 46 | var tokenKeyHash = cacheGet( 'token_key_hash' ); 47 | 48 | // check if the cached version exists 49 | if( isNull( tokenKeyHash ) ) { 50 | // it doesn't, generate the token key hash 51 | // NOTE: Adjust the value and iterations of this hash to make it unique to your application 52 | // e.g. 'powerTokenKey' instead of 'tokenKey' and '28' iterations instead of '45' 53 | tokenKeyHash = 'f' & application.securityService.uberHash( 'tokenKey', 'SHA-512', 45 ); 54 | 55 | // and put it into the cache for 30 mins, 15 mins idle 56 | // NOTE: adjust the cache times for longer durations in production 57 | cachePut( 'token_key_hash', tokenKeyHash, createTimeSpan( 0, 0, 30, 0 ), createTimeSpan( 0, 0, 15, 0 ) ); 58 | } 59 | 60 | return tokenKeyHash; 61 | } 62 | 63 | /** 64 | * @displayname getTokenHash 65 | * @description I return a standard hashed value for the CSRF token variable 66 | * @return string 67 | */ 68 | public string function getTokenHash() { 69 | // get the cached token hash 70 | var tokenHash = cacheGet( 'token_hash' ); 71 | 72 | // check if the cached version exists 73 | if( isNull( tokenHash ) ) { 74 | // it doesn't, generate the token hash 75 | // NOTE: Adjust the value and iterations of this hash to make it unique to your application 76 | // e.g. 'powerToken' instead of 'token' and '72' iterations instead of '69' 77 | tokenHash = 'f' & application.securityService.uberHash( 'token', 'SHA-512', 69 ); 78 | 79 | // and put it into the cache for 30 mins, 15 mins idle 80 | // NOTE: adjust the cache times for longer durations in production 81 | cachePut( 'token_hash', tokenHash, createTimeSpan( 0, 0, 30, 0 ), createTimeSpan( 0, 0, 15, 0 ) ); 82 | } 83 | 84 | return tokenHash; 85 | } 86 | 87 | /** 88 | * @displayname getEncUid 89 | * @description I return an encrypted string version of the primary key of the bean that extends this bean 90 | * @param format {String} I am the format of the encryption - one of 'url' (default) or 'form' 91 | * @return string 92 | */ 93 | public string function getEncUid( string format = 'url' ) { 94 | 95 | var cf11fix = 'get' & getPrimaryKey(); 96 | 97 | // check if we're using form encrytion format 98 | if( arguments.format eq 'form' ) { 99 | // we are, return the form encrypted primary key 100 | return application.securityService.dataEnc( this[ cf11fix ](), 'form' ); 101 | // otherwise 102 | } else { 103 | // we're using url encryption, return the url encrypted primary key 104 | return application.securityService.dataEnc( this[ cf11fix ](), 'url' ); 105 | } 106 | } 107 | 108 | /** 109 | * @displayname getDecUid 110 | * @description I return a decrypted string version of the passed in guid 111 | * @param encGuid {String} I am the encrypted guid to decrypt 112 | * @param format {String} I am the format of the encryption - one of 'url' (default) or 'form' 113 | * @return string 114 | */ 115 | public string function getDecUid( required string encGuid, string format = 'url' ) { 116 | 117 | // check if we're using form decrytion format 118 | if( arguments.format eq 'form' ) { 119 | // we are, return the decrypted guid using form decryption 120 | return application.securityService.dataDec( arguments.encGuid, 'form' ); 121 | // otherwise 122 | } else { 123 | // we're using url decryption, return the decrypted guid using url decryption 124 | return application.securityService.dataDec( arguments.encGuid, 'url' ); 125 | } 126 | } 127 | 128 | /** 129 | * @displayname getPrimaryKey 130 | * @description I return the primary key of the bean that extends this bean 131 | * @return string 132 | */ 133 | public string function getPrimaryKey() { 134 | 135 | var primaryKey = ''; 136 | var metaProperty = ''; 137 | 138 | // get component metadata from the cache 139 | var metaData = cacheGet( 'bean_meta_' & hash( getCurrentTemplatePath() ) ); 140 | // check that the component metadata exists in the cache 141 | if( isNull( metaData ) ) { 142 | // it doesn't, get the components meta data 143 | metaData = getMetaData( this ); 144 | // and store it in the cache for 30 mins / 15 mins idle 145 | // NOTE: adjust the cache times for longer durations in production 146 | cachePut( 'bean_meta_' & hash( getCurrentTemplatePath() ), metaData, createTimeSpan( 0, 0, 30, 0 ), createTimeSpan( 0, 0, 15, 0 ) ); 147 | } 148 | 149 | // get the primary key from the cache using the metadata information 150 | primaryKey = cacheGet( metaData.name & '_primary_key' ); 151 | 152 | // check that we have a cached primary key 153 | if( isNull( primaryKey ) ) { 154 | // we don't, loop through the components properties 155 | for( metaProperty in metaData.properties ) { 156 | // check if the 'primary' attribute is set on this property and is true 157 | if( structKeyExists( metaProperty, 'primary' ) and metaProperty.primary ) { 158 | // it does, set the primary key value to this properties name 159 | primaryKey = metaProperty.name; 160 | // and store it in the cache for 30 mins / 15 mins idle 161 | // NOTE: adjust the cache times for longer durations in production 162 | cachePut( metaData.name & '_primary_key', primaryKey, createTimeSpan( 0, 0, 30, 0 ), createTimeSpan( 0, 0, 15, 0 ) ); 163 | // and break out of the loop since we have our primary key now 164 | break; 165 | } 166 | } 167 | } 168 | 169 | // return the primary key name 170 | return primaryKey; 171 | } 172 | 173 | /** 174 | * @displayname getJson 175 | * @description I return a serialized json string of the current beans memento 176 | * @return string 177 | */ 178 | public string function getJson() { 179 | return serializeJSON( getMemento() ); 180 | } 181 | 182 | } -------------------------------------------------------------------------------- /model/beans/Session.cfc: -------------------------------------------------------------------------------- 1 | /** * * @file model\beans\Session.cfc * @author Denard Springle ( denard.springle@gmail.com ) * @description I am a session bean used for storing user sessions * */ component displayname="Session" accessors="true" { property name="sessionId" type="string" default=""; property name="userId" type="numeric" default="0"; property name="firstName" type="string" default=""; property name="lastName" type="string" default=""; property name="hmacCode" type="string" default=""; property mfaCode; property isAuthenticated; property name="role" type="numeric" default="0"; property name="lastActionAt" type="any" default=""; public struct function getMemento() { local._Session = {}; local._Session[ 'sessionId' ] = getSessionId(); local._Session[ 'userId' ] = getUserId(); local._Session[ 'firstName' ] = getFirstName(); local._Session[ 'lastName' ] = getLastName(); local._Session[ 'hmacCode' ] = getHmacCode(); local._Session[ 'mfaCode' ] = getMfaCode(); local._Session[ 'isAuthenticated' ] = getIsAuthenticated(); local._Session[ 'role' ] = getRole(); local._Session[ 'lastActionAt' ] = getLastActionAt(); return local._Session; } } -------------------------------------------------------------------------------- /model/beans/SmsProvider.cfc: -------------------------------------------------------------------------------- 1 | component displayname="SmsProvider" accessors="true" { property providerId; property provider; property email; property isActive; /** * @displayname init * @description I am the constructor method for SmsProvider * @returntype this */ public function init( numeric providerId = 0, string provider = '', string email = '', boolean isActive = false ) { setProviderId( arguments.providerId ); setProvider( arguments.provider ); setEmail( arguments.email ); setIsActive( arguments.isActive ); return this; } // SETTERS // public function setProviderId( numeric providerId = 0 ) { variables.providerId = arguments.providerId; } public function setProvider( string provider = '' ) { variables.provider = arguments.provider; } public function setEmail( string email = '' ) { variables.email = arguments.email; } public function setIsActive( boolean isActive = false ) { variables.isActive = arguments.isActive; } // GETTERS // public function getProviderId() { return variables.providerId; } public function getProvider() { return variables.provider; } public function getEmail() { return variables.email; } public function getIsActive() { return variables.isActive; } // UTILITY // public function getMemento() { return variables; } } -------------------------------------------------------------------------------- /model/beans/User.cfc: -------------------------------------------------------------------------------- 1 | /** * * @file User.cfc * @author Denard Springle ( denard.springle@gmail.com ) * @description I am a user bean used for storing user data * */ component displayname="User" extends="BaseBean" accessors="true" { property name="userId" type="numeric" default="0" primary=true; property name="providerId" type="numeric" default="0"; property name="username" type="string" default=""; property name="password" type="string" default=""; property name="firstName" type="string" default=""; property name="lastName" type="string" default=""; property name="phone" type="string" default=""; property name="role" type="numeric" default="0"; property name="isActive" type="boolean" default="false"; public struct function getMemento() { local._User = {}; local._User[ 'userId' ] = getUserId(); local._User[ 'providerId' ] = getProviderId(); local._User[ 'username' ] = getUsername(); local._User[ 'password' ] = getPassword(); local._User[ 'firstName' ] = getFirstName(); local._User[ 'lastName' ] = getLastName(); local._User[ 'phone' ] = getPhone(); local._User[ 'role' ] = getRole(); local._User[ 'isActive' ] = getIsActive(); return local._User; } } -------------------------------------------------------------------------------- /model/beans/instant.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function init() { 4 | variables.when = now(); 5 | } 6 | 7 | function created() { 8 | return variables.when; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /model/services/MailService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @file model/services/MailService.cfc 4 | * @author Denard Springle ( denard.springle@gmail.com ) 5 | * @description I am the email service used to send the mfa code to the user 6 | * 7 | */ 8 | 9 | component displayname="mailService" accessors="true" { 10 | 11 | public function init(){ 12 | 13 | variables.mailService = new mail(); 14 | 15 | return this; 16 | } 17 | 18 | public void function sendMfaCode( required numeric phone, required string providerEmail, required string mfaCode ) { 19 | 20 | // clear the mail service of any previously used data 21 | variables.mailService.clear(); 22 | 23 | variables.mailService.setFrom( 'twofactorauth@vsgcom.net' ); 24 | variables.mailService.setTo( arguments.phone & arguments.providerEmail ); 25 | variables.mailService.setSubject( 'Auth Code' ); 26 | variables.mailService.setType( 'text' ); 27 | variables.mailService.setBody( arguments.mfaCode ); 28 | 29 | variables.mailService.send(); 30 | 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /model/services/SmsProviderService.cfc: -------------------------------------------------------------------------------- 1 | component displayname="SmsProviderService" accessors="true" { // PUBLIC METHODS // // CREATE // /** * @displayname createNewSmsProvider * @description I insert a new smsProvider record into the smsProviders table in the database * @param smsProvider {Any} I am the SmsProvider bean * @returnType numeric */ public numeric function createNewSmsProvider( required any smsProvider ) { var qPutSmsProvider = ''; var queryService = new query(); var sql = ''; try { sql = 'INSERT INTO smsProviders ( provider, email, isActive ) VALUES ( :provider, :email, :isActive )'; queryService.setSQL( sql ); queryService.addParam( name='provider', value='#arguments.smsProvider.getProvider()#', cfsqltype='cf_sql_varchar' ); queryService.addParam( name='email', value='#arguments.smsProvider.getEmail()#', cfsqltype='cf_sql_varchar' ); queryService.addParam( name='isActive', value='#arguments.smsProvider.getIsActive()#', cfsqltype='cf_sql_bit' ); qPutSmsProvider = queryService.execute(); // catch any errors // } catch (any e) { writeDump( e ); abort; } return qPutSmsProvider.getPrefix().IDENTITYCOL; } // RETRIEVE - BY ID // /** * @displayname getSmsProviderByID * @description I return a SmsProvider bean populated with the details of a specific smsProvider record * @param id {Numeric} I am the numeric auto-increment id of the smsProvider to search for * @returnType any */ public any function getSmsProviderByID( required numeric id ) { var qGetSmsProvider = ''; var queryService = new query(); var smsProviderObject = createObject( 'component', 'model.beans.SmsProvider' ); var sql = ''; try { sql = 'SELECT providerId, provider, email, isActive FROM smsProviders WHERE providerId = :id'; queryService.setSQL( sql ); queryService.addParam( name = 'id', value = '#arguments.id#', cfsqltype = 'cf_sql_integer' ); qGetSmsProvider = queryService.execute().getResult(); // catch any errors // } catch (any e) { writeDump( e ); abort; } if( qGetSmsProvider.RecordCount ) { return smsProviderObject.init( providerId = qGetSmsProvider.providerId, provider = qGetSmsProvider.provider, email = qGetSmsProvider.email, isActive = qGetSmsProvider.isActive ); } else { return smsProviderObject.init(); } } // UPDATE // /** * @displayname updateSmsProvider * @description I update this smsProvider record in the smsProviders table of the database * @param smsProvider {Any} I am the SmsProvider bean * @returnType numeric */ public numeric function updateSmsProvider( required any smsProvider ) { var qUpdSmsProvider = ''; var queryService = new query(); var sql = ''; try { sql = 'UPDATE smsProviders SET provider = :provider, email = :email, isActive = :isActive WHERE providerId = :providerId'; queryService.setSQL( sql ); queryService.addParam( name = 'providerId', value = '#arguments.smsProvider.getProviderId()#', cfsqltype = 'cf_sql_int' ); queryService.addParam( name = 'provider', value = '#arguments.smsProvider.getProvider()#', cfsqltype = 'cf_sql_varchar' ); queryService.addParam( name = 'email', value = '#arguments.smsProvider.getEmail()#', cfsqltype = 'cf_sql_varchar' ); queryService.addParam( name = 'isActive', value = '#arguments.smsProvider.getIsActive()#', cfsqltype = 'cf_sql_bit' ); qUpdSmsProvider = queryService.execute().getResult(); // catch any errors // } catch (any e) { writeDump( e ); abort; } return arguments.smsProvider.getProviderId(); } // DELETE // /** * @displayname deleteSmsProviderByID * @description I delete a smsProvider record from the smsProviders table in the database * @param id {Numeric} I am the numeric auto-increment id of the smsProvider to delete * @returnType boolean */ public boolean function deleteSmsProviderByID( required numeric id ) { var qDelSmsProvider = ''; var queryService = new query(); var sql = 'DELETE FROM smsProviders WHERE providerId = :id'; try { queryService.setSQL( sql ); queryService.addParam( name = 'id', value = '#arguments.id#', cfsqltype = 'cf_sql_integer' ); qDelSmsProvider = queryService.execute().getResult(); // catch any errors // } catch (any e) { writeDump( e ); abort; } return true; } // UTILITY METHODS // // SAVE // /** * @displayname saveSmsProviderByID * @description I save a smsProvider record in the smsProviders table in the database * @param smsProvider {Any} I am the SmsProvider bean * @returnType numeric */ public numeric function saveSmsProvider( required any smsProvider ) { if( exists( arguments.smsProvider ) ) { return updateSmsProvider( arguments.smsProvider ); } else { return createNewSmsProvider( arguments.smsProvider ); } } // EXISTS // /** * @displayname exists * @description I check if a smsProvider record exists in the smsProviders table in the database * @param smsProvider {Any} I am the SmsProvider bean * @returnType boolean */ public boolean function exists( required any smsProvider ) { var qGetSmsProvider = ''; var queryService = new query(); var sql = 'SELECT providerId FROM smsProviders WHERE providerId = :providerId'; queryService.setSQL( sql ); queryService.addParam( name = 'providerId', value = '#arguments.smsProvider.getProviderId()#', cfsqltype = 'cf_sql_integer' ); qGetSmsProvider = queryService.execute().getResult(); if( qGetSmsProvider.recordCount ) { return true; } else { return false; } } // FILTER // /** * @displayname filter * @description I run a filtered query of all records within the smsProviders table in the database * @param returnColumns {String} I am the columns in the smsProviders table that should be returned in this query (default: all columns) * @param provider {String} I am the value for provider in the smsProviders table that should be returned in this query * @param email {String} I am the value for email in the smsProviders table that should be returned in this query * @param isActive {Boolean} I am the value for isActive in the smsProviders table that should be returned in this query * @param orderBy {String} I am the order to return records in the smsProviders table returned in this query * @param cache {Boolean} I am a flag (true/false) to determine if this query should be cached (default: false) * @param cacheTime {Any} I am the timespan the query should be cached for (default: 1 hour) * @returnType query */ public query function filter( string returnColumns = 'providerId, provider, email, isActive', string provider, string email, boolean isActive, string orderBy, boolean cache = false, any cacheTime = createTimeSpan(0,1,0,0) ) { var thisFilter = structNew(); if( isDefined( 'arguments.provider' ) AND len( arguments.provider ) ) { thisFilter.provider = arguments.provider; } if( isDefined( 'arguments.email' ) AND len( arguments.email ) ) { thisFilter.email = arguments.email; } if( isDefined( 'arguments.isActive' ) AND len( arguments.isActive ) ) { thisFilter.isActive = arguments.isActive; } if( isDefined( 'arguments.orderBy' ) AND len( arguments.orderBy ) ) { thisFilter.order_by = arguments.orderBy; } if( isDefined( 'arguments.cache' ) AND len( arguments.cache ) ) { thisFilter.cache = arguments.cache; } thisFilter.returnColumns = arguments.returnColumns; if( !structIsEmpty( thisFilter ) AND structKeyExists( thisFilter, 'cache' ) AND thisFilter.cache ) { return cacheFilteredSmsProviderRecords( thisFilter, arguments.cacheTime ); } else { return filterSmsProviderRecords( thisFilter ); } } // PRIVATE METHODS // // QUERY - CACHE FILTERED SMSPROVIDER RECORDS // /** * @displayname cacheFilteredSmsProviderRecords * @description I run a query that will cache and return all smsProvider records. If a filter has been applied, I will refine results based on the filter * @param filter {Struct} I am the filter struct to apply to this query * @param cacheTime {Time} I am the time to cache this query (use createTimeSpan) * @returnType query */ private query function cacheFilteredSmsProviderRecords( struct filter = {}, cacheTime = createTimeSpan( 0, 1, 0, 0 ) ) { var cachedQueryName = hash( serializeJSON( arguments.filter ), 'MD5' ); var queryService = new query( name = cachedQueryName, cachedWithin = arguments.cacheTime ); var sql = 'SELECT #arguments.filter.returnColumns# FROM smsProviders WHERE 1 = 1 '; if( !structIsEmpty( arguments.filter ) ) { // filter is applied // if( structKeyExists( arguments.filter, 'provider' ) ) { sql = sql & 'AND provider = :provider '; queryService.addParam( name = 'provider', value = '#arguments.filter.provider#', cfsqltype = 'cf_sql_varchar' ); } if( structKeyExists( arguments.filter, 'email' ) ) { sql = sql & 'AND email = :email '; queryService.addParam( name = 'email', value = '#arguments.filter.email#', cfsqltype = 'cf_sql_varchar' ); } if( structKeyExists( arguments.filter, 'isActive' ) ) { sql = sql & 'AND isActive = :isActive '; queryService.addParam( name = 'isActive', value = '#arguments.filter.isActive#', cfsqltype = 'cf_sql_bit' ); } if( structKeyExists( arguments.filter, 'order_by' ) ) { sql = sql & 'ORDER BY #arguments.filter.order_by#'; } } return queryService.setSQL( sql ).execute().getResult(); } // QUERY - FILTER SMSPROVIDER RECORDS // /** * @displayname filterSmsProviderRecords * @description I run a query that will return all smsProvider records. If a filter has been applied, I will refine results based on the filter * @param filter {Struct} I am the filter struct to apply to this query * @returnType query */ private query function filterSmsProviderRecords( struct filter = {} ) { var queryService = new query(); var sql = 'SELECT #arguments.filter.returnColumns# FROM smsProviders WHERE 1 = 1 '; if( !structIsEmpty( arguments.filter ) ) { // filter is applied // if( structKeyExists( arguments.filter, 'provider' ) ) { sql = sql & 'AND provider = :provider '; queryService.addParam( name = 'provider', value = '#arguments.filter.provider#', cfsqltype = 'cf_sql_varchar' ); } if( structKeyExists( arguments.filter, 'email' ) ) { sql = sql & 'AND email = :email '; queryService.addParam( name = 'email', value = '#arguments.filter.email#', cfsqltype = 'cf_sql_varchar' ); } if( structKeyExists( arguments.filter, 'isActive' ) ) { sql = sql & 'AND isActive = :isActive '; queryService.addParam( name = 'isActive', value = '#arguments.filter.isActive#', cfsqltype = 'cf_sql_bit' ); } if( structKeyExists( arguments.filter, 'order_by' ) ) { sql = sql & 'ORDER BY #arguments.filter.order_by#'; } } return queryService.setSQL( sql ).execute().getResult(); } } -------------------------------------------------------------------------------- /model/services/UserService.cfc: -------------------------------------------------------------------------------- 1 | /** * * @file UserService.cfc * @author Denard Springle ( denard.springle@gmail.com ) * @description I provide user related data access functions * */ component displayname="UserService" accessors="true" { public function init() { return this; } // PUBLIC METHODS // // CREATE // /** * @displayname createNewUser * @description I insert a new user record into the users table in the database * @param user {Any} I am the User bean * @return numeric */ public numeric function createNewUser( required any user ) { var qPutUser = ''; var queryService = new query(); var sql = ''; try { sql = 'INSERT INTO users ( providerId, username, password, firstName, lastName, phone, role, isActive ) VALUES ( :providerId, :username, :password, :firstName, :lastName, :phone, :role, :isActive )'; queryService.setSQL( sql ); queryService.addParam( name='providerId', value='#arguments.user.getProviderId()#', cfsqltype='cf_sql_integer' ); queryService.addParam( name='username', value='#arguments.user.getUsername()#', cfsqltype='cf_sql_varchar' ); queryService.addParam( name='password', value='#arguments.user.getPassword()#', cfsqltype='cf_sql_varchar' ); queryService.addParam( name='firstName', value='#arguments.user.getFirstName()#', cfsqltype='cf_sql_varchar' ); queryService.addParam( name='lastName', value='#arguments.user.getLastName()#', cfsqltype='cf_sql_varchar' ); queryService.addParam( name='phone', value='#arguments.user.getPhone()#', cfsqltype='cf_sql_varchar' ); queryService.addParam( name='role', value='#arguments.user.getRole()#', cfsqltype='cf_sql_integer' ); queryService.addParam( name='isActive', value='#arguments.user.getIsActive()#', cfsqltype='cf_sql_bit' ); qPutUser = queryService.execute(); // catch any errors // } catch (any e) { writeDump( e ); abort; } return qPutUser.getPrefix().IDENTITYCOL; } // RETRIEVE - BY ID // /** * @displayname getUserByID * @description I return a User bean populated with the details of a specific user record * @param id {Numeric} I am the numeric auto-increment id of the user to search for * @return any */ public any function getUserByID( required numeric id ) { var qGetUser = ''; var queryService = new query(); var sql = ''; try { sql = 'SELECT userId, username, password, firstName, lastName, role, isActive FROM users WHERE userId = :id'; queryService.setSQL( sql ); queryService.addParam( name = 'id', value = '#arguments.id#', cfsqltype = 'cf_sql_integer' ); qGetUser = queryService.execute().getResult(); // catch any errors // } catch (any e) { writeDump( e ); abort; } if( qGetUser.RecordCount ) { return new model.beans.User( userId = qGetUser.userId, providerId = qGetUser.providerId, username = qGetUser.username, password = qGetUser.password, firstName = qGetUser.firstName, lastName = qGetUser.lastName, phone = qGetUser.phone, role = qGetUser.role, isActive = qGetUser.isActive ); } else { return new model.beans.User(); } } // UPDATE // /** * @displayname updateUser * @description I update this user record in the users table of the database * @param user {Any} I am the User bean * @return numeric */ public numeric function updateUser( required any user ) { var qUpdUser = ''; var queryService = new query(); var sql = ''; try { sql = 'UPDATE users SET username = :username, providerId = providerId, password = :password, firstName = :firstName, lastName = :lastName, phone = :phone, role = :role, isActive = :isActive WHERE userId = :userId'; queryService.setSQL( sql ); queryService.addParam( name = 'userId', value = '#arguments.user.getUserId()#', cfsqltype = 'cf_sql_integer' ); queryService.addParam( name = 'providerId', value='#arguments.user.getProviderId()#', cfsqltype='cf_sql_integer' ); queryService.addParam( name = 'username', value = '#arguments.user.getUsername()#', cfsqltype = 'cf_sql_varchar' ); queryService.addParam( name = 'password', value = '#arguments.user.getPassword()#', cfsqltype = 'cf_sql_varchar' ); queryService.addParam( name = 'firstName', value = '#arguments.user.getFirstName()#', cfsqltype = 'cf_sql_varchar' ); queryService.addParam( name = 'lastName', value = '#arguments.user.getLastName()#', cfsqltype = 'cf_sql_varchar' ); queryService.addParam( name = 'phone', value='#arguments.user.getPhone()#', cfsqltype='cf_sql_varchar' ); queryService.addParam( name = 'role', value = '#arguments.user.getRole()#', cfsqltype = 'cf_sql_integer' ); queryService.addParam( name = 'isActive', value = '#arguments.user.getIsActive()#', cfsqltype = 'cf_sql_bit' ); qUpdUser = queryService.execute().getResult(); // catch any errors // } catch (any e) { writeDump( e ); abort; } return arguments.user.getUserId(); } // DELETE // /** * @displayname deleteUserByID * @description I delete a user record from the users table in the database * @param id {Numeric} I am the numeric auto-increment id of the user to delete * @return boolean */ public boolean function deleteUserByID( required numeric id ) { var qDelUser = ''; var queryService = new query(); var sql = 'DELETE FROM users WHERE userId = :id'; try { queryService.setSQL( sql ); queryService.addParam( name = 'id', value = '#arguments.id#', cfsqltype = 'cf_sql_integer' ); qDelUser = queryService.execute().getResult(); // catch any errors // } catch (any e) { writeDump( e ); abort; } return true; } // UTILITY METHODS // // SAVE // /** * @displayname saveUserByID * @description I save a user record in the users table in the database * @param user {Any} I am the User bean * @return numeric */ public numeric function saveUser( required any user ) { if( exists( arguments.user ) ) { return updateUser( arguments.user ); } else { return createNewUser( arguments.user ); } } // EXISTS // /** * @displayname exists * @description I check if a user record exists in the users table in the database * @param user {Any} I am the User bean * @return boolean */ public boolean function exists( required any user ) { var qGetUser = ''; var queryService = new query(); var sql = 'SELECT userId FROM users WHERE userId = :userId'; queryService.setSQL( sql ); queryService.addParam( name = 'userId', value = '#arguments.user.getUserId()#', cfsqltype = 'cf_sql_integer' ); qGetUser = queryService.execute().getResult(); if( qGetUser.recordCount ) { return true; } else { return false; } } // FILTER // /** * @displayname filter * @description I run a filtered query of all records within the users table in the database * @param returnColumns {String} I am the columns in the users table that should be returned in this query (default: all columns) * @param username {String} I am the value for username in the users table that should be returned in this query * @param password {String} I am the value for password in the users table that should be returned in this query * @param firstName {String} I am the value for firstName in the users table that should be returned in this query * @param lastName {String} I am the value for lastName in the users table that should be returned in this query * @param role {Numeric} I am the value for role in the users table that should be returned in this query * @param isActive {Boolean} I am the value for isActive in the users table that should be returned in this query * @param orderBy {String} I am the order to return records in the users table returned in this query * @param cache {Boolean} I am a flag (true/false) to determine if this query should be cached (default: false) * @param cacheTime {Any} I am the timespan the query should be cached for (default: 1 hour) * @return query */ public query function filter( string returnColumns = 'userId, providerId, username, password, firstName, lastName, phone, role, isActive', string providerId, string username, string password, string firstName, string lastName, string phone, numeric role, boolean isActive, string orderBy, boolean cache = false, any cacheTime = createTimeSpan(0,1,0,0) ) { var thisFilter = structNew(); if( isDefined( 'arguments.providerId' ) AND len( arguments.providerId ) ) { thisFilter.rproviderId = arguments.providerId; } if( isDefined( 'arguments.username' ) AND len( arguments.username ) ) { thisFilter.username = arguments.username; } if( isDefined( 'arguments.password' ) AND len( arguments.password ) ) { thisFilter.password = arguments.password; } if( isDefined( 'arguments.firstName' ) AND len( arguments.firstName ) ) { thisFilter.firstName = arguments.firstName; } if( isDefined( 'arguments.lastName' ) AND len( arguments.lastName ) ) { thisFilter.lastName = arguments.lastName; } if( isDefined( 'arguments.phone' ) AND len( arguments.phone ) ) { thisFilter.phone = arguments.phone; } if( isDefined( 'arguments.role' ) AND len( arguments.role ) ) { thisFilter.role = arguments.role; } if( isDefined( 'arguments.isActive' ) AND len( arguments.isActive ) ) { thisFilter.isActive = arguments.isActive; } if( isDefined( 'arguments.orderBy' ) AND len( arguments.orderBy ) ) { thisFilter.order_by = arguments.orderBy; } if( isDefined( 'arguments.cache' ) AND len( arguments.cache ) ) { thisFilter.cache = arguments.cache; } thisFilter.returnColumns = arguments.returnColumns; if( !structIsEmpty( thisFilter ) AND structKeyExists( thisFilter, 'cache' ) AND thisFilter.cache ) { return cacheFilteredUserRecords( thisFilter, arguments.cacheTime ); } else { return filterUserRecords( thisFilter ); } } // PRIVATE METHODS // // QUERY - CACHE FILTERED USER RECORDS // /** * @displayname cacheFilteredUserRecords * @description I run a query that will cache and return all user records. If a filter has been applied, I will refine results based on the filter * @param filter {Struct} I am the filter struct to apply to this query * @param cacheTime {Time} I am the time to cache this query (use createTimeSpan) * @return query */ private query function cacheFilteredUserRecords( struct filter = {}, cacheTime = createTimeSpan( 0, 1, 0, 0 ) ) { var cachedQueryName = hash( serializeJSON( arguments.filter ), 'MD5' ); var queryService = new query( datasource = variables.datasource, name = cachedQueryName, cachedWithin = arguments.cacheTime ); var sql = 'SELECT #arguments.filter.returnColumns# FROM users WHERE 1 = 1 '; if( !structIsEmpty( arguments.filter ) ) { // filter is applied // if( structKeyExists( arguments.filter, 'providerId' ) ) { sql = sql & 'AND providerId = :providerId '; queryService.addParam( name = 'providerId', value = '#arguments.filter.providerId#', cfsqltype = 'cf_sql_integer' ); } if( structKeyExists( arguments.filter, 'username' ) ) { sql = sql & 'AND username = :username '; queryService.addParam( name = 'username', value = '#arguments.filter.username#', cfsqltype = 'cf_sql_varchar' ); } if( structKeyExists( arguments.filter, 'password' ) ) { sql = sql & 'AND password = :password '; queryService.addParam( name = 'password', value = '#arguments.filter.password#', cfsqltype = 'cf_sql_varchar' ); } if( structKeyExists( arguments.filter, 'firstName' ) ) { sql = sql & 'AND firstName = :firstName '; queryService.addParam( name = 'firstName', value = '#arguments.filter.firstName#', cfsqltype = 'cf_sql_varchar' ); } if( structKeyExists( arguments.filter, 'lastName' ) ) { sql = sql & 'AND lastName = :lastName '; queryService.addParam( name = 'lastName', value = '#arguments.filter.lastName#', cfsqltype = 'cf_sql_varchar' ); } if( structKeyExists( arguments.filter, 'phone' ) ) { sql = sql & 'AND phone = :phone '; queryService.addParam( name = 'phone', value = '#arguments.filter.phone#', cfsqltype = 'cf_sql_varchar' ); } if( structKeyExists( arguments.filter, 'role' ) ) { sql = sql & 'AND role = :role '; queryService.addParam( name = 'role', value = '#arguments.filter.role#', cfsqltype = 'cf_sql_integer' ); } if( structKeyExists( arguments.filter, 'isActive' ) ) { sql = sql & 'AND isActive = :isActive '; queryService.addParam( name = 'isActive', value = '#arguments.filter.isActive#', cfsqltype = 'cf_sql_bit' ); } if( structKeyExists( arguments.filter, 'order_by' ) ) { sql = sql & 'ORDER BY #arguments.filter.order_by#'; } } return queryService.setSQL( sql ).execute().getResult(); } // QUERY - FILTER USER RECORDS // /** * @displayname filterUserRecords * @description I run a query that will return all user records. If a filter has been applied, I will refine results based on the filter * @param filter {Struct} I am the filter struct to apply to this query * @return query */ private query function filterUserRecords( struct filter = {} ) { var queryService = new query(); var sql = 'SELECT #arguments.filter.returnColumns# FROM users WHERE 1 = 1 '; if( !structIsEmpty( arguments.filter ) ) { // filter is applied // if( structKeyExists( arguments.filter, 'providerId' ) ) { sql = sql & 'AND providerId = :providerId '; queryService.addParam( name = 'providerId', value = '#arguments.filter.providerId#', cfsqltype = 'cf_sql_integer' ); } if( structKeyExists( arguments.filter, 'username' ) ) { sql = sql & 'AND username = :username '; queryService.addParam( name = 'username', value = '#arguments.filter.username#', cfsqltype = 'cf_sql_varchar' ); } if( structKeyExists( arguments.filter, 'password' ) ) { sql = sql & 'AND password = :password '; queryService.addParam( name = 'password', value = '#arguments.filter.password#', cfsqltype = 'cf_sql_varchar' ); } if( structKeyExists( arguments.filter, 'firstName' ) ) { sql = sql & 'AND firstName = :firstName '; queryService.addParam( name = 'firstName', value = '#arguments.filter.firstName#', cfsqltype = 'cf_sql_varchar' ); } if( structKeyExists( arguments.filter, 'lastName' ) ) { sql = sql & 'AND lastName = :lastName '; queryService.addParam( name = 'lastName', value = '#arguments.filter.lastName#', cfsqltype = 'cf_sql_varchar' ); } if( structKeyExists( arguments.filter, 'phone' ) ) { sql = sql & 'AND phone = :phone '; queryService.addParam( name = 'phone', value = '#arguments.filter.phone#', cfsqltype = 'cf_sql_varchar' ); } if( structKeyExists( arguments.filter, 'role' ) ) { sql = sql & 'AND role = :role '; queryService.addParam( name = 'role', value = '#arguments.filter.role#', cfsqltype = 'cf_sql_integer' ); } if( structKeyExists( arguments.filter, 'isActive' ) ) { sql = sql & 'AND isActive = :isActive '; queryService.addParam( name = 'isActive', value = '#arguments.filter.isActive#', cfsqltype = 'cf_sql_bit' ); } if( structKeyExists( arguments.filter, 'order_by' ) ) { sql = sql & 'ORDER BY #arguments.filter.order_by#'; } } return queryService.setSQL( sql ).execute().getResult(); } } -------------------------------------------------------------------------------- /model/services/formatter.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | public string function longdate( any when ) { 4 | return dateFormat( when, 'long' ) & " at " & timeFormat( when, 'long' ); 5 | } 6 | 7 | } 8 | --------------------------------------------------------------------------------