@azure-iot/authentication
This library provides support for user authentication in Azure IoT microservices.
Developer Setup
1. Install Node
Node can be found here
2. npm install
This will download and install all dependencies required to build this script.
3. npm run build
This will build the project, putting the output into the src
folder.
Usage
This library provides the Authentication
module, which takes in an express
application and a few configuration values, and returns a middleware that the
application can use on routes to ensure the user is authenticated.
Internally, the library sets up an express session and a passport session, both
backed by mongodb, to ensure the req.user
object contains the authenticated
user information correctly when the route is executed.
This module does not actually authenticate against an OAuth providers directly:
it merely sets up passport so the user is redirected to a specified login URL
(typically hosted on a different service), which will then perform the authentication
and redirect back to the page the user was originally trying to visit.
Example
import {Authentication} from '@azure-iot/authentication';
const app = express();
const auth = await Authentication.initialize(
app,
'http://shell/login',
'keyboard cat',
'mongodb://localhost:27071');
app.get('/', auth.ensureAuthentication, (req, res) => res.sendStatus(200));
Control flow
Authentication.initialize
initializes express and passport sessions backed by mongodb.- User requests a service route (e.g.,
http://localhost:3000/
) that specifies
auth.ensureAuthentication
as a middleware. - Express creates a new session for the user and stores it in mongo.
- The Auth middleware sees that Passport hasn't authenticated the user, stores
the current URL (
http://localhost:3000/
) in the session, and redirects the
user to the login page (in our example, http://shell/login
). - Passport (running in the Shell service as a middleware to the
/login
route)
sees that the user is not authenticated, and redirects to the OAuth provider
(in our case, AAD) to actually authenticate the user. - AAD asks for the user's credentials (or redirects to a federated AD instance
which will ask for the credentials), and redirects back to a callback page
in Shell with the user's tokens (e.g.,
http://shell/auth/aad/return?code=foo&id_token=...
) - Passport parses the returned tokens (which contains information like the user's
name, unique id, and email), and gives it to Shell.
- Shell creates a new
User
entry in mongo (if one with the specified unique id
doesn't already exist), and gives it to Passport. - Passport stores the user's unique id in the express session (using serialization
code specified in the Authentication module.)
- Shell redirects back to the user's original URL (stored in the session in step 4.)
- Back in the original service, Express picks up the session id stored in the cookie,
and retrieves the full session information from mongo.
- Passport retrieves the user information specified in the session, and makes it
available to other request handlers in the
req.user
property. - The Auth middleware sees that Passport has authenticated the user, and calls the
next request handler.
Full example
This library will most likely be used only in a production environment, with
the configuration fetched from the Config service. The following code demonstrates
a helper module that a service can use to query the config service and initialize
authentication with retry logic (for production environment, in case the config
service hasn't been initialized yet), and fallback logic (for dev purposes,
when the config needs to be fetched from a file instead of the config service.)
import * as express from 'express';
import * as request from 'request';
import {Authentication} from '@azure-iot/authentication';
export class Config {
constructor(
public IotHubConnectionString: string,
public EnsureAuthentication: express.RequestHandler) {}
public static async initialize(app: express.Express): Promise<Config> {
const waitForConfig = process.env.NODE_ENV === 'production';
const configUrl: string = process.env.CONFIG_URL || 'http://localhost:3009';
let result: Config = null;
do {
try {
const discovery = await getHal<void>(configUrl + '/api/discovery');
const settingsLink = discovery._links['settings:list'];
if (!settingsLink) throw new Error('Config service does not provide settings:list');
const configSettings = await getHal<ConfigSettings>(configUrl + settingsLink.href);
if (!configSettings.iotHubConnStr) throw new Error('Config service does not provide setting "iotHubConnStr"');
if (!configSettings.loginUrl) throw new Error('Config service does not provide setting "loginUrl"');
if (!configSettings.mongoUri) throw new Error('Config service does not provide setting "mongoUri"');
if (!configSettings.sessionSecret) throw new Error('Config service does not provide setting "sessionSecret"');
const auth = await Authentication.initialize(
app,
configSettings.loginUrl,
configSettings.sessionSecret,
configSettings.mongoUri);
return new Config(
configSettings.iotHubConnStr,
auth.ensureAuthenticated);
} catch (err) {
process.stderr.write(`WARNING: Could not initialize from Config Service: ${err}.\n`);
if (waitForConfig) {
await new Promise((resolve, reject) => setTimeout(resolve, 5000));
} else {
const userConfigFile = path.join(__dirname, '../user-config.json');
if (!fs.existsSync(userConfigFile)) {
console.log('Unable to find the user configuration: please fill out the information in ' + userConfigFile);
process.exit(1);
}
let userConfig: {
IotHubConnectionString: string;
} = require(userConfigFile);
return new Config(
userConfig.IotHubConnectionString,
(req, res, next) => next());
}
}
} while (!result);
}
}
interface ConfigSettings {
iotHubConnStr: string;
loginUrl: string;
mongoUri: string;
sessionSecret: string;
'device-management': {
logLevel: string;
consoleReporting: string;
};
}
interface HalLink {
href: string;
}
interface HalResponse {
_links: {
self: HalLink;
[rel: string]: HalLink;
};
}
async function getHal<T>(uri: string) {
return new Promise<T & HalResponse>((resolve, reject) => {
request.get(uri, {json: true}, (err, response, body) => {
err ? reject(err) : resolve(body);
});
});
}