Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Initiating OAuth 2.0 with PKCE (Server-Side)

Hello! Welcome back to our series on mastering TanStack Start.

In our last lesson, we took the first crucial step in our Google OAuth integration by setting up a project in the Google Cloud Console, generating a Client ID and Secret, and securely storing them in our application's environment.

Today, we'll build on that foundation. The learning outcome for this lesson is to implement the server-side logic to initiate the OAuth authorization code flow with PKCE. This means we will create the "Sign in with Google" starting point: a server route that generates the necessary security tokens, constructs a special URL, and redirects the user to Google's authentication page.

Let's dive into the mechanics of this flow.

1. Understanding the Authorization Code Flow

The "Authorization Code Flow" is the standard, most secure method for web server applications to handle OAuth 2.0. It ensures that sensitive tokens are never exposed to the user's browser directly. Instead, our server communicates with Google's server behind the scenes.

To get a clear, high-level overview of this multi-step process, please watch the following video.

OAuth Authorization code flow

This video, 'OAuth Authorization code flow' by Jan Goebel, provides an excellent explanation of the entire flow. It covers the roles of the client and server, the purpose of redirecting the user, and how the final tokens are obtained.

Please watch the following segments: Initiating the Flow (2:45 - 4:12): Focus on how the application redirects the user to the authorization server with specific parameters. User Consent & Redirect Back (4:12 - 6:33): Understand the user's role and how they are sent back to our app with an 'authorization code'. Exchanging the Code (6:33 - 8:01): See how the server uses the code to get the actual access token. Server-Side Applications (10:11 - 11:07): This part is particularly relevant as it describes the flow for a 'confidential client' like our SSR application, emphasizing the role of the client secret.

As the video explains, the core idea is a two-part exchange:

  1. Our app sends the user to Google. The user logs in and consents. Google sends the user back to our app with a temporary authorization code.
  2. Our server takes that authorization code and secretly exchanges it with Google for a long-lived access token.

2. Securing the Flow with PKCE

A potential vulnerability in this flow is that the authorization code could be intercepted during the redirect back to our application. To mitigate this, the modern best practice is to use Proof Key for Code Exchange (PKCE), pronounced "pixy".

PKCE adds a dynamic, request-specific secret to the flow, ensuring that even if the authorization code is stolen, it's useless without the original secret.

This next video explains the concept clearly.

OAuth PKCE | OAuth Proof Key for Code Exchange explained

The video 'OAuth PKCE | OAuth Proof Key for Code Exchange explained' from the same channel breaks down this essential security extension.

Watch from 1:48 to 8:27. Focus on: The core mechanism: How a code_verifier is created, hashed into a code_challenge, and how they are used at different stages of the flow. Why it's better than state: Understand the additional protection it offers. Best Practice: Note the recommendation to use PKCE for all clients, including confidential server-side applications like ours.

The key takeaway is that PKCE provides a more robust security guarantee than the state parameter alone, and it is now the recommended standard for all OAuth 2.0 implementations.

This diagram provides a great visual summary of the complete flow we are about to implement.

This diagram shows the sequence of interactions between the user, our client application, the Authorization Server (Google), and the Resource Server (Google APIs). Note the generation of the `code_verifier` and `code_challenge` at the start, and their use at different points in the flow.

3. Implementing the Authorization Redirect

Now, let's translate this theory into code. We will create a server-side API route in our TanStack Start project. When a user navigates to this route (e.g., by clicking a "Sign in with Google" link), our server will perform the necessary steps and redirect them.

Create a new file at src/routes/api/auth/google.ts.

Step 3.1: Generating the state and PKCE Parameters

First, we need to generate two random values for each sign-in attempt:

  1. state: A random string to protect against Cross-Site Request Forgery (CSRF).
  2. code_verifier: A high-entropy random string that serves as the "proof key" for PKCE.

We will use Node.js's built-in crypto module for this. We then hash the code_verifier to create the code_challenge.

OAuth 2.0 for iOS & Desktop Apps

Google's documentation, 'OAuth 2.0 for iOS & Desktop Apps', provides the exact technical specifications for creating the PKCE parameters. While it's for native apps, the PKCE part is universal.

Read the section 'Step 1: Generate a code verifier and challenge'. Pay attention to the required length of the code_verifier and the formula for creating the code_challenge using SHA256.

