Let me be honest with you—when I first tried to wire up authentication in ToolJet, I expected a simple “Sign Up / Sign In” toggle. Instead, I found myself wrestling with JWTs, session states, and OAuth callbacks while trying to figure out why my frontend kept redirecting in circles. If you’re reading this, you’re probably in that same boat. Let’s walk through this together, from the ground up, like we’re sitting at a coffee shop debugging this out.
First, Let’s Talk About Why Authentication Matters in Your App
You’re building an internal dashboard, a customer portal, or maybe a multi-tenant SaaS tool. In every single one of those cases, you need to know who is looking at the data. ToolJet gives you several pathways to achieve this, and picking the right one depends entirely on your use case. Let’s not skip that decision-making part.
The three main approaches in ToolJet are:
- Built-in Authentication (email/password with ToolJet managing the users)
- OAuth 2.0 / Social Login (Google, GitHub, GitLab, etc.)
- Custom Authentication (your own backend handles auth, ToolJet validates tokens)
Most developers I know start with #1 or #2 and then migrate to #3 as their requirements get complex. I’ll cover all three, but I’ll spend the most time on #3 because that’s where things get interesting—and where most people get stuck.
Approach 1: Built-in Authentication (The Quick Start Path)
ToolJet has a built-in authentication system that works out of the box if you’re self-hosting. Here’s what you need to know.
What You Get
- User registration and login pages are generated for you
- Password reset flows (with email integration)
- Role-based access control (Admin, Developer, Viewer)
- Session management handled automatically
How to Enable It
If you’re running ToolJet via Docker (which most people do), this is as simple as setting environment variables:
# docker-compose.yml snippet
services:
tooljet:
image: tooljet/tooljet:latest
environment:
- TOOLJET_AUTH_ENABLED=true
- TOOLJET_SIGNUP_ENABLED=true # Allow new users to register
- TOOLJET_EMAIL_HOST=smtp.gmail.com # For password reset emails
- TOOLJET_EMAIL_PORT=587
- TOOLJET_EMAIL_USERNAME=your@email.com
- TOOLJET_EMAIL_PASSWORD=your_app_password
- TOOLJET_EMAIL_FROM=noreply@yourdomain.com
ports:
- "3000:3000"
volumes:
- tooljet_data:/var/lib/tooljet
That’s it. Restart the container, and you’ll have a full auth system at /auth/sign-in.
The Catch
The built-in auth is great for internal tools and prototypes. But if you need to integrate with your existing user database, or if you need SSO with your company’s identity provider, you’re going to hit a wall. That’s when you move to custom authentication.
Approach 2: OAuth 2.0 / Social Login
ToolJet supports OAuth login out of the box for several providers. This is perfect if you want users to log in with their Google, GitHub, or GitLab accounts without managing passwords.
Supported Providers
- GitHub
- GitLab
- Generic OAuth 2.0 (for any provider)
Configuration via Environment Variables
# docker-compose.yml
services:
tooljet:
environment:
# Google OAuth
- TOOLJET_GOOGLE_CLIENT_ID=your_google_client_id
- TOOLJET_GOOGLE_CLIENT_SECRET=your_google_client_secret
# GitHub OAuth
- TOOLJET_GITHUB_CLIENT_ID=your_github_client_id
- TOOLJET_GITHUB_CLIENT_SECRET=your_github_client_secret
# GitLab OAuth
- TOOLJET_GITLAB_BASE_URL=https://gitlab.com
- TOOLJET_GITLAB_CLIENT_ID=your_gitlab_client_id
- TOOLJET_GITLAB_CLIENT_SECRET=your_gitlab_client_secret
Setting Up Google OAuth (Step-by-Step)
This is the most common scenario, so let’s walk through it carefully.
Step 1: Create a Google Cloud Project
Go to console.cloud.google.com, create a new project, and enable the Google+ API (yes, it’s still required for OAuth).
Step 2: Create OAuth Credentials
Navigate to APIs & Services → Credentials → Create Credentials → OAuth client ID.
- Application type: Web application
- Name:
ToolJet Auth - Authorized redirect URIs:
https://your-tooljet-domain.com/auth/oauth/callback/google
Replace your-tooljet-domain.com with your actual domain. This is critical—if the redirect URI doesn’t match exactly, you’ll get a redirect_uri_mismatch error that’s incredibly annoying to debug.
Step 3: Copy the Credentials
You’ll get a Client ID and Client Secret. Paste these into your environment variables.
Step 4: Test It
Restart your ToolJet container and try signing in. You should see a “Sign in with Google” button on the auth page.
The Generic OAuth Option
If you need to authenticate against a custom provider (like your company’s internal SSO), you can use the generic OAuth settings:
- TOOLJET_AUTH_GENERIC_CLIENT_ID=your_client_id
- TOOLJET_AUTH_GENERIC_CLIENT_SECRET=your_client_secret
- TOOLJET_AUTH_GENERIC_AUTHORIZATION_URL=https://your-provider.com/oauth/authorize
- TOOLJET_AUTH_GENERIC_TOKEN_URL=https://your-provider.com/oauth/token
- TOOLJET_AUTH_GENERIC_USERINFO_URL=https://your-provider.com/oauth/userinfo
- TOOLJET_AUTH_GENERIC_SCOPE=openid profile email
This requires understanding the OAuth 2.0 flow, which brings me to the next section.
Approach 3: Custom Authentication (The Powerful Way)
This is where things get really useful. With custom authentication, you handle the login logic yourself—whether that’s validating against your existing user database, checking API keys, or implementing any custom logic. ToolJet just validates the token and extracts user information.
How It Works: The JWT Flow
The custom auth in ToolJet is based on JSON Web Tokens (JWT). Here’s the mental model:
- User logs in through your custom login page (not ToolJet’s)
- Your backend validates credentials (checks database, API, etc.)
- Your backend issues a JWT signed with a secret key
- The frontend sends this JWT to ToolJet to establish a session
- ToolJet validates the JWT and sets a session cookie
- All subsequent requests include the session cookie automatically
Building a Custom Login Page in ToolJet
Here’s a practical example. Let’s say you have an existing user API at https://api.yourcompany.com/auth/login. You want to create a login page in ToolJet that calls this API and sets up a session.
Step 1: Create a New Query
In the ToolJet query editor, create a new query called loginUser:
const response = await fetch('https://api.yourcompany.com/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: queryState.email,
password: queryState.password
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Login failed');
}
return data;
Step 2: Create the Login Form
Add two input components to your page:
- Input component named
emailwith placeholder “Email” - Input component named
passwordwith placeholder “Password” and type set to “password”
Add a button and bind its onClick event to this JavaScript:
try {
const result = await queries.loginUser.execute();
// Extract the JWT from the response
const token = result.token;
// Set the ToolJet session using the custom auth API
await fetch('/api/v1/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: token
})
});
// Redirect to the dashboard
router.push('/dashboard');
} catch (error) {
// Show error to user
alerts.error(error.message || 'Invalid credentials');
}
Step 3: Configure Custom Authentication on the Backend
This is the part that trips people up. You need to tell ToolJet about your custom auth endpoint. This is done through the ToolJet server configuration:
// In your ToolJet server code or configuration
{
auth: {
enabled: true,
type: 'custom',
custom: {
// This is the endpoint that validates tokens
verifyTokenEndpoint: 'https://api.yourcompany.com/auth/verify',
// This is the endpoint for issuing tokens (optional, for some setups)
issueTokenEndpoint: 'https://api.yourcompany.com/auth/login'
}
}
}
In environment variables:
- TOOLJET_AUTH_ENABLED=true
- TOOLJET_AUTH_TYPE=custom
- TOOLJET_AUTH_CUSTOM_VERIFY_TOKEN_ENDPOINT=https://api.yourcompany.com/auth/verify
- TOOLJET_AUTH_CUSTOM_ISSUE_TOKEN_ENDPOINT=https://api.yourcompany.com/auth/login
How the Token Verification Endpoint Should Work
Your verify endpoint needs to accept a JWT and return user information. Here’s an example in Node.js:
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const SECRET_KEY = process.env.JWT_SECRET_KEY;
app.post('/auth/verify', async (req, res) => {
const { token } = req.body;
if (!token) {
return res.status(401).json({ error: 'Token required' });
}
try {
// Verify the JWT
const decoded = jwt.verify(token, SECRET_KEY);
// Return user info in the format ToolJet expects
res.json({
id: decoded.userId,
email: decoded.email,
name: decoded.name,
role: decoded.role || 'viewer'
});
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
});
app.listen(3001, () => console.log('Auth server running on port 3001'));
Important: ToolJet expects the response to have at minimum id and email fields. The name and role fields are optional but recommended.
How to Issue Tokens on Your Backend
Here’s a complete example of a login endpoint that issues JWTs:
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const app = express();
const SECRET_KEY = process.env.JWT_SECRET_KEY;
const TOKEN_EXPIRY = '24h';
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
// Find user in your database
const user = await db.users.findOne({ email });
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Verify password
const isValidPassword = await bcrypt.compare(password, user.passwordHash);
if (!isValidPassword) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Create JWT payload
const payload = {
userId: user.id,
email: user.email,
name: user.name,
role: user.role || 'viewer'
};
// Sign the token
const token = jwt.sign(payload, SECRET_KEY, {
expiresIn: TOKEN_EXPIRY
});
res.json({ token });
});
app.listen(3001, () => console.log('Auth server running on port 3001'));
Setting Up Roles for Access Control
Once authentication is working, you probably want to control what different users can see. ToolJet supports role-based access control.
In your verify endpoint, make sure you’re returning a role field. Then, in ToolJet, you can create page-level or component-level access rules:
// Check if user has admin role before showing sensitive data
if (user.role !== 'admin') {
router.push('/access-denied');
}
You can also create a helper query to check roles:
const { queryState } = this;
const userRole = queryState.user?.role || 'viewer';
return userRole === 'admin';
Handling Session Management
One thing that isn’t immediately obvious is how sessions work in ToolJet with custom authentication. When a user logs in, ToolJet creates a session and stores it in a session cookie. This session is tied to the JWT you issued.
Automatic Session Renewal
ToolJet can automatically renew sessions if you configure it properly. The key is making sure your token expiry is longer than your session duration, or implementing a refresh token mechanism.
Here’s how to add refresh token support:
Backend (Node.js example):
const crypto = require('crypto');
// When user logs in
app.post('/auth/login', async (req, res) => {
// ... validate credentials ...
const refreshToken = crypto.randomBytes(40).toString('hex');
// Store refresh token in database (associated with user)
await db.refreshTokens.create({
userId: user.id,
token: refreshToken,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days
});
const accessToken = jwt.sign(payload, SECRET_KEY, { expiresIn: '1h' });
res.json({
token: accessToken,
refreshToken: refreshToken
});
});
// Refresh endpoint
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
const storedToken = await db.refreshTokens.findOne({ token: refreshToken });
if (!storedToken || storedToken.expiresAt < new Date()) {
return res.status(401).json({ message: 'Invalid refresh token' });
}
// Issue new access token
const newAccessToken = jwt.sign(payload, SECRET_KEY, { expiresIn: '1h' });
res.json({ token: newAccessToken });
});
Frontend (ToolJet query):
const { queryState } = this;
const refreshToken = localStorage.getItem('refreshToken');
try {
const response = await fetch('/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
const data = await response.json();
localStorage.setItem('token', data.token);
return data;
} catch (error) {
// Redirect to login if refresh fails
router.push('/login');
}
Security Best Practices
Let’s talk about security because getting this wrong can have serious consequences.
1. Use HTTPS Everywhere
Never send authentication tokens over HTTP. This is non-negotiable. Make sure your ToolJet instance and your auth backend are both served over HTTPS.
2. Store Secrets Securely
Never hardcode JWT secret keys or OAuth client secrets in your code. Use environment variables or a secrets manager:
# In your docker-compose.yml
environment:
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
- TOOLJET_SECRET_KEY=${TOOLJET_SECRET_KEY}
Then create a .env file:
JWT_SECRET_KEY=a_very_long_random_string_minimum_32_characters
TOOLJET_SECRET_KEY=another_long_random_string_minimum_32_characters
3. Implement Rate Limiting
Add rate limiting to your auth endpoints to prevent brute force attacks:
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: 'Too many login attempts, please try again later'
});
app.post('/auth/login', loginLimiter, async (req, res) => {
// ... login logic ...
});
4. Set Secure Cookie Flags
When ToolJet sets session cookies, ensure they have the right flags. In your nginx reverse proxy (if you’re using one):
proxy_cookie_flags ~ all "secure httponly samesite=strict";
This ensures cookies are:
- Secure: Only sent over HTTPS
- HttpOnly: Not accessible via JavaScript
- SameSite=Strict: Prevents CSRF attacks
5. Implement Logout Properly
Don’t just clear local storage—actually invalidate the session on the server:
// Logout query in ToolJet
const logout = async () => {
try {
await fetch('/api/v1/auth/logout', {
method: 'POST'
});
// Clear local storage
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
// Clear ToolJet session
await fetch('/auth/logout', {
method: 'POST'
});
router.push('/login');
} catch (error) {
console.error('Logout failed', error);
}
};
Debugging Common Issues
Even with perfect documentation, things go wrong. Here are the issues I’ve seen most often and how to fix them.
Issue 1: “Invalid Token” on Login
Symptoms: User logs in successfully on your backend, but ToolJet rejects the token.
Checklist:
- Is the JWT secret key the same on both your backend and ToolJet configuration?
