@sempervirens/plugin-authentication-endpoints v0.0.11
Sempervirens Plugin: Authentication Endpoints
A set of endpoints to facilitate user registration, login, and password reset with Express and MongoDB.
Contents
Installation
npm i @sempervirens/plugin-authentication-endpoints
Usage
An authentication system is somewhat complex with a number of moving parts (server with backend and frontend endpoints, database, email). In large productions, multiple servers, proxies and other network and security measures may be additional factors. To get started with this modest implementation of an authentication system, it may be easier to copy the example below and go from there. The explanations may provide helpful supplemental material.
Email Configuration
If the emailConfig
is not passed into the site loader configuration, then no emails are sent. However, an email is required to be sent in order to use the /reset-password
functionality.
The default emailConfig
uses Nodemailer with Gmail. Nodemailer configurations may be passed in the emailConfig
options
property. See @sempervirens/emailer for more information.
See the email examples below for the properties that are passed into the emailConfig
emails
array, and the properties that are passed into the email template
functions. The default resetLinkExpiresIn
is 10 minutes.
Making Requests
After generating JWT public and private keys (see @sempervirens/authorizer), getting MongoDB set up (see @sempervirens/dao), and defining files as in the examples below, when the Sempervirens server is started with authenticationEndopints
, the endpoints are ready for requests.
Register
Local: POST {protocol}://localhost[:{port}]/{domain}/api/register
Remote: POST {protocol}://{domain}[:{port}]/api/register
First a /register
page should be present on the frontend that displays a registration form (see example below). When the form is submitted, it posts the input values to /api/register
.
When calling the /api/register
endopint, the body
parameters are processed and a user is created in the database with those properties. The user schema (see below) must define the parameters in order for them to be added to the created user document. email
, password
, and repeatPassword
are required. _id
is returned.
Note: /register
is the frontend page. /api/register
is the backend endpoint
Login
Local: POST {protocol}://localhost[:{port}]/{domain}/api/login
Remote: POST {protocol}://{domain}[:{port}]/api/login
First a /login
page should be present on the frontend that displays a login form (see /register
example below). When the form is submitted, it posts the input values to /api/login
.
When calling the /login
endopint, the body
parameters are evaluated to determine if the user entered the correct email and password combination. email
and password
are required. _id
is returned.
Note: /login
is the frontend page. /api/login
is the backend endpoint
Reset Password
First a /rest-password
page should be present on the frontend that displays a reset password form in two steps (see /register
and /reset-password
examples below). When the form is submitted, it posts the input values to /api/reset-password/email
or to /api/reset-password/reset
.
/reset-password
is a single endpoint that takes an action
path parameter.
Note: /reset-password
and /reset-password?token={token}
are the frontend pages. /api/reset-passwrod/email
and /api/reset-password/reset
are the backend endpoints.
Local: POST {protocol}://localhost[:{port}]/{domain}/api/password-reset/email
Remote: POST {protocol}://{domain}[:{port}]/api/password-reset/email
First a fontend page should be present on the website at /reset-password
where a Reset Password form displays with an "Email" input, if a token query param is not present or the token is invalid.
When the form is submitted, the /api/reset-password/email
endpoint is called. Then the body
parameters are evaluated, a token is generated, and an email is sent containing the token. The default duration of the token is 10 minutes. The user then clicks the token link and should be directed back to a page on the website where a form displays.
Reset
Local: POST {protocol}://localhost[:{port}]/{domain}/api/password-reset/reset
Remote: POST {protocol}://{domain}[:{port}]/api/password-reset/reset
First a frontend page should be present on the website where a Reset Password form displays with "Password" and "Repeat Password" fields, if a valid query param token is included in the request.
After clicking the token URL in the email, the site opens to the /reset-password?token={token}
page. When the user submits the form, the body
parameters and token
are evaluated, and if valid the password is updated.
Code Example
Please note, these are not exhaustive examples and are intended only to give an idea about how to use the authentication plugin in the context of the Sempervirens server system.
import { readFileSync } from 'fs';
import authorizer from '@sempervirens/authorizer';
import dao from '@sempervirens/dao';
import Server from '@sempervirens/server';
import cspMiddleware from '@sempervirens/plugin-csp-middleware';
import authenticationEndpoints from '@sempervirens/plugin-authentication-endpoint';
import authenticationEndpoints from '../index.js';
// User schema (separate file)
import crypto from 'crypto';
import mongoose from 'mongoose';
// Example only
// "salt" may be generated with crypto.randomBytes, etc.
// "hash" may be generated with crypto.pbkdf2Sync, crypto.createHmac, etc.
// "email" and "password" are required.
// "salt" is STRONGLY recommended.
const userSchema = new mongoose.Schema({
email: { type: String, required: true, index: { unique: true } },
password: { type: String, required: true },
salt: { type: String }
});
const generateHash = function(password, salt) {
return password + salt; // Not a real hash function
};
const hashPassword = function(next) {
this.salt = 'salt'; // Not a real salt
this.password = generateHash(this.password, this.salt);
next();
};
userSchema.pre('save', hashPassword);
// methods.passwordMatches is required.
userSchema.methods.passwordMatches = function(password) {
return generateHash(password, this.salt) == this.password;
};
// Initialize authorizer
authorizer.init({
jwtPublicKey: readFileSync('./path/to/public/key', 'utf8'),
jwtPrivateKey: readFileSync('./path/to/private/key', 'utf8')
});
// Initialize dao
dao.initDb({
host: 'localhost',
port: '27017',
connectionOptions: {
useNewUrlParser: true,
useUnifiedTopology: true
},
name: 'testdb',
models: [
{ name: 'User', schema: userSchema }
]
});
// Initialize emails (separate files)
const registerEmail = {
subject: 'Site 1 Registration Confirmation',
template: ({ requestBody: { first, last }, baseUrl }) => {
return '' +
`
<html>
<head></head>
<body>
Hi ${first} ${last},<br><br>
Thanks for registering! Please follow the link below to log in:<br><br>
<a href="${baseUrl}/login">Log in</a><br><br>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur.<br><br>
Best regards,<br>
Administrator<br>
</body>
</html>`;
}
};
const resetPasswordEmail = {
subject: 'Site 1 Password Reset',
resetLinkExpiresIn: '20m', // Default 10m
template: ({ record: { first, last }, baseUrl, token }) => {
return '' +
`
<html>
<head></head>
<body>
Hi ${first} ${last},<br><br>
Please follow the link below to reset your password:<br><br>
<a href="${baseUrl}/reset-password?token=${token}">Reset Password</a><br><br>
The link will be valid for twenty minutes.<br><br>
Consequat nisl vel pretium lectus quam id leo in. Neque laoreet suspendisse
interdum consectetur libero id faucibus nisl. Vitae congue mauris rhoncus aenean
vel elit scelerisque.<br><br>
Please disregard this email if you did not request to reset your password.<br><br>
Best regards,<br>
Administrator
</body>
</html>`;
}
};
// Start the server
new Server({
port: 8080,
sites: [
{
domain: 'site-1',
data: {
dbName: 'testdb',
baseUrl: 'http://localhost:8080/site-1', // Passed into resetPasswordEmail template
emailConfig: {
from: 'your test email address', // Easiest is a Gmail test account with an app password
password: '',
name: 'Test Admin',
// options: {} // Nodemailer options
emails: {
registerEmail,
resetPasswordEmail
}
}
},
middleware: [cspMiddleware()],
endpoints: [
// Backend API endpoints
...authenticationEndpoints(),
// Frontend pages for SSR
// (For SPA sites, these routes are defined instead in the app loaded in index.html, which Sempervirens loads by default. See @sempervirens/site-loader for directory structure setup.)
{
path: 'GET /register',
// Example only, separate RequestHandler file recommended
handler: ({ req, res }) => {
const nonce = res.getNonce();
res.send([
'<html><head><title>Register</title></head>',
'<body><form>',
'<input name="email" placeholder="Email"><br>',
'<input type="password" name="password" placeholder="Password"><br>',
'<input type="password" name="repeatPassword" placeholder="Repeat Password"><br>',
'<input type="submit" id="submit" value="Submit"></form>',
`<script nonce="${nonce}">`,
` document.getElementById('submit').addEventListener('click', submit);`,
` async function submit(e) {
e.preventDefault();
const values = Array
.from(document.forms[0].elements)
.reduce((values, elem) => {
if (elem.type != 'submit') {
values[elem.name] = elem.value;
}
return values;
}, {});
const res = await (await fetch('http://localhost:8080/site-1/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values)
})).json();
console.log(res);
}`,
'</script>',
'</body></html>'
].join(''));
}
},
{
path: 'GET /login',
handler: ({ req, res }) => {
// See 'GET /register' for an example of form submission
res.send([
'<html><head><title>Login</title></head>',
'<body><form action="/api/login">email, password inputs</form></body></html>'
].join(''));
}
},
{
path: 'GET /reset-password/reset',
handler: ({ req, res }) => {
// See 'GET /register' for an example of form submission
const token = req.query.token;
if (!token || !authorizer.isValid(token)) {
// Submits to 'GET /api/reset-password/email'
res.send([
'<html><head><title>Reset Password</title></head>',
'<body><form action="/api/reset-password/email">email input</form></body></html>'
].join(''));
} else {
// Submits to 'GET /api/reset-password/reset'
res.send([
'<html><head><title>Reset Password</title></head>',
'<body><form action="/api/reset-password/reset">password, repeat password inputs</form></body></html>'
].join(''));
}
}
},
]
}
]
}).start();