W
Wasp2w ago
NEROX

API auth with a Chrome Extension (third party)

I'm building a Chrome extension which makes Auth calls to my app's API. What this extension does is to access the local storage to get the wasp:sessionId and with it make the API calls. The problem I have is that I need to have the app page open to access the wasp:sessionId. Isn't it unsafe to store this sessionId in the extension? For that reason before making a call, I access the value in local storage. Maybe this approach is not the right one. Maybe this is not related with Wasp
5 Replies
kapa.ai
kapa.ai2w ago
Hi @NEROX. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- Your question is definitely related to Wasp, as it concerns authentication in a Wasp application and how to securely connect a Chrome extension to it. Based on the knowledge sources, Wasp's authentication system has evolved over time. Currently, Wasp uses sessions for authentication, with session data stored in localStorage on the client and in the database on the server, leveraging the Lucia Auth v3 library for session management Docs > Auth > Overview > Sessions. Regarding your approach with the Chrome extension, there are a few considerations: 1. Security concerns: Accessing localStorage directly from a Chrome extension to get the session ID does raise security concerns. The Wasp team has previously discussed the security implications of storing authentication tokens in localStorage versus cookies, with cookies generally considered more secure for web applications GitHub Issue #573. 2. Alternative approach: For third-party applications like Chrome extensions, a more secure approach would be to create a dedicated authentication flow. One community member suggested: "use custom API endpoints and have a way to create JWT tokens... Store the JWT tokens in local storage. What you put in the JWT? Probably just some user ID would be enough. On each request to your custom API, send that JWT in the headers." Discord support thread 3. API communication: For securing the communication between your extension and backend, using HTTPS should provide sufficient security for the data in transit. A better approach might be to implement a specific authentication endpoint in your Wasp app that generates a token specifically for your extension. This way, you don't need to rely on accessing the main app's session directly. The Wasp team has also discussed supporting multiple clients, which could be relevant to your use case: "provide SDK or multiple SDKs, let's say for JS, that allow you to interact with basic functionality of your app via this SDK" GitHub Issue #1475. In summary, rather than accessing the wasp:sessionId from the main app's localStorage, consider implementing a dedicated authentication flow for your extension with its own tokens and storage mechanism. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @NEROX, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
NEROX
NEROXOP2w ago
@kapa.ai Explain me the full process of implementing JWT making sure that the JWT is from the device you are currently on. Have a secret also at the user level.
kapa.ai
kapa.ai2w ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Based on the knowledge sources, I can explain how to implement JWT authentication in a Wasp application with device-specific validation and user-level secrets. Here's the process: 1. Set up JWT Secret First, you need to set up a JWT secret for your Wasp application. This should be stored as an environment variable:
JWT_SECRET=your_secure_random_string
JWT_SECRET=your_secure_random_string
Wasp previously had hardcoded JWT secrets, but this was changed to use environment variables for security reasons GitHub Issue #129. The JWT secret should never be hardcoded in your application code. 2. Create a JWT Generation Endpoint You'll need to create a custom API endpoint to generate JWTs. Here's how you can define it in your main.wasp file:
api generateJwt {
fn: import { generateJwt } from "@src/server/api/generateJwt.js",
entities: [User],
httpRoute: (POST, "/api/generate-jwt")
}
api generateJwt {
fn: import { generateJwt } from "@src/server/api/generateJwt.js",
entities: [User],
httpRoute: (POST, "/api/generate-jwt")
}
3. Implement the JWT Generation Function In your server code, implement the JWT generation function:
// generateJwt.ts
import * as jwt from 'jsonwebtoken';
import { Request, Response } from 'express';

// Authenticate user and return user ID if successful
async function authenticateUser(username: string, password: string): Promise<{ isAuthenticated: boolean; userId?: string }> {
// Implement actual user authentication logic here
// This should fetch the user from the database and verify credentials
// For demonstration, assuming authentication is successful
return { isAuthenticated: true, userId: 'user123' };
}
export async function generateJwt(req: Request, res: Response): Promise<void> {
const { username, password, deviceId } = req.body;

try {
const authResult = await authenticateUser(username, password);

if (!authResult.isAuthenticated) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}

// Get user-specific secret from database (could be stored in user profile)
const userSecret = await getUserSecret(authResult.userId);

// User payload for JWT including device identifier
const userPayload = {
username,
userId: authResult.userId,
deviceId: deviceId // Include device identifier in the token
};

// Retrieve the secret key from environment variables
const secretKey = process.env.JWT_SECRET;

if (!secretKey) {
throw new Error('JWT secret key is missing');
}

// Combine application secret with user-specific secret
const combinedSecret = `${secretKey}${userSecret}`;

// Token options
const options = { expiresIn: '1h' }; // Token expires in 1 hour

// Generate the JWT
const token = jwt.sign(userPayload, combinedSecret, options);

// Send the JWT back to the client
res.json({ token });

} catch (error) {
console.error('Error generating JWT token:', error);
res.status(500).json({ error: 'Internal server error' });
}
}

// Function to get user-specific secret
async function getUserSecret(userId: string): Promise<string> {
// Implement logic to retrieve user-specific secret from database
// This could be a field in your User model
return 'user_specific_secret';
}
// generateJwt.ts
import * as jwt from 'jsonwebtoken';
import { Request, Response } from 'express';

