Fatih Aygün
Posted on August 13, 2022
GitHub offers a free API for authenticating users. It's based on OAuth, an open standard for authentication. OAuth is a fairly large subject but our use case is not that complicated. You can learn more on GitHub's documentation but this is how it works in essence:
- We'll create a GitHub app and enter a callback URL there. We'll receive a client ID and a client secret (which are just random-looking strings).
- We'll add a "Sign in with GitHub" link to our page. The link will point to a GitHub URL that will include our client ID and a random string that we'll generate (called "state") in query parameters.
- GitHub will show the user a page depending on their authentication status:
- The user will be shown the GitHub sign-in page (only if they are not already signed in).
- The user will be asked whether they want to authorize our app (only if they have not already authorized our app recently).
- If they accept (or had accepted recently), GitHub will redirect the user to the callback URL we defined in step 1.
- The redirection will include a code and the state we sent in step 2 as query parameters. If the state doesn't match the random string that we sent, we'll know something fishy is going on and abort the process. Otherwise, we'll send a POST request to
https://github.com/login/oauth/access_token
along with our client ID, client secret, and the code we received as a query parameter. If everything goes well, GitHub will reply with an access token. - We'll use the access token in the
Authorization
header every time we want to get the user's profile data from GitHub.
We have a plan. Let's start.
Create a GitHub app
Head over to GitHub Developer Settings, click OAuth Apps on the left and then click the "New OAuth app" button. It's gonna ask you a few questions. Enter http://localhost:5173
for the homepage URL and http://localhost:5173/login
for the callback URL, and fill the rest as you like. We're giving localhost
addresses because we have to test our app before deploying to its final URL. You can just update the URLs when you deploy or create a new app and keep this one for testing and development.
After you submit the form, you'll end up on a page where you will see your app's client ID. You will also see a "Generate a new client secret" button. Go ahead and generate one and copy both into a new file in your repository's root directory and save it with the name .env
. It should look like this:
GITHUB_CLIENT_ID=<your client ID>
GITHUB_CLIENT_SECRET=<your client secret>
It's good practice to keep our app secrets and configuration in environment variables. Now add this file to your .gitignore
file so you don't accidentally push your secret to GitHub. To load this into the environment during development, we'll install the dotenv
package:
npm install -D dotenv
Then we'll import it and call it in our vite.config.ts
. The file will end up looking like this:
import { defineConfig } from "vite";
import rakkas from "rakkasjs/vite-plugin";
import tsconfigPaths from "vite-tsconfig-paths";
dotenv.config();
export default defineConfig({
envDir: ".",
plugins: [
tsconfigPaths(),
rakkas({
adapter: "cloudflare-workers",
}),
],
});
Now we will be able to access the variables with, e.g., process.env.GITHUB_CLIENT_ID
in our server-side code. process.env
is a Node-specific global but Rakkas makes it available on Cloudflare Workers too.
Adding a "sign in" link
Right now, we only have a single page. But it's not gonna be like that forever. We probably want to see the "sign in" link on the header of every page. Rakkas has a layout system for shared elements like this. Layouts wrap nested layouts and pages under the same directory and its subdirectories. So if we create a layout.tsx
file in the src/routes
directory, it will wrap every single page on our app.
We said the "sign in" link would point to a GitHub URL. That URL, according to GitHub's documentation, is https://github.com/login/oauth/authorize?client_id=<CLIENT_ID>${clientId}&state=<STATE>
. Our client ID is in process.env.GITHUB_CLIENT_ID
which is only accessible on the server-side. So we'll use useServerSideQuery
again to access it. We'll tackle the handling of the state
parameter later, let's give it 12345
for now. So here's the first draft of our src/routes/layout.tsx
:
import { LayoutProps, useServerSideQuery } from "rakkasjs";
export default function MainLayout({ children }: LayoutProps) {
const {
data: { clientId, state },
} = useServerSideQuery(() => ({
clientId: process.env.GITHUB_CLIENT_ID,
state: "12345",
}));
return (
<>
<header>
<strong>uBlog</strong>
<a
style={{ float: "right" }}
href={
"https://github.com/login/oauth/authorize" +
`?client_id=${clientId}` +
`&state=${state}`
}
>
Sign in with GitGub
</a>
<hr />
</header>
{children}
</>
);
}
When you launch the dev server you'll see that we have a site header now. And the "Sign in with GitHub" link will take you to GitHub's authorization page. If you go ahead and authorize your app GitHub will redirect you to a URL that looks like http://localhost:5173/login?code=<BUNCH_OF_RANDOM_LOOKING_CHARS>&state=12345
. http://localhost:5173/login
is the URL we entered as the callback URL and the rest are the parameters sent by GitHub. Of course, you will get a 404 error because we haven't implemented that endpoint yet. Let's do that now.
Login callback
We'll create a src/routes/login.page.tsx
file to implement the login callback. In it, we will use the code
query parameter to get an access token from GitHub and then use that access token to get the user's profile data. We will use the useServerSideQuery
hook again because we don't want to expose our client secret to the client. Remember, the useServerSideQuery
callback runs on the server and will not be part of the client bundle. Let's see what the profile data looks like first by printing it as JSON:
import { PageProps, useServerSideQuery } from "rakkasjs";
export default function LoginPage({ url }: PageProps) {
const error = url.searchParams.get("error");
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const { data: userData } = useServerSideQuery(async () => {
if (code && state === "12345") {
const { access_token: token } = await fetch(
"https://github.com/login/oauth/access_token" +
`?client_id=${process.env.GITHUB_CLIENT_ID}` +
`&client_secret=${process.env.GITHUB_CLIENT_SECRET}` +
`&code=${code}`,
{
method: "POST",
headers: { Accept: "application/json" },
}
).then((r) => r.json<{ access_token: string }>());
if (token) {
const userData = fetch("https://api.github.com/user", {
headers: {
Authorization: `token ${token}`,
},
}).then((r) => r.json());
return userData;
}
}
});
if (error) {
return <div>Error: {error}</div>;
}
return <pre>{JSON.stringify(userData, null, 2)}</pre>;
}
If everything goes well, you should see your GitHub user profile data in JSON format when you click on "Sign in with GitHub". Mine looks like this:
{
"login": "cyco130",
"id": 10846005,
"node_id": "MDQ6VXNlcjEwODQ2MDA1",
"avatar_url": "https://avatars.githubusercontent.com/u/10846005?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/cyco130",
"html_url": "https://github.com/cyco130",
"followers_url": "https://api.github.com/users/cyco130/followers",
"following_url": "https://api.github.com/users/cyco130/following{/other_user}",
"gists_url": "https://api.github.com/users/cyco130/gists{/gist_id}",
"starred_url": "https://api.github.com/users/cyco130/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/cyco130/subscriptions",
"organizations_url": "https://api.github.com/users/cyco130/orgs",
"repos_url": "https://api.github.com/users/cyco130/repos",
"events_url": "https://api.github.com/users/cyco130/events{/privacy}",
"received_events_url": "https://api.github.com/users/cyco130/received_events",
"type": "User",
"site_admin": false,
"name": "Fatih Aygün",
"company": "Lityum AŞ",
"blog": "",
"location": "Istanbul",
"email": null,
"hireable": null,
"bio": "Programmer, musician, amateur linguist.",
"twitter_username": "cyco130",
"public_repos": 32,
"public_gists": 4,
"followers": 26,
"following": 25,
"created_at": "2015-02-04T09:24:28Z",
"updated_at": "2022-06-29T03:02:45Z"
}
Success! We've accomplished quite a lot! It's a good time to take a break!
What's next?
In the next article, we'll finish our authentication feature. We will use cookies to remember who is who.
You can find the progress up to this point on GitHub.
Posted on August 13, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.