A middleware package to handle authentication and security via a Draco server
npm install draco-authreq.draco).
req.draco.user and res.locals.user.
loginURL and logoutURL functions on req and res.locals.
NeedsAuthentication: Ensures routes are accessed by authenticated users.
NeedsPermission: Ensures routes are accessed by users with specific permissions.
NeedsOnePermissionOf: Ensures routes are accessed by users with one of the provided permissions.
req.logEvent.
req.hasPermission or req.hasOnePermissionOf.
console
> > npm install --save draco-auth
> `
Usage
===
Prerequisites
---
Before using draco-auth, ensure the following are set up on the main Draco server:
- Users: Create users on the Draco server.
- Applets: Create applets on the Draco server and generate an API key for each applet.
- Permissions: Assign permissions for the applet to the necessary users on the Draco server.
Configuration
---
draco-auth requires the Draco server's host and API key to function. These must be provided via environment variables.
The simplest way to set this up is by using a .env file.
1. Set Up Environment Variables
Create a .env file in your project root (if you haven't already) and add your Draco host and API key:
> `env
> DRACO_HOST=https://DRACO_URL_HERE
> DRACO_API_KEY=your_draco_api_key_here
> `
Ensure you load environment variables in your application (commonly done using the dotenv package):
> `javascript
> require('dotenv').config();
> `
$3
#### Using TypeScript (Recommended)
1. Define Permissions
Create a permissions.ts file (or any suitable file) and define your permissions using an enum:
> `typescript
> export enum Permissions {
> PERMISSION_1 = 'permission_1',
> PERMISSION_2 = 'permission_2',
> }
> `
2. Set Up Express with draco-auth
In your main Express application file (e.g., index.ts):
> `typescript
> import express from 'express';
> import dracoAuth, { NeedsAuthentication, NeedsPermission, NeedsOnePermissionOf } > from 'draco-auth';
> import { Permissions } from './permissions'; // Adjust the import path as necessary
> import dotenv from 'dotenv';
>
> dotenv.config(); // Load environment variables
>
> const app = express();
>
> // Express setup
> app.set('view engine', 'pug'); // Example setup
> app.use(express.static('public'));
>
> // Integrate draco-auth middleware
> app.use(dracoAuth({
> permissions: Object.values(Permissions)
> }));
>
> // Define routes
> app.get('/', (req, res) => {
> res.send('This is an unprotected route');
> })
>
> app.get('/protected', NeedsAuthentication, (req, res) => {
> res.send('This is a protected route.');
> });
>
> app.get('/privileged', NeedsOnePermissionOf([Permissions.Manager, Permissions.> Admin]), (req, res) => {
> res.send('Privileged page.');
> });
>
> app.get('/admin', NeedsPermission(Permissions.Admin), (req, res) => {
> res.send('Admin dashboard.');
> });
>
> app.listen(3000, () => {
> console.log('⚡ Server is running on port 3000');
> });
> `
#### Using JavaScript
1. Set Up Express with draco-auth
In your main Express application file (e.g., index.js):
> `javascript
> const express = require('express');
> const dracoAuth = require('draco-auth');
> const dotenv = require('dotenv');
>
> dotenv.config(); // Load environment variables
>
> const app = express();
>
> // Express setup
> app.set('view engine', 'pug'); // Example setup
> app.use(express.static('public'));
>
> // Integrate draco-auth middleware
> app.use(dracoAuth.default({
> permissions: [
> 'admin',
> 'manager',
> 'permission_1',
> 'permission_2',
> // Add more permissions as needed
> ]
> }));
>
> // Define routes
> app.get('/', (req, res) => {
> res.send('This is an unprotected route.');
> });
>
> app.get('/protected', dracoAuth.NeedsAuthentication, (req, res) => {
> res.send('This is a protected route.');
> });
>
> app.get('/privileged', dracoAuth.NeedsOnePermissionOf(['manager', 'admin']), (req, > res) => {
> res.send('Privileged page.');
> });
>
> app.get('/admin', dracoAuth.NeedsPermission('admin'), (req, res) => {
> res.send('Admin dashboard.');
> });
>
> app.listen(3000, () => {
> console.log('Server is running on port 3000');
> });
> `
$3
#### Middleware Functions
- dracoAuth(options)
Initializes the draco-auth middleware.
Options:
- permissions: string[]
An array of permission strings that can be assigned to users.
- NeedsAuthentication
Middleware to ensure that a user is authenticated. If not, redirects them to the Draco login screen.
Usage:
> `typescript
> app.get('/secure', NeedsAuthentication, (req, res) => {
> res.send('Secure Content');
> });
> `
- NeedsPermission(permission: string, redirect?: string)
Middleware to ensure that a user is authenticated and possesses a specific permission. If the user lacks the required permission, they are redirected to the specified URL (defaults to /).
Parameters:
- permission: string
The permission required to access the route.
- redirect?: string
Optional. The URL to redirect unauthorized users to.
Usage:
> `typescript
> app.get('/admin', NeedsPermission('admin'), (req, res) => {
> res.send('Admin Content');
> });
> `
- NeedsOnePermissionOf(permissions: string[], redirect?: string)
Middleware to ensure that a user is authenticated and possesses at least one permission from the provided array. If the user lacks all the required permission, they are redirected to the specified URL (defaults to /).
Parameters:
- permissions: string[]
The permissions which are allowed to access the route.
- redirect?: string
Optional. The URL to redirect unauthorized users to.
Usage:
> `typescript
> app.get('/admin', NeedsOnePermissionOf(['admin', 'manager']), (req, res) => {
> res.send('Privileged Content');
> });
> `
#### Request Object Extensions
The middleware augments the Express req and res objects with additional properties and functions:
- req.draco
An object representing the user's session.
- req.draco.user
The authenticated user's information, adhering to the following interface:
> `typescript
> interface User {
> name: string;
> surname: string;
> email: string;
> image_path?: string;
> permissions: string[];
> }
> `
- res.locals.user
Same as req.draco.user, available for use in front-end templates.
- req.loginURL(redirect_url: string): string
Generates a URL that redirects the user to the Draco login screen. After successful login, the user is redirected to redirect_url.
Usage:
> `typescript
> const loginLink = req.loginURL('/dashboard');
> `
Available in res.locals.loginURL for use in templates.
- req.logoutURL(redirect_url: string): string
Generates a URL that logs the user out by destroying their session and clears local permissions. After logout, the user is redirected to redirect_url.
Usage:
> `typescript
> const logoutLink = req.logoutURL('/goodbye');
> `
Available in res.locals.logoutURL for use in templates.
- req.logEvent(event: string, additional_information?: string): void
Sends an event to the Draco server for logging. Requires the user to be authenticated.
Parameters:
- event: string
The name of the event (e.g., 'item_deleted').
- additional_information?: string
Optional. Additional details about the event.
Usage:
> `typescript
> req.logEvent('item_deleted', 'Deleted item with ID 123');
> `
- req.hasPermission(permission: string): boolean
Checks if the authenticated user has a specific permission.
Parameters:
- permission: string
The permission to check.
Usage:
> `typescript
> if (req.hasPermission('delete_entries')) {
> // Perform delete operation
> }
> `
Available in res.locals.can for use in templates.
Usage:
> `pug
> - if (can('delete_entries'))
> // Show delete button
> `
- req.hasOnePermissionOf(permissions: ...string[]): boolean
Checks whether the authenticated user has one of the specified permission.
Parameters:
- permissions: ...string[]
The permissions to check.
Usage:
> `typescript
> if (req.hasOnePermissionOf('delete_entries', 'admin')) {
> // Perform delete operation
> }
> `
Available in res.locals.can for use in templates.
Usage:
> `pug
> - if (can('delete_entries'))
> // Show delete button
> `
- res.locals.loginURL
Same as req.loginURL, available for use in front-end templates.
- res.locals.logoutURL
Same as req.logoutURL, available for use in front-end templates.
- res.locals.user
The authenticated user's information, available for use in front-end templates.
Additional Information
===
- Public Hosting: While draco-auth is intended for private use, it is hosted publicly on NPM for ease of deployment. Ensure that sensitive information, such as API keys, is securely managed.
- Caching: User information and permissions are cached for 1 minute on the applet. Any changes made on the Draco server may take up to a minute to reflect in the application.
Example
===
Here's a concise example combining the above concepts in a TypeScript Express application:
> `typescript
> import express from 'express';
> import dracoAuth, { NeedsAuthentication, NeedsPermission } from 'draco-auth';
> import { Permissions } from './permissions';
> import dotenv from 'dotenv';
>
> dotenv.config();
>
> const app = express();
>
> app.set('view engine', 'pug');
> app.use(express.static('public'));
>
> app.use(dracoAuth({
> permissions: Object.values(Permissions)
> }));
>
> app.get('/', NeedsAuthentication, (req, res) => {
> res.render('home');
> });
>
> app.get('/dashboard', NeedsAuthentication, (req, res) => {
> res.render('dashboard');
> });
>
> app.get('/admin', NeedsPermission('admin'), (req, res) => {
> res.render('admin');
> });
>
> app.listen(3000, () => {
> console.log('Server running on port 3000');
> });
> `
In your Pug templates, you can access the user, loginURL, and logoutURL via res.locals:
> `
> //- home.pug
> - if(user)
> h1 Welcome, #{ user.name }!
> a(href=logoutURL('/goodbye')) Logout
> - else
> a(href=loginURL('/dashboard')) Login
> `
Development
===
Building
---
Before making changes to draco-auth, ensure the following steps have been done:
- Clone: Clone this repository.
- Build: Run npm run build to generate the dist folder in the repo directory.
- Test app: Set up a test application (or clone draco-test if you have access to the company Bitbucket).
Troubleshooting
===
Problem. Strange Redirects after Login
---
After being redirected to Draco to sign in to an applet, if you are sent to an unfamiliar URL instead of back to the applet, it is likely an issue with the request headers.
If you are using nginx or another reverse-proxy, ensure you are setting the following header after the proxy pass:
> `nginx
> proxy_set_header X-Forwarded-For $remote_addr;
> ``