Compare commits

...

19 Commits

Author SHA1 Message Date
Balázs Orbán
f2a07932b9 chore(release): bump package version(s) [skip ci] 2022-12-03 12:38:32 +01:00
Balázs Orbán
25c7ce1d2b chore: revert sync-example action 2022-12-03 12:31:22 +01:00
Vedant
227a233bd8 chore(adapters): update firebase sdk version in firebase adapter (#5902)
* Update firebase SDK version

* Update pnpm-lock.yaml

* add `--debug`

* move flag

* add `FIREBASE_TOKEN`

* chore: upgrade `firebase-tools`

* revert peer dep change

* remove --debug flag

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-12-03 11:29:09 +00:00
Vedant
cf9f133aa3 fix(adapters): tsconfig for firebase and pouchdb (#5945)
* Update tsconfig.json

* Update tsconfig.json
2022-12-03 09:48:55 +01:00
Arnaud Zheng
2301c1be44 chore(types): fix typo in comment (#5815)
Co-authored-by: Nico Domino <yo@ndo.dev>
2022-12-02 16:39:28 +01:00
jintak0401
6e408e24bf fix(provider): modify response.name to response.nickname (Naver) (#5915)
fix(provider): modify response.name to response.nickname (Naver Provider)

Co-authored-by: Thang Vu <hi@thvu.dev>
2022-12-01 08:12:16 +07:00
Jason Brady
f277989c69 feat(core): make pkce and state maxAge configurable on the cookies (#4719)
* feat(cookies): make pkce and state maxAge configurable on the cookies (#4660)

* added tests for pkce and state handlers
2022-12-01 08:02:42 +07:00
Nico Domino
6146e93288 chore(docs): add new provider styling notes to CONTRIBUTING.md (#5900) 2022-11-30 15:44:50 +01:00
Nico Domino
1ff565da6c chore(docs): update oauth.md styling docs (#5899)
* chore(docs): update oauth.md styling docs

* chore: add note about styling object

* chore: typo

* chore: filename typos
2022-11-30 15:43:55 +01:00
Nico Domino
41f75cf870 chore(actions): version bump all deps (#5903) 2022-11-28 11:10:15 +00:00
Justin W Hall
dd591ed8d0 chore(docs): fix path Strava provider file (#5853)
Co-authored-by: Nico Domino <yo@ndo.dev>
2022-11-27 14:04:17 +01:00
koolskateguy89
297bc2317f chore(docs): fix spelling in docs (#5867)
Co-authored-by: Nico Domino <yo@ndo.dev>
2022-11-27 13:44:35 +01:00
Tormod Flesjø
b170138e70 chore: update mongodb.md with typescript filetypes (#5889) 2022-11-27 13:11:54 +01:00
Balázs Orbán
a307079e0f fix(ts): improve unstable_getServerSession return type (#5792)
Co-authored-by: Thang Vu <hi@thvu.dev>
2022-11-24 15:13:05 +01:00
Jacob Penny
d52b7a6b7d chore(adapters): fix typo on Firebase Adapter (#5813) 2022-11-24 14:31:43 +01:00
Jake Mulley
30b69a07eb chore: fix import statement for next-auth/providers/email (#5860) 2022-11-21 12:07:09 +01:00
Balázs Orbán
0d1757814f fix(next): improve dev environment variable handling (#5763)
* fix(next): HIDE `NEXTAUTH_URL` warning locally

* refactor: move out `process.env` from core

* fix tests

* simplify

* swap
2022-11-20 09:08:10 +00:00
Thang Vu
068f9b50b8 chore(release): bump package version(s) [skip ci] 2022-11-19 21:29:28 +07:00
Martin Sonnberger
dac490b7a1 feat(adapters): Add Supabase adapter (#5050)
* Add Supabase adapter

* Add Supabase adapter

* Add schema setup to docs

* supabase config changes

* chore: update to supabase-js v2.

* chore: migrate to next_auth schema.

* feat: add supabase examples.

* chore: update docs.

* chore: add telemetry.

* fix: resolve issues after merge.

* chore: extend session type.

* typo

* chore: remove unnecessary grants.

* fix: schema constraints.

Co-authored-by: thorwebdev <thor@supabase.io>
Co-authored-by: Thor 雷神 Schaeff <5748289+thorwebdev@users.noreply.github.com>
Co-authored-by: Thang Vu <hi@thvu.dev>
2022-11-19 20:56:20 +07:00
58 changed files with 4499 additions and 2347 deletions

View File

@@ -31,6 +31,7 @@ body:
- "@next-auth/pouchdb-adapter"
- "@next-auth/prisma-adapter"
- "@next-auth/sequelize-adapter"
- "@next-auth/supabase-adapter"
- "@next-auth/typeorm-legacy-adapter"
- "@next-auth/upstash-redis-adapter"
- "@next-auth/xata-adapter"

View File

@@ -30,6 +30,9 @@ prisma:
sequelize:
- "@next-auth/sequelize-adapter"
supabase:
- "@next-auth/supabase-adapter"
typeorm-legacy:
- "@next-auth/typeorm-legacy-adapter"

View File

@@ -42,6 +42,9 @@ prisma:
sequelize:
- packages/adapter-sequelize/**
supabase:
- packages/adapter-supabase/**
typeorm-legacy:
- packages/adapter-typeorm-legacy/**

View File

@@ -18,10 +18,10 @@ jobs:
language: ["javascript"]
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

View File

@@ -11,7 +11,7 @@ jobs:
name: Triage
runs-on: ubuntu-latest
steps:
- uses: github/issue-labeler@v2.4.1
- uses: github/issue-labeler@v2.5
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: ".github/issue-labeler.yml"

View File

@@ -10,7 +10,7 @@ jobs:
name: Triage
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v3
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: ".github/pr-labeler.yml"

View File

@@ -15,11 +15,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Init
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Install pnpm
uses: pnpm/action-setup@v2.2.1
uses: pnpm/action-setup@v2.2.4
with:
version: 7.5.1
- name: Setup Node
@@ -49,11 +49,11 @@ jobs:
environment: Production
steps:
- name: Init
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v2.2.1
uses: pnpm/action-setup@v2.2.4
with:
version: 7.5.1
- name: Setup Node
@@ -81,9 +81,9 @@ jobs:
environment: Preview
steps:
- name: Init
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.2.1
uses: pnpm/action-setup@v2.2.4
with:
version: 7.5.1
- name: Setup Node
@@ -106,7 +106,7 @@ jobs:
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN_PKG }}
- name: Comment version on PR
uses: NejcZdovc/comment-pr@v1
uses: NejcZdovc/comment-pr@v2
with:
message:
"🎉 Experimental release [published 📦️ on npm](https://npmjs.com/package/next-auth/v/${{ env.VERSION }})!\n \

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Run GitHub File Sync
# Can update to v1 when https://github.com/BetaHuhn/repo-file-sync-action/issues/168 is resolved
uses: BetaHuhn/repo-file-sync-action@v1.16.5

1
.gitignore vendored
View File

@@ -65,6 +65,7 @@ dev.db*
packages/adapter-prisma/prisma/dev.db
packages/adapter-prisma/prisma/migrations
db.sqlite
packages/adapter-supabase/supabase/.branches
# Tests
coverage

View File

@@ -87,6 +87,7 @@ If you think your custom provider might be useful to others, we encourage you to
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/packages/next-auth/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
3. Add provider logo svgs, like `google-dark.svg` (dark mode) and `google.svg` (light mode) to the `/packages/next-auth/provider-logos/` directory. Don't forget to set the provider's styling options in the `provider.style` config object.
That's it! 🎉 Others will be able to discover this provider much more easily now!

View File

@@ -48,4 +48,11 @@ EMAIL_FROM=user@gmail.com
DATABASE_URL=
WIKIMEDIA_ID=
WIKIMEDIA_SECRET=
WIKIMEDIA_SECRET=
# Supabase Example Configuration
# Supabase Example Configuration
# NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
# SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU
# SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs

View File

@@ -90,6 +90,12 @@ export default function Header() {
<li className={styles.navItem}>
<Link href="/middleware-protected">Middleware protected</Link>
</li>
<li className={styles.navItem}>
<Link href="/supabase-client-rls">Supabase RLS</Link>
</li>
<li className={styles.navItem}>
<Link href="/supabase-ssr">Supabase RLS(SSR)</Link>
</li>
</ul>
</nav>
</header>

View File

@@ -16,9 +16,12 @@
"dependencies": {
"@next-auth/fauna-adapter": "workspace:*",
"@next-auth/prisma-adapter": "workspace:*",
"@next-auth/supabase-adapter": "workspace:*",
"@next-auth/typeorm-legacy-adapter": "workspace:*",
"@prisma/client": "^3",
"@supabase/supabase-js": "^2.0.5",
"faunadb": "^4",
"jsonwebtoken": "^8.5.1",
"next": "13.0.2",
"next-auth": "workspace:*",
"nodemailer": "^6",
@@ -26,6 +29,7 @@
"react-dom": "^18"
},
"devDependencies": {
"@types/jsonwebtoken": "^8.5.5",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"fake-smtp-server": "^0.8.0",

View File

@@ -1,5 +1,6 @@
import NextAuth from "next-auth"
import type { NextAuthOptions } from "next-auth"
import jwt from "jsonwebtoken"
// Providers
import Apple from "next-auth/providers/apple"
@@ -18,7 +19,6 @@ import Freshbooks from "next-auth/providers/freshbooks"
import GitHub from "next-auth/providers/github"
import Gitlab from "next-auth/providers/gitlab"
import Google from "next-auth/providers/google"
import Hubspot from "next-auth/providers/hubspot"
import IDS4 from "next-auth/providers/identity-server4"
import Instagram from "next-auth/providers/instagram"
import Keycloak from "next-auth/providers/keycloak"
@@ -30,43 +30,76 @@ import Osu from "next-auth/providers/osu"
import Patreon from "next-auth/providers/patreon"
import Slack from "next-auth/providers/slack"
import Spotify from "next-auth/providers/spotify"
import Todoist from "next-auth/providers/todoist"
import Trakt from "next-auth/providers/trakt"
import Twitch from "next-auth/providers/twitch"
import Twitter, { TwitterLegacy } from "next-auth/providers/twitter"
import Vk from "next-auth/providers/vk"
import Wikimedia from "next-auth/providers/wikimedia"
import WorkOS from "next-auth/providers/workos"
import Zitadel from "next-auth/providers/zitadel"
// Adapters
import { PrismaClient } from "@prisma/client"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { Client as FaunaClient } from "faunadb"
import { FaunaAdapter } from "@next-auth/fauna-adapter"
import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
import { SupabaseAdapter } from "@next-auth/supabase-adapter"
// // Prisma
// import { PrismaClient } from "@prisma/client"
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
// const client = globalThis.prisma || new PrismaClient()
// if (process.env.NODE_ENV !== "production") globalThis.prisma = client
// const adapter = PrismaAdapter(client)
// // Fauna
// import { Client as FaunaClient } from "faunadb"
// import { FaunaAdapter } from "@next-auth/fauna-adapter"
// const opts = { secret: process.env.FAUNA_SECRET, domain: process.env.FAUNA_DOMAIN }
// const client = globalThis.fauna || new FaunaClient(opts)
// if (process.env.NODE_ENV !== "production") globalThis.fauna = client
// const adapter = FaunaAdapter(client)
// // TypeORM
// import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
// const adapter = TypeORMLegacyAdapter({
// type: "sqlite",
// name: "next-auth-test-memory",
// database: "./typeorm/dev.db",
// synchronize: true,
// })
// Add an adapter you want to test here.
const adapters = {
prisma() {
const client = globalThis.prisma || new PrismaClient()
if (process.env.NODE_ENV !== "production") globalThis.prisma = client
return PrismaAdapter(client)
},
typeorm() {
return TypeORMLegacyAdapter({
type: "sqlite",
name: "next-auth-test-memory",
database: "./typeorm/dev.db",
synchronize: true,
})
},
fauna() {
const client =
globalThis.fauna ||
new FaunaClient({
secret: process.env.FAUNA_SECRET,
domain: process.env.FAUNA_DOMAIN,
})
if (process.env.NODE_ENV !== "production") global.fauna = client
return FaunaAdapter(client)
},
supabase() {
return SupabaseAdapter({
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
})
},
noop() {
return undefined
},
}
export const authOptions: NextAuthOptions = {
// adapter,
adapter: adapters.noop(),
callbacks: {
async session({ session, user }) {
// NOTE: this is needed when using Supabase with RLS. Otherwise this callback can be removed.
const signingSecret = process.env.SUPABASE_JWT_SECRET
if (signingSecret) {
const payload = {
aud: "authenticated",
exp: Math.floor(new Date(session.expires).getTime() / 1000),
sub: user.id,
email: user.email,
role: "authenticated",
}
session.supabaseAccessToken = jwt.sign(payload, signingSecret)
}
return session
},
},
debug: process.env.NODE_ENV !== "production",
theme: {
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
@@ -94,7 +127,6 @@ export const authOptions: NextAuthOptions = {
GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),
Gitlab({ clientId: process.env.GITLAB_ID, clientSecret: process.env.GITLAB_SECRET }),
Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET }),
Hubspot({ clientId: process.env.HUBSPOT_ID, clientSecret: process.env.HUBSPOT_SECRET }),
IDS4({ clientId: process.env.IDS4_ID, clientSecret: process.env.IDS4_SECRET, issuer: process.env.IDS4_ISSUER }),
Instagram({ clientId: process.env.INSTAGRAM_ID, clientSecret: process.env.INSTAGRAM_SECRET }),
Keycloak({ clientId: process.env.KEYCLOAK_ID, clientSecret: process.env.KEYCLOAK_SECRET, issuer: process.env.KEYCLOAK_ISSUER }),
@@ -106,7 +138,6 @@ export const authOptions: NextAuthOptions = {
Patreon({ clientId: process.env.PATREON_ID, clientSecret: process.env.PATREON_SECRET }),
Slack({ clientId: process.env.SLACK_ID, clientSecret: process.env.SLACK_SECRET }),
Spotify({ clientId: process.env.SPOTIFY_ID, clientSecret: process.env.SPOTIFY_SECRET }),
Todoist({ clientId: process.env.TODOIST_ID, clientSecret: process.env.TODOIST_SECRET }),
Trakt({ clientId: process.env.TRAKT_ID, clientSecret: process.env.TRAKT_SECRET }),
Twitch({ clientId: process.env.TWITCH_ID, clientSecret: process.env.TWITCH_SECRET }),
Twitter({ version: "2.0", clientId: process.env.TWITTER_ID, clientSecret: process.env.TWITTER_SECRET }),
@@ -114,7 +145,6 @@ export const authOptions: NextAuthOptions = {
Vk({ clientId: process.env.VK_ID, clientSecret: process.env.VK_SECRET }),
Wikimedia({ clientId: process.env.WIKIMEDIA_ID, clientSecret: process.env.WIKIMEDIA_SECRET }),
WorkOS({ clientId: process.env.WORKOS_ID, clientSecret: process.env.WORKOS_SECRET }),
Zitadel({ issuer: process.env.ZITADEL_ISSUER, clientId: process.env.ZITADEL_CLIENT_ID, clientSecret: process.env.ZITADEL_CLIENT_SECRET }),
],
}

View File

@@ -0,0 +1,30 @@
// This is an example of how to query data from Supabase with RLS.
// Learn more about Row Levele Security (RLS): https://supabase.com/docs/guides/auth/row-level-security
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "../auth/[...nextauth]"
import { createClient } from "@supabase/supabase-js"
export default async (req, res) => {
const session = await unstable_getServerSession(req, res, authOptions)
if (!session)
return res.send(JSON.stringify({ error: "No session!" }, null, 2))
const { supabaseAccessToken } = session
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
global: {
headers: {
Authorization: `Bearer ${supabaseAccessToken}`,
},
},
}
)
// Now you can query with RLS enabled.
const { data, error } = await supabase.from("users").select("*")
res.send(JSON.stringify({ supabaseAccessToken, data, error }, null, 2))
}

View File

@@ -0,0 +1,49 @@
import Layout from "../components/layout"
import { useState, useEffect } from "react"
import { useSession } from "next-auth/react"
import { createClient } from "@supabase/supabase-js"
export default function Page() {
const { data: session } = useSession()
const [data, setData] = useState(null)
useEffect(() => {
if (session) {
console.log(session)
// User is logged in, let's fetch their data.
const { supabaseAccessToken } = session
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
global: {
headers: { Authorization: `Bearer ${supabaseAccessToken}` },
},
}
)
// Fetch data with RLS enabled.
supabase
.from("users")
.select("*")
.then(({ data }) => setData(data))
}
}, [session])
return (
<Layout>
<h1>Fetch Data from Supabase with RLS</h1>
<h2>Client-side data fetching with RLS:</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
<h2>API Example</h2>
<p>
You can also use Supabase in API routes. See the code in the
`/pages/api/examples/supabase-rls.js` file.
</p>
<p>
<em>You must be signed in to see responses.</em>
</p>
<p>/api/examples/supabase-rls</p>
<iframe src="/api/examples/supabase-rls" />
</Layout>
)
}

View File

@@ -0,0 +1,68 @@
// This is an example of how to protect content using server rendering
// and fetching data from Supabase with RLS enabled.
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "./api/auth/[...nextauth]"
import { createClient } from "@supabase/supabase-js"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"
export default function Page({ data, session }) {
// If no session exists, display access denied message
if (!session) {
return (
<Layout>
<AccessDenied />
</Layout>
)
}
// If session exists, display content
return (
<Layout>
<h1>Protected Page</h1>
<p>Data fetched during SSR from Supabase with RSL enabled:</p>
<pre>{JSON.stringify(data, null, 2)}</pre>
</Layout>
)
}
export async function getServerSideProps(context) {
const session = await unstable_getServerSession(
context.req,
context.res,
authOptions
)
if (!session)
return {
props: {
session,
data: null,
error: "No session",
},
}
const { supabaseAccessToken } = session
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
global: {
headers: {
Authorization: `Bearer ${supabaseAccessToken}`,
},
},
}
)
// Now you can query with RLS enabled.
const { data, error } = await supabase.from("users").select("*")
return {
props: {
session,
data,
error,
},
}
}

View File

@@ -6,6 +6,8 @@ declare module "next-auth" {
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
interface Session {
// A JWT which can be used as Authorization header with supabase-js for RLS.
supabaseAccessToken?: string
user: {
/** The user's postal address. */
address: string

View File

@@ -15,9 +15,9 @@ The MongoDB adapter does not handle connections automatically, so you will have
npm install next-auth @next-auth/mongodb-adapter mongodb
```
2. Add `lib/mongodb.js`
2. Add `lib/mongodb.ts`
```js
```ts
// This approach is taken from https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
import { MongoClient } from 'mongodb'

View File

@@ -21,6 +21,7 @@ We have a list of official adapters that are distributed as their own packages u
- [`neo4j`](./neo4j)
- [`typeorm-legacy`](./typeorm)
- [`sequelize`](./sequelize)
- [`supabase`](./supabase)
- [`dgraph`](./dgraph)
- [`upstash-redis`](./upstash-redis)

View File

@@ -0,0 +1,309 @@
---
id: supabase
title: Supabase
---
# Supabase
This is the Supabase Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
:::note
This adapter is developed by the community and not officially maintained or supported by Supabase. It uses the Supabase Database to store user and session data in a separate `next_auth` schema. It is a standalone Auth server that does not interface with Supabase Auth and therefore provides a different feature set.
If youre looking for an officially maintained Auth server with additional features like [built-in email server](https://supabase.com/docs/guides/auth/auth-email#configure-email-settings?utm_source=next-auth-docs&medium=referral&campaign=next-auth), [phone auth](https://supabase.com/docs/guides/auth/auth-twilio?utm_source=next-auth-docs&medium=referral&campaign=next-auth), and [Multi Factor Authentication (MFA / 2FA)](https://supabase.com/contact/mfa?utm_source=next-auth-docs&medium=referral&campaign=next-auth), please use [Supabase Auth](https://supabase.com/auth) with the [Auth Helpers for Next.js](https://supabase.com/docs/guides/auth/auth-helpers/nextjs?utm_source=next-auth-docs&medium=referral&campaign=next-auth).
:::
## Getting Started
1. Install `@supabase/supabase-js`, `next-auth` and `@next-auth/supabase-adapter`.
```bash npm2yarn2pnpm
npm install @supabase/supabase-js next-auth @next-auth/supabase-adapter
```
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
```js title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import { SupabaseAdapter } from "@next-auth/supabase-adapter"
// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
export default NextAuth({
// https://next-auth.js.org/configuration/providers
providers: [...],
adapter: SupabaseAdapter({
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
}),
// ...
})
```
## Setup
### Create the `next_auth` schema in Supabase
Setup your database as described in our main [schema](/adapters/models), by copying the SQL schema below in the Supabase [SQL Editor](https://app.supabase.com/project/_/sql).
Alternatively you can select the NextAuth Quickstart card on the [SQL Editor page](https://app.supabase.com/project/_/sql), or [create a migration with the Supabase CLI](https://supabase.com/docs/guides/cli/local-development#database-migrations?utm_source=next-auth-docs&medium=referral&campaign=next-auth).
```sql
--
-- Name: next_auth; Type: SCHEMA;
--
CREATE SCHEMA next_auth;
GRANT USAGE ON SCHEMA next_auth TO service_role;
GRANT ALL ON SCHEMA next_auth TO postgres;
--
-- Create users table
--
CREATE TABLE IF NOT EXISTS next_auth.users
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
name text,
email text,
"emailVerified" timestamp with time zone,
image text,
CONSTRAINT users_pkey PRIMARY KEY (id),
CONSTRAINT email_unique UNIQUE (email)
);
GRANT ALL ON TABLE next_auth.users TO postgres;
GRANT ALL ON TABLE next_auth.users TO service_role;
--- uid() function to be used in RLS policies
CREATE FUNCTION next_auth.uid() RETURNS uuid
LANGUAGE sql STABLE
AS $$
select
coalesce(
nullif(current_setting('request.jwt.claim.sub', true), ''),
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')
)::uuid
$$;
--
-- Create sessions table
--
CREATE TABLE IF NOT EXISTS next_auth.sessions
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
expires timestamp with time zone NOT NULL,
"sessionToken" text NOT NULL,
"userId" uuid,
CONSTRAINT sessions_pkey PRIMARY KEY (id),
CONSTRAINT sessionToken_unique UNIQUE ("sessionToken"),
CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId")
REFERENCES next_auth.users (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE
);
GRANT ALL ON TABLE next_auth.sessions TO postgres;
GRANT ALL ON TABLE next_auth.sessions TO service_role;
--
-- Create accounts table
--
CREATE TABLE IF NOT EXISTS next_auth.accounts
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
type text NOT NULL,
provider text NOT NULL,
"providerAccountId" text NOT NULL,
refresh_token text,
access_token text,
expires_at bigint,
token_type text,
scope text,
id_token text,
session_state text,
oauth_token_secret text,
oauth_token text,
"userId" uuid,
CONSTRAINT accounts_pkey PRIMARY KEY (id),
CONSTRAINT provider_unique UNIQUE (provider, "providerAccountId"),
CONSTRAINT "accounts_userId_fkey" FOREIGN KEY ("userId")
REFERENCES next_auth.users (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE
);
GRANT ALL ON TABLE next_auth.accounts TO postgres;
GRANT ALL ON TABLE next_auth.accounts TO service_role;
--
-- Create verification_tokens table
--
CREATE TABLE IF NOT EXISTS next_auth.verification_tokens
(
identifier text,
token text,
expires timestamp with time zone NOT NULL,
CONSTRAINT verification_tokens_pkey PRIMARY KEY (token),
CONSTRAINT token_unique UNIQUE (token),
CONSTRAINT token_identifier_unique UNIQUE (token, identifier)
);
GRANT ALL ON TABLE next_auth.verification_tokens TO postgres;
GRANT ALL ON TABLE next_auth.verification_tokens TO service_role;
```
### Expose the `next_auth` schema in Supabase
Expose the `next_auth` schema via the Serverless API in the [API settings](https://app.supabase.com/project/_/settings/api) by adding `next_auth` to the "Exposed schemas" list.
When developing locally add `next_auth` to the `schemas` array in the `config.toml` file in the `supabase` folder that was generated by the [Supabase CLI](https://supabase.com/docs/guides/cli/local-development#initialize-your-project?utm_source=next-auth-docs&medium=referral&campaign=next-auth).
## Enabling Row Level Security (RLS)
Postgres provides a powerful feature called [Row Level Security (RLS)](https://supabase.com/docs/guides/auth/row-level-security?utm_source=next-auth-docs&medium=referral&campaign=next-auth) to limit access to data.
This works by sending a signed JWT to your [Supabase Serverless API](https://supabase.com/docs/guides/api?utm_source=next-auth-docs&medium=referral&campaign=next-auth). There is two steps to make this work with NextAuth:
### 1. Generate the Supabase `access_token` JWT in the session callback
To sign the JWT use the `jsonwebtoken` package:
```bash npm2yarn2pnpm
npm install jsonwebtoken
```
Using the [NexthAuth Session callback](https://next-auth.js.org/configuration/callbacks#session-callback) create the Supabase `access_token` and append it to the `session` object.
To sign the JWT use the Supabase JWT secret which can be found in the [API settings](https://app.supabase.com/project/_/settings/api)
```js title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import { SupabaseAdapter } from "@next-auth/supabase-adapter"
import jwt from "jsonwebtoken"
// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
export default NextAuth({
// https://next-auth.js.org/configuration/providers
providers: [...],
adapter: SupabaseAdapter({
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
}),
callbacks: {
async session({ session, user }) {
const signingSecret = process.env.SUPABASE_JWT_SECRET
if (signingSecret) {
const payload = {
aud: "authenticated",
exp: Math.floor(new Date(session.expires).getTime() / 1000),
sub: user.id,
email: user.email,
role: "authenticated",
}
session.supabaseAccessToken = jwt.sign(payload, signingSecret)
}
return session
},
},
// ...
})
```
### 2. Inject the Supabase `access_token` JWT into the Supabase Client
For example, given the following public schema:
```sql
/**
* USERS
* Note: This table contains user data. Users should only be able to view and update their own data.
*/
create table users (
-- UUID from next_auth.users
id uuid not null primary key,
name text,
email text,
image text,
constraint "users_id_fkey" foreign key ("id")
references next_auth.users (id) match simple
on update no action
on delete cascade -- if user is deleted in NextAuth they will also be deleted in our public table.
);
alter table users enable row level security;
create policy "Can view own user data." on users for select using (next_auth.uid() = id);
create policy "Can update own user data." on users for update using (next_auth.uid() = id);
/**
* This trigger automatically creates a user entry when a new user signs up via NextAuth.
*/
create function public.handle_new_user()
returns trigger as $$
begin
insert into public.users (id, name, email, image)
values (new.id, new.name, new.email, new.image);
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on next_auth.users
for each row execute procedure public.handle_new_user();
```
The `supabaseAccessToken` is now available on the `session` object and can be passed to the supabase-js client. This works in any environment: client-side, server-side (API routes, SSR), as well as in middleware edge functions!
```js
// ...
// Use `useSession()` or `unstable_getServerSession()` to get the NextAuth session.
const { supabaseAccessToken } = session
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
global: {
headers: {
Authorization: `Bearer ${supabaseAccessToken}`,
},
},
}
)
// Now you can query with RLS enabled.
const { data, error } = await supabase.from("users").select("*")
```
## Usage with TypeScript
You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion.
Creating a new supabase client object:
```tsx
import { createClient } from "@supabase/supabase-js"
import { Database } from "../database.types"
const supabase = createClient<Database>()
```
### Extend the session type with the `supabaseAccessToken`
In order to extend the `session` object with the `supabaseAccessToken` we need to extend the `session` interface in a `types/next-auth.d.ts` file:
```ts title="types/next-auth.d.ts"
import NextAuth, { DefaultSession } from "next-auth"
declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
interface Session {
// A JWT which can be used as Authorization header with supabase-js for RLS.
supabaseAccessToken?: string
user: {
/** The user's postal address. */
address: string
} & DefaultSession["user"]
}
}
```

View File

@@ -472,7 +472,8 @@ cookies: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: useSecureCookies
secure: useSecureCookies,
maxAge: 900
}
},
state: {
@@ -482,6 +483,7 @@ cookies: {
sameSite: "lax",
path: "/",
secure: useSecureCookies,
maxAge: 900
},
},
nonce: {

View File

@@ -13,7 +13,7 @@ Adding support for signing in via email in addition to one or more OAuth service
Configuration is similar to other providers, but the options are different:
```js title="pages/api/auth/[...nextauth].js"
import EmailProvider from `next-auth/providers/email`
import EmailProvider from "next-auth/providers/email"
...
providers: [
EmailProvider({

View File

@@ -174,6 +174,10 @@ interface OAuthConfig {
issuer?: string
client?: Partial<ClientMetadata>
allowDangerousEmailAccountLinking?: boolean
/**
* Object containing the settings for the styling of the providers sign-in buttons
*/
style: ProviderStyleType
}
```
@@ -428,7 +432,8 @@ If you think your custom provider might be useful to others, we encourage you to
You only need to add three changes:
1. Add your config: [`src/providers/{provider}.ts`](https://github.com/nextauthjs/next-auth/tree/main/packages/next-auth/src/providers)<br />
• make sure you use a named default export, like this: `export default function YourProvider`
- Make sure you use a named default export, like this: `export default function YourProvider`
- Add two SVG's of the provider logo, like `google-dark.svg` (dark mode) and `google.svg` (light mode), to the `/packages/next-auth/provider-logos/` directory as well as the styling config to the provider config object. See existing provider for example
2. Add provider documentation: [`/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/docs/docs/providers)
3. Add the new provider name to the `Provider type` dropdown options in [`the provider issue template`](<[http](https://github.com/nextauthjs/next-auth/edit/main/.github/ISSUE_TEMPLATE/2_bug_provider.yml)>)

View File

@@ -11,7 +11,7 @@ http://developers.strava.com/docs/reference/
The **Strava Provider** comes with a set of default options:
- [Strava Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/strava.js)
- [Strava Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/strava.ts)
You can override any of the options to suit your own use case. Ensure the redirect_uri configuration fits your needs accordingly.

View File

@@ -61,7 +61,7 @@ For the time being, the `withAuth` middleware only supports `"jwt"` as [session
More details can be found [here](https://next-auth.js.org/configuration/nextjs#middleware).
:::tip
To inclue all `dashboard` nested routes (sub pages like `/dashboard/settings`, `/dashboard/profile`) you can pass `matcher: "/dashboard/:path*"` to `config`.
To include all `dashboard` nested routes (sub pages like `/dashboard/settings`, `/dashboard/profile`) you can pass `matcher: "/dashboard/:path*"` to `config`.
For other patterns check out the [Next.js Middleware documentation](https://nextjs.org/docs/advanced-features/middleware#matcher).
:::

View File

@@ -65,6 +65,7 @@ module.exports = {
"adapters/neo4j",
"adapters/typeorm",
"adapters/sequelize",
"adapters/supabase",
"adapters/mikro-orm",
"adapters/dgraph",
"adapters/upstash-redis",

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/firebase-adapter",
"version": "1.0.2",
"version": "1.0.3",
"description": "Firebase adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -29,7 +29,7 @@
},
"scripts": {
"build": "tsc",
"test": "FIRESTORE_EMULATOR_HOST=localhost:8080 firebase emulators:exec --only firestore --project next-auth-test jest"
"test": "FIRESTORE_EMULATOR_HOST=localhost:8080 firebase --token '$FIREBASE_TOKEN' emulators:exec --only firestore --project next-auth-test jest"
},
"peerDependencies": {
"firebase": "^9.7.0",
@@ -38,9 +38,9 @@
"devDependencies": {
"@next-auth/adapter-test": "workspace:*",
"@next-auth/tsconfig": "workspace:*",
"firebase": "^9.7.0",
"firebase-tools": "^10.7.2",
"firebase": "^9.14.0",
"firebase-tools": "^11.16.1",
"jest": "^27.4.3",
"next-auth": "workspace:*"
}
}
}

View File

@@ -86,10 +86,10 @@ export function FirestoreAdapter({
async getUserByEmail(email) {
const userQuery = query(Users, where("email", "==", email), limit(1))
const userSnapshots = await getDocs(userQuery)
const userSnpashot = userSnapshots.docs[0]
const userSnapshot = userSnapshots.docs[0]
if (userSnpashot?.exists() && Users.converter) {
return Users.converter.fromFirestore(userSnpashot)
if (userSnapshot?.exists() && Users.converter) {
return Users.converter.fromFirestore(userSnapshot)
}
return null

View File

@@ -1,5 +1,5 @@
{
"extends": "@next-auth/tsconfig/tsconfig.base.json",
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/pouchdb-adapter",
"version": "0.1.4",
"version": "0.1.5",
"description": "PouchDB adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",

View File

@@ -1,5 +1,5 @@
{
"extends": "@next-auth/tsconfig/tsconfig.base.json",
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",

View File

@@ -0,0 +1,3 @@
# Run `supabase start` in this directory to get the Supabase API URL and service role key!
SUPABASE_URL=http://localhost:54321
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU

View File

@@ -0,0 +1,57 @@
<p align="center">
<br/>
<a href="https://next-auth.js.org" target="_blank">
<img height="64px" src="https://next-auth.js.org/img/logo/logo-sm.png" /></a><img height="64px" src="./logo.svg" />
<h3 align="center"><b>Supabase Adapter</b> - NextAuth.js</h3>
<p align="center">
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg?branch=main" alt="Build Test" />
<img src="https://img.shields.io/bundlephobia/minzip/@next-auth/supabase-adapter/latest" alt="Bundle Size"/>
<img src="https://img.shields.io/npm/v/@next-auth/supabase-adapter" alt="@next-auth/supabase-adapter Version" />
</p>
</p>
## Overview
This is the Supabase Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
You can find more Supabase information in the docs at [next-auth.js.org/adapters/supabase](https://next-auth.js.org/adapters/supabase).
## Getting Started
1. Install `@supabase/supabase-js`, `next-auth` and `@next-auth/supabase-adapter`.
```js
npm install @supabase/supabase-js next-auth @next-auth/supabase-adapter
```
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
```js
import NextAuth from "next-auth"
import { SupabaseAdapter } from "@next-auth/supabase-adapter"
// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
export default NextAuth({
// https://next-auth.js.org/configuration/providers
providers: [
// ...
],
adapter: SupabaseAdapter({
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
}),
// ...
})
```
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md).
## License
ISC

View File

@@ -0,0 +1,15 @@
<svg width="109" height="113" viewBox="0 0 109 113" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint0_linear)"/>
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint1_linear)" fill-opacity="0.2"/>
<path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/>
<defs>
<linearGradient id="paint0_linear" x1="53.9738" y1="54.974" x2="94.1635" y2="71.8295" gradientUnits="userSpaceOnUse">
<stop stop-color="#249361"/>
<stop offset="1" stop-color="#3ECF8E"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="36.1558" y1="30.578" x2="54.4844" y2="65.0806" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,40 @@
{
"name": "@next-auth/supabase-adapter",
"version": "0.2.0",
"description": "Supabase adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},
"author": "Martin Sonnberger <martin.sonnberger@icloud.com>",
"main": "dist/index.js",
"keywords": [
"next-auth",
"next.js",
"supabase"
],
"license": "ISC",
"private": false,
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc",
"test": "./tests/test.sh"
},
"peerDependencies": {
"@supabase/supabase-js": "^2.0.5",
"next-auth": "workspace:*"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:^0.0.0",
"@next-auth/tsconfig": "workspace:^0.0.0",
"@supabase/supabase-js": "^2.0.5",
"jest": "^27.4.3",
"next-auth": "workspace:*"
},
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}

View File

@@ -0,0 +1,140 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json }
| Json[]
export interface Database {
next_auth: {
Tables: {
accounts: {
Row: {
id: string
type: string | null
provider: string | null
providerAccountId: string | null
refresh_token: string | null
access_token: string | null
expires_at: number | null
token_type: string | null
scope: string | null
id_token: string | null
session_state: string | null
oauth_token_secret: string | null
oauth_token: string | null
userId: string | null
}
Insert: {
id?: string
type?: string | null
provider?: string | null
providerAccountId?: string | null
refresh_token?: string | null
access_token?: string | null
expires_at?: number | null
token_type?: string | null
scope?: string | null
id_token?: string | null
session_state?: string | null
oauth_token_secret?: string | null
oauth_token?: string | null
userId?: string | null
}
Update: {
id?: string
type?: string | null
provider?: string | null
providerAccountId?: string | null
refresh_token?: string | null
access_token?: string | null
expires_at?: number | null
token_type?: string | null
scope?: string | null
id_token?: string | null
session_state?: string | null
oauth_token_secret?: string | null
oauth_token?: string | null
userId?: string | null
}
}
sessions: {
Row: {
expires: string | null
sessionToken: string | null
userId: string | null
id: string
}
Insert: {
expires?: string | null
sessionToken?: string | null
userId?: string | null
id?: string
}
Update: {
expires?: string | null
sessionToken?: string | null
userId?: string | null
id?: string
}
}
users: {
Row: {
name: string | null
email: string | null
emailVerified: string | null
image: string | null
id: string
}
Insert: {
name?: string | null
email?: string | null
emailVerified?: string | null
image?: string | null
id?: string
}
Update: {
name?: string | null
email?: string | null
emailVerified?: string | null
image?: string | null
id?: string
}
}
verification_tokens: {
Row: {
id: number
identifier: string | null
token: string | null
expires: string | null
}
Insert: {
id?: number
identifier?: string | null
token?: string | null
expires?: string | null
}
Update: {
id?: number
identifier?: string | null
token?: string | null
expires?: string | null
}
}
}
Views: {
[_ in never]: never
}
Functions: {
uid: {
Args: Record<PropertyKey, never>
Returns: string
}
}
Enums: {
[_ in never]: never
}
}
}

View File

@@ -0,0 +1,212 @@
import { createClient } from "@supabase/supabase-js"
import { Database } from "./database.types"
import {
Adapter,
AdapterSession,
AdapterUser,
VerificationToken,
} from "next-auth/adapters"
function isDate(date: any) {
return (
new Date(date).toString() !== "Invalid Date" && !isNaN(Date.parse(date))
)
}
export function format<T>(obj: Record<string, any>): T {
for (const [key, value] of Object.entries(obj)) {
if (value === null) {
delete obj[key]
}
if (isDate(value)) {
obj[key] = new Date(value)
}
}
return obj as T
}
export const SupabaseAdapter = ({
url,
secret,
}: {
url: string
secret: string
}): Adapter => {
const supabase = createClient<Database, "next_auth">(url, secret, {
db: { schema: "next_auth" },
global: {
headers: { "X-Client-Info": "@next-auth/supabase-adapter@0.1.0" },
},
})
return {
async createUser(user) {
const { data, error } = await supabase
.from("users")
.insert({
...user,
emailVerified: user.emailVerified?.toISOString(),
})
.select()
.single()
if (error) throw error
return format<AdapterUser>(data)
},
async getUser(id) {
const { data, error } = await supabase
.from("users")
.select()
.eq("id", id)
.maybeSingle()
if (error) throw error
if (!data) return null
return format<AdapterUser>(data)
},
async getUserByEmail(email) {
const { data, error } = await supabase
.from("users")
.select()
.eq("email", email)
.maybeSingle()
if (error) throw error
if (!data) return null
return format<AdapterUser>(data)
},
async getUserByAccount({ providerAccountId, provider }) {
const { data, error } = await supabase
.from("accounts")
.select("users (*)")
.match({ provider, providerAccountId })
.maybeSingle()
if (error) throw error
if (!data || !data.users) return null
return format<AdapterUser>(data.users)
},
async updateUser(user) {
const { data, error } = await supabase
.from("users")
.update({
...user,
emailVerified: user.emailVerified?.toISOString(),
})
.eq("id", user.id)
.select()
.single()
if (error) throw error
return format<AdapterUser>(data)
},
async deleteUser(userId) {
const { error } = await supabase.from("users").delete().eq("id", userId)
if (error) throw error
},
async linkAccount(account) {
const { error } = await supabase.from("accounts").insert(account)
if (error) throw error
},
async unlinkAccount({ providerAccountId, provider }) {
const { error } = await supabase
.from("accounts")
.delete()
.match({ provider, providerAccountId })
if (error) throw error
},
async createSession({ sessionToken, userId, expires }) {
const { data, error } = await supabase
.from("sessions")
.insert({ sessionToken, userId, expires: expires.toISOString() })
.select()
.single()
if (error) throw error
return format<AdapterSession>(data)
},
async getSessionAndUser(sessionToken) {
const { data, error } = await supabase
.from("sessions")
.select("*, users(*)")
.eq("sessionToken", sessionToken)
.maybeSingle()
if (error) throw error
if (!data) return null
const { users: user, ...session } = data
return {
user: format<AdapterUser>(
user as Database["next_auth"]["Tables"]["users"]["Row"]
),
session: format<AdapterSession>(session),
}
},
async updateSession(session) {
const { data, error } = await supabase
.from("sessions")
.update({
...session,
expires: session.expires?.toISOString(),
})
.eq("sessionToken", session.sessionToken)
.select()
.single()
if (error) throw error
return format<AdapterSession>(data)
},
async deleteSession(sessionToken) {
const { error } = await supabase
.from("sessions")
.delete()
.eq("sessionToken", sessionToken)
if (error) throw error
},
async createVerificationToken(token) {
const { data, error } = await supabase
.from("verification_tokens")
.insert({
...token,
expires: token.expires.toISOString(),
})
.select()
.single()
if (error) throw error
const { id, ...verificationToken } = data
return format<VerificationToken>(verificationToken)
},
async useVerificationToken({ identifier, token }) {
const { data, error } = await supabase
.from("verification_tokens")
.delete()
.match({ identifier, token })
.select()
.maybeSingle()
if (error) throw error
if (!data) return null
const { id, ...verificationToken } = data
return format<VerificationToken>(verificationToken)
},
}
}

View File

@@ -0,0 +1,44 @@
# A string used to distinguish different Supabase projects on the same host. Defaults to the working
# directory name when running `supabase init`.
project_id = "nextauth"
[api]
# Port to use for the API URL.
port = 54321
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
# endpoints. public and storage are always included.
schemas = ["next_auth"]
# Extra schemas to add to the search_path of every request.
extra_search_path = ["extensions"]
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
# for accidental or malicious requests.
max_rows = 1000
[db]
# Port to use for the local database URL.
port = 54322
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
# server_version;` on the remote database to check.
major_version = 14
[studio]
# Port to use for Supabase Studio.
port = 54323
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
# are monitored, and you can view the emails that would have been sent from the web interface.
[inbucket]
# Port to use for the email testing server web interface.
port = 54324
[auth]
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://localhost:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://localhost:3000"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one
# week).
jwt_expiry = 3600
# Allow/disallow new user signups to your project.
enable_signup = true

View File

@@ -0,0 +1,101 @@
--
-- Name: next_auth; Type: SCHEMA;
--
CREATE SCHEMA next_auth;
GRANT USAGE ON SCHEMA next_auth TO service_role;
GRANT ALL ON SCHEMA next_auth TO postgres;
--
-- Create users table
--
CREATE TABLE IF NOT EXISTS next_auth.users
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
name text,
email text,
"emailVerified" timestamp with time zone,
image text,
CONSTRAINT users_pkey PRIMARY KEY (id),
CONSTRAINT email_unique UNIQUE (email)
);
GRANT ALL ON TABLE next_auth.users TO postgres;
GRANT ALL ON TABLE next_auth.users TO service_role;
--- uid() function to be used in RLS policies
CREATE FUNCTION next_auth.uid() RETURNS uuid
LANGUAGE sql STABLE
AS $$
select
coalesce(
nullif(current_setting('request.jwt.claim.sub', true), ''),
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')
)::uuid
$$;
--
-- Create sessions table
--
CREATE TABLE IF NOT EXISTS next_auth.sessions
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
expires timestamp with time zone NOT NULL,
"sessionToken" text NOT NULL,
"userId" uuid,
CONSTRAINT sessions_pkey PRIMARY KEY (id),
CONSTRAINT sessionToken_unique UNIQUE ("sessionToken"),
CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId")
REFERENCES next_auth.users (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE
);
GRANT ALL ON TABLE next_auth.sessions TO postgres;
GRANT ALL ON TABLE next_auth.sessions TO service_role;
--
-- Create accounts table
--
CREATE TABLE IF NOT EXISTS next_auth.accounts
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
type text NOT NULL,
provider text NOT NULL,
"providerAccountId" text NOT NULL,
refresh_token text,
access_token text,
expires_at bigint,
token_type text,
scope text,
id_token text,
session_state text,
oauth_token_secret text,
oauth_token text,
"userId" uuid,
CONSTRAINT accounts_pkey PRIMARY KEY (id),
CONSTRAINT provider_unique UNIQUE (provider, "providerAccountId"),
CONSTRAINT "accounts_userId_fkey" FOREIGN KEY ("userId")
REFERENCES next_auth.users (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE
);
GRANT ALL ON TABLE next_auth.accounts TO postgres;
GRANT ALL ON TABLE next_auth.accounts TO service_role;
--
-- Create verification_tokens table
--
CREATE TABLE IF NOT EXISTS next_auth.verification_tokens
(
identifier text,
token text,
expires timestamp with time zone NOT NULL,
CONSTRAINT verification_tokens_pkey PRIMARY KEY (token),
CONSTRAINT token_unique UNIQUE (token),
CONSTRAINT token_identifier_unique UNIQUE (token, identifier)
);
GRANT ALL ON TABLE next_auth.verification_tokens TO postgres;
GRANT ALL ON TABLE next_auth.verification_tokens TO service_role;

View File

@@ -0,0 +1,33 @@
/**
* USERS
* Note: This table contains user data. Users should only be able to view and update their own data.
*/
create table users (
-- UUID from next_auth.users
id uuid not null primary key,
name text,
email text,
image text,
constraint "users_id_fkey" foreign key ("id")
references next_auth.users (id) match simple
on update no action
on delete cascade -- if user is deleted in NextAuth they will also be deleted in our public table.
);
alter table users enable row level security;
create policy "Can view own user data." on users for select using (next_auth.uid() = id);
create policy "Can update own user data." on users for update using (next_auth.uid() = id);
/**
* This trigger automatically creates a user entry when a new user signs up via NextAuth.
*/
create function public.handle_new_user()
returns trigger as $$
begin
insert into public.users (id, name, email, image)
values (new.id, new.name, new.email, new.image);
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on next_auth.users
for each row execute procedure public.handle_new_user();

View File

@@ -0,0 +1,76 @@
import { runBasicTests } from "@next-auth/adapter-test"
import { format, SupabaseAdapter } from "../src"
import { createClient } from "@supabase/supabase-js"
import type {
AdapterSession,
AdapterUser,
VerificationToken,
} from "next-auth/adapters"
import type { Account } from "next-auth"
const supabase = createClient(
process.env.SUPABASE_URL ?? "http://localhost:54321",
process.env.SUPABASE_SERVICE_ROLE_KEY ??
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU",
{ db: { schema: "next_auth" } }
)
runBasicTests({
adapter: SupabaseAdapter({
url: process.env.SUPABASE_URL ?? "http://localhost:54321",
secret:
process.env.SUPABASE_SERVICE_ROLE_KEY ??
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU",
}),
db: {
async session(sessionToken) {
const { data, error } = await supabase
.from("sessions")
.select()
.eq("sessionToken", sessionToken)
.maybeSingle()
if (error) throw error
if (!data) return null
return format<AdapterSession>(data)
},
async user(id) {
const { data, error } = await supabase
.from("users")
.select()
.eq("id", id)
.maybeSingle()
if (error) throw error
if (!data) return null
return format<AdapterUser>(data)
},
async account({ provider, providerAccountId }) {
const { data, error } = await supabase
.from("accounts")
.select()
.match({ provider, providerAccountId })
.maybeSingle()
if (error) throw error
if (!data) return null
return format<Account>(data)
},
async verificationToken({ identifier, token }) {
const { data, error } = await supabase
.from("verification_tokens")
.select()
.match({ identifier, token })
.single()
if (error) throw error
const { id, ...verificationToken } = data
return format<VerificationToken>(verificationToken)
},
},
})

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# install Supabase CLI when run on CI
if [ "$CI" = true ]; then
wget -O supabase.deb https://github.com/supabase/cli/releases/download/v0.29.0/supabase_0.29.0_linux_amd64.deb
sudo dpkg -i supabase.deb
fi
# Start Supabase, grep key and set it as SUPABASE_SERVICE_ROLE_KEY environment variable
line=$(supabase start | grep 'service_role key')
IFS=':'; arr=($line); unset IFS;
export SUPABASE_SERVICE_ROLE_KEY=${arr[1]}
# Always stop Supabase, but exit with 1 when tests are failing
if npx jest; then
supabase stop
else
supabase stop && exit 1
fi

View File

@@ -0,0 +1,8 @@
{
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"exclude": ["tests", "dist", "jest.config.js"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "4.16.4",
"version": "4.18.0",
"description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",

View File

@@ -52,7 +52,8 @@ async function getBody(req: Request): Promise<Record<string, any> | undefined> {
// TODO:
async function toInternalRequest(
req: RequestInternal | Request
req: RequestInternal | Request,
trustHost: boolean = false
): Promise<RequestInternal> {
if (req instanceof Request) {
const url = new URL(req.url)
@@ -70,7 +71,11 @@ async function toInternalRequest(
cookies: parseCookie(req.headers.get("cookie") ?? ""),
providerId: nextauth[1],
error: url.searchParams.get("error") ?? nextauth[1],
host: detectHost(headers["x-forwarded-host"] ?? headers.host),
host: detectHost(
trustHost,
headers["x-forwarded-host"] ?? headers.host,
"http://localhost:3000"
),
query,
}
}
@@ -82,7 +87,7 @@ export async function NextAuthHandler<
>(params: NextAuthHandlerParams): Promise<OutgoingResponse<Body>> {
const { options: userOptions, req: incomingRequest } = params
const req = await toInternalRequest(incomingRequest)
const req = await toInternalRequest(incomingRequest, userOptions.trustHost)
setLogger(userOptions.logger, userOptions.debug)

View File

@@ -93,6 +93,7 @@ export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
sameSite: "lax",
path: "/",
secure: useSecureCookies,
maxAge: 60 * 15, // 15 minutes in seconds
},
},
state: {
@@ -102,6 +103,7 @@ export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
sameSite: "lax",
path: "/",
secure: useSecureCookies,
maxAge: 60 * 15, // 15 minutes in seconds
},
},
nonce: {

View File

@@ -26,13 +26,15 @@ export async function createPKCE(options: InternalOptions<"oauth">): Promise<
const code_verifier = generators.codeVerifier()
const code_challenge = generators.codeChallenge(code_verifier)
const maxAge = cookies.pkceCodeVerifier.options.maxAge ?? PKCE_MAX_AGE
const expires = new Date()
expires.setTime(expires.getTime() + PKCE_MAX_AGE * 1000)
expires.setTime(expires.getTime() + maxAge * 1000)
// Encrypt code_verifier and save it to an encrypted cookie
const encryptedCodeVerifier = await jwt.encode({
...options.jwt,
maxAge: PKCE_MAX_AGE,
maxAge,
token: { code_verifier },
})
@@ -40,7 +42,7 @@ export async function createPKCE(options: InternalOptions<"oauth">): Promise<
code_challenge,
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
code_verifier,
PKCE_MAX_AGE,
maxAge,
})
return {

View File

@@ -17,17 +17,18 @@ export async function createState(
}
const state = generators.state()
const maxAge = cookies.state.options.maxAge ?? STATE_MAX_AGE
const encodedState = await jwt.encode({
...jwt,
maxAge: STATE_MAX_AGE,
maxAge,
token: { state },
})
logger.debug("CREATE_STATE", { state, maxAge: STATE_MAX_AGE })
logger.debug("CREATE_STATE", { state, maxAge })
const expires = new Date()
expires.setTime(expires.getTime() + STATE_MAX_AGE * 1000)
expires.setTime(expires.getTime() + maxAge * 1000)
return {
value: state,
cookie: {

View File

@@ -38,10 +38,10 @@ export interface NextAuthOptions {
providers: Provider[]
/**
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
* If not specified, it falls back to `jwt.secret` or `NEXTAUTH_SECRET` from environment vairables.
* Otherwise it will use a hash of all configuration options, including Client ID / Secrets for entropy.
* If not specified, it falls back to `jwt.secret` or `NEXTAUTH_SECRET` from environment variables.
* Otherwise, it will use a hash of all configuration options, including Client ID / Secrets for entropy.
*
* NOTE: The last behavior is extrmely volatile, and will throw an error in production.
* NOTE: The last behavior is extremely volatile, and will throw an error in production.
* * **Default value**: `string` (SHA hash of the "options" object)
* * **Required**: No - **but strongly recommended**!
*
@@ -203,6 +203,16 @@ export interface NextAuthOptions {
* [Documentation](https://next-auth.js.org/configuration/options#cookies) | [Usage example](https://next-auth.js.org/configuration/options#example)
*/
cookies?: Partial<CookiesOptions>
/**
* If set to `true`, NextAuth.js will use either the `x-forwarded-host` or `host` headers,
* instead of `NEXTAUTH_URL`
* Make sure that reading `x-forwarded-host` on your hosting platform can be trusted.
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
* but **may have complex implications** or side effects.
* You should **try to avoid using advanced options** unless you are very comfortable using them.
* @default Boolean(process.env.AUTH_TRUST_HOST ?? process.env.VERCEL)
*/
trustHost?: boolean
}
/**

View File

@@ -9,6 +9,7 @@ import type {
} from "next"
import type { NextAuthOptions, Session } from ".."
import type {
CallbacksOptions,
NextAuthAction,
NextAuthRequest,
NextAuthResponse,
@@ -21,12 +22,17 @@ async function NextAuthNextHandler(
) {
const { nextauth, ...query } = req.query
options.secret =
options.secret ?? options.jwt?.secret ?? process.env.NEXTAUTH_SECRET
options.secret ??= options.jwt?.secret ?? process.env.NEXTAUTH_SECRET
options.trustHost ??= !!(process.env.AUTH_TRUST_HOST ?? process.env.VERCEL)
const handler = await NextAuthHandler({
req: {
host: detectHost(req.headers["x-forwarded-host"]),
host: detectHost(
options.trustHost,
req.headers["x-forwarded-host"],
process.env.NEXTAUTH_URL ??
(process.env.NODE_ENV !== "production" && "http://localhost:3000")
),
body: req.body,
query,
cookies: req.cookies,
@@ -85,17 +91,25 @@ export default NextAuth
let experimentalWarningShown = false
let experimentalRSCWarningShown = false
export async function unstable_getServerSession(
type GetServerSessionOptions = Partial<Omit<NextAuthOptions, "callbacks">> & {
callbacks?: Omit<NextAuthOptions['callbacks'], "session"> & {
session?: (...args: Parameters<CallbacksOptions["session"]>) => any
}
}
export async function unstable_getServerSession<
O extends GetServerSessionOptions,
R = O["callbacks"] extends { session: (...args: any[]) => infer U }
? U
: Session
>(
...args:
| [
GetServerSidePropsContext["req"],
GetServerSidePropsContext["res"],
NextAuthOptions
]
| [NextApiRequest, NextApiResponse, NextAuthOptions]
| [NextAuthOptions]
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"], O]
| [NextApiRequest, NextApiResponse, O]
| [O]
| []
): Promise<Session | null> {
): Promise<R | null> {
if (!experimentalWarningShown && process.env.NODE_ENV !== "production") {
console.warn(
"[next-auth][warn][EXPERIMENTAL_API]",
@@ -123,7 +137,8 @@ export async function unstable_getServerSession(
let req, res, options: NextAuthOptions
if (isRSC) {
options = args[0] ?? { providers: [] }
options = Object.assign({}, args[0], { providers: [] })
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { headers, cookies } = require("next/headers")
req = {
@@ -138,15 +153,21 @@ export async function unstable_getServerSession(
} else {
req = args[0]
res = args[1]
options = args[2]
options = Object.assign(args[2], { providers: [] })
}
options.secret = options.secret ?? process.env.NEXTAUTH_SECRET
options.secret ??= process.env.NEXTAUTH_SECRET
options.trustHost ??= !!(process.env.AUTH_TRUST_HOST ?? process.env.VERCEL)
const session = await NextAuthHandler<Session | {} | string>({
options,
req: {
host: detectHost(req.headers["x-forwarded-host"]),
host: detectHost(
options.trustHost,
req.headers["x-forwarded-host"],
process.env.NEXTAUTH_URL ??
(process.env.NODE_ENV !== "production" && "http://localhost:3000")
),
action: "session",
method: "GET",
cookies: req.cookies,
@@ -162,7 +183,7 @@ export async function unstable_getServerSession(
if (status === 200) {
// @ts-expect-error
if (isRSC) delete body.expires
return body as Session
return body as R
}
throw new Error((body as any).message)
}

View File

@@ -6,6 +6,7 @@ import { NextResponse, NextRequest } from "next/server"
import { getToken } from "../jwt"
import parseUrl from "../utils/parse-url"
import { detectHost } from "../utils/detect-host"
type AuthorizedCallback = (params: {
token: JWT | null
@@ -89,7 +90,17 @@ export interface NextAuthMiddlewareOptions {
* The same `secret` used in the `NextAuth` configuration.
* Defaults to the `NEXTAUTH_SECRET` environment variable.
*/
secret?: string
secret?: NextAuthOptions["secret"]
/**
* If set to `true`, NextAuth.js will use either the `x-forwarded-host` or `host` headers,
* instead of `NEXTAUTH_URL`
* Make sure that reading `x-forwarded-host` on your hosting platform can be trusted.
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
* but **may have complex implications** or side effects.
* You should **try to avoid using advanced options** unless you are very comfortable using them.
* @default Boolean(process.env.VERCEL ?? process.env.AUTH_TRUST_HOST)
*/
trustHost?: NextAuthOptions["trustHost"]
}
// TODO: `NextMiddleware` should allow returning `void`
@@ -98,14 +109,25 @@ type NextMiddlewareResult = ReturnType<NextMiddleware> | void // eslint-disable-
async function handleMiddleware(
req: NextRequest,
options: NextAuthMiddlewareOptions | undefined,
options: NextAuthMiddlewareOptions | undefined = {},
onSuccess?: (token: JWT | null) => Promise<NextMiddlewareResult>
) {
const { pathname, search, origin, basePath } = req.nextUrl
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
const errorPage = options?.pages?.error ?? "/api/auth/error"
const authPath = parseUrl(process.env.NEXTAUTH_URL).path
options.trustHost = Boolean(
options.trustHost ?? process.env.VERCEL ?? process.env.AUTH_TRUST_HOST
)
const host = detectHost(
options.trustHost,
req.headers.get("x-forwarded-host"),
process.env.NEXTAUTH_URL ??
(process.env.NODE_ENV !== "production" && "http://localhost:3000")
)
const authPath = parseUrl(host).path
const publicPaths = ["/_next", "/favicon.ico"]
// Avoid infinite redirects/invalid response
@@ -146,7 +168,10 @@ async function handleMiddleware(
// the user is not logged in, redirect to the sign-in page
const signInUrl = new URL(`${basePath}${signInPage}`, origin)
signInUrl.searchParams.append("callbackUrl", `${basePath}${pathname}${search}`)
signInUrl.searchParams.append(
"callbackUrl",
`${basePath}${pathname}${search}`
)
return NextResponse.redirect(signInUrl)
}

View File

@@ -31,7 +31,7 @@ export default function Naver<P extends NaverProfile>(
profile(profile) {
return {
id: profile.response.id,
name: profile.response.name,
name: profile.response.nickname,
email: profile.response.email,
image: profile.response.profile_image,
}

View File

@@ -1,8 +1,12 @@
/** Extract the host from the environment */
export function detectHost(forwardedHost: any) {
// If we detect a Vercel environment, we can trust the host
if (process.env.VERCEL ?? process.env.AUTH_TRUST_HOST)
return forwardedHost
// If `NEXTAUTH_URL` is `undefined` we fall back to "http://localhost:3000"
return process.env.NEXTAUTH_URL
export function detectHost(
trusted: boolean,
forwardedValue: string | string[] | undefined | null,
defaultValue: string | false
): string | undefined {
if (trusted && forwardedValue) {
return Array.isArray(forwardedValue) ? forwardedValue[0] : forwardedValue
}
return defaultValue || undefined
}

View File

@@ -1,21 +1,15 @@
import { NextMiddleware } from "next/server"
import { NextMiddleware, NextRequest } from "next/server"
import { NextAuthMiddlewareOptions, withAuth } from "../src/next/middleware"
it("should not match pages as public paths", async () => {
const options: NextAuthMiddlewareOptions = {
pages: {
signIn: "/",
error: "/",
},
pages: { signIn: "/", error: "/" },
secret: "secret",
}
const nextUrl: any = {
pathname: "/protected/pathA",
search: "",
origin: "http://127.0.0.1",
}
const req: any = { nextUrl, headers: { authorization: "" } }
const req = new NextRequest("http://127.0.0.1/protected/pathA", {
headers: { authorization: "" },
})
const handleMiddleware = withAuth(options) as NextMiddleware
const res = await handleMiddleware(req, null as any)
@@ -24,15 +18,11 @@ it("should not match pages as public paths", async () => {
})
it("should not redirect on public paths", async () => {
const options: NextAuthMiddlewareOptions = {
secret: "secret",
}
const nextUrl: any = {
pathname: "/_next/foo",
search: "",
origin: "http://127.0.0.1",
}
const req: any = { nextUrl, headers: { authorization: "" } }
const options: NextAuthMiddlewareOptions = { secret: "secret" }
const req = new NextRequest("http://127.0.0.1/_next/foo", {
headers: { authorization: "" },
})
const handleMiddleware = withAuth(options) as NextMiddleware
const res = await handleMiddleware(req, null as any)
@@ -40,55 +30,66 @@ it("should not redirect on public paths", async () => {
})
it("should redirect according to nextUrl basePath", async () => {
const options: NextAuthMiddlewareOptions = {
secret: "secret"
}
const nextUrl: any = {
pathname: "/protected/pathA",
search: "",
origin: "http://127.0.0.1",
basePath: "/custom-base-path",
}
const req: any = { nextUrl, headers: { authorization: "" } }
const options: NextAuthMiddlewareOptions = { secret: "secret" }
const handleMiddleware = withAuth(options) as NextMiddleware
const res = await handleMiddleware(req, null as any)
expect(res).toBeDefined()
expect(res.status).toEqual(307)
expect(res.headers.get('location')).toContain("http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA")
})
it("should redirect according to nextUrl basePath", async () => {
// given
const options: NextAuthMiddlewareOptions = {
secret: "secret"
}
const handleMiddleware = withAuth(options) as NextMiddleware
// when
const res = await handleMiddleware({
const req = {
nextUrl: {
pathname: "/protected/pathA",
search: "",
origin: "http://127.0.0.1",
basePath: "/custom-base-path"
}, headers: { authorization: "" }
} as any, null as any)
basePath: "/custom-base-path",
},
headers: new Headers({ authorization: "" }),
}
const handleMiddleware = withAuth(options) as NextMiddleware
const res = await handleMiddleware(req as NextRequest, null as any)
expect(res).toBeDefined()
expect(res?.status).toEqual(307)
expect(res?.headers.get("location")).toContain(
"http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA"
)
})
it("should redirect according to nextUrl basePath", async () => {
// given
const options: NextAuthMiddlewareOptions = { secret: "secret" }
const handleMiddleware = withAuth(options) as NextMiddleware
const req1 = {
nextUrl: {
pathname: "/protected/pathA",
search: "",
origin: "http://127.0.0.1",
basePath: "/custom-base-path",
},
headers: new Headers({ authorization: "" }),
}
// when
const res = await handleMiddleware(req1 as NextRequest, null as any)
// then
expect(res).toBeDefined()
expect(res.status).toEqual(307)
expect(res.headers.get("location")).toContain("http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA")
expect(res?.status).toEqual(307)
expect(res?.headers.get("location")).toContain(
"http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA"
)
// and when follow redirect
const resFromRedirectedUrl = await handleMiddleware({
const req2 = {
nextUrl: {
pathname: "/api/auth/signin",
search: "callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA",
origin: "http://127.0.0.1",
basePath: "/custom-base-path"
}, headers: { authorization: "" }
} as any, null as any)
basePath: "/custom-base-path",
},
headers: new Headers({ authorization: "" }),
}
// and when follow redirect
const resFromRedirectedUrl = await handleMiddleware(
req2 as NextRequest,
null as any
)
// then return sign in page
expect(resFromRedirectedUrl).toBeUndefined()

View File

@@ -0,0 +1,139 @@
import { mockLogger } from "./lib"
import type { InternalOptions, LoggerInstance, InternalProvider, CallbacksOptions, Account, Awaitable, Profile, Session, User, CookiesOptions } from "../src"
import { createPKCE } from "../src/core/lib/oauth/pkce-handler"
import { InternalUrl } from "../src/utils/parse-url"
import { JWT, JWTDecodeParams, JWTEncodeParams, JWTOptions } from "../src/jwt"
import { CredentialInput } from "../src/providers"
let logger: LoggerInstance
let url: InternalUrl
let provider: InternalProvider<"oauth">
let jwt: JWTOptions
let callbacks: CallbacksOptions
let cookies: CookiesOptions
let options: InternalOptions<"oauth">
beforeEach(() => {
logger = mockLogger()
url = {
origin: "http://localhost:3000",
host: "localhost:3000",
path: "/api/auth",
base: "http://localhost:3000/api/auth",
toString: () => "http://localhost:3000/api/auth"
}
provider = {
type: "oauth",
id: "testId",
name: "testName",
signinUrl: "/",
callbackUrl: "/",
checks: ["pkce", "state"]
}
jwt = {
secret: "secret",
maxAge: 0,
encode: function (params: JWTEncodeParams): Awaitable<string> {
throw new Error("Function not implemented.")
},
decode: function (params: JWTDecodeParams): Awaitable<JWT | null> {
throw new Error("Function not implemented.")
}
}
callbacks = {
signIn: function (params: { user: User; account: Account; profile: Profile & Record<string, unknown>; email: { verificationRequest?: boolean | undefined }; credentials?: Record<string, CredentialInput> | undefined }): Awaitable<string | boolean> {
throw new Error("Function not implemented.")
},
redirect: function (params: { url: string; baseUrl: string }): Awaitable<string> {
throw new Error("Function not implemented.")
},
session: function (params: { session: Session; user: User; token: JWT }): Awaitable<Session> {
throw new Error("Function not implemented.")
},
jwt: function (params: { token: JWT; user?: User | undefined; account?: Account | undefined; profile?: Profile | undefined; isNewUser?: boolean | undefined }): Awaitable<JWT> {
throw new Error("Function not implemented.")
}
}
cookies = {
sessionToken: { name: "", options: undefined },
callbackUrl: { name: "", options: undefined },
csrfToken: { name: "", options: undefined },
pkceCodeVerifier: { name: "", options: {} },
state: { name: "", options: undefined },
nonce: { name: "", options: undefined }
}
options = {
url,
action: "session",
provider,
secret: "",
debug: false,
logger,
session: { strategy: "jwt", maxAge: 0, updateAge: 0 },
pages: {},
jwt,
events: {},
callbacks,
cookies,
callbackUrl: '',
providers: [],
theme: {}
}
})
describe("createPKCE", () => {
it("returns a code challenge, code challenge method, and cookie", async () => {
const pkce = await createPKCE(options)
expect(pkce?.code_challenge).not.toBeNull()
expect(pkce?.code_challenge_method).toEqual("S256")
expect(pkce?.cookie).not.toBeNull()
})
it("does not return a pkce when the provider does not support pkce", async () => {
options.provider.checks = ["state"]
const pkce = await createPKCE(options)
expect(pkce).toBeUndefined()
})
it("sets the cookie expiration to a default of 15 minutes when the max age option is not provided", async () => {
const pkce = await createPKCE(options)
const defaultMaxAge = 60 * 15 // 15 minutes in seconds
const expires = new Date()
expires.setTime(expires.getTime() + defaultMaxAge * 1000)
validateCookieExpiration({pkce, expires})
expect(pkce?.cookie.options.maxAge).toBeUndefined()
})
it("sets the cookie expiration and max age to the provided max age from the options", async () => {
const maxAge = 60 * 20 // 20 minutes
cookies.pkceCodeVerifier.options.maxAge = maxAge
const pkce = await createPKCE(options)
const expires = new Date()
expires.setTime(expires.getTime() + maxAge * 1000)
validateCookieExpiration({pkce, expires})
expect(pkce?.cookie.options.maxAge).toEqual(maxAge)
})
})
// comparing the parts instead of getTime() because the milliseconds
// will not match since the two Date objects are created milliseconds apart
const validateCookieExpiration = ({pkce, expires}) => {
const cookieExpires = pkce?.cookie.options.expires
expect(cookieExpires.getFullYear()).toEqual(expires.getFullYear())
expect(cookieExpires.getMonth()).toEqual(expires.getMonth())
expect(cookieExpires.getFullYear()).toEqual(expires.getFullYear())
expect(cookieExpires.getHours()).toEqual(expires.getHours())
expect(cookieExpires.getMinutes()).toEqual(expires.getMinutes())
}

View File

@@ -0,0 +1,134 @@
import { mockLogger } from "./lib"
import type { InternalOptions, LoggerInstance, InternalProvider, CallbacksOptions, Account, Awaitable, Profile, Session, User, CookiesOptions } from "../src"
import { createState } from "../src/core/lib/oauth/state-handler"
import { InternalUrl } from "../src/utils/parse-url"
import { JWT, JWTOptions, encode, decode } from "../src/jwt"
import { CredentialInput } from "../src/providers"
let logger: LoggerInstance
let url: InternalUrl
let provider: InternalProvider<"oauth">
let jwt: JWTOptions
let callbacks: CallbacksOptions
let cookies: CookiesOptions
let options: InternalOptions<"oauth">
beforeEach(() => {
logger = mockLogger()
url = {
origin: "http://localhost:3000",
host: "localhost:3000",
path: "/api/auth",
base: "http://localhost:3000/api/auth",
toString: () => "http://localhost:3000/api/auth"
}
provider = {
type: "oauth",
id: "testId",
name: "testName",
signinUrl: "/",
callbackUrl: "/",
checks: ["pkce", "state"]
}
jwt = {
secret: "secret",
maxAge: 0,
encode,
decode
}
callbacks = {
signIn: function (params: { user: User; account: Account; profile: Profile & Record<string, unknown>; email: { verificationRequest?: boolean | undefined }; credentials?: Record<string, CredentialInput> | undefined }): Awaitable<string | boolean> {
throw new Error("Function not implemented.")
},
redirect: function (params: { url: string; baseUrl: string }): Awaitable<string> {
throw new Error("Function not implemented.")
},
session: function (params: { session: Session; user: User; token: JWT }): Awaitable<Session> {
throw new Error("Function not implemented.")
},
jwt: function (params: { token: JWT; user?: User | undefined; account?: Account | undefined; profile?: Profile | undefined; isNewUser?: boolean | undefined }): Awaitable<JWT> {
throw new Error("Function not implemented.")
}
}
cookies = {
sessionToken: { name: "", options: undefined },
callbackUrl: { name: "", options: undefined },
csrfToken: { name: "", options: undefined },
pkceCodeVerifier: { name: "", options: undefined },
state: { name: "", options: {} },
nonce: { name: "", options: undefined }
}
options = {
url,
action: "session",
provider,
secret: "",
debug: false,
logger,
session: { strategy: "jwt", maxAge: 0, updateAge: 0 },
pages: {},
jwt,
events: {},
callbacks,
cookies,
callbackUrl: '',
providers: [],
theme: {}
}
})
describe("createState", () => {
it("returns a state, and cookie", async () => {
const state = await createState(options)
expect(state?.value).not.toBeNull()
expect(state?.cookie).not.toBeNull()
})
it("does not return a state when the provider does not support state", async () => {
options.provider.checks = ["pkce"]
const state = await createState(options)
expect(state).toBeUndefined()
})
it("sets the cookie expiration to a default of 15 minutes when the max age option is not provided", async () => {
const state = await createState(options)
const defaultMaxAge = 60 * 15 // 15 minutes in seconds
const expires = new Date()
expires.setTime(expires.getTime() + defaultMaxAge * 1000)
validateCookieExpiration({state, expires})
expect(state?.cookie.options.maxAge).toBeUndefined()
})
it("sets the cookie expiration and max age to the provided max age from the options", async () => {
const maxAge = 60 * 20 // 20 minutes
cookies.state.options.maxAge = maxAge
const state = await createState(options)
const expires = new Date()
expires.setTime(expires.getTime() + maxAge * 1000)
validateCookieExpiration({state, expires})
expect(state?.cookie.options.maxAge).toEqual(maxAge)
})
})
// comparing the parts instead of getTime() because the milliseconds
// will not match since the two Date objects are created milliseconds apart
const validateCookieExpiration = ({state, expires}) => {
const cookieExpires = state?.cookie.options.expires
expect(cookieExpires.getFullYear()).toEqual(expires.getFullYear())
expect(cookieExpires.getMonth()).toEqual(expires.getMonth())
expect(cookieExpires.getFullYear()).toEqual(expires.getFullYear())
expect(cookieExpires.getHours()).toEqual(expires.getHours())
expect(cookieExpires.getMinutes()).toEqual(expires.getMinutes())
}

4913
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff