diff --git a/docs/pages/tutorials/github-oauth/astro.md b/docs/pages/tutorials/github-oauth/astro.md index 13e130a7f..c65a78b9a 100644 --- a/docs/pages/tutorials/github-oauth/astro.md +++ b/docs/pages/tutorials/github-oauth/astro.md @@ -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 +``` + +## 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 + + + +

Sign in

+ Sign in with GitHub + + +``` + +## 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 { + 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 { + 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 { + 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 +
+ +
+``` diff --git a/docs/pages/tutorials/github-oauth/nextjs-app.md b/docs/pages/tutorials/github-oauth/nextjs-app.md index 94199cef4..bf408b017 100644 --- a/docs/pages/tutorials/github-oauth/nextjs-app.md +++ b/docs/pages/tutorials/github-oauth/nextjs-app.md @@ -1,3 +1,291 @@ --- title: "GitHub OAuth in Next.js App router" --- + +# Tutorial: GitHub OAuth in Next.js App router + +Before starting, make sure you've setup your database and middleware as described in the [Getting started](/getting-started/nextjs-app) page. + +An [example project](https://github.com/lucia-auth/examples/tree/v3/nextjs-app/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/nextjs-app/github-oauth). + +``` +npx degit https://github.com/lucia-auth/examples/tree/v3/nextjs-app/github-oauth +``` + +## 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:3000/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: { + expires: false, + attributes: { + secure: process.env.NODE_ENV === "production" + } + }, + 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(process.env.GITHUB_CLIENT_ID!, process.env.GITHUB_CLIENT_SECRET!); +``` + +## Sign in page + +Create `app/login/page.tsx` and add a basic sign in button, which should be a link to `/login/github`. + +```tsx +// app/login/page.tsx +export default async function Page() { + return ( + <> +

Sign in

+ Sign in with GitHub + + ); +} +``` + +## Create authorization URL + +Create an API route in `app/login/github/route.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 +// app/login/github/route.ts +import { generateState } from "arctic"; +import { github } from "../../../lib/auth"; +import { cookies } from "next/headers"; + +export async function GET(): Promise { + const state = generateState(); + const url = await github.createAuthorizationURL(state); + + cookies().set("github_oauth_state", state, { + path: "/", + secure: process.env.NODE_ENV === "production", + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax" + }); + + return Response.redirect(url); +} +``` + +## Validate callback + +Create an API route in `app/login/github/callback/route.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 +// app/login/github/callback/route.ts +import { github, lucia } from "@/lib/auth"; +import { cookies } from "next/headers"; +import { OAuth2RequestError } from "arctic"; +import { generateId } from "lucia"; + +export async function GET(request: Request): Promise { + const url = new URL(request.url); + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + const storedState = 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); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + return new Response(null, { + status: 302, + headers: { + Location: "/" + } + }); + } + + 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); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + return new Response(null, { + status: 302, + headers: { + Location: "/" + } + }); + } 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 + +Create `validateRequest()`. This will check for the session cookie, validates it, and sets a new cookie if necessary. Make sure to catch errors when setting cookies and wrap the function with `cache()` to prevent unnecessary database calls. To learn more, see the [Validating requests](/basics/validate-session-cookies/nextjs-app) page. + +CSRF protection should be implemented but Next.js handles it when using form actions (but not for API routes). + +```ts +import { cookies } from "next/headers"; +import { cache } from "react"; + +import type { Session, User } from "lucia"; + +export const lucia = new Lucia(); + +export const validateRequest = cache( + async (): Promise<{ user: User; session: Session } | { user: null; session: null }> => { + const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null; + if (!sessionId) { + return { + user: null, + session: null + }; + } + + const result = await lucia.validateSession(sessionId); + // next.js throws when you attempt to set cookie when rendering page + try { + if (result.session && result.session.fresh) { + const sessionCookie = lucia.createSessionCookie(result.session.id); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + if (!result.session) { + const sessionCookie = lucia.createBlankSessionCookie(); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + } catch {} + return result; + } +); +``` + +This function can then be used in server components and form actions to get the current session and user. + +```tsx +import { redirect } from "next/navigation"; +import { validateRequest } from "@/lib/auth"; + +export default async function Page() { + const { user } = await validateRequest(); + if (!user) { + return redirect("/login"); + } + return

