TokenGenerator.js 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. const crypto = require('crypto')
  2. const V1Api = require('../V1/V1Api')
  3. const Features = require('../../infrastructure/Features')
  4. const { callbackify } = require('util')
  5. // (From Overleaf `random_token.rb`)
  6. // Letters (not numbers! see generate_token) used in tokens. They're all
  7. // consonants, to avoid embarassing words (I can't think of any that use only
  8. // a y), and lower case "l" is omitted, because in many fonts it is
  9. // indistinguishable from an upper case "I" (and sometimes even the number 1).
  10. const TOKEN_LOWERCASE_ALPHA = 'bcdfghjkmnpqrstvwxyz'
  11. const TOKEN_NUMERICS = '123456789'
  12. const TOKEN_ALPHANUMERICS =
  13. TOKEN_LOWERCASE_ALPHA + TOKEN_LOWERCASE_ALPHA.toUpperCase() + TOKEN_NUMERICS
  14. // This module mirrors the token generation in Overleaf (`random_token.rb`),
  15. // for the purposes of implementing token-based project access, like the
  16. // 'unlisted-projects' feature in Overleaf
  17. function _randomString(length, alphabet) {
  18. const result = crypto
  19. .randomBytes(length)
  20. .toJSON()
  21. .data.map(b => alphabet[b % alphabet.length])
  22. .join('')
  23. return result
  24. }
  25. // Generate a 12-char token with only characters from TOKEN_LOWERCASE_ALPHA,
  26. // suitable for use as a read-only token for a project
  27. function readOnlyToken() {
  28. return _randomString(12, TOKEN_LOWERCASE_ALPHA)
  29. }
  30. // Generate a longer token, with a numeric prefix,
  31. // suitable for use as a read-and-write token for a project
  32. function readAndWriteToken() {
  33. const numerics = _randomString(10, TOKEN_NUMERICS)
  34. const token = _randomString(12, TOKEN_LOWERCASE_ALPHA)
  35. const fullToken = `${numerics}${token}`
  36. return { token: fullToken, numericPrefix: numerics }
  37. }
  38. function generateReferralId() {
  39. return _randomString(16, TOKEN_ALPHANUMERICS)
  40. }
  41. async function generateUniqueReadOnlyToken() {
  42. const retryOperation = async () => {
  43. const token = readOnlyToken()
  44. if (!Features.hasFeature('saas')) {
  45. return token
  46. }
  47. const { response, body } = await V1Api.promises.request({
  48. url: `/api/v1/overleaf/docs/read_token/${token}/exists`,
  49. json: true,
  50. })
  51. if (response.statusCode !== 200) {
  52. throw new Error(
  53. `non-200 response from v1 read-token-exists api: ${response.statusCode}`
  54. )
  55. }
  56. if (body.exists === true) {
  57. throw new Error(`token already exists in v1: ${token}`)
  58. }
  59. return token
  60. }
  61. const MAX_RETRIES = 10
  62. for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
  63. try {
  64. return await retryOperation()
  65. } catch (err) {
  66. if (attempt === MAX_RETRIES) {
  67. throw err
  68. }
  69. }
  70. }
  71. }
  72. const TokenGenerator = {
  73. _randomString,
  74. readOnlyToken,
  75. readAndWriteToken,
  76. generateReferralId,
  77. generateUniqueReadOnlyToken: callbackify(generateUniqueReadOnlyToken),
  78. }
  79. TokenGenerator.promises = {
  80. generateUniqueReadOnlyToken,
  81. }
  82. module.exports = TokenGenerator