mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
19 Commits
chore/docs
...
@next-auth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2a07932b9 | ||
|
|
25c7ce1d2b | ||
|
|
227a233bd8 | ||
|
|
cf9f133aa3 | ||
|
|
2301c1be44 | ||
|
|
6e408e24bf | ||
|
|
f277989c69 | ||
|
|
6146e93288 | ||
|
|
1ff565da6c | ||
|
|
41f75cf870 | ||
|
|
dd591ed8d0 | ||
|
|
297bc2317f | ||
|
|
b170138e70 | ||
|
|
a307079e0f | ||
|
|
d52b7a6b7d | ||
|
|
30b69a07eb | ||
|
|
0d1757814f | ||
|
|
068f9b50b8 | ||
|
|
dac490b7a1 |
1
.github/ISSUE_TEMPLATE/3_bug_adapter.yml
vendored
1
.github/ISSUE_TEMPLATE/3_bug_adapter.yml
vendored
@@ -31,6 +31,7 @@ body:
|
|||||||
- "@next-auth/pouchdb-adapter"
|
- "@next-auth/pouchdb-adapter"
|
||||||
- "@next-auth/prisma-adapter"
|
- "@next-auth/prisma-adapter"
|
||||||
- "@next-auth/sequelize-adapter"
|
- "@next-auth/sequelize-adapter"
|
||||||
|
- "@next-auth/supabase-adapter"
|
||||||
- "@next-auth/typeorm-legacy-adapter"
|
- "@next-auth/typeorm-legacy-adapter"
|
||||||
- "@next-auth/upstash-redis-adapter"
|
- "@next-auth/upstash-redis-adapter"
|
||||||
- "@next-auth/xata-adapter"
|
- "@next-auth/xata-adapter"
|
||||||
|
|||||||
3
.github/issue-labeler.yml
vendored
3
.github/issue-labeler.yml
vendored
@@ -30,6 +30,9 @@ prisma:
|
|||||||
sequelize:
|
sequelize:
|
||||||
- "@next-auth/sequelize-adapter"
|
- "@next-auth/sequelize-adapter"
|
||||||
|
|
||||||
|
supabase:
|
||||||
|
- "@next-auth/supabase-adapter"
|
||||||
|
|
||||||
typeorm-legacy:
|
typeorm-legacy:
|
||||||
- "@next-auth/typeorm-legacy-adapter"
|
- "@next-auth/typeorm-legacy-adapter"
|
||||||
|
|
||||||
|
|||||||
3
.github/pr-labeler.yml
vendored
3
.github/pr-labeler.yml
vendored
@@ -42,6 +42,9 @@ prisma:
|
|||||||
sequelize:
|
sequelize:
|
||||||
- packages/adapter-sequelize/**
|
- packages/adapter-sequelize/**
|
||||||
|
|
||||||
|
supabase:
|
||||||
|
- packages/adapter-supabase/**
|
||||||
|
|
||||||
typeorm-legacy:
|
typeorm-legacy:
|
||||||
- packages/adapter-typeorm-legacy/**
|
- packages/adapter-typeorm-legacy/**
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -18,10 +18,10 @@ jobs:
|
|||||||
language: ["javascript"]
|
language: ["javascript"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|||||||
2
.github/workflows/label-issue.yml
vendored
2
.github/workflows/label-issue.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
name: Triage
|
name: Triage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: github/issue-labeler@v2.4.1
|
- uses: github/issue-labeler@v2.5
|
||||||
with:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
configuration-path: ".github/issue-labeler.yml"
|
configuration-path: ".github/issue-labeler.yml"
|
||||||
|
|||||||
2
.github/workflows/label-pr.yml
vendored
2
.github/workflows/label-pr.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
name: Triage
|
name: Triage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v3
|
- uses: actions/labeler@v4
|
||||||
with:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
configuration-path: ".github/pr-labeler.yml"
|
configuration-path: ".github/pr-labeler.yml"
|
||||||
|
|||||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Init
|
- name: Init
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2.2.1
|
uses: pnpm/action-setup@v2.2.4
|
||||||
with:
|
with:
|
||||||
version: 7.5.1
|
version: 7.5.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
@@ -49,11 +49,11 @@ jobs:
|
|||||||
environment: Production
|
environment: Production
|
||||||
steps:
|
steps:
|
||||||
- name: Init
|
- name: Init
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2.2.1
|
uses: pnpm/action-setup@v2.2.4
|
||||||
with:
|
with:
|
||||||
version: 7.5.1
|
version: 7.5.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
@@ -81,9 +81,9 @@ jobs:
|
|||||||
environment: Preview
|
environment: Preview
|
||||||
steps:
|
steps:
|
||||||
- name: Init
|
- name: Init
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2.2.1
|
uses: pnpm/action-setup@v2.2.4
|
||||||
with:
|
with:
|
||||||
version: 7.5.1
|
version: 7.5.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN_PKG }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN_PKG }}
|
||||||
- name: Comment version on PR
|
- name: Comment version on PR
|
||||||
uses: NejcZdovc/comment-pr@v1
|
uses: NejcZdovc/comment-pr@v2
|
||||||
with:
|
with:
|
||||||
message:
|
message:
|
||||||
"🎉 Experimental release [published 📦️ on npm](https://npmjs.com/package/next-auth/v/${{ env.VERSION }})!\n \
|
"🎉 Experimental release [published 📦️ on npm](https://npmjs.com/package/next-auth/v/${{ env.VERSION }})!\n \
|
||||||
|
|||||||
2
.github/workflows/sync-examples.yml
vendored
2
.github/workflows/sync-examples.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Run GitHub File Sync
|
- name: Run GitHub File Sync
|
||||||
# Can update to v1 when https://github.com/BetaHuhn/repo-file-sync-action/issues/168 is resolved
|
# 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
|
uses: BetaHuhn/repo-file-sync-action@v1.16.5
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -65,6 +65,7 @@ dev.db*
|
|||||||
packages/adapter-prisma/prisma/dev.db
|
packages/adapter-prisma/prisma/dev.db
|
||||||
packages/adapter-prisma/prisma/migrations
|
packages/adapter-prisma/prisma/migrations
|
||||||
db.sqlite
|
db.sqlite
|
||||||
|
packages/adapter-supabase/supabase/.branches
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
coverage
|
coverage
|
||||||
|
|||||||
@@ -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`!)
|
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)
|
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!
|
That's it! 🎉 Others will be able to discover this provider much more easily now!
|
||||||
|
|
||||||
|
|||||||
@@ -48,4 +48,11 @@ EMAIL_FROM=user@gmail.com
|
|||||||
DATABASE_URL=
|
DATABASE_URL=
|
||||||
|
|
||||||
WIKIMEDIA_ID=
|
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
|
||||||
@@ -90,6 +90,12 @@ export default function Header() {
|
|||||||
<li className={styles.navItem}>
|
<li className={styles.navItem}>
|
||||||
<Link href="/middleware-protected">Middleware protected</Link>
|
<Link href="/middleware-protected">Middleware protected</Link>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -16,9 +16,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next-auth/fauna-adapter": "workspace:*",
|
"@next-auth/fauna-adapter": "workspace:*",
|
||||||
"@next-auth/prisma-adapter": "workspace:*",
|
"@next-auth/prisma-adapter": "workspace:*",
|
||||||
|
"@next-auth/supabase-adapter": "workspace:*",
|
||||||
"@next-auth/typeorm-legacy-adapter": "workspace:*",
|
"@next-auth/typeorm-legacy-adapter": "workspace:*",
|
||||||
"@prisma/client": "^3",
|
"@prisma/client": "^3",
|
||||||
|
"@supabase/supabase-js": "^2.0.5",
|
||||||
"faunadb": "^4",
|
"faunadb": "^4",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
"next": "13.0.2",
|
"next": "13.0.2",
|
||||||
"next-auth": "workspace:*",
|
"next-auth": "workspace:*",
|
||||||
"nodemailer": "^6",
|
"nodemailer": "^6",
|
||||||
@@ -26,6 +29,7 @@
|
|||||||
"react-dom": "^18"
|
"react-dom": "^18"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jsonwebtoken": "^8.5.5",
|
||||||
"@types/react": "^18.0.15",
|
"@types/react": "^18.0.15",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"fake-smtp-server": "^0.8.0",
|
"fake-smtp-server": "^0.8.0",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import NextAuth from "next-auth"
|
import NextAuth from "next-auth"
|
||||||
import type { NextAuthOptions } from "next-auth"
|
import type { NextAuthOptions } from "next-auth"
|
||||||
|
import jwt from "jsonwebtoken"
|
||||||
|
|
||||||
// Providers
|
// Providers
|
||||||
import Apple from "next-auth/providers/apple"
|
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 GitHub from "next-auth/providers/github"
|
||||||
import Gitlab from "next-auth/providers/gitlab"
|
import Gitlab from "next-auth/providers/gitlab"
|
||||||
import Google from "next-auth/providers/google"
|
import Google from "next-auth/providers/google"
|
||||||
import Hubspot from "next-auth/providers/hubspot"
|
|
||||||
import IDS4 from "next-auth/providers/identity-server4"
|
import IDS4 from "next-auth/providers/identity-server4"
|
||||||
import Instagram from "next-auth/providers/instagram"
|
import Instagram from "next-auth/providers/instagram"
|
||||||
import Keycloak from "next-auth/providers/keycloak"
|
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 Patreon from "next-auth/providers/patreon"
|
||||||
import Slack from "next-auth/providers/slack"
|
import Slack from "next-auth/providers/slack"
|
||||||
import Spotify from "next-auth/providers/spotify"
|
import Spotify from "next-auth/providers/spotify"
|
||||||
import Todoist from "next-auth/providers/todoist"
|
|
||||||
import Trakt from "next-auth/providers/trakt"
|
import Trakt from "next-auth/providers/trakt"
|
||||||
import Twitch from "next-auth/providers/twitch"
|
import Twitch from "next-auth/providers/twitch"
|
||||||
import Twitter, { TwitterLegacy } from "next-auth/providers/twitter"
|
import Twitter, { TwitterLegacy } from "next-auth/providers/twitter"
|
||||||
import Vk from "next-auth/providers/vk"
|
import Vk from "next-auth/providers/vk"
|
||||||
import Wikimedia from "next-auth/providers/wikimedia"
|
import Wikimedia from "next-auth/providers/wikimedia"
|
||||||
import WorkOS from "next-auth/providers/workos"
|
import WorkOS from "next-auth/providers/workos"
|
||||||
import Zitadel from "next-auth/providers/zitadel"
|
|
||||||
|
|
||||||
// Adapters
|
// 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
|
// Add an adapter you want to test here.
|
||||||
// import { PrismaClient } from "@prisma/client"
|
const adapters = {
|
||||||
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
prisma() {
|
||||||
// const client = globalThis.prisma || new PrismaClient()
|
const client = globalThis.prisma || new PrismaClient()
|
||||||
// if (process.env.NODE_ENV !== "production") globalThis.prisma = client
|
if (process.env.NODE_ENV !== "production") globalThis.prisma = client
|
||||||
// const adapter = PrismaAdapter(client)
|
return PrismaAdapter(client)
|
||||||
|
},
|
||||||
// // Fauna
|
typeorm() {
|
||||||
// import { Client as FaunaClient } from "faunadb"
|
return TypeORMLegacyAdapter({
|
||||||
// import { FaunaAdapter } from "@next-auth/fauna-adapter"
|
type: "sqlite",
|
||||||
// const opts = { secret: process.env.FAUNA_SECRET, domain: process.env.FAUNA_DOMAIN }
|
name: "next-auth-test-memory",
|
||||||
// const client = globalThis.fauna || new FaunaClient(opts)
|
database: "./typeorm/dev.db",
|
||||||
// if (process.env.NODE_ENV !== "production") globalThis.fauna = client
|
synchronize: true,
|
||||||
// const adapter = FaunaAdapter(client)
|
})
|
||||||
|
},
|
||||||
// // TypeORM
|
fauna() {
|
||||||
// import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
|
const client =
|
||||||
// const adapter = TypeORMLegacyAdapter({
|
globalThis.fauna ||
|
||||||
// type: "sqlite",
|
new FaunaClient({
|
||||||
// name: "next-auth-test-memory",
|
secret: process.env.FAUNA_SECRET,
|
||||||
// database: "./typeorm/dev.db",
|
domain: process.env.FAUNA_DOMAIN,
|
||||||
// synchronize: true,
|
})
|
||||||
// })
|
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 = {
|
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",
|
debug: process.env.NODE_ENV !== "production",
|
||||||
theme: {
|
theme: {
|
||||||
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
|
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 }),
|
GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),
|
||||||
Gitlab({ clientId: process.env.GITLAB_ID, clientSecret: process.env.GITLAB_SECRET }),
|
Gitlab({ clientId: process.env.GITLAB_ID, clientSecret: process.env.GITLAB_SECRET }),
|
||||||
Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_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 }),
|
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 }),
|
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 }),
|
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 }),
|
Patreon({ clientId: process.env.PATREON_ID, clientSecret: process.env.PATREON_SECRET }),
|
||||||
Slack({ clientId: process.env.SLACK_ID, clientSecret: process.env.SLACK_SECRET }),
|
Slack({ clientId: process.env.SLACK_ID, clientSecret: process.env.SLACK_SECRET }),
|
||||||
Spotify({ clientId: process.env.SPOTIFY_ID, clientSecret: process.env.SPOTIFY_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 }),
|
Trakt({ clientId: process.env.TRAKT_ID, clientSecret: process.env.TRAKT_SECRET }),
|
||||||
Twitch({ clientId: process.env.TWITCH_ID, clientSecret: process.env.TWITCH_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 }),
|
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 }),
|
Vk({ clientId: process.env.VK_ID, clientSecret: process.env.VK_SECRET }),
|
||||||
Wikimedia({ clientId: process.env.WIKIMEDIA_ID, clientSecret: process.env.WIKIMEDIA_SECRET }),
|
Wikimedia({ clientId: process.env.WIKIMEDIA_ID, clientSecret: process.env.WIKIMEDIA_SECRET }),
|
||||||
WorkOS({ clientId: process.env.WORKOS_ID, clientSecret: process.env.WORKOS_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 }),
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
apps/dev/pages/api/examples/supabase-rls.js
Normal file
30
apps/dev/pages/api/examples/supabase-rls.js
Normal 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))
|
||||||
|
}
|
||||||
49
apps/dev/pages/supabase-client-rls.js
Normal file
49
apps/dev/pages/supabase-client-rls.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
68
apps/dev/pages/supabase-ssr.js
Normal file
68
apps/dev/pages/supabase-ssr.js
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
2
apps/dev/types/nextauth.d.ts
vendored
2
apps/dev/types/nextauth.d.ts
vendored
@@ -6,6 +6,8 @@ declare module "next-auth" {
|
|||||||
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
||||||
*/
|
*/
|
||||||
interface Session {
|
interface Session {
|
||||||
|
// A JWT which can be used as Authorization header with supabase-js for RLS.
|
||||||
|
supabaseAccessToken?: string
|
||||||
user: {
|
user: {
|
||||||
/** The user's postal address. */
|
/** The user's postal address. */
|
||||||
address: string
|
address: string
|
||||||
|
|||||||
@@ -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
|
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
|
// This approach is taken from https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
|
||||||
import { MongoClient } from 'mongodb'
|
import { MongoClient } from 'mongodb'
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ We have a list of official adapters that are distributed as their own packages u
|
|||||||
- [`neo4j`](./neo4j)
|
- [`neo4j`](./neo4j)
|
||||||
- [`typeorm-legacy`](./typeorm)
|
- [`typeorm-legacy`](./typeorm)
|
||||||
- [`sequelize`](./sequelize)
|
- [`sequelize`](./sequelize)
|
||||||
|
- [`supabase`](./supabase)
|
||||||
- [`dgraph`](./dgraph)
|
- [`dgraph`](./dgraph)
|
||||||
- [`upstash-redis`](./upstash-redis)
|
- [`upstash-redis`](./upstash-redis)
|
||||||
|
|
||||||
|
|||||||
309
docs/docs/adapters/supabase.md
Normal file
309
docs/docs/adapters/supabase.md
Normal 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 you’re 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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -472,7 +472,8 @@ cookies: {
|
|||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
path: '/',
|
path: '/',
|
||||||
secure: useSecureCookies
|
secure: useSecureCookies,
|
||||||
|
maxAge: 900
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
@@ -482,6 +483,7 @@ cookies: {
|
|||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
path: "/",
|
path: "/",
|
||||||
secure: useSecureCookies,
|
secure: useSecureCookies,
|
||||||
|
maxAge: 900
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
nonce: {
|
nonce: {
|
||||||
|
|||||||
@@ -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:
|
Configuration is similar to other providers, but the options are different:
|
||||||
|
|
||||||
```js title="pages/api/auth/[...nextauth].js"
|
```js title="pages/api/auth/[...nextauth].js"
|
||||||
import EmailProvider from `next-auth/providers/email`
|
import EmailProvider from "next-auth/providers/email"
|
||||||
...
|
...
|
||||||
providers: [
|
providers: [
|
||||||
EmailProvider({
|
EmailProvider({
|
||||||
|
|||||||
@@ -174,6 +174,10 @@ interface OAuthConfig {
|
|||||||
issuer?: string
|
issuer?: string
|
||||||
client?: Partial<ClientMetadata>
|
client?: Partial<ClientMetadata>
|
||||||
allowDangerousEmailAccountLinking?: boolean
|
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:
|
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 />
|
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)
|
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)>)
|
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)>)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ http://developers.strava.com/docs/reference/
|
|||||||
|
|
||||||
The **Strava Provider** comes with a set of default options:
|
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.
|
You can override any of the options to suit your own use case. Ensure the redirect_uri configuration fits your needs accordingly.
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
More details can be found [here](https://next-auth.js.org/configuration/nextjs#middleware).
|
||||||
|
|
||||||
:::tip
|
:::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).
|
For other patterns check out the [Next.js Middleware documentation](https://nextjs.org/docs/advanced-features/middleware#matcher).
|
||||||
:::
|
:::
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ module.exports = {
|
|||||||
"adapters/neo4j",
|
"adapters/neo4j",
|
||||||
"adapters/typeorm",
|
"adapters/typeorm",
|
||||||
"adapters/sequelize",
|
"adapters/sequelize",
|
||||||
|
"adapters/supabase",
|
||||||
"adapters/mikro-orm",
|
"adapters/mikro-orm",
|
||||||
"adapters/dgraph",
|
"adapters/dgraph",
|
||||||
"adapters/upstash-redis",
|
"adapters/upstash-redis",
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ interface OAuthConfig {
|
|||||||
region?: string
|
region?: string
|
||||||
issuer?: string
|
issuer?: string
|
||||||
client?: Partial<ClientMetadata>
|
client?: Partial<ClientMetadata>
|
||||||
allowDangerousEmailAccountLinking?: boolean
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -254,6 +253,7 @@ providers: [
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Override default options
|
### Override default options
|
||||||
|
|
||||||
For built-in providers, in most cases you will only need to specify the `clientId` and `clientSecret`. If you need to override any of the defaults, add your own [options](#options).
|
For built-in providers, in most cases you will only need to specify the `clientId` and `clientSecret`. If you need to override any of the defaults, add your own [options](#options).
|
||||||
@@ -297,11 +297,10 @@ GoogleProvider({
|
|||||||
|
|
||||||
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily!
|
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily!
|
||||||
|
|
||||||
You only need to add three changes:
|
You only need to add two changes:
|
||||||
|
|
||||||
1. Add your config: [`src/providers/{provider}.ts`](https://github.com/nextauthjs/next-auth/tree/main/packages/next-auth/src/providers)<br />
|
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`
|
||||||
2. Add provider documentation: [`/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/docs/docs/providers)
|
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)>)
|
|
||||||
|
|
||||||
That's it! 🎉 Others will be able to discover and use this provider much more easily now!
|
That's it! 🎉 Others will be able to discover and use this provider much more easily now!
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-auth/firebase-adapter",
|
"name": "@next-auth/firebase-adapter",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"description": "Firebase adapter for next-auth.",
|
"description": "Firebase adapter for next-auth.",
|
||||||
"homepage": "https://next-auth.js.org",
|
"homepage": "https://next-auth.js.org",
|
||||||
"repository": "https://github.com/nextauthjs/next-auth",
|
"repository": "https://github.com/nextauthjs/next-auth",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"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": {
|
"peerDependencies": {
|
||||||
"firebase": "^9.7.0",
|
"firebase": "^9.7.0",
|
||||||
@@ -38,9 +38,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next-auth/adapter-test": "workspace:*",
|
"@next-auth/adapter-test": "workspace:*",
|
||||||
"@next-auth/tsconfig": "workspace:*",
|
"@next-auth/tsconfig": "workspace:*",
|
||||||
"firebase": "^9.7.0",
|
"firebase": "^9.14.0",
|
||||||
"firebase-tools": "^10.7.2",
|
"firebase-tools": "^11.16.1",
|
||||||
"jest": "^27.4.3",
|
"jest": "^27.4.3",
|
||||||
"next-auth": "workspace:*"
|
"next-auth": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,10 +86,10 @@ export function FirestoreAdapter({
|
|||||||
async getUserByEmail(email) {
|
async getUserByEmail(email) {
|
||||||
const userQuery = query(Users, where("email", "==", email), limit(1))
|
const userQuery = query(Users, where("email", "==", email), limit(1))
|
||||||
const userSnapshots = await getDocs(userQuery)
|
const userSnapshots = await getDocs(userQuery)
|
||||||
const userSnpashot = userSnapshots.docs[0]
|
const userSnapshot = userSnapshots.docs[0]
|
||||||
|
|
||||||
if (userSnpashot?.exists() && Users.converter) {
|
if (userSnapshot?.exists() && Users.converter) {
|
||||||
return Users.converter.fromFirestore(userSnpashot)
|
return Users.converter.fromFirestore(userSnapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@next-auth/tsconfig/tsconfig.base.json",
|
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-auth/pouchdb-adapter",
|
"name": "@next-auth/pouchdb-adapter",
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"description": "PouchDB adapter for next-auth.",
|
"description": "PouchDB adapter for next-auth.",
|
||||||
"homepage": "https://next-auth.js.org",
|
"homepage": "https://next-auth.js.org",
|
||||||
"repository": "https://github.com/nextauthjs/next-auth",
|
"repository": "https://github.com/nextauthjs/next-auth",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@next-auth/tsconfig/tsconfig.base.json",
|
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
|||||||
3
packages/adapter-supabase/.env.example
Normal file
3
packages/adapter-supabase/.env.example
Normal 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
|
||||||
57
packages/adapter-supabase/README.md
Normal file
57
packages/adapter-supabase/README.md
Normal 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
|
||||||
15
packages/adapter-supabase/logo.svg
Normal file
15
packages/adapter-supabase/logo.svg
Normal 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 |
40
packages/adapter-supabase/package.json
Normal file
40
packages/adapter-supabase/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
140
packages/adapter-supabase/src/database.types.ts
Normal file
140
packages/adapter-supabase/src/database.types.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
212
packages/adapter-supabase/src/index.ts
Normal file
212
packages/adapter-supabase/src/index.ts
Normal 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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
44
packages/adapter-supabase/supabase/config.toml
Normal file
44
packages/adapter-supabase/supabase/config.toml
Normal 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
|
||||||
@@ -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;
|
||||||
@@ -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();
|
||||||
76
packages/adapter-supabase/tests/index.test.ts
Normal file
76
packages/adapter-supabase/tests/index.test.ts
Normal 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)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
19
packages/adapter-supabase/tests/test.sh
Executable file
19
packages/adapter-supabase/tests/test.sh
Executable 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
|
||||||
8
packages/adapter-supabase/tsconfig.json
Normal file
8
packages/adapter-supabase/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"exclude": ["tests", "dist", "jest.config.js"]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-auth",
|
"name": "next-auth",
|
||||||
"version": "4.16.4",
|
"version": "4.18.0",
|
||||||
"description": "Authentication for Next.js",
|
"description": "Authentication for Next.js",
|
||||||
"homepage": "https://next-auth.js.org",
|
"homepage": "https://next-auth.js.org",
|
||||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ async function getBody(req: Request): Promise<Record<string, any> | undefined> {
|
|||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
async function toInternalRequest(
|
async function toInternalRequest(
|
||||||
req: RequestInternal | Request
|
req: RequestInternal | Request,
|
||||||
|
trustHost: boolean = false
|
||||||
): Promise<RequestInternal> {
|
): Promise<RequestInternal> {
|
||||||
if (req instanceof Request) {
|
if (req instanceof Request) {
|
||||||
const url = new URL(req.url)
|
const url = new URL(req.url)
|
||||||
@@ -70,7 +71,11 @@ async function toInternalRequest(
|
|||||||
cookies: parseCookie(req.headers.get("cookie") ?? ""),
|
cookies: parseCookie(req.headers.get("cookie") ?? ""),
|
||||||
providerId: nextauth[1],
|
providerId: nextauth[1],
|
||||||
error: url.searchParams.get("error") ?? 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,
|
query,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +87,7 @@ export async function NextAuthHandler<
|
|||||||
>(params: NextAuthHandlerParams): Promise<OutgoingResponse<Body>> {
|
>(params: NextAuthHandlerParams): Promise<OutgoingResponse<Body>> {
|
||||||
const { options: userOptions, req: incomingRequest } = params
|
const { options: userOptions, req: incomingRequest } = params
|
||||||
|
|
||||||
const req = await toInternalRequest(incomingRequest)
|
const req = await toInternalRequest(incomingRequest, userOptions.trustHost)
|
||||||
|
|
||||||
setLogger(userOptions.logger, userOptions.debug)
|
setLogger(userOptions.logger, userOptions.debug)
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
|
|||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
path: "/",
|
path: "/",
|
||||||
secure: useSecureCookies,
|
secure: useSecureCookies,
|
||||||
|
maxAge: 60 * 15, // 15 minutes in seconds
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
@@ -102,6 +103,7 @@ export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
|
|||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
path: "/",
|
path: "/",
|
||||||
secure: useSecureCookies,
|
secure: useSecureCookies,
|
||||||
|
maxAge: 60 * 15, // 15 minutes in seconds
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
nonce: {
|
nonce: {
|
||||||
|
|||||||
@@ -26,13 +26,15 @@ export async function createPKCE(options: InternalOptions<"oauth">): Promise<
|
|||||||
const code_verifier = generators.codeVerifier()
|
const code_verifier = generators.codeVerifier()
|
||||||
const code_challenge = generators.codeChallenge(code_verifier)
|
const code_challenge = generators.codeChallenge(code_verifier)
|
||||||
|
|
||||||
|
const maxAge = cookies.pkceCodeVerifier.options.maxAge ?? PKCE_MAX_AGE
|
||||||
|
|
||||||
const expires = new Date()
|
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
|
// Encrypt code_verifier and save it to an encrypted cookie
|
||||||
const encryptedCodeVerifier = await jwt.encode({
|
const encryptedCodeVerifier = await jwt.encode({
|
||||||
...options.jwt,
|
...options.jwt,
|
||||||
maxAge: PKCE_MAX_AGE,
|
maxAge,
|
||||||
token: { code_verifier },
|
token: { code_verifier },
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ export async function createPKCE(options: InternalOptions<"oauth">): Promise<
|
|||||||
code_challenge,
|
code_challenge,
|
||||||
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
|
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
|
||||||
code_verifier,
|
code_verifier,
|
||||||
PKCE_MAX_AGE,
|
maxAge,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -17,17 +17,18 @@ export async function createState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const state = generators.state()
|
const state = generators.state()
|
||||||
|
const maxAge = cookies.state.options.maxAge ?? STATE_MAX_AGE
|
||||||
|
|
||||||
const encodedState = await jwt.encode({
|
const encodedState = await jwt.encode({
|
||||||
...jwt,
|
...jwt,
|
||||||
maxAge: STATE_MAX_AGE,
|
maxAge,
|
||||||
token: { state },
|
token: { state },
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.debug("CREATE_STATE", { state, maxAge: STATE_MAX_AGE })
|
logger.debug("CREATE_STATE", { state, maxAge })
|
||||||
|
|
||||||
const expires = new Date()
|
const expires = new Date()
|
||||||
expires.setTime(expires.getTime() + STATE_MAX_AGE * 1000)
|
expires.setTime(expires.getTime() + maxAge * 1000)
|
||||||
return {
|
return {
|
||||||
value: state,
|
value: state,
|
||||||
cookie: {
|
cookie: {
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ export interface NextAuthOptions {
|
|||||||
providers: Provider[]
|
providers: Provider[]
|
||||||
/**
|
/**
|
||||||
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
|
* 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.
|
* 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.
|
* 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)
|
* * **Default value**: `string` (SHA hash of the "options" object)
|
||||||
* * **Required**: No - **but strongly recommended**!
|
* * **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)
|
* [Documentation](https://next-auth.js.org/configuration/options#cookies) | [Usage example](https://next-auth.js.org/configuration/options#example)
|
||||||
*/
|
*/
|
||||||
cookies?: Partial<CookiesOptions>
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
} from "next"
|
} from "next"
|
||||||
import type { NextAuthOptions, Session } from ".."
|
import type { NextAuthOptions, Session } from ".."
|
||||||
import type {
|
import type {
|
||||||
|
CallbacksOptions,
|
||||||
NextAuthAction,
|
NextAuthAction,
|
||||||
NextAuthRequest,
|
NextAuthRequest,
|
||||||
NextAuthResponse,
|
NextAuthResponse,
|
||||||
@@ -21,12 +22,17 @@ async function NextAuthNextHandler(
|
|||||||
) {
|
) {
|
||||||
const { nextauth, ...query } = req.query
|
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({
|
const handler = await NextAuthHandler({
|
||||||
req: {
|
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,
|
body: req.body,
|
||||||
query,
|
query,
|
||||||
cookies: req.cookies,
|
cookies: req.cookies,
|
||||||
@@ -85,17 +91,25 @@ export default NextAuth
|
|||||||
|
|
||||||
let experimentalWarningShown = false
|
let experimentalWarningShown = false
|
||||||
let experimentalRSCWarningShown = 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:
|
...args:
|
||||||
| [
|
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"], O]
|
||||||
GetServerSidePropsContext["req"],
|
| [NextApiRequest, NextApiResponse, O]
|
||||||
GetServerSidePropsContext["res"],
|
| [O]
|
||||||
NextAuthOptions
|
|
||||||
]
|
|
||||||
| [NextApiRequest, NextApiResponse, NextAuthOptions]
|
|
||||||
| [NextAuthOptions]
|
|
||||||
| []
|
| []
|
||||||
): Promise<Session | null> {
|
): Promise<R | null> {
|
||||||
if (!experimentalWarningShown && process.env.NODE_ENV !== "production") {
|
if (!experimentalWarningShown && process.env.NODE_ENV !== "production") {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[next-auth][warn][EXPERIMENTAL_API]",
|
"[next-auth][warn][EXPERIMENTAL_API]",
|
||||||
@@ -123,7 +137,8 @@ export async function unstable_getServerSession(
|
|||||||
|
|
||||||
let req, res, options: NextAuthOptions
|
let req, res, options: NextAuthOptions
|
||||||
if (isRSC) {
|
if (isRSC) {
|
||||||
options = args[0] ?? { providers: [] }
|
options = Object.assign({}, args[0], { providers: [] })
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const { headers, cookies } = require("next/headers")
|
const { headers, cookies } = require("next/headers")
|
||||||
req = {
|
req = {
|
||||||
@@ -138,15 +153,21 @@ export async function unstable_getServerSession(
|
|||||||
} else {
|
} else {
|
||||||
req = args[0]
|
req = args[0]
|
||||||
res = args[1]
|
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>({
|
const session = await NextAuthHandler<Session | {} | string>({
|
||||||
options,
|
options,
|
||||||
req: {
|
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",
|
action: "session",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
cookies: req.cookies,
|
cookies: req.cookies,
|
||||||
@@ -162,7 +183,7 @@ export async function unstable_getServerSession(
|
|||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
if (isRSC) delete body.expires
|
if (isRSC) delete body.expires
|
||||||
return body as Session
|
return body as R
|
||||||
}
|
}
|
||||||
throw new Error((body as any).message)
|
throw new Error((body as any).message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { NextResponse, NextRequest } from "next/server"
|
|||||||
|
|
||||||
import { getToken } from "../jwt"
|
import { getToken } from "../jwt"
|
||||||
import parseUrl from "../utils/parse-url"
|
import parseUrl from "../utils/parse-url"
|
||||||
|
import { detectHost } from "../utils/detect-host"
|
||||||
|
|
||||||
type AuthorizedCallback = (params: {
|
type AuthorizedCallback = (params: {
|
||||||
token: JWT | null
|
token: JWT | null
|
||||||
@@ -89,7 +90,17 @@ export interface NextAuthMiddlewareOptions {
|
|||||||
* The same `secret` used in the `NextAuth` configuration.
|
* The same `secret` used in the `NextAuth` configuration.
|
||||||
* Defaults to the `NEXTAUTH_SECRET` environment variable.
|
* 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`
|
// TODO: `NextMiddleware` should allow returning `void`
|
||||||
@@ -98,14 +109,25 @@ type NextMiddlewareResult = ReturnType<NextMiddleware> | void // eslint-disable-
|
|||||||
|
|
||||||
async function handleMiddleware(
|
async function handleMiddleware(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
options: NextAuthMiddlewareOptions | undefined,
|
options: NextAuthMiddlewareOptions | undefined = {},
|
||||||
onSuccess?: (token: JWT | null) => Promise<NextMiddlewareResult>
|
onSuccess?: (token: JWT | null) => Promise<NextMiddlewareResult>
|
||||||
) {
|
) {
|
||||||
const { pathname, search, origin, basePath } = req.nextUrl
|
const { pathname, search, origin, basePath } = req.nextUrl
|
||||||
|
|
||||||
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
|
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
|
||||||
const errorPage = options?.pages?.error ?? "/api/auth/error"
|
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"]
|
const publicPaths = ["/_next", "/favicon.ico"]
|
||||||
|
|
||||||
// Avoid infinite redirects/invalid response
|
// Avoid infinite redirects/invalid response
|
||||||
@@ -146,7 +168,10 @@ async function handleMiddleware(
|
|||||||
|
|
||||||
// the user is not logged in, redirect to the sign-in page
|
// the user is not logged in, redirect to the sign-in page
|
||||||
const signInUrl = new URL(`${basePath}${signInPage}`, origin)
|
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)
|
return NextResponse.redirect(signInUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default function Naver<P extends NaverProfile>(
|
|||||||
profile(profile) {
|
profile(profile) {
|
||||||
return {
|
return {
|
||||||
id: profile.response.id,
|
id: profile.response.id,
|
||||||
name: profile.response.name,
|
name: profile.response.nickname,
|
||||||
email: profile.response.email,
|
email: profile.response.email,
|
||||||
image: profile.response.profile_image,
|
image: profile.response.profile_image,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
/** Extract the host from the environment */
|
/** Extract the host from the environment */
|
||||||
export function detectHost(forwardedHost: any) {
|
export function detectHost(
|
||||||
// If we detect a Vercel environment, we can trust the host
|
trusted: boolean,
|
||||||
if (process.env.VERCEL ?? process.env.AUTH_TRUST_HOST)
|
forwardedValue: string | string[] | undefined | null,
|
||||||
return forwardedHost
|
defaultValue: string | false
|
||||||
// If `NEXTAUTH_URL` is `undefined` we fall back to "http://localhost:3000"
|
): string | undefined {
|
||||||
return process.env.NEXTAUTH_URL
|
if (trusted && forwardedValue) {
|
||||||
|
return Array.isArray(forwardedValue) ? forwardedValue[0] : forwardedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue || undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
import { NextMiddleware } from "next/server"
|
import { NextMiddleware, NextRequest } from "next/server"
|
||||||
import { NextAuthMiddlewareOptions, withAuth } from "../src/next/middleware"
|
import { NextAuthMiddlewareOptions, withAuth } from "../src/next/middleware"
|
||||||
|
|
||||||
it("should not match pages as public paths", async () => {
|
it("should not match pages as public paths", async () => {
|
||||||
const options: NextAuthMiddlewareOptions = {
|
const options: NextAuthMiddlewareOptions = {
|
||||||
pages: {
|
pages: { signIn: "/", error: "/" },
|
||||||
signIn: "/",
|
|
||||||
error: "/",
|
|
||||||
},
|
|
||||||
secret: "secret",
|
secret: "secret",
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextUrl: any = {
|
const req = new NextRequest("http://127.0.0.1/protected/pathA", {
|
||||||
pathname: "/protected/pathA",
|
headers: { authorization: "" },
|
||||||
search: "",
|
})
|
||||||
origin: "http://127.0.0.1",
|
|
||||||
}
|
|
||||||
const req: any = { nextUrl, headers: { authorization: "" } }
|
|
||||||
|
|
||||||
const handleMiddleware = withAuth(options) as NextMiddleware
|
const handleMiddleware = withAuth(options) as NextMiddleware
|
||||||
const res = await handleMiddleware(req, null as any)
|
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 () => {
|
it("should not redirect on public paths", async () => {
|
||||||
const options: NextAuthMiddlewareOptions = {
|
const options: NextAuthMiddlewareOptions = { secret: "secret" }
|
||||||
secret: "secret",
|
|
||||||
}
|
const req = new NextRequest("http://127.0.0.1/_next/foo", {
|
||||||
const nextUrl: any = {
|
headers: { authorization: "" },
|
||||||
pathname: "/_next/foo",
|
})
|
||||||
search: "",
|
|
||||||
origin: "http://127.0.0.1",
|
|
||||||
}
|
|
||||||
const req: any = { nextUrl, headers: { authorization: "" } }
|
|
||||||
|
|
||||||
const handleMiddleware = withAuth(options) as NextMiddleware
|
const handleMiddleware = withAuth(options) as NextMiddleware
|
||||||
const res = await handleMiddleware(req, null as any)
|
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 () => {
|
it("should redirect according to nextUrl basePath", async () => {
|
||||||
const options: NextAuthMiddlewareOptions = {
|
const options: NextAuthMiddlewareOptions = { secret: "secret" }
|
||||||
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 handleMiddleware = withAuth(options) as NextMiddleware
|
const req = {
|
||||||
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({
|
|
||||||
nextUrl: {
|
nextUrl: {
|
||||||
pathname: "/protected/pathA",
|
pathname: "/protected/pathA",
|
||||||
search: "",
|
search: "",
|
||||||
origin: "http://127.0.0.1",
|
origin: "http://127.0.0.1",
|
||||||
basePath: "/custom-base-path"
|
basePath: "/custom-base-path",
|
||||||
}, headers: { authorization: "" }
|
},
|
||||||
} as any, null as any)
|
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
|
// then
|
||||||
expect(res).toBeDefined()
|
expect(res).toBeDefined()
|
||||||
expect(res.status).toEqual(307)
|
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?.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 req2 = {
|
||||||
const resFromRedirectedUrl = await handleMiddleware({
|
|
||||||
nextUrl: {
|
nextUrl: {
|
||||||
pathname: "/api/auth/signin",
|
pathname: "/api/auth/signin",
|
||||||
search: "callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA",
|
search: "callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA",
|
||||||
origin: "http://127.0.0.1",
|
origin: "http://127.0.0.1",
|
||||||
basePath: "/custom-base-path"
|
basePath: "/custom-base-path",
|
||||||
}, headers: { authorization: "" }
|
},
|
||||||
} as any, null as any)
|
headers: new Headers({ authorization: "" }),
|
||||||
|
}
|
||||||
|
// and when follow redirect
|
||||||
|
const resFromRedirectedUrl = await handleMiddleware(
|
||||||
|
req2 as NextRequest,
|
||||||
|
null as any
|
||||||
|
)
|
||||||
|
|
||||||
// then return sign in page
|
// then return sign in page
|
||||||
expect(resFromRedirectedUrl).toBeUndefined()
|
expect(resFromRedirectedUrl).toBeUndefined()
|
||||||
|
|||||||
139
packages/next-auth/tests/pkce-handler.test.ts
Normal file
139
packages/next-auth/tests/pkce-handler.test.ts
Normal 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())
|
||||||
|
}
|
||||||
134
packages/next-auth/tests/state-handler.test.ts
Normal file
134
packages/next-auth/tests/state-handler.test.ts
Normal 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
4913
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user