This blog post focuses on the creation of a express.js middleware package, using the Creating a TS-written NPM package for usage in Node-JS or Browser: The Long Guide as a basis. I'll use that guide up to Configuring GIT to generate a basic Typescript project. From that point, we'll continue below.
Goal of the package
The package we'll be writing will be providing a middleware to easily add TOTP (Time-based One-Time Password, the 6-digit codes generated by your Authenticator app), commonly used for Two-Factor Authentication, to any express project.
The exact package structure is yet to be determined, but I'd imagine it will look like the snippet below. In this example, all options are included. In our final package, all options are optional: The library should function without any configuration.
import * as express from "express";
import { initTotp, ensureTotpVerified } from "bonaroo-totp-express";
const app = express();
// Configure our library, storing the settings in the express app.
// Additionally, will check every request whether TOTP was verified.
app.use(initTotp({
storage: "signed-cookie", // or "session"
property: "totpVerified",
setup: {
// path of setup page
path: "/user/totp-setup",
// view to render setup page
view: "/user/totp-setup",
// assigns to `res.locals` when rendering the setup page
locals: {
totpSecret: "totpSecret",
totpPeriod: "totpPeriod",
totpQrCodeSvg: "totpQrCodeSvg",
totpErrorCode: "totpErrorCode",
totpFormTarget: "totpFormTarget",
totpFormMethod: "totpFormMethod",
totpCodeInputName: "totpCodeInputName",
},
},
verify: {
// path of the verify page
path: "/user/totp-verify",
// view to render verify page
view: "/user/totp-verify",
// assigns to `res.locals` when rendering the verify page
locals: {
totpFormTarget: "totpFormTarget",
totpFormMethod: "totpFormMethod",
totpCodeInputName: "totpCodeInputName",
totpErrorCode: "totpErrorCode",
},
},
getIssuer = (req) => "Sample Issuer",
getUserAccountName = (req) => req.user ? req.user.name : null,
getUserTotpSecret = (req) => req.user ? req.user.totpSecret : null,
async setUserTotpSecret(req: express.Request, totpSecret: string) {
req.user.totpSecret = totpSecret;
await req.user.save();
},
}));
app.get("/sign-in", /* */);
app.get("/sign-out", /* */);
// For all routes below, require totp verification.
app.use(ensureTotpVerified());
app.get("/user", /* */);
app.get("/admin", /* */);
Goal of this post
I'll be writing the progress of this library to demonstrate the flow of writing an actual working NodeJS package, including comprehensive tests and configuration options for package users.
This post will also include trouble-shooting and mistakes. Therefore, this post will be incredibly long.
About TOTP: Time-based One Time Password
HTOP: (HMAC-based One-Time Password) is an algorithm where a stream of passwords is generated using some kind of hashing method, a secret, and a counter.
This secret is shared between the authenticator and the authenticated, allowing the authenticated to generate passwords derived from the secret to confirm their identity with the authenticator. The authenticator checks the password against a configurable window of passwords generated from the same counter.
This does require both the authenticator and the authenticated to synchronise their counters somehow. One could, for example, increment the authenticated counter by 1 everytime a password is generated by the authenticated, and increment the authenticator counter by 1 when a password is used used at the authenticator.
To automate the counter synchronisation process, one could use time as a counter: Time-based One-Time Password is born. Both parties increment their counter every n
seconds (usually 30), starting at some point in time. The authenticated generates a password, and the authenticator checks the password against a small window, usually about up to one minute earlier and later.
Because the passwords are 6 digits, and are valid for 30 seconds up to a few minutes, brute-forcing the passwords is a real danger. Therefore, persistent throttling is required for a secure implementation. The authenticator should lock-out a user with an increasing timeout after receiving invalid codes.
Step 1: Creating and testing a middleware that always assigns totpVerified=false
.
Because I use TDD to write my code, I start writing a test first. Because we're writing a middleware that tightly integrates with express.js, we'll need a way to test express applications.
I'll be using a library called Supertest to test our express application.
$ npm i -D supertest @types/supertest
npm WARN deprecated superagent@3.8.3: Please note that v5.0.1+ of superagent removes User-Agent header by default, therefore you may need to add it yourself (e.g. GitHub blocks requests without a User-Agent header). This notice will go away with v5.0.2+ once it is released.
npm WARN bonaroo-totp-express@1.0.0 No description
+ @types/supertest@2.0.8
+ supertest@4.0.2
added 10 packages from 62 contributors and audited 876631 packages in 4.896s
found 0 vulnerabilities
Now, let's create an express app in our tests and test it using supertest.
test/index.spec.ts
import { hello } from "../src";
import * as express from "express";
import supertest = require("supertest");
test("hello", () => {
expect(hello("foo")).toEqual("Hello foo");
});
test("supertest", () => {
const app = express();
app.get("/", (req, res) => res.type("txt").send("Hi"));
return supertest(app)
.get("/")
.expect(200, "Hi")
.expect("Content-Type", /text/);
});
$ npm t
> bonaroo-totp-express@1.0.0 test /Users/hinloopen/Projects/Github/bonaroo-totp-express
> jest
PASS test/index.spec.ts
✓ hello (2ms)
✓ supertest (15ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.626s, estimated 1s
Ran all test suites.
That works. Now let's write a test that expects req.totpVerified
to equal false
.
Writing a test to test the middleware of an express app is a bit tricky, since the user of the middleware uses it inside request handlers. Therefore, you're interested in testing the behavour of the middleware inside a request handler of an express app.
To allow testing our middleware, we want to run some assertions inside a request handler while a request is being handled. Therefore, we must do the following:
- create an express server
- create and add our to-be-tested middleware to the server
- add a request handler to actually check the behavior of the middleware
- invoke the server to execute the middleware and
- check the result of the assertions inside the request handler
Let's start with writing a bare middleware that can be added to an express app, so we can implement the middleware later.
src/totpInit.ts
export function totpInit(options: any) {
return (req: Request, res: Response, next: NextFunction) => {
next();
}
}
With our new middleware, let's setup a (failing) test that can assert the state of a request after the middleware is executed.
test/totpInit.spec.ts
import { totpInit } from "../src/totpInit";
import * as express from "express";
import supertest = require("supertest");
test("totpInit assigns req.totpVerified to false", (done) => {
const app = express();
// add our middleware
app.use(totpInit({}));
// test our middleware
app.get("/", (req, res) => {
expect((req as any).totpVerified).toEqual(false);
res.type("txt").send("Hi");
});
// catch errors & invoke
app.use((error, req, res, next) => { done(error); next(error); });
supertest(app).get("/").expect(200).then(() => done(), done);
});
This test explained: What are these
done
's?This test is asynchronous, using callbacks. When the function passed to
test()
returns, the test is not yet completed. To let Jest know this is an async test, we can either return a promise or accept a callback function in the test callback, here calleddone
.This
done
callback needs to be called at least once, with either no arguments (indicating a success), or one argument (usually an error) indicating a failure.app.use((error, req, res, next) => { done(error); next(error); });
This line registers an express error handler: Any unhandled error thrown in any express request handler or middleware is captured by this error handler.
When ourexpect
fails, an error is thrown. This error is caught with this express error handler. We call Jest'sdone
with the error to let Jest know our test finished with error. After that, we let express also know about the error so it can render a 500 response.supertest(app).get("/").expect(200)
This line starts and calls the express server and expects a 200 response. A Promise is returned. The promise either resolves to the request's response, or any error.
.then(() => done(), done);
.then
accepts up to 2 callbacks: The first is the resolve callback, invoked with a successful response. The second is the error callback, invoked with an error.When the response was successful, we want to let Jest know the test finished without error. We invoke
() => done()
to calldone
without any argument (because passing any argument causes Jest to consider it a failure).When the response was not successful, we want to let Jest know the test failed with error, and pass the error to jest. You could use
(err) => done(err)
as an error callback, but sincedone
accepts an error as first argument, and the callback is also invoked with an error as first argument, we can just usedone
as an error callback.
$ npm t
> bonaroo-totp-express@1.0.0 test /Users/hinloopen/Projects/Github/bonaroo-totp-express
> jest
PASS test/index.spec.ts
FAIL test/totpInit.spec.ts
● totpInit assigns req.totpVerified to false
expect(received).toEqual(expected) // deep equality
Expected: false
Received: undefined
9 |
10 | app.get("/", (req, res) => {
> 11 | expect((req as any).totpVerified).toEqual(false);
| ^
12 | res.type("txt").send("Hi");
13 | });
14 |
at test/totpInit.spec.ts:11:39
at Layer.handle [as handle_request] (node_modules/express/lib/router/layer.js:95:5)
at next (node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (node_modules/express/lib/router/layer.js:95:5)
at node_modules/express/lib/router/index.js:281:22
at Function.process_params (node_modules/express/lib/router/index.js:335:12)
at next (node_modules/express/lib/router/index.js:275:10)
at src/totpInit.ts:5:5
at Layer.handle [as handle_request] (node_modules/express/lib/router/layer.js:95:5)
Test Suites: 1 failed, 1 passed, 2 total
Tests: 1 failed, 2 passed, 3 total
Snapshots: 0 total
Time: 0.9s, estimated 2s
Ran all test suites.
npm ERR! Test failed. See above for more details.
Yep. That's a failing test. Now let's make it succeed.
src/totpInit.ts
import { Request, Response, NextFunction } from "express";
export function totpInit(options: any) {
return (req: Request, res: Response, next: NextFunction) => {
req.totpVerified = false;
next();
}
}
O-oh: Typescript doesn't allow req.totpVerified
to be assigned since it doesn't exist on express
's Request
type. Let's fix that:
src/types.d.ts
declare namespace Express {
export interface Request {
totpVerified?: boolean;
}
}
Now we can remove our as any
hack from the test as well:
test/totpInit.ts (partial)
app.get("/", (req, res) => {
expect(req.totpVerified).toEqual(false);
res.type("txt").send("Hi");
});
Step 2: Creating the Setup page
The TOTP setup page includes creating a secret, sharing this secret with the user, and letting the user confirm the secret is stored by letting the user generate a password using their app.
This page consists of 2 routes: One GET for the setup form, and one POST to submit the setup, either rendering a failure or redirecting after success.
To render the setup page, we'll need a TOTP secret, and a handy QR code containing a special URL containing the secret.
For our library, we want to provide a default view, but we also want to allow the user to only generate the secret and QR code in a middleware before rendering their own view.
Let's first create this middleware. We can expose this middleware to our library user, and use it ourself in our library's default view.
This middleware will assign the secret and QR code in locals named totpSecret
and totpQrCodeUrl
respectively.
Let's start with the test:
test/totpSetup.spec.ts
import { totpSetup } from "../src/totpSetup";
import * as express from "express";
import supertest = require("supertest");
import url = require("url");
test("totpSetup assigns a secret and qr code", (done) => {
const app = express();
app.get("/", totpSetup(), (req, res) => {
expect(typeof res.locals.totpSecret).toEqual("string");
expect(typeof res.locals.totpQrCodeUrl).toEqual("string");
res.type("txt").send("Hi");
});
app.use((error, req, res, next) => { done(error); next(error); });
supertest(app).get("/").expect(200).then(() => done(), done);
});
Since the secret is a random value, you cannot really test the value. You can, however, check whether it's a string.
Next we check the QR Code URL. There is no real easy way to confirm the QR code's content is valid. Let's just assert it's a string again.
The test obviously fails with compile errors: totpSetup
doesn't exist.
Let's fix the compile errors first by creating a bare middleware named totpSetup
.
src/totpSetup.ts
import { Request, Response, NextFunction } from "express";
export function totpSetup() {
return (req: Request, res: Response, next: NextFunction) => {
next();
}
}
FAIL test/totpSetup.spec.ts
✕ totpSetup assigns a secret and qr code (14ms)
● totpSetup assigns a secret and qr code
expect(received).toEqual(expected) // deep equality
Expected: "string"
Received: "undefined"
8 |
9 | app.get("/", totpSetup(), (req, res) => {
> 10 | expect(typeof res.locals.totpSecret).toEqual("string");
| ^
11 | expect(typeof res.locals.totpQrCodeUrl).toEqual("string");
12 | res.type("txt").send("Hi");
13 | });
totpSetup
now exists, removing the comile errors. Now we get expectation failed errors, because totpSetup
doesn't actually do anything.
To implement totpSetup
, let's create the TOTP secret & QR code. There are many packages that can help us generate & verify TOTP secrets. Let's find some by searching for TOTP in the npm search.
Sorting by popularity, totp and speakeasy are on top. Totp looks like a poorly documented port with little weekly downloads - I suspect it's only at top because it's an exact match.
The next thing is a library called speakeasy. I never used speakeasy, but given the 39K weekly downloads I'll assume it's alright. Let's see if it is actually something we're looking for.
After a quick peek at the readme, it looks like generating secrets & verifying tokens is present in the package, and it looks easy to use. It's well documented and even has examples for creating a QR code.
Let's install it, and use it. The docs mention using qrcode
to generate QR codes, so let's grab that one as well.
npm i speakeasy qrcode && npm i -D @types/speakeasy @types/qrcode
src/totpSetup.ts
import { Request, Response, NextFunction } from "express";
import { generateSecret } from "speakeasy";
import { toDataURL } from "qrcode";
export function totpSetup() {
return (req: Request, res: Response, next: NextFunction) => {
const secret = generateSecret();
res.locals.totpSecret = secret.base32;
toDataURL(secret.otpauth_url, (err, url) => {
res.locals.totpQrCodeUrl = url;
next();
});
}
}
The test succeeds, but that does not mean our code actually works. The test only checks whether the secret and URL are strings.
We're not sure whether the QR code is actually correct, or whether it's even an URL. I'm also not sure how we could test that.
We could somehow scan the image using a QR code parser, parsing the URL, and use the URL to generate a code. Maybe we could write an integration test for this later.
I'm also not sure whether the secret is actually a real secret. To verify the secret, we somehow need to verify every request a unique, random secret is generated. Testing whether something returns a random thing is pretty hard.
Let's first check whether the QR code actually resolves to an URL, and whether that URL is a valid OTP url.
Testing the QR code
To parse the QR code, we're installing a QR code reader package in development. The QR code reader package I picked at random requires jimp to read the image data. Fine.
npm i -D qrcode-reader jimp
It seems jimp doesn't play nice with Typescript without changes. Jimp requires allowSyntheticDefaultImports
to be set to true
according to their documentation. However, even with this setting enabled, I still either get runtime errors (cannot call read on Jimp) or compiler errors (property read does not exists on Jimp). I'm not interested to find out why, so let's convert its type to any
before using it.
Jimp allows reading files or buffers, but it cannot read base64 data URLs. Therefore, I'm asserting the URL is a data url, and I'm converting the base64 data to a buffer.
Because reading the image dataurl and reading the QR code is a messy bit of code with type-hacks and mixing callbacks and promises, I'm hiding it in a function somewhere, never to be seen again.
test/support/expectAndParseQrCode.ts
import * as Jimp from "jimp";
import * as QrCode from "qrcode-reader";
export async function expectAndParseQrCodeDataUrl(maybeDataUrl: any): Promise<string> {
expect(typeof maybeDataUrl).toEqual("string");
const dataUrl = maybeDataUrl as string;
expect(dataUrl).toMatch(/^data:image\/png;base64,/);
const buffer = Buffer.from(dataUrl.substr("data:image/png;base64,".length), "base64");
const image = await (Jimp as any).read(buffer);
return new Promise((resolve, reject) => {
const qr = new QrCode();
qr.callback = (error, value) => error ? reject(error) : resolve(value.result);
qr.decode(image.bitmap);
});
}
We'll be creating more of these support functions. Let's export them all from a single file to make importing them easier.
test/support/index.ts
export * from "./expectAndParseQrCode";
Now that we can parse our QR code, we want to assert the parsed result is a valid URL. After a quick Google search, I found a small article about the structure of the URL.
It looks like the scheme must be otpauth://
and the secret is stored in a secret
querystring param. The secret value must be base32 encoded.
The URL host must must be either hotp or totp (totp in our case), followed by the label or name of the account. Example URL:
otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30
Let's finish our test to check whether the URL is looks like an otpauth
url as described by Google's docs.
test/totpSetup.spec.ts (additions)
import { expectAndParseQrCodeDataUrl } from "./support/expectAndParseQrCode";
import { parse } from "url";
test("totpSetup's totpQrCodeUrl is a QR code containing an OTP auth url", (done) => {
const app = express();
app.get("/", totpSetup(), async (req, res, next) => {
try {
const value = await expectAndParseQrCodeDataUrl(res.locals.totpQrCodeUrl);
const url = parse(value, true);
expect(url).toMatchObject({
protocol: "otpauth:",
slashes: true,
host: "totp",
query: { secret: res.locals.totpSecret },
});
res.type("txt").send("Hi");
} catch (error) {
next(error);
}
});
app.use((error, req, res, next) => { done(error); next(error); });
supertest(app).get("/").expect(200).then(() => done(), done);
});
Great. That works.
Refactoring our tests
I really don't like that const app = express(); app.use; supertest(app)
pattern in every test. Let's attempt to extract that inside a support function.
test/support/testMiddleware.ts
import * as express from "express";
import { Request, Response, NextFunction } from "express";
import supertest = require("supertest");
export function testMiddleware(
middleware: (req: Request, res: Response, next: NextFunction) => void,
test: (req: Request, res: Response) => Promise<void>,
): Promise<supertest.Response> {
const app = express();
app.get("/", middleware, async (req, res, next) => {
try {
await test(req, res);
res.type("txt").send("Hi");
} catch (error) {
next(error);
}
});
return new Promise((resolve, reject) => {
app.use((error, req, res, next) => { reject(error); next(error); });
supertest(app).get("/").expect(200).then(resolve, reject);
});
}
Now I can use this new function in my tests that use a similar pattern, rewriting all tests for totpSetup
and totpInit
.
test/totpSetup.spec.ts
import { totpSetup } from "../src/totpSetup";
import { expectAndParseQrCodeDataUrl } from "./support/expectAndParseQrCode";
import { parse } from "url";
import { testMiddleware } from "./support";
test("totpSetup assigns a secret and qr code",
() => testMiddleware(totpSetup(), async (req, res) => {
expect(typeof res.locals.totpSecret).toEqual("string");
expect(typeof res.locals.totpQrCodeUrl).toEqual("string");
}));
test("totpSetup's totpQrCodeUrl is a QR code containing an OTP auth url",
() => testMiddleware(totpSetup(), async (req, res) => {
const value = await expectAndParseQrCodeDataUrl(res.locals.totpQrCodeUrl);
const url = parse(value, true);
expect(url).toMatchObject({
protocol: "otpauth:",
slashes: true,
host: "totp",
query: { secret: res.locals.totpSecret, },
});
}));
test/totpInit.spec.ts
import { totpInit } from "../src/totpInit";
import { testMiddleware } from "./support";
test("totpInit assigns req.totpVerified to false",
() => testMiddleware(totpInit({}), async (req, res) => {
expect((req as any).totpVerified).toEqual(false);
}));
I'm not perfectly sure whether this is an improvement or not. Readability is still pretty poor, but at least the boilerplate is gone.
Handling the setup POST
So far, totpSetup
only included locals for rendering the form. Next, we need to handle submissions. The form submission should include:
- The generated secret included in the QR code
- An example code to verify the user setup their app correctly
Let's call these totpSecret
and totpToken
. We expect these values to be submitted via POST. I want to create a separate middleware for handling the setup POST request, and I'll call it totpSetupSubmit
. I also want a middleware that can be used to handle both the GET (to render the form) and POST (to submit the form). Finally, I want to an isolated middleware for only rendering the form.
By creating 2 middlewares, we keep the responsibilities isolated and the tests simple. By creating a combined middleware, we allow the user to use our library with less code.
First, let's rename our totpSetup
middleware to totpSetupForm
, since it's only responsibility is to populate the form. Using my editor's Replace All function, I replace totpSetup
with totpSetupForm
, and I finish my remame by renaming the files of the test and source.
Next, let's create the tests for totpSetupSubmit
before implementing it. I like to write empty tests with descriptions to define the features of the thing I'll be testing. For totpSetupSubmit
, I want to test the following:
- When submitted with secret and valid token, store the secret using totpInit's
setUserTotpSecret
. - When submitted without secret, add
SECRET_REQUIRED
tototpErrorCodes
. - When submitted without token, add
TOKEN_REQUIRED
tototpErrorCodes
. - When submitted with secret but random token, add
TOKEN_VERIFY_FAILURE
tototpErrorCodes
. - When submitted with secret but old token, add
TOKEN_VERIFY_FAILURE
tototpErrorCodes
.
Let's convert these to tests. I'll with the easy ones: The REQUIRED-errors. Because we cannot use testMiddleware
in it's current state, let's first write our test "the old" way.
test/totpSetupSubmit.spec.ts
import * as express from "express";
import supertest = require("supertest");
import { totpInit } from "../src/totpInit";
import { totpSetupSubmit } from "../src/totpSetupSubmit";
test("totpSetupSubmit() without secret, add SECRET_REQUIRED to totpErrorCodes", async () => {
const app = express();
app.post("/", totpInit({}), totpSetupSubmit(), async (req, res, next) => {
try {
expect(res.locals).toMatchObject({
totpErrorCodes: ["SECRET_REQUIRED"],
});
res.type("txt").send("Hi");
} catch (error) {
next(error);
}
});
return new Promise((resolve, reject) => {
app.use((error, req, res, next) => { reject(error); next(error); });
supertest(app)
.post("/")
.send({ token: "123456", secret: "" })
.expect(200)
.then(resolve, reject);
});
});
Now let's make the test succeed in the laziest way possible, so we can focus on improving our test code.
src/totpSetupSubmit.ts
import { NextFunction, Request, Response } from "express";
export function totpSetupSubmit() {
return (req: Request, res: Response, next: NextFunction) => {
res.locals.totpErrorCodes = ["SECRET_REQUIRED"];
next();
};
}
Great. Now we can figure out how to change testMiddleware
to make it compatible with our POST request. Maybe add an options hash? We also need to be able to include multiple middleware functions, so let's make it accept an array.
To read the POST request body, we'll need body-parser
as well. Let's install it.
npm i -D body-parser @types/body-parser
test/support/testMiddleware.ts
import * as express from "express";
import { Request, Response, NextFunction } from "express";
import supertest = require("supertest");
import bodyParser = require("body-parser");
/**
* Test an express middleware by creating an express app & sending a request to
* the app. The `test`-callback is invoked inside a request handler handling the
* request.
* @param middleware The middleware to add to the request app
* @param test A function to test the request and/or response with inside a
* request handler.
* @param options
*/
export function testMiddleware(
middleware: testMiddleware.Middleware|testMiddleware.Middleware[],
test: (req: Request, res: Response) => Promise<void>,
options: Partial<testMiddleware.Options> = {},
): Promise<supertest.Response> {
const { path, method, data } = { ...testMiddleware.DEFAULT_OPTIONS, ...options };
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(...(Array.isArray(middleware) ? middleware : [middleware]));
// Create a request handler to test the middleware with.
// If the test didn't fail, render a 200 OK response.
app[method](path, async (req, res, next) => {
try {
await test(req, res);
res.type("txt").send("Hi");
} catch (error) {
next(error);
}
});
return new Promise((resolve, reject) => {
// Register an error handler to capture errors in the request handler.
// If an error is caught, reject the promise to indicate a test failure.
// To ensure the express request will finish, we'll also pass the same error
// back to express.
app.use((error, req, res, next) => { reject(error); next(error); });
// Invoke the request handler by sending a request to the express app.
supertest(app)[method](path).send(data).expect(200).then(resolve, reject);
});
}
export namespace testMiddleware {
export type Middleware = (req: Request, res: Response, next: NextFunction) => void;
export const DEFAULT_OPTIONS: Options = {
path: "/",
method: "get",
data: {},
}
export interface Options {
path: string;
method: "get" | "post";
data: string | object;
}
}
Because readability of this function is pretty bad, I did attempt to improve it with some comments describing what is going on.
test/totpSetupSubmit.spec.ts
import { totpInit } from "../src/totpInit";
import { totpSetupSubmit } from "../src/totpSetupSubmit";
import { testMiddleware } from "./support/testMiddleware";
test("totpSetupSubmit() without secret, add SECRET_REQUIRED to totpErrorCodes", async () => {
await testMiddleware([totpInit({}), totpSetupSubmit()], async (req, res) => {
expect(res.locals).toMatchObject({ totpErrorCodes: ["SECRET_REQUIRED"] })
}, { method: "post", data: { token: "123456", secret: "" } });
});
While not the most readable, it is shorter and we don't repeat ourselves in future tests. Let's add the second test. With our second test, we test without code but with secret. I generated a secret to use in my test using the speakeasy
library.
test/totpSetupSubmit.spec.ts (addition)
test("totpSetupSubmit() without token, add TOKEN_REQUIRED to totpErrorCodes", async () => {
await testMiddleware([totpInit({}), totpSetupSubmit()], async (req, res) => {
expect(res.locals).toMatchObject({ totpErrorCodes: ["TOKEN_REQUIRED"] })
}, { method: "post", data: { token: "", secret: "ORRSC22KLVQUWW3HKRKEU6BDPVITSLBSIMSE4S3FPJZFCXRDJJTA" } });
});
To make this test succeed, we're going to need some actual logic in our totpSetupSubmit
middleware. Let's check for the params.
src/totpSetupSubmit.ts
import { NextFunction, Request, Response } from "express";
export function totpSetupSubmit() {
return (req: Request, res: Response, next: NextFunction) => {
const totpErrorCodes: string[] = [];
if (!req.body.token) {
totpErrorCodes.push("TOKEN_REQUIRED");
}
if (!req.body.secret) {
totpErrorCodes.push("SECRET_REQUIRED");
}
res.locals.totpErrorCodes = totpErrorCodes;
next();
};
}
Now let's actually verify the code. First, I'll write the tests for a success case, and 2 failure cases (invalid code and old token).
test/totpSetupSubmit.spec.ts (addition)
const SECRET = speakeasy.generateSecret().base32;
const TOKEN = speakeasy.totp({ encoding: "base32", secret: SECRET });
const OLD_TOKEN = speakeasy.totp({ encoding: "base32", secret: SECRET, time: Date.parse("2000-01-01") / 1000 });
const INVALID_TOKEN = "123456";
test("totpSetupSubmit() with secret but random token, add TOKEN_VERIFY_FAILURE to totpErrorCodes", async () => {
await testMiddleware([totpInit({}), totpSetupSubmit()], async (req, res) => {
expect(res.locals).toMatchObject({ totpErrorCodes: ["TOKEN_VERIFY_FAILURE"] })
}, { method: "post", data: { token: INVALID_TOKEN, secret: SECRET } });
});
test("totpSetupSubmit() with secret but old token, add TOKEN_VERIFY_FAILURE to totpErrorCodes", async () => {
await testMiddleware([totpInit({}), totpSetupSubmit()], async (req, res) => {
expect(res.locals).toMatchObject({ totpErrorCodes: ["TOKEN_VERIFY_FAILURE"] })
}, { method: "post", data: { token: OLD_TOKEN, secret: SECRET } });
});
test("totpSetupSubmit() with secret and valid token, no error is assigned", async () => {
await testMiddleware([totpInit({}), totpSetupSubmit()], async (req, res) => {
expect(res.locals).toMatchObject({ totpErrorCodes: [] })
}, { method: "post", data: { token: TOKEN, secret: SECRET } });
});
And the implementation...
src/totpSetupSubmit.ts
import { NextFunction, Request, Response } from "express";
import * as speakeasy from "speakeasy";
export function totpSetupSubmit() {
return (req: Request, res: Response, next: NextFunction) => {
const totpErrorCodes: string[] = [];
res.locals.totpErrorCodes = totpErrorCodes;
const { token, secret } = req.body;
if (!token) {
totpErrorCodes.push("TOKEN_REQUIRED");
}
if (!secret) {
totpErrorCodes.push("SECRET_REQUIRED");
}
if (totpErrorCodes.length > 0) {
next();
return;
}
const isVerifiedToken = speakeasy.totp.verify({
secret: secret,
encoding: "base32",
token,
window: 2
});
if (!isVerifiedToken) {
totpErrorCodes.push("TOKEN_VERIFY_FAILURE");
}
next();
};
}
Next, on success, I want to provide the user the chance to store the secret. There are multiple ways to implement this. I could assign something like res.locals.totpSetupSuccess=true
, and let the user store the secret in a second middleware, or I could add a callback in the totpSetupSubmit
function that is invoked on success.
Because storing the secret is essential, I think I'll stick with the callback implementation. I'll be adding an options-argument to totpSetupSubmit
containing a callback that is invoked with the secret on any successful setup.
This callback is very likely to perform some I/O to store the secret, so I'll be sure to await
the callback function. I'll call this callback setUserTotpSecret
.
To start, I'll add 2 tests: One testing to ensure setUserTotpSecret
is invoked on success, and one to ensure setUserTotpSecret
is NOT invoked on failure.
test/totpSetupSubmit.spec.ts (partial)
test("totpSetupSubmit() invokes setUserTotpSecret on success", async () => {
const setUserTotpSecret = jest.fn();
let request: Request;
await testMiddleware(
[
totpInit({}),
totpSetupSubmit({ setUserTotpSecret }),
],
async (req, res) => { request = req; },
{ method: "post", data: { token: TOKEN, secret: SECRET } },
);
expect(setUserTotpSecret).toHaveBeenCalledWith(request, SECRET);
});
test("totpSetupSubmit() does not invoke setUserTotpSecret on failure", async () => {
const setUserTotpSecret = jest.fn();
await testMiddleware(
[
totpInit({}),
totpSetupSubmit({ setUserTotpSecret }),
],
async (req, res) => {},
{ method: "post", data: { token: INVALID_TOKEN, secret: SECRET } },
);
expect(setUserTotpSecret).not.toHaveBeenCalled();
});
I'm still struggling to make the tests more readable. I've added some whitespace, maybe that helps?
src/totpSetupSubmit.ts (partial)
export function totpSetupSubmit(options: Partial<totpSetupSubmit.Options> = {}) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { setUserTotpSecret } = { ...totpSetupSubmit.DEFAULT_OPTIONS, ...options };
// ...
if (!isVerifiedToken) {
totpErrorCodes.push("TOKEN_VERIFY_FAILURE");
next();
return;
}
await setUserTotpSecret(req, secret);
next();
} catch (error) {
next(error);
}
};
}
export namespace totpSetupSubmit {
export interface Options {
setUserTotpSecret(req: Request, secret: string): Promise<void>|void;
}
export const DEFAULT_OPTIONS: Options = {
setUserTotpSecret: () => {},
};
}
If you want to use await
inside a request handler, you'll have to capture the error manually: express ignores the returned promise. I've added try { } catch (error) { next(error); }
to catch any async error and pass it through the next middleware.
To allow package users to either render a success or failure response, we'll also need to assign the result of the setup in either the request or response. My plan is to set the following properties:
-
totpSetupSuccess
: Was the token verified and was the secret stored? -
totpSecret
: The BASE32 secret -
totpVerified
: Will be set to true ontotpSetupSuccess
.
I'll assign these both to req
(with type-definition, for usage in other middleware) and res.locals
(without type-definition, for usage in views).
Test & implementation below.
test/totpSetupSubmit.spec.ts (partial)
test("totpSetupSubmit() on success assigns res.locals. & req. totpVerified & totpSetupSuccess=true", () => testMiddleware(
totpSetupSubmit({}),
async (req, res) => {
expect(res.locals.totpSetupSuccess).toEqual(true);
expect(req.totpSetupSuccess).toEqual(true);
expect(res.locals.totpVerified).toEqual(true);
expect(req.totpVerified).toEqual(true);
},
{ method: "post", data: { token: TOKEN, secret: SECRET } },
));
test("totpSetupSubmit() assigns res.locals. & req. totpVerified & totpSetupSuccess=false", () => testMiddleware(
totpSetupSubmit({}),
async (req, res) => {
expect(res.locals.totpSetupSuccess).toEqual(false);
expect(req.totpSetupSuccess).toEqual(false);
expect(res.locals.totpVerified).toEqual(false);
expect(req.totpVerified).toEqual(false);
},
{ method: "post", data: { token: INVALID_TOKEN, secret: SECRET } },
));
test("totpSetupSubmit() assigns res.locals. & req.totpSecret", () => testMiddleware(
totpSetupSubmit({}),
async (req, res) => {
expect(res.locals.totpSecret).toEqual(SECRET);
expect(req.totpSecret).toEqual(SECRET);
},
{ method: "post", data: { token: TOKEN, secret: SECRET } },
));
src/totpSetupSubmit.ts (partial)
export function totpSetupSubmit(options: Partial<totpSetupSubmit.Options> = {}) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
// ...
const totpErrorCodes: string[] = [];
res.locals.totpErrorCodes = totpErrorCodes;
res.locals.totpVerified = req.totpVerified = false;
res.locals.totpSetupSuccess = req.totpSetupSuccess = false;
// ...
if (!isVerifiedToken) {
totpErrorCodes.push("TOKEN_VERIFY_FAILURE");
next();
return;
}
await setUserTotpSecret(req, secret);
res.locals.totpVerified = req.totpVerified = true;
res.locals.totpSetupSuccess = req.totpSetupSuccess = true;
next();
} catch(error) {
// ...
}
};
}
The middleware is now usable. I'll add some nice-to-haves later.
Step 3: Verifying the token
Now let's focus on creating a middleware to verify the token. For setup, we had 2 middlewares: One for rendering the form, and one for submitting the form. For verification, we don't need any data to render a form. We only need to be able to submit it, hence we're only creating a middleware for submitting the form.
I'll call this middleware totpVerifySubmit
.
To verify the token being submitted, we'll be needing a previously stored secret and a token from the request body. We'll ask the package user to provide the current user's secret using a getUserTotpSecret
callback in an options hash. The token will be extracted from a param named token
.
Let's start off with tests.
test/totpVerifySubmit.spec.ts
import { testMiddleware } from "./support/testMiddleware";
import * as speakeasy from "speakeasy";
import { totpVerifySubmit } from "../src/totpVerifySubmit";
const SECRET = speakeasy.generateSecret().base32;
const TOKEN = speakeasy.totp({ encoding: "base32", secret: SECRET });
const OLD_TOKEN = speakeasy.totp({ encoding: "base32", secret: SECRET, time: Date.parse("2000-01-01") / 1000 });
const INVALID_TOKEN = "123456";
const getUserTotpSecret = () => Promise.resolve(SECRET);
it("totpVerifySubmit() without token assigns TOKEN_REQUIRED to totpErrorCodes", () => testMiddleware(
totpVerifySubmit({ getUserTotpSecret }),
async (req, res) => {
expect(res.locals).toMatchObject({ totpErrorCodes: ["TOKEN_REQUIRED"] });
expect(req.totpVerified).toEqual(false);
},
{ method: "post", data: { token: "" } }
));
it("totpVerifySubmit() with null-secret from getUserTotpSecret assigns SECRET_REQUIRED to totpErrorCodes", () => testMiddleware(
totpVerifySubmit({ getUserTotpSecret: () => null }),
async (req, res) => {
expect(res.locals).toMatchObject({ totpErrorCodes: ["SECRET_REQUIRED"] });
expect(req.totpVerified).toEqual(false);
},
{ method: "post", data: { token: TOKEN } }
));
it("totpVerifySubmit() with blank-secret from getUserTotpSecretassigns SECRET_REQUIRED to totpErrorCodes", () => testMiddleware(
totpVerifySubmit({ getUserTotpSecret: () => "" }),
async (req, res) => {
expect(res.locals).toMatchObject({ totpErrorCodes: ["SECRET_REQUIRED"] });
expect(req.totpVerified).toEqual(false);
},
{ method: "post", data: { token: TOKEN } }
));
it("totpVerifySubmit() with valid secret and token assigns totpVerified=true", () => testMiddleware(
totpVerifySubmit({ getUserTotpSecret }),
async (req, res) => {
expect(res.locals).toMatchObject({ totpErrorCodes: [] });
expect(req.totpVerified).toEqual(true);
},
{ method: "post", data: { token: TOKEN } }
));
it("totpVerifySubmit() with random token token assigns TOKEN_VERIFY_FAILURE to totpErrorCodes", () => testMiddleware(
totpVerifySubmit({ getUserTotpSecret }),
async (req, res) => {
expect(res.locals).toMatchObject({ totpErrorCodes: ["TOKEN_VERIFY_FAILURE"] });
expect(req.totpVerified).toEqual(false);
},
{ method: "post", data: { token: INVALID_TOKEN } }
));
it("totpVerifySubmit() with old token token assigns TOKEN_VERIFY_FAILURE to totpErrorCodes", () => testMiddleware(
totpVerifySubmit({ getUserTotpSecret }),
async (req, res) => {
expect(res.locals).toMatchObject({ totpErrorCodes: ["TOKEN_VERIFY_FAILURE"] });
expect(req.totpVerified).toEqual(false);
},
{ method: "post", data: { token: OLD_TOKEN } }
));
Now let's implement totpVerifySubmit
and make the tests succeed.
src/totpVerifySubmit.ts
import { NextFunction, Request, Response } from "express";
import * as speakeasy from "speakeasy";
export function totpVerifySubmit(options: Partial<totpVerifySubmit.Options> = {}) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { getUserTotpSecret } = { ...totpVerifySubmit.DEFAULT_OPTIONS, ...options };
const totpErrorCodes: string[] = [];
res.locals.totpErrorCodes = totpErrorCodes;
res.locals.totpVerified = req.totpVerified = false;
const { token } = req.body;
const secret = await getUserTotpSecret(req);
if (!token) {
totpErrorCodes.push("TOKEN_REQUIRED");
}
if (!secret) {
totpErrorCodes.push("SECRET_REQUIRED");
}
if (totpErrorCodes.length > 0) {
next();
return;
}
const isVerifiedToken = speakeasy.totp.verify({
secret,
encoding: "base32",
token,
window: 2,
});
if (!isVerifiedToken) {
totpErrorCodes.push("TOKEN_VERIFY_FAILURE");
next();
return;
}
res.locals.totpVerified = req.totpVerified = true;
next();
} catch (error) {
next(error);
}
};
}
export namespace totpVerifySubmit {
export interface Options {
getUserTotpSecret(req: Request): Promise<string>|string;
}
export const DEFAULT_OPTIONS: Options = {
getUserTotpSecret: () => { throw new Error(`getUserTotpSecret not implemented`); },
};
}
Now both the tests and the implementations for totpSetupSubmit
and totpVerifySubmit
are pretty similar. Let's see if we can extract some duplicate code.
Both middlewares assign totpVerified
and totpErrorCodes
, and both middlewares verify a token with a secret. Both extract options and have that try { } catch(error) { }
pattern.
Because I'm writing a package with express middlewares, maybe I can extract the token verification part to its own middleware, but without the specifics of extracting the token or secret. This new middleware should be able to be used by both totpSetupSubmit
and totpVerifySubmit
.
To achieve this separation, both middlewares must first extract their secret and token in a way our new middleware can access it: Let's store it in req
!
src/types.d.ts (partial)
declare namespace Express {
export interface Request {
// ...
totpToken?: string;
totpErrorCodes?: string[];
}
}
Now let's move some stuff around: Let's create our new middleware. Because we're only refactoring our code without changing functionality, writing new tests isn't necessary. Instead, let's rely on the tests to verify our refactoring is done correctly.
While refactoring, I'll keep watch of npm t -- --watchAll
, running all tests on every change.
Also, to make it easier to track our middleware's state, instead of using a list of variables all prefixed totp
, I think it's better to use a single variable (both on req
and res.locals
) called totp
, and store our package request-state in there.
src/TotpState.ts
export interface TotpState {
verified?: boolean;
secret?: string;
setupSuccess?: boolean;
token?: string;
errorCodes?: string[];
}
Because sometimes our changes to Request
weren't picked up, I've added them to both "express".Request
and Express.Request
.
src/types.d.ts
import { TotpState } from "./TotpState";
declare namespace Express {
export interface Request {
totp?: TotpState;
}
}
declare module "express" {
export interface Request {
totp?: TotpState;
}
}
Now refactor our assignments from req.totp*
to req.totp.*
, practially rewriting all tests and implementations.
To avoid having to ensure totp
is set on req
and res.locals
, I'll ensure it's done once in totpInit
. The changes from req.totpVerified
to req.totp.verified
(and all other assignments) are not included in this post, because they involve almost all files while the change is trivial.
src/totpInit.ts
import { NextFunction, Request, Response } from "express";
export function totpInit(options: any) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.totp) {
res.locals.totp = req.totp = {};
}
req.totp.verified = false;
next();
};
}
With this change in totpInit
, for any other middleware, I'll assume req.totp
is always equal to res.locals.totp
, and they are always set. To ensure totpInit
is always invoked before my other middleware, I'll always add totpInit
in all middleware tests.
test/totpSetupSubmit.spec.ts (partial)
test("totpSetupSubmit() without secret, add SECRET_REQUIRED to totpErrorCodes", () => testMiddleware(
[totpInit({}), totpSetupSubmit()],
async (req, res) => expect(req.totp).toMatchObject({ errorCodes: ["SECRET_REQUIRED"] }),
{ method: "post", data: { token: TOKEN, secret: "" } })
);
Now let's implement totpVerify
and change totpVerifySubmit
and totpSetupSubmit
to be used with totpVerify
.
src/totpVerify.ts
import { Request, Handler } from "express";
import * as speakeasy from "speakeasy";
import "./types";
export function totpVerify(): Handler {
return (req: Request, res, next) => {
const errorCodes = req.totp.errorCodes = [] as string[];
const { token, secret } = req.totp;
if (!token) {
errorCodes.push("TOKEN_REQUIRED");
}
if (!secret) {
errorCodes.push("SECRET_REQUIRED");
}
if (errorCodes.length > 0) {
next();
return;
}
req.totp.verified = speakeasy.totp.verify({
secret,
encoding: "base32",
token,
window: 2,
});
if (!req.totp.verified) {
errorCodes.push("TOKEN_VERIFY_FAILURE");
}
next();
}
}
Now I want to change totpVerifySubmit
to only extract the secret and token, and use totpVerify
to verify the token. For this, I want to change totpVerifySubmit
to combine 2 middlewares: A middleware for extracting secret (from getUserTotpSecret
) and token (from body
), and the new totpVerify
middleware.
Express can be used to combine multiple middlewares into a single middleware.
src/totpVerifySubmit.ts (partial)
import * as express from "express";
import { NextFunction, Request, Response } from "express";
import { totpVerify } from "./totpVerify";
import "./types";
export function totpVerifySubmit(options: Partial<totpVerifySubmit.Options> = {}) {
const app = express();
app.use(totpVerifySubmit.assignSecretAndTokenForVerification(options));
app.use(totpVerify());
return app;
}
export namespace totpVerifySubmit {
export function assignSecretAndTokenForVerification(options: Partial<Options> = {}) {
const { getUserTotpSecret } = { ...DEFAULT_OPTIONS, ...options };
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { token } = req.body;
const secret = await getUserTotpSecret(req);
req.totp.secret = secret;
req.totp.token = token;
next();
} catch (error) {
next(error);
}
}
}
// ...
}
src/totpSetupSubmit.ts (partial)
import * as express from "express";
import { NextFunction, Request, Response } from "express";
import "./types";
import { totpVerify } from "./totpVerify";
export function totpSetupSubmit(options: Partial<totpSetupSubmit.Options> = {}) {
const app = express();
app.use(totpSetupSubmit.extractTokenAndSecretForSubmission);
app.use(totpVerify());
app.use(totpSetupSubmit.completeSubmitAfterVerification(options));
return app;
}
export namespace totpSetupSubmit {
export function extractTokenAndSecretForSubmission(req: Request, res: Response, next: NextFunction) {
const { token, secret } = req.body;
req.totp.secret = secret;
req.totp.token = token;
next();
}
export function completeSubmitAfterVerification(options: Partial<Options> = {}) {
const { setUserTotpSecret } = { ...DEFAULT_OPTIONS, ...options };
return async (req: Request, res: Response, next: NextFunction) => {
try {
req.totp.setupSuccess = req.totp.verified;
if (req.totp.setupSuccess) {
await setUserTotpSecret(req, req.totp.secret!);
}
next();
} catch (error) {
next(error);
}
}
}
// ...
}
Both verify and setup still work, but the code is a bit cleaner in my opinion.
Now let's clean index.ts
and export our middlewares.
src/index.ts
export * from "./ITotpState";
export * from "./totpInit";
export * from "./totpSetupForm";
export * from "./totpSetupSubmit";
export * from "./totpVerify";
export * from "./totpVerifySubmit";
Now, let's also test whether all middlewares are actually exported. That way, we can be sure our package users can actually use our middlewares.
test/index.spec.ts
import * as src from "../src";
for (const name of ["totpInit", "totpSetupForm", "totpSetupSubmit", "totpVerify", "totpVerifySubmit"]) {
test(`middleware ${name}() is exported and a function`, () => {
expect(src).toHaveProperty(name);
expect(typeof src[name]).toEqual("function");
});
}
Recap
To recap, we now have the following exports:
import { Handler } from "express";
/**
* An interface representing the current TOTP state, present on every request.
*/
interface ITotpState {
verified?: boolean;
secret?: string;
setupSuccess?: boolean;
token?: string;
qrCodeUrl?: string;
errorCodes?: string[];
}
/**
* Assign a blank ITotpState on `req.totp` and `res.locals.totp`.
*/
function totpInit(options?: any): Handler;
/**
* Generate a new secret and QR code and assign them to `totp.secret` and
* `totp.qrCodeUrl`.
*/
function totpSetupForm(): Handler;
/**
* Extract the secret and token from the request body, verify the token against
* the secret, and store the secret when verification was successful using
* `setUserTotpSecret` in `options`.
*/
function totpSetupSubmit(options: Partial<totpSetupSubmit.IOptions>): Handler;
/**
* Verify `totp.token` against `totp.secret` and assign `totp.verified` with
* `true` on success, or `totp.errorCodes` with error codes on failure.
*/
function totpVerify(): Handler;
/**
* Extract totp token from request body and verify it against a secret fetched
* from `getUserTotpSecret` in `options`.
*
* Assign `totp.verified` with `true` on success, or `totp.errorCodes` with
* error codes on failure.
*/
function totpVerifySubmit(): Handler;
Let's deploy it to NPM and test our package. I already wrote about publishing packages to NPM here, so I'll keep it short.
Deploying to NPM
tsconfig.json (partial)
{
"compilerOptions": {
// ...
"declaration": true
},
}
package.json (partial)
{
"main": "dist/index.js",
"files": ["dist"],
"scripts": {
"prepublishOnly": "npm run ci",
// ...
}
// ...
}
$ npm publish --dry-run
> bonaroo-totp-express@1.0.0 prepublishOnly .
> npm run ci
> bonaroo-totp-express@1.0.0 ci /home/toby/bonaroo-totp-express
> npm run lint & npm run build & npm t & wait
> bonaroo-totp-express@1.0.0 lint /home/toby/bonaroo-totp-express
> tslint -c tslint.json -p tsconfig.json
> bonaroo-totp-express@1.0.0 build /home/toby/bonaroo-totp-express
> tsc
> bonaroo-totp-express@1.0.0 test /home/toby/bonaroo-totp-express
> jest
PASS test/index.spec.ts
PASS test/totpVerifySubmit.spec.ts
PASS test/totpSetupSubmit.spec.ts
PASS test/totpInit.spec.ts
PASS test/totpSetupForm.spec.ts
Test Suites: 5 passed, 5 total
Tests: 24 passed, 24 total
Snapshots: 0 total
Time: 2.056s
Ran all test suites.
npm notice
npm notice 📦 bonaroo-totp-express@1.0.0
npm notice === Tarball Contents ===
npm notice 358B dist/index.js
npm notice 79B dist/ITotpState.js
npm notice 388B dist/totpInit.js
npm notice 497B dist/totpSetup.js
npm notice 604B dist/totpSetupForm.js
npm notice 5.4kB dist/totpSetupSubmit.js
npm notice 1.0kB dist/totpVerify.js
npm notice 5.1kB dist/totpVerifySubmit.js
npm notice 1.3kB package.json
npm notice 170B dist/index.js.map
npm notice 112B dist/ITotpState.js.map
npm notice 396B dist/totpInit.js.map
npm notice 468B dist/totpSetup.js.map
npm notice 487B dist/totpSetupForm.js.map
npm notice 1.3kB dist/totpSetupSubmit.js.map
npm notice 842B dist/totpVerify.js.map
npm notice 1.1kB dist/totpVerifySubmit.js.map
npm notice 23B README.md
npm notice 192B dist/index.d.ts
npm notice 897B dist/ITotpState.d.ts
npm notice 167B dist/totpInit.d.ts
npm notice 188B dist/totpSetupForm.d.ts
npm notice 761B dist/totpSetupSubmit.d.ts
npm notice 257B dist/totpVerify.d.ts
npm notice 663B dist/totpVerifySubmit.d.ts
npm notice === Tarball Details ===
npm notice name: bonaroo-totp-express
npm notice version: 1.0.0
npm notice package size: 5.8 kB
npm notice unpacked size: 22.9 kB
npm notice shasum: ea5fd4c4b0a1f77b514a2cd1bcf0fe912fd42df1
npm notice integrity: sha512-o/I6Tfd19PP4B[...]FtW7UUZ3EGODQ==
npm notice total files: 25
npm notice
+ bonaroo-totp-express@1.0.0
LGTM.
npm publish
Thank you for reading! And if you want to know more or have any other questions, please get in touch with us via info@bonaroo.nl.
In my next blog post, I'll write an example project using the middlewares from my new package. After the example project, I'll add some default views and routes in the package to make it as easy as possible to integrate the package in express applications.
Top comments (0)