Ver Fonte

Merge pull request #29966 from overleaf/acf-migration2-security-middleware

(2) Create Express authentication middleware (replaces swagger-security)

GitOrigin-RevId: 0a15ec7ca601430a1ae00c7f81da456948650a0c
Anna Claire Fields há 10 meses atrás
pai
commit
c1dc70fc92

+ 144 - 0
services/history-v1/api/middleware/security.js

@@ -0,0 +1,144 @@
+'use strict'
+
+const basicAuth = require('basic-auth')
+const config = require('config')
+const HTTPStatus = require('http-status')
+const jwt = require('jsonwebtoken')
+const tsscmp = require('tsscmp')
+const { validateReq } = require('@overleaf/validation-tools')
+const schemas = require('../schema')
+
+function hasValidBasicAuthCredentials(req) {
+  const credentials = basicAuth(req)
+  if (!credentials) return false
+
+  // No security in the name, so just use straight comparison.
+  if (credentials.name !== 'staging') return false
+
+  const password = config.get('basicHttpAuth.password')
+  if (password && tsscmp(credentials.pass, password)) return true
+
+  // Support an old password so we can change the password without downtime.
+  if (config.has('basicHttpAuth.oldPassword')) {
+    const oldPassword = config.get('basicHttpAuth.oldPassword')
+    if (oldPassword && tsscmp(credentials.pass, oldPassword)) return true
+  }
+
+  return false
+}
+
+function setupSSL(app) {
+  const httpsOnly = config.get('httpsOnly') === 'true'
+  if (!httpsOnly) {
+    return
+  }
+  app.enable('trust proxy')
+  app.use(function (req, res, next) {
+    if (req.protocol === 'https') {
+      next()
+      return
+    }
+    if (req.method === 'GET' || req.method === 'HEAD') {
+      res.redirect('https://' + req.headers.host + req.url)
+    } else {
+      res
+        .status(HTTPStatus.FORBIDDEN)
+        .send('Please use HTTPS when submitting data to this server.')
+    }
+  })
+}
+
+exports.setupSSL = setupSSL
+
+function handleJWTAuth(req, res, next) {
+  if (hasValidBasicAuthCredentials(req)) {
+    return next()
+  }
+
+  let token
+  if (req.query.token) {
+    token = req.query.token
+  } else if (
+    req.headers.authorization &&
+    req.headers.authorization.split(' ')[0] === 'Bearer'
+  ) {
+    token = req.headers.authorization.split(' ')[1]
+  }
+
+  if (!token) {
+    const err = new Error('jwt missing')
+    err.statusCode = HTTPStatus.UNAUTHORIZED
+    err.headers = { 'WWW-Authenticate': 'Bearer' }
+    return next(err)
+  }
+
+  let decoded
+  try {
+    decoded = decodeJWT(token)
+  } catch (error) {
+    if (
+      error instanceof jwt.JsonWebTokenError ||
+      error instanceof jwt.TokenExpiredError
+    ) {
+      const err = new Error(error.message)
+      err.statusCode = HTTPStatus.UNAUTHORIZED
+      err.headers = { 'WWW-Authenticate': 'Bearer error="invalid_token"' }
+      return next(err)
+    }
+    throw error
+  }
+
+  const { params } = validateReq(req, schemas.projectId)
+  if (decoded.project_id.toString() !== params.project_id.toString()) {
+    const err = new Error('Wrong project_id')
+    err.statusCode = HTTPStatus.FORBIDDEN
+    return next(err)
+  }
+
+  req.jwt = decoded
+  next()
+}
+
+/**
+ * Verify and decode the given JSON Web Token
+ */
+function decodeJWT(token) {
+  const key = config.get('jwtAuth.key')
+  const algorithm = config.get('jwtAuth.algorithm')
+  try {
+    return jwt.verify(token, key, { algorithms: [algorithm] })
+  } catch (err) {
+    // Support an old key so we can change the key without downtime.
+    if (config.has('jwtAuth.oldKey')) {
+      const oldKey = config.get('jwtAuth.oldKey')
+      return jwt.verify(token, oldKey, { algorithms: [algorithm] })
+    } else {
+      throw err
+    }
+  }
+}
+
+function handleBasicAuth(req, res, next) {
+  if (hasValidBasicAuthCredentials(req)) {
+    return next()
+  }
+  const error = new Error()
+  error.statusCode = HTTPStatus.UNAUTHORIZED
+  error.headers = { 'WWW-Authenticate': 'Basic realm="Application"' }
+  return next(error)
+}
+
+function getAuthHandlers() {
+  if (!config.has('jwtAuth.key') || !config.has('basicHttpAuth.password')) {
+    throw new Error('missing authentication env vars')
+  }
+
+  const handlers = {}
+  handlers.jwt = handleJWTAuth
+  handlers.basic = handleBasicAuth
+  handlers.token = handleJWTAuth
+  return handlers
+}
+
+exports.hasValidBasicAuthCredentials = hasValidBasicAuthCredentials
+exports.getAuthHandlers = getAuthHandlers

+ 7 - 0
services/history-v1/api/schema.js

@@ -42,6 +42,13 @@ const changeSchema = z.object({
 })
 
 const schemas = {
+  projectId: z.object({
+    params: z
+      .object({
+        project_id: z.string().optional(),
+      })
+      .optional(),
+  }),
   initializeProject: z.object({
     body: z
       .object({