mirror of
https://github.com/KeygraphHQ/juice-shop.git
synced 2026-11-10 10:41:36 -10:00
470 lines
13 KiB
TypeScript
470 lines
13 KiB
TypeScript
/*
|
|
* Copyright (c) 2014-2025 Bjoern Kimminich & the OWASP Juice Shop contributors.
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
import * as frisby from 'frisby'
|
|
import config from 'config'
|
|
import jwt from 'jsonwebtoken'
|
|
import * as otplib from 'otplib'
|
|
|
|
import * as security from '../../lib/insecurity'
|
|
|
|
const Joi = frisby.Joi
|
|
|
|
const REST_URL = 'http://localhost:3000/rest'
|
|
const API_URL = 'http://localhost:3000/api'
|
|
|
|
const jsonHeader = { 'content-type': 'application/json' }
|
|
|
|
async function login ({ email, password, totpSecret }: { email: string, password: string, totpSecret?: string }) {
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
const loginRes = await frisby
|
|
.post(REST_URL + '/user/login', {
|
|
email,
|
|
password
|
|
}).catch((res: any) => {
|
|
if (res.json?.type && res.json.status === 'totp_token_required') {
|
|
return res
|
|
}
|
|
throw new Error(`Failed to login '${email}'`)
|
|
})
|
|
|
|
if (loginRes.json.status && loginRes.json.status === 'totp_token_required') {
|
|
if (!totpSecret) {
|
|
throw new Error('login with totp required but no totp secret provided to login function')
|
|
}
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
const totpRes = await frisby
|
|
.post(REST_URL + '/2fa/verify', {
|
|
tmpToken: loginRes.json.data.tmpToken,
|
|
totpToken: otplib.authenticator.generate(totpSecret)
|
|
})
|
|
|
|
return totpRes.json.authentication
|
|
}
|
|
|
|
return loginRes.json.authentication
|
|
}
|
|
|
|
async function register ({ email, password, totpSecret }: { email: string, password: string, totpSecret?: string }) {
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
const res = await frisby
|
|
.post(API_URL + '/Users/', {
|
|
email,
|
|
password,
|
|
passwordRepeat: password,
|
|
securityQuestion: null,
|
|
securityAnswer: null
|
|
}).catch(() => {
|
|
throw new Error(`Failed to register '${email}'`)
|
|
})
|
|
|
|
if (totpSecret) {
|
|
const { token } = await login({ email, password })
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.post(
|
|
REST_URL + '/2fa/setup',
|
|
{
|
|
headers: {
|
|
Authorization: 'Bearer ' + token,
|
|
'content-type': 'application/json'
|
|
},
|
|
body: {
|
|
password,
|
|
setupToken: security.authorize({
|
|
secret: totpSecret,
|
|
type: 'totp_setup_secret'
|
|
}),
|
|
initialToken: otplib.authenticator.generate(totpSecret)
|
|
}
|
|
}).expect('status', 200).catch(() => {
|
|
throw new Error(`Failed to enable 2fa for user: '${email}'`)
|
|
})
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
function getStatus (token: string) {
|
|
return frisby.get(
|
|
REST_URL + '/2fa/status',
|
|
{
|
|
headers: {
|
|
Authorization: 'Bearer ' + token,
|
|
'content-type': 'application/json'
|
|
}
|
|
})
|
|
}
|
|
|
|
describe('/rest/2fa/verify', () => {
|
|
it('POST should return a valid authentication when a valid tmp token is passed', async () => {
|
|
const tmpTokenWurstbrot = security.authorize({
|
|
userId: 10,
|
|
type: 'password_valid_needs_second_factor_token'
|
|
})
|
|
|
|
const totpToken = otplib.authenticator.generate('IFTXE3SPOEYVURT2MRYGI52TKJ4HC3KH')
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.post(REST_URL + '/2fa/verify', {
|
|
headers: jsonHeader,
|
|
body: {
|
|
tmpToken: tmpTokenWurstbrot,
|
|
totpToken
|
|
}
|
|
})
|
|
.expect('status', 200)
|
|
.expect('header', 'content-type', /application\/json/)
|
|
.expect('jsonTypes', 'authentication', {
|
|
token: Joi.string(),
|
|
umail: Joi.string(),
|
|
bid: Joi.number()
|
|
})
|
|
.expect('json', 'authentication', {
|
|
umail: `wurstbrot@${config.get<string>('application.domain')}`
|
|
})
|
|
})
|
|
|
|
it('POST should fail if a invalid totp token is used', async () => {
|
|
const tmpTokenWurstbrot = security.authorize({
|
|
userId: 10,
|
|
type: 'password_valid_needs_second_factor_token'
|
|
})
|
|
|
|
const totpToken = otplib.authenticator.generate('THIS9ISNT8THE8RIGHT8SECRET')
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.post(REST_URL + '/2fa/verify', {
|
|
headers: jsonHeader,
|
|
body: {
|
|
tmpToken: tmpTokenWurstbrot,
|
|
totpToken
|
|
}
|
|
})
|
|
.expect('status', 401)
|
|
})
|
|
|
|
it('POST should fail if a unsigned tmp token is used', async () => {
|
|
const tmpTokenWurstbrot = jwt.sign({
|
|
userId: 10,
|
|
type: 'password_valid_needs_second_factor_token'
|
|
}, 'this_surly_isnt_the_right_key')
|
|
|
|
const totpToken = otplib.authenticator.generate('IFTXE3SPOEYVURT2MRYGI52TKJ4HC3KH')
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.post(REST_URL + '/2fa/verify', {
|
|
headers: jsonHeader,
|
|
body: {
|
|
tmpToken: tmpTokenWurstbrot,
|
|
totpToken
|
|
}
|
|
})
|
|
.expect('status', 401)
|
|
})
|
|
})
|
|
|
|
describe('/rest/2fa/status', () => {
|
|
it('GET should indicate 2fa is setup for 2fa enabled users', async () => {
|
|
const { token } = await login({
|
|
email: `wurstbrot@${config.get<string>('application.domain')}`,
|
|
password: 'EinBelegtesBrotMitSchinkenSCHINKEN!',
|
|
totpSecret: 'IFTXE3SPOEYVURT2MRYGI52TKJ4HC3KH'
|
|
})
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.get(
|
|
REST_URL + '/2fa/status',
|
|
{
|
|
headers: {
|
|
Authorization: 'Bearer ' + token,
|
|
'content-type': 'application/json'
|
|
}
|
|
})
|
|
.expect('status', 200)
|
|
.expect('header', 'content-type', /application\/json/)
|
|
.expect('jsonTypes', {
|
|
setup: Joi.boolean()
|
|
})
|
|
.expect('json', {
|
|
setup: true
|
|
})
|
|
})
|
|
|
|
it('GET should indicate 2fa is not setup for users with 2fa disabled', async () => {
|
|
const { token } = await login({
|
|
email: `J12934@${config.get<string>('application.domain')}`,
|
|
password: '0Y8rMnww$*9VFYE§59-!Fg1L6t&6lB'
|
|
})
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.get(
|
|
REST_URL + '/2fa/status',
|
|
{
|
|
headers: {
|
|
Authorization: 'Bearer ' + token,
|
|
'content-type': 'application/json'
|
|
}
|
|
})
|
|
.expect('status', 200)
|
|
.expect('header', 'content-type', /application\/json/)
|
|
.expect('jsonTypes', {
|
|
setup: Joi.boolean(),
|
|
secret: Joi.string(),
|
|
email: Joi.string(),
|
|
setupToken: Joi.string()
|
|
})
|
|
.expect('json', {
|
|
setup: false,
|
|
email: `J12934@${config.get<string>('application.domain')}`
|
|
})
|
|
})
|
|
|
|
it('GET should return 401 when not logged in', async () => {
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.get(REST_URL + '/2fa/status')
|
|
.expect('status', 401)
|
|
})
|
|
})
|
|
|
|
describe('/rest/2fa/setup', () => {
|
|
it('POST should be able to setup 2fa for accounts without 2fa enabled', async () => {
|
|
const email = '[email protected]'
|
|
const password = '123456'
|
|
|
|
const secret = 'ASDVAJSDUASZGDIADBJS'
|
|
|
|
await register({ email, password })
|
|
const { token } = await login({ email, password })
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.post(
|
|
REST_URL + '/2fa/setup',
|
|
{
|
|
headers: {
|
|
Authorization: 'Bearer ' + token,
|
|
'content-type': 'application/json'
|
|
},
|
|
body: {
|
|
password,
|
|
setupToken: security.authorize({
|
|
secret,
|
|
type: 'totp_setup_secret'
|
|
}),
|
|
initialToken: otplib.authenticator.generate(secret)
|
|
}
|
|
})
|
|
.expect('status', 200)
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.get(
|
|
REST_URL + '/2fa/status',
|
|
{
|
|
headers: {
|
|
Authorization: 'Bearer ' + token,
|
|
'content-type': 'application/json'
|
|
}
|
|
})
|
|
.expect('status', 200)
|
|
.expect('jsonTypes', {
|
|
setup: Joi.boolean()
|
|
})
|
|
.expect('json', {
|
|
setup: true
|
|
})
|
|
})
|
|
|
|
it('POST should fail if the password doesnt match', async () => {
|
|
const email = '[email protected]'
|
|
const password = '123456'
|
|
|
|
const secret = 'ASDVAJSDUASZGDIADBJS'
|
|
|
|
await register({ email, password })
|
|
const { token } = await login({ email, password })
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.post(
|
|
REST_URL + '/2fa/setup',
|
|
{
|
|
headers: {
|
|
Authorization: 'Bearer ' + token,
|
|
'content-type': 'application/json'
|
|
},
|
|
body: {
|
|
password: password + ' this makes the password wrong',
|
|
setupToken: security.authorize({
|
|
secret,
|
|
type: 'totp_setup_secret'
|
|
}),
|
|
initialToken: otplib.authenticator.generate(secret)
|
|
}
|
|
})
|
|
.expect('status', 401)
|
|
})
|
|
|
|
it('POST should fail if the initial token is incorrect', async () => {
|
|
const email = '[email protected]'
|
|
const password = '123456'
|
|
|
|
const secret = 'ASDVAJSDUASZGDIADBJS'
|
|
|
|
await register({ email, password })
|
|
const { token } = await login({ email, password })
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.post(
|
|
REST_URL + '/2fa/setup',
|
|
{
|
|
headers: {
|
|
Authorization: 'Bearer ' + token,
|
|
'content-type': 'application/json'
|
|
},
|
|
body: {
|
|
password,
|
|
setupToken: security.authorize({
|
|
secret,
|
|
type: 'totp_setup_secret'
|
|
}),
|
|
initialToken: otplib.authenticator.generate(secret + 'ASJDVASGDKASVDUAGS')
|
|
}
|
|
})
|
|
.expect('status', 401)
|
|
})
|
|
|
|
it('POST should fail if the token is of the wrong type', async () => {
|
|
const email = '[email protected]'
|
|
const password = '123456'
|
|
|
|
const secret = 'ASDVAJSDUASZGDIADBJS'
|
|
|
|
await register({ email, password })
|
|
const { token } = await login({ email, password })
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.post(
|
|
REST_URL + '/2fa/setup',
|
|
{
|
|
headers: {
|
|
Authorization: 'Bearer ' + token,
|
|
'content-type': 'application/json'
|
|
},
|
|
body: {
|
|
password,
|
|
setupToken: security.authorize({
|
|
secret,
|
|
type: 'totp_setup_secret_foobar'
|
|
}),
|
|
initialToken: otplib.authenticator.generate(secret)
|
|
}
|
|
})
|
|
.expect('status', 401)
|
|
})
|
|
|
|
it('POST should fail if the account has already set up 2fa', async () => {
|
|
const email = `wurstbrot@${config.get<string>('application.domain')}`
|
|
const password = 'EinBelegtesBrotMitSchinkenSCHINKEN!'
|
|
const totpSecret = 'IFTXE3SPOEYVURT2MRYGI52TKJ4HC3KH'
|
|
|
|
const { token } = await login({ email, password, totpSecret })
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.post(
|
|
REST_URL + '/2fa/setup',
|
|
{
|
|
headers: {
|
|
Authorization: 'Bearer ' + token,
|
|
'content-type': 'application/json'
|
|
},
|
|
body: {
|
|
password,
|
|
setupToken: security.authorize({
|
|
secret: totpSecret,
|
|
type: 'totp_setup_secret'
|
|
}),
|
|
initialToken: otplib.authenticator.generate(totpSecret)
|
|
}
|
|
})
|
|
.expect('status', 401)
|
|
})
|
|
})
|
|
|
|
describe('/rest/2fa/disable', () => {
|
|
it('POST should be able to disable 2fa for account with 2fa enabled', async () => {
|
|
const email = '[email protected]'
|
|
const password = '123456'
|
|
const totpSecret = 'ASDVAJSDUASZGDIADBJS'
|
|
|
|
await register({ email, password, totpSecret })
|
|
const { token } = await login({ email, password, totpSecret })
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await getStatus(token)
|
|
.expect('status', 200)
|
|
.expect('json', {
|
|
setup: true
|
|
})
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.post(
|
|
REST_URL + '/2fa/disable',
|
|
{
|
|
headers: {
|
|
Authorization: 'Bearer ' + token,
|
|
'content-type': 'application/json'
|
|
},
|
|
body: {
|
|
password
|
|
}
|
|
}
|
|
).expect('status', 200)
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await getStatus(token)
|
|
.expect('status', 200)
|
|
.expect('json', {
|
|
setup: false
|
|
})
|
|
})
|
|
|
|
it('POST should not be possible to disable 2fa without the correct password', async () => {
|
|
const email = '[email protected]'
|
|
const password = '123456'
|
|
const totpSecret = 'ASDVAJSDUASZGDIADBJS'
|
|
|
|
await register({ email, password, totpSecret })
|
|
const { token } = await login({ email, password, totpSecret })
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await getStatus(token)
|
|
.expect('status', 200)
|
|
.expect('json', {
|
|
setup: true
|
|
})
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await frisby.post(
|
|
REST_URL + '/2fa/disable',
|
|
{
|
|
headers: {
|
|
Authorization: 'Bearer ' + token,
|
|
'content-type': 'application/json'
|
|
},
|
|
body: {
|
|
password: password + ' this makes the password wrong'
|
|
}
|
|
}
|
|
).expect('status', 401)
|
|
|
|
// @ts-expect-error FIXME promise return handling broken
|
|
await getStatus(token)
|
|
.expect('status', 200)
|
|
.expect('json', {
|
|
setup: true
|
|
})
|
|
})
|
|
})
|