From e0f37c1e520b5e4242dbcb8ff350c278f9503178 Mon Sep 17 00:00:00 2001 From: Z User Date: Fri, 27 Mar 2026 23:16:09 +0000 Subject: [PATCH] Add dedicated auth routes for token exchange - Create /api/auth routes for authentication - Add POST /api/auth/token for code-to-token exchange - Add GET /api/auth/callback for redirect-based flow - Add GET /api/auth/config for frontend config - Backend handles token exchange with client secret - Works with Regular Web Application Auth0 type --- src/index.js | 79 ++------------------------ src/routes/auth.js | 139 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 73 deletions(-) create mode 100644 src/routes/auth.js diff --git a/src/index.js b/src/index.js index 38146ce..20ea6d1 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,7 @@ import { dirname, join } from 'path'; import usersRouter from './routes/users.js'; import webhooksRouter from './routes/webhooks.js'; +import authRouter from './routes/auth.js'; import { ApiResponse } from './utils/helpers.js'; import { validateAccessToken, ensureUserExists } from './middleware/auth.js'; import { initDatabase, waitForDb } from './db/database.js'; @@ -109,82 +110,14 @@ app.get('/api', (req, res) => { }); // ============================================ -// Auth0 Routes +// Auth Routes // ============================================ -/** - * @route GET /api/login - * @desc Initiate Auth0 login - * @access Public - */ -app.get('/api/login', (req, res) => { - const redirectUri = encodeURIComponent(`${process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'}/api/callback`); - const authUrl = `https://${AUTH0_DOMAIN}/authorize?response_type=code&client_id=${AUTH0_CLIENT_ID}&redirect_uri=${redirectUri}&scope=openid%20profile%20email`; - res.redirect(authUrl); -}); +app.use('/api/auth', authRouter); -/** - * @route GET /api/callback - * @desc Handle Auth0 callback (exchange code for tokens) - * @access Public - * - * Note: This is a basic implementation. In production, you'd want to: - * 1. Exchange the code for tokens server-side - * 2. Set secure HTTP-only cookies - * 3. Or redirect back to frontend with tokens - */ -app.get('/api/callback', async (req, res) => { - const { code, error, error_description } = req.query; - - if (error) { - console.error('Auth0 error:', error, error_description); - return res.redirect(`${process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'}?error=${encodeURIComponent(error_description || error)}`); - } - - if (!code) { - return res.redirect(`${process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'}?error=no_code`); - } - - try { - // Exchange code for tokens - const tokenResponse = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - grant_type: 'authorization_code', - client_id: AUTH0_CLIENT_ID, - client_secret: process.env.AUTH0_CLIENT_SECRET, - code, - redirect_uri: `${process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'}/api/callback` - }) - }); - - const tokens = await tokenResponse.json(); - - if (tokens.error) { - throw new Error(tokens.error_description || tokens.error); - } - - // Redirect to frontend with tokens - // In production, consider using HTTP-only cookies instead - const frontendUrl = process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'; - res.redirect(`${frontendUrl}/?access_token=${tokens.access_token}&id_token=${tokens.id_token}&expires_in=${tokens.expires_in}`); - } catch (err) { - console.error('Token exchange error:', err); - res.redirect(`${process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'}?error=${encodeURIComponent(err.message)}`); - } -}); - -/** - * @route GET /api/logout - * @desc Logout and redirect to Auth0 logout - * @access Public - */ -app.get('/api/logout', (req, res) => { - const returnTo = encodeURIComponent(process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'); - const logoutUrl = `https://${AUTH0_DOMAIN}/v2/logout?client_id=${AUTH0_CLIENT_ID}&returnTo=${returnTo}`; - res.redirect(logoutUrl); -}); +// Legacy routes for backwards compatibility +app.get('/api/login', (req, res) => res.redirect('/api/auth/login')); +app.get('/api/logout', (req, res) => res.redirect('/api/auth/logout')); // ============================================ // API Routes diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000..be36b50 --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,139 @@ +import express from 'express'; +import { dbGet, dbRun, waitForDb } from '../db/database.js'; +import { generateId, ApiResponse, asyncHandler } from '../utils/helpers.js'; + +const router = express.Router(); + +const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN || 'dev-t13zhs74oltgqtfx.us.auth0.com'; +const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID; +const AUTH0_CLIENT_SECRET = process.env.AUTH0_CLIENT_SECRET; + +/** + * @route GET /api/auth/callback + * @desc Exchange Auth0 code for tokens (called by frontend) + * @access Public + */ +router.get('/callback', asyncHandler(async (req, res) => { + const { code, error, error_description } = req.query; + + if (error) { + console.error('Auth0 error:', error, error_description); + return res.redirect(`${process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'}?error=${encodeURIComponent(error_description || error)}`); + } + + if (!code) { + return res.redirect(`${process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'}?error=no_code`); + } + + try { + // Exchange code for tokens + const tokenResponse = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + grant_type: 'authorization_code', + client_id: AUTH0_CLIENT_ID, + client_secret: AUTH0_CLIENT_SECRET, + code, + redirect_uri: `${process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'}/dashboard.html` + }) + }); + + const tokens = await tokenResponse.json(); + + if (tokens.error) { + console.error('Token exchange error:', tokens.error); + throw new Error(tokens.error_description || tokens.error); + } + + // Redirect to frontend with tokens + const frontendUrl = process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'; + res.redirect(`${frontendUrl}/dashboard.html?access_token=${tokens.access_token}&id_token=${tokens.id_token}&expires_in=${tokens.expires_in}`); + + } catch (err) { + console.error('Token exchange error:', err); + res.redirect(`${process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'}?error=${encodeURIComponent(err.message)}`); + } +})); + +/** + * @route POST /api/auth/token + * @desc Exchange code for tokens (alternative POST method) + * @access Public + */ +router.post('/token', asyncHandler(async (req, res) => { + const { code, redirect_uri } = req.body; + + if (!code) { + return res.status(400).json(ApiResponse(false, null, 'Authorization code required')); + } + + try { + const tokenResponse = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + grant_type: 'authorization_code', + client_id: AUTH0_CLIENT_ID, + client_secret: AUTH0_CLIENT_SECRET, + code, + redirect_uri: redirect_uri || `${process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'}/dashboard.html` + }) + }); + + const tokens = await tokenResponse.json(); + + if (tokens.error) { + return res.status(400).json(ApiResponse(false, null, tokens.error_description || tokens.error)); + } + + res.json(ApiResponse(true, { + access_token: tokens.access_token, + id_token: tokens.id_token, + expires_in: tokens.expires_in, + token_type: tokens.token_type + })); + + } catch (err) { + console.error('Token exchange error:', err); + res.status(500).json(ApiResponse(false, null, err.message)); + } +})); + +/** + * @route GET /api/auth/login + * @desc Get Auth0 login URL + * @access Public + */ +router.get('/login', (req, res) => { + const redirectUri = encodeURIComponent(`${process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'}/dashboard.html`); + const scope = encodeURIComponent('openid profile email'); + const authUrl = `https://${AUTH0_DOMAIN}/authorize?response_type=code&client_id=${AUTH0_CLIENT_ID}&redirect_uri=${redirectUri}&scope=${scope}`; + res.redirect(authUrl); +}); + +/** + * @route GET /api/auth/logout + * @desc Logout URL + * @access Public + */ +router.get('/logout', (req, res) => { + const returnTo = encodeURIComponent(process.env.APP_URL || 'https://moxiegen.client.guacamolebox.net'); + const logoutUrl = `https://${AUTH0_DOMAIN}/v2/logout?client_id=${AUTH0_CLIENT_ID}&returnTo=${returnTo}`; + res.redirect(logoutUrl); +}); + +/** + * @route GET /api/auth/config + * @desc Get Auth0 public config for frontend + * @access Public + */ +router.get('/config', (req, res) => { + res.json(ApiResponse(true, { + domain: AUTH0_DOMAIN, + clientId: AUTH0_CLIENT_ID, + audience: process.env.AUTH0_AUDIENCE || `https://${AUTH0_DOMAIN}/api/v2/` + })); +}); + +export default router;