Authentication System
Comprehensive guide to how Soloist handles user authentication, security, and session management
Overview
Soloist uses @convex-dev/auth, a secure authentication library built specifically for Convex backends. This system provides email/password authentication with email verification, password reset capabilities, and robust session management.
Key Features
- Email/password authentication with customizable validation rules
- Email verification using one-time passwords (OTP)
- Password reset functionality via email
- Secure session management with JWT tokens
- Automatic user role assignment and management
- OAuth integration ready (GitHub OAuth configured)
Architecture
Core Components
Backend (Convex)
- •
convex/auth.ts- Auth configuration - •
convex/CustomPassword.ts- Password provider - •
convex/ResendOTP.ts- Email verification - •
convex/http.ts- HTTP routes - •
convex/users.ts- User management
Frontend (Next.js)
- •
app/api/auth/[...convex]/route.ts- Auth proxy - •
auth/oauth/SignInWithGitHub.tsx- Sign in UI - •
hooks/useConvexUser.ts- Auth hook - •
ConvexClientProvider.tsx- Context provider
Authentication Flow
Sign Up Flow
User Submits Registration Form
User provides email and password through the registration interface. The frontend validates basic input requirements before submitting.
Password Validation
The CustomPassword provider enforces strict password requirements:
- Minimum 8 characters long
- At least one number (0-9)
- At least one lowercase letter (a-z)
- At least one uppercase letter (A-Z)
- At least one special character (!@#$%^&*...)
Security Note: If password validation fails, a ConvexError is thrown with specific error messages. This prevents weak passwords from being stored.
Account Creation
If validation passes:
- Password is hashed using industry-standard bcrypt algorithm
- User record is created in the
userstable - Default role "user" is assigned via
internal.admin.setDefaultRole - Auth ID is set via
internal.users.setUserAuthId
Security: Passwords are never stored in plain text. The bcrypt algorithm adds computational cost to prevent brute-force attacks.
Email Verification Sent
The ResendOTP provider generates and sends a verification code:
- 8-digit numeric OTP generated using
oslo/crypto - Email sent via Resend API with HTML and plain-text versions
- Code expires after 15 minutes
- Includes branding and clear instructions
Implementation Detail: Emails are sent from msimon@acdc.digital (verified domain) to comply with Resend free tier restrictions. Production should use a dedicated sending domain.
User Verifies Email
User enters the 8-digit code from their email. The system validates the code, marks the email as verified by setting emailVerificationTime, and completes the registration process.
Sign In Flow
Credentials Submitted
User provides email and password through SignInWithGitHub component or custom sign-in form. Request is sent to Convex Auth.
Credential Verification
Convex Auth performs secure verification:
- Looks up user by email in
userstable - Compares provided password against stored bcrypt hash
- Checks if email is verified (if required by configuration)
- Verifies account is not locked or disabled
Security: Generic error messages like "Invalid credentials" prevent attackers from determining whether an email exists in the system.
Session Token Generated
Upon successful authentication:
- JWT (JSON Web Token) is generated containing user identity
- Token includes user ID, email, and other claims
- Token is signed with secret key (not visible to client)
- Expiration time is set (session duration)
Security: JWT tokens are cryptographically signed, making them tamper-proof. Any modification invalidates the signature.
Token Stored Securely
The JWT token is stored in the browser using httpOnly cookies (when possible) or secure localStorage. This token is automatically included in all subsequent requests to Convex.
HttpOnly Cookies: When available, httpOnly cookies prevent JavaScript from accessing the token, protecting against XSS attacks.
User Context Established
The useConvexUser hook queries api.auth.getUserId to retrieve the authenticated user's ID. This ID is used throughout the application to fetch user-specific data.
Session Management
How Sessions Work
Every request to Convex includes the JWT token, which is validated on the server side. The getAuthUserId(ctx) function extracts and verifies the user ID from the token.
Token Validation Process:
- Client sends request with JWT token in Authorization header or cookie
- Convex backend extracts token from request
- Token signature is verified using secret key
- Token expiration is checked
- User ID is extracted from valid token
- Request proceeds with authenticated user context
Security Best Practices Implemented:
- ✓ Server-side validation: All tokens are validated on the backend
- ✓ Short-lived tokens: Tokens expire, requiring periodic re-authentication
- ✓ Automatic refresh: Convex handles token refresh transparently
- ✓ Secure storage: Tokens stored in httpOnly cookies when possible
- ✓ HTTPS only: Production tokens only transmitted over HTTPS
Authentication in Convex Functions
All protected Convex functions use a consistent pattern for authentication:
import { getAuthUserId } from "@convex-dev/auth/server";
export const myProtectedQuery = query({
args: {},
handler: async (ctx) => {
// Get authenticated user ID
const userId = await getAuthUserId(ctx);
// If not authenticated, return null or throw error
if (!userId) {
return null; // or throw new ConvexError("Not authenticated")
}
// Proceed with authenticated logic
const user = await ctx.db.get(userId);
return user;
},
});Critical Security Rule:
NEVER use ctx.auth.getUserIdentity().subject directly. Always use getAuthUserId(ctx) which properly validates the token and returns the correct database user ID.
Password Reset Flow
User Requests Password Reset
User provides their email address through the "Forgot Password" form. The system looks up the account.
Reset Code Generated
The ResendOTPPasswordReset provider:
- Generates an 8-digit reset code using secure random generation
- Associates the code with the user's account
- Sets expiration time (15 minutes)
- Sends email with code via Resend API
Security Note: The system does not reveal whether an email exists in the database. It always shows "Check your email" even for non-existent accounts, preventing account enumeration attacks.
User Submits Reset Code
User enters the code from their email and provides a new password. Both are validated before processing.
Password Updated
If code is valid and not expired:
- New password is validated against strength requirements
- Password is hashed with bcrypt
- Old password hash is replaced with new one
- Reset code is invalidated
- User is notified of successful password change
Security: After password change, all existing sessions could optionally be invalidated (not currently implemented) to force re-authentication on all devices.
OAuth Integration (GitHub)
Soloist supports GitHub OAuth for authentication. This provides a streamlined sign-in experience using users' GitHub accounts.
OAuth Flow
- 1.
User Clicks "Sign in with GitHub"
The
SignInWithGitHubcomponent callssignIn("github", ) - 2.
Redirect to GitHub
User is redirected to GitHub's OAuth authorization page to approve access
- 3.
GitHub Callback
GitHub redirects back to
/api/auth/callback/githubwith authorization code - 4.
Token Exchange
Convex exchanges the code for GitHub access token and retrieves user profile
- 5.
Account Linking/Creation
If user exists (by GitHub ID or email), they're signed in. Otherwise, new account is created with GitHub profile data
- 6.
Session Established
JWT token is generated and user is authenticated
OAuth Security Benefits:
- • No password storage required for OAuth users
- • Leverages GitHub's security infrastructure
- • Automatic access revocation when user removes app from GitHub
- • Reduces password reuse risks
HTTP Routes & Authentication Proxy
Authentication requests are proxied through Next.js API routes to Convex's HTTP endpoints.
Route Structure
The route app/api/auth/[...convex]/route.ts acts as a catch-all proxy:
// Extract path: /api/auth/signin/password → /signin/password
const pathSegments = url.pathname.split('/api/auth/').pop();
// Build Convex URL
const convexUrl = new URL(CONVEX_SITE_URL);
convexUrl.pathname = `/${pathSegments}`;
// Forward request to Convex with proper headers
const response = await fetch(convexUrl.toString(), {
method: req.method,
headers,
body,
redirect: 'manual'
});Key Headers Forwarded:
- •
content-type- Request content type - •
authorization- JWT token - •
cookie- Session cookies - •
user-agent- Client identification - •
origin- Request origin for CORS
Why Use a Proxy?
- • Enables setting httpOnly cookies from same domain
- • Simplifies CORS configuration
- • Provides single authentication endpoint for frontend
- • Allows middleware injection for logging/monitoring
- • Supports manual redirect handling for OAuth flows
Convex HTTP Routes
In convex/http.ts, authentication routes are registered:
import { httpRouter } from "convex/server";
import { auth } from "./auth";
const http = httpRouter();
// Registers all auth routes automatically
auth.addHttpRoutes(http);
export default http;This automatically creates routes like /signin, /signout, /callback/github, etc.
Database Schema for Authentication
Convex Auth extends the standard users table with authentication-specific fields and supporting tables.
Users Table
users: defineTable({
// Standard Convex Auth fields
name: v.optional(v.string()),
image: v.optional(v.string()),
email: v.optional(v.string()),
emailVerificationTime: v.optional(v.float64()),
phone: v.optional(v.string()),
phoneVerificationTime: v.optional(v.float64()),
isAnonymous: v.optional(v.boolean()),
// Custom application fields
authId: v.optional(v.string()),
githubId: v.optional(v.number()),
role: v.optional(v.union(v.literal("user"), v.literal("admin"))),
})
.index("email", ["email"])
.index("byAuthId", ["authId"])
.index("byRole", ["role"])Auth Tables (from authTables)
Convex Auth automatically creates several supporting tables:
authSessions
Stores active session tokens and their expiration times
authAccounts
Links users to authentication providers (email/password, GitHub, etc.)
authVerificationCodes
Stores pending verification codes for email verification and password reset
authRefreshTokens
Manages refresh tokens for extending sessions without re-authentication
Important Schema Note:
These auth tables are managed entirely by Convex Auth. Never modify them directly. Use the provided auth functions and callbacks to customize behavior.
Security Analysis
✓ Security Strengths
Password Security
- Strong password requirements enforced
- Bcrypt hashing with sufficient computational cost
- No plain-text password storage anywhere
Token Management
- JWT tokens are cryptographically signed
- Tokens have expiration times
- Server-side validation on every request
- HttpOnly cookies prevent XSS token theft (when available)
API Security
- Consistent use of
getAuthUserId(ctx) - Internal functions inaccessible from client
- User data access restricted to authenticated users
- Permission checks before data access
Email Verification
- OTP codes generated with secure randomness
- Time-limited verification codes (15 minutes)
- Prevents email-based account hijacking
⚠ Potential Security Considerations
1. Session Invalidation After Password Change
Issue: When a user changes their password, existing sessions on other devices remain valid.
Risk: If an attacker has stolen a session token, changing the password won't immediately protect the account.
Recommendation: Implement session invalidation on password change. When password is updated, delete all authSessions records for that user, forcing re-authentication everywhere.
2. Rate Limiting Not Evident
Issue: No visible rate limiting on authentication endpoints.
Risk: Attackers could attempt brute force attacks on passwords or OTP codes without being throttled.
Recommendation: Implement rate limiting on sign-in attempts, OTP verification, and password reset requests. Consider IP-based and email-based limits.
3. Account Lockout Not Implemented
Issue: No mechanism to temporarily lock accounts after repeated failed login attempts.
Risk: Targeted account attacks could continue indefinitely without detection.
Recommendation: Add failed login attempt tracking. After 5-10 failed attempts, temporarily lock the account for 15-30 minutes and send notification email to user.
4. Email Verification Not Required
Issue: Code sends verification emails but may not prevent unverified users from accessing the app.
Risk: Users could access the system with unverified emails, potentially creating accounts with fake or mistyped email addresses.
Recommendation: Enforce email verification before allowing full access. Check emailVerificationTime in protected queries and restrict unverified accounts.
5. HTTPS Enforcement
Issue: No visible HTTPS enforcement in code (though likely handled at deployment level).
Risk: If HTTPS is not enforced, tokens and credentials could be intercepted in transit.
Recommendation: Ensure production deployment forces HTTPS. Add middleware to redirect HTTP to HTTPS if not already handled by hosting platform.
6. Audit Logging
Issue: No comprehensive audit logging for security events.
Risk: Security incidents difficult to investigate without logs of authentication events.
Recommendation: Log key events: sign-in attempts (success/failure), password changes, email changes, OTP requests, password resets. Include timestamp, IP address, and user agent.
✓ Good Security Practices Observed
- • Generic error messages that don't reveal whether accounts exist
- • Internal functions properly separated from public API
- • User ID validation before data access
- • Secure random generation for OTP codes
- • Proper use of Convex Auth's security features
- • Password complexity requirements
- • Time-limited verification codes
Developer Reference
Common Authentication Patterns
Check if User is Authenticated
import { useConvexUser } from "@/hooks/useConvexUser";
function MyComponent() {
const { isAuthenticated, userId } = useConvexUser();
if (!isAuthenticated) {
return <SignInPrompt />;
}
return <AuthenticatedContent />;
}Protect a Convex Query
import { query } from "./_generated/server";
import { getAuthUserId } from "@convex-dev/auth/server";
export const getMyData = query({
args: {},
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) {
throw new ConvexError("Not authenticated");
}
return await ctx.db
.query("myTable")
.withIndex("by_userId", (q) => q.eq("userId", userId))
.collect();
},
});Sign Out a User
import { useAuthActions } from "@convex-dev/auth/react";
function SignOutButton() {
const { signOut } = useAuthActions();
const handleSignOut = async () => {
await signOut();
// User is now signed out, token cleared
};
return <button onClick={handleSignOut}>Sign Out</button>;
}Get Current User Profile
import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
function UserProfile() {
const user = useQuery(api.users.viewer);
if (!user) return <div>Loading...</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}Environment Variables Required
# Convex
NEXT_PUBLIC_CONVEX_URL=https://your-project.convex.cloud
# Authentication
AUTH_RESEND_KEY=re_xxxxxxxxxxxxx # Resend API key for emails
# OAuth (if using GitHub)
GITHUB_CLIENT_ID=xxxxxxxxxxxxx
GITHUB_CLIENT_SECRET=xxxxxxxxxxxxx
# Stripe (for payment processing)
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxConclusion
Soloist's authentication system leverages Convex Auth to provide a solid foundation for user authentication and session management. The implementation follows many security best practices, including password hashing, token-based authentication, and email verification.
Current Security Posture: Good for development and early production. The core authentication mechanisms are sound, with proper password handling and token management.
Recommended Enhancements: Before scaling to larger user bases, implement rate limiting, account lockout mechanisms, comprehensive audit logging, and enforce email verification. These additions would elevate the security posture from "good" to "excellent."