Hello! Welcome to our final lesson in the course on mastering TanStack Start and building production-ready authentication patterns.
In our last session, we took a crucial step towards making our application robust by learning to manage and log server-side failures during the OAuth token exchange. We built a simple structured logger and saw how vital it is to capture contextual information when things go wrong.
Today, we will expand on that foundation to implement a comprehensive logging strategy for all critical authentication events. Your goal is to implement structured logging for critical authentication events (e.g., login, logout, token refresh) within server functions. We will move beyond just logging failures and learn to create a complete, auditable trail of security-sensitive actions in our application. This is a non-negotiable aspect of any production system, essential for security monitoring, compliance, and effective debugging.
We will replace our basic logger with a powerful, industry-standard library—Winston—to create structured, searchable, and persistent logs. This final lesson will tie together our authentication logic with the observability required to maintain it in the real world.
1. Defining Your Logging Strategy: The "What" and "Why"
Before writing any code, a solid plan is essential. We need to answer three questions: What events are important? What information should we record for each event? And, just as critically, what information must we never record?
What Events to Log?
For an authentication system, some events are too important to ignore. Logging them provides a security audit trail and helps diagnose user-reported issues.
Logging - OWASP Cheat Sheet Series
To determine which events are considered critical from a security and operational standpoint, we can turn to the OWASP (Open Web Application Security Project) Logging Cheat Sheet, an industry-standard guide.
Please read the section 'Which events to log'. Focus on the list of events that should always be logged, paying special attention to 'Authentication successes and failures' and 'Session management failures'. These items form the core requirements for our task.
Based on the OWASP guidelines and the context of our application, our list of must-log events includes:
- User Login Success: A user successfully authenticated.
- User Login Failure: An attempt to log in failed (e.g., wrong password).
- User Logout: A user explicitly logged out.
- Token Refresh Success: An access token was successfully refreshed.
- Token Refresh Failure: The attempt to refresh a token failed (e.g., invalid refresh token).
- OAuth Callback Failure: (Covered in the last lesson) The OAuth flow failed.
- Authorization Failure: A user tried to access a resource they are not permitted to see.
What Data to Include in Each Log?
A log message like "Login failed" is insufficient. For logs to be useful, they must contain context. The OWASP guide provides a great framework: "when, where, who, and what."
Logging - OWASP Cheat Sheet Series
Now that we know which events to capture, let's define the structure of our log entries. What specific pieces of data will make them useful for debugging and analysis?
Read the section titled 'Event attributes'. As you read, think about how the 'when, where, who, and what' categories map to a structured JSON object. This will guide the design of our log payloads.
For a typical authentication event, a structured log entry in JSON might look like this:
{
"timestamp": "2023-11-10T14:22:01.123Z", // When
"level": "INFO", // What (Severity)
"message": "User login successful", // What (Description)
"service": "auth-service", // Where
"userId": "usr_abc123", // Who
"ipAddress": "203.0.113.54", // Who (Context)
"requestId": "xyz-789" // Context for tracing
}
What Data to Exclude?
Logging is a double-edged sword. If you log sensitive information, your logs can become a major security vulnerability.
Logging - OWASP Cheat Sheet Series
This is the most important rule of secure logging. Accidentally logging sensitive data can have severe consequences. Let's review the definitive list of what to exclude.
Carefully read the section 'Data to exclude'. This is non-negotiable. Pay special attention to items like 'Session identification values', 'Access tokens', 'Authentication passwords', and sensitive PII. We must ensure none of these ever appear in our logs.
Rule of Thumb: Log identifiers (like userId), but never credentials or secrets (like passwords, JWTs, or API keys).
2. The "How": Implementing Structured Logging with Winston
In our previous lesson, we used a simple console.log wrapper. Now, we'll graduate to Winston, a highly popular and configurable logging library for Node.js. It allows us to easily implement the strategy we just defined.
First, add Winston to your project:npm install winston
Core Concepts of Winston
Winston's power comes from its composable architecture, primarily built around formats, levels, and transports.
Winston - Logging in JavaScript & Node.js applications
To get started with Winston, let's watch a short video that covers its fundamental concepts. This will explain how to create a logger, format its output, and direct it to different destinations.
Watch from the beginning to 7:44. Focus on understanding the roles of createLogger, levels, format.json(), and transports (specifically Console and File). This provides the building blocks for our logger module.
Creating a Centralized Logger Module
Let's create a dedicated module for our logger configuration. This ensures logging is consistent across our entire server-side application.
Create a new file: src/server/logger.ts
// src/server/logger.ts
import winston from 'winston';
const { format, transports } = winston;
const { combine, timestamp, json, errors } = format;
// Define standard log levels
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4,
};
// Use the highest level in production, and debug in development
const level = process.env.NODE_ENV === 'production' ? 'warn' : 'debug';
// Define the format for our logs
const logFormat = combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
errors({ stack: true }), // This will add a stack trace for error-level logs
json() // This is the key for structured logging
);
// Define the transports (destinations) for the logs
const logTransports = [
// In development, we'll log everything to the console with pretty printing
new transports.Console({
format: process.env.NODE_ENV === 'production'
? logFormat // In prod, use the standard JSON format
: format.combine(logFormat, format.prettyPrint()), // In dev, make it readable
}),
// In a production-like environment, you'd also want to write to files
// new transports.File({ filename: 'logs/error.log', level: 'error' }),
// new transports.File({ filename: 'logs/auth-events.log' }),
];
// Create the main logger instance
const logger = winston.createLogger({
level,
levels,
format: logFormat,
transports: logTransports,
defaultMeta: { service: 'your-app-name' }, // Add service name to all logs
});
export default logger;
With this module, we now have a powerful, reusable logger that:
- Outputs structured JSON.
- Includes a timestamp in every log.
- Includes stack traces for errors.
- Can be configured via environment variables.
- Is ready to be imported and used anywhere in our server code.
Enriching Logs with Request-Specific Context
Often, we want to add context that is specific to a single request, like a userId or requestId. Winston's child() loggers are perfect for this.
Winston - Logging in JavaScript & Node.js applications
A key part of effective logging is adding context. Let's see how Winston's child loggers allow us to automatically attach common metadata—like a user's ID—to a series of related log events.
Watch the segment from 11:32 to 15:20. This demonstrates adding custom key-value pairs and using child() loggers. This is the exact pattern we'll use to associate logs with specific users or requests.
3. Logging Critical Events in Server Functions
Now, let's integrate our new logger into the server functions that handle authentication.
Logging Login Events
In your server function that handles username/password login:
// Example: src/routes/api/auth/login.ts
import { server$ } from '@tanstack/bling';
import logger from '~/server/logger'; // Import our new logger
export const POST = server$(async (c) => {
const { username, password } = await c.req.json();
const ipAddress = c.req.headers.get('x-forwarded-for') || c.req.conn.remoteAddr;
try {
const user = await validateUserCredentials(username, password);
// Create a child logger with the user's ID for all subsequent logs in this request
const userLogger = logger.child({ userId: user.id, ipAddress });
userLogger.info('User login successful');
// ... create session, set cookie ...
return c.json({ success: true });
} catch (error) {
// Log the failed attempt. Note the 'warn' level.
logger.warn('User login failed', {
username,
reason: 'Invalid credentials',
ipAddress
});
// ... return 401 Unauthorized ...
}
});
Produced Log on Success:
{
"message": "User login successful",
"level": "info",
"timestamp": "...",
"service": "your-app-name",
"userId": "usr_abc123",
"ipAddress": "203.0.113.54"
}
Logging Logout Events
In your logout handler, you'd have access to the user's session.
// Example: src/routes/api/auth/logout.ts
import { server$ } from '@tanstack/bling';
import logger from '~/server/logger';
import { getAuthenticatedUser } from '~/server/auth';
export const POST = server$(async (c) => {
const user = await getAuthenticatedUser(c);
if (user) {
logger.info('User logout successful', { userId: user.id });
// ... invalidate session, clear cookie ...
}
return c.json({ success: true });
});
Logging Token Refresh Events
In the server function or middleware that handles token refreshing:
// Example: In a middleware or dedicated token refresh endpoint
async function handleTokenRefresh(refreshToken: string) {
try {
const { newAccessToken, userId } = await refreshTokens(refreshToken);
logger.info('Token refresh successful', { userId });
return newAccessToken;
} catch (error) {
const decodedToken = decode(refreshToken); // A function to decode without verifying
const userId = decodedToken?.userId || 'unknown';
logger.error('Token refresh failed', {
userId,
reason: 'Invalid or expired refresh token'
});
throw new Error('Refresh failed');
}
}
Here, we use error because a failed refresh often has a direct impact on the user's experience, potentially logging them out.
Test your understanding!
A user reports they were suddenly logged out around 3:30 PM. You suspect their refresh token might have expired or become invalid. In your log aggregation tool (like Datadog, Splunk, or even just grep), how would you construct a query to find all relevant events for this specific user around the time of the incident? What fields from your structured logs would you use?
Show answer
My query strategy would be:
- Filter by User: I'd start by filtering all log entries where
userIdmatches the affected user's ID. - Filter by Time: I would narrow the time window to a few minutes around 3:30 PM (e.g.,
3:25 PMto3:35 PM). - Filter by Event Type: I would search for log messages containing
Token refresh. Specifically, I'd look for entries withlevel: 'error'andmessage: 'Token refresh failed'. - Analyze the Context: The
reasonfield in the resulting log entry would tell me why it failed (e.g., 'Invalid or expired refresh token'), confirming my hypothesis. If I had arequestIdfield, I could use it to find all other logs (like HTTP requests) associated with that single failed operation.
This precise query demonstrates the power of structured logging. You can pinpoint the exact cause of failure for a specific user, which would be nearly impossible with a mess of unstructured text logs.
Conclusion
Congratulations on completing the entire course! In this final lesson, you've implemented one of the most critical features for a production-ready application: a robust and structured logging system.
Key Takeaways:
- Log with Purpose: We established a clear strategy for what authentication events to log (logins, logouts, refreshes) and, more importantly, what sensitive data to exclude.
- Structure is Power: You learned why structured JSON logging is superior to plain text, enabling powerful querying, analysis, and alerting.
- Use the Right Tools: We implemented a production-grade logger with Winston, configuring it with appropriate levels, formats, and transports.
- Context is King: You applied this logger to your server functions, using child loggers to enrich events with critical context like
userIdandipAddress, turning simple messages into valuable data points.
Throughout this course, you have journeyed from initializing a TanStack Start project to building a complete, secure, and observable authentication system. You've mastered file-based routing, server-side data loading, JWT session management, token refresh mechanics, OAuth 2.0 integration, and now, production-grade logging.
You are now well-equipped to build sophisticated, modern web applications that are not only feature-rich but also secure, resilient, and maintainable. Well done, and best of luck with your future projects
Can't find a good explanation? Sign up and we'll make it for you
Sign up