// Authenticate user and return user ID if successful
async function authenticateUser(username: string, password: string): Promise<{ isAuthenticated: boolean; userId?: string }> {
// Implement actual user authentication logic here
// This should fetch the user from the database and verify credentials
// For demonstration, assuming authentication is successful
return { isAuthenticated: true, userId: 'user123' };
}
export async function generateJwt(req: Request, res: Response): Promise<void> {
const { username, password, deviceId } = req.body;

try {
const authResult = await authenticateUser(username, password);

if (!authResult.isAuthenticated) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}

// Get user-specific secret from database (could be stored in user profile)
const userSecret = await getUserSecret(authResult.userId);

// User payload for JWT including device identifier
const userPayload = {
username,
userId: authResult.userId,
deviceId: deviceId // Include device identifier in the token
};

// Retrieve the secret key from environment variables
const secretKey = process.env.JWT_SECRET;

if (!secretKey) {
throw new Error('JWT secret key is missing');
}

// Combine application secret with user-specific secret
const combinedSecret = `${secretKey}${userSecret}`;

// Token options
const options = { expiresIn: '1h' }; // Token expires in 1 hour

// Generate the JWT
const token = jwt.sign(userPayload, combinedSecret, options);

// Send the JWT back to the client
res.json({ token });

} catch (error) {
console.error('Error generating JWT token:', error);
res.status(500).json({ error: 'Internal server error' });
}
}

// Function to get user-specific secret
async function getUserSecret(userId: string): Promise<string> {
// Implement logic to retrieve user-specific secret from database
// This could be a field in your User model
return 'user_specific_secret';
}
4. Verify JWT on API Requests When receiving API requests, verify the JWT and check that the device ID matches:
// verifyJwt.ts
import * as jwt from 'jsonwebtoken';
export async function verifyJwt(token: string, deviceId: string): Promise<any> {
try {
// Get application secret
const secretKey = process.env.JWT_SECRET;

if (!secretKey) {
throw new Error('JWT secret key is missing');
}

// Decode token without verification to get userId
const decoded = jwt.decode(token);
if (!decoded || typeof decoded === 'string') {
throw new Error('Invalid token');
}

// Get user-specific secret
const userSecret = await getUserSecret(decoded.userId);

// Combine secrets
const combinedSecret = `${secretKey}${userSecret}`;

// Verify token with combined secret
const verified = jwt.verify(token, combinedSecret);

// Check if the device ID in the token matches the provided device ID
if (verified.deviceId !== deviceId) {
throw new Error('Device ID mismatch');
}

return verified;
} catch (error) {
// Handle different types of JWT errors appropriately
if (error.name === 'TokenExpiredError') {
throw new Error('Token expired');
}
throw error;
}
}
// verifyJwt.ts
import * as jwt from 'jsonwebtoken';
export async function verifyJwt(token: string, deviceId: string): Promise<any> {
try {
// Get application secret
const secretKey = process.env.JWT_SECRET;

if (!secretKey) {
throw new Error('JWT secret key is missing');
}

// Decode token without verification to get userId
const decoded = jwt.decode(token);
if (!decoded || typeof decoded === 'string') {
throw new Error('Invalid token');
}

// Get user-specific secret
const userSecret = await getUserSecret(decoded.userId);

// Combine secrets
const combinedSecret = `${secretKey}${userSecret}`;

// Verify token with combined secret
const verified = jwt.verify(token, combinedSecret);

// Check if the device ID in the token matches the provided device ID
if (verified.deviceId !== deviceId) {
throw new Error('Device ID mismatch');
}

return verified;
} catch (error) {
// Handle different types of JWT errors appropriately
if (error.name === 'TokenExpiredError') {
throw new Error('Token expired');
}
throw error;
}
}
5. Handle Token Invalidation For security, you should implement token invalidation when users log out. Since JWTs are stateless, you'll need to store some state on the server:
// Store logout time in user profile
async function invalidateTokens(userId: string): Promise<void> {
// Update user's lastLogoutTime in the database
// All tokens issued before this time will be considered invalid
}

// When verifying tokens, check against the lastLogoutTime
function isTokenValid(token, user): boolean {
const tokenIssuedAt = token.iat * 1000; // Convert to milliseconds
return tokenIssuedAt > user.lastLogoutTime;
}
// Store logout time in user profile
async function invalidateTokens(userId: string): Promise<void> {
// Update user's lastLogoutTime in the database
// All tokens issued before this time will be considered invalid
}

// When verifying tokens, check against the lastLogoutTime
function isTokenValid(token, user): boolean {
const tokenIssuedAt = token.iat * 1000; // Convert to milliseconds
return tokenIssuedAt > user.lastLogoutTime;
}
6. Client-Side Implementation On the client side (your Chrome extension), you need to: 1. Generate a unique device ID for the extension 2. Send this device ID when authenticating 3. Store the received JWT securely 4. Include both the JWT and device ID in subsequent API requests This approach ensures that the JWT is tied to the specific device and includes user-specific secrets, making it more secure against token theft and replay attacks. Remember that when implementing this in a real application, you should follow best practices for secure storage of tokens and secrets. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @NEROX, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
NEROX
NEROXOP2w ago
I have created an issue because I have tried quite a few things and it doesn't fit in a Discord message: https://github.com/wasp-lang/wasp/issues/2646
GitHub
Wasp API returning 401 Unauthorized despite auth: false when Auth...
Wasp Version: ^0.16.2 Problem Description I have a Wasp application with a custom API endpoint askDocumentsApi defined for the route POST /api/ask. This API is intended to be called from a Chrome e...

Did you find this page helpful?