Best Practices for Implementing Auth System in Chrome Extension Connected to OpenSaaS

Hello, everyone! I'm currently developing a SaaS product and have created a Chrome extension. I'm at the stage where I need to implement an authentication system that connects the Chrome extension with our SaaS backend, which I've referred to as OpenSaaS for this example. The primary goal is to securely authenticate users through the Chrome extension, ensuring that only authorized users can access and use the extension's features. Here's what I have in mind for the authentication flow: - Users click on the extension icon and are prompted to log in through a popup if they aren't already authenticated. - Upon logging in, the credentials are sent to OpenSaaS's authentication API. - The API returns a token upon successful authentication, which the extension then stores securely. - This token is used for subsequent API calls to authenticate the user. I'm looking for advice on the following: - Secure Token Storage: What are the best practices for securely storing and managing the authentication token within a Chrome extension? - Authentication Flow: Is there a recommended pattern or best practice for implementing the authentication flow in a Chrome extension, especially concerning SaaS products? - API Communication: Any tips on securing the communication between the Chrome extension and the SaaS backend? Additionally, if there are any security considerations or common pitfalls I should be aware of, I'd greatly appreciate your insights. Thank you in advance for your help and guidance!
7 Replies
Filip
Filip2mo ago
Hi @Álvaro P., I've never worked with Chrome extensions. Did some quick googling and found a couple of links I could point you to, but you've probably found the same links yourself 😄 I'll tag @miho in hope he knows something about extensions, because I really am a blank slate in this regard. What I can say is that, for the "Any tips on securing the communication between the Chrome extension and the SaaS backend?" part, as long as you're using HTTPS, there's nothing extra you have to do - the communication is as secure as it reasonably needs to be.
miho
miho2mo ago
Hm, off the top of my head, use custom API endpoints and have a way to create JWT tokens (We used oslo for this internally). 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. Check it on each request. We did really work on this "third-party app is authenticated" scenario. Hmmm @martinsos is this similar to mobile apps using Wasp?
Álvaro P.
Álvaro P.2mo ago
Hi guys! Thanks for the answers. After searching a lot, as @miho says that is the key. The key is to generate a JWT key when the user logs in. I'm having trouble with the /api/generate-jwt endpoint in my Wasp app. Although I've defined the endpoint in my main.wasp file and implemented the generateJwt function in TypeScript, when I test the endpoint with curl, it returns a 404 Not Found error. I've made sure to restart the Wasp server after making changes. Could someone help me understand why the endpoint might not be found, even though it's been defined? Here's how I've set up the API and function: // main.wasp api generateJwt { fn: import { generateJwt } from "@src/server/api/generateJwt.js", entities: [User], httpRoute: (POST, "/api/generate-jwt") } The generateJwt.ts script is responsible for handling user authentication and JWT (JSON Web Token) creation in a Wasp application. It includes a mock authentication function that, for demonstration purposes, approves any login attempt and assigns a mock user ID. Upon successful authentication, it generates a JWT containing the user's username and user ID, signs it with a secret key retrieved from the environment, and sends the token back to the client. // generateJwt.ts import * as jwt from 'jsonwebtoken'; import { Request, Response } from 'express'; // Adjusted authenticateUser function to also return user ID upon successful authentication async function authenticateUser(username: string, password: string): Promise<{ isAuthenticated: boolean; userId?: string }> { // TODO: Implement actual user authentication logic here // This should fetch the user's ID from the database based on the provided username and password // For demonstration, assuming authentication is successful and returning a mock user ID return { isAuthenticated: true, userId: 'user123' }; } export async function generateJwt(req: Request, res: Response): Promise<void> { const { username, password } = req.body; try { const authResult = await authenticateUser(username, password); if (!authResult.isAuthenticated) { res.status(401).json({ error: 'Invalid credentials' }); return; } // User payload for JWT const userPayload = { username, userId: authResult.userId }; // Retrieve the secret key from environment variables const secretKey = process.env.JWT_SECRET_KEY; if (!secretKey) { throw new Error('JWT secret key is missing'); } // Token options const options = { expiresIn: '1h' }; // Token expires in 1 hour // Generate the JWT const token = jwt.sign(userPayload, secretKey, 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' }); } }
MEE6
MEE62mo ago
Wohooo @Álvaro P., you just became a Waspeteer level 1!
Álvaro P.
Álvaro P.2mo ago
curl -v -X POST http://localhost:3000/api/generate-jwt \ -H "Content-Type: application/json" \ -d '{"username":"testuser","password":"verysecurepassword"}' Note: Unnecessary use of -X or --request, POST is already inferred. * Trying [::1]:3000... * connect to ::1 port 3000 failed: Connection refused * Trying 127.0.0.1:3000... * Connected to localhost (127.0.0.1) port 3000
POST /api/generate-jwt HTTP/1.1 Host: localhost:3000 User-Agent: curl/8.4.0 Accept: / Content-Type: application/json Content-Length: 55
< HTTP/1.1 404 Not Found < Access-Control-Allow-Origin: * < Date: Tue, 02 Apr 2024 05:29:02 GMT < Connection: keep-alive < Keep-Alive: timeout=5 < Content-Length: 0 < * Connection #0 to host localhost left intact
Filip
Filip2mo ago
HI @Álvaro P., I'll get on this ASAP. Btw, here's a protip, you can use code blocks and syntax highlighting in discord as you would on github: This code
`​`​`typescript
// generateJwt.ts
import * as jwt from 'jsonwebtoken';
import { Request, Response } from 'express';

// Adjusted authenticateUser function to also return user ID upon successful authentication
async function authenticateUser(username: string, password: string): Promise<{ isAuthenticated: boolean; userId?: string }> {
// ...
}

// ...
`​`​`
`​`​`typescript
// generateJwt.ts
import * as jwt from 'jsonwebtoken';
import { Request, Response } from 'express';

// Adjusted authenticateUser function to also return user ID upon successful authentication
async function authenticateUser(username: string, password: string): Promise<{ isAuthenticated: boolean; userId?: string }> {
// ...
}

// ...
`​`​`
Renders to this:
// generateJwt.ts
import * as jwt from 'jsonwebtoken';
import { Request, Response } from 'express';

