├── .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 |
Please enter your authorization code below.
69 |This page was last generated on #encodeForHtml( rc.today )#.
An error occurred!
7 |If you have forgotten your password, use this form to have a new system generated password emailed to you.
32 |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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------