juice-shop/juice-shop

View on GitHub
test/api/2faSpec.ts

Summary

Maintainability
A
3 hrs
Test Coverage
/*
 * Copyright (c) 2014-2024 Bjoern Kimminich & the OWASP Juice Shop contributors.
 * SPDX-License-Identifier: MIT
 */

import frisby = require('frisby')
import config from 'config'
import jwt from 'jsonwebtoken'
const Joi = frisby.Joi
const security = require('../../lib/insecurity')

const otplib = require('otplib')

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') {
    // @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 = 'fooooo1@bar.com'
    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 = 'fooooo2@bar.com'
    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 inital token is incorrect', async () => {
    const email = 'fooooo3@bar.com'
    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 = 'fooooo4@bar.com'
    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 = 'fooooodisable1@bar.com'
    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 = 'fooooodisable1@bar.com'
    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
      })
  })
})