overleaf/services/history-v1/api/app/security.js

148 lines
4.1 KiB
JavaScript
Raw Normal View History

'use strict'
const basicAuth = require('basic-auth')
const config = require('config')
const HTTPStatus = require('http-status')
const jwt = require('jsonwebtoken')
const tsscmp = require('tsscmp')
function setupBasicHttpAuthForSwaggerDocs(app) {
app.use('/docs', function (req, res, next) {
if (hasValidBasicAuthCredentials(req)) {
return next()
}
res.header('WWW-Authenticate', 'Basic realm="Application"')
res.status(HTTPStatus.UNAUTHORIZED).end()
})
}
exports.setupBasicHttpAuthForSwaggerDocs = setupBasicHttpAuthForSwaggerDocs
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, authOrSecDef, scopesOrApiKey, next) {
// as a temporary solution, to make the OT demo still work
// this handler will also check for basic authorization
if (hasValidBasicAuthCredentials(req)) {
return next()
}
let token, err
if (authOrSecDef.name === 'token') {
token = req.query.token
} else if (
req.headers.authorization &&
req.headers.authorization.split(' ')[0] === 'Bearer'
) {
token = req.headers.authorization.split(' ')[1]
}
if (!token) {
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
) {
err = new Error(error.message)
err.statusCode = HTTPStatus.UNAUTHORIZED
err.headers = { 'WWW-Authenticate': 'Bearer error="invalid_token"' }
return next(err)
}
throw error
}
if (decoded.project_id.toString() !== req.swagger.params.project_id.value) {
err = new Error('Wrong project_id')
err.statusCode = HTTPStatus.FORBIDDEN
return next(err)
}
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, authOrSecDef, scopesOrApiKey, 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 getSwaggerHandlers() {
const handlers = {}
if (!config.has('jwtAuth.key') || !config.has('basicHttpAuth.password')) {
throw new Error('missing authentication env vars')
}
handlers.jwt = handleJWTAuth
handlers.basic = handleBasicAuth
handlers.token = handleJWTAuth
return handlers
}
exports.getSwaggerHandlers = getSwaggerHandlers