// Adjusted authenticateUser function to also return user ID upon successful authentication
async function authenticateUser(username: string, password: string): Promise<{ isAuthenticated: boolean; userId?: string }> {
// ...
}

// ...
// generateJwt.ts
import * as jwt from 'jsonwebtoken';
import { Request, Response } from 'express';

// Adjusted authenticateUser function to also return user ID upon successful authentication
async function authenticateUser(username: string, password: string): Promise<{ isAuthenticated: boolean; userId?: string }> {
// ...
}

// ...
Good news @Álvaro P.! Since you've done an excellent job describing the problem, this was an easy one to diagnose (and luckily fix) 😄 The server is running on port 3001, not 3000. Wasp spins up two processes: one serving the client files (port 3000), and another one serving the server files (port 3001). Anyway, change the port and everything should work:
curl -v -X POST http://localhost:3001/api/generate-jwt \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"verysecurepassword"}'
curl -v -X POST http://localhost:3001/api/generate-jwt \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"verysecurepassword"}'
martinsos
martinsos2mo ago
@miho : yeah, this is connected to the effort of making it easier to use auth in "third-party" apps, including mobile. Ideally, for this chrome extension, a Wasp dev would just use the exact same logic as we do for the Wasp app, on the client side. We don't, however, currently have a super easy way for them to get that logic available -> what we would probably want to do at some point is generate a small JS/TS package that does only auth, that they can import in other projects. And not only JS/TS, but also C#, Java, ... (to support mobile). It would be interesting to explore what this package could look like, and how would they use in their third party app during development, how it would fit into the dev pipeline of it, be it chrome extension or a mobile app or something else. Since we don't have that now, what is instead needed is replicating the logic we have themselves, completely or to some degree. I do wonder how hard is that at the moment? @miho you will know the best, I would love to understand it better, how much work do they actually need to do at the moment to replicate, and how much is done by Wasp's server? p.s. we have an issue on GH for enabling auth for external clients, we should add this info there once we are done discussing, it will be helpful.