The task
Technically, the objective is formulated in the following routine way:
✎ | You've got to provide a login procedure (e.g. username, password, email). The number of attempts is limited (maxAttempts). The time for the attempts is limited (timeForAttempts). The access expires automatically within a certain period (expiry), after which the client has to re-submit the credentials for a new login. The client can also logout. Specific routes are protected (by isLogged), others are open to unrestricted access. |
We'll proceed in a plain and simple manner: on login, we'll set the HTTP cookie along with the time of its expiration (Set-Cookie + Max-Age). On logout we'll remove the said cookie. At each request, we'll check whether the cookie is properly set and, if so, we'll allow further actions.
For that purpose, we need two .js files: a module containing the class/object for authentication (and which exports it), and the main file (server.js) which requires the module and uses it for the server's functionality.
The auth.js module:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | module.exports = { maxAttempts: 3, timeForAttempts: 40, // seconds attempts: 0, expity: 40, // seconds from login loginTime: 0, user: '', isLogged: function(req, res) { let ix, coo = req.headers['cookie']; if(!coo || (ix = coo.indexOf('user='))<0 || coo.substr(ix + 5) != (this.user)) return false; return true; }, login: function(req, res) { if(this.attempts > this.maxAttempts-2) { this.error(res, 'You consumed max login attempts.'); return; } const Tm = Math.floor((new Date()).getTime()/1000); if(this.loginTime>0 && (Tm-this.loginTime)>this.timeForAttempts) { this.error(res, 'The time for login attempts has expired.'); return; } var Js, body = ''; req.on('data', chunk => body += chunk); req.on('end', ()=> { try { Js = JSON.parse(body); } catch(e) { Js = null; } if(!Js || !Js.name || !Js.pssw) { res.end('You did not deliver your credentials'); return; } if(Js.name == 'user' && Js.pssw == 'pssw') { this.user = Js.name; this.loginTime = Tm; res.writeHead(200, { 'Content-Type': 'text/plain', 'Set-Cookie': `user=${this.user}; Max-Age=${this.expiry}` }); res.end('Logged successfully'); } else { if(this.attempts == 0) this.loginTime = Tm; this.attempts++; this.error(res, `Wrong username or password.\n${this.maxAttempts-this.attempts} attempts left.`); } }); }, logout: function(res) { this.loginTime = 0; this.attempts = 0; res.writeHead(200, { 'Content-Type': 'text/plain', 'Set-Cookie': 'user=?; Max-Age=-1' }); res.end('You are logged out.'); }, error: function(res, mssg) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(mssg); } } |
The server.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | const http = require('http'), fs = require('fs'), Auth = require('./auth'); // --- loads the module const server = http.createServer( (req, res) => { if(req.url == '/') { serveFile(res, './index.html'); return; } if(req.url.substr(0, 6) == '/login') { Auth.login(req, res); return; } if(req.url.substr(0, 5) == '/info') { // --- protected function setFileType(fName) { const ix = fName.lastIndexOf('.'); if(ix<0) return "text/plain"; const Ext = fName.substr(ix); if(Ext == '.js') return "text/javascript"; if(".css.html".includes(Ext)) return "text/"+Ext.substr(1); if(".jpeg.png.gif.bmp.webp.ico".includes(Ext)) return "image/"+Ext.substr(1); if(".xml.pdf".includes(Ext)) return "application/"+Ext.substr(1); return "text/plain"; } function serveFile(res, fileName) { fs.readFile( fileName, (err, data)=>{ if(err) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end(`> File [${fileName}] not found!`); return; } console.log(fileName + ': ' + setFileType(fileName)); res.writeHead(200, { 'Content-Type': setFileType(fileName) }); res.end(data); }); } function sendInfo(req, res) { var ix = req.url.indexOf('?'); res.writeHead(200, { 'Content-Type': 'text/html' }); res.write("<html><head><link rel='stylesheet' type='text/css' href='theme.css'></head><body><h1>Server Info:</h1>"); res.write(`<h3>URL: ${(ix<0?req.url:(req.url.substr(0,ix) + '|' + req.url.substr(ix+1)))}</h3>`); res.write('<h2>HEADERS:</h2><table>'); const Hs = req.headers; for(var k in Hs) if(Hs.hasOwnProperty(k)) res.write(`<tr><td>${k}</td><td>${Hs[k]}</td></tr>`); res.write('</table>'); if(ix>-1) { res.write('<h2>QUERIES:</h2><table>'); var arr = req.url.substr(ix+1).split('&'); for(var s of arr) { var itms = s.split('='); res.write(`<tr><td>${itms[0]}</td><td>${decodeURI(itms[1].replace(/\+/g, ' '))}</td></tr>`); } res.write('</table>'); } res.end('</body></html>'); } |
The index.html
The route '/' (localhost:8080) serves the index.html. This file provides login, logout and info functionalities. The '/info' route is protected. The login function uses fetch to POST a json structure, containing name and pssw fields. If these fields are equal to "user" and "pssw" respectively, the login is successful. In your application, you'll have to replace the line #37 of auth.js with your own checker, looking the username and password up in a database.
This is the ludicrously simple example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | <html> <body> <a href="/info?name=Name1&name=Name2&Age=Unknown">Info</a> <input id='user' type='text' value='user' /> <input id='pssw' type='password' value='pssw' /> <button onclick='login(true)'>Login</button> <button onclick='login(false)'>Logout</button> <script> function ID(x) { return document.getElementById(x); } function login(flag) { // true for login, false for logout var data, url, Dt = { name: ID('user').value, pssw: ID('pssw').value }; if(flag) { data = JSON.stringify(Dt); url = "/login"; } else { data = '{}'; url = "/logout"; } fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: data }) .then( res => res.text() ) .then( txt => ID("out").innerHTML = `FROM SERVER: ${txt}` ) .catch( err => alert(err) ); } </script> </body> </html> |
Update
Now that javascript ECMAScript 6 standards allow the class definition in a more traditional way, here is another version of auth.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | class Auth { constructor() { this.maxAttempts = 3; this.timeForAttempts = 40; // seconds this.attempts = 0; this.expity = 40; // seconds from login this.loginTime = 0; this.user = ''; } isLogged(req, res) { let ix, coo = req.headers['cookie']; if(!coo || (ix = coo.indexOf('user='))<0 || coo.substr(ix + 5) != (this.user)) return false; if( (Math.floor((new Date()).getTime()/1000) - this.loginTime) > this.expity) return false; return true; } login(req, res) { if(this.attempts > this.maxAttempts-2) { this.error(res, 'You consumed max login attempts.'); return; } const Tm = Math.floor((new Date()).getTime()/1000); if(this.loginTime>0 && (Tm-this.loginTime)>this.timeForAttempts) { this.error(res, 'The time for login attempts has expired.'); return; } var Js, body = ''; req.on('data', chunk => body += chunk); req.on('end', ()=> { try { Js = JSON.parse(body); } catch(e) { Js = null; } if(!Js || !Js.name || !Js.pssw) { res.end('You did not deliver your credentials'); return; } if(Js.name == 'user' && Js.pssw == 'pssw') { this.user = Js.name; this.loginTime = Tm; res.writeHead(200, { 'Content-Type': 'text/plain', 'Set-Cookie': `user=${this.user}; Max-Age=${this.expiry}` }); res.end('Logged successfully'); } else { if(this.attempts == 0) this.loginTime = Tm; this.attempts++; this.error(res, `Wrong username or password.\n${this.maxAttempts-this.attempts} attempts left.`); } }); } logout(res) { this.loginTime = 0; this.attempts = 0; res.writeHead(200, { 'Content-Type': 'text/plain', 'Set-Cookie': 'user=?; Max-Age=-1' }); res.end('You are logged out.'); } error(res, mssg) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(mssg); } } module.exports = Auth |
If you intend to you the class in auth.js, than in server.js you'll have to instantiate it by
const Auth = new auth();before using its Auth.*() methods.
✎ | In this example, you don't need to url-encode or base64-encode the credentials submitted as the json data by the POST method, as shown in the above. However, if you envisage any further (oftentimes redundant) sophistication, you may also encrypt the json fields on the client side and decrypt them on the server. Alternatively, you may consider setting a token instead of username in a cookie. Whatever your solution, using encrypted channels (HTTPS) is core in ensuring authorised access. |
As to the HTTP Cookies, you can use the same authentication strategy in your C/C++ application.
No comments:
Post a Comment