How Security is Handled in the ACE-Step UI Backend: JWT & Middleware Implementation
The ACE-Step UI backend implements stateless authentication using JSON Web Tokens (JWT) verified through Express middleware, with user privileges stored in SQLite and enforced via database lookups.
The ACE-Step UI project (fspecii/ace-step-ui) is designed as a local, single-user application that requires API protection without the complexity of enterprise identity providers. According to the source code, the backend handles security through a lightweight, three-layer validation system that combines cryptographically signed tokens, request middleware, and minimal database storage to enforce both authentication and authorization.
JWT Configuration and Token Issuance
Security configuration is centralized in server/src/config/index.ts, where the JWT secret and expiration are defined. The system reads JWT_SECRET from environment variables, falling back to a development default, and sets tokens to expire after 365 days by default.
Token creation occurs in server/src/routes/auth.ts via the issueAccessToken function. This method signs a payload containing the user's id and username using the configured secret:
// server/src/routes/auth.ts
function issueAccessToken(payload: { id: string; username: string }): string {
return jwt.sign(payload, config.jwt.secret, { expiresIn: config.jwt.expiresIn });
}
Authentication Middleware Architecture
The backend provides three distinct middleware functions in server/src/middleware/auth.ts to handle different security requirements.
Strict Token Validation with authMiddleware
The authMiddleware enforces mandatory authentication for protected endpoints. It inspects the Authorization header for a Bearer token, verifies it against the JWT secret, and attaches the decoded user object to the request. Invalid or missing tokens result in a 401 response:
// server/src/middleware/auth.ts
export function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, config.jwt.secret);
req.user = decoded;
next();
} catch {
res.status(401).json({ error: 'Invalid or expired token' });
}
}
Optional Authentication Flows
For public endpoints that may still benefit from user context, optionalAuthMiddleware parses the token if present but never blocks the request. This allows the API to return personalized data for logged-in users while remaining accessible to anonymous traffic.
Admin Privilege Enforcement
The adminMiddleware extends validation by querying the SQLite database after token verification. It checks the users.is_admin column to confirm elevated privileges, returning 403 for authenticated users who lack admin rights. This database-backed check ensures the JWT payload alone cannot grant administrative access.
Securing API Routes
Route handlers apply these middleware layers declaratively. For example, song creation in server/src/routes/songs.ts requires standard authentication:
// server/src/routes/songs.ts
router.post('/', authMiddleware, async (req: AuthenticatedRequest, res) => {
const userId = req.user!.id;
// Song creation logic...
});
Conversely, administrative cleanup endpoints in server/src/routes/users.ts apply the stricter adminMiddleware:
// server/src/routes/users.ts
router.delete('/admin/cleanup', adminMiddleware, async (req, res) => {
// Administrative action logic...
});
Input Sanitization and Database Security
Before persistence, user-supplied data undergoes sanitization. Usernames are trimmed, stripped of special characters (allowing only alphanumeric, underscore, and hyphen), and truncated to 50 characters:
const sanitizedUsername = username
.trim()
.replace(/[^a-zA-Z0-9_-]/g, '')
.slice(0, 50);
The SQLite database (acestep.db) stores only user IDs, usernames, and the is_admin boolean flag. Notably, the system does not persist user passwords, relying instead on the JWT's cryptographic integrity for session security.
Environment Configuration and Secrets Management
Sensitive configuration resides in environment variables, with a .env.example file documenting required values without exposing real secrets. The JWT secret, database path, and token expiration are all configurable via environment variables, allowing production deployments to override development defaults.
Frontend Integration Pattern
Client applications attach tokens to requests via the Authorization header. A typical service implementation retrieves the token from storage and includes it in fetch calls:
// Frontend service example
export async function api<T>(endpoint: string, options: { token?: string } = {}): Promise<T> {
const headers: HeadersInit = { 'Content-Type': 'application/json' };
if (options.token) {
headers['Authorization'] = `Bearer ${options.token}`;
}
const response = await fetch(`${API_BASE}${endpoint}`, {
headers,
credentials: 'include',
});
return response.json();
}
Summary
- Stateless JWT authentication validates users via signed tokens stored in the
Authorizationheader, implemented inserver/src/middleware/auth.ts. - Three middleware layers provide strict authentication (
authMiddleware), optional user context (optionalAuthMiddleware), and admin verification (adminMiddleware). - Database-backed authorization confirms admin privileges by querying the
is_admincolumn in SQLite, preventing privilege escalation through token manipulation. - Input sanitization removes special characters and enforces length limits on usernames before database insertion.
- Environment-driven secrets allow customization of the JWT secret and expiration via
.envconfiguration, with sensible defaults for local development.
Frequently Asked Questions
Does the ACE-Step UI backend store user passwords?
No. The SQLite database stores only the user ID, username, and is_admin flag. Authentication relies entirely on JWT tokens signed with a server-side secret, eliminating the need for password storage and the associated security risks.
How long do authentication tokens remain valid?
By default, tokens expire after 365 days as configured in server/src/config/index.ts. You can override this duration by setting the JWT_EXPIRES_IN environment variable to adjust the token lifetime for your security requirements.
How does the backend differentiate between regular users and administrators?
After JWT verification, the adminMiddleware queries the users table in SQLite to check the is_admin column. Only users with this flag set to 1 can access endpoints protected by this middleware, ensuring database-level authorization beyond the token payload.
Can the JWT secret be customized for production deployments?
Yes. The secret is read from the JWT_SECRET environment variable, falling back to a hard-coded development value only when the variable is unset. Production deployments should always specify a cryptographically secure secret via the environment configuration documented in .env.example.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →