security.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  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 { parseReq } = 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 setupBasicHttpAuthForSwaggerDocs(app) {
  44. app.use('/docs', function (req, res, next) {
  45. if (hasValidBasicAuthCredentials(req)) {
  46. return next()
  47. }
  48. res.header('WWW-Authenticate', 'Basic realm="Application"')
  49. res.status(HTTPStatus.UNAUTHORIZED).end()
  50. })
  51. }
  52. exports.setupBasicHttpAuthForSwaggerDocs = setupBasicHttpAuthForSwaggerDocs
  53. function configureJWTAuth(mode = 'jwt') {
  54. return function handleJWTAuth(req, res, next) {
  55. if (hasValidBasicAuthCredentials(req)) {
  56. return next()
  57. }
  58. let token
  59. if ((mode === 'either' || mode === 'token') && req.query.token) {
  60. token = req.query.token
  61. } else if (
  62. (mode === 'either' || mode === 'jwt') &&
  63. req.headers.authorization &&
  64. req.headers.authorization.split(' ')[0] === 'Bearer'
  65. ) {
  66. token = req.headers.authorization.split(' ')[1]
  67. }
  68. if (!token) {
  69. const err = new Error('jwt missing')
  70. err.statusCode = HTTPStatus.UNAUTHORIZED
  71. err.headers = { 'WWW-Authenticate': 'Bearer' }
  72. return next(err)
  73. }
  74. let decoded
  75. try {
  76. decoded = decodeJWT(token)
  77. } catch (error) {
  78. if (
  79. error instanceof jwt.JsonWebTokenError ||
  80. error instanceof jwt.TokenExpiredError
  81. ) {
  82. const err = new Error(error.message)
  83. err.statusCode = HTTPStatus.UNAUTHORIZED
  84. err.headers = {
  85. 'WWW-Authenticate': 'Bearer error="invalid_token"',
  86. }
  87. return next(err)
  88. }
  89. throw error
  90. }
  91. const { params } = parseReq(req, schemas.projectId)
  92. if (decoded.project_id.toString() !== params.project_id.toString()) {
  93. const err = new Error('Wrong project_id')
  94. err.statusCode = HTTPStatus.FORBIDDEN
  95. return next(err)
  96. }
  97. req.jwt = decoded
  98. next()
  99. }
  100. }
  101. /**
  102. * Verify and decode the given JSON Web Token
  103. */
  104. function decodeJWT(token) {
  105. const key = config.get('jwtAuth.key')
  106. const algorithm = config.get('jwtAuth.algorithm')
  107. try {
  108. return jwt.verify(token, key, { algorithms: [algorithm] })
  109. } catch (err) {
  110. // Support an old key so we can change the key without downtime.
  111. if (config.has('jwtAuth.oldKey')) {
  112. const oldKey = config.get('jwtAuth.oldKey')
  113. return jwt.verify(token, oldKey, { algorithms: [algorithm] })
  114. } else {
  115. throw err
  116. }
  117. }
  118. }
  119. function handleBasicAuth(req, res, next) {
  120. if (hasValidBasicAuthCredentials(req)) {
  121. return next()
  122. }
  123. const error = new Error()
  124. error.statusCode = HTTPStatus.UNAUTHORIZED
  125. error.headers = { 'WWW-Authenticate': 'Basic realm="Application"' }
  126. return next(error)
  127. }
  128. function getAuthHandlers() {
  129. if (!config.has('jwtAuth.key') || !config.has('basicHttpAuth.password')) {
  130. throw new Error('missing authentication env vars')
  131. }
  132. const handlers = {}
  133. handlers.jwt = configureJWTAuth('jwt')
  134. handlers.token = configureJWTAuth('token')
  135. handlers.either = configureJWTAuth('either')
  136. handlers.basic = handleBasicAuth
  137. return handlers
  138. }
  139. exports.hasValidBasicAuthCredentials = hasValidBasicAuthCredentials
  140. exports.configureJWTAuth = configureJWTAuth
  141. exports.handleBasicAuth = handleBasicAuth
  142. exports.getAuthHandlers = getAuthHandlers