Sign in with GitHub
Tags: react, cloud
Published: 2022-08-13 08:06:28

GitHub offers a free API for authenticating users. It's based on [OAuth](https://en.wikipedia.org/wiki/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](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#web-application-flow) but this is how it works in essence:

1. 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).
2. 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.
3. 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.
4. 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.
5. 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](https://github.com/settings/developers), 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:

```bash
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:

```bash
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:

```ts
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](https://rakkasjs.org/guide/layouts) 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`:

```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:

```tsx
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:

```json
{
	"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](https://github.com/rakkasjs/ublog/tree/article-4).