Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our final lesson in this course on production-ready authentication patterns.
In our last session, we built a powerful, type-safe, and reusable authorization system by composing checks at the route and API levels. We focused on the "happy path"—what happens when a user is correctly authenticated and authorized. But as any experienced developer knows, a truly robust system is defined by how it handles failure.
Today, we'll address the other side of that coin. Your goal is to learn how to manage and log server-side failures during the OAuth token exchange and user info retrieval process. We'll focus on the critical moment in the OAuth flow: when your server receives a callback from Google and attempts to exchange an authorization code for tokens. Many things can go wrong here, and simply letting your application crash or fail silently is not an option in a production environment.
We will explore how to anticipate specific failure modes, handle them gracefully from the user's perspective, and—most importantly—create a detailed, structured log of these events so you can diagnose and resolve issues quickly.
1. The Anatomy of an OAuth Callback Failure
Let's pinpoint the exact moment of risk. After a user approves your application on Google's consent screen, Google redirects them back to your specified callback URL (e.g., /api/auth/google/callback). This request includes an authorization_code and a state parameter.
Your server-side handler for this route is responsible for several critical steps:
- Validating the
stateparameter to prevent Cross-Site Request Forgery (CSRF). - Exchanging the
authorization_codefor anaccess_tokenandrefresh_tokenby making aPOSTrequest to Google's token endpoint. - Using the
access_tokento fetch the user's profile information from Google's user info endpoint.
Steps 2 and 3 involve network requests to an external service (Google), and this is where a multitude of issues can arise. A failure here could be due to a user action (like taking too long), a configuration error on your part, or a problem on Google's end.
2. Identifying and Categorizing Failure Modes
To handle errors effectively, you first need to know what you're up against. Generic catch (e) blocks aren't enough. We need to understand the specific error responses Google provides.
Google's documentation is your best friend here. It provides a detailed list of potential errors that can occur during the token exchange.
Using OAuth 2.0 for Server to Server Applications
The Google Identity documentation provides a crucial table of error codes that their OAuth 2.0 endpoint can return. Understanding these is fundamental to building a resilient error-handling strategy.
Please review the section titled 'JWT error codes'. While the title refers to JWTs for service accounts, these error codes are standard for the token endpoint and are highly relevant to our web server flow. Pay close attention to the error field, its meaning, and the suggested resolution. This table is the foundation for our error handling logic.
After reviewing the table, you can see that errors fall into a few broad categories:
- Client Configuration Errors (
invalid_client,unauthorized_client): These happen if yourGOOGLE_CLIENT_IDorGOOGLE_CLIENT_SECRETare incorrect, or if your app isn't authorized for the requested scopes in the Google Cloud Console. These are developer-fixable problems. - Invalid Grant Errors (
invalid_grant): These are common. They can mean the authorization code is expired, has already been used, or is malformed. The user might have taken too long on the consent screen, or tried to refresh the callback page. These are often user-recoverable. - Scope & Policy Errors (
invalid_scope,admin_policy_enforced): These indicate a mismatch between the scopes your application requests and what's allowed, either by configuration or by the user's Google Workspace administrator policies. These are configuration or environmental issues.
3. Implementing Graceful Error Handling
Now, let's translate this knowledge into code within our TanStack Start server function for the OAuth callback. The core strategy is to wrap the external calls in a try...catch block and inspect the error object to make an informed decision.
Most OAuth client libraries (like lucia-auth's providers or oauth4webapi) will throw an error object that contains the specific code from the provider.
Here is a conceptual implementation of an OAuth callback handler:
// Example: src/routes/api/auth/google/callback.ts
import { server$ } from '@tanstack/bling';
import { exchangeCodeForTokens, getUserInfo } from '~/server/google-auth'; // Your auth functions
import { log }s from '~/server/logging'; // Our new logger
export const GET = server$(async (c) => {
const { code, state, error: oauthError } = c.req.query();
const storedState = getCookie(c, 'oauth_state');
// 1. Handle user cancellation or direct errors from Google
if (oauthError) {
log('WARN', 'OAuth flow cancelled by user or provider error', {
error: oauthError,
});
return c.redirect('/login?error=access_denied');
}
// 2. Validate CSRF state token
if (!state || !storedState || state !== storedState) {
log('ERROR', 'OAuth state mismatch (potential CSRF)', {
receivedState: state,
expectedState: storedState,
});
return c.redirect('/login?error=invalid_state');
}
try {
// 3. Attempt token exchange
const tokens = await exchangeCodeForTokens(code);
// 4. Attempt to fetch user info
const googleUser = await getUserInfo(tokens.accessToken);
// ... success path: create session, set cookie, redirect to dashboard ...
return c.redirect('/dashboard');
} catch (e) {
// 5. THIS IS OUR FOCUS: The catch block
if (e instanceof OAuth2RequestError) { // Assuming a typed error from a library
log('ERROR', 'OAuth token exchange failed', {
errorCode: e.error, // e.g., 'invalid_grant'
errorDescription: e.error_description,
// Add a traceId if you have one from a middleware
});
// Decide what to tell the user based on the error
switch (e.error) {
case 'invalid_grant':
case 'invalid_request':
// User-recoverable error. Guide them back to the start.
return c.redirect('/login?error=auth_failed');
case 'invalid_client':
case 'unauthorized_client':
case 'invalid_scope':
// Developer/configuration error. Show a generic error page.
// The detailed log is for the dev team.
return c.redirect('/login?error=server_config_error');
default:
// Unknown error, log it and show a generic message.
log('ERROR', 'Unknown OAuth error', { originalError: e });
return c.redirect('/login?error=unknown');
}
}
// Handle non-OAuth errors (e.g., network failure)
log('ERROR', 'An unexpected error occurred during OAuth callback', { error: e });
return c.redirect('/login?error=unknown');
}
});
This approach achieves two goals:
- Manages the failure: It provides a reasonable user experience by redirecting them with a hint about what went wrong, rather than showing a broken page.
- Sets the stage for logging: It identifies and categorizes the error, preparing the exact information we need to log for observability.
4. Implementing Structured Logging
console.log is useful for development, but in production, you need structured, searchable logs. Plain text messages are difficult to parse, filter, and alert on automatically. This is where structured logging in a format like JSON comes in.
To understand why and how to structure our logs, let's look at some industry best practices. This article from Curity provides excellent guidance on production-grade logging for authentication systems.
Please read the sections 'Use Technical Support Logs', 'JSON Log Formats', and 'Log Ingestion'. Focus on how error events are logged with specific fields, the benefits of a JSON structure, and the example of a clean, type-safe log entry after processing.
From the resource, we can extract several key principles for our logger:
- Use Log Levels: Differentiate the severity of events (
INFO,WARN,ERROR). - Use a Structured Format (JSON): This makes logs machine-readable for platforms like Datadog, Splunk, or Elastic.
- Include Rich Context: A log message like "Error" is useless. A good log entry includes a timestamp, log level, a clear message, and a payload with relevant data like error codes, trace IDs, and user identifiers (if safe to do so).
Let's create a very simple structured logger for our TanStack Start application. In a real-world project, you'd use a battle-tested library like pino, but this demonstrates the core concept.
// src/server/logging.ts
/**
* A simple structured JSON logger.
* In production, use a dedicated library like pino for performance and features.
*/
export function log(
level: 'INFO' | 'WARN' | 'ERROR',
message: string,
context: Record<string, any> = {}
) {
const logEntry = {
timestamp: new Date().toISOString(),
level,
message,
...context,
};
// In a real app, you might write to different streams based on level
if (level === 'ERROR') {
console.error(JSON.stringify(logEntry));
} else {
console.log(JSON.stringify(logEntry));
}
}
Now, when we use log() in our catch block from the previous section, we're no longer just printing a string. We're generating a structured, data-rich event.
A failed token exchange due to an expired code would produce a log entry like this in your server's output:
{
"timestamp": "2023-10-27T10:30:00.123Z",
"level": "ERROR",
"message": "OAuth token exchange failed",
"errorCode": "invalid_grant",
"errorDescription": "Bad Request"
}
This is infinitely more valuable than a simple console.error('oops'). Your log aggregation platform can now parse this, and you can build dashboards to track the frequency of invalid_grant errors or set up alerts for critical errors like invalid_client.
Test your understanding!
Your application starts seeing a spike in admin_policy_enforced errors from the Google OAuth callback.
- According to the documentation, what is the likely meaning of this error?
- What log level would be appropriate for this event?
- What specific pieces of contextual information would be most valuable to include in the structured log entry to help a developer or administrator debug this?
Show answer
- Meaning: The user's Google Workspace administrator has policies that prevent your application from being granted one or more of the requested scopes.
- Log Level:
ERRORis appropriate. While not a bug in your code per se, it's a hard failure preventing a user from logging in due to an environmental constraint. - Contextual Information:
- The
errorCodeitself:admin_policy_enforced. - The
scopesthat your application requested. This is crucial for the admin to check against their policies. - The user's email or domain (if you have it and it's permissible under your privacy policy) to identify which organization is affected.
- A
traceIdto correlate this event with other logs from the same request.
- The
Conclusion
Congratulations on completing the course! In this final lesson, you've tackled a crucial aspect of building production-ready applications: robust failure handling and observability. You have learned that simply catching errors is not enough; you must understand, categorize, and log them in a structured way to maintain a healthy and debuggable system.
Key Takeaways:
- Anticipate Failures: The OAuth callback is a critical failure point. Use provider documentation to understand the specific errors that can occur.
- Handle Gracefully: Differentiate between user-recoverable errors (e.g., expired code) and system/configuration errors (e.g., invalid client ID). Guide the user accordingly.
- Log with Structure: Avoid plain text
console.log. Use structured JSON logging to make server-side events machine-readable and easy to analyze. - Context is King: Your logs should include not just an error message, but also the severity (log level), a timestamp, and rich contextual data (error codes, trace IDs) to make debugging effective.
By combining the advanced composition patterns from our last lesson with the resilient error handling and logging strategies from today, you are now equipped to build sophisticated, secure, and maintainable authentication systems with TanStack Start. You've covered the entire lifecycle, from project setup and routing to advanced OAuth flows and production-grade observability.