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:
parent
e8919fc985
commit
e0f37c1e52
79
src/index.js
79
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
|
||||
|
||||
139
src/routes/auth.js
Normal file
139
src/routes/auth.js
Normal 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;
|
||||
Loading…
Reference in New Issue
Block a user