Soloist.v2.0.0

File Structure

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

1

User Submits Registration Form

User provides email and password through the registration interface. The frontend validates basic input requirements before submitting.

2

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.

3

Account Creation

If validation passes:

  • Password is hashed using industry-standard bcrypt algorithm
  • User record is created in the users table
  • 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.

4

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.

5

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

1

Credentials Submitted

User provides email and password through SignInWithGitHub component or custom sign-in form. Request is sent to Convex Auth.

2

Credential Verification

Convex Auth performs secure verification:

  • Looks up user by email in users table
  • 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.

3

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.

4

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.

5

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:

  1. Client sends request with JWT token in Authorization header or cookie
  2. Convex backend extracts token from request
  3. Token signature is verified using secret key
  4. Token expiration is checked
  5. User ID is extracted from valid token
  6. 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

1

User Requests Password Reset

User provides their email address through the "Forgot Password" form. The system looks up the account.

2

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.

3

User Submits Reset Code

User enters the code from their email and provides a new password. Both are validated before processing.

4

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. 1.

    User Clicks "Sign in with GitHub"

    The SignInWithGitHub component calls signIn("github", )

  2. 2.

    Redirect to GitHub

    User is redirected to GitHub's OAuth authorization page to approve access

  3. 3.

    GitHub Callback

    GitHub redirects back to /api/auth/callback/github with authorization code

  4. 4.

    Token Exchange

    Convex exchanges the code for GitHub access token and retrieves user profile

  5. 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. 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_xxxxxxxxxxxxx

Conclusion

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."