Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions shatter-backend/src/controllers/auth_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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' });
}
};

28 changes: 28 additions & 0 deletions shatter-backend/src/models/auth_code_model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Schema, model } from 'mongoose';

export interface IAuthCode {
code: string;
userId: Schema.Types.ObjectId;
createdAt: Date;
}

const AuthCodeSchema = new Schema<IAuthCode>({
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<IAuthCode>('AuthCode', AuthCodeSchema);
5 changes: 4 additions & 1 deletion shatter-backend/src/routes/auth_routes.ts
Original file line number Diff line number Diff line change
@@ -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();

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