-
-
Notifications
You must be signed in to change notification settings - Fork 500
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a4f95c6
commit 3d777d8
Showing
5 changed files
with
1,139 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,236 @@ | ||
--- | ||
title: "GitHub OAuth in Astro" | ||
--- | ||
|
||
# Tutorial: GitHub OAuth in Astro | ||
|
||
Before starting, make sure you've setup your database and middleware as described in the [Getting started](/getting-started/astro) page. | ||
|
||
An [example project](https://github.com/lucia-auth/examples/tree/v3/astro/github-oauth) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/examples/tree/v3/astro/github-oauth). | ||
|
||
``` | ||
npx degit https://github.com/lucia-auth/examples/tree/v3/astro/github-oauth <directory_name> | ||
``` | ||
|
||
## Create an OAuth App | ||
|
||
[Create a GitHub OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app). Set the redirect URI to `http://localhost:4321/login/github/callback`. Copy and paste the client ID and secret to your `.env` file. | ||
|
||
```bash | ||
# .env | ||
GITHUB_CLIENT_ID="" | ||
GITHUB_CLIENT_SECRET="" | ||
``` | ||
|
||
## Update database | ||
|
||
Add a `github_id` and `username` column to your user table. | ||
|
||
| column | type | attributes | | ||
| ----------- | -------- | ---------- | | ||
| `github_id` | `number` | unique | | ||
| `username` | `string` | | | ||
|
||
Create a `DatabaseUserAttributes` interface in the module declaration and add your database columns. By default, Lucia will not expose any database columns to the `User` type. To add a `githubId` and `username` field to it, use the `getUserAttributes()` option. | ||
|
||
```ts | ||
import { Lucia } from "lucia"; | ||
|
||
export const lucia = new Lucia(adapter, { | ||
sessionCookie: { | ||
attributes: { | ||
secure: import.meta.env.PROD | ||
} | ||
}, | ||
getUserAttributes: (attributes) => { | ||
return { | ||
// attributes has the type of DatabaseUserAttributes | ||
githubId: attributes.github_id, | ||
username: attributes.username | ||
}; | ||
} | ||
}); | ||
|
||
declare module "lucia" { | ||
interface Register { | ||
Lucia: typeof lucia; | ||
} | ||
interface DatabaseUserAttributes { | ||
github_id: number; | ||
username: string; | ||
} | ||
} | ||
``` | ||
|
||
## Setup Arctic | ||
|
||
We recommend using [Arctic](https://arctic.js.org) for implementing OAuth. It is a lightweight library that provides APIs for creating authorization URLs, validating callbacks, and refreshing access tokens. This is the easiest way to implement OAuth with Lucia and it supports most major providers. | ||
|
||
``` | ||
npm install arctic | ||
``` | ||
|
||
Initialize the GitHub provider with the client ID and secret. | ||
|
||
```ts | ||
import { GitHub } from "arctic"; | ||
|
||
export const github = new GitHub( | ||
import.meta.env.GITHUB_CLIENT_ID, | ||
import.meta.env.GITHUB_CLIENT_SECRET | ||
); | ||
``` | ||
|
||
## Sign in page | ||
|
||
Create `pages/login/index.astro` and add a basic sign in button, which should be a link to `/login/github`. | ||
|
||
```html | ||
<!-- pages/login/index.astro --> | ||
<html lang="en"> | ||
<body> | ||
<h1>Sign in</h1> | ||
<a href="/login/github">Sign in with GitHub</a> | ||
</body> | ||
</html> | ||
``` | ||
|
||
## Create authorization URL | ||
|
||
Create an API route in `pages/login/github/index.ts`. Generate a new state, create a new authorization URL with createAuthorizationURL(), store the state, and redirect the user to the authorization URL. The user will be prompted to sign in with GitHub. | ||
|
||
```ts | ||
// pages/login/github/index.ts | ||
import { generateState } from "arctic"; | ||
import { github } from "@lib/auth"; | ||
|
||
import type { APIContext } from "astro"; | ||
|
||
export async function GET(context: APIContext): Promise<Response> { | ||
const state = generateState(); | ||
const url = await github.createAuthorizationURL(state); | ||
|
||
context.cookies.set("github_oauth_state", state, { | ||
path: "/", | ||
secure: import.meta.env.PROD, | ||
httpOnly: true, | ||
maxAge: 60 * 10, | ||
sameSite: "lax" | ||
}); | ||
|
||
return context.redirect(url.toString()); | ||
} | ||
``` | ||
|
||
## Validate callback | ||
|
||
Create an API route in `pages/login/github/callback.ts` to handle the callback. First, get the state from the cookie and the search params and compare them. Validate the authorization code in the search params with `validateAuthorizationCode()`. This will throw a [`OAuth2RequestError`](https://oslo.js.org/reference/oauth2/OAuth2RequestError) if the code or credentials are invalid. After validating the code, get the user's profile using the access token. Check if the user is already registered with the GitHub ID and create a new user if not. Finally, create a new session and set the session cookie. | ||
|
||
```ts | ||
// pages/login/github/callback.ts | ||
import { github, lucia } from "@lib/auth"; | ||
import { OAuth2RequestError } from "arctic"; | ||
import { generateId } from "lucia"; | ||
|
||
import type { APIContext } from "astro"; | ||
import type { DatabaseUser } from "../../../lib/db"; | ||
|
||
export async function GET(context: APIContext): Promise<Response> { | ||
const code = context.url.searchParams.get("code"); | ||
const state = context.url.searchParams.get("state"); | ||
const storedState = context.cookies.get("github_oauth_state")?.value ?? null; | ||
if (!code || !state || !storedState || state !== storedState) { | ||
return new Response(null, { | ||
status: 400 | ||
}); | ||
} | ||
|
||
try { | ||
const tokens = await github.validateAuthorizationCode(code); | ||
const githubUserResponse = await fetch("https://api.github.com/user", { | ||
headers: { | ||
Authorization: `Bearer ${tokens.accessToken}` | ||
} | ||
}); | ||
const githubUser: GitHubUser = await githubUserResponse.json(); | ||
const existingUser = await db.table("user").where("github_id", "=", githubUser.id).get(); | ||
|
||
if (existingUser) { | ||
const session = await lucia.createSession(existingUser.id, {}); | ||
const sessionCookie = lucia.createSessionCookie(session.id); | ||
context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); | ||
return context.redirect("/"); | ||
} | ||
|
||
const userId = generateId(15); | ||
await db.table("user").insert({ | ||
id: userId, | ||
github_id: githubUser.id, | ||
username: githubUser.login | ||
}); | ||
const session = await lucia.createSession(userId, {}); | ||
const sessionCookie = lucia.createSessionCookie(session.id); | ||
context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); | ||
return context.redirect("/"); | ||
} catch (e) { | ||
// the specific error message depends on the provider | ||
if (e instanceof OAuth2RequestError) { | ||
// invalid code | ||
return new Response(null, { | ||
status: 400 | ||
}); | ||
} | ||
return new Response(null, { | ||
status: 500 | ||
}); | ||
} | ||
} | ||
|
||
interface GitHubUser { | ||
id: string; | ||
login: string; | ||
} | ||
``` | ||
|
||
## Validate requests | ||
|
||
You can validate requests by checking `locals.user`. The field `user.username` is available since we defined the `getUserAttributes()` option. You can protect pages, such as `/`, by redirecting unauthenticated users to the login page. | ||
|
||
```ts | ||
const user = Astro.locals.user; | ||
if (!user) { | ||
return Astro.redirect("/login"); | ||
} | ||
|
||
const username = user.username; | ||
``` | ||
|
||
## Sign out | ||
|
||
Sign out users by invalidating their session with `Lucia.invalidateSession()`. Make sure to remove their session cookie by setting a blank session cookie created with `Lucia.createBlankSessionCookie()`. | ||
|
||
```ts | ||
import { lucia } from "../../auth"; | ||
import type { APIContext } from "astro"; | ||
|
||
export async function POST(context: APIContext): Promise<Response> { | ||
if (!context.locals.session) { | ||
return new Response(null, { | ||
status: 401 | ||
}); | ||
} | ||
|
||
await lucia.invalidateSession(context.locals.session.id); | ||
|
||
const sessionCookie = lucia.createBlankSessionCookie(); | ||
context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); | ||
|
||
return Astro.redirect("/login"); | ||
} | ||
``` | ||
|
||
```html | ||
<form method="post" action="/api/logout"> | ||
<button>Sign out</button> | ||
</form> | ||
``` |
Oops, something went wrong.