security.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. 'use strict'
  2. const basicAuth = require('basic-auth')
  3. const config = require('config')
  4. const HTTPStatus = require('http-status')
  5. const jwt = require('jsonwebtoken')
  6. const tsscmp = require('tsscmp')
  7. const { validateReq } = require('@overleaf/validation-tools')
  8. const schemas = require('../schema')
  9. function hasValidBasicAuthCredentials(req) {
  10. const credentials = basicAuth(req)
  11. if (!credentials) return false
  12. if (credentials.name !== 'staging') return false
  13. const password = config.get('basicHttpAuth.password')
  14. if (password && tsscmp(credentials.pass, password)) return true
  15. // Support an old password so we can change the password without downtime.
  16. if (config.has('basicHttpAuth.oldPassword')) {
  17. const oldPassword = config.get('basicHttpAuth.oldPassword')
  18. if (oldPassword && tsscmp(credentials.pass, oldPassword)) return true
  19. }
  20. return false
  21. }
  22. function setupSSL(app) {
  23. const httpsOnly = config.get('httpsOnly') === 'true'
  24. if (!httpsOnly) {
  25. return
  26. }
  27. app.enable('trust proxy')
  28. app.use(function (req, res, next) {
  29. if (req.protocol === 'https') {
  30. next()
  31. return
  32. }
  33. if (req.method === 'GET' || req.method === 'HEAD') {
  34. res.redirect('https://' + req.headers.host + req.url)
  35. } else {
  36. res
  37. .status(HTTPStatus.FORBIDDEN)
  38. .send('Please use HTTPS when submitting data to this server.')
  39. }
  40. })
  41. }
  42. exports.setupSSL = setupSSL
  43. function configureJWTAuth(mode = 'jwt') {
  44. return function handleJWTAuth(req, res, next) {
  45. if (hasValidBasicAuthCredentials(req)) {
  46. return next()
  47. }
  48. let token
  49. if ((mode === 'either' || mode === 'token') && req.query.token) {
  50. token = req.query.token
  51. } else if (
  52. (mode === 'either' || mode === 'jwt') &&
  53. req.headers.authorization &&
  54. req.headers.authorization.split(' ')[0] === 'Bearer'
  55. ) {
  56. token = req.headers.authorization.split(' ')[1]
  57. }
  58. if (!token) {
  59. const err = new Error('jwt missing')
  60. err.statusCode = HTTPStatus.UNAUTHORIZED
  61. err.headers = { 'WWW-Authenticate': 'Bearer' }
  62. return next(err)
  63. }
  64. let decoded
  65. try {
  66. decoded = decodeJWT(token)
  67. } catch (error) {
  68. if (
  69. error instanceof jwt.JsonWebTokenError ||
  70. error instanceof jwt.TokenExpiredError
  71. ) {
  72. const err = new Error(error.message)
  73. err.statusCode = HTTPStatus.UNAUTHORIZED
  74. err.headers = {
  75. 'WWW-Authenticate': 'Bearer error="invalid_token"',
  76. }
  77. return next(err)
  78. }
  79. throw error
  80. }
  81. const { params } = validateReq(req, schemas.projectId)
  82. if (decoded.project_id.toString() !== params.project_id.toString()) {
  83. const err = new Error('Wrong project_id')
  84. err.statusCode = HTTPStatus.FORBIDDEN
  85. return next(err)
  86. }
  87. req.jwt = decoded
  88. next()
  89. }
  90. }
  91. /**
  92. * Verify and decode the given JSON Web Token
  93. */
  94. function decodeJWT(token) {
  95. const key = config.get('jwtAuth.key')
  96. const algorithm = config.get('jwtAuth.algorithm')
  97. try {
  98. return jwt.verify(token, key, { algorithms: [algorithm] })
  99. } catch (err) {
  100. // Support an old key so we can change the key without downtime.
  101. if (config.has('jwtAuth.oldKey')) {
  102. const oldKey = config.get('jwtAuth.oldKey')
  103. return jwt.verify(token, oldKey, { algorithms: [algorithm] })
  104. } else {
  105. throw err
  106. }
  107. }
  108. }
  109. function handleBasicAuth(req, res, next) {
  110. if (hasValidBasicAuthCredentials(req)) {
  111. return next()
  112. }
  113. const error = new Error()
  114. error.statusCode = HTTPStatus.UNAUTHORIZED
  115. error.headers = { 'WWW-Authenticate': 'Basic realm="Application"' }
  116. return next(error)
  117. }
  118. function getAuthHandlers() {
  119. if (!config.has('jwtAuth.key') || !config.has('basicHttpAuth.password')) {
  120. throw new Error('missing authentication env vars')
  121. }
  122. const handlers = {}
  123. handlers.jwt = configureJWTAuth('jwt')
  124. handlers.token = configureJWTAuth('token')
  125. handlers.either = configureJWTAuth('either')
  126. handlers.basic = handleBasicAuth
  127. return handlers
  128. }
  129. exports.hasValidBasicAuthCredentials = hasValidBasicAuthCredentials
  130. exports.configureJWTAuth = configureJWTAuth
  131. exports.handleBasicAuth = handleBasicAuth
  132. exports.getAuthHandlers = getAuthHandlers