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!
44 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.
Hello, has any progress been made on this front for using auth with "thrid-party" apps? We are looking at chrome extensions
Also is there a way to authenticate the user through an API call? We are looking to authenticate the user inside a custom API call to generate a jwt but don't know which function to call to authenticate the user once they pass in both username and password from the external api call
@miho
know which function to call to authenticate the user once they pass in both username and password from the external api callThere isn't really a function 😄 since you are recreating a part of the Wasp auth, you need to do what we do under the hood: find the user in the DB and check if the password is OK. This is how you can do it 🙂 let's say you have this custom endpoint set up:
You can then check the login credentials (let's say I'm using username & password auth) like this: https://gist.github.com/infomiho/baf8330ce3733c64906173b1533f2f45#file-src-api-ts
Gist
Custom login API endpoint
Custom login API endpoint. GitHub Gist: instantly share code, notes, and snippets.
I was able to get the session ID with the API endpoint like this:
So you just need to send that session ID in the headers like so:
Hi, when i try to create the custom login and run test the APIU endpoint, i get "message": "Invalid username or password" but i am certain they are correct. I have even added statements and it comes back that the user is not found. is there something i am doing wrong?
Could you share your core in a gist?
What do you get in the server logs? That's it's an invalid pw or the user doesn't exist?
user does not exist
Did you register for an account with that username previouly? It needs to exist in the DB
yes i have registered with email and with google and nothing
Did you confirm the email after you registered? What do you see in the DB when you run
wasp db studio
?Yes I confirmed everything and the users are there in the dB. I have even made one of the users a pro user with stripe
I'd advise you to compare this line to what you have in the DB: https://gist.github.com/gyasi87/3a171e7a2631dc9965f9bc70952e65e3#file-api-ts-L35
Hint: if you are using the
email
provider, it should say "email"
instead of "username"
there 😄For some reason with email and google the username is the email in the db. I tried the code at the top to generate the jwt token and that worked with the user email I signed up with email.
Wohooo @Gyasi, you just became a Waspeteer level 1!
@miho i changed username to email and it worked with the email provider as you said. thank you. is there a way to get it to work with google provider?
There isn't a simple way to do it, you'd have to recreate the Google OAuth provider with custom API endpoints e.g. https://gist.github.com/infomiho/3c63de7d53aba59d6293bcb59501a029
We are working on this to make it easier to use Wasp with external apps https://github.com/wasp-lang/wasp/issues/1973
Gist
Implementing custom OAuth provider with Wasp 0.14.0+ (Spotify in th...
Implementing custom OAuth provider with Wasp 0.14.0+ (Spotify in this case) - main.wasp
GitHub
Make Wasp Auth usable in external apps / services · Issue #1973 · w...
We have users that are building Wasp app, but they also have some other apps that they want to use the same auth system (Wasp Auth). For example, we had a user building a Unity game, and wanted to ...
ok great thank you for all your help
@miho do you guys have a rough timeline when this would be implemented?
Also interested in implementing this feature, so would also love a rough timeline 🙂
Hah, it is one of the very next things we will be working on regarding Auth, and it is on the "let's try to do it if we will manage" list for this quarter (Q3). right after "these features we must finish this quarter". Meaning it might happen this quarter, and if not, will very likely become top priority for the next quarter. This quarter is a bit tricky because a lot of us are going for vacations this or next month since we have been working quite hard so far this year, so this quarter will be a bit more modest. But we do understand the importance of this feature and it is on top of our minds!
@miho the crowd is going wild
Awesome, thanks for the update 🙏
Yeah, as @martinsos said, it's pretty high on our priority list. Getting the email and username methods to work - should be straightforward.
But getting the Google and other OAuth methods to work could prove tricky to a degree because there are redirects involved 😄 it'll probably require some sort of a setup like when you authenticate a CLI via a browser - two separate contexts that need to communicate securely.
But anyho, it will take time and energy to get this down right 😄
@kapa.ai I want on button click from my SaaS a token generated that i can send to my chrome extension using sendMessage. Then when from the chrome extensions requests are mdae to my SaaS i can authorize this token based on subscriptionplan and status. How can i do this?
Let me check the knowledge base for you. One moment! :books:
----------------------
Based on the information provided in the knowledge sources, you can follow these steps to implement your desired functionality:
1. Generate a JWT token: When a user logs in from your SaaS, generate a JWT token. This token can be generated in your Wasp application using a custom API endpoint. The token can include user-specific information such as user ID and subscription plan. Here's an example of how you can set up the API and function:
(Source: Discord support thread: Best Practices for Implementing Auth System in Chrome Extension Connected to OpenSaaS (https://discord.com/channels/686873244791210014/1223328118278983680))
2. Send the token to the Chrome extension: After generating the token, you can send it to your Chrome extension using
chrome.runtime.sendMessage
.
3. Store the token in the Chrome extension: Once the token is received in the Chrome extension, you can store it securely for subsequent use.
4. Authorize requests from the Chrome extension: When the Chrome extension makes requests to your SaaS, it should include the stored token in the request headers. On the server side, you can then validate this token and authorize the request based on the user's subscription plan and status.
Please note that this is a high-level overview and the actual implementation may vary based on your specific requirements and the technologies you are using. Also, remember to always use HTTPS for secure communication between the Chrome extension and your SaaS backend.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
Sorry @Sven, 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?@kapa.ai How could i do the authentication on the SaaS side and then send the token to the chrome extensions. Then when a request is made frome the ext. to the SaaS it should check the subcriptoin status before it 200OK.
Let me check the knowledge base for you. One moment! :books:
----------------------
To implement the authentication flow you described, you can follow these steps:
1. User Authentication: When a user logs in from your SaaS, you need to authenticate the user. This can be done by sending the user's credentials to your authentication API and verifying them against your user database.
2. Generate JWT Token: Upon successful authentication, generate a JWT token. This token can include user-specific information such as user ID and subscription status. You can use the
jsonwebtoken
library to generate the token as shown in the code snippet from the knowledge source.
3. Send Token to Chrome Extension: After generating the token, you can send it to your Chrome extension. This can be done using a method suitable for your application, such as through an API response or via direct communication between your SaaS and the extension.
4. Store Token in Chrome Extension: Once the token is received in the Chrome extension, you can store it securely for subsequent use. Chrome extensions provide various storage APIs that you can use for this purpose.
5. Authorize Requests: When the Chrome extension makes requests to your SaaS, it should include the stored token in the request headers. On the server side, you can then validate this token and authorize the request based on the user's subscription status.
Here's a simplified example of how you can generate a JWT token in your Wasp application:
(Source: Discord support thread: Best Practices for Implementing Auth System in Chrome Extension Connected to OpenSaaS (https://discord.com/channels/686873244791210014/1223328118278983680))
Please note that this is a high-level overview and the actual implementation may vary based on your specific requirements and the technologies you are using. Also, remember to always use HTTPS for secure communication between the Chrome extension and your SaaS backend.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
Sorry @Sven, 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?@kapa.ai why would username and password be nessecary? I want it to maybe check context.user or something
Let me check the knowledge base for you. One moment! :books:
----------------------
In Wasp, the
context.user
object is used to access the currently logged-in user's information on the server. When authentication is enabled, all queries and actions have access to the user
object through the context
argument. context.user
contains all User entity's fields and the auth identities connected to the user. The hashedPassword
field is stripped out from the identities for security reasons.
Here's an example of how you can use context.user
in your actions:
(Source: Wasp Docs: Using the context.user
object (https://wasp-lang.dev/docs/auth/overview#using-the-contextuser-object))
In this example, context.user
is checked to ensure that a user is logged in before creating a task. If context.user
is undefined
, it means that the user is not logged in, and an HTTP 403 error is thrown.
The username and password are not necessary in this context. They are typically used for user authentication during the login process. Once a user is authenticated, a token is generated and sent to the client. This token is then included in subsequent requests to the server, which allows the server to identify the user and populate context.user
.
If you want to check the subscription status of the user, you can add a subscriptionStatus
field to your User entity and then access it via context.user.subscriptionStatus
.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
Sorry @Sven, 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?@kapa.ai
Error connecting to Chrome extension: TypeError: Cannot read properties of undefined (reading 'sendMessage')
at connectChromeExt (ChromeExtensiePage.tsx:32:22)
Let me check the knowledge base for you. One moment! :books:
----------------------
The error message you're seeing typically occurs when you're trying to access a property or method on an object that is
undefined
. In this case, it seems like the object that's supposed to contain the sendMessage
method is undefined
.
Given the context of your question, it's likely that the object in question is chrome.runtime
. This object is part of the Chrome extension APIs and should be defined when your code is running as part of a Chrome extension. If it's undefined
, it could mean one of the following:
1. Your code is not running as part of a Chrome extension. The Chrome extension APIs are only available to scripts that are running as part of an extension. They are not available to scripts running on regular web pages.
2. The chrome
object is not available at the time your code is running. This could happen if your code is executing before the extension APIs have been fully loaded.
To debug this issue, you can add some logging to your code to check the value of chrome
and chrome.runtime
:
If chrome
or chrome.runtime
is undefined
, you'll need to figure out why that's the case. If your code is supposed to be running as part of a Chrome extension, you might need to check your extension's manifest file to make sure it's correctly configured.
If chrome
and chrome.runtime
are defined but chrome.runtime.sendMessage
is undefined
, it could mean that the sendMessage
API is not available. This could happen if the API is not supported by the version of Chrome you're using, or if the API is not included in the "permissions" field of your extension's manifest file.
Please note that without the actual code or more specific details about your extension's setup, it's hard to provide a more specific solution. The above is a general guide based on the error message and the typical causes of such an error.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
Sorry @Sven, 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?