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
This commit is contained in:
Z User 2026-03-27 23:16:09 +00:00
parent e8919fc985
commit e0f37c1e52
2 changed files with 145 additions and 73 deletions

View File

@ -7,6 +7,7 @@ import { dirname, join } from 'path';
import usersRouter from './routes/users.js'; import usersRouter from './routes/users.js';
import webhooksRouter from './routes/webhooks.js'; import webhooksRouter from './routes/webhooks.js';
import authRouter from './routes/auth.js';
import { ApiResponse } from './utils/helpers.js'; import { ApiResponse } from './utils/helpers.js';
import { validateAccessToken, ensureUserExists } from './middleware/auth.js'; import { validateAccessToken, ensureUserExists } from './middleware/auth.js';
import { initDatabase, waitForDb } from './db/database.js'; import { initDatabase, waitForDb } from './db/database.js';
@ -109,82 +110,14 @@ app.get('/api', (req, res) => {
}); });
// ============================================ // ============================================
// Auth0 Routes // Auth Routes
// ============================================ // ============================================
/** app.use('/api/auth', authRouter);
* @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);
});
/** // Legacy routes for backwards compatibility
* @route GET /api/callback app.get('/api/login', (req, res) => res.redirect('/api/auth/login'));
* @desc Handle Auth0 callback (exchange code for tokens) app.get('/api/logout', (req, res) => res.redirect('/api/auth/logout'));
* @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);
});
// ============================================ // ============================================
// API Routes // API Routes

139
src/routes/auth.js Normal file
View File

@ -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;