--- services/web/app/src/Features/Collaborators/CollaboratorsController.js
+++ services/web/app/src/Features/Collaborators/CollaboratorsController.js
@@ -11,6 +11,7 @@ const Errors = require('../Errors/Errors')
 const logger = require('@overleaf/logger')
 const { expressify } = require('../../util/promises')
 const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
+const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')

 module.exports = {
   removeUserFromProject: expressify(removeUserFromProject),
@@ -18,6 +19,7 @@ module.exports = {
   getAllMembers: expressify(getAllMembers),
   setCollaboratorInfo: expressify(setCollaboratorInfo),
   transferOwnership: expressify(transferOwnership),
+  getShareTokens: expressify(getShareTokens),
 }

 async function removeUserFromProject(req, res, next) {
@@ -114,3 +116,37 @@ async function _removeUserIdFromProject(projectId, userId) {
   )
   await TagsHandler.promises.removeProjectFromAllTags(userId, projectId)
 }
+
+async function getShareTokens(req, res) {
+  const projectId = req.params.Project_id
+  const userId = SessionManager.getLoggedInUserId(req.session)
+
+  let tokens
+  if (userId) {
+    tokens = await CollaboratorsGetter.promises.getPublicShareTokens(
+      ObjectId(userId),
+      ObjectId(projectId)
+    )
+  } else {
+    // anonymous access, the token is already available in the session
+    const readOnly = TokenAccessHandler.getRequestToken(req, projectId)
+    tokens = { readOnly }
+  }
+  if (!tokens) {
+    return res.sendStatus(403)
+  }
+
+  if (tokens.readOnly || tokens.readAndWrite) {
+    logger.info(
+      {
+        projectId,
+        userId: userId || 'anonymous',
+        ip: req.ip,
+        tokens: Object.keys(tokens),
+      },
+      'project tokens accessed'
+    )
+  }
+
+  res.json(tokens)
+}
--- services/web/app/src/Features/Collaborators/CollaboratorsGetter.js
+++ services/web/app/src/Features/Collaborators/CollaboratorsGetter.js
@@ -25,6 +25,7 @@ module.exports = {
   getInvitedCollaboratorCount: callbackify(getInvitedCollaboratorCount),
   getProjectsUserIsMemberOf: callbackify(getProjectsUserIsMemberOf),
   isUserInvitedMemberOfProject: callbackify(isUserInvitedMemberOfProject),
+  getPublicShareTokens: callbackify(getPublicShareTokens),
   userIsTokenMember: callbackify(userIsTokenMember),
   getAllInvitedMembers: callbackify(getAllInvitedMembers),
   promises: {
@@ -37,6 +38,7 @@ module.exports = {
     getInvitedCollaboratorCount,
     getProjectsUserIsMemberOf,
     isUserInvitedMemberOfProject,
+    getPublicShareTokens,
     userIsTokenMember,
     getAllInvitedMembers,
   },
@@ -133,6 +135,40 @@ async function isUserInvitedMemberOfProject(userId, projectId) {
   return false
 }

+async function getPublicShareTokens(userId, projectId) {
+  const memberInfo = await Project.findOne(
+    {
+      _id: projectId,
+    },
+    {
+      isOwner: { $eq: ['$owner_ref', userId] },
+      hasTokenReadOnlyAccess: {
+        $and: [
+          { $in: [userId, '$tokenAccessReadOnly_refs'] },
+          { $eq: ['$publicAccesLevel', PublicAccessLevels.TOKEN_BASED] },
+        ],
+      },
+      tokens: 1,
+    }
+  )
+    .lean()
+    .exec()
+
+  if (!memberInfo) {
+    return null
+  }
+
+  if (memberInfo.isOwner) {
+    return memberInfo.tokens
+  } else if (memberInfo.hasTokenReadOnlyAccess) {
+    return {
+      readOnly: memberInfo.tokens.readOnly,
+    }
+  } else {
+    return {}
+  }
+}
+
 async function getProjectsUserIsMemberOf(userId, fields) {
   const limit = pLimit(2)
   const [readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly] =
--- services/web/app/src/Features/Collaborators/CollaboratorsRouter.js
+++ services/web/app/src/Features/Collaborators/CollaboratorsRouter.js
@@ -22,6 +22,10 @@ const rateLimiters = {
     points: 200,
     duration: 60 * 10,
   }),
+  getProjectTokens: new RateLimiter('get-project-tokens', {
+    points: 200,
+    duration: 60 * 10,
+  }),
 }

 module.exports = {
@@ -139,5 +143,12 @@ module.exports = {
       CollaboratorsInviteController.acceptInvite,
       AnalyticsRegistrationSourceMiddleware.clearSource()
     )
+
+    webRouter.get(
+      '/project/:Project_id/tokens',
+      RateLimiterMiddleware.rateLimit(rateLimiters.getProjectTokens),
+      AuthorizationMiddleware.ensureUserCanReadProject,
+      CollaboratorsController.getShareTokens
+    )
   },
 }
--- services/web/app/src/Features/Editor/EditorController.js
+++ services/web/app/src/Features/Editor/EditorController.js
@@ -581,20 +581,7 @@ const EditorController = {
           { newAccessLevel }
         )
         if (newAccessLevel === PublicAccessLevels.TOKEN_BASED) {
-          ProjectDetailsHandler.ensureTokensArePresent(
-            projectId,
-            function (err, tokens) {
-              if (err) {
-                return callback(err)
-              }
-              EditorRealTimeController.emitToRoom(
-                projectId,
-                'project:tokens:changed',
-                { tokens }
-              )
-              callback()
-            }
-          )
+          ProjectDetailsHandler.ensureTokensArePresent(projectId, callback)
         } else {
           callback()
         }
--- services/web/app/src/Features/Editor/EditorHttpController.js
+++ services/web/app/src/Features/Editor/EditorHttpController.js
@@ -67,8 +67,6 @@ async function joinProject(req, res, next) {
   if (!project) {
     return res.sendStatus(403)
   }
-  // Hide access tokens if this is not the project owner
-  TokenAccessHandler.protectTokens(project, privilegeLevel)
   // Hide sensitive data if the user is restricted
   if (isRestrictedUser) {
     project.owner = { _id: project.owner._id }
--- services/web/app/src/Features/Project/ProjectController.js
+++ services/web/app/src/Features/Project/ProjectController.js
@@ -343,7 +343,7 @@ const ProjectController = {
     const userId = SessionManager.getLoggedInUserId(req.session)
     ProjectGetter.findAllUsersProjects(
       userId,
-      'name lastUpdated publicAccesLevel archived trashed owner_ref tokens',
+      'name lastUpdated publicAccesLevel archived trashed owner_ref',
       (err, projects) => {
         if (err != null) {
           return next(err)
@@ -1072,7 +1072,6 @@ const ProjectController = {
     // If a project is simultaneously trashed and archived, we will consider it archived but not trashed.
     const trashed = ProjectHelper.isTrashed(project, userId) && !archived

-    TokenAccessHandler.protectTokens(project, accessLevel)
     const model = {
       id: project._id,
       name: project.name,
--- services/web/app/src/Features/Project/ProjectDetailsHandler.js
+++ services/web/app/src/Features/Project/ProjectDetailsHandler.js
@@ -207,14 +207,13 @@ async function ensureTokensArePresent(projectId) {
     project.tokens.readOnly != null &&
     project.tokens.readAndWrite != null
   ) {
-    return project.tokens
+    return
   }
   await _generateTokens(project)
   await Project.updateOne(
     { _id: projectId },
     { $set: { tokens: project.tokens } }
   ).exec()
-  return project.tokens
 }

 async function clearTokens(projectId) {
--- services/web/app/src/Features/Project/ProjectEditorHandler.js
+++ services/web/app/src/Features/Project/ProjectEditorHandler.js
@@ -49,7 +49,6 @@ module.exports = ProjectEditorHandler = {
       ),
       members: [],
       invites,
-      tokens: project.tokens,
       imageName:
         project.imageName != null
           ? Path.basename(project.imageName)
--- services/web/app/src/Features/TokenAccess/TokenAccessHandler.js
+++ services/web/app/src/Features/TokenAccess/TokenAccessHandler.js
@@ -246,22 +246,6 @@ const TokenAccessHandler = {
     })
   },

-  protectTokens(project, privilegeLevel) {
-    if (!project || !project.tokens) {
-      return
-    }
-    if (privilegeLevel === PrivilegeLevels.OWNER) {
-      return
-    }
-    if (privilegeLevel !== PrivilegeLevels.READ_AND_WRITE) {
-      project.tokens.readAndWrite = ''
-      project.tokens.readAndWritePrefix = ''
-    }
-    if (privilegeLevel !== PrivilegeLevels.READ_ONLY) {
-      project.tokens.readOnly = ''
-    }
-  },
-
   getV1DocPublishedInfo(token, callback) {
     // default to allowing access
     if (!Settings.apis.v1 || !Settings.apis.v1.url) {
@@ -304,7 +288,6 @@ TokenAccessHandler.promises = promisifyAll(TokenAccessHandler, {
     '_projectFindOne',
     'grantSessionTokenAccess',
     'getRequestToken',
-    'protectTokens',
   ],
   multiResult: {
     validateTokenForAnonymousAccess: ['isValidReadAndWrite', 'isValidReadOnly'],
--- services/web/frontend/js/features/share-project-modal/components/link-sharing.js
+++ services/web/frontend/js/features/share-project-modal/components/link-sharing.js
@@ -1,4 +1,4 @@
-import { useCallback, useState } from 'react'
+import { useCallback, useState, useEffect } from 'react'
 import PropTypes from 'prop-types'
 import { Button, Col, Row } from 'react-bootstrap'
 import { Trans } from 'react-i18next'
@@ -10,6 +10,8 @@ import CopyLink from '../../../shared/components/copy-link'
 import { useProjectContext } from '../../../shared/context/project-context'
 import * as eventTracking from '../../../infrastructure/event-tracking'
 import { useUserContext } from '../../../shared/context/user-context'
+import { getJSON } from '../../../infrastructure/fetch-json'
+import useAbortController from '../../../shared/hooks/use-abort-controller'

 export default function LinkSharing({ canAddCollaborators }) {
   const [inflight, setInflight] = useState(false)
@@ -27,8 +29,7 @@ export default function LinkSharing({ canAddCollaborators }) {
       )
         .then(() => {
           // NOTE: not calling `updateProject` here as it receives data via
-          // project:publicAccessLevel:changed and project:tokens:changed
-          // over the websocket connection
+          // project:publicAccessLevel:changed over the websocket connection
           // TODO: eventTracking.sendMB('project-make-token-based') when newPublicAccessLevel is 'tokenBased'
         })
         .finally(() => {
@@ -106,7 +107,17 @@ PrivateSharing.propTypes = {
 }

 function TokenBasedSharing({ setAccessLevel, inflight, canAddCollaborators }) {
-  const { tokens } = useProjectContext()
+  const { _id: projectId } = useProjectContext()
+
+  const [tokens, setTokens] = useState(null)
+
+  const { signal } = useAbortController()
+
+  useEffect(() => {
+    getJSON(`/project/${projectId}/tokens`, { signal })
+      .then(data => setTokens(data))
+      .catch(error => console.error(error))
+  }, [projectId, signal])

   return (
     <Row className="public-access-level">
@@ -194,7 +205,17 @@ LegacySharing.propTypes = {
 }

 export function ReadOnlyTokenLink() {
-  const { tokens } = useProjectContext()
+  const { _id: projectId } = useProjectContext()
+
+  const [tokens, setTokens] = useState(null)
+
+  const { signal } = useAbortController()
+
+  useEffect(() => {
+    getJSON(`/project/${projectId}/tokens`, { signal })
+      .then(data => setTokens(data))
+      .catch(error => console.error(error))
+  }, [projectId, signal])

   return (
     <Row className="public-access-level">
--- services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js
+++ services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js
@@ -31,16 +31,6 @@ export default App.controller(
       })
     }

-    /* tokens */
-
-    ide.socket.on('project:tokens:changed', data => {
-      if (data.tokens != null) {
-        $scope.$applyAsync(() => {
-          $scope.project.tokens = data.tokens
-        })
-      }
-    })
-
     ide.socket.on('project:membership:changed', data => {
       if (data.members) {
         listProjectMembers($scope.project._id)
--- services/web/frontend/js/shared/context/mock/mock-ide.js
+++ services/web/frontend/js/shared/context/mock/mock-ide.js
@@ -27,10 +27,6 @@ export const getMockIde = () => {
           zotero: false,
         },
         publicAccessLevel: '',
-        tokens: {
-          readOnly: '',
-          readAndWrite: '',
-        },
         owner: {
           _id: '',
           email: '',
--- services/web/frontend/js/shared/context/project-context.js
+++ services/web/frontend/js/shared/context/project-context.js
@@ -28,10 +28,6 @@ export const projectShape = {
     versioning: PropTypes.bool,
   }),
   publicAccessLevel: PropTypes.string,
-  tokens: PropTypes.shape({
-    readOnly: PropTypes.string,
-    readAndWrite: PropTypes.string,
-  }),
   owner: PropTypes.shape({
     _id: PropTypes.string.isRequired,
     email: PropTypes.string.isRequired,
@@ -81,7 +77,6 @@ export function ProjectProvider({ children }) {
     invites,
     features,
     publicAccesLevel: publicAccessLevel,
-    tokens,
     owner,
   } = project || projectFallback

@@ -94,7 +89,6 @@ export function ProjectProvider({ children }) {
       invites,
       features,
       publicAccessLevel,
-      tokens,
       owner,
     }
   }, [
@@ -105,7 +99,6 @@ export function ProjectProvider({ children }) {
     invites,
     features,
     publicAccessLevel,
-    tokens,
     owner,
   ])
