Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our series on building a secure authentication system.
In our last lesson, we successfully completed the OAuth 2.0 authorization code flow by exchanging the temporary code for a set of tokens from Google, including the all-important access_token and id_token.
Today, we'll put those tokens to use. The goal for this lesson is to retrieve and validate the user's profile information from Google's APIs using the tokens we just obtained. This step is the bridge between authenticating a user with Google and identifying them within our own application. By the end of this lesson, you'll have the user's verified name, email, and unique Google ID, ready for the next stage.
1. From Token to Identity: Accessing User Data
The tokens you received from Google are credentials. The access_token is a key that grants permission to access specific Google APIs on behalf of the user, while the id_token is a verifiable assertion about the user's identity. The permissions and data you can access are defined by the scopes you requested in the initial authorization step (e.g., openid, profile, email).
There are two primary, standard-compliant methods for retrieving the user's profile information on the server side:
- Using the
access_tokento call theuserinfoendpoint. - Decoding and validating the
id_tokendirectly.
Let's explore these options to understand their mechanics and trade-offs.
The article 'Verifying the User Info' from oauth.com provides an excellent breakdown of these methods. It will give you a clear conceptual framework before we dive into the implementation.
Please read the following sections: 'Verifying the User Info' (from ID Token): This explains the concept of decoding the id_token and why it's a secure option in this context. 'Using the Access Token to Retrieve User Info': This describes the userinfo endpoint, which uses the access_token. 'Which method to use?': This concluding section summarizes the trade-offs, which will help solidify your understanding.
As you've read, both methods are valid, but they serve slightly different purposes. Using the userinfo endpoint is a classic OAuth 2.0 pattern for fetching data from a resource server. Decoding the id_token is a core feature of OpenID Connect (OIDC), the identity layer built on top of OAuth 2.0, and is specifically designed for retrieving identity claims.
2. The userinfo Endpoint in Practice
Let's briefly look at how a raw API call to the userinfo endpoint would work. This helps in understanding what our libraries are abstracting away. The process involves making a GET request to a specific URL and including the access_token in the Authorization header.
This Stack Overflow thread, 'Get user info via Google API', contains several answers that provide the exact endpoint URL and curl examples. It's a great practical reference.
Scan through the top answers by Cheeso and Glen Thompson. Pay attention to two key things: The endpoint URL: https://www.googleapis.com/oauth2/v.../userinfo (the version might vary, but the structure is the same). The curl example showing how the access_token is passed as a Bearer token in the Authorization header.
The curl command you saw, curl -H "Authorization: Bearer <access_token>", is a standard way of interacting with protected APIs. While we could construct this fetch request manually, the google-auth-library we've already installed provides a more robust and idiomatic way to handle identity verification, which brings us to our implementation.
3. Implementation: Verifying the id_token
For our use case—securely identifying the user upon sign-in—the recommended approach is to verify and decode the id_token. The google-auth-library provides a verifyIdToken method that handles this perfectly.
This method is superior to simply decoding the token because it performs several critical security checks:
- Signature Verification: It cryptographically verifies that the token was signed by Google and has not been tampered with.
- Audience (
aud) Check: It ensures the token was issued specifically for our application by checking if theaudclaim matches ourGOOGLE_CLIENT_ID. - Issuer (
iss) Check: It confirms the token was issued by Google. - Expiry Check: It verifies that the token is not expired.
Let's update our callback handler to perform this verification.
Update the file: src/routes/api/auth/google/callback.ts
We will add the new logic inside the try...catch block from the previous lesson, right after we receive the tokens.
import {
eventHandler,
getQuery,
getCookie,
deleteCookie,
} from "vinxi/http";
import { oauth2Client } from "../../../../lib/google-auth";
export default eventHandler(async (event) => {
// --- Part 1: Validation (from previous lesson) ---
const query = getQuery(event);
const storedState = getCookie(event, "oauth_state");
if (!query.state || !storedState || query.state !== storedState) {
event.node.res.statusCode = 400;
return { error: "State mismatch" };
}
if (query.error) {
event.node.res.statusCode = 401;
return { error: query.error, description: query.error_description };
}
// --- Part 2: Token Exchange & Profile Retrieval ---
const authCode = query.code as string;
const storedCodeVerifier = getCookie(event, "pkce_code_verifier");
if (!authCode || !storedCodeVerifier) {
event.node.res.statusCode = 400;
return { error: "Missing authorization code or PKCE verifier" };
}
const cookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
path: "/",
};
deleteCookie(event, "oauth_state", cookieOptions);
deleteCookie(event, "pkce_code_verifier", cookieOptions);
try {
// 1. Exchange the authorization code for tokens
const { tokens } = await oauth2Client.getToken({
code: authCode,
codeVerifier: storedCodeVerifier,
});
// Ensure the id_token exists
if (!tokens.id_token) {
event.node.res.statusCode = 500;
return { error: "ID token not found in token response" };
}
// 2. Verify the ID token to get user profile information
const ticket = await oauth2Client.verifyIdToken({
idToken: tokens.id_token,
audience: process.env.GOOGLE_CLIENT_ID, // Specify the CLIENT_ID of the app that accesses the backend
});
const payload = ticket.getPayload();
if (!payload) {
event.node.res.statusCode = 500;
return { error: "Failed to get token payload" };
}
// 3. User information is now in the payload
// The 'sub' field is Google's unique identifier for the user.
const userProfile = {
googleId: payload.sub,
email: payload.email,
name: payload.name,
picture: payload.picture,
};
// For now, we'll return the profile to confirm success.
// In the next lesson, we'll use this to create a local user and a JWT.
return {
message: "Authentication successful! User profile retrieved.",
user: userProfile,
};
} catch (error: any) {
console.error("Authentication process failed:", error.response?.data || error.message);
event.node.res.statusCode = 500;
return { error: "Failed during authentication process." };
}
});
Code Breakdown:
- Get Tokens: We still call
oauth2Client.getToken()as we did in the last lesson. - Verify ID Token: This is the new, crucial step. We pass the
id_tokenfrom thetokensobject tooauth2Client.verifyIdToken(). We also provide ourGOOGLE_CLIENT_IDas theaudienceto ensure the token was intended for our application. - Get Payload: If verification is successful,
ticket.getPayload()returns a JSON object containing the user's identity claims. This payload is now considered trustworthy. - Extract Profile: We extract the key fields we need:
sub(the unique subject/user ID),email,name, andpicture. We map them to auserProfileobject for clarity. - Return Profile: We return this
userProfileobject in the response. If you test the flow now, you should see your own Google profile information returned as JSON in the browser.
Conclusion
Excellent work! You have now connected the dots from a user clicking "Sign in with Google" to having their verified, trustworthy identity information available on your server.
Key Takeaways:
- There are two main ways to get user info: calling the
userinfoendpoint with anaccess_tokenor decoding theid_token. - For authenticating a user (the "who"), the OpenID Connect pattern of verifying the
id_tokenis the most direct and secure method. - The
google-auth-library'sverifyIdTokenmethod is a powerful tool that performs essential security validations, such as checking the signature, audience, issuer, and expiration. - A successful verification yields a payload containing the user's unique ID (
sub), email, name, and other profile details permitted by the requested scopes.
Next Lesson Preview:
With the user's Google profile in hand, what's next? We need to integrate this identity into our own application's authentication system. In the next lesson, we will map the OAuth identity to an application-specific JWT. This involves looking up the user in our database by their Google ID, creating a new user record if one doesn't exist, and then issuing our own JWT to manage their session going forward.