Fetching the User Profile
What the access token gives you
The access token is a key to GitHub’s API. With it, your server can read data about the user who authorized your app. The scopes you requested (read:user user:email) determine what you can access.
Fetch the profile
Add a function to call GitHub’s API. Create src/github.ts:
// src/github.ts
export interface GitHubUser {
id: number;
login: string;
name: string | null;
email: string | null;
avatar_url: string;
}
export async function fetchGitHubUser(accessToken: string): Promise<GitHubUser> {
const response = await fetch("https://api.github.com/user", {
headers: {
authorization: `Bearer ${accessToken}`,
"user-agent": "oauth-course",
accept: "application/vnd.github+json",
},
});
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
return response.json() as Promise<GitHubUser>;
} Three headers:
authorization: Bearer <token> — The access token, using the same Bearer scheme that JWTs use. GitHub’s API reads this header, verifies the token, and returns data for the associated user.
user-agent — GitHub’s API requires this header. Requests without it are rejected with 403 Forbidden. Any non-empty string works.
accept: application/vnd.github+json — Tells GitHub to return the latest version of their JSON API format. This is optional but recommended by GitHub’s docs.
The email problem
GitHub’s /user endpoint returns the user’s public profile. The email field is the user’s publicly visible email. Many GitHub users set their email to private, in which case this field is null.
To get the user’s actual email, you need to call a second endpoint:
// src/github.ts
export interface GitHubEmail {
email: string;
primary: boolean;
verified: boolean;
}
export async function fetchGitHubEmails(accessToken: string): Promise<GitHubEmail[]> {
const response = await fetch("https://api.github.com/user/emails", {
headers: {
authorization: `Bearer ${accessToken}`,
"user-agent": "oauth-course",
accept: "application/vnd.github+json",
},
});
if (!response.ok) {
throw new Error(`GitHub emails API error: ${response.status}`);
}
return response.json() as Promise<GitHubEmail[]>;
} This returns an array of the user’s email addresses with metadata. We want the primary, verified email:
export function getPrimaryEmail(emails: GitHubEmail[]): string | null {
const primary = emails.find((e) => e.primary && e.verified);
return primary?.email ?? null;
} We only trust verified emails. An unverified email could be anything the user typed in without confirming.
Wire it into the callback
Update the callback handler in src/routes/github.ts. Replace the TODO at the end:
const accessToken = tokenData.access_token as string;
// Fetch user profile and email
const [githubUser, githubEmails] = await Promise.all([
fetchGitHubUser(accessToken),
fetchGitHubEmails(accessToken),
]);
const email = githubUser.email ?? getPrimaryEmail(githubEmails);
// TODO: create or find local user, create session
return Response.json({
githubUser: {
id: githubUser.id,
login: githubUser.login,
name: githubUser.name,
email,
avatarUrl: githubUser.avatar_url,
},
}); Add the imports at the top of the file:
import { fetchGitHubUser, fetchGitHubEmails, getPrimaryEmail } from "../github.js"; We fetch the profile and emails in parallel with Promise.all. This saves a round trip since the two requests are independent.
For the email, we first try the profile’s email field. If it is null (private), we fall back to the primary verified email from the emails endpoint.
Try it out
Go through the full flow again:
- Visit
http://localhost:3000 - Click “Log in with GitHub”
- Authorize the app
You should now see your GitHub profile data including your name, email, and avatar URL.
The access token’s job is done
After fetching the profile and email, the access token has served its purpose. We do not need to store it (unless we plan to make more GitHub API calls later, like reading the user’s repos).
For login purposes, we only needed the token long enough to fetch the user’s identity. In the next lesson, we will create a local user account from this data and start a session.
Exercises
Exercise 1: Log in and examine the response. What fields does the GitHub profile contain? Try logging in with a GitHub account that has a private email. Does the email come from the profile or the emails endpoint?
Exercise 2: Remove the user:email scope from the authorization redirect and log in again. What happens to the email? Without that scope, the emails endpoint returns 403 Forbidden, and you only get the public profile email (which might be null).
Exercise 3: Check what happens if you use an expired or invalid access token. Change the token string before calling fetchGitHubUser and observe the error.
Why do we call both /user and /user/emails instead of just /user?
Why do we only trust verified emails from GitHub?