Add validateCaptcha function to @rondo.dev/captcha
This commit is contained in:
parent
b2620e8050
commit
a9f37103bd
28
packages/captcha/src/Captcha.ts
Normal file
28
packages/captcha/src/Captcha.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export type CaptchaType = 'image' | 'audio'
|
||||||
|
|
||||||
|
export interface Captcha {
|
||||||
|
type: CaptchaType
|
||||||
|
value: string
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
namespace Express {
|
||||||
|
interface Session {
|
||||||
|
captcha?: Captcha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCaptcha(
|
||||||
|
value: string,
|
||||||
|
type: CaptchaType,
|
||||||
|
): Captcha {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
type,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -67,7 +67,11 @@ describe('audio', () => {
|
|||||||
.set('cookie', cookie)
|
.set('cookie', cookie)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res2.body.captcha).toEqual(jasmine.any(String))
|
expect(res2.body.captcha).toEqual({
|
||||||
|
value: jasmine.any(String),
|
||||||
|
type: 'audio',
|
||||||
|
timestamp: jasmine.any(Number),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('fails with error 500 when unable to generate', async () => {
|
it('fails with error 500 when unable to generate', async () => {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Request, Response } from 'express'
|
|||||||
import SVGCaptcha from 'svg-captcha'
|
import SVGCaptcha from 'svg-captcha'
|
||||||
import { Command, ReadableProcess, ReadableWritable, run } from './run'
|
import { Command, ReadableProcess, ReadableWritable, run } from './run'
|
||||||
import { TextStream } from './TextStream'
|
import { TextStream } from './TextStream'
|
||||||
|
import { createCaptcha } from './Captcha'
|
||||||
|
|
||||||
export interface AudioConfig {
|
export interface AudioConfig {
|
||||||
commands: Command[]
|
commands: Command[]
|
||||||
@ -14,7 +15,7 @@ export const audio = (config: AudioConfig) => async (
|
|||||||
) => {
|
) => {
|
||||||
const { commands, size } = config
|
const { commands, size } = config
|
||||||
const captcha = SVGCaptcha.randomText(size)
|
const captcha = SVGCaptcha.randomText(size)
|
||||||
req.session!.captcha = captcha
|
req.session!.captcha = createCaptcha(captcha, 'audio')
|
||||||
let speech: ReadableProcess
|
let speech: ReadableProcess
|
||||||
try {
|
try {
|
||||||
speech = await speak('test', commands)
|
speech = await speak('test', commands)
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
|
import bodyParser from 'body-parser'
|
||||||
|
import cookieParser from 'cookie-parser'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import session from 'express-session'
|
import session from 'express-session'
|
||||||
import cookieParser from 'cookie-parser'
|
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
|
import { Captcha } from './Captcha'
|
||||||
import { image } from './image'
|
import { image } from './image'
|
||||||
|
import { validateCaptcha } from './validateCaptcha'
|
||||||
|
|
||||||
describe('image', () => {
|
describe('image', () => {
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use(cookieParser())
|
app.use(cookieParser())
|
||||||
|
app.use(bodyParser.json())
|
||||||
app.use(session({
|
app.use(session({
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
resave: false,
|
resave: false,
|
||||||
@ -17,22 +21,92 @@ describe('image', () => {
|
|||||||
app.get('/captcha/session', (req, res) => {
|
app.get('/captcha/session', (req, res) => {
|
||||||
res.json(req.session)
|
res.json(req.session)
|
||||||
})
|
})
|
||||||
|
app.post('/captcha/validate', validateCaptcha(), (req, res) => {
|
||||||
|
res.send('OK')
|
||||||
|
})
|
||||||
|
app.post('/captcha/validate/expired', validateCaptcha({
|
||||||
|
property: 'c',
|
||||||
|
ttl: -1,
|
||||||
|
}), (req, res) => {
|
||||||
|
res.send('OK')
|
||||||
|
})
|
||||||
|
|
||||||
|
async function getImage() {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/captcha/image')
|
||||||
|
.expect('Content-type', /svg/)
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
return res.header['set-cookie'][0]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCaptcha() {
|
||||||
|
const cookie = await getImage()
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/captcha/session')
|
||||||
|
.set('cookie', cookie)
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
return {captcha: res.body.captcha as Captcha, cookie}
|
||||||
|
}
|
||||||
|
|
||||||
describe('/captcha/image', () => {
|
describe('/captcha/image', () => {
|
||||||
it('generates a new captcha', async () => {
|
it('generates a new captcha', async () => {
|
||||||
const res1 = await request(app)
|
const { captcha } = await getCaptcha()
|
||||||
.get('/captcha/image')
|
expect(captcha).toEqual({
|
||||||
.expect('Content-type', /svg/)
|
value: jasmine.any(String),
|
||||||
.expect(200)
|
type: 'image',
|
||||||
|
timestamp: jasmine.any(Number),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const cookie = res1.header['set-cookie'][0]
|
describe('/captcha/validate', () => {
|
||||||
|
it('fails when no captcha in session', async () => {
|
||||||
const res2 = await request(app)
|
await request(app)
|
||||||
.get('/captcha/session')
|
.post('/captcha/validate')
|
||||||
|
.send({ captcha: '123' })
|
||||||
|
.expect(403)
|
||||||
|
})
|
||||||
|
it('fails when captcha is expired', async () => {
|
||||||
|
const { captcha, cookie } = await getCaptcha()
|
||||||
|
await request(app)
|
||||||
|
.post('/captcha/validate/expired')
|
||||||
|
.send({ captcha: captcha.value })
|
||||||
.set('cookie', cookie)
|
.set('cookie', cookie)
|
||||||
|
.expect(403)
|
||||||
|
})
|
||||||
|
it('fails when captcha does not match', async () => {
|
||||||
|
const { cookie } = await getCaptcha()
|
||||||
|
await request(app)
|
||||||
|
.post('/captcha/validate')
|
||||||
|
.set('cookie', cookie)
|
||||||
|
.send({ captcha: 'invalid-captcha' })
|
||||||
|
.expect(403)
|
||||||
|
})
|
||||||
|
it('validates captcha', async () => {
|
||||||
|
const { captcha, cookie } = await getCaptcha()
|
||||||
|
await request(app)
|
||||||
|
.post('/captcha/validate')
|
||||||
|
.set('cookie', cookie)
|
||||||
|
.send({ captcha: captcha.value })
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
.expect('OK')
|
||||||
|
})
|
||||||
|
it('cannot validate same captcha twice', async () => {
|
||||||
|
const { captcha, cookie } = await getCaptcha()
|
||||||
|
await request(app)
|
||||||
|
.post('/captcha/validate')
|
||||||
|
.set('cookie', cookie)
|
||||||
|
.send({ captcha: captcha.value })
|
||||||
|
.expect(200)
|
||||||
|
.expect('OK')
|
||||||
|
|
||||||
expect(res2.body.captcha).toEqual(jasmine.any(String))
|
await request(app)
|
||||||
|
.post('/captcha/validate')
|
||||||
|
.set('cookie', cookie)
|
||||||
|
.send({ captcha: captcha.value })
|
||||||
|
.expect(403)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import SVGCaptcha from 'svg-captcha'
|
import SVGCaptcha from 'svg-captcha'
|
||||||
import { Request, Response } from 'express'
|
import { Request, Response } from 'express'
|
||||||
|
import { createCaptcha } from './Captcha'
|
||||||
|
|
||||||
export interface ImageConfig {
|
export interface ImageConfig {
|
||||||
size: number
|
size: number
|
||||||
@ -9,7 +10,7 @@ export const image = (config: ImageConfig) => (req: Request, res: Response) => {
|
|||||||
const { text, data } = SVGCaptcha.create({
|
const { text, data } = SVGCaptcha.create({
|
||||||
size: config.size,
|
size: config.size,
|
||||||
})
|
})
|
||||||
req.session!.captcha = text
|
req.session!.captcha = createCaptcha(text, 'image')
|
||||||
res.type('svg')
|
res.type('svg')
|
||||||
res.status(200)
|
res.status(200)
|
||||||
res.send(data)
|
res.send(data)
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from './audio'
|
export * from './audio'
|
||||||
export * from './image'
|
export * from './image'
|
||||||
export * from './commands'
|
export * from './commands'
|
||||||
|
export * from './validateCaptcha'
|
||||||
|
|||||||
33
packages/captcha/src/validateCaptcha.ts
Normal file
33
packages/captcha/src/validateCaptcha.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { RequestHandler } from 'express'
|
||||||
|
import createError from 'http-errors'
|
||||||
|
|
||||||
|
export interface ValidationConfig {
|
||||||
|
readonly property: string
|
||||||
|
readonly ttl: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultConfig: ValidationConfig = {
|
||||||
|
property: 'captcha',
|
||||||
|
ttl: 10 * 60 * 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateCaptcha = (
|
||||||
|
config?: Partial<ValidationConfig>,
|
||||||
|
): RequestHandler => {
|
||||||
|
const cfg: ValidationConfig = Object.assign({}, defaultConfig, config)
|
||||||
|
|
||||||
|
return (req, res, next) => {
|
||||||
|
const captcha = req.session && req.session.captcha
|
||||||
|
if (!captcha) {
|
||||||
|
return next(createError(403, 'Invalid captcha'))
|
||||||
|
}
|
||||||
|
if (Date.now() >= captcha.timestamp + cfg.ttl) {
|
||||||
|
return next(createError(403, 'Invalid captcha'))
|
||||||
|
}
|
||||||
|
if (captcha.value !== req.body[cfg.property]) {
|
||||||
|
return next(createError(403, 'Invalid captcha'))
|
||||||
|
}
|
||||||
|
delete req.session!.captcha
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user