Hi, {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()`. + +```tsx +import { lucia, validateRequest } from "@/lib/auth"; +import { redirect } from "next/navigation"; +import { cookies } from "next/headers"; + +export default async function Page() { + return ( +
+ +
+ ); +} + +async function logout(): Promise { + "use server"; + const { session } = await validateRequest(); + if (!session) { + return { + error: "Unauthorized" + }; + } + + await lucia.invalidateSession(session.id); + + const sessionCookie = lucia.createBlankSessionCookie(); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + return redirect("/login"); +} +``` diff --git a/docs/pages/tutorials/github-oauth/nextjs-pages.md b/docs/pages/tutorials/github-oauth/nextjs-pages.md index f751ef546..f16f813eb 100644 --- a/docs/pages/tutorials/github-oauth/nextjs-pages.md +++ b/docs/pages/tutorials/github-oauth/nextjs-pages.md @@ -1,3 +1,322 @@ --- title: "GitHub OAuth in Next.js Pages router" --- + +# Tutorial: GitHub OAuth in Next.js Pages router + +Before starting, make sure you've setup your database and middleware as described in the [Getting started](/getting-started/nextjs-pages) page. + +An [example project](https://github.com/lucia-auth/examples/tree/v3/nextjs-pages/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/nextjs-pages/github-oauth). + +``` +npx degit https://github.com/lucia-auth/examples/tree/v3/nextjs-pages/github-oauth +``` + +## 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:3000/api/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: process.env.NODE_ENV === "production" + } + }, + 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(process.env.GITHUB_CLIENT_ID!, process.env.GITHUB_CLIENT_SECRET!); +``` + +## Sign in page + +Create `pages/login.tsx` and add a basic sign in button, which should be a link to `/login/github`. + +```tsx +// pages/login.tsx +export default function Page() { + return ( + <> +

Sign in

+ Sign in with GitHub + + ); +} +``` + +## Create authorization URL + +Create an API route in `pages/api/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/api/login/github/index.ts +import { github } from "@/lib/auth"; +import { generateState } from "arctic"; +import { serializeCookie } from "oslo/cookie"; + +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "GET") { + res.status(404).end(); + return; + } + const state = generateState(); + const url = await github.createAuthorizationURL(state); + res + .appendHeader( + "Set-Cookie", + serializeCookie("github_oauth_state", state, { + path: "/", + secure: process.env.NODE_ENV === "production", + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax" + }) + ) + .redirect(url.toString()); +} +``` + +## Validate callback + +Create an API route in `pages/api/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/api/login/github/callback.ts +import { github, lucia } from "@/lib/auth"; +import { OAuth2RequestError } from "arctic"; +import { generateId } from "lucia"; + +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "GET") { + res.status(404).end(); + return; + } + const code = req.query.code?.toString() ?? null; + const state = req.query.state?.toString() ?? null; + const storedState = req.cookies.github_oauth_state ?? null; + if (!code || !state || !storedState || state !== storedState) { + console.log(code, state, storedState); + res.status(400).end(); + return; + } + 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, {}); + return res + .appendHeader("Set-Cookie", lucia.createSessionCookie(session.id).serialize()) + .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, {}); + return res + .appendHeader("Set-Cookie", lucia.createSessionCookie(session.id).serialize()) + .redirect("/"); + } catch (e) { + // the specific error message depends on the provider + if (e instanceof OAuth2RequestError) { + // invalid code + return new Response(null, { + status: 400 + }); + } + res.status(500).end(); + return; + } +} + +interface GitHubUser { + id: string; + login: string; +} +``` + +## Validate requests + +Create `validateRequest()`. This will check for the session cookie, validates it, and sets a new cookie if necessary. To learn more, see the [Validating requests](/basics/validate-session-cookies/nextjs-pages) page. + +CSRF protection should be implemented and you should already have a middleware for it. + +```ts +import type { Session, User } from "lucia"; +import type { IncomingMessage, ServerResponse } from "http"; + +export const lucia = new Lucia(); + +export async function validateRequest( + req: IncomingMessage, + res: ServerResponse +): Promise<{ user: User; session: Session } | { user: null; session: null }> { + const sessionId = lucia.readSessionCookie(req.headers.cookie ?? ""); + if (!sessionId) { + return { + user: null, + session: null + }; + } + const result = await lucia.validateSession(sessionId); + if (result.session && result.session.fresh) { + res.appendHeader("Set-Cookie", lucia.createSessionCookie(result.session.id).serialize()); + } + if (!result.session) { + res.appendHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()); + } + return result; +} +``` + +This function can then be used in both `getServerSideProps()` and API routes. + +```tsx +import { validateRequest } from "@/lib/auth"; + +import type { + GetServerSidePropsContext, + GetServerSidePropsResult, + InferGetServerSidePropsType +} from "next"; +import type { User } from "lucia"; + +export async function getServerSideProps(context: GetServerSidePropsContext): Promise< + GetServerSidePropsResult<{ + user: User; + }> +> { + const { user } = await validateRequest(context.req, context.res); + if (!user) { + return { + redirect: { + permanent: false, + destination: "/login" + } + }; + } + return { + props: { + user + } + }; +} + +export default function Page({ user }: InferGetServerSidePropsType) { + return

Hi, {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 +// pages/api/logout.ts +import { lucia, validateRequest } from "@/lib/auth"; + +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + res.status(404).end(); + return; + } + const { session } = await validateRequest(req, res); + if (!session) { + res.status(401).end(); + return; + } + await lucia.invalidateSession(session.id); + res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()).status(200).end(); +} +``` + +```tsx +import { useRouter } from "next/router"; + +import type { FormEvent } from "react"; + +export default function Page({ user }: InferGetServerSidePropsType) { + const router = useRouter(); + + async function onSubmit(e: FormEvent) { + e.preventDefault(); + const formElement = e.target as HTMLFormElement; + await fetch(formElement.action, { + method: formElement.method + }); + router.push("/login"); + } + + return ( +
+ +
+ ); +} +``` diff --git a/docs/pages/tutorials/github-oauth/sveltekit.md b/docs/pages/tutorials/github-oauth/sveltekit.md index f1dee1e15..74969df4b 100644 --- a/docs/pages/tutorials/github-oauth/sveltekit.md +++ b/docs/pages/tutorials/github-oauth/sveltekit.md @@ -1,3 +1,301 @@ --- title: "GitHub OAuth in SvelteKit" --- + +# Tutorial: GitHub OAuth in SvelteKit + +Before starting, make sure you've setup your database and middleware as described in the [Getting started](/getting-started/sveltekit) page. + +An [example project](https://github.com/lucia-auth/examples/tree/v3/sveltekit/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/sveltekit/github-oauth). + +``` +npx degit https://github.com/lucia-auth/examples/tree/v3/sveltekit/github-oauth +``` + +## 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:5173/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"; +import { dev } from "$app/environment"; + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: dev + } + }, + 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 `routes/login/+page.svelte` and add a basic sign in button, which should be a link to `/login/github`. + +```svelte + +

Sign in

+Sign in with GitHub +``` + +## Create authorization URL + +Create an API route in `routes/login/github/+server.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 +// routes/login/github/+server.ts +import { github } from "$lib/server/auth"; +import { generateState } from "arctic"; +import { redirect } from "@sveltejs/kit"; + +import type { RequestEvent } from "@sveltejs/kit"; + +export async function GET(event: RequestEvent): Promise { + const state = generateState(); + const url = await github.createAuthorizationURL(state); + + event.cookies.set("github_oauth_state", state, { + path: "/", + secure: import.meta.env.PROD, + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax" + }); + + return redirect(302, url.toString()); +} +``` + +## Validate callback + +Create an API route in `routes/login/github/callback/+server.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 +// routes/login/github/callback/+server.ts +import { github, lucia } from "$lib/server/auth"; +import { OAuth2RequestError } from "arctic"; +import { generateId } from "lucia"; + +import type { RequestEvent } from "@sveltejs/kit"; + +export async function GET(event: RequestEvent): Promise { + const code = event.url.searchParams.get("code"); + const state = event.url.searchParams.get("state"); + const storedState = event.cookies.get("github_oauth_state") ?? 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); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }); + } else { + 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); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }); + } + return new Response(null, { + status: 302, + headers: { + Location: "/" + } + }); + } 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 { + 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 +
+ +
+``` + +## 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 +// +page.server.ts +import type { PageServerLoad, Actions } from "./$types"; + +export const load: PageServerLoad = async (event) => { + if (!event.locals.user) throw redirect(302, "/login"); + return { + username: event.locals.user.username + }; +}; +``` + +## Sign out user + +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 +// routes/+page.server.ts +import { lucia } from "$lib/server/auth"; +import { fail, redirect } from "@sveltejs/kit"; + +import type { Actions, PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ locals }) => { + // ... +}; + +export const actions: Actions = { + default: async (event) => { + if (!event.locals.session) { + return fail(401); + } + await auth.invalidateSession(event.locals.session.id); + const sessionCookie = lucia.createBlankSessionCookie(); + context.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes + }); + return redirect(302, "/login"); + } +}; +``` + +```svelte + + + +
+ +
+``` diff --git a/docs/pages/tutorials/username-and-password/nextjs-pages.md b/docs/pages/tutorials/username-and-password/nextjs-pages.md index 6da205e2d..ddbd3fef0 100644 --- a/docs/pages/tutorials/username-and-password/nextjs-pages.md +++ b/docs/pages/tutorials/username-and-password/nextjs-pages.md @@ -326,6 +326,7 @@ export default function Page({ user }: InferGetServerSidePropsType