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
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.
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?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' });
}
}
Wohooo @Álvaro P., you just became a Waspeteer level 1!
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
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
Renders to this:
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:
@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.