// OAuth endpoints. // GET /CollabCentral/:app/authUrl — returns the Webex authorize URL // the browser should redirect to. // GET /CollabCentral/:app/oauth — Webex redirects the user back to // this URL with a `code` query // param; we exchange it for tokens // and set the `id`+`displayName` // session cookies. // // registerAuthRoutes(app, { buildAuthUrl, getOAuthRedirectUri, webex, // env, logger }) // Options for the session cookies set after a successful OAuth round-trip. // // httpOnly is deliberately false: js/app.js reads `id` (to decide whether // to redirect to OAuth) and `displayName` (to render the user chip in the // header) via document.cookie. Enabling httpOnly here hides the cookies // from JS, which causes an infinite auth loop where every page load thinks // the user is not signed in and kicks off a fresh OAuth exchange — burning // Webex tokens until the CTS token limit is reached. // // sameSite is 'lax' rather than 'strict' so the cookie survives the OAuth // redirect chain webexapis.com -> /oauth -> /sendMessage.html on all // browsers. const SESSION_COOKIE_OPTIONS = { httpOnly: false, secure: true, sameSite: 'lax', maxAge: 24 * 60 * 60 * 1000 }; export function registerAuthRoutes(app, deps) { var buildAuthUrl = deps.buildAuthUrl; var getOAuthRedirectUri = deps.getOAuthRedirectUri; var webex = deps.webex; var env = deps.env || {}; var logger = deps.logger || function () {}; app.get('/CollabCentral/:app/authUrl', function (req, res) { var url = buildAuthUrl(req.params.app); if (!url) { return res.status(500).send('OAuth is not configured. Check WEBEX_INTEGRATION_CLIENT_ID and OAUTH_CALLBACK_URL_TEMPLATE.'); } res.send(url); }); app.get('/CollabCentral/:app/oauth', async function (req, res) { var appName = req.params.app; var authCode = req.query.code; if (!env.WEBEX_INTEGRATION_CLIENT_ID || !env.WEBEX_INTEGRATION_CLIENT_SECRET || !env.OAUTH_CALLBACK_URL_TEMPLATE) { logger('oauth', 'Missing Webex integration env vars; cannot complete OAuth.'); return res.status(500).send('OAuth is not configured on the server.'); } if (!authCode) { return res.status(400).send('Missing OAuth `code` query parameter.'); } var url = new URL('https://webexapis.com/v1/access_token'); var params = new URLSearchParams(); params.append('grant_type', 'authorization_code'); params.append('client_id', env.WEBEX_INTEGRATION_CLIENT_ID); params.append('client_secret', env.WEBEX_INTEGRATION_CLIENT_SECRET); params.append('code', authCode); params.append('redirect_uri', getOAuthRedirectUri(appName)); var response; try { response = await webex.fetchWithRateLimit(url, { method: 'POST', body: params }); } catch (error) { logger('oauth', 'Token exchange fetch failed: ' + (error && error.message || error)); return res.status(502).send('Failed to reach Webex to complete OAuth.'); } if (!response.ok) { var errText = await response.text().catch(() => ''); logger('oauth', 'Token exchange returned ' + response.status + ' ' + response.statusText + ' ' + errText); return res.status(401).send(response.statusText || 'OAuth token exchange failed.'); } var jsonData = await response.json(); var whoami; try { whoami = await webex.whoAmI(jsonData.access_token); } catch (err) { logger('oauth', 'whoAmI failed: ' + (err && err.message || err)); return res.status(502).send('Failed to read your Webex profile after OAuth.'); } logger('oauth', whoami.displayName + ' successfully authed for ' + appName + '.'); // Only the two cookies that the server (req.cookies.id, // .displayName) and the client (getCookie('id'), // getCookie('displayName')) actually read. Access/refresh tokens // deliberately stay out of the browser: they never need to leave // the server, and putting them in cookies would expose them to // any XSS that might slip in later. res .cookie('id', whoami.id, SESSION_COOKIE_OPTIONS) .cookie('displayName', whoami.displayName, SESSION_COOKIE_OPTIONS) .redirect(301, '/CollabCentral/' + appName + '/sendMessage.html'); }); }