Here's how to implement this in our API route. We also need to store the state and code_verifier in secure, HTTP-only cookies so we can retrieve and verify them in the next step (the callback).

// src/routes/api/auth/google.ts
import { eventHandler, setCookie, sendRedirect } from "vinxi/http";
import crypto from "node:crypto";

// Helper function to Base64URL-encode a buffer
function base64URLEncode(buffer: Buffer) {
  return buffer
    .toString("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=/g, "");
}

export default eventHandler(async (event) => {
  // 1. Generate state and code_verifier
  const state = crypto.randomBytes(32).toString("hex");
  const codeVerifier = base64URLEncode(crypto.randomBytes(32));

  // 2. Create code_challenge
  const codeChallenge = base64URLEncode(
    crypto.createHash("sha256").update(codeVerifier).digest()
  );

  // 3. Store state and code_verifier in cookies
  const cookieOptions = {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    path: "/",
    maxAge: 60 * 15, // 15 minutes
  };
  setCookie(event, "oauth_state", state, cookieOptions);
  setCookie(event, "oauth_code_verifier", codeVerifier, cookieOptions);

  // ... (next step: construct URL and redirect)
});

Step 3.2: Constructing the Google Authorization URL

Next, we construct the URL to Google's authorization endpoint. This URL will contain several query parameters that tell Google about our application and what we're requesting.

Using OAuth 2.0 for Web Server Applications | Authorization

The Google Identity documentation for Web Server Applications provides a definitive list of all the parameters we can use.

Review the 'HTTP/REST' parameter table under 'Step 1: Set authorization parameters'. This table details required parameters like client_id, redirect_uri, and scope, as well as recommended ones like state and access_type.

We'll use the values from our .env file and the parameters we just generated.

// src/routes/api/auth/google.ts
// (Continuing from the previous snippet)

  // 4. Construct the authorization URL
  const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth");
  const params = authUrl.searchParams;

  params.set("client_id", process.env.GOOGLE_CLIENT_ID!);
  params.set("redirect_uri", process.env.GOOGLE_REDIRECT_URI!);
  params.set("response_type", "code");
  params.set("scope", "openid email profile");
  params.set("state", state);
  params.set("code_challenge", codeChallenge);
  params.set("code_challenge_method", "S256");
  params.set("access_type", "offline"); // To get a refresh token
  params.set("prompt", "consent"); // Force consent screen for testing

  // 5. Redirect the user
  return sendRedirect(event, authUrl.toString());
});

A few notes on the parameters:

  • scope: We're requesting openid, email, and profile, which are standard for getting basic user identity information.
  • access_type: 'offline': This is important. It tells Google we want a refresh token in addition to an access token. We'll use this in a later module to keep the user logged in without them having to sign in again.
  • prompt: 'consent': This forces the consent screen to appear on every login attempt. It's very useful during development but should typically be removed in production.

Step 3.3: Creating the "Sign In" Link

The final piece is to add a link in our UI that points to this new API route. For example, on a login page (src/routes/login.tsx):

// src/routes/login.tsx
export default function LoginPage() {
  return (
    <div>
      <h1>Sign In</h1>
      <a href="/api/auth/google">
        Sign in with Google
      </a>
    </div>
  );
}

When a user clicks this link, their browser requests /api/auth/google. Our server-side handler runs, sets the cookies, builds the Google URL, and responds with a 302 redirect, sending the user's browser seamlessly to Google's login page.

Conclusion

Today, we successfully implemented the first half of the OAuth 2.0 Authorization Code Flow with PKCE.

Key Takeaways:

  • The Authorization Code Flow is a secure, redirect-based mechanism for a server to obtain access tokens on behalf of a user.
  • PKCE is an essential security layer that protects against authorization code interception by using a dynamic code_verifier and code_challenge. It is the current best practice for all OAuth clients.
  • The initiation process involves generating state (for CSRF protection) and a code_verifier (for PKCE), storing them in secure cookies, and building a specific URL for Google's authorization endpoint.
  • Our server-side API route handles this logic and redirects the user, keeping all sensitive generation logic off the client.

You have now set the stage perfectly for the next step. In our next lesson, we will create the server endpoint to handle the OAuth callback. This involves receiving the user back from Google, validating the state parameter, and preparing to exchange the authorization code for the actual tokens.

Can't find a good explanation? Sign up and we'll make it for you

Sign up