diff --git a/shatter-backend/src/controllers/auth_controller.ts b/shatter-backend/src/controllers/auth_controller.ts index 12bf3d2..7d23c0c 100644 --- a/shatter-backend/src/controllers/auth_controller.ts +++ b/shatter-backend/src/controllers/auth_controller.ts @@ -2,6 +2,7 @@ import crypto from 'crypto'; import jwt from 'jsonwebtoken'; import { Request, Response } from 'express'; import { User } from '../models/user_model'; +import { AuthCode } from '../models/auth_code_model'; import { hashPassword, comparePassword } from '../utils/password_hash'; import { generateToken } from '../utils/jwt_utils'; import { getLinkedInAuthUrl, getLinkedInAccessToken, getLinkedInProfile } from '../utils/linkedin_oauth'; @@ -281,9 +282,10 @@ export const linkedinCallback = async (req: Request, res: Response) => { await user.save(); } - // Generate JWT token and redirect to frontend - const token = generateToken(user._id.toString()); - return res.redirect(`${frontendUrl}/auth/callback?token=${token}&userId=${user._id}`); + // Generate single-use auth code and redirect to frontend + const authCode = crypto.randomBytes(32).toString('hex'); + await AuthCode.create({ code: authCode, userId: user._id }); + return res.redirect(`${frontendUrl}/auth/callback?code=${authCode}`); } catch (error: any) { console.error('LinkedIn callback error:', error); @@ -297,3 +299,41 @@ export const linkedinCallback = async (req: Request, res: Response) => { } }; + +/** + * POST /api/auth/exchange + * Exchange a single-use auth code for a JWT token + * + * @param req.body.code - The auth code from the OAuth callback redirect + * @returns 200 with userId and JWT token on success + */ +export const exchangeAuthCode = async (req: Request, res: Response) => { + try { + const { code } = req.body as { code?: string }; + + if (!code) { + return res.status(400).json({ error: 'Auth code is required' }); + } + + // Find and delete the auth code in one atomic operation (single-use) + const authCodeDoc = await AuthCode.findOneAndDelete({ code }); + + if (!authCodeDoc) { + return res.status(401).json({ error: 'Invalid or expired auth code' }); + } + + // Generate JWT token + const token = generateToken(authCodeDoc.userId.toString()); + + res.status(200).json({ + message: 'Authentication successful', + userId: authCodeDoc.userId, + token, + }); + + } catch (err: any) { + console.error('POST /api/auth/exchange error:', err); + res.status(500).json({ error: 'Failed to exchange auth code' }); + } +}; + diff --git a/shatter-backend/src/models/auth_code_model.ts b/shatter-backend/src/models/auth_code_model.ts new file mode 100644 index 0000000..8a593d8 --- /dev/null +++ b/shatter-backend/src/models/auth_code_model.ts @@ -0,0 +1,28 @@ +import { Schema, model } from 'mongoose'; + +export interface IAuthCode { + code: string; + userId: Schema.Types.ObjectId; + createdAt: Date; +} + +const AuthCodeSchema = new Schema({ + code: { + type: String, + required: true, + unique: true, + index: true, + }, + userId: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + }, + createdAt: { + type: Date, + default: Date.now, + expires: 60, // TTL: auto-deletes after 60 seconds + }, +}); + +export const AuthCode = model('AuthCode', AuthCodeSchema); diff --git a/shatter-backend/src/routes/auth_routes.ts b/shatter-backend/src/routes/auth_routes.ts index 7747523..e1487fc 100644 --- a/shatter-backend/src/routes/auth_routes.ts +++ b/shatter-backend/src/routes/auth_routes.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { signup, login, linkedinAuth, linkedinCallback } from '../controllers/auth_controller'; +import { signup, login, linkedinAuth, linkedinCallback, exchangeAuthCode } from '../controllers/auth_controller'; const router = Router(); @@ -13,4 +13,7 @@ router.post('/login', login); router.get('/linkedin', linkedinAuth); router.get('/linkedin/callback', linkedinCallback); +// Auth code exchange (OAuth callback → JWT) +router.post('/exchange', exchangeAuthCode); + export default router;