mirror of
https://github.com/chenasraf/express-otp.git
synced 2026-05-17 17:48:11 +00:00
refactor: cleanup
This commit is contained in:
@@ -19,11 +19,23 @@ const totp = otipi<typeof sampleUser>({
|
||||
},
|
||||
})
|
||||
|
||||
app.use('/generate', totp.createToken())
|
||||
|
||||
app.use('/token/uri', totp.generateTokenURL())
|
||||
|
||||
app.use('/token/qr', totp.generateTokenQR())
|
||||
app.use('/generate', (req, res) => res.status(200).send(totp.generateNewSecret()))
|
||||
app.use('/token/uri', async (req, res) =>
|
||||
res
|
||||
.status(200)
|
||||
.setHeader('Content-Type', 'text/plain')
|
||||
.send(await totp.generateSecretURL(sampleUser.username, sampleUser.secret)),
|
||||
)
|
||||
app.use('/token/qr', async (req, res) =>
|
||||
res
|
||||
.status(200)
|
||||
.setHeader('Content-Type', 'text/html')
|
||||
.send(
|
||||
'<img src="' +
|
||||
(await totp.generateSecretQR(sampleUser.username, sampleUser.secret)) +
|
||||
'" style="width: 100%; height: 100%; object-fit: contain; image-rendering: pixelated;" />',
|
||||
),
|
||||
)
|
||||
|
||||
app.use(totp.authenticate())
|
||||
|
||||
|
||||
198
src/index.ts
198
src/index.ts
@@ -5,6 +5,9 @@ import _totp from 'totp-generator'
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
export interface TotpOptions {
|
||||
/** The issuer for your app (required) */
|
||||
issuer: string
|
||||
/** The time it takes for a new token to generate, in seconds. Defaults to 30 */
|
||||
period?: number | undefined
|
||||
/**
|
||||
* The desired SHA variant (SHA-1, SHA-224, SHA-256, SHA-384, SHA-512,
|
||||
@@ -22,38 +25,121 @@ export interface TotpOptions {
|
||||
| 'SHA3-512'
|
||||
| 'SHAKE128'
|
||||
| 'SHAKE256'
|
||||
/** Amount of token digits to use. Defaults to 6 */
|
||||
digits?: number | undefined
|
||||
/** The epoch time. Defaults to 0 (unix epoch) */
|
||||
timestamp?: number | undefined
|
||||
}
|
||||
|
||||
export interface UserData<U> {
|
||||
/** The user object that will get injected into further requests. */
|
||||
user: U
|
||||
/** The secret key of the user, used for generating a comparison key. */
|
||||
secret: string
|
||||
/** The username used for generating the token URL/QR. */
|
||||
username: string
|
||||
}
|
||||
|
||||
type PromiseOrValue<T> = T | Promise<T>
|
||||
|
||||
export interface TotpApiOptions<U> {
|
||||
issuer: string
|
||||
/**
|
||||
* If the return value is not `undefined`, it uses this function to verify and then inject the correct user into further
|
||||
* requests in a middleware (usually before request processing).
|
||||
*
|
||||
* This is where you would decide which users this request belongs to, via whatever method you want - going into DB,
|
||||
* checking headers, etc.
|
||||
*
|
||||
* - The `user` property of the return value will be injected into further requests.
|
||||
* - The `secret` property of the return value will be used to generate a comparison key.
|
||||
* - The `username` property of the return value will be used to generate the token URL/QR.
|
||||
*
|
||||
* Returning `undefined` will cause the request to continue as normal, without any user injected.
|
||||
*
|
||||
* It is up to you to return an error in the actual request if necessary.
|
||||
*
|
||||
* @param {Request} req The request object.
|
||||
*/
|
||||
getUser(req: Request): PromiseOrValue<UserData<U> | undefined>
|
||||
getToken?(req: Request): PromiseOrValue<string>
|
||||
|
||||
/**
|
||||
* This function should return the token from the request (e.g. from a header, or from a query parameter).
|
||||
*
|
||||
* @param {Request} req The request object.
|
||||
* @returns {string | undefined} The token from the request, or `undefined` if it doesn't exist.
|
||||
*/
|
||||
getToken?(req: Request): PromiseOrValue<string | undefined>
|
||||
}
|
||||
|
||||
export interface TotpMiddlewares {
|
||||
authenticate: () => (req: Request, res: Response, next: () => void) => Promise<void>
|
||||
generateTokenURL: () => (req: Request, res: Response, next: () => void) => Promise<void>
|
||||
generateTokenQR: () => (req: Request, res: Response, next: () => void) => Promise<void>
|
||||
createToken: () => (req: Request, res: Response, next: () => void) => Promise<void>
|
||||
/**
|
||||
* Middleware for authenticating a user, using their secret and the token provided in the request.
|
||||
*
|
||||
* Use `getUser` in the options to control which user gets used for comparing the token to and later injected into
|
||||
* further requests.
|
||||
*
|
||||
* Use `getToken` in the options to control how the token is fetched in the request (query, headers, etc).
|
||||
*/
|
||||
authenticate(): (req: Request, res: Response, next: () => void) => Promise<void>
|
||||
|
||||
/**
|
||||
* Function for generating a secret URL for a user from a given `secret` and `username`.
|
||||
*
|
||||
* @param {string} username The username to use for generating the URL.
|
||||
* @param {string} secret The secret to use for generating the URL.
|
||||
*
|
||||
* @returns {string} The URL for the user.
|
||||
*/
|
||||
generateSecretURL(username: string, secret: string): Promise<string>
|
||||
|
||||
/**
|
||||
* Function for generating a QR code for a user from a given `secret` and `username`.
|
||||
*
|
||||
* This returns a PNG image as a data URL.
|
||||
*
|
||||
* @param {string} username The username to use for generating the URL.
|
||||
* @param {string} secret The secret to use for generating the URL.
|
||||
*
|
||||
* @returns {Promise<string>} The QR code as a data URL.
|
||||
*/
|
||||
generateSecretQR(username: string, secret: string): Promise<string>
|
||||
|
||||
/**
|
||||
* Function for generating a QR code for a user from a given `secret` and `username`.
|
||||
*
|
||||
* This writes the QR code directly to a file, which you can later use to serve to the user.
|
||||
*
|
||||
* @param {string} username The username to use for generating the URL.
|
||||
* @param {string} secret The secret to use for generating the URL.
|
||||
* @param {string} filename The path of the file to write the QR image to.
|
||||
*/
|
||||
generateSecretQR(username: string, secret: string, filename: string): Promise<void>
|
||||
|
||||
/**
|
||||
* Function for generating a QR code for a user from a given `secret` and `username`.
|
||||
*
|
||||
* If `filename` is provided, this writes the QR code directly to that path, which you can later use to serve to the user.
|
||||
* If `filename` is omitted (or blank), this returns a PNG image as a data URL.
|
||||
*
|
||||
* @param {string} username The username to use for generating the URL.
|
||||
* @param {string} secret The secret to use for generating the URL.
|
||||
* @param {string} filename If provided, will use as path of the file to write the QR image to.
|
||||
*/
|
||||
generateSecretQR(username: string, secret: string, filename?: string): Promise<string | void>
|
||||
|
||||
/**
|
||||
* Generates a random, 32-byte secret key. You can attach this to your user object or DB however you want.
|
||||
*/
|
||||
generateNewSecret(): Promise<string>
|
||||
}
|
||||
|
||||
export function base32secret(length = 32) {
|
||||
function base32secret(length = 32) {
|
||||
return encode(crypto.randomBytes(32)).slice(0, length)
|
||||
}
|
||||
|
||||
export type OTPUriParams = Record<'secret' | 'issuer' | 'username', string>
|
||||
type OTPUriParams = Record<'secret' | 'issuer' | 'username', string>
|
||||
|
||||
export function generateTokenUri({ secret, issuer, username }: OTPUriParams) {
|
||||
function generateTokenUri({ secret, issuer, username }: OTPUriParams) {
|
||||
const uri = new URL('otpauth://totp/')
|
||||
uri.searchParams.set('secret', secret)
|
||||
uri.searchParams.set('issuer', issuer)
|
||||
@@ -66,14 +152,26 @@ export function generateTokenUri({ secret, issuer, username }: OTPUriParams) {
|
||||
return uri.toString()
|
||||
}
|
||||
|
||||
export function generateQR(uri: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
QR.toDataURL(uri, (err, uri) => {
|
||||
function generateQR(uri: string, filename?: string): Promise<string> | Promise<void> {
|
||||
if (!filename) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
QR.toDataURL(uri, (err, uri) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(uri)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
QR.toFile(filename, uri, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(uri)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -91,7 +189,7 @@ export default function totp<U>(_options: TotpOptions & TotpApiOptions<U>): Totp
|
||||
..._options,
|
||||
} as Required<TotpOptions & TotpApiOptions<U>>
|
||||
|
||||
async function authenticate(req: Request, res: Response, next: () => void) {
|
||||
async function authenticate(req: Request, res: Response, next: () => void): Promise<void> {
|
||||
const resp = await options.getUser(req)
|
||||
if (!resp) {
|
||||
res.status(401)
|
||||
@@ -116,72 +214,36 @@ export default function totp<U>(_options: TotpOptions & TotpApiOptions<U>): Totp
|
||||
next()
|
||||
}
|
||||
|
||||
async function generateTokenURL(req: Request, res: Response) {
|
||||
const resp = await options.getUser(req)
|
||||
if (!resp) {
|
||||
res.status(401)
|
||||
res.send('Unauthorized')
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
const { username, secret } = resp
|
||||
|
||||
if (!secret || !username) {
|
||||
res.status(400)
|
||||
res.send('Invalid secret or username')
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
async function generateSecretURL(username: string, secret: string): Promise<string> {
|
||||
return generateTokenUri({
|
||||
secret,
|
||||
issuer: options.issuer,
|
||||
username,
|
||||
})
|
||||
}
|
||||
|
||||
async function generateSecretQR(
|
||||
username: string,
|
||||
secret: string,
|
||||
filename?: string,
|
||||
): Promise<never> {
|
||||
const uri = generateTokenUri({
|
||||
secret,
|
||||
issuer: options.issuer,
|
||||
username,
|
||||
})
|
||||
|
||||
res.setHeader('Content-Type', 'text/plain')
|
||||
res.status(200)
|
||||
res.send(uri)
|
||||
return generateQR(uri, filename) as Promise<never>
|
||||
}
|
||||
|
||||
async function generateTokenQR(req: Request, res: Response) {
|
||||
const resp = await options.getUser(req)
|
||||
if (!resp) {
|
||||
res.status(401)
|
||||
res.send('Unauthorized')
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
const { username, secret } = resp
|
||||
|
||||
if (!secret || !username) {
|
||||
res.status(400)
|
||||
res.send('Invalid secret or username')
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
const uri = generateTokenUri({
|
||||
secret,
|
||||
issuer: options.issuer,
|
||||
username,
|
||||
})
|
||||
|
||||
res.setHeader('Content-Type', 'text/plain')
|
||||
res.status(200)
|
||||
res.send(await generateQR(uri))
|
||||
}
|
||||
|
||||
async function createToken(req: Request, res: Response) {
|
||||
res.setHeader('Content-Type', 'text/plain')
|
||||
res.status(200)
|
||||
res.send(base32secret())
|
||||
async function generateNewSecret(): Promise<string> {
|
||||
return base32secret()
|
||||
}
|
||||
|
||||
return {
|
||||
authenticate: () => authenticate,
|
||||
generateTokenURL: () => generateTokenURL,
|
||||
generateTokenQR: () => generateTokenQR,
|
||||
createToken: () => createToken,
|
||||
generateSecretURL,
|
||||
generateSecretQR,
|
||||
generateNewSecret,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user