jwt.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. const jwt = require('jsonwebtoken')
  2. const { encrypt, decrypt } = require('./utils')
  3. // The Uppy auth token is an encrypted JWT & JSON encoded container.
  4. // It used to simply contain an OAuth access_token and refresh_token for a specific provider.
  5. // However now we allow more data to be stored in it. This allows for storing other state or parameters needed for that
  6. // specific provider, like username, password, host names etc.
  7. // The different providers APIs themselves will verify these inner tokens through Provider classes.
  8. // The expiry of the Uppy auth token should be higher than the expiry of the refresh token.
  9. // Because some refresh tokens normally never expire, we set the Uppy auth token expiry very high.
  10. // Chrome has a maximum cookie expiry of 400 days, so we'll use that (we also store the auth token in a cookie)
  11. //
  12. // If the Uppy auth token expiry were set too low (e.g. 24hr), we could risk this situation:
  13. // A user starts an upload, thus creating an auth token. They leave the upload running over night.
  14. // The upload finishes after a few hours, but with some failed files. Then the next day (say after 25hr)
  15. // they would retry the failed tiles, however now the Uppy auth token has expired and
  16. // even though the provider refresh token would still have been accepted and
  17. // there's no way for them to retry their failed files.
  18. // With 400 days, there's still a theoretical possibility but very low.
  19. const MAX_AGE_REFRESH_TOKEN = 60 * 60 * 24 * 400
  20. const MAX_AGE_24H = 60 * 60 * 24
  21. module.exports.MAX_AGE_24H = MAX_AGE_24H
  22. module.exports.MAX_AGE_REFRESH_TOKEN = MAX_AGE_REFRESH_TOKEN
  23. /**
  24. *
  25. * @param {*} data
  26. * @param {string} secret
  27. * @param {number} maxAge
  28. */
  29. const generateToken = (data, secret, maxAge) => {
  30. return jwt.sign({ data }, secret, { expiresIn: maxAge })
  31. }
  32. /**
  33. *
  34. * @param {string} token
  35. * @param {string} secret
  36. */
  37. const verifyToken = (token, secret) => {
  38. // @ts-ignore
  39. return jwt.verify(token, secret, {}).data
  40. }
  41. /**
  42. *
  43. * @param {*} payload
  44. * @param {string} secret
  45. */
  46. module.exports.generateEncryptedToken = (payload, secret, maxAge = MAX_AGE_24H) => {
  47. // return payload // for easier debugging
  48. return encrypt(generateToken(payload, secret, maxAge), secret)
  49. }
  50. /**
  51. * @param {*} payload
  52. * @param {string} secret
  53. */
  54. module.exports.generateEncryptedAuthToken = (payload, secret, maxAge) => {
  55. return module.exports.generateEncryptedToken(JSON.stringify(payload), secret, maxAge)
  56. }
  57. /**
  58. *
  59. * @param {string} token
  60. * @param {string} secret
  61. */
  62. module.exports.verifyEncryptedToken = (token, secret) => {
  63. const ret = verifyToken(decrypt(token, secret), secret)
  64. if (!ret) throw new Error('No payload')
  65. return ret
  66. }
  67. /**
  68. *
  69. * @param {string} token
  70. * @param {string} secret
  71. */
  72. module.exports.verifyEncryptedAuthToken = (token, secret, providerName) => {
  73. const json = module.exports.verifyEncryptedToken(token, secret)
  74. const tokens = JSON.parse(json)
  75. if (!tokens[providerName]) throw new Error(`Missing token payload for provider ${providerName}`)
  76. return tokens
  77. }
  78. function getCommonCookieOptions ({ companionOptions }) {
  79. const cookieOptions = {
  80. httpOnly: true,
  81. }
  82. // Fix to show thumbnails on Chrome
  83. // https://community.transloadit.com/t/dropbox-and-box-thumbnails-returning-401-unauthorized/15781/2
  84. // Note that sameSite cookies also require secure (which needs https), so thumbnails don't work from localhost
  85. // to test locally, you can manually find the URL of the image and open it in a separate browser tab
  86. if (companionOptions.server && companionOptions.server.protocol === 'https') {
  87. cookieOptions.sameSite = 'none'
  88. cookieOptions.secure = true
  89. }
  90. if (companionOptions.cookieDomain) {
  91. cookieOptions.domain = companionOptions.cookieDomain
  92. }
  93. return cookieOptions
  94. }
  95. const getCookieName = (authProvider) => `uppyAuthToken--${authProvider}`
  96. const addToCookies = ({ res, token, companionOptions, authProvider, maxAge = MAX_AGE_24H * 1000 }) => {
  97. const cookieOptions = {
  98. ...getCommonCookieOptions({ companionOptions }),
  99. maxAge,
  100. }
  101. // send signed token to client.
  102. res.cookie(getCookieName(authProvider), token, cookieOptions)
  103. }
  104. module.exports.addToCookiesIfNeeded = (req, res, uppyAuthToken, maxAge) => {
  105. // some providers need the token in cookies for thumbnail/image requests
  106. if (req.companion.provider.needsCookieAuth) {
  107. addToCookies({
  108. res,
  109. token: uppyAuthToken,
  110. companionOptions: req.companion.options,
  111. authProvider: req.companion.provider.authProvider,
  112. maxAge,
  113. })
  114. }
  115. }
  116. /**
  117. *
  118. * @param {object} res
  119. * @param {object} companionOptions
  120. * @param {string} authProvider
  121. */
  122. module.exports.removeFromCookies = (res, companionOptions, authProvider) => {
  123. // options must be identical to those given to res.cookie(), excluding expires and maxAge.
  124. // https://expressjs.com/en/api.html#res.clearCookie
  125. const cookieOptions = getCommonCookieOptions({ companionOptions })
  126. res.clearCookie(getCookieName(authProvider), cookieOptions)
  127. }