Skip to content

Commit

Permalink
ISSUE #5412 verified teamspace auth is working
Browse files Browse the repository at this point in the history
  • Loading branch information
carmenfan committed Mar 2, 2025
1 parent bedd887 commit 41fdebd
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 52 deletions.
22 changes: 21 additions & 1 deletion backend/src/v5/middleware/permissions/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,32 @@ const {
const { isTeamspaceAdmin, isTeamspaceMember } = require('./components/teamspaces');
const { isProjectAdmin } = require('./components/projects');
const { modelTypes } = require('../../models/modelSettings.constants');
const { respond } = require('../../utils/responder');
const { templates } = require('../../utils/responseCodes');
const { validSession } = require('../auth');
const { validateMany } = require('../common');

const Permissions = {};

Permissions.hasAccessToTeamspace = validateMany([convertAllUUIDs, validSession, isTeamspaceMember]);
const isCookieAuthenticatedAgainstTS = async (req, res, next) => {
if (!req.session.user?.auth) {
// No auth info - this is an apiKey.
await next();
}

const authenticatedTeamspace = req.session.user.auth?.authorisedTeamspace;
if (authenticatedTeamspace === req.params.teamspace) {
await next();
} else {
respond(req, res, templates.notAuthenticatedAgainstTeamspace);
}
};

// Use this one when you don't care if the user is authenticated against the teamspace
Permissions.isMemberOfTeamspace = validateMany([convertAllUUIDs, validSession, isTeamspaceMember]);
// Use this one when you want to make sure the user session is authenticated against the teamspace
Permissions.hasAccessToTeamspace = validateMany([
Permissions.isMemberOfTeamspace, isCookieAuthenticatedAgainstTS]);
Permissions.isTeamspaceAdmin = validateMany([Permissions.hasAccessToTeamspace, isTeamspaceAdmin]);
Permissions.isAdminToProject = validateMany([Permissions.hasAccessToTeamspace, isProjectAdmin]);

Expand Down
81 changes: 40 additions & 41 deletions backend/src/v5/middleware/sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,27 +42,48 @@ Sessions.manageSessions = async (req, res, next) => {
middleware(req, res, next);
};

const updateSessionDetails = (req) => {
const updatedUser = { ...req.loginData, webSession: false };
Sessions.destroySession = (req, res) => {
const username = req.session?.user?.username;

try {
const sessionData = { user: { username: req.session?.user?.username } };
const callback = () => respond({ ...req, session: sessionData }, res, templates.ok,
req.v4 ? { username } : undefined);
destroySession(req.session, res, callback, true);
} catch (err) {
// istanbul ignore next
respond(req, res, err);
}
};

Sessions.appendCSRFToken = async (req, res, next) => {
const { domain, maxAge } = config.cookie;
const token = generateUUIDString();
res.cookie(CSRF_COOKIE, token, { httpOnly: false, secure: true, sameSite: 'Strict', maxAge, domain });
req.token = token;
await next();
};

Sessions.updateSession = async (req, res, next) => {
const { session } = req;
if (req.token) {
session.token = req.token;
}

const { ssoInfo: { userAgent, referer } } = req.session;
const updatedUser = { ...req.loginData, webSession: session?.user?.webSession || false };
// If there is ssoInfo, this is a new session
const { ssoInfo: { userAgent, referer } } = session;
if (referer) {
updatedUser.referer = referer;
}

delete req.session.ssoInfo;

if (userAgent) {
updatedUser.webSession = isFromWebBrowser(userAgent);
updatedUser.userAgent = userAgent;
}

if (req.token) {
session.token = req.token;
}
delete req.session.ssoInfo;

session.user = deleteIfUndefined(updatedUser);
session.cookie.domain = config.cookie_domain;

if (config.cookie.maxAge) {
Expand All @@ -72,41 +93,19 @@ const updateSessionDetails = (req) => {
const ipAddress = req.ips[0] || req.ip;
session.ipAddress = ipAddress;

publish(events.SESSION_CREATED, {
username: updatedUser.username,
sessionID: req.sessionID,
ipAddress,
userAgent,
socketId: req.headers[SOCKET_HEADER],
referer: updatedUser.referer });

return session;
};

Sessions.destroySession = (req, res) => {
const username = req.session?.user?.username;

try {
const sessionData = { user: { username: req.session?.user?.username } };
const callback = () => respond({ ...req, session: sessionData }, res, templates.ok,
req.v4 ? { username } : undefined);
destroySession(req.session, res, callback, true);
} catch (err) {
// istanbul ignore next
respond(req, res, err);
if (!session.reAuth) {
publish(events.SESSION_CREATED, {
username: updatedUser.username,
sessionID: req.sessionID,
ipAddress,
userAgent,
socketId: req.headers[SOCKET_HEADER],
referer: updatedUser.referer });
}
};

Sessions.appendCSRFToken = async (req, res, next) => {
const { domain, maxAge } = config.cookie;
const token = generateUUIDString();
res.cookie(CSRF_COOKIE, token, { httpOnly: false, secure: true, sameSite: 'Strict', maxAge, domain });
req.token = token;
await next();
};
delete req.session.reAuth;
session.user = deleteIfUndefined(updatedUser);

Sessions.updateSession = async (req, res, next) => {
updateSessionDetails(req);
await next();
};

Expand Down
11 changes: 9 additions & 2 deletions backend/src/v5/middleware/sso/frontegg.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const { addPkceProtection } = require('./pkce');
const { createNewUserRecord } = require('../../processors/users');
const { destroySession } = require('../../utils/sessions');
const { errorCodes } = require('../../services/sso/sso.constants');
const { getTeamspaceRefId } = require('../../models/teamspaceSettings');
const { logger } = require('../../utils/logger');
const { respond } = require('../../utils/responder');
const { validateMany } = require('../common');
Expand Down Expand Up @@ -116,13 +117,19 @@ const getToken = (urlUsed) => async (req, res, next) => {
}
};

const redirectForAuth = (redirectURL) => (req, res) => {
const redirectForAuth = (redirectURL) => async (req, res) => {
try {
if (!req.query.redirectUri) {
respond(req, res, createResponseCode(templates.invalidArguments, 'redirectUri(query string) is required'));
return;
}

let accountId;
if (req.params.teamspace) {
accountId = await getTeamspaceRefId(req.params.teamspace);
req.session.reAuth = true;
}

req.authParams = {
redirectURL,
state: toBase64(JSON.stringify({
Expand All @@ -132,7 +139,7 @@ const redirectForAuth = (redirectURL) => (req, res) => {
codeChallenge: req.session.pkceCodes.challenge,
};

const link = generateAuthenticationCodeUrl(req.authParams);
const link = generateAuthenticationCodeUrl(req.authParams, accountId);
respond(req, res, templates.ok, { link });
} catch (err) {
respond(req, res, err);
Expand Down
2 changes: 1 addition & 1 deletion backend/src/v5/middleware/sso/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ const setSessionInfo = async (req, res, next) => {
};

req.session.ssoInfo = ssoInfo;
req.session.token = req.token;

req.session.token = req.token;
await next();
};

Expand Down
7 changes: 6 additions & 1 deletion backend/src/v5/models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,12 @@ User.removeUser = (user) => db.deleteOne(USERS_DB_NAME, USERS_COL, { user });
User.removeUsers = (users) => db.deleteMany(USERS_DB_NAME, USERS_COL, { user: { $in: users } });

User.ensureIndicesExist = async () => {
await db.createIndex(USERS_DB_NAME, USERS_COL, { 'customData.userId': 1 }, { runInBackground: true, unique: true });
try {
await db.createIndex(USERS_DB_NAME, USERS_COL, { 'customData.userId': 1 }, { runInBackground: true, unique: true });
} catch (err) {
// Note this will fail pre 5.16 migration.
logger.logWarning('Failed to create index on user ID. Please ensure 5.16 migration script has been executed.');

Check failure on line 169 in backend/src/v5/models/users.js

View workflow job for this annotation

GitHub Actions / Run Backend lint

'logger' is not defined

Check failure on line 169 in backend/src/v5/models/users.js

View workflow job for this annotation

GitHub Actions / Run Backend lint

'logger' is not defined
}
};

module.exports = User;
43 changes: 39 additions & 4 deletions backend/src/v5/routes/authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
*/

const { generateLinkToAuthenticator, generateToken, redirectToStateURL } = require('../middleware/sso/frontegg');
const { isLoggedIn, notLoggedIn } = require('../middleware/auth');
const { Router } = require('express');
const { createEndpointURL } = require('../utils/config');
const { notLoggedIn } = require('../middleware/auth');
const { isMemberOfTeamspace } = require('../middleware/permissions/permissions');
const { updateSession } = require('../middleware/sessions');

const AUTH_POST = '/authenticate-post';
Expand All @@ -31,7 +32,7 @@ const establishRoutes = () => {
* @openapi
* /authentication/authenticate:
* get:
* description: Returns a link 3DR's authentication page and then to a URI provided upon success. The process works like the standard SSO protocol.
* description: General authentication route to establish a session.
* tags: [Authentication]
* operationId: authenticate
* parameters:
Expand All @@ -42,7 +43,7 @@ const establishRoutes = () => {
* description: a URI to redirect to when authentication finished
* responses:
* 200:
* description: returns a link to 3D Repo's authentication page and then to a provided URI upon success
* description: Returns a link to 3DR's authentication page and then redirects to a URI provided upon success. The process works like the standard SSO protocol.
* content:
* application/json:
* schema:
Expand All @@ -55,8 +56,42 @@ const establishRoutes = () => {
*/
router.get('/authenticate', notLoggedIn, generateLinkToAuthenticator(authenticateRedirectUrl));

/**
* @openapi
* /authentication/authenticate/{teamspace}:
* get:
* description: Authenticates a user against a particular teamspace, the user has to have already established a session to use this endpoint.
* tags: [Authentication]
* operationId: authenticate
* parameters:
* - in: path
* name: teamspace
* description: Name of the teamspace to authenticate against
* required: true
* schema:
* type: string
* - in: query
* name: redirectUri
* schema:
* type: string
* description: a URI to redirect to when authentication finished
* responses:
* 200:
* description: Returns a link to 3DR's authentication page and then redirects to a URI provided upon success. The process works like the standard SSO protocol.
* content:
* application/json:
* schema:
* type: object
* properties:
* link:
* type: string
* description: link to 3D Repo's authenticator
*
*/
router.get('/authenticate/:teamspace', isLoggedIn, isMemberOfTeamspace, generateLinkToAuthenticator(authenticateRedirectUrl));

// This endpoint is not exposed in swagger as it is not designed to be called by clients
router.get(AUTH_POST, notLoggedIn, generateToken(authenticateRedirectUrl), updateSession, redirectToStateURL);
router.get(AUTH_POST, generateToken(authenticateRedirectUrl), updateSession, redirectToStateURL);

return router;
};
Expand Down
5 changes: 3 additions & 2 deletions backend/src/v5/utils/responseCodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ ResponseCodes.templates = {
// Auth
notLoggedIn: { message: 'You are not logged in.', status: 401 },
alreadyLoggedIn: { message: 'You are already logged in.', status: 401 },
notAuthorized: { message: 'You do not have sufficient access rights for this action.', status: 401 },
licenceExpired: { message: 'Licence expired.', status: 401 },
notAuthenticatedAgainstTeamspace: { message: 'You are not authenticated against this teamspace.', status: 401 },
notAuthorized: { message: 'You do not have sufficient access rights for this action.', status: 403 },
licenceExpired: { message: 'Licence expired.', status: 403 },
incorrectUsernameOrPassword: { message: 'Incorrect username or password.', status: 400 },
incorrectPassword: { message: 'Incorrect password.', status: 400 },

Expand Down

0 comments on commit 41fdebd

Please sign in to comment.