Basic Authentication is when raw(or with basic encoding) username and password is sent to the server, typically in its body
Provided example sets up a simple NodeJs server which exposes API (url) for Basic HTTP Authentication
Check example Android/iOS mobile applications made in Flutter or React Native which sends request to the NodeJs server for authentication
Also check example of providing user data after authentication
This has following parts:
Provided example sets up a nodejs express server
Check NodeJs Installation
Express is a Nodejs web application framework
Among other things, it is used to add middlewares to a web app, so that every request passes through these middlewaresallowing various functionalities such as request body parsing, authentication etc and also custom middlewares
Some middlewares like authentication might return a response and stop further processing of a request
//import express
const express = require('express');
// import external module to be used as middleware
const bodyParser = require('body-parser');
// import custom middleware
const authMiddleware = require('auth/auth_middleware');
// declare an express app
const app = express();
// add bodyParser.json() middleware to application to parse body of each request to json
// so that any further middleware or method can access request body as json
app.use(bodyParser.json());
// use custom authentication middleware
app.use(authMiddleware);
// assign a function to the route(or path) '/base', which only accepts Http Get requests (for Post request use app.post())
app.get('/base',function(req, res, next)
{
// Send a string as response, can be changed to html or json content
res.send('Message from Express server');
});
// start server at port 8800
const server = app.listen(8800, function () {
// function called after server is started
console.log('Server listening on port ' + port);
});
next
next
is a part of connect middleware
Middlewares are added as a stack
Each middleware calls next()
which invokes the next middleware
Middlewares are executed one by one for each request to the app, until a middleware does not call next() within it
This flow is also stopped if next is called with an argument next(err)
which is considered as an error
These can be tested to check if they behave differently, but maybe prefer altering a request req
variable to send data to next middleware than trying with next(some_object)
Create a new directory
mkdir nodejs_auth
Change directory to newly created directory
cd nodejs_auth
Run npm init
command to create a package.json file so that node packages can be installed
npm init -y
-y
is added so that it doesn't prompt for setting some common info about package
Run following command from project directory
npm install async cors express rootpath --save
Using lib
as folder name to store backend files is like a convention
Sub directories are not created for simplicity
Role of service is to provide user data to the controller methods
Example provided here has a hardcoded json list
Typically, a service can get data from database or other services
const users = [
{ id: 1, username: 'test', password: 'test', firstName: 'Test', lastName: 'User', phone: '5235234132' },
{ id: 2, username: 'ncuser', password: 'ncpassword', firstName: 'En', lastName: 'Cee', phone: '7685496767' }];
module.exports = {
authenticate,
getUserInfo
};
async function authenticate(username, password) {
const user = users.find(u => u.username === username && u.password === password);
if (user) {
const { id, ...otherUserData } = user;
return {id: id, username: username};
}
}
async function getUserInfo(req, res) {
const user = users.find(u => u.id = req.user.id);
const { password, ...userWithoutPassword } = user;
res.json(userWithoutPassword);
}
The controller assigns methods to specific routes for express Router
These routes are the url paths that a client uses to call specific api
Instance of Express framework's Router is retrieved by calling express.Router()
router.post()
assigns controller method for a route which acceps Http Post requests and router.get()
for Get Apis
const express = require('express');
// get Router instance
const router = express.Router();
// variable to access exported methods of user_service.js
const userService = require('./user_service');
// set controller methods for routes
router.post('/authenticate', authenticate);
router.get('/getInfo', getInfo);
module.exports = router;
function authenticate(req, res, next) {
// password is expected to be in base64 encoded form, which is decoded to send raw password to service
var password = Buffer.from(req.body.password, 'base64').toString('ascii')
userService.authenticate(req.body.username, password)
// here 'user' is the object returned from authenticate() if not null, which contains 'id' of user
.then(user => user ? res.json(user) :
res.status(400).json({ message: 'Username or password is incorrect' }))
// error is handled by global handler
.catch(err => next(err));
}
function getInfo(req, res, next) {
userService.getUserInfo(req, res)
}
Declare an error Handler to be used globally for the web app
Error handlers take 4 arguments, where first arg err
is the error
module.exports = errorHandler;
function errorHandler(err, req, res, next) {
if (typeof (err) === 'string') {
// custom application error
return res.status(400).json({ message: err });
}
// default to 500 server error
return res.status(500).json({ message: err.message });
}
Middlewares take 3 arguments and call next()
when they are done processing for a request so that subsequent middleware can be executed
next
can be thought of to be similar to Python's continue
statement
Authorization string is expected to be present in a http header named Authorization
and in the formBasic base64_encoded(username:password)
where base64_encoded denotes that the concatenation username:password
is encoded to base64 format
const userService = require('./user_service');
const async = require('async');
module.exports = authMiddleware;
async function authMiddleware(req, res, next) {
// make authenticate path public i.e. skip authMiddleware middleware for path
if (req.path === '/users/authenticate') {
next();
}
// check for basic auth header
if (!req.headers.authorization || req.headers.authorization.indexOf('Basic ') === -1) {
return res.status(401).json({ message: 'Missing Authorization Header' });
}
// verify auth credentials
const base64Credentials = req.headers.authorization.split(' ')[1];
const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
const [username, password] = credentials.split(':');
const user = await userService.authenticate( username, password );
if (!user) {
return res.status(401).json({ message: 'Invalid Authentication Credentials' });
}
// attach user to request object
req.user = user
next();
}
index.js can be renamed to some other file, provided it corresponds to main
of package.json
require('rootpath')();
const express = require('express');
const app = express();
const cors = require('cors');
const bodyParser = require('body-parser');
const errorHandler = require('lib/error_handler');
const authMiddleware = require('lib/middleware');
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(cors());
app.use(authMiddleware);
// api routes
app.use('/users', require('lib/controller'));
// global error handler
app.use(errorHandler);
// configure port number based on whether server is configured as running in production
// to set NODE_ENV as 'production'
const port = process.env.NODE_ENV === 'production' ? 80 : 4000;
// start server
const server = app.listen(port, function () {
console.log('Server listening on port ' + port);
});
Run following command from project directory
nodejs index.js
To test running in production
sudo NODE_ENV='production' nodejs index.js
To make it as accessible over a local network such as wifi, enter local ip address with command
nodejs index.js 192.168.43.34
Here, 192.168.43.34 is ip address
Save following code in a file named test.html
(or some other name with .html extension) in a directory
Replace url in case server is run in some ip address other that localhost
Open the file in a browser (double-click should do that)
<!doctype html>
<html>
<meta charset="utf-8">
<body>
<div>
<div>
<h3>Login Form</h3>
<table>
<tr>
<td><label for="id_username">Enter username</label></td>
<td><input type="text" id="id_username"></td>
</tr>
<tr>
<td><label for="id_username">Enter password</label></td>
<td><input type="password" id="id_password"></td>
</tr>
<tr>
<td><button id='btn'>Authenticate</button></td>
<td><button id='infoBtn'>Get user info</button></td>
</tr>
</table>
<pre><code id="output"></code></pre>
</div>
</div>
</body>
<script>
output = document.getElementById('output')
function authReq() {
output.innerHTML = ""
username = document.getElementById('id_username').value
password = document.getElementById('id_password').value
const encoded = btoa(password);
fetch('http://127.0.0.1:4500/users/authenticate', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({username: username, password: encoded})
})
.then(response => response.json())
.then((value) => {
output.innerHTML = JSON.stringify(value)
});
}
function getInfo() {
output.innerHTML = ""
const encoded = 'Basic ' + btoa(username+":"+password)
fetch('http://127.0.0.1:4500/users/getInfo', {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': encoded
},
})
.then(response => response.json())
.then((value) => {
output.innerHTML = JSON.stringify(value)
});
}
document.getElementById('btn').onclick = authReq
document.getElementById('infoBtn').onclick = getInfo
</script>
</html>
Test with username and password set in server
The corresponding server for mobile applications uses an updated version of above code which adds some more fields to user data and adds another api