Compare commits

..

76 Commits

Author SHA1 Message Date
GitHub Actions
5d1c35e8aa chore(release): bump package version(s) [skip ci] 2023-02-24 14:20:38 +00:00
James Birtles
a77f557cbf fix: claims being serialized multiple times over (#6781) 2023-02-23 15:02:13 +01:00
Thang Vu
657492d921 docs: Improve Getting Started (#6551)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Lluis Agusti <hi@llu.lu>
2023-02-23 14:55:14 +01:00
Will
d1d63bddba chore: Add 🥈 Silver Financial Sponsors (#6757)
Add Silver Sponsors to the README
2023-02-23 11:32:48 +07:00
Will
99ac4899b5 feat(providers): add Beyond Identity Provider (#6013)
* Add Beyond Identity Provider

* Add Beyond Identity OIDC Provider

* Add Beyond Identity OIDC Provider

* Add "pkce" support

* Mirror Auth0 instead of Okta

* Code Review feedback
2023-02-23 11:04:21 +07:00
Balázs Orbán
2130765a57 docs: remove duplicate from sidebar 2023-02-19 17:13:06 +01:00
Balázs Orbán
4079274aaf fix(providers): document Azure AD B2C 2023-02-19 17:12:54 +01:00
OrJDev
3d7985fd6d fix: multiple set-cookie headers (#6749)
* fix: multiple set-cookie headers

* fx
2023-02-17 19:08:41 +01:00
Thang Vu
331e0ce51e fix(adapters): comply to Node.js native ESM resolver (#6753)
* fix(adapters): comply to Node.js native ESM resolver for Firebase adapter

* fix import

* use single file

---------

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2023-02-17 14:39:06 +00:00
Balázs Orbán
8af666a4cc fix: correct package name 2023-02-17 13:05:40 +01:00
Jakob Norlin
fed0a67917 fix: compatibility with edge runtimes (#6739)
* Bump @panva/hkdf to v1.0.3

Fixes #6736

* Update to v1.0.4

v.1.0.3 failed to publish it seems

* update lockfile

---------

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2023-02-17 11:24:17 +00:00
Erik André Jakobsen
0332d86942 docs: fix dead external prisma link (#6721)
A very minor change, but thought I would fix it when I saw it :)
2023-02-17 11:15:49 +00:00
Balázs Orbán
8b38d32430 docs: update refresh token guide
closes #6696
2023-02-13 13:44:40 +00:00
Balázs Orbán
1e5f840a26 chore: fix import
fixes #6699
2023-02-13 14:31:22 +01:00
Lioness100
5981712681 fix: address typos (#6701)
* docs: fix typos

* revert: typo fixes in packages/next-auth
2023-02-13 12:51:54 +00:00
Skyf0l
dd2b85c6a5 chore: add missing angle bracket in package.json contributors (#6698) 2023-02-12 21:01:56 +07:00
Balázs Orbán
3c0475acae fix(ts): fix typo in JSDoc 2023-02-10 02:18:08 +01:00
Balázs Orbán
416881c4c9 Merge branch 'main' of github.com:nextauthjs/next-auth 2023-02-10 02:10:30 +01:00
Balázs Orbán
b91167091c fix(ts): use default exports for all providers uniformly
closes #6615
2023-02-10 02:10:27 +01:00
JT A
497dacff41 docs: fix code example nesting (#6666)
While doing the Getting Started guide, we noticed that the configuration was incorrect.

cc: @obrowner-rdc
2023-02-10 01:51:29 +01:00
JT A
2a1e1d1cd2 docs: use correct env variable names (#6668)
The documentation had the incorrect environment variable names used in the configuration for `Email` provider.
2023-02-10 01:50:00 +01:00
Balázs Orbán
00f65b3476 docs: fix indent 2023-02-10 01:47:55 +01:00
Pavlos Vinieratos
470e55f8db docs: fix typo (#6674)
Update 02-oauth-tutorial.mdx
2023-02-10 01:42:43 +01:00
Balázs Orbán
d722962206 fix(ts): correct credentials types in authorize callback (#6648)
* fix(ts): correct `credentials` types in `authorize` callback

* Apply suggestions from code review

Co-authored-by: Thang Vu <hi@thvu.dev>

---------

Co-authored-by: Thang Vu <hi@thvu.dev>
2023-02-09 13:05:56 +01:00
AlandSleman
cee1bddbd5 docs: Fix Getting started page (#6636)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2023-02-07 15:08:29 +01:00
Rémi Robichet
e0ae913e5c docs: fix type import (#6638) 2023-02-07 15:06:48 +01:00
Thang Vu
1db27fcd07 chore(monorepo): cache docs/build 2023-02-06 10:03:32 +07:00
Thang Vu
95407df289 chore(monorepo): cache typedoc generated files 2023-02-06 09:54:36 +07:00
Balázs Orbán
22adc2eb3c docs: fix redirects 2023-02-06 00:04:44 +01:00
Balázs Orbán
725f976b39 docs: fix redirects 2023-02-06 00:03:28 +01:00
MatyiFKBT
28ae5d4639 fix(providers): update Foursquare API version (#6620) 2023-02-05 15:30:57 +01:00
GitHub Actions
0f4d43e9ad chore(release): bump package version(s) [skip ci] 2023-02-05 13:52:31 +00:00
Thang Vu
28583b8ab0 feat: drop crypto dependency, convert to ESM (#6603)
Co-authored-by: Balázs Orbán <info@balazsorban.com>

BREAKING CHANGE:
- This package now only ships ESM, as all maintained Node.js versions have native support
- Dropped the `crypto` Node.js import in favor of `uuid`. When `globalThis.crypto` is the default in the future, we can remove `uuid` again
2023-02-05 14:44:33 +01:00
Balázs Orbán
1e7538a955 fix: publish .d.ts.map, add experimental warning 2023-02-05 14:42:07 +01:00
Wyatt Ades
4258857e52 feat(adapters): move to firebase-admin in Firebase Adapter (#6225)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
fixes https://github.com/nextauthjs/next-auth/issues/5049
closes https://github.com/nextauthjs/next-auth/pull/6230
closes https://github.com/nextauthjs/next-auth/pull/5449
closes https://github.com/nextauthjs/next-auth/pull/5055
fixes https://github.com/nextauthjs/next-auth/issues/4927

BREAKING CHANGE:
The adapter now expects `firebase-admin` instead of the `firebase` package and also supports either passing a config object or a firestore instance.
2023-02-05 14:41:20 +01:00
Balázs Orbán
e9d8805609 docs: api reference restructure (#6608) 2023-02-04 15:39:12 +01:00
Mayvis
fb43c5da05 docs: enhance prisma mongodb doc to prevent warning (#6598)
Fixes https://github.com/nextauthjs/next-auth/issues/6597
2023-02-02 13:28:17 +01:00
Corey Jepperson
326eadf0ed fix: don't add /error to url pathname when email verification is successful (#6492)
fix handleAuthorized making bad pth when authorize

Co-authored-by: Corey Jepperson <corey@entropy.cc>
Co-authored-by: Thang Vu <hi@thvu.dev>
2023-01-31 17:37:38 +07:00
Thang Vu
a5e0db4bb3 feat(providers): add Notion provider (#6567)
* add notion provider along with logo and styles
"

* adjust notion documentation

* update issue template with Notion provider

* update docs and provider with code from TomYeoman

* feat: move Notion provider to core

* get it working

---------

Co-authored-by: Harrison Broadbent <harrisonbroadbent@gmail.com>
Co-authored-by: Harrison Broadbent <harrisonbroadbent@Harrisons-MacBook-Air.local>
Co-authored-by: Thang Vu <hi@thvu.dev>
2023-01-31 17:10:47 +07:00
Balázs Orbán
334e23343a chore: fix typo 2023-01-31 01:19:16 +01:00
OrJDev
be046a6cb2 chore: suggest using the correct command to seek for packages (#6570)
* fix: suggest using the correct command to seek for packages

* seek for adapter packages aswell

* cleanup: seek for auth org packages
2023-01-30 20:39:37 +01:00
Frank Dumont
bdee262abe fix: typo in log message (#6569) 2023-01-30 20:37:53 +01:00
Balázs Orbán
3f89e668ec fix(ts): mark options provider config option internal (#6564)
* chore(dev): use workspace modules in Svelte app

* fix(ts): mark `options` provider config option internal
2023-01-30 12:34:54 +00:00
Balázs Orbán
533320eb94 chore: generate oauth-types on build 2023-01-29 14:33:41 +01:00
James
dfe6509472 fix: comment Discord profile, fix @auth/sveltekit build on Windows (#6550)
* fix: discord types were inaccurate

* fix: build on windows computers

* Apply review suggestions

* Update discord.ts

---------

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2023-01-29 12:05:14 +00:00
Oskar
1bde7cc8df fix(core): correct docs link (#6446) 2023-01-26 12:16:47 +00:00
Balázs Orbán
cef05d5e2d Merge branch 'main' of github.com:nextauthjs/next-auth 2023-01-26 13:11:14 +01:00
Balázs Orbán
c0dea283ba fix(core): avoid circular dependency
Fixes #6508
2023-01-26 13:11:04 +01:00
Balázs Orbán
0204766e0f fix(core): don't lock nodemailer version as peer dependency 2023-01-25 14:29:16 +00:00
Jérémie Sellam
a336ba762c fix(adapters): allow already initialized firebase app 🐛 (#6230)
* 🐛 Fix already initialized firebase app

* remove comment

Co-authored-by: Jérémie Sellam <jeremie@southpigalle.io>
Co-authored-by: Thang Vu <hi@thvu.dev>
2023-01-25 19:57:52 +07:00
Balázs Orbán
681d53c2f8 chore(core): cleanup 2023-01-24 14:01:40 +01:00
Thang Vu
06e891c0ea chore: cache output for @auth/sveltekit 2023-01-24 15:22:42 +07:00
GitHub Actions
b9a84350b5 chore(release): bump package version(s) [skip ci] 2023-01-24 02:02:00 +00:00
Balázs Orbán
44c38247da chore: trigger CI 2023-01-24 02:58:49 +01:00
Balázs Orbán
9b9af4d5e5 chore: bump versions [skip ci] 2023-01-24 02:56:31 +01:00
Balázs Orbán
fd2179bdca Merge branch 'main' of github.com:nextauthjs/next-auth 2023-01-24 02:42:16 +01:00
Balázs Orbán
7bb037bb9d chore: temp. disable E2E tests 2023-01-24 02:42:13 +01:00
Robin Panta
52f70e9f4f docs: update "guide deep-dive" link (#6473)
Fixes https://github.com/nextauthjs/next-auth/issues/6466
2023-01-24 02:34:45 +01:00
Balázs Orbán
505f69b519 chore: fix pipeline 2023-01-24 02:29:10 +01:00
Balázs Orbán
b21709db40 chore: update lock file 2023-01-24 02:26:49 +01:00
Balázs Orbán
aff7b37ef9 Merge branch 'main' of github.com:nextauthjs/next-auth 2023-01-24 02:26:00 +01:00
Balázs Orbán
daa85be1ad Revert "chore(next-auth): remove engines restriction (#6428)"
This reverts commit 035836da98.
2023-01-24 02:25:45 +01:00
Balázs Orbán
c31718ca10 fix(core): sign cookies with built-in jwt methods (#6488) 2023-01-24 02:21:56 +01:00
Balázs Orbán
fbcfedf0e8 fix(providers): default image to null for Azure AD 2023-01-24 02:21:27 +01:00
Balázs Orbán
bd032335eb docs: rename file 2023-01-23 13:41:21 +01:00
Balázs Orbán
128e0f3a10 docs: update RBAC guide 2023-01-23 13:40:45 +01:00
Thang Vu
557fb9d741 chore: ignore e2e actions in forks 2023-01-23 12:28:48 +07:00
Thang Vu
b4d6ed5f5f feat(providers): add Asgardeo provider (#6452)
* implement asgardeo auth provider

* Import asgardeo provider in to providers

* Improve provider configuration

* simplify and improve the asgardeo provider

* Delete package-lock.json

* converted server origin to organization

* revamp provider configs

* update profile interface

* Remove asgardeo issuer parameter and add docs

* fixed docs

* Update asgardeo.md

* Update docs and provider branding

* Remove mistakenly added code from dev app

* move to core

* Delete asgardeo.md

Co-authored-by: Yathindra <yathindrarawya123@gmail.com>
Co-authored-by: Yathindra Kodithuwakku <32919513+yathindrakodithuwakku@users.noreply.github.com>
Co-authored-by: Yathindra Kodithuwakku <32919513+yathindrak@users.noreply.github.com>
2023-01-22 01:30:42 +07:00
Richard Shin
035836da98 chore(next-auth): remove engines restriction (#6428)
* fix: add node 19 as compatible engine

* remove engines restriction

Co-authored-by: Thang Vu <hi@thvu.dev>
2023-01-21 16:29:45 +07:00
Atila Fassina
294039a497 docs(xata-adapter): adjust json schema (#6440) 2023-01-20 11:17:00 +00:00
Balázs Orbán
b2450ef625 fix(providers): conform Twitch provider to spec with escape hatch (#6365)
* fix(core): add explicit non-conform escape hatch

* fix(core): default to first supported auth method

* fix(core): stringify `claims` authorization url param

* fix(providers): conform Twitch provider to spec with escape hatch

* configure `client_secret_post` explicitly in provider
2023-01-19 10:28:14 +00:00
Balázs Orbán
a81bb3e51e feat(core): option to opt out of CSRF checks (#6379)
* feat(core): add way to opt-out of CSRF checks

* fix logic

* add warning if CSRF endpoint used when skipped
2023-01-19 10:27:18 +00:00
Robin Panta
bb506f7eb9 docs: Fix token expiry comparision in database strategy (#6430)
* Fix token expiry comparision in database strategy

fixes the condition used for example
in database strategy

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2023-01-18 15:26:23 +00:00
Thang Vu
87d9cc4244 feat: e2e tests (#6380)
* feat: e2e test init

* run e2e test on CI

* Add credentials to ci

* Update pnpm-lock.yaml

* move test to dev

* add dotenv

* remove in examples

* add e2e command

* revert

* add output cache for turbo e2e

* correct path for upload artifact

* Update release.yml
2023-01-18 19:43:50 +07:00
uatemycookie22
d2e3b76031 docs: Update 02-oauth-tutorial.mdx (#6408)
Fix typos in 02-oath-tutorial.mdx
2023-01-17 00:24:48 +01:00
Jan-David-Black
c36834b3b0 docs: Updating to _app.tsx (#6398)
file should be called `_app.tsx` instead of `_app.ts`
2023-01-17 00:24:04 +01:00
127 changed files with 4726 additions and 4725 deletions

View File

@@ -23,8 +23,8 @@ pnpm-lock.yaml
.docusaurus
build
docs/docs/reference/03-core
docs/docs/reference/04-sveltekit
docs/docs/reference/core
docs/docs/reference/sveltekit
static
# --------------- Packages ---------------

View File

@@ -30,7 +30,7 @@ body:
Run this command in your project's root folder and paste the result:
```sh
npx envinfo --system --binaries --browsers --npmPackages "next,react,next-auth"
npx envinfo --system --binaries --browsers --npmPackages "next,react,next-auth,@auth/*"
```
Alternatively, you can manually gather the version information from your package.json for these packages: "next", "react" and "next-auth". Please also mention your OS and Node.js version, as well as the browser you are using.
validations:

View File

@@ -25,12 +25,14 @@ body:
- "Custom provider"
- "42 School"
- "Apple"
- "Asgardeo"
- "Atlassian"
- "Auth0"
- "Authentik"
- "Azure Active Directory"
- "Azure Active Directory B2C"
- "Battlenet"
- "Beyond Identity"
- "Box"
- "Bungie"
- "Cognito"
@@ -57,6 +59,7 @@ body:
- "Medium"
- "Naver"
- "Netlify"
- "Notion"
- "Okta"
- "OneLogin"
- "Osso"
@@ -87,7 +90,7 @@ body:
Run this command in your project's root folder and paste the result:
```sh
npx envinfo --system --binaries --browsers --npmPackages "next,react,next-auth"
npx envinfo --system --binaries --browsers --npmPackages "next,react,next-auth,@auth/*"
```
Alternatively, you can manually gather the version information from your package.json for these packages: "next", "react" and "next-auth". Please also mention your OS and Node.js version, as well as the browser you are using.
validations:

View File

@@ -44,7 +44,7 @@ body:
Run this command in your project's root folder and paste the result:
```sh
npx envinfo --system --binaries --browsers --npmPackages "next,react,next-auth" && npx envinfo --npmPackages "@next-auth/*"
npx envinfo --system --binaries --browsers --npmPackages "next,react,next-auth,@auth/*" && npx envinfo --npmPackages "@next-auth/*"
```
Alternatively, if the above command did not work, we need the version of the following packages from your package.json: "next", "react", "next-auth" and your adapter. Please also mention your OS and Node.js version, as well as the browser you are using.
validations:

View File

@@ -35,6 +35,22 @@ jobs:
UPSTASH_REDIS_KEY: ${{ secrets.UPSTASH_REDIS_KEY }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
# - name: Run E2E tests
# if: github.repository == 'nextauthjs/next-auth'
# run: pnpm e2e
# timeout-minutes: 15
# env:
# AUTH0_USERNAME: ${{ secrets.AUTH0_USERNAME }}
# AUTH0_PASSWORD: ${{ secrets.AUTH0_PASSWORD }}
# TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
# TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
# - name: Upload E2E artifacts
# if: github.repository == 'nextauthjs/next-auth'
# uses: actions/upload-artifact@v3
# with:
# name: playwright-report
# path: apps/dev/nextjs/playwright-report/
# retention-days: 30
# - name: Coverage
# uses: codecov/codecov-action@v1
# with:

22
.gitignore vendored
View File

@@ -12,6 +12,7 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log
ui-debug.log
.pnpm-debug.log
@@ -34,13 +35,10 @@ packages/next-auth/utils
packages/next-auth/core
packages/next-auth/jwt
packages/next-auth/react
packages/next-auth/adapters.d.ts
packages/next-auth/adapters.js
packages/next-auth/index.d.ts
packages/next-auth/index.js
packages/next-auth/next
packages/next-auth/middleware.d.ts
packages/next-auth/middleware.js
packages/*/*.js
packages/*/*.d.ts
packages/*/*.d.ts.map
# Development app
apps/dev/src/css
@@ -81,14 +79,12 @@ docs/.docusaurus
docs/providers.json
# Core
packages/core/*.js
packages/core/*.d.ts
packages/core/*.d.ts.map
packages/core/src/providers/oauth-types.ts
packages/core/lib
packages/core/providers
packages/core/src/lib/pages/styles.ts
docs/docs/reference/03-core
docs/docs/reference/04-sveltekit
docs/docs/reference/core
docs/docs/reference/sveltekit
# SvelteKit
@@ -98,3 +94,7 @@ packages/frameworks-sveltekit/.svelte-kit
packages/frameworks-sveltekit/package
packages/frameworks-sveltekit/vite.config.js.timestamp-*
packages/frameworks-sveltekit/vite.config.ts.timestamp-*
# Adapters
docs/docs/reference/adapter

View File

@@ -20,8 +20,8 @@ pnpm-lock.yaml
.docusaurus
build
docs/docs/reference/03-core
docs/docs/reference/04-sveltekit
docs/docs/reference/core
docs/docs/reference/sveltekit
static
docs/providers.json

View File

@@ -9,20 +9,39 @@ NEXTAUTH_URL=http://localhost:3000
# and/or verification tokens.
NEXTAUTH_SECRET=secret
ASGARDEO_CLIENT_ID=
ASGARDEO_CLIENT_SECRET=
ASGARDEO_ISSUER=
AUTH0_ID=
AUTH0_SECRET=
AUTH0_ISSUER=
KEYCLOAK_ID=
KEYCLOAK_SECRET=
KEYCLOAK_ISSUER=
# Beyond Identity Provider
BEYOND_IDENTITY_CLIENT_ID=
BEYOND_IDENTITY_CLIENT_SECRET=
BEYOND_IDENTITY_ISSUER=
GITHUB_ID=
GITHUB_SECRET=
NOTION_ID=
NOTION_SECRET=
NOTION_REDIRECT_URI=
IDS4_ID=
IDS4_SECRET=
IDS4_ISSUER=
GITHUB_ID=
GITHUB_SECRET=
KEYCLOAK_ID=
KEYCLOAK_SECRET=
KEYCLOAK_ISSUER=
LINE_ID=
LINE_SECRET=
TRAKT_ID=
TRAKT_SECRET=
TWITCH_ID=
TWITCH_SECRET=
@@ -30,11 +49,8 @@ TWITCH_SECRET=
TWITTER_ID=
TWITTER_SECRET=
LINE_ID=
LINE_SECRET=
TRAKT_ID=
TRAKT_SECRET=
WIKIMEDIA_ID=
WIKIMEDIA_SECRET=
# Example configuration for a Gmail account (will need SMTP enabled)
EMAIL_SERVER=smtps://user@gmail.com:password@smtp.gmail.com:465
@@ -47,12 +63,9 @@ EMAIL_FROM=user@gmail.com
# MongoDB: DATABASE_URL=mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true
DATABASE_URL=
WIKIMEDIA_ID=
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
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs

4
apps/dev/nextjs/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
/test-results/
/playwright-report/
/playwright/.cache/

View File

@@ -9,10 +9,12 @@
"build": "next build",
"start": "next start",
"email": "fake-smtp-server",
"start:email": "pnpm email"
"start:email": "pnpm email",
"e2e": "pnpm dlx playwright test"
},
"license": "ISC",
"dependencies": {
"@auth/core": "workspace:*",
"@next-auth/fauna-adapter": "workspace:*",
"@next-auth/prisma-adapter": "workspace:*",
"@next-auth/supabase-adapter": "workspace:*",
@@ -22,15 +24,16 @@
"faunadb": "^4",
"next": "13.1.1",
"next-auth": "workspace:*",
"@auth/core": "workspace:*",
"nodemailer": "^6",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@playwright/test": "1.29.2",
"@types/jsonwebtoken": "^8.5.5",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"dotenv": "^16.0.3",
"fake-smtp-server": "^0.8.0",
"pg": "^8.7.3",
"prisma": "^3",

View File

@@ -2,9 +2,11 @@ import { Auth, type AuthConfig } from "@auth/core"
// Providers
import Apple from "@auth/core/providers/apple"
import Asgardeo from "@auth/core/providers/asgardeo"
import Auth0 from "@auth/core/providers/auth0"
import AzureAD from "@auth/core/providers/azure-ad"
import AzureB2C from "@auth/core/providers/azure-ad-b2c"
import BeyondIdentity from "@auth/core/providers/beyondidentity"
import BoxyHQSAML from "@auth/core/providers/boxyhq-saml"
// import Cognito from "@auth/core/providers/cognito"
import Credentials from "@auth/core/providers/credentials"
@@ -23,6 +25,7 @@ import Instagram from "@auth/core/providers/instagram"
import Line from "@auth/core/providers/line"
import LinkedIn from "@auth/core/providers/linkedin"
import Mailchimp from "@auth/core/providers/mailchimp"
import Notion from "@auth/core/providers/notion"
// import Okta from "@auth/core/providers/okta"
import Osu from "@auth/core/providers/osu"
import Patreon from "@auth/core/providers/patreon"
@@ -68,7 +71,7 @@ import WorkOS from "@auth/core/providers/workos"
export const authConfig: AuthConfig = {
// adapter,
// debug: process.env.NODE_ENV !== "production",
debug: process.env.NODE_ENV !== "production",
theme: {
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
brandColor: "#1786fb",
@@ -82,6 +85,7 @@ export const authConfig: AuthConfig = {
},
}),
Apple({ clientId: process.env.APPLE_ID, clientSecret: process.env.APPLE_SECRET }),
Asgardeo({ clientId: process.env.ASGARDEO_CLIENT_ID, clientSecret: process.env.ASGARDEO_CLIENT_SECRET, issuer: process.env.ASGARDEO_ISSUER }),
Auth0({ clientId: process.env.AUTH0_ID, clientSecret: process.env.AUTH0_SECRET, issuer: process.env.AUTH0_ISSUER }),
AzureAD({
clientId: process.env.AZURE_AD_CLIENT_ID,
@@ -89,6 +93,7 @@ export const authConfig: AuthConfig = {
tenantId: process.env.AZURE_AD_TENANT_ID,
}),
AzureB2C({ clientId: process.env.AZURE_B2C_ID, clientSecret: process.env.AZURE_B2C_SECRET, issuer: process.env.AZURE_B2C_ISSUER }),
BeyondIdentity({ clientId: process.env.BEYOND_IDENTITY_CLIENT_ID, clientSecret: process.env.BEYOND_IDENTITY_CLIENT_SECRET, issuer: process.env.BEYOND_IDENTITY_ISSUER }),
BoxyHQSAML({ issuer: "https://jackson-demo.boxyhq.com", clientId: "tenant=boxyhq.com&product=saml-demo.boxyhq.com", clientSecret: "dummy" }),
// Cognito({ clientId: process.env.COGNITO_ID, clientSecret: process.env.COGNITO_SECRET, issuer: process.env.COGNITO_ISSUER }),
Discord({ clientId: process.env.DISCORD_ID, clientSecret: process.env.DISCORD_SECRET }),
@@ -105,6 +110,7 @@ export const authConfig: AuthConfig = {
Line({ clientId: process.env.LINE_ID, clientSecret: process.env.LINE_SECRET }),
LinkedIn({ clientId: process.env.LINKEDIN_ID, clientSecret: process.env.LINKEDIN_SECRET }),
Mailchimp({ clientId: process.env.MAILCHIMP_ID, clientSecret: process.env.MAILCHIMP_SECRET }),
Notion({ clientId: process.env.NOTION_ID, clientSecret: process.env.NOTION_SECRET, redirectUri: process.env.NOTION_REDIRECT_URI }),
// Okta({ clientId: process.env.OKTA_ID, clientSecret: process.env.OKTA_SECRET, issuer: process.env.OKTA_ISSUER }),
Osu({ clientId: process.env.OSU_CLIENT_ID, clientSecret: process.env.OSU_CLIENT_SECRET }),
Patreon({ clientId: process.env.PATREON_ID, clientSecret: process.env.PATREON_SECRET }),

View File

@@ -0,0 +1,107 @@
import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
};
export default config;

View File

@@ -0,0 +1,39 @@
import { test, expect } from "@playwright/test"
test("Sign in with Auth0", async ({ page }) => {
// Go to NextAuth example app
await page.goto("https://next-auth-example.vercel.app")
// Click 'Sign In'
await page.click("#__next > header > div > p > a")
// Auth0 Login Provider
await page.click('body > div > div form[action*="auth0"] > button')
// Enter Credentials (Username/Password Login) on Auth0 Widget
await page.type("#username", process.env.AUTH0_USERNAME!)
await page.type("#password", process.env.AUTH0_PASSWORD!)
// Snap a screenshot
// await page.screenshot({ path: "1-auth0-login.png", fullPage: true })
// Press submit on Auth0 form
await page.click('body > div > main > section > div button[type="submit"]')
// Wait for next-auth example page login status header to appear
await page.waitForTimeout(2000)
// Snap a screenshot
// await page.screenshot({
// path: "2-next-auth-redirect-result.png",
// fullPage: false,
// })
// Check session object after successful login
const response = await page.goto(
"https://next-auth-example.vercel.app/api/auth/session"
)
const session = await response?.json()
expect(session?.user?.email).toBe(process.env.AUTH0_USERNAME)
// TODO: Check whole object with .toEqual()
})

View File

@@ -19,8 +19,8 @@
"vite": "4.0.1"
},
"dependencies": {
"@auth/core": "0.2.5",
"@auth/sveltekit": "0.1.12"
"@auth/core": "workspace:*",
"@auth/sveltekit": "workspace:*"
},
"type": "module"
}

View File

@@ -3,7 +3,7 @@ import { Protected } from "~/components";
export const { routeData, Page } = Protected((session) => {
return (
<main class="flex flex-col gap-2 items-center">
<h1>This is a proteced route</h1>
<h1>This is a protected route</h1>
</main>
);
});

View File

@@ -2,11 +2,10 @@
"name": "playground-nuxt",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "export NODE_OPTIONS='--no-experimental-fetch' && nuxt dev",
"build": "nuxt prepare && nuxt build",
"dev": "nuxt prepare && export NODE_OPTIONS='--no-experimental-fetch' && nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
"preview": "nuxt preview"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.1.1",

View File

@@ -5,52 +5,40 @@ sidebar_position: 0
## About Auth.js
Auth.js is a complete open-source authentication solution for [Next.js](http://nextjs.org/) applications.
Auth.js is a complete open-source authentication solution for web applications. Check out the live demos of Auth.js in action:
It is designed from the ground up to support Next.js and Serverless.
- [Next.js](https://next-auth-example.vercel.app/)
- [SvelteKit](https://sveltekit-auth-example.vercel.app/)
- [SolidStart](https://auth-solid.vercel.app/)
Check our tutorials to see how easy it is to use Auth.js for authentication:
Continue to our tutorials to see how to use Auth.js for authentication:
- [Setup with OAuth](/getting-started/oauth-tutorial)
- [Setup with magic links](/getting-started/email-tutorial)
- [Integrating with external auth](/getting-started/credentials-tutorial)
### Flexible and easy to use
### Battery included
- Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A, 2.0 and OpenID Connect
- Built-in support for [many popular sign-in services](/reference/providers/oauth-builtin)
- Supports [email / passwordless authentication](/getting-started/email-tutorial)
- Supports stateless authentication with [any backend](/getting-started/credentials-tutorial) (Active Directory, LDAP, etc)
- Supports both JSON Web Tokens and database sessions
- Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
- Built in support for 60+ popular services (Google, Facebook, Auth0, Apple…)
- Built-in email/password-less/magic link
- Use with any OAuth 2 or OpenID Connect provider
- Use with any username/password store
### Own your own data
### Flexible
- Runtime agnostic - run anywhere! Vercel Edge Functions, Node.js, Serverless, etc.
- Use with any modern framework! Next.js, SolidStart, SvelteKit, etc.
- [Bring Your Own Database](/getting-started/databases) - or none! MySQL, Postgres, MSSQL, MongoDB, etc. Choose database sessions or JWT.
Auth.js can be used with or without a database.
- An open-source solution that allows you to keep control of your data
- Supports Bring Your Own Database (BYOD) and can be used with any database
- Built-in support for [MySQL, MariaDB, Postgres, SQL Server, MongoDB and SQLite](/getting-started/databases)
- Works great with databases from popular hosting providers
- Can also be used _without a database_ (e.g. OAuth + JWT)
_Note: Email sign-in requires a database to be configured to store single-use verification tokens._
_Note: Email sign-in requires a database to store single-use verification tokens._
### Secure by default
- Promotes the use of passwordless sign-in mechanisms
- Designed to be secure by default and encourage best practices for safeguarding user data
- Uses Cross-Site Request Forgery Tokens on POST routes (sign in, sign out)
- Default cookie policy aims for the most restrictive policy appropriate for each cookie
- When JSON Web Tokens are enabled, they are encrypted by default (JWE) with A256GCM
- Auto-generates symmetric signing and encryption keys for developer convenience
- Features tab/window syncing and keepalive messages to support short-lived sessions
- Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org/)
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who can sign in and how often sessions have to be re-validated.
- Signed, prefixed, server-only cookies
- Built-in CSRF protection
- Doesn't rely on client-side JavaScript
- JWT with JWS / JWE / JWK.
## Credits
Auth.js is an open-source project that is only possible [thanks to contributors](/contributors).
If you would like to financially support the development of Auth.js, you can find more information on our [OpenCollective](https://opencollective.com/nextauth) page.
To financially support the development of Auth.js, you can check our [OpenCollective](https://opencollective.com/nextauth) page. We appreciate your support 💚.

View File

@@ -8,28 +8,43 @@ import gettingClientIdSecretImg from "./img/getting-started-oauth-clientid-secre
import startAppAndSignInImg from "./img/getting-started-app-start.png"
import githubAuthCredentials from "./img/getting-started-github-auth.png"
import nextAuthUserLoggedIn from "./img/getting-started-nextauth-success.png"
import Tabs from "@theme/Tabs"
import TabItem from "@theme/TabItem"
We know, authentication is hard. Is a rabbit hole and it's easy to get lost on it. The goal of making Auth.js is that you can add authentication easily to your project with just a few lines of code.
The goal of Auth.js is that you can add authentication easily to your project with just a few lines of code.
The easiest way is to setup Auth.js with an [OAuth](https://en.wikipedia.org/wiki/OAuth) provider. In this tutorial we'll be setting Auth.js in a **Next.js app** to be able to login with **Github**.
The fastest way to set up Auth.js is with an [OAuth](/concepts/oauth) provider. In this tutorial, we'll be setting Auth.js in a web application to be able to log in with **GitHub**.
:::info
Auth.js comes with a long list of [built-in providers](/reference/providers/oauth-builtin) (Google, Facebook, Twitter, etc...) you can also integrate it with your own OAuth service easily by [building a custom provider](/guides/providers/custom-provider). Auth.js can integrate as well with other frameworks like SvelteKit, SolidStart and Gatsby.
Auth.js comes with a list of [built-in providers](/reference/providers/oauth-builtin) (Google, Facebook, Twitter, etc.). You can also integrate it with your OAuth service by [building a custom provider](/guides/providers/custom-provider).
:::
## 1. Configuring Auth.js
To add Auth.js to your project:
<Tabs groupId="frameworks" queryString>
<TabItem value="next" label="Next.js" default>
### Prerequisites
This tutorial assumes you have a Next.js application set up. If you don't, you can follow the [Next.js tutorial](https://nextjs.org/learn/basics/create-nextjs-app) to get started.
### Installing NextAuth.js
```bash npm2yarn
npm install next-auth
```
:::info
We are working on a new `@auth/nextjs` package that will make it easier to set up Auth.js with Next.js. Stay tuned! For now, you can use the `next-auth` package.
:::
### Creating the server config
To add Auth.js to a [**Next.js**](https://nextjs.org/) project, create the following [API route](https://nextjs.org/docs/api-routes/introduction):
Create the following [API route](https://nextjs.org/docs/api-routes/dynamic-api-routes#catch-all-api-routes) file. This route contains the necessary configuration for NextAuth.js, as well as the dynamic route handler:
```
pages/api/auth/[...nextauth].ts
```
This route will contain the **dynamic route handler** for Auth.js which describes your global auth configuration:
```ts title="pages/api/auth/[...nextauth].js"
```ts title="pages/api/auth/[...nextauth].ts"
import NextAuth from "next-auth"
import GithubProvider from "next-auth/providers/github"
@@ -43,16 +58,33 @@ export default NextAuth({
})
```
Behind the scenes this creates all the relevant OAuth API routes within `/api/auth/*` so that auth API requests to:
:::info
- `/api/auth/callback`
- `/api/auth/signIn`
- `/api/auth/signOut`
- etc...
Behind the scenes, this creates all the relevant OAuth API routes within `/api/auth/*` so that auth API requests to:
can be handled by Auth.js. In this way, Auth.js stays in charge of handling the whole authentication request/response flow of your application for you.
- [GET `/api/auth/signin`](https://authjs.dev/reference/rest-api#get--apiauthsignin)
- [POST `/api/auth/signin/:provider`](https://authjs.dev/reference/rest-api#post--apiauthsigninprovider)
- [GET/POST `/api/auth/callback/:provider`](https://authjs.dev/reference/rest-api#get--post--apiauthcallbackprovider)
- [GET `/api/auth/signout`](https://authjs.dev/reference/rest-api#get--apiauthsignout)
- [POST `/api/auth/signout`](https://authjs.dev/reference/rest-api#post--apiauthsignout)
- [GET `/api/auth/session`](https://authjs.dev/reference/rest-api#get--apiauthsession)
- [GET `/api/auth/csrf`](https://authjs.dev/reference/rest-api#get--apiauthcsrf)
- [GET `/api/auth/providers`](https://authjs.dev/reference/rest-api#get--apiauthproviders)
You may notice there are some environment variables in the code example above. `GITHUB_ID` and `GITHUB_SECRET` are provided by the OAuth provider (in this case **Github**) see ["Configuring OAuth Provider"](/getting-started/oauth-tutorial#2-configuring-oauth-provider) section on how to get those.
can be handled by NextAuth.js. In this way, NextAuth.js stays in charge of the whole application's authentication request/response flow.
NextAuth.js is fully customizable - [our guides section](/guides/overview) teaches you how to set it up to handle auth in different ways. All the possible configuration options are [listed here](/reference/configuration/auth-config).
:::
### Adding environment variables
You may notice we are using environment variables in the code example above. We take the value of `GITHUB_ID` and `GITHUB_SECRET` from the GitHub Developer OAuth Portal. See [Configuring OAuth Provider](/getting-started/oauth-tutorial#2-configuring-oauth-provider) section on how to get those.
In your project root, create a `.env.local` file and add the `NEXTAUTH_SECRET` environment variable:
```title=".env.local"
NEXTAUTH_SECRET="This is an example"
```
`NEXTAUTH_SECRET` is a random string used by the library to encrypt tokens and email verification hashes, and **it's mandatory to keep things secure**! 🔥 🔐 . You can use:
@@ -62,15 +94,11 @@ $ openssl rand -base64 32
or https://generate-secret.vercel.app/32 to generate a random value for it.
:::info
Auth.js is extremely customizable, [our guides section](/guides/overview) will teach you how you can set it up to handle auth in different ways. All the possible configuration options are [listed here](/reference/configuration/auth-config).
:::
### Exposing the session via `SessionProvider`:
### Exposing the session via provider
NextAuth.js provides [`useSession()`](/reference/react/#usesession) - a [React Hooks](https://reactjs.org/docs/hooks-intro.html) to access the session data and status. To use it first you'll need to expose the session context - [`<SessionProvider />`](/reference/react/#sessionprovider) - at the top level of your application:
To be able to use `useSession` first you'll need to expose the session context, [`<SessionProvider />`](/reference/react/#sessionprovider), at the top level of your application:
```ts title="pages/_app.ts"
```ts title="pages/_app.tsx"
import { SessionProvider } from "next-auth/react"
export default function App({
@@ -85,7 +113,7 @@ export default function App({
}
```
Instances of `useSession` (more on it in the next section) will then have access to the session data and status. The `<SessionProvider />` also takes care of keeping the session updated and synced between browser tabs and windows. 💪🏽
Instances of `useSession` (more on it in the next section) will have access to the session data and status. The `<SessionProvider />` also keep the session updated and synced between browser tabs and windows. 💪🏽
:::tip
Check our [client docs](/reference/react/) to learn all the available options for handling sessions on the browser.
@@ -93,14 +121,14 @@ Check our [client docs](/reference/react/) to learn all the available options fo
### Consuming the session via hooks
Auth.js exposes a [`useSession()`](/reference/react/#usesession) React Hook so that you can easily check if someone is signed in:
You can use the `useSession` hook from anywhere in your application (E.g. in a header component). Behind the scenes, the hook will connect to the `<SessionProvider />` to read the current user session. Learn more about React Context in the [React docs](https://reactjs.org/docs/context.html).
```ts title="pages/overview.tsx"
import { useSession, signIn, signOut } from "next-auth/react"
export default function CamperVanPage() {
const { data: session, status } = useSession()
const userEmail = session?.user.email;
const userEmail = session?.user.email
if (status === "loading") {
return <p>Hang on there...</p>
@@ -119,23 +147,22 @@ export default function CamperVanPage() {
return (
<>
<p>Not signed in.</p>
<button onClick={() => signIn()}>Sign in</button>
<button onClick={() => signIn("github")}>Sign in</button>
</>
)
}
```
You can use the `useSession` hook from anywhere in your application (e.g. in a header component). Behind the scenes, the hook will connect to the `<SessionProvider />` to read the current user session.
### Protecting API Routes
Protecting your custom API Routes (.i.e not allowing a resource to be accessed in case the user is not logged in) is easy! You can use [`getSession()`](/reference/utilities/#getsession) to know whether a session exists or not:
To protect your API Routes (blocking unauthorized access to resources), you can use [`getServerSession()`](/reference/nextjs#getserversession) to know whether a session exists or not:
```ts title="pages/api/movies/list.ts"
import { getSession } from "next-auth/react"
import { getServerSession } from "next-auth/next"
import { authOptions } from "../auth/[...nextauth]"
export default async function listMovies(req, res) {
const session = await getSession({ req })
const session = await getServerSession(req, res, authOptions)
if (session) {
res.send({
@@ -152,21 +179,33 @@ export default async function listMovies(req, res) {
}
```
</TabItem>
<TabItem value="sveltekit" label="SvelteKit">
TODO: SvelteKit
</TabItem>
<TabItem value="solidstart" label="SolidStart">
TODO: SolidStart
</TabItem>
<TabItem value="core" label="Vanilla (No Framework)">
TODO Core
</TabItem>
</Tabs>
## 2. Configuring OAuth Provider
Ok, we have our Next.js app setup with NextAuth, however, if you run the app right now, it won't work as we haven't configured our OAuth provider (**Github**) yet.
Ok, we have our app set up with NextAuth.js, however, if you run the app right now, it won't work as we haven't configured our OAuth provider (**GitHub**) yet.
:::info
When using OAuth you're asking for a third-party service (in this case Github, although it could be Google, Twitter, etc...) to handle user authentication for your app.
When using OAuth you're asking for a third-party service (in this case GitHub, although it could be Google, Twitter, etc...) to handle user authentication for your app.
:::
We need to register our new Next.js app in Github, so that when Auth.js forwards the authorization requests to it, Github can recognize your application and prompt the user to sign in.
We need to register our new app in GitHub, so that when NextAuth.js forwards the authorization requests to it, GitHub can recognize your application and prompt the user to sign in.
<img src={creatingOauthAppImg} />
Log in into **Github**, go to `Settings / Developers / OAuth Apps` and click on "New OAuth App".
Log in to **GitHub**, go to [`Settings / Developers / OAuth Apps`](https://github.com/settings/developers) and click "New OAuth App"
Next you'll be presented with a screen to add details about your new application. Fill in the required fields, but pay extra attention to the **Authorization Callback URL** one:
Next, you'll be presented with a screen to add details about your new application. Fill in the required fields, but pay extra attention to the **Authorization Callback URL** one:
<img src={addingCallbackUrlImg} />
@@ -176,37 +215,54 @@ The callback URL we insert should have the following pattern:
[origin]/api/auth/callback/[provider]
```
In this case, given we want to try our authentication working locally on our machine and we're using **Github** as our OAuth provider, it'll be:
In this case, given we want to try our authentication working locally on our machine and we're using **GitHub** as our OAuth provider, it'll be:
<Tabs groupId="frameworks">
<TabItem value="next" label="Next.js" default>
```
http://localhost:3000/api/auth/callback/github
```
:::info
Auth.js will already magically create this API endpoint for you when we start the application later. Note that because we're using Next.js, locally it starts our server on the port `3000`, hence the origin is `http://localhost:3000`.
NextAuth.js will already create this API endpoint for you when we start the application later. Note that because we're using Next.js, locally it starts our server on port `3000` by default. Hence, the origin is `http://localhost:3000`.
:::
Next you'll be presented with the following screen which presents all the configuration for your new OAuth app. For now, let's we need two things from it: the **Client ID** and **Client Secret** for our new OAuth app:
</TabItem>
<TabItem value="sveltekit" label="SvelteKit">
```
http://localhost:5173/auth/callback/github
```
</TabItem>
<TabItem value="solidstart" label="SolidStart">
TODO SolidStart
</TabItem>
<TabItem value="core" label="Vanilla (No Framework)">
TODO Core
</TabItem>
</Tabs>
:::info
The last part of the URL, `[provider]`, is the ID of the provider you're using. In this case, we're using GitHub, so it's `github`. If you're using Google, it'll be `google`, etc... We keep track of the provider IDs internally.
The same id is used in the `signIn()` method we saw earlier.
:::
To register, tap on "Register application" button.
The next screen shows all the configurations for your newly created OAuth app. For now, we need two things from it - the **Client ID** and **Client Secret**:
<img src={gettingClientIdSecretImg} />
The Client ID is always there, a public identifier of your OAuth application within Github. Click on the **Generate a new client Secret** button and should be presented with a new string (which is just a randomized string).
The Client ID is always there, a public identifier of your OAuth application within GitHub. Click on the **Generate a new client Secret** button and should be presented with a new string (which is just a randomized string).
:::warning
🔥 Keep both your Client ID and Client Secret secure and never expose them to the public or shared with people outside your organization. With tem a malicious actor could hijack your application and cause you and your user serious problems!
Keep both your Client ID and Client Secret secure and never expose them to the public or share them with people outside your organization. With them, a malicious actor could hijack your application and cause you and your user serious problems!
:::
Now let's copy both the Client ID and Client Secret and paste them in an environment file in the root of your project like so:
```title=".env.local"
GITHUB_ID=12345
GITHUB_SECRET=67890
```
Cool! We have finished the configuring our OAuth provider, now let's wire all together so we can finally see authentication working in our app!
Cool! We have finished configuring our OAuth provider, now let's wire all together so we can finally see authentication working in our app!
:::info
As noted previously, Auth.js has built-in support for multiple OAuth providers, <a href="">here the full list</a>. You can also easily build your own in case the provider you need is not on the list.
As noted previously, NextAuth.js has built-in support for multiple OAuth providers, <a href="">here is the full list</a>. You can also easily build your own in case the provider you need is not on the list.
Note that, for each provider, the configuration process will be similar to what we just did:
@@ -214,13 +270,25 @@ Note that, for each provider, the configuration process will be similar to what
2. Create create your OAuth application within it
3. Set the callback URL
4. Get the Client ID and Generate a Client Secret
:::
:::
## 3. Wiring all together
Finally, we just need to reference our **Client ID** and **Client Secret** we just generated in the previous in our Auth.js config. In this way the library will be able to use them when forwarding users to Github, and Github will be able to recognize the request as generated from our application:
Finally, we just need to reference our **Client ID** and **Client Secret** we just generated in the previous in our Auth.js config. In this way, the library will be able to use them when forwarding users to GitHub, and GitHub will be able to recognize the request as generated from our application.
```ts title="pages/api/auth/[...nextauth].js"
Now let's copy both the Client ID and Client Secret and paste them into an environment file in the root of your project like so:
```title=".env.local"
GITHUB_ID=12345
GITHUB_SECRET=67890
```
Here is our server configuration file again:
<Tabs groupId="frameworks">
<TabItem value="next" label="Next.js" default>
```ts title="pages/api/auth/[...nextauth].ts"
import NextAuth from "next-auth"
import GithubProvider from "next-auth/providers/github"
@@ -240,50 +308,52 @@ Great! We're now ready to run our application locally. Start the Next.js app by
$ npm run next dev
```
</TabItem>
<TabItem value="sveltekit" label="SvelteKit">
TODO SvelteKit
</TabItem>
<TabItem value="solidstart" label="SolidStart">
TODO SolidStart
</TabItem>
<TabItem value="core" label="Vanilla (No Framework)">
TODO Core
</TabItem>
</Tabs>
You should see the following page:
<img src={startAppAndSignInImg} />
Click on "Sign in" and then on "Sign in with Github": Auth.js will redirect you to Github, and Github will recognize our app [that we just registered](#2-configuring-oauth-provider) and ask the user (in this case you) to enter its credentials to proceed:
Click on "Sign in" and then on "Sign in with GitHub": Auth.js will redirect you to GitHub, and GitHub will recognize our app [that we just registered](#2-configuring-oauth-provider) and ask the user (in this case you) to enter its credentials to proceed:
<img src={githubAuthCredentials} />
Once inserted and correct, Github will redirect the user to our app and Auth.js will take care of any further calls with Github to get access to the user profile and start a user sessions safely in the background:
Once inserted and correct, GitHub will redirect the user to our app and NextAuth.js will take care of any further calls with GitHub to get access to the user profile and start a user session safely in the background:
<img src={nextAuthUserLoggedIn} />
Great! We have completed the whole E2E authentication flow setup so that users can login in our application through Github!
:::info
You can create your own Sign In page instead of using the default one from Auth.js. You can learn how to do so in our dedicated guide for it.
:::
Great! We have completed the whole E2E authentication flow setup so that users can log in to our application through GitHub!
## 4. Deploying to production
### Configuring different environments
It's normal to test your application under different environments. Usually you'll have a development environment (when you run the application locally in your machine), a staging environment (for teams members to try the application) and a production environment.
It's normal to test your application in different environments. Usually, you'll have a development environment (when you run the application locally on your machine), a staging environment (for team members to try the application), and a production environment.
For each environment, you're going to need to create an OAuth application in your provider respectively, as [we did previously](#2-configuring-oauth-provider), and point the **callback URL** to it.
For each environment, you need to create an OAuth application in your provider respectively, as [we did previously](#2-configuring-oauth-provider), and point the **callback URL** to it.
For instance in the previous section, we pointed the callback URL to:
For instance, in the previous section, we pointed the callback URL to `http://localhost:3000/api/auth/callback/github` as we wanted to test our application in the development environment.
```
http://localhost:3000/api/auth/callback/github
```
If we were to deploy our app to production, we would need to create a new **OAuth App** in GitHub (calling it something like "Van life prod") and point the **callback URL** to our production domain: `https://example.com/api/auth/callback/github`
as we wanted to test our application in the development environment.
If we were to deploy our app to production, we would need to create again a new **OAuth App** in Github (calling it something like "Van life prod") and point the **callback URL** to our production domain:
```
https://example.com/api/auth/callback/github
```
Finally, we would need just to point the environment variables we set ( `GITHUB_ID` and `GITHUB_SECRET` ) to the credentials of the OAuth app we want our application to run against.
Finally, we would need to point the environment variables we set ( `GITHUB_ID` and `GITHUB_SECRET` ) to the credentials of the OAuth app we want our application to run with.
### Setting up `NEXTAUTH_URL`
:::tip
Skip this section if you are deploying to Vercel.
:::
When deploying your site, **you need to set** the `NEXTAUTH_URL` environment variable to the canonical URL of your website:
```

View File

@@ -72,11 +72,11 @@ export default NextAuth({
providers: [
Email({
server: {
host: process.env.EMAIL_SERVER_HOST,
port: Number(process.env.EMAIL_SERVER_PORT),
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD,
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
},
from: process.env.EMAIL_FROM,
@@ -147,8 +147,8 @@ import EmailProvider from "next-auth/providers/email"
export default NextAuth({
secret: process.env.NEXTAUTH_SECRET,
+ adapter: MongoDBAdapter(clientPromise),
providers: [
+ adapter: MongoDBAdapter(clientPromise),
EmailProvider({
server: {
host: process.env.EMAIL_SERVER_HOST,
@@ -188,7 +188,7 @@ Let's now check our email, and look for one sent from NextAuth (check your spam
<img src={mailboxImg} alt="Screenshot of mailbox" />
Nice! We got one, coming from the sender specified in the `EMAIL_FROM` environment variable from our configuration above and that's is the sender we verified in Sengrid.
Nice! We got one, coming from the sender specified in the `EMAIL_FROM` environment variable from our configuration above and that's is the sender we verified in Sendgrid.
Click on "Sign in" and a new browser tab will open, you should then land on your application as authenticated!

View File

@@ -5,7 +5,7 @@ title: TypeScript
Auth.js has its own type definitions to use in your TypeScript projects safely. Even if you don't use TypeScript, IDEs like VSCode will pick this up to provide you with a better developer experience. While you are typing, you will get suggestions about what certain objects/functions look like, and sometimes links to documentation, examples, and other valuable resources.
Check out the example repository showcasing how to use `next-auth` on a Next.js application with TypeScript:
https://github.com/nextauthjs/next-auth-typescript-example
https://github.com/nextauthjs/next-auth-example
---

View File

@@ -22,7 +22,7 @@ Using a JWT to store the `refresh_token` is less secure than saving it in a data
#### JWT strategy
Using the [jwt](../../reference/03-core/interfaces/types.CallbacksOptions.md#jwt) and [session](../../reference/03-core/interfaces/types.CallbacksOptions.md#session) callbacks, we can persist OAuth tokens and refresh them when they expire.
Using the [jwt](../../reference/core/types#jwt) and [session](../../reference/core/types#session) callbacks, we can persist OAuth tokens and refresh them when they expire.
Below is a sample implementation using Google's Identity Provider. Please note that the OAuth 2.0 request in the `refreshAccessToken()` function will vary between different providers, but the core logic should remain similar.
@@ -45,10 +45,10 @@ export default Auth(new Request("https://example.com"), {
// Save the access token and refresh token in the JWT on the initial login
return {
access_token: account.access_token,
expires_at: Date.now() + account.expires_in * 1000,
expires_at: Math.floor(Date.now() / 1000 + account.expires_in),
refresh_token: account.refresh_token,
}
} else if (Date.now() < token.expires_at) {
} else if (Date.now() < token.expires_at * 1000) {
// If the access token has not expired yet, return it
return token
} else {
@@ -74,7 +74,7 @@ export default Auth(new Request("https://example.com"), {
return {
...token, // Keep the previous token properties
access_token: tokens.access_token,
expires_at: Date.now() + tokens.expires_in * 1000,
expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in),
// Fall back to old refresh token, but note that
// many providers may only allow using a refresh token once.
refresh_token: tokens.refresh_token ?? token.refresh_token,
@@ -136,7 +136,7 @@ export default Auth(new Request("https://example.com"), {
const [google] = await prisma.account.findMany({
where: { userId: user.id, provider: "google" },
})
if (google.expires_at >= Date.now()) {
if (google.expires_at * 1000 < Date.now()) {
// If the access token has expired, try to refresh it
try {
// https://accounts.google.com/.well-known/openid-configuration
@@ -159,7 +159,7 @@ export default Auth(new Request("https://example.com"), {
await prisma.account.update({
data: {
access_token: tokens.access_token,
expires_at: Date.now() + tokens.expires_in * 1000,
expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in),
refresh_token: tokens.refresh_token ?? google.refresh_token,
},
where: {

View File

@@ -0,0 +1,153 @@
---
title: Role-based authentication
---
There are two ways to add role-based authentication (RBAC) to your application, based on the [session strategy](/concepts/session-strategies) you choose. Let's see an example for each of these.
## Getting the role
We are going to start by adding a `profile()` callback to the providers' config to determine the user role:
```ts title="/pages/api/auth/[...nextauth].ts"
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
export default NextAuth({
providers: [
Google({
profile(profile) {
return { role: profile.role ?? "user", ... }
},
...
})
],
})
```
:::tip
To determine the user's role, you can either add your logic or if your provider assigns roles already, use that instead.
:::
## Persisting the role
### With JWT
When you don't have a database configured, the role will be persisted in a cookie, by using the `jwt()` callback. On sign-in, the `role` property is exposed from the `profile` callback on the `user` object. Persist the `user.role` value by assigning it to `token.role`. That's it!
If you also want to use the role on the client, you can expose it via the `session` callback.
```ts title="/pages/api/auth/[...nextauth].ts"
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
export default NextAuth({
providers: [
Google({
profile(profile) {
return { role: profile.role ?? "user", ... }
},
...
})
],
// highlight-start
callbacks: {
jwt({ token, user }) {
if(user) token.role = user.role
return token
},
session({ session, token }) {
session.user.role = token.role
return session
}
}
// highlight-end
})
```
:::info
With this strategy, if you want to update the role, the user needs to be forced to sign in again.
:::
### With Database
When you have a database, you can save the user role on the [User model](/reference/adapters/models#user). The below example is showing you how to do this with Prisma, but the idea is the same for all adapters.
First, add a `role` column to the User model.
```ts title="/prisma/schema.prisma"
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
role String? // New column
accounts Account[]
sessions Session[]
}
```
The `profile()` callback's return value is used to create users in the database. That's it! Your newly created users will now have an assigned role.
If you also want to use the role on the client, you can expose it via the `session` callback.
```ts title="/pages/api/auth/[...nextauth].ts"
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
// highlight-next-line
import prisma from "lib/prisma"
export default NextAuth({
// highlight-next-line
adapter: PrismaAdapter(prisma),
providers: [
Google({
profile(profile) {
return { role: profile.role ?? "user", ... }
}
...
})
],
// highlight-start
callbacks: {
session({ session, user }) {
session.user.role = user.role
return session
}
}
// highlight-end
})
```
:::info
It is up to you how you want to manage to update the roles, either through direct database access or building your role update API.
:::
## Using the role
If you want to use the role in the client, for both cases above, when using the `useSession` hook, `session.user.role` will have the required role if you exposed it via the `session` callback. You can use this to render a different UI for different users.
```ts title="/pages/admin.tsx"
import { useSession } from "next-auth/react"
export default function Page() {
const session = await useSession()
if (session?.user.role === "admin") {
return <p>You are an admin, welcome!</p>
}
return <p>You are not authorized to view this page!</p>
}
```
:::tip
When using Next.js and JWT, you can alternatively also use [Middleware](https://next-auth.js.org/configuration/nextjs#wrap-middleware) to redirect the user based on their role, even before rendering the page.
:::
## Resources
- [Concepts: Session strategies](/concepts/session-strategies)
- [Next.js: Middleware](https://next-auth.js.org/configuration/nextjs#wrap-middleware)
- [Adapters: User model](/reference/adapters/models#user)
- [Adapters: Prisma adapter](/reference/adapters/prisma)
- [TypeScript](/getting-started/typescript)

View File

@@ -1,64 +0,0 @@
---
title: Role based logins
---
To add role based authentication to your application, you must do three things.
1. Update your database schema
2. Add the `role` to the session object
3. Check for `role` in your pages/components
First modify the `user` table and add a `role` column with the type of `String?`.
Below is an example Prisma schema file.
```javascript title="/prisma/schema.prisma"
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
role String? // New Column
accounts Account[]
sessions Session[]
}
```
Next, implement a custom session callback in the `[...nextauth].js` file, as shown below.
```javascript title="/pages/api/auth/[...nextauth].js"
callbacks: {
async session({ session, token, user }) {
session.user.role = user.role; // Add role value to user object so it is passed along with session
return session;
},
```
Going forward, when using the `getSession` hook, check that `session.user.role` matches the required role. The example below assumes the role `'admin'` is required.
```javascript title="/pages/admin.js"
import { getSession } from "next-auth/react"
export default function Page() {
const session = await getSession({ req })
if (session && session.user.role === "admin") {
return (
<div>
<h1>Admin</h1>
<p>Welcome to the Admin Portal!</p>
</div>
)
} else {
return (
<div>
<h1>You are not authorized to view this page!</h1>
</div>
)
}
}
```
Then it is up to you how you manage your roles, either through direct database access or building your own role update API.

View File

@@ -2,7 +2,7 @@
title: Corporate proxy
---
Using Auth.js behind a corporate proxy is not supported out of the box. This is due to the fact that the underlying library we use, [`openid-client`](https://npm.im/openid-client) which uses the built-in Node.js `http` / `https` libraries, and those do not support proxys by default:
Using Auth.js behind a corporate proxy is not supported out of the box. This is due to the fact that the underlying library we use, [`openid-client`](https://npm.im/openid-client) which uses the built-in Node.js `http` / `https` libraries, and those do not support proxies by default:
- [`http` docs](https://nodejs.org/dist/latest-v18.x/docs/api/http.html)
- [`https` docs](https://nodejs.org/dist/latest-v18.x/docs/api/https.html)

View File

@@ -26,7 +26,7 @@ export default NextAuth({
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
// You might want to pull this call out so we're not making a new LDAP client on every login attemp
// You might want to pull this call out so we're not making a new LDAP client on every login attempt
const client = ldap.createClient({
url: process.env.LDAP_URI,
})

View File

@@ -16,4 +16,4 @@ sidebar_label: Email options
See our guides on magic links authentication for further tips on how to customize this provider:
- [Tutorial](/getting-started/email-tutorial)
- [Guide deep-dive](guides/providers/email)
- [Guide deep-dive](/guides/providers/email)

View File

@@ -23,8 +23,8 @@ AUTH_SECRET=your_auth_secret
in this example we are using github so make sure to set the following environment variables:
```
GITHUB_ID=your_github_oatuh_id
GITHUB_SECRET=your_github_oatuh_secret
GITHUB_ID=your_github_oauth_id
GITHUB_SECRET=your_github_oauth_secret
```
```ts

View File

@@ -11,7 +11,7 @@ When using SSR, I recommend creating a `Protected` component that will trigger s
```tsx
// components/Protected.tsx
import { type Session } from "@auth/core";
import { type Session } from "@auth/core/types";
import { getSession } from "@auth/solid-start";
import { Component, Show } from "solid-js";
import { useRouteData } from "solid-start";
@@ -60,7 +60,7 @@ import Protected from "~/components/Protected";
export const { routeData, Page } = Protected((session) => {
return (
<main class="flex flex-col gap-2 items-center">
<h1>This is a proteced route</h1>
<h1>This is a protected route</h1>
</main>
);
});
@@ -110,7 +110,7 @@ And now you can easily create a protected route:
export default () => {
return (
<main class="flex flex-col gap-2 items-center">
<h1>This is a proteced route</h1>
<h1>This is a protected route</h1>
</main>
);
};

View File

@@ -33,7 +33,7 @@ providers: [
```
:::warning
Trakt does not allow hotlinking images. Even the authenticated user's profie picture.
Trakt does not allow hotlinking images. Even the authenticated user's profile picture.
:::
:::warning

View File

@@ -91,7 +91,7 @@ type VerificationToken {
## Securing your database
For production deployments you will want to restrict the access to the types used
by next-auth. The main form of access control used in Dgraph is via `@auth` directive alongide types in the schema.
by next-auth. The main form of access control used in Dgraph is via `@auth` directive alongside types in the schema.
#### Secure schema

View File

@@ -1,75 +0,0 @@
---
id: firebase
title: Firebase
---
:::warning
This adapter is still experimental and does not work with Auth.js 4 or newer. If you would like to help out upgrading it, please visit [this PR](https://github.com/nextauthjs/next-auth/pull/3873)
:::
This is the Firebase Adapter for [`next-auth`](https://authjs.dev). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
## Getting Started
1. Install the necessary packages
```bash npm2yarn
npm install next-auth @next-auth/firebase-adapter@experimental
```
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import { FirebaseAdapter } from "@next-auth/firebase-adapter"
import firebase from "firebase/app"
import "firebase/firestore"
const firestore = (
firebase.apps[0] ?? firebase.initializeApp(/* your config */)
).firestore()
// For more information on each option (and a full list of options) go to
// https://authjs.dev/reference/configuration/auth-options
export default NextAuth({
// https://authjs.dev/reference/providers/
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
adapter: FirebaseAdapter(firestore),
...
})
```
## Options
When initializing the firestore adapter, you must pass in the firebase config object with the details from your project. More details on how to obtain that config object can be found [here](https://support.google.com/firebase/answer/7015592).
An example firebase config looks like this:
```js
const firebaseConfig = {
apiKey: "AIzaSyDOCAbC123dEf456GhI789jKl01-MnO",
authDomain: "myapp-project-123.firebaseapp.com",
databaseURL: "https://myapp-project-123.firebaseio.com",
projectId: "myapp-project-123",
storageBucket: "myapp-project-123.appspot.com",
messagingSenderId: "65211879809",
appId: "1:65211879909:web:3ae38ef1cdcb2e01fe5f0c",
measurementId: "G-8GSGZQ44ST",
}
```
See [firebase.google.com/docs/web/setup](https://firebase.google.com/docs/web/setup) for more details.
:::tip **From Firebase**
**Caution**: We do not recommend manually modifying an app's Firebase config file or object. If you initialize an app with invalid or missing values for any of these required "Firebase options", then your end users may experience serious issues.
For open source projects, we generally do not recommend including the app's Firebase config file or object in source control because, in most cases, your users should create their own Firebase projects and point their apps to their own Firebase resources (via their own Firebase config file or object).
:::

View File

@@ -45,7 +45,7 @@ You need to use at least Prisma 2.26.0. Create a schema file in `prisma/schema.p
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL") // Only needed when using a cloud provider that doesn't support the creation of new databases, like Heroku. Learn more: https://pris.ly/migrate-shadow
shadowDatabaseUrl = env("SHADOW_DATABASE_URL") // Only needed when using a cloud provider that doesn't support the creation of new databases, like Heroku. Learn more: https://pris.ly/d/migrate-shadow
}
generator client {
@@ -139,9 +139,10 @@ Prisma supports MongoDB, and so does Auth.js. Following the instructions of the
id String @id @default(auto()) @map("_id") @db.ObjectId
```
2. The Native database type attribute to `@db.String` from `@db.Text`.
2. The Native database type attribute to `@db.String` from `@db.Text` and userId to `@db.ObjectId`.
```prisma
user_id String @db.ObjectId
refresh_token String? @db.String
access_token String? @db.String
id_token String? @db.String

View File

@@ -32,7 +32,6 @@ Now that we're ready, let's create a new Xata project using our next-auth schema
```json title="schema.json"
{
"formatVersion": "",
"tables": [
{
"name": "nextauth_users",

View File

@@ -1,25 +0,0 @@
---
title: Overview
sidebar_label: Overview
sidebar_position: 0
---
## Core
## Providers
- OAuth/OIDC
- Email/Passwordless
- Credentials
## Database Adapters
## Frameworks
- Next.js
- SvelteKit
- SolidStart
- Remix
- Nuxt
- Gatsby
- etc.

View File

@@ -62,7 +62,7 @@ const docusaurusConfig = {
position: "left",
},
{
to: "/reference/core/modules/main",
to: "/reference/core",
// TODO: change to this when the overview page looks better.
// to: "/reference",
activeBasePath: "/reference",
@@ -101,7 +101,7 @@ const docusaurusConfig = {
announcementBar: {
id: "new-major-announcement",
content:
"<a target='_blank' rel='noopener noreferrer' href='https://next-auth.js.org'>NextAuth.js</a> is becoming Auth.js! 🎉 We're creating Authentication for the Web. Everyone included. Starting with SvelteKit, check out <a href='/reference/sveltekit'>the docs</a>.",
"<a target='_blank' rel='noopener noreferrer' href='https://next-auth.js.org'>NextAuth.js</a> is becoming Auth.js! 🎉 We're creating Authentication for the Web. Everyone included. Starting with SvelteKit, check out <a href='/reference/sveltekit'>the docs</a>. Note, this site is under active development.",
backgroundColor: "#000",
textColor: "#fff",
},
@@ -182,10 +182,7 @@ const docusaurusConfig = {
lastVersion: "current",
showLastUpdateAuthor: true,
showLastUpdateTime: true,
remarkPlugins: [
require("@sapphire/docusaurus-plugin-npm2yarn2pnpm").npm2yarn2pnpm,
require("remark-github"),
],
remarkPlugins: [require("@sapphire/docusaurus-plugin-npm2yarn2pnpm").npm2yarn2pnpm],
versions: {
current: {
label: "experimental",
@@ -204,20 +201,14 @@ const docusaurusConfig = {
{
...typedocConfig,
id: "core",
plugin: ["./tyepdoc"],
entryPoints: [
"index.ts",
"adapters.ts",
"errors.ts",
"jwt.ts",
"types.ts",
]
.map((e) => `${coreSrc}/${e}`)
.concat(providers),
tsconfig: "../packages/core/tsconfig.json",
out: "reference/03-core",
plugin: [require.resolve("./typedoc-mdn-links")],
watch: process.env.TYPEDOC_WATCH,
includeExtension: false,
entryPoints: ["index.ts", "adapters.ts", "errors.ts", "jwt.ts", "types.ts"].map((e) => `${coreSrc}/${e}`).concat(providers),
tsconfig: "../packages/core/tsconfig.json",
out: "reference/core",
sidebar: {
indexLabel: "index",
},
},
],
[
@@ -225,14 +216,29 @@ const docusaurusConfig = {
{
...typedocConfig,
id: "sveltekit",
plugin: ["./tyepdoc"],
entryPoints: ["index.ts", "client.ts"].map(
(e) => `../packages/frameworks-sveltekit/src/lib/${e}`
),
tsconfig: "../packages/frameworks-sveltekit/tsconfig.json",
out: "reference/04-sveltekit",
plugin: [require.resolve("./typedoc-mdn-links")],
watch: process.env.TYPEDOC_WATCH,
includeExtension: false,
entryPoints: ["index.ts", "client.ts"].map((e) => `../packages/frameworks-sveltekit/src/lib/${e}`),
tsconfig: "../packages/frameworks-sveltekit/tsconfig.json",
out: "reference/sveltekit",
sidebar: {
indexLabel: "index",
},
},
],
[
"docusaurus-plugin-typedoc",
{
...typedocConfig,
id: "firebase-adapter",
plugin: [require.resolve("./typedoc-mdn-links")],
watch: process.env.TYPEDOC_WATCH,
entryPoints: ["../packages/adapter-firebase/src/index.ts"],
tsconfig: "../packages/adapter-firebase/tsconfig.json",
out: "reference/adapter/firebase",
sidebar: {
indexLabel: "Firebase",
},
},
],
],

View File

@@ -3,7 +3,7 @@
"repository": "https://github.com/nextauthjs/next-auth",
"name": "docs",
"scripts": {
"start": "TYPEDOC_WATCH=true docusaurus start --no-open --port 8000",
"start": "TYPEDOC_WATCH=true docusaurus start --no-open",
"dev": "pnpm providers && pnpm snippets && pnpm start",
"build": "pnpm providers && docusaurus build",
"docusaurus": "docusaurus",
@@ -27,7 +27,6 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-marquee-slider": "^1.1.5",
"remark-github": "10.1.0",
"styled-components": "5.3.6"
},
"devDependencies": {
@@ -37,7 +36,9 @@
"@docusaurus/preset-classic": "2.2.0",
"@docusaurus/theme-common": "2.2.0",
"@docusaurus/types": "2.2.0",
"docusaurus-plugin-typedoc": "^0.18.0"
"docusaurus-plugin-typedoc": "1.0.0-next.2",
"typedoc": "^0.23.24",
"typedoc-plugin-markdown": "4.0.0-next.2"
},
"browserslist": {
"production": [

View File

@@ -18,7 +18,7 @@ for (const file of files) {
body.push(" */")
const name = file.replace(/\.md$/, "")
result[name] = {
description: `Snippet genereated from ${file} by pnpm \`generate-snippet\``,
description: `Snippet generated from ${file} by pnpm \`generate-snippet\``,
scope: "typescript",
prefix: name,
body,

View File

@@ -14,61 +14,28 @@ module.exports = {
},
],
referenceSidebar: [
"reference/index",
{
type: "category",
label: "@auth/core",
link: {
type: "doc",
id: "reference/core/modules/main",
},
items: [
{
type: "autogenerated",
dirName: "reference/03-core/modules",
// See: https://github.com/facebook/docusaurus/issues/5689
// exclude: ["index"],
},
{
type: "category",
label: "Reflections",
collapsed: true,
className: "reflection-category", // See src/index.css
items: [{ type: "autogenerated", dirName: "reference/03-core" }],
},
],
link: { type: "doc", id: "reference/core/index" },
items: [{ type: "autogenerated", dirName: "reference/core" }],
},
{
type: "category",
label: "@auth/sveltekit",
link: { type: "doc", id: "reference/sveltekit/modules/main" },
items: [
{ type: "autogenerated", dirName: "reference/04-sveltekit/modules" },
{
type: "category",
label: "Reflections",
collapsed: true,
className: "reflection-category", // See src/index.css
items: [{ type: "autogenerated", dirName: "reference/04-sveltekit" }],
},
],
link: { type: "doc", id: "reference/sveltekit/index" },
items: [{ type: "autogenerated", dirName: "reference/sveltekit" }],
},
{
type: "category",
label: "@auth/solid-start",
link: {
type: "doc",
id: "reference/solidstart/index",
},
items: ["reference/solidstart/client", "reference/solidstart/protected"],
link: { type: "doc", id: "reference/solidstart/index" },
items: [{ type: "autogenerated", dirName: "reference/04-solidstart" }],
},
{
type: "category",
label: "@auth/nextjs",
link: {
type: "doc",
id: "reference/nextjs/index",
},
link: { type: "doc", id: "reference/nextjs/index" },
items: [
"reference/nextjs/client",
{
@@ -83,24 +50,8 @@ module.exports = {
label: "Database Adapters",
link: { type: "doc", id: "reference/adapters/overview" },
items: [
{
type: "autogenerated",
dirName: "reference/06-adapters",
// See: https://github.com/facebook/docusaurus/issues/5689
// exclude: ["index"],
},
],
},
{
type: "category",
label: "OAuth Providers",
items: [
{
type: "autogenerated",
dirName: "reference/05-oauth-providers",
// See: https://github.com/facebook/docusaurus/issues/5689
// exclude: ["index"],
},
{ type: "doc", id: "reference/adapter/firebase/index" },
{ type: "autogenerated", dirName: "reference/06-adapters" },
],
},
"reference/utilities/client",

View File

@@ -7,7 +7,7 @@ import { Auth } from "@auth/core"
import $1 from "@auth/core/providers/$2"
const request = new Request("https://example.com")
const resposne = await AuthHandler(request, {
const response = await AuthHandler(request, {
providers: [$1({ clientId: "", clientSecret: "" })],
})
```

View File

@@ -9,7 +9,7 @@ import Auth from "@auth/core"
import { $1 } from "@auth/core/providers/$2"
const request = new Request("https://example.com")
const resposne = await AuthHandler(request, {
const response = await AuthHandler(request, {
providers: [$1({ clientId: "", clientSecret: "" })],
})
```

View File

@@ -272,27 +272,4 @@ html[data-theme="dark"] #carbonads > span {
html[data-theme="dark"] #carbonads .carbon-poweredby {
color: #aaa;
background: #1e2021;
}
/*
This is a hack to hide the "Reflection" category and "main" module from the sidebar.
This is because:
1. opening any page under the "Reflection" category would hide the entire sidebar.
2. the "main" module would show up twice.
See sidebars.js
*/
.reflection-category,
.theme-doc-sidebar-item-link-level-2 [href="/reference/core/modules/main"],
.theme-doc-sidebar-item-link-level-2
[href="/reference/sveltekit/modules/main"] {
display: none;
}
/*
HACK: to hide the "Classes" header and duplicate items together with the "typedoc-plugin-markdown" patch.
See: https://github.com/TypeStrong/typedoc/issues/2006
*/
/* h3.anchor + p:has(code, strong), */ /** hack did not work as it hides property types elsewhere */
#classes {
display: none;
}
}

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 99.881 86.449">
<g id="asgardeo-trifactor-logo-dark-16x40" transform="translate(-553.024 -388.98)">
<path id="Path_264" data-name="Path 264" d="M743.533,388.98l9.161,15.892-10.153,17.6h20.306l9.209,15.892H714.97Z" transform="translate(-119.151 0)" fill="#ff7300"/>
<path id="Path_265" data-name="Path 265" d="M705.95,438.364l9.209-15.892h20.306l-10.153-17.6,9.162-15.892,28.6,49.393Z" transform="translate(-152.926 0.009)" fill="#ff7300"/>
<path id="Path_266" data-name="Path 266" d="M749.175,446.183l-10.153-17.6-10.2,17.6H710.46l28.6-49.393,28.515,49.393Z" transform="translate(-136.043 29.246)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 711 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 99.881 86.449">
<g id="asgardeo-trifactor-logo-light-16x40" transform="translate(-553.024 -388.98)">
<path id="Path_264" data-name="Path 264" d="M743.533,388.98l9.161,15.892-10.153,17.6h20.306l9.209,15.892H714.97Z" transform="translate(-119.151)" fill="#ff7300"/>
<path id="Path_265" data-name="Path 265" d="M705.95,438.364l9.209-15.892h20.306l-10.153-17.6,9.162-15.892,28.6,49.393Z" transform="translate(-152.926 0.009)" fill="#ff7300"/>
<path id="Path_266" data-name="Path 266" d="M749.175,446.183l-10.153-17.6-10.2,17.6H710.46l28.6-49.393,28.515,49.393Z" transform="translate(-136.043 29.246)" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 722 B

View File

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" role="img">
<!-- <path d="M15.968 0 7.684 16.57 3.763 8.726H.949L7.684 21.47 18.783 0h-2.815ZM6.767 27.162v2.878h2.498v-7.747l-2.498 4.869Z" fill="#5077C5"/> -->
<path d="M 22.102,0 13.818,16.57 9.897,8.726 H 7.083 L 13.818,21.47 24.917,0 Z m -9.201,27.162 v 2.878 h 2.498 v -7.747 z" fill="#5077c5"/>
</svg>

After

Width:  |  Height:  |  Size: 408 B

View File

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" role="img">
<!-- <path d="M15.968 0 7.684 16.57 3.763 8.726H.949L7.684 21.47 18.783 0h-2.815ZM6.767 27.162v2.878h2.498v-7.747l-2.498 4.869Z" fill="#5077C5"/> -->
<path d="M 22.102,0 13.818,16.57 9.897,8.726 H 7.083 L 13.818,21.47 24.917,0 Z m -9.201,27.162 v 2.878 h 2.498 v -7.747 z" fill="#5077c5"/>
</svg>

After

Width:  |  Height:  |  Size: 408 B

5
docs/static/img/providers/notion.svg vendored Normal file
View File

@@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>Notion icon</title>
<path d="M6.017 4.313l55.333 -4.087c6.797 -0.583 8.543 -0.19 12.817 2.917l17.663 12.443c2.913 2.14 3.883 2.723 3.883 5.053v68.243c0 4.277 -1.553 6.807 -6.99 7.193L24.467 99.967c-4.08 0.193 -6.023 -0.39 -8.16 -3.113L3.3 79.94c-2.333 -3.113 -3.3 -5.443 -3.3 -8.167V11.113c0 -3.497 1.553 -6.413 6.017 -6.8z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M61.35 0.227l-55.333 4.087C1.553 4.7 0 7.617 0 11.113v60.66c0 2.723 0.967 5.053 3.3 8.167l13.007 16.913c2.137 2.723 4.08 3.307 8.16 3.113l64.257 -3.89c5.433 -0.387 6.99 -2.917 6.99 -7.193V20.64c0 -2.21 -0.873 -2.847 -3.443 -4.733L74.167 3.143c-4.273 -3.107 -6.02 -3.5 -12.817 -2.917zM25.92 19.523c-5.247 0.353 -6.437 0.433 -9.417 -1.99L8.927 11.507c-0.77 -0.78 -0.383 -1.753 1.557 -1.947l53.193 -3.887c4.467 -0.39 6.793 1.167 8.54 2.527l9.123 6.61c0.39 0.197 1.36 1.36 0.193 1.36l-54.933 3.307 -0.68 0.047zM19.803 88.3V30.367c0 -2.53 0.777 -3.697 3.103 -3.893L86 22.78c2.14 -0.193 3.107 1.167 3.107 3.693v57.547c0 2.53 -0.39 4.67 -3.883 4.863l-60.377 3.5c-3.493 0.193 -5.043 -0.97 -5.043 -4.083zm59.6 -54.827c0.387 1.75 0 3.5 -1.75 3.7l-2.91 0.577v42.773c-2.527 1.36 -4.853 2.137 -6.797 2.137 -3.107 0 -3.883 -0.973 -6.21 -3.887l-19.03 -29.94v28.967l6.02 1.363s0 3.5 -4.857 3.5l-13.39 0.777c-0.39 -0.78 0 -2.723 1.357 -3.11l3.497 -0.97v-38.3L30.48 40.667c-0.39 -1.75 0.58 -4.277 3.3 -4.473l14.367 -0.967 19.8 30.327v-26.83l-5.047 -0.58c-0.39 -2.143 1.163 -3.7 3.103 -3.89l13.4 -0.78z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,23 +1,16 @@
{
"excludeNotDocumented": true,
"$schema": "https://typedoc.org/schema.json",
"allReflectionsHaveOwnDocument": true,
"cleanOutputDir": true,
"disableSources": true,
"hideBreadcrumbs": true,
"excludeExternals": true,
"excludeInternal": true,
"excludeNotDocumented": true,
"excludePrivate": true,
"cleanOutputDir": true,
"excludeProtected": true,
"hideHierarchy": true,
"gitRevision": "main",
"hideBreadcrumbs": true,
"hideGenerator": true,
"intentionallyNotExported": [
"ReturnTypes",
"CallbackParameters",
"JsonValue"
],
"readme": "none",
"sort": ["kind", "static-first", "required-first", "alphabetical"],
"kindSortOrder": [
"Function",
"TypeAlias",
@@ -41,5 +34,13 @@
"IndexSignature",
"GetSignature",
"SetSignature"
]
}
],
"readme": "none",
"sort": [
"kind",
"static-first",
"required-first",
"alphabetical"
],
"symbolsWithOwnFile": "none"
}

View File

@@ -73,7 +73,7 @@
"value": "sveltekit.authjs.dev"
}
],
"destination": "https://authjs.dev/reference/sveltekit/modules/main"
"destination": "https://authjs.dev/reference/sveltekit"
},
{
"source": "/",
@@ -93,7 +93,7 @@
"value": "errors.authjs.dev"
}
],
"destination": "https://authjs.dev/reference/core/modules/errors/:path*"
"destination": "https://authjs.dev/reference/core/errors/:path*"
},
{
"source": "/:path(.*)",
@@ -123,7 +123,7 @@
"value": "providers.authjs.dev"
}
],
"destination": "https://authjs.dev/reference/core/functions/providers_:path.default"
"destination": "https://authjs.dev/reference/core/providers_:path.default"
}
]
}

View File

@@ -18,7 +18,8 @@
"lint": "prettier --check .",
"format": "prettier --write .",
"release": "release",
"version:pr": "node ./config/version-pr"
"version:pr": "node ./config/version-pr",
"e2e": "turbo run e2e --filter=next-auth-app"
},
"devDependencies": {
"@actions/core": "^1.10.0",
@@ -40,8 +41,6 @@
"prettier": "2.8.1",
"prettier-plugin-svelte": "^2.8.1",
"turbo": "1.6.3",
"typedoc": "^0.23.22",
"typedoc-plugin-markdown": "^3.14.0",
"typescript": "4.9.4"
},
"engines": {
@@ -63,7 +62,6 @@
"undici": "5.11.0"
},
"patchedDependencies": {
"typedoc-plugin-markdown@3.14.0": "patches/typedoc-plugin-markdown@3.14.0.patch",
"@balazsorban/monorepo-release@0.1.8": "patches/@balazsorban__monorepo-release@0.1.8.patch"
}
}

View File

@@ -60,7 +60,7 @@ The simplest way to use Dgraph is by copy pasting the unsecure schema into your
## Securing your database
Fore sake of security and mostly if your client directly communicate with the graphql server you obviously want to restrict the access to the types used by next-auth. That's why you see a lot of @auth directive alongide this types in the schema.
Fore sake of security and mostly if your client directly communicate with the graphql server you obviously want to restrict the access to the types used by next-auth. That's why you see a lot of @auth directive alongside this types in the schema.
### Dgraph.Authorization

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/dgraph-adapter",
"version": "1.0.4",
"version": "1.0.5",
"description": "Dgraph adapter for next-auth.",
"homepage": "https://authjs.dev",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -50,4 +50,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -85,7 +85,7 @@ export default NextAuth({
The table respects the single table design pattern. This has many advantages:
- Only one table to manage, monitor and provision.
- Querying relations is faster than with multi-table schemas (for eg. retreiving all sessions for a user).
- Querying relations is faster than with multi-table schemas (for eg. retrieving all sessions for a user).
- Only one table needs to be replicated, if you want to go multi-region.
Here is a schema of the table :

View File

@@ -1,7 +1,7 @@
{
"name": "@next-auth/dynamodb-adapter",
"repository": "https://github.com/nextauthjs/next-auth",
"version": "1.0.6",
"version": "3.0.1",
"description": "AWS DynamoDB adapter for next-auth.",
"keywords": [
"next-auth",
@@ -9,11 +9,18 @@
"oauth",
"dynamodb"
],
"type": "module",
"types": "./index.d.ts",
"homepage": "https://authjs.dev",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},
"main": "dist/index.js",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js"
}
},
"private": false,
"publishConfig": {
"access": "public"
@@ -26,7 +33,10 @@
},
"files": [
"README.md",
"dist"
"index.js",
"index.d.ts",
"index.d.ts.map",
"src"
],
"author": "Pol Marnette",
"license": "ISC",
@@ -41,7 +51,11 @@
"@next-auth/adapter-test": "workspace:*",
"@next-auth/tsconfig": "workspace:*",
"@shelf/jest-dynamodb": "^2.1.0",
"@types/uuid": "^9.0.0",
"jest": "^27.4.3",
"next-auth": "workspace:*"
},
"dependencies": {
"uuid": "^9.0.0"
}
}

View File

@@ -1,4 +1,4 @@
import { randomBytes } from "crypto"
import { v4 as uuid } from "uuid"
import type {
BatchWriteCommandInput,
@@ -12,16 +12,12 @@ import type {
VerificationToken,
} from "next-auth/adapters"
import { format, generateUpdateExpression } from "./utils"
export { format, generateUpdateExpression }
export interface DynamoDBAdapterOptions {
tableName?: string,
partitionKey?: string,
sortKey?: string,
indexName?: string,
indexPartitionKey?: string,
tableName?: string
partitionKey?: string
sortKey?: string
indexName?: string
indexPartitionKey?: string
indexSortKey?: string
}
@@ -30,17 +26,17 @@ export function DynamoDBAdapter(
options?: DynamoDBAdapterOptions
): Adapter {
const TableName = options?.tableName ?? "next-auth"
const pk = options?.partitionKey ?? 'pk'
const sk = options?.sortKey ?? 'sk'
const IndexName = options?.indexName ?? 'GSI1'
const GSI1PK = options?.indexPartitionKey ?? 'GSI1PK'
const GSI1SK = options?.indexSortKey ?? 'GSI1SK'
const pk = options?.partitionKey ?? "pk"
const sk = options?.sortKey ?? "sk"
const IndexName = options?.indexName ?? "GSI1"
const GSI1PK = options?.indexPartitionKey ?? "GSI1PK"
const GSI1SK = options?.indexSortKey ?? "GSI1SK"
return {
async createUser(data) {
const user: AdapterUser = {
...(data as any),
id: randomBytes(16).toString("hex"),
id: uuid(),
}
await client.put({
@@ -50,8 +46,8 @@ export function DynamoDBAdapter(
[pk]: `USER#${user.id}`,
[sk]: `USER#${user.id}`,
type: "USER",
[GSI1PK]: `USER#${user.email as string}`,
[GSI1SK]: `USER#${user.email as string}`,
[GSI1PK]: `USER#${user.email}`,
[GSI1SK]: `USER#${user.email}`,
}),
})
@@ -165,7 +161,7 @@ export function DynamoDBAdapter(
async linkAccount(data) {
const item = {
...data,
id: randomBytes(16).toString("hex"),
id: uuid(),
[pk]: `USER#${data.userId}`,
[sk]: `ACCOUNT#${data.provider}#${data.providerAccountId}`,
[GSI1PK]: `ACCOUNT#${data.provider}`,
@@ -229,7 +225,7 @@ export function DynamoDBAdapter(
},
async createSession(data) {
const session = {
id: randomBytes(16).toString("hex"),
id: uuid(),
...data,
}
await client.put({
@@ -327,3 +323,73 @@ export function DynamoDBAdapter(
},
}
}
// https://github.com/honeinc/is-iso-date/blob/master/index.js
const isoDateRE =
/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/
function isDate(value: any) {
return value && isoDateRE.test(value) && !isNaN(Date.parse(value))
}
const format = {
/** Takes a plain old JavaScript object and turns it into a Dynamodb object */
to(object: Record<string, any>) {
const newObject: Record<string, unknown> = {}
for (const key in object) {
const value = object[key]
if (value instanceof Date) {
// DynamoDB requires the TTL attribute be a UNIX timestamp (in secs).
if (key === "expires") newObject[key] = value.getTime() / 1000
else newObject[key] = value.toISOString()
} else newObject[key] = value
}
return newObject
},
/** Takes a Dynamo object and returns a plain old JavaScript object */
from<T = Record<string, unknown>>(object?: Record<string, any>): T | null {
if (!object) return null
const newObject: Record<string, unknown> = {}
for (const key in object) {
// Filter DynamoDB specific attributes so it doesn't get passed to core,
// to avoid revealing the type of database
if (["pk", "sk", "GSI1PK", "GSI1SK"].includes(key)) continue
const value = object[key]
if (isDate(value)) newObject[key] = new Date(value)
// hack to keep type property in account
else if (key === "type" && ["SESSION", "VT", "USER"].includes(value))
continue
// The expires property is stored as a UNIX timestamp in seconds, but
// JavaScript needs it in milliseconds, so multiply by 1000.
else if (key === "expires" && typeof value === "number")
newObject[key] = new Date(value * 1000)
else newObject[key] = value
}
return newObject as T
},
}
function generateUpdateExpression(object: Record<string, any>): {
UpdateExpression: string
ExpressionAttributeNames: Record<string, string>
ExpressionAttributeValues: Record<string, unknown>
} {
const formattedSession = format.to(object)
let UpdateExpression = "set"
const ExpressionAttributeNames: Record<string, string> = {}
const ExpressionAttributeValues: Record<string, unknown> = {}
for (const property in formattedSession) {
UpdateExpression += ` #${property} = :${property},`
ExpressionAttributeNames["#" + property] = property
ExpressionAttributeValues[":" + property] = formattedSession[property]
}
UpdateExpression = UpdateExpression.slice(0, -1)
return {
UpdateExpression,
ExpressionAttributeNames,
ExpressionAttributeValues,
}
}
export { format, generateUpdateExpression }

View File

@@ -1,67 +0,0 @@
// https://github.com/honeinc/is-iso-date/blob/master/index.js
const isoDateRE =
/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/
function isDate(value: any) {
return value && isoDateRE.test(value) && !isNaN(Date.parse(value))
}
export const format = {
/** Takes a plain old JavaScript object and turns it into a Dynamodb object */
to(object: Record<string, any>) {
const newObject: Record<string, unknown> = {}
for (const key in object) {
const value = object[key]
if (value instanceof Date) {
// DynamoDB requires the TTL attribute be a UNIX timestamp (in secs).
if (key === "expires") newObject[key] = value.getTime() / 1000
else newObject[key] = value.toISOString()
} else newObject[key] = value
}
return newObject
},
/** Takes a Dynamo object and returns a plain old JavaScript object */
from<T = Record<string, unknown>>(object?: Record<string, any>): T | null {
if (!object) return null
const newObject: Record<string, unknown> = {}
for (const key in object) {
// Filter DynamoDB specific attributes so it doesn't get passed to core,
// to avoid revealing the type of database
if (["pk", "sk", "GSI1PK", "GSI1SK"].includes(key)) continue
const value = object[key]
if (isDate(value)) newObject[key] = new Date(value)
// hack to keep type property in account
else if (key === "type" && ["SESSION", "VT", "USER"].includes(value))
continue
// The expires property is stored as a UNIX timestamp in seconds, but
// JavaScript needs it in milliseconds, so multiply by 1000.
else if (key === "expires" && typeof value === "number")
newObject[key] = new Date(value * 1000)
else newObject[key] = value
}
return newObject as T
},
}
export function generateUpdateExpression(object: Record<string, any>): {
UpdateExpression: string
ExpressionAttributeNames: Record<string, string>
ExpressionAttributeValues: Record<string, unknown>
} {
const formatedSession = format.to(object)
let UpdateExpression = "set"
const ExpressionAttributeNames: Record<string, string> = {}
const ExpressionAttributeValues: Record<string, unknown> = {}
for (const property in formatedSession) {
UpdateExpression += ` #${property} = :${property},`
ExpressionAttributeNames["#" + property] = property
ExpressionAttributeValues[":" + property] = formatedSession[property]
}
UpdateExpression = UpdateExpression.slice(0, -1)
return {
UpdateExpression,
ExpressionAttributeNames,
ExpressionAttributeValues,
}
}

View File

@@ -1,4 +1,4 @@
import { format } from "../src/utils"
import { format } from "../src/"
describe("dynamodb utils.format", () => {
it("format.to() preserves non-Date non-expires properties", () => {

View File

@@ -2,7 +2,15 @@
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
"outDir": ".",
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"skipDefaultLibCheck": true,
"strictNullChecks": true,
"stripInternal": true,
"declarationMap": true,
"declaration": true
},
"exclude": ["tests", "dist", "jest.config.js", "jest-dynamodb-config.js"]
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,16 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.1.3](https://github.com/nextauthjs/adapters/compare/@next-auth/firebase-adapter@0.1.2...@next-auth/firebase-adapter@0.1.3) (2021-08-17)
**Note:** Version bump only for package @next-auth/firebase-adapter
## [0.1.2](https://github.com/nextauthjs/adapters/compare/@next-auth/firebase-adapter@0.1.1...@next-auth/firebase-adapter@0.1.2) (2021-07-02)
**Note:** Version bump only for package @next-auth/firebase-adapter
## [0.1.1](https://github.com/nextauthjs/adapters/compare/@next-auth/firebase-adapter@0.1.0...@next-auth/firebase-adapter@0.1.1) (2021-06-30)
**Note:** Version bump only for package @next-auth/firebase-adapter

View File

@@ -1,8 +1,8 @@
<p align="center">
<br/>
<a href="https://authjs.dev" target="_blank">
<img height="64px" src="https://authjs.dev/img/logo/logo-sm.png" /></a><img height="64px" src="https://raw.githubusercontent.com/nextauthjs/adapters/main/packages/firebase/logo.svg" />
<h3 align="center"><b>Firebase Adapter</b> - NextAuth.js</h3>
<img height="64px" src="https://authjs.dev/img/logo/logo-sm.png" /></a><img height="64px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/adapter-firebase/logo.svg" />
<h3 align="center"><b>Firebase Adapter</b> - Auth.js</h3>
<p align="center">
Open Source. Full Stack. Own Your Data.
</p>
@@ -13,72 +13,12 @@
</p>
</p>
## Overview
This is the Firebase Adapter for [`auth.js`](https://authjs.dev). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
This is the official Firebase Adapter for [Auth.js](https://authjs.dev) / [NextAuth.js](https://next-auth.js.org/), using the [Firebase Admin SDK](https://firebase.google.com/docs/admin/setup) and [Firestore](https://firebase.google.com/docs/firestore).
You can find more Firebase information in the docs at [authjs.dev/reference/adapters/firebase](https://authjs.dev/reference/adapters/firebase).
## Documentation
## Getting Started
1. Install `next-auth` and `@next-auth/firebase-adapter`.
```js
npm install next-auth @next-auth/firebase-adapter
```
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
```js
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import { FirestoreAdapter } from "@next-auth/firebase-adapter"
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore"
const app = initializeApp({ projectId: "next-auth-test" });
const firestore = getFirestore(app);
// For more information on each option (and a full list of options) go to
// https://authjs.dev/reference/configuration/auth-options
export default NextAuth({
// https://authjs.dev/reference/providers/oauth-builtin
providers: [
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
adapter: FirestoreAdapter(firestore),
...
})
```
## Options
When initializing the firestore adapter, you must pass in the firebase config object with the details from your project. More details on how to obtain that config object can be found [here](https://support.google.com/firebase/answer/7015592).
An example firebase config looks like this:
```js
const firebaseConfig = {
apiKey: "AIzaSyDOCAbC123dEf456GhI789jKl01-MnO",
authDomain: "myapp-project-123.firebaseapp.com",
databaseURL: "https://myapp-project-123.firebaseio.com",
projectId: "myapp-project-123",
storageBucket: "myapp-project-123.appspot.com",
messagingSenderId: "65211879809",
appId: "1:65211879909:web:3ae38ef1cdcb2e01fe5f0c",
measurementId: "G-8GSGZQ44ST",
}
```
See [firebase.google.com/docs/web/setup](https://firebase.google.com/docs/web/setup) for more details.
> **From Firebase - Caution**: We do not recommend manually modifying an app's Firebase config file or object. If you initialize an app with invalid or missing values for any of these required "Firebase options", then your end users may experience serious issues.
>
> For open source projects, we generally do not recommend including the app's Firebase config file or object in source control because, in most cases, your users should create their own Firebase projects and point their apps to their own Firebase resources (via their own Firebase config file or object).
Check out the [documentation](https://authjs.dev/reference/adapter/firebase) to learn how to use this adapter in your project.
## Contributing

View File

@@ -1,5 +1,8 @@
{
"emulators": {
"firestore": {
"rules": "firestore.rules"
},
"emulator": {
"firestore": {
"port": 8080
}

View File

@@ -0,0 +1,10 @@
rules_version = '2';
// Deny read/write access to all users under any conditions
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}

View File

@@ -1 +0,0 @@
module.exports = require("@next-auth/adapter-test/jest/jest-preset")

View File

@@ -33,4 +33,4 @@
<circle cx="144" cy="144" r="40" fill="#757575"/>
<path d="M144 146l-18 8v-8l18-8 18 8v7-1.5 2.5zm0-22l18 8v8l-18-8-18 8v-8zm6.75 29l9 4-15.75 7v-8z" fill="#fff" fill-rule="evenodd"/>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/firebase-adapter",
"version": "1.0.3",
"version": "2.0.1",
"description": "Firebase adapter for next-auth.",
"homepage": "https://authjs.dev",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -12,35 +12,44 @@
"Nico Domino <yo@ndo.dev>",
"Alex Meuer <github@alexmeuer.com>"
],
"main": "dist/index.js",
"type": "module",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js"
}
},
"files": [
"dist",
"index.d.ts"
"src",
"*.js",
"*.d.ts*"
],
"license": "ISC",
"keywords": [
"next-auth",
"next.js",
"firebase"
"firebase",
"firebase-admin"
],
"private": false,
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "tsc -w",
"build": "tsc",
"test": "FIRESTORE_EMULATOR_HOST=localhost:8080 firebase --token '$FIREBASE_TOKEN' emulators:exec --only firestore --project next-auth-test jest"
"test": "firebase emulators:exec --only firestore --project next-auth-test 'jest -c tests/jest.config.js'"
},
"peerDependencies": {
"firebase": "^9.7.0",
"firebase-admin": "^11.4.1",
"next-auth": "^4"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:*",
"@next-auth/tsconfig": "workspace:*",
"firebase": "^9.14.0",
"firebase-admin": "^11.4.1",
"firebase-tools": "^11.16.1",
"jest": "^27.4.3",
"jest": "^29.3.1",
"next-auth": "workspace:*"
}
}
}

View File

@@ -1,58 +0,0 @@
import { Timestamp } from "firebase/firestore"
import type {
FirestoreDataConverter,
QueryDocumentSnapshot,
WithFieldValue,
} from "firebase/firestore"
const isTimestamp = (value: unknown): value is Timestamp =>
typeof value === "object" && value !== null && value instanceof Timestamp
interface GetConverterOptions {
excludeId?: boolean
}
export const getConverter = <Document extends Record<string, unknown>>(
options?: GetConverterOptions
): FirestoreDataConverter<Document> => ({
// `PartialWithFieldValue` implicitly types `object` as `any`, so we want to explicitly type it
toFirestore(object: WithFieldValue<Document>) {
const document: Record<string, unknown> = {}
Object.keys(object).forEach((key) => {
if (object[key] !== undefined) {
document[key] = object[key]
}
})
return document
},
// We need to explicitly type `snapshot` since it uses `DocumentData` for generic type
fromFirestore(snapshot: QueryDocumentSnapshot<Document>) {
if (!snapshot.exists()) {
return snapshot
}
let document: Document = snapshot.data()
if (!options?.excludeId) {
document = {
...document,
id: snapshot.id,
}
}
for (const key in document) {
const value = document[key]
if (isTimestamp(value)) {
document = {
...document,
[key]: value.toDate(),
}
}
}
return document
},
})

View File

@@ -1,20 +1,35 @@
import { initializeApp } from "firebase/app"
import type { FirebaseOptions } from "firebase/app"
/**
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
* <span>
* Official <b>Firebase</b> adapter for Auth.js / NextAuth.js,
* using the <a href="https://firebase.google.com/docs/admin/setup">Firebase Admin SDK</a>
* &nbsp;and <a href="https://firebase.google.com/docs/firestore">Firestore</a>.</span>
* <a href="https://firebase.google.com/">
* <img style={{display: "block"}} src="https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/adapter-firebase/logo.svg" height="48" width="48"/>
* </a>
* </div>
*
* ## Installation
*
* ```bash npm2yarn2pnpm
* npm install next-auth @next-auth/firebase-adapter firebase-admin
* ```
*
* ## References
* - [`GOOGLE_APPLICATION_CREDENTIALS` environment variable](https://cloud.google.com/docs/authentication/application-default-credentials#GAC)
* - [Firebase Admin SDK setup](https://firebase.google.com/docs/admin/setup#initialize-sdk)
*
* @module @next-auth/firebase-adapter
*/
import { type AppOptions, getApps, initializeApp } from "firebase-admin/app"
import {
addDoc,
collection,
deleteDoc,
doc,
getDoc,
getDocs,
Firestore,
getFirestore,
limit,
query,
runTransaction,
setDoc,
where,
connectFirestoreEmulator,
} from "firebase/firestore"
initializeFirestore,
Timestamp,
} from "firebase-admin/firestore"
import type {
Adapter,
@@ -24,259 +39,419 @@ import type {
VerificationToken,
} from "next-auth/adapters"
import { getConverter } from "./converter"
export type IndexableObject = Record<string, unknown>
export interface FirestoreAdapterOptions {
emulator?: {
host?: string
port?: number
}
/** Configure the Firebase Adapter. */
export interface FirebaseAdapterConfig extends AppOptions {
/**
* The name of the app passed to {@link https://firebase.google.com/docs/reference/admin/node/firebase-admin.md#initializeapp `initializeApp()`}.
*/
name?: string
firestore?: Firestore
/**
* Use this option if mixed `snake_case` and `camelCase` field names in the database is an issue for you.
* Passing `snake_case` will convert all field and collection names to `snake_case`.
* E.g. the collection `verificationTokens` will be `verification_tokens`,
* and fields like `emailVerified` will be `email_verified` instead.
*
*
* @example
* ```ts title="pages/api/auth/[...nextauth].ts"
* import NextAuth from "next-auth"
* import { FirestoreAdapter } from "@next-auth/firebase-adapter"
*
* export default NextAuth({
* adapter: FirestoreAdapter({ namingStrategy: "snake_case" })
* // ...
* })
* ```
*/
namingStrategy?: "snake_case"
}
export function FirestoreAdapter({
emulator,
...firebaseOptions
}: FirebaseOptions & FirestoreAdapterOptions): Adapter {
const firebaseApp = initializeApp(firebaseOptions)
const db = getFirestore(firebaseApp)
/**
* #### Usage
*
* First, create a Firebase project and generate a service account key.
* Visit: `https://console.firebase.google.com/u/0/project/{project-id}/settings/serviceaccounts/adminsdk` (replace `{project-id}` with your project's id)
*
* Now you have a few options to authenticate with the Firebase Admin SDK in your app:
*
* ##### 1. `GOOGLE_APPLICATION_CREDENTIALS` environment variable:
* - Download the service account key and save it in your project. (Make sure to add the file to your `.gitignore`!)
* - Add [`GOOGLE_APPLICATION_CREDENTIALS`](https://cloud.google.com/docs/authentication/application-default-credentials#GAC) to your environment variables and point it to the service account key file.
* - The adapter will automatically pick up the environment variable and use it to authenticate with the Firebase Admin SDK.
*
* @example
* ```ts title="pages/api/auth/[...nextauth].ts"
* import NextAuth from "next-auth"
* import { FirestoreAdapter } from "@next-auth/firebase-adapter"
*
* export default NextAuth({
* adapter: FirestoreAdapter(),
* // ...
* })
* ```
*
* ##### 2. Service account values as environment variables
*
* - Download the service account key to a temporary location. (Make sure to not commit this file to your repository!)
* - Add the following environment variables to your project: `FIREBASE_PROJECT_ID`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_PRIVATE_KEY`.
* - Pass the config to the adapter, using the environment variables as shown in the example below.
*
* @example
* ```ts title="pages/api/auth/[...nextauth].ts"
* import NextAuth from "next-auth"
* import { FirestoreAdapter } from "@next-auth/firebase-adapter"
* import { cert } from "firebase-admin/app"
*
* export default NextAuth({
* adapter: FirestoreAdapter({
* credential: cert({
* projectId: process.env.FIREBASE_PROJECT_ID,
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
* })
* })
* // ...
* })
* ```
*
* ##### 3. Use an existing Firestore instance
*
* If you already have a Firestore instance, you can pass that to the adapter directly instead.
*
* :::note
* When passing an instance and in a serverless environment, remember to handle duplicate app initialization.
* :::
*
* :::tip
* You can use the {@link initFirestore} utility to initialize the app and get an instance safely.
* :::
*
* @example
* ```ts title="pages/api/auth/[...nextauth].ts"
* import NextAuth from "next-auth"
* import { FirestoreAdapter } from "@next-auth/firebase-adapter"
* import { firestore } from "lib/firestore"
*
* export default NextAuth({
* adapter: FirestoreAdapter(firestore),
* // ...
* })
* ```
*/
export function FirestoreAdapter(
config?: FirebaseAdapterConfig | Firestore
): Adapter {
const { db, namingStrategy = "default" } =
config instanceof Firestore
? { db: config }
: { ...config, db: config?.firestore ?? initFirestore(config) }
if (emulator) {
connectFirestoreEmulator(
db,
emulator?.host ?? "localhost",
emulator?.port ?? 3001
)
}
const Users = collection(db, "users").withConverter(
getConverter<AdapterUser & IndexableObject>()
)
const Sessions = collection(db, "sessions").withConverter(
getConverter<AdapterSession & IndexableObject>()
)
const Accounts = collection(db, "accounts").withConverter(
getConverter<AdapterAccount>()
)
const VerificationTokens = collection(db, "verificationTokens").withConverter(
getConverter<VerificationToken & IndexableObject>({ excludeId: true })
)
const preferSnakeCase = namingStrategy === "snake_case"
const C = collestionsFactory(db, preferSnakeCase)
const mapper = mapFieldsFactory(preferSnakeCase)
return {
async createUser(newUser) {
const userRef = await addDoc(Users, newUser)
const userSnapshot = await getDoc(userRef)
async createUser(userInit) {
const { id: userId } = await C.users.add(userInit as AdapterUser)
if (userSnapshot.exists() && Users.converter) {
return Users.converter.fromFirestore(userSnapshot)
}
const user = await getDoc(C.users.doc(userId))
if (!user) throw new Error("[createUser] Failed to fetch created user")
throw new Error("[createUser] Failed to create user")
return user
},
async getUser(id) {
const userSnapshot = await getDoc(doc(Users, id))
if (userSnapshot.exists() && Users.converter) {
return Users.converter.fromFirestore(userSnapshot)
}
return null
return await getDoc(C.users.doc(id))
},
async getUserByEmail(email) {
const userQuery = query(Users, where("email", "==", email), limit(1))
const userSnapshots = await getDocs(userQuery)
const userSnapshot = userSnapshots.docs[0]
if (userSnapshot?.exists() && Users.converter) {
return Users.converter.fromFirestore(userSnapshot)
}
return null
return await getOneDoc(C.users.where("email", "==", email))
},
async getUserByAccount({ provider, providerAccountId }) {
const accountQuery = query(
Accounts,
where("provider", "==", provider),
where("providerAccountId", "==", providerAccountId),
limit(1)
const account = await getOneDoc(
C.accounts
.where("provider", "==", provider)
.where(mapper.toDb("providerAccountId"), "==", providerAccountId)
)
const accountSnapshots = await getDocs(accountQuery)
const accountSnapshot = accountSnapshots.docs[0]
if (!account) return null
if (accountSnapshot?.exists()) {
const { userId } = accountSnapshot.data()
const userDoc = await getDoc(doc(Users, userId))
if (userDoc.exists() && Users.converter) {
return Users.converter.fromFirestore(userDoc)
}
}
return null
return await getDoc(C.users.doc(account.userId))
},
async updateUser(partialUser) {
const userRef = doc(Users, partialUser.id)
if (!partialUser.id) throw new Error("[updateUser] Missing id")
await setDoc(userRef, partialUser, { merge: true })
const userRef = C.users.doc(partialUser.id)
const userSnapshot = await getDoc(userRef)
await userRef.set(partialUser, { merge: true })
if (userSnapshot.exists() && Users.converter) {
return Users.converter.fromFirestore(userSnapshot)
}
const user = await getDoc(userRef)
if (!user) throw new Error("[updateUser] Failed to fetch updated user")
throw new Error("[updateUser] Failed to update user")
return user
},
async deleteUser(userId) {
const userRef = doc(Users, userId)
const accountsQuery = query(Accounts, where("userId", "==", userId))
const sessionsQuery = query(Sessions, where("userId", "==", userId))
await db.runTransaction(async (transaction) => {
const accounts = await C.accounts
.where(mapper.toDb("userId"), "==", userId)
.get()
const sessions = await C.sessions
.where(mapper.toDb("userId"), "==", userId)
.get()
// TODO: May be better to use events instead of transactions?
await runTransaction(db, async (transaction) => {
const accounts = await getDocs(accountsQuery)
const sessions = await getDocs(sessionsQuery)
transaction.delete(C.users.doc(userId))
transaction.delete(userRef)
accounts.forEach((account) => transaction.delete(account.ref))
sessions.forEach((session) => transaction.delete(session.ref))
})
},
async linkAccount(account) {
const accountRef = await addDoc(Accounts, account)
const accountSnapshot = await getDoc(accountRef)
if (accountSnapshot.exists() && Accounts.converter) {
return Accounts.converter.fromFirestore(accountSnapshot)
}
async linkAccount(accountInit) {
const ref = await C.accounts.add(accountInit)
const account = await ref.get().then((doc) => doc.data())
return account ?? null
},
async unlinkAccount({ provider, providerAccountId }) {
const accountQuery = query(
Accounts,
where("provider", "==", provider),
where("providerAccountId", "==", providerAccountId),
limit(1)
await deleteDocs(
C.accounts
.where("provider", "==", provider)
.where(mapper.toDb("providerAccountId"), "==", providerAccountId)
.limit(1)
)
const accountSnapshots = await getDocs(accountQuery)
const accountSnapshot = accountSnapshots.docs[0]
if (accountSnapshot?.exists()) {
await deleteDoc(accountSnapshot.ref)
}
},
async createSession(session) {
const sessionRef = await addDoc(Sessions, session)
const sessionSnapshot = await getDoc(sessionRef)
async createSession(sessionInit) {
const ref = await C.sessions.add(sessionInit)
const session = await ref.get().then((doc) => doc.data())
if (sessionSnapshot.exists() && Sessions.converter) {
return Sessions.converter.fromFirestore(sessionSnapshot)
}
if (session) return session ?? null
throw new Error("[createSession] Failed to create session")
throw new Error("[createSession] Failed to fetch created session")
},
async getSessionAndUser(sessionToken) {
const sessionQuery = query(
Sessions,
where("sessionToken", "==", sessionToken),
limit(1)
const session = await getOneDoc(
C.sessions.where(mapper.toDb("sessionToken"), "==", sessionToken)
)
const sessionSnapshots = await getDocs(sessionQuery)
const sessionSnapshot = sessionSnapshots.docs[0]
if (!session) return null
if (sessionSnapshot?.exists() && Sessions.converter) {
const session = Sessions.converter.fromFirestore(sessionSnapshot)
const userDoc = await getDoc(doc(Users, session.userId))
const user = await getDoc(C.users.doc(session.userId))
if (!user) return null
if (userDoc.exists() && Users.converter) {
const user = Users.converter.fromFirestore(userDoc)
return { session, user }
}
}
return null
return { session, user }
},
async updateSession(partialSession) {
const sessionQuery = query(
Sessions,
where("sessionToken", "==", partialSession.sessionToken),
limit(1)
)
const sessionSnapshots = await getDocs(sessionQuery)
const sessionSnapshot = sessionSnapshots.docs[0]
const sessionId = await db.runTransaction(async (transaction) => {
const sessionSnapshot = (
await transaction.get(
C.sessions
.where(
mapper.toDb("sessionToken"),
"==",
partialSession.sessionToken
)
.limit(1)
)
).docs[0]
if (!sessionSnapshot?.exists) return null
if (sessionSnapshot?.exists()) {
await setDoc(sessionSnapshot.ref, partialSession, { merge: true })
transaction.set(sessionSnapshot.ref, partialSession, { merge: true })
const sessionDoc = await getDoc(sessionSnapshot.ref)
return sessionSnapshot.id
})
if (sessionDoc?.exists() && Sessions.converter) {
const session = Sessions.converter.fromFirestore(sessionDoc)
if (!sessionId) return null
return session
}
}
return null
const session = await getDoc(C.sessions.doc(sessionId))
if (session) return session
throw new Error("[updateSession] Failed to fetch updated session")
},
async deleteSession(sessionToken) {
const sessionQuery = query(
Sessions,
where("sessionToken", "==", sessionToken),
limit(1)
await deleteDocs(
C.sessions
.where(mapper.toDb("sessionToken"), "==", sessionToken)
.limit(1)
)
const sessionSnapshots = await getDocs(sessionQuery)
const sessionSnapshot = sessionSnapshots.docs[0]
if (sessionSnapshot?.exists()) {
await deleteDoc(sessionSnapshot.ref)
}
},
async createVerificationToken(verificationToken) {
const verificationTokenRef = await addDoc(
VerificationTokens,
verificationToken
)
const verificationTokenSnapshot = await getDoc(verificationTokenRef)
if (verificationTokenSnapshot.exists() && VerificationTokens.converter) {
const { id, ...verificationToken } =
VerificationTokens.converter.fromFirestore(verificationTokenSnapshot)
return verificationToken
}
await C.verification_tokens.add(verificationToken)
return verificationToken
},
async useVerificationToken({ identifier, token }) {
const verificationTokensQuery = query(
VerificationTokens,
where("identifier", "==", identifier),
where("token", "==", token),
limit(1)
)
const verificationTokenSnapshots = await getDocs(verificationTokensQuery)
const verificationTokenSnapshot = verificationTokenSnapshots.docs[0]
const verificationTokenSnapshot = (
await C.verification_tokens
.where("identifier", "==", identifier)
.where("token", "==", token)
.limit(1)
.get()
).docs[0]
if (verificationTokenSnapshot?.exists() && VerificationTokens.converter) {
await deleteDoc(verificationTokenSnapshot.ref)
if (!verificationTokenSnapshot) return null
const { id, ...verificationToken } =
VerificationTokens.converter.fromFirestore(verificationTokenSnapshot)
return verificationToken
}
return null
const data = verificationTokenSnapshot.data()
await verificationTokenSnapshot.ref.delete()
return data
},
}
}
// for consistency, store all fields as snake_case in the database
const MAP_TO_FIRESTORE: Record<string, string | undefined> = {
userId: "user_id",
sessionToken: "session_token",
providerAccountId: "provider_account_id",
emailVerified: "email_verified",
}
const MAP_FROM_FIRESTORE: Record<string, string | undefined> = {}
for (const key in MAP_TO_FIRESTORE) {
MAP_FROM_FIRESTORE[MAP_TO_FIRESTORE[key]!] = key
}
const identity = <T>(x: T) => x
/** @internal */
export function mapFieldsFactory(preferSnakeCase?: boolean) {
if (preferSnakeCase) {
return {
toDb: (field: string) => MAP_TO_FIRESTORE[field] ?? field,
fromDb: (field: string) => MAP_FROM_FIRESTORE[field] ?? field,
}
}
return { toDb: identity, fromDb: identity }
}
/** @internal */
function getConverter<Document extends Record<string, any>>(options: {
excludeId?: boolean
preferSnakeCase?: boolean
}): FirebaseFirestore.FirestoreDataConverter<Document> {
const mapper = mapFieldsFactory(options?.preferSnakeCase ?? false)
return {
toFirestore(object) {
const document: Record<string, unknown> = {}
for (const key in object) {
if (key === "id") continue
const value = object[key]
if (value !== undefined) {
document[mapper.toDb(key)] = value
} else {
console.warn(`FirebaseAdapter: value for key "${key}" is undefined`)
}
}
return document
},
fromFirestore(
snapshot: FirebaseFirestore.QueryDocumentSnapshot<Document>
): Document {
const document = snapshot.data()! // we can guarantee it exists
const object: Record<string, unknown> = {}
if (!options?.excludeId) {
object.id = snapshot.id
}
for (const key in document) {
let value: any = document[key]
if (value instanceof Timestamp) value = value.toDate()
object[mapper.fromDb(key)] = value
}
return object as Document
},
}
}
/** @internal */
export async function getOneDoc<T>(
querySnapshot: FirebaseFirestore.Query<T>
): Promise<T | null> {
const querySnap = await querySnapshot.limit(1).get()
return querySnap.docs[0]?.data() ?? null
}
/** @internal */
async function deleteDocs<T>(
querySnapshot: FirebaseFirestore.Query<T>
): Promise<void> {
const querySnap = await querySnapshot.get()
for (const doc of querySnap.docs) {
await doc.ref.delete()
}
}
/** @internal */
export async function getDoc<T>(
docRef: FirebaseFirestore.DocumentReference<T>
): Promise<T | null> {
const docSnap = await docRef.get()
return docSnap.data() ?? null
}
/** @internal */
export function collestionsFactory(
db: FirebaseFirestore.Firestore,
preferSnakeCase = false
) {
return {
users: db
.collection("users")
.withConverter(getConverter<AdapterUser>({ preferSnakeCase })),
sessions: db
.collection("sessions")
.withConverter(getConverter<AdapterSession>({ preferSnakeCase })),
accounts: db
.collection("accounts")
.withConverter(getConverter<AdapterAccount>({ preferSnakeCase })),
verification_tokens: db
.collection(
preferSnakeCase ? "verification_tokens" : "verificationTokens"
)
.withConverter(
getConverter<VerificationToken>({ preferSnakeCase, excludeId: true })
),
}
}
/**
* Utility function that helps making sure that there is no duplicate app initialization issues in serverless environments.
* If no parameter is passed, it will use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to initialize a Firestore instance.
*
* @example
* ```ts title="lib/firestore.ts"
* import { initFirestore } from "@next-auth/firebase-adapter"
* import { cert } from "firebase-admin/app"
*
* export const firestore = initFirestore({
* credential: cert({
* projectId: process.env.FIREBASE_PROJECT_ID,
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
* })
* })
* ```
*/
export function initFirestore(
options: AppOptions & { name?: FirebaseAdapterConfig["name"] } = {}
) {
const apps = getApps()
const app = options.name ? apps.find((a) => a.name === options.name) : apps[0]
if (app) return getFirestore(app)
return initializeFirestore(initializeApp(options, options.name))
}

View File

@@ -1,118 +1,57 @@
import { runBasicTests } from "@next-auth/adapter-test"
import { FirestoreAdapter } from "../src"
import { FirestoreAdapter, type FirebaseAdapterConfig } from "../src"
import {
getFirestore,
connectFirestoreEmulator,
terminate,
collection,
query,
where,
limit,
getDocs,
collestionsFactory,
initFirestore,
getDoc,
doc,
} from "firebase/firestore"
import { initializeApp } from "firebase/app"
import { getConverter } from "../src/converter"
import type {
AdapterSession,
AdapterUser,
VerificationToken,
} from "next-auth/adapters"
import type { Account } from "next-auth"
getOneDoc,
mapFieldsFactory,
} from "../src"
const app = initializeApp({ projectId: "next-auth-test" })
const firestore = getFirestore(app)
describe.each([
{ namingStrategy: "snake_case" },
{ namingStrategy: "default" },
] as Partial<FirebaseAdapterConfig>[])(
"FirebaseAdapter with config: %s",
(config) => {
config.name = `next-auth-test-${config.namingStrategy}`
config.projectId = "next-auth-test"
config.databaseURL = "http://localhost:8080"
connectFirestoreEmulator(firestore, "localhost", 8080)
const db = initFirestore(config)
const preferSnakeCase = config.namingStrategy === "snake_case"
const mapper = mapFieldsFactory(preferSnakeCase)
const C = collestionsFactory(db, preferSnakeCase)
type IndexableObject = Record<string, unknown>
for (const [name, collection] of Object.entries(C)) {
test(`collection "${name}" should be empty`, async () => {
expect((await collection.count().get()).data().count).toBe(0)
})
}
const Users = collection(firestore, "users").withConverter(
getConverter<AdapterUser & IndexableObject>()
runBasicTests({
adapter: FirestoreAdapter(config),
db: {
disconnect: async () => await db.terminate(),
session: (sessionToken) =>
getOneDoc(
C.sessions.where(mapper.toDb("sessionToken"), "==", sessionToken)
),
user: (userId) => getDoc(C.users.doc(userId)),
account: ({ provider, providerAccountId }) =>
getOneDoc(
C.accounts
.where("provider", "==", provider)
.where(mapper.toDb("providerAccountId"), "==", providerAccountId)
),
verificationToken: ({ identifier, token }) =>
getOneDoc(
C.verification_tokens
.where("identifier", "==", identifier)
.where("token", "==", token)
),
},
})
}
)
const Sessions = collection(firestore, "sessions").withConverter(
getConverter<AdapterSession & IndexableObject>()
)
const Accounts = collection(firestore, "accounts").withConverter(
getConverter<Account>()
)
const VerificationTokens = collection(
firestore,
"verificationTokens"
).withConverter(
getConverter<VerificationToken & IndexableObject>({ excludeId: true })
)
runBasicTests({
adapter: FirestoreAdapter({ projectId: "next-auth-test" }),
db: {
async disconnect() {
await terminate(firestore)
},
async session(sessionToken) {
const snapshotQuery = query(
Sessions,
where("sessionToken", "==", sessionToken),
limit(1)
)
const snapshots = await getDocs(snapshotQuery)
const snapshot = snapshots.docs[0]
if (snapshot?.exists() && Sessions.converter) {
const session = Sessions.converter.fromFirestore(snapshot)
return session
}
return null
},
async user(id) {
const snapshot = await getDoc(doc(Users, id))
if (snapshot?.exists() && Users.converter) {
const user = Users.converter.fromFirestore(snapshot)
return user
}
return null
},
async account({ provider, providerAccountId }) {
const snapshotQuery = query(
Accounts,
where("provider", "==", provider),
where("providerAccountId", "==", providerAccountId),
limit(1)
)
const snapshots = await getDocs(snapshotQuery)
const snapshot = snapshots.docs[0]
if (snapshot?.exists() && Accounts.converter) {
const account = Accounts.converter.fromFirestore(snapshot)
return account
}
return null
},
async verificationToken({ identifier, token }) {
const snapshotQuery = query(
VerificationTokens,
where("identifier", "==", identifier),
where("token", "==", token),
limit(1)
)
const snapshots = await getDocs(snapshotQuery)
const snapshot = snapshots.docs[0]
if (snapshot?.exists() && VerificationTokens.converter) {
const verificationToken =
VerificationTokens.converter.fromFirestore(snapshot)
return verificationToken
}
},
},
})

View File

@@ -0,0 +1,11 @@
import config from "@next-auth/adapter-test/jest/jest-preset.js"
//TODO: update rest of the packages to Jest 29+
const {testURL, ...rest} = config
export default {
...rest,
testEnvironmentOptions: {
url: testURL
},
rootDir: ".."
}

View File

@@ -1,11 +1,23 @@
{
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"strict": true,
"noUncheckedIndexedAccess": true,
"moduleResolution": "node"
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"outDir": ".",
"rootDir": "src",
"skipDefaultLibCheck": true,
"strictNullChecks": true,
"stripInternal": true,
"declarationMap": true,
"declaration": true
},
"exclude": ["tests", "dist", "jest.config.js"]
}
"include": [
"src/**/*"
],
"exclude": [
"tests"
]
}

View File

@@ -22,7 +22,7 @@ Depending on your architecture you can use PouchDB's http adapter to reach any d
1. Install `next-auth` and `@next-auth/pouchdb-adapter`, as well as `pouchdb`.
> **Prerequesite**: Your PouchDB instance MUST provide the `pouchdb-find` plugin since it is used internally by the adapter to build and manage indexes
> **Prerequisite**: Your PouchDB instance MUST provide the `pouchdb-find` plugin since it is used internally by the adapter to build and manage indexes
```js
npm install next-auth @next-auth/pouchdb-adapter pouchdb

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/pouchdb-adapter",
"version": "0.1.5",
"version": "0.1.6",
"description": "PouchDB adapter for next-auth.",
"homepage": "https://authjs.dev",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -51,4 +51,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -41,7 +41,7 @@ export const PouchDBAdapter: Adapter<
> = (pouchdb) => {
return {
async getAdapter({ session, secret, ...appOptions }) {
// create PoucDB indexes if they don't exist
// create PouchDB indexes if they don't exist
const res = await pouchdb.getIndexes()
const indexes = res.indexes.map((index) => index.name, [])
if (!indexes.includes("nextAuthUserByEmail")) {

View File

@@ -24,7 +24,7 @@ This is the Upstash Redis adapter for [`next-auth`](https://authjs.dev). This pa
npm install next-auth @next-auth/upstash-redis-adapter @upstash/redis
```
2. Add the follwing code to your `pages/api/[...nextauth].js` next-auth configuration object.
2. Add the following code to your `pages/api/[...nextauth].js` next-auth configuration object.
```js
import NextAuth from "next-auth"

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/upstash-redis-adapter",
"version": "3.0.3",
"version": "3.0.4",
"description": "Upstash adapter for next-auth. It uses Upstash's connectionless (HTTP based) Redis client.",
"homepage": "https://authjs.dev",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -49,4 +49,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/xata-adapter",
"version": "0.2.0",
"version": "0.2.2",
"description": "Xata adapter for next-auth.",
"homepage": "https://authjs.dev",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -43,4 +43,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@auth/core",
"version": "0.2.5",
"version": "0.5.0",
"description": "Authentication for the Web.",
"keywords": [
"authentication",
@@ -27,7 +27,7 @@
"types": "./index.d.ts",
"files": [
"*.js",
"*.d.ts",
"*.d.ts*",
"lib",
"providers",
"src"
@@ -61,7 +61,7 @@
},
"license": "ISC",
"dependencies": {
"@panva/hkdf": "^1.0.2",
"@panva/hkdf": "^1.0.4",
"cookie": "0.5.0",
"jose": "^4.11.1",
"oauth4webapi": "^2.0.6",
@@ -69,7 +69,7 @@
"preact-render-to-string": "5.2.3"
},
"peerDependencies": {
"nodemailer": "6.8.0"
"nodemailer": "^6.8.0"
},
"peerDependenciesMeta": {
"nodemailer": {
@@ -77,10 +77,11 @@
}
},
"scripts": {
"build": "pnpm css && tsc",
"build": "pnpm css && pnpm providers && tsc",
"clean": "rm -rf *.js *.d.ts* lib providers",
"css": "node scripts/generate-css",
"dev": "pnpm css && tsc -w"
"dev": "pnpm css && pnpm providers && tsc -w",
"providers": "node scripts/generate-providers"
},
"devDependencies": {
"@next-auth/tsconfig": "workspace:*",
@@ -92,4 +93,4 @@
"postcss": "8.4.19",
"postcss-nested": "6.0.0"
}
}
}

View File

@@ -0,0 +1,18 @@
import { join } from "path"
import { readdirSync, writeFileSync } from "fs"
const providersPath = join(process.cwd(), "src/providers")
const files = readdirSync(providersPath, "utf8")
const providers = files.map((file) => {
const strippedProviderName = file.substring(0, file.indexOf("."))
return `"${strippedProviderName}"`
}).filter((provider) => provider !== '"oauth-types"' && provider !== '"index"')
const result = `
// THIS FILE IS AUTOGENERATED. DO NOT EDIT.
export type OAuthProviderType =
| ${providers.join("\n | ")}`
writeFileSync(join(providersPath, "oauth-types.ts"), result)

View File

@@ -5,7 +5,7 @@
* A database adapter provides a common interface for Auth.js so that it can work with
* _any_ database/ORM adapter without concerning itself with the implementation details of the database/ORM.
*
* Auth.js supports 2 session strtategies to persist the login state of a user.
* Auth.js supports 2 session strategies to persist the login state of a user.
* The default is to use a cookie + {@link https://authjs.dev/concepts/session-strategies#jwt JWT}
* based session store (`strategy: "jwt"`),
* but you can also use a database adapter to store the session in a database.
@@ -26,7 +26,7 @@
*
* ## Usage
*
* {@link https://authjs.dev/reference/adapters/overview Built-in adapters} already implement this interfac, so you likely won't need to
* {@link https://authjs.dev/reference/adapters/overview Built-in adapters} already implement this interface, so you likely won't need to
* implement it yourself. If you do, you can use the following example as a
* starting point.
*

View File

@@ -1,4 +1,8 @@
/**
*
* :::warning Experimental
* `@auth/core` is under active development.
* :::
*
* This is the main entry point to the Auth.js library.
*
@@ -18,7 +22,7 @@
* ```ts
* import { Auth } from "@auth/core"
*
* const request = new Request("https://example.com"
* const request = new Request("https://example.com")
* const response = await Auth(request, {...})
*
* console.log(response instanceof Response) // true
@@ -27,14 +31,14 @@
* ## Resources
*
* - [Getting started](https://authjs.dev/getting-started/introduction)
* - [Most common use case guides](https://authjs.dev/guides/overview)
* - [Most common use case guides](https://authjs.dev/guides)
*
* @module main
* @module index
*/
import { assertConfig } from "./lib/assert.js"
import { ErrorPageLoop } from "./errors.js"
import { AuthInternal } from "./lib/index.js"
import { AuthInternal, skipCSRFCheck } from "./lib/index.js"
import renderPage from "./lib/pages/index.js"
import { logger, setLogger, type LoggerInstance } from "./lib/utils/logger.js"
import { toInternalRequest, toResponse } from "./lib/web.js"
@@ -51,6 +55,8 @@ import type {
import type { Provider } from "./providers/index.js"
import { JWTOptions } from "./jwt.js"
export { skipCSRFCheck }
/**
* Core functionality provided by Auth.js.
*
@@ -160,7 +166,7 @@ export async function Auth(
* const response = await AuthHandler(request, authConfig)
* ```
*
* @see [Initiailzation](https://authjs.dev/reference/configuration/auth-options)
* @see [Initialization](https://authjs.dev/reference/configuration/auth-options)
*/
export interface AuthConfig {
/**
@@ -298,14 +304,3 @@ export interface AuthConfig {
trustHost?: boolean
skipCSRFCheck?: typeof skipCSRFCheck
}
/**
* :::danger
* This option is inteded for framework authors.
* :::
*
* Auth.js comes with built-in {@link https://authjs.dev/concepts/security#csrf CSRF} protection, but
* if you are implementing a framework that is already protected against CSRF attacks, you can skip this check by
* passing this value to {@link AuthConfig.skipCSRFCheck}.
*/
export const skipCSRFCheck = Symbol("skip-csrf-check")

View File

@@ -6,7 +6,7 @@
* issued and used by Auth.js.
*
* The JWT issued by Auth.js is _encrypted by default_, using the _A256GCM_ algorithm ({@link https://www.rfc-editor.org/rfc/rfc7516 JWE}).
* It uses the `AUTH_SECRET` environment variable to dervice a sufficient encryption key.
* It uses the `AUTH_SECRET` environment variable to derive a sufficient encryption key.
*
* :::info Note
* Auth.js JWTs are meant to be used by the same app that issued them.
@@ -203,7 +203,7 @@ export interface JWTOptions {
/**
* The secret used to encode/decode the Auth.js issued JWT.
*
* @deprecated Set the `AUTH_SECRET` environment vairable or
* @deprecated Set the `AUTH_SECRET` environment variable or
* use the top-level `secret` option instead
*/
secret: string

View File

@@ -15,8 +15,8 @@ import type { SessionToken } from "./cookie.js"
* It prevents insecure behaviour, such as linking OAuth accounts unless a user is
* signed in and authenticated with an existing valid account.
*
* All verification (e.g. OAuth flows or email address verificaiton flows) are
* done prior to this handler being called to avoid additonal complexity in this
* All verification (e.g. OAuth flows or email address verification flows) are
* done prior to this handler being called to avoid additional complexity in this
* handler.
*/
export async function handleLogin(
@@ -203,7 +203,7 @@ export async function handleLogin(
// accounts (by email or provider account id)...
//
// If no account matching the same [provider].id or .email exists, we can
// create a new account for the user, link it to the OAuth acccount and
// create a new account for the user, link it to the OAuth account and
// create a new session for them so they are signed in with it.
const { id: _, ...newUser } = { ...profile, emailVerified: null }
user = await createUser(newUser)

View File

@@ -16,7 +16,7 @@ interface CreateCSRFTokenParams {
* where 'token' is the CSRF token and 'hash' is a hash made of the token and
* the secret, and the two values are joined by a pipe '|'. By storing the
* value and the hash of the value (with the secret used as a salt) we can
* verify the cookie was set by the server and not by a malicous attacker.
* verify the cookie was set by the server and not by a malicious attacker.
*
* For more details, see the following OWASP links:
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie

View File

@@ -1,5 +1,4 @@
import { UnknownAction } from "../errors.js"
import { skipCSRFCheck } from "../index.js"
import { SessionStore } from "./cookie.js"
import { init } from "./init.js"
import renderPage from "./pages/index.js"
@@ -72,9 +71,9 @@ export async function AuthInternal<
if (pages.signIn) {
let signinUrl = `${pages.signIn}${
pages.signIn.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(options.callbackUrl)}`
}${new URLSearchParams({ callbackUrl: options.callbackUrl })}`
if (error)
signinUrl = `${signinUrl}&error=${encodeURIComponent(error)}`
signinUrl = `${signinUrl}&${new URLSearchParams({ error })}`
return { redirect: signinUrl, cookies }
}
@@ -186,3 +185,14 @@ export async function AuthInternal<
}
throw new UnknownAction(`Cannot handle action: ${action}`)
}
/**
* :::danger
* This option is intended for framework authors.
* :::
*
* Auth.js comes with built-in {@link https://authjs.dev/concepts/security#csrf CSRF} protection, but
* if you are implementing a framework that is already protected against CSRF attacks, you can skip this check by
* passing this value to {@link AuthConfig.skipCSRFCheck}.
*/
export const skipCSRFCheck = Symbol("skip-csrf-check")

View File

@@ -60,7 +60,7 @@ export async function init({
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle by default
// User provided options are overriden by other options,
// User provided options are overridden by other options,
// except for the options with special handling above
const options: InternalOptions = {
debug: false,

View File

@@ -1,7 +1,7 @@
import * as checks from "./checks.js"
import * as o from "oauth4webapi"
import type {
CookiesOptions,
InternalOptions,
RequestInternal,
ResponseInternal,
@@ -58,10 +58,10 @@ export async function getAuthorizationUrl(
const cookies: Cookie[] = []
if (provider.checks?.includes("state")) {
const { value, raw } = await createState(options)
authParams.set("state", raw)
cookies.push(value)
const state = await checks.state.create(options)
if (state) {
authParams.set("state", state.value)
cookies.push(state.cookie)
}
if (provider.checks?.includes("pkce")) {
@@ -70,17 +70,17 @@ export async function getAuthorizationUrl(
// a random `nonce` must be used for CSRF protection.
provider.checks = ["nonce"]
} else {
const { code_challenge, pkce } = await createPKCE(options)
authParams.set("code_challenge", code_challenge)
const { value, cookie } = await checks.pkce.create(options)
authParams.set("code_challenge", value)
authParams.set("code_challenge_method", "S256")
cookies.push(pkce)
cookies.push(cookie)
}
}
if (provider.checks?.includes("nonce")) {
const nonce = await createNonce(options)
const nonce = await checks.nonce.create(options)
if (nonce) {
authParams.set("nonce", nonce.value)
cookies.push(nonce)
cookies.push(nonce.cookie)
}
// TODO: This does not work in normalizeOAuth because authorization endpoint can come from discovery
@@ -90,54 +90,5 @@ export async function getAuthorizationUrl(
}
logger.debug("authorization url is ready", { url, cookies, provider })
return { redirect: url, cookies }
}
/** Returns a signed cookie. */
export async function signCookie(
type: keyof CookiesOptions,
value: string,
maxAge: number,
options: InternalOptions<"oauth">
): Promise<Cookie> {
const { cookies, jwt, logger } = options
logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge })
const expires = new Date()
expires.setTime(expires.getTime() + maxAge * 1000)
return {
name: cookies[type].name,
value: await jwt.encode({ ...jwt, maxAge, token: { value } }),
options: { ...cookies[type].options, expires },
}
}
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
async function createState(options: InternalOptions<"oauth">) {
const raw = o.generateRandomState()
const maxAge = STATE_MAX_AGE
const value = await signCookie("state", raw, maxAge, options)
return { value, raw }
}
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
async function createPKCE(options: InternalOptions<"oauth">) {
const code_verifier = o.generateRandomCodeVerifier()
const code_challenge = await o.calculatePKCECodeChallenge(code_verifier)
const maxAge = PKCE_MAX_AGE
const pkce = await signCookie(
"pkceCodeVerifier",
code_verifier,
maxAge,
options
)
return { code_challenge, pkce }
}
const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
async function createNonce(options: InternalOptions<"oauth">) {
const raw = o.generateRandomNonce()
const maxAge = NONCE_MAX_AGE
return await signCookie("nonce", raw, maxAge, options)
return { redirect: url.toString(), cookies }
}

View File

@@ -1,8 +1,6 @@
import * as checks from "./checks.js"
import * as o from "oauth4webapi"
import { OAuthCallbackError, OAuthProfileParseError } from "../../errors.js"
import { useNonce } from "./nonce-handler.js"
import { usePKCECodeVerifier } from "./pkce-handler.js"
import { useState } from "./state-handler.js"
import type {
InternalOptions,
@@ -73,7 +71,7 @@ export async function handleOAuth(
const resCookies: Cookie[] = []
const state = await useState(cookies, resCookies, options)
const state = await checks.state.use(cookies, resCookies, options)
const parameters = o.validateAuthResponse(
as,
@@ -91,7 +89,7 @@ export async function handleOAuth(
throw new OAuthCallbackError(parameters.error)
}
const codeVerifier = await usePKCECodeVerifier(
const codeVerifier = await checks.pkce.use(
cookies?.[options.cookies.pkceCodeVerifier.name],
options
)
@@ -99,12 +97,15 @@ export async function handleOAuth(
if (codeVerifier) resCookies.push(codeVerifier.cookie)
// TODO:
const nonce = await useNonce(cookies?.[options.cookies.nonce.name], options)
const nonce = await checks.nonce.use(
cookies?.[options.cookies.nonce.name],
options
)
if (nonce && provider.type === "oidc") {
resCookies.push(nonce.cookie)
}
const codeGrantResponse = await o.authorizationCodeGrantRequest(
let codeGrantResponse = await o.authorizationCodeGrantRequest(
as,
client,
parameters,
@@ -112,6 +113,12 @@ export async function handleOAuth(
codeVerifier?.codeVerifier ?? "auth" // TODO: review fallback code verifier
)
if (provider.token?.conform) {
codeGrantResponse =
(await provider.token.conform(codeGrantResponse.clone())) ??
codeGrantResponse
}
let challenges: o.WWWAuthenticateChallenge[] | undefined
if ((challenges = o.parseWwwAuthenticateChallenges(codeGrantResponse))) {
for (const challenge of challenges) {
@@ -196,7 +203,7 @@ async function getProfile(
// If we didn't get a response either there was a problem with the provider
// response *or* the user cancelled the action with the provider.
//
// Unfortuately, we can't tell which - at least not in a way that works for
// Unfortunately, we can't tell which - at least not in a way that works for
// all providers, so we return an empty object; the user should then be
// redirected back to the sign up page. We log the error to help developers
// who might be trying to debug this when configuring a new provider.

View File

@@ -0,0 +1,155 @@
import * as o from "oauth4webapi"
import * as jwt from "../../jwt.js"
import type {
InternalOptions,
RequestInternal,
CookiesOptions,
} from "../../types.js"
import type { Cookie } from "../cookie.js"
import { InvalidState } from "../../errors.js"
/** Returns a signed cookie. */
export async function signCookie(
type: keyof CookiesOptions,
value: string,
maxAge: number,
options: InternalOptions<"oauth">
): Promise<Cookie> {
const { cookies, logger } = options
logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge })
const expires = new Date()
expires.setTime(expires.getTime() + maxAge * 1000)
return {
name: cookies[type].name,
value: await jwt.encode({ ...options.jwt, maxAge, token: { value } }),
options: { ...cookies[type].options, expires },
}
}
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const pkce = {
async create(options: InternalOptions<"oauth">) {
const code_verifier = o.generateRandomCodeVerifier()
const value = await o.calculatePKCECodeChallenge(code_verifier)
const maxAge = PKCE_MAX_AGE
const cookie = await signCookie(
"pkceCodeVerifier",
code_verifier,
maxAge,
options
)
return { cookie, value }
},
/**
* Returns code_verifier if provider uses PKCE,
* and clears the container cookie afterwards.
*/
async use(
codeVerifier: string | undefined,
options: InternalOptions<"oauth">
): Promise<{ codeVerifier: string; cookie: Cookie } | undefined> {
const { cookies, provider } = options
if (!provider?.checks?.includes("pkce") || !codeVerifier) {
return
}
const pkce = (await jwt.decode({
...options.jwt,
token: codeVerifier,
})) as any
return {
codeVerifier: pkce?.value ?? undefined,
cookie: {
name: cookies.pkceCodeVerifier.name,
value: "",
options: { ...cookies.pkceCodeVerifier.options, maxAge: 0 },
},
}
},
}
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const state = {
async create(options: InternalOptions<"oauth">) {
if (!options.provider.checks.includes("state")) return
// TODO: support customizing the state
const value = o.generateRandomState()
const maxAge = STATE_MAX_AGE
const cookie = await signCookie("state", value, maxAge, options)
return { cookie, value }
},
/**
* Returns state from the saved cookie
* if the provider supports states,
* and clears the container cookie afterwards.
*/
async use(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">
): Promise<string | undefined> {
const { provider, jwt } = options
if (!provider.checks.includes("state")) return
const state = cookies?.[options.cookies.state.name]
if (!state) throw new InvalidState("State was missing from the cookies.")
// IDEA: Let the user do something with the returned state
const value = (await jwt.decode({ ...options.jwt, token: state })) as any
if (!value?.value) throw new InvalidState("Could not parse state cookie.")
// Clear the state cookie after use
resCookies.push({
name: options.cookies.state.name,
value: "",
options: { ...options.cookies.state.options, maxAge: 0 },
})
return value.value
},
}
const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const nonce = {
async create(options: InternalOptions<"oauth">) {
if (!options.provider.checks.includes("nonce")) return
const value = o.generateRandomNonce()
const maxAge = NONCE_MAX_AGE
const cookie = await signCookie("nonce", value, maxAge, options)
return { cookie, value }
},
/**
* Returns nonce from if the provider supports nonce,
* and clears the container cookie afterwards.
*/
async use(
nonce: string | undefined,
options: InternalOptions<"oauth">
): Promise<{ value: string; cookie: Cookie } | undefined> {
const { cookies, provider } = options
if (!provider?.checks?.includes("nonce") || !nonce) {
return
}
const value = (await jwt.decode({ ...options.jwt, token: nonce })) as any
return {
value: value?.value ?? undefined,
cookie: {
name: cookies.nonce.name,
value: "",
options: { ...cookies.nonce.options, maxAge: 0 },
},
}
},
}

View File

@@ -1,77 +0,0 @@
import * as o from "oauth4webapi"
import * as jwt from "../../jwt.js"
import type { InternalOptions } from "../../types.js"
import type { Cookie } from "../cookie.js"
const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
/**
* Returns nonce if the provider supports it
* and saves it in a cookie
*/
export async function createNonce(options: InternalOptions<"oauth">): Promise<
| undefined
| {
value: string
cookie: Cookie
}
> {
const { cookies, logger, provider } = options
if (!provider.checks?.includes("nonce")) {
// Provider does not support nonce, return nothing.
return
}
const nonce = o.generateRandomNonce()
const expires = new Date()
expires.setTime(expires.getTime() + NONCE_MAX_AGE * 1000)
// Encrypt nonce and save it to an encrypted cookie
const encryptedNonce = await jwt.encode({
...options.jwt,
maxAge: NONCE_MAX_AGE,
token: { nonce },
})
logger.debug("CREATE_ENCRYPTED_NONCE", {
nonce,
maxAge: NONCE_MAX_AGE,
})
return {
cookie: {
name: cookies.nonce.name,
value: encryptedNonce,
options: { ...cookies.nonce.options, expires },
},
value: nonce,
}
}
/**
* Returns nonce from if the provider supports nonce,
* and clears the container cookie afterwards.
*/
export async function useNonce(
nonce: string | undefined,
options: InternalOptions<"oauth">
): Promise<{ value: string; cookie: Cookie } | undefined> {
const { cookies, provider } = options
if (!provider?.checks?.includes("nonce") || !nonce) {
return
}
const value = (await jwt.decode({ ...options.jwt, token: nonce })) as any
return {
value: value?.value ?? undefined,
cookie: {
name: cookies.nonce.name,
value: "",
options: { ...cookies.nonce.options, maxAge: 0 },
},
}
}

View File

@@ -1,87 +0,0 @@
import * as o from "oauth4webapi"
import * as jwt from "../../jwt.js"
import type { InternalOptions } from "../../types.js"
import type { Cookie } from "../cookie.js"
const PKCE_CODE_CHALLENGE_METHOD = "S256"
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
/**
* Returns `code_challenge` and `code_challenge_method`
* and saves them in a cookie.
*/
export async function createPKCE(options: InternalOptions<"oauth">): Promise<
| undefined
| {
code_challenge: string
code_challenge_method: "S256"
cookie: Cookie
}
> {
const { cookies, logger, provider } = options
if (!provider.checks?.includes("pkce")) {
// Provider does not support PKCE, return nothing.
return
}
const code_verifier = o.generateRandomCodeVerifier()
const code_challenge = await o.calculatePKCECodeChallenge(code_verifier)
const maxAge = cookies.pkceCodeVerifier.options.maxAge ?? PKCE_MAX_AGE
const expires = new Date()
expires.setTime(expires.getTime() + maxAge * 1000)
// Encrypt code_verifier and save it to an encrypted cookie
const encryptedCodeVerifier = await jwt.encode({
...options.jwt,
maxAge,
token: { code_verifier },
})
logger.debug("CREATE_PKCE_CHALLENGE_VERIFIER", {
code_challenge,
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
code_verifier,
maxAge,
})
return {
code_challenge,
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
cookie: {
name: cookies.pkceCodeVerifier.name,
value: encryptedCodeVerifier,
options: { ...cookies.pkceCodeVerifier.options, expires },
},
}
}
/**
* Returns code_verifier if provider uses PKCE,
* and clears the container cookie afterwards.
*/
export async function usePKCECodeVerifier(
codeVerifier: string | undefined,
options: InternalOptions<"oauth">
): Promise<{ codeVerifier: string; cookie: Cookie } | undefined> {
const { cookies, provider } = options
if (!provider?.checks?.includes("pkce") || !codeVerifier) {
return
}
const pkce = (await jwt.decode({
...options.jwt,
token: codeVerifier,
})) as any
return {
codeVerifier: pkce?.value ?? undefined,
cookie: {
name: cookies.pkceCodeVerifier.name,
value: "",
options: { ...cookies.pkceCodeVerifier.options, maxAge: 0 },
},
}
}

View File

@@ -1,72 +0,0 @@
import * as o from "oauth4webapi"
import type { InternalOptions, RequestInternal } from "../../types.js"
import type { Cookie } from "../cookie.js"
import { InvalidState } from "../../errors.js"
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
/** Returns state if the provider supports it */
export async function createState(
options: InternalOptions<"oauth">
): Promise<{ cookie: Cookie; value: string } | undefined> {
const { logger, provider, jwt, cookies } = options
if (!provider.checks?.includes("state")) {
// Provider does not support state, return nothing
return
}
const state = o.generateRandomState()
const maxAge = cookies.state.options.maxAge ?? STATE_MAX_AGE
const encodedState = await jwt.encode({
...jwt,
maxAge,
token: { state },
})
logger.debug("CREATE_STATE", { state, maxAge })
const expires = new Date()
expires.setTime(expires.getTime() + maxAge * 1000)
return {
value: state,
cookie: {
name: cookies.state.name,
value: encodedState,
options: { ...cookies.state.options, expires },
},
}
}
/**
* Returns state from the saved cookie
* if the provider supports states,
* and clears the container cookie afterwards.
*/
export async function useState(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">
): Promise<string | undefined> {
const { provider, jwt } = options
if (!provider.checks.includes("state")) return
const state = cookies?.[options.cookies.state.name]
if (!state) throw new InvalidState("State was missing from the cookies.")
// IDEA: Let the user do something with the returned state
const value = (await jwt.decode({ ...options.jwt, token: state })) as any
if (!value?.value) throw new InvalidState("Could not parse state cookie.")
// Clear the state cookie after use
resCookies.push({
name: options.cookies.state.name,
value: "",
options: { ...options.cookies.state.options, maxAge: 0 },
})
return value.value
}

View File

@@ -15,7 +15,7 @@
--color-control-border: #bbb;
--color-button-active-background: #f9f9f9;
--color-button-active-border: #aaa;
--color-seperator: #ccc;
--color-separator: #ccc;
}
.__next-auth-theme-dark {
@@ -26,7 +26,7 @@
--color-control-border: #555;
--color-button-active-background: #060606;
--color-button-active-border: #666;
--color-seperator: #444;
--color-separator: #444;
}
@media (prefers-color-scheme: dark) {
@@ -38,7 +38,7 @@
--color-control-border: #555;
--color-button-active-background: #060606;
--color-button-active-border: #666;
--color-seperator: #444;
--color-separator: #444;
}
}
@@ -218,7 +218,7 @@ a.site {
hr {
display: block;
border: 0;
border-top: 1px solid var(--color-seperator);
border-top: 1px solid var(--color-separator);
margin: 2rem auto 1rem auto;
overflow: visible;

View File

@@ -96,6 +96,11 @@ function normalizeEndpoint(
// NOTE: This need to be checked when constructing the URL
// for the authorization, token and userinfo endpoints.
const url = new URL(e?.url ?? "https://authjs.dev")
for (const k in e?.params) url.searchParams.set(k, e?.params[k])
return { url, request: e?.request }
if (e?.params != null) {
for (let [key, value] of Object.entries(e.params)) {
if (key === "claims") value = JSON.stringify(value)
url.searchParams.set(key, String(value))
}
}
return { url, request: e?.request, conform: e?.conform }
}

View File

@@ -53,7 +53,7 @@ export async function callback(params: {
cookies.push(...authorizationResult.cookies)
}
logger.debug("authroization result", authorizationResult)
logger.debug("authorization result", authorizationResult)
const { profile, account, OAuthProfile } = authorizationResult
@@ -149,7 +149,7 @@ export async function callback(params: {
return {
redirect: `${pages.newUser}${
pages.newUser.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(callbackUrl)}`,
}${new URLSearchParams({ callbackUrl })}`,
cookies,
}
}
@@ -256,7 +256,7 @@ export async function callback(params: {
return {
redirect: `${pages.newUser}${
pages.newUser.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(callbackUrl)}`,
}${new URLSearchParams({ callbackUrl })}`,
cookies,
}
}
@@ -264,7 +264,7 @@ export async function callback(params: {
// Callback URL is already verified at this point, so safe to use if specified
return { redirect: callbackUrl, cookies }
} else if (provider.type === "credentials" && method === "POST") {
const credentials = body
const credentials = body ?? {}
// TODO: Forward the original request as is, instead of reconstructing it
Object.entries(query ?? {}).forEach(([k, v]) =>
@@ -350,6 +350,6 @@ export async function callback(params: {
logger.error(error)
url.searchParams.set("error", CallbackRouteError.name)
url.pathname += "/error"
return { redirect: url, cookies }
return { redirect: url.toString(), cookies }
}
}

View File

@@ -7,19 +7,20 @@ export async function handleAuthorized(
params: any,
{ url, logger, callbacks: { signIn } }: InternalOptions
) {
url.pathname += "/error"
try {
const authorized = await signIn(params)
if (!authorized) {
url.pathname += "/error"
logger.debug("User not authorized", params)
url.searchParams.set("error", "AccessDenied")
return { status: 403 as const, redirect: url }
return { status: 403 as const, redirect: url.toString() }
}
} catch (e) {
url.pathname += "/error"
const error = new AuthorizedCallbackError(e as Error)
logger.error(error)
url.searchParams.set("error", "Configuration")
return { status: 500 as const, redirect: url }
return { status: 500 as const, redirect: url.toString() }
}
}

View File

@@ -54,7 +54,7 @@ export async function signin(
logger.error(error)
url.searchParams.set("error", error.name)
url.pathname += "/error"
return { redirect: url }
return { redirect: url.toString() }
}
}

View File

@@ -8,7 +8,7 @@ import type { SessionStore } from "../cookie.js"
* If the session strategy is database,
* The session is also deleted from the database.
* In any case, the session cookie is cleared and
* an `events.signOut` is emitted.
* {@link EventCallbacks.signOut} is emitted.
*/
export async function signout(
sessionStore: SessionStore,

View File

@@ -76,27 +76,22 @@ export function toResponse(res: ResponseInternal): Response {
res.cookies?.forEach((cookie) => {
const { name, value, options } = cookie
const cookieHeader = serialize(name, value, options)
if (headers.has("Set-Cookie")) {
headers.append("Set-Cookie", cookieHeader)
} else {
headers.set("Set-Cookie", cookieHeader)
}
if (headers.has("Set-Cookie")) headers.append("Set-Cookie", cookieHeader)
else headers.set("Set-Cookie", cookieHeader)
// headers.set("Set-Cookie", cookieHeader) // TODO: Remove. Seems to be a bug with Headers in the runtime
})
const body =
headers.get("content-type") === "application/json"
? JSON.stringify(res.body)
: res.body
let body = res.body
const response = new Response(body, {
headers,
status: res.redirect ? 302 : res.status ?? 200,
})
if (headers.get("content-type") === "application/json")
body = JSON.stringify(res.body)
else if (headers.get("content-type") === "application/x-www-form-urlencoded")
body = new URLSearchParams(res.body).toString()
if (res.redirect) {
response.headers.set("Location", res.redirect.toString())
}
const status = res.redirect ? 302 : res.status ?? 200
const response = new Response(body, { headers, status })
if (res.redirect) response.headers.set("Location", res.redirect)
return response
}

View File

@@ -0,0 +1,112 @@
/**
* <div style={{backgroundColor: "#24292f", display: "flex", justifyContent: "space-between", color: "#fff", padding: 16}}>
* <span>Built-in <b>Asgardeo</b> integration.</span>
* <a href="https://wso2.com/asgardeo/">
* <img style={{display: "block"}} src="https://authjs.dev/img/providers/asgardeo-dark.svg" height="48" width="48"/>
* </a>
* </div>
*
* ---
* @module providers/asgardeo
*/
import type { OIDCConfig, OIDCUserConfig } from "./index.js"
export interface AsgardeoProfile {
sub: string
given_name: string
email: string
picture: string
}
/**
* Add Asgardeo login to your page.
* ## Documentation
*
* https://wso2.com/asgardeo/docs/guides/authentication
*
*
* ## Instructions
*
* - Log into https://console.asgardeo.io.
* - Next, go to "Application" tab (More info: https://wso2.com/asgardeo/docs/guides/applications/register-oidc-web-app/).
* - Register standard based - Open id connect, application.
* - Add callback URL: http://localhost:3000/api/auth/callback/asgardeo and https://your-domain.com/api/auth/callback/asgardeo
* - After registering the application, go to protocol tab.
* - Check `code` grant type.
* - Add Authorized redirect URLs & Allowed origins fields.
* - Make Email, First Name, Photo URL user attributes mandatory from the console.
*
* Create a `.env` file in the project root add the following entries:
*
* These values can be collected from the application created.
*
* ```
* ASGARDEO_CLIENT_ID=<Copy client ID from protocol tab here>
* ASGARDEO_CLIENT_SECRET=<Copy client from protocol tab here>
* ASGARDEO_ISSUER=<Copy the issuer url from the info tab here>
* ```
*
* In `pages/api/auth/[...nextauth].js` find or add the `Asgardeo` entries:
*
* ```js
* import Asgardeo from "next-auth/providers/asgardeo";
* ...
* providers: [
* Asgardeo({
* clientId: process.env.ASGARDEO_CLIENT_ID,
* clientSecret: process.env.ASGARDEO_CLIENT_SECRET,
* issuer: process.env.ASGARDEO_ISSUER
* }),
* ],
*
* ...
* ```
*
* ## Resources
*
* @see [Asgardeo - Authentication Guide](https://wso2.com/asgardeo/docs/guides/authentication)
* @see [Learn more about OAuth](https://authjs.dev/concepts/oauth)
* @see [Source code](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/asgardeo.ts)
*
* ## Notes
*
* By default, Auth.js assumes that the Asgardeo provider is
* based on the [OAuth 2](https://www.rfc-editor.org/rfc/rfc6749.html) specification.
*
* :::tip
*
* The Asgardeo provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/asgardeo.ts).
* To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/providers/custom-provider#override-default-options).
*
* :::
*
* :::info **Disclaimer**
*
* If you think you found a bug in the default configuration, you can [open an issue](https://authjs.dev/new/provider-issue).
*
* Auth.js strictly adheres to the specification and it cannot take responsibility for any deviation from
* the spec by the provider. You can open an issue, but if the problem is non-compliance with the spec,
* we might not pursue a resolution. You can ask for more help in [Discussions](https://authjs.dev/new/github-discussions).
*
* :::
*/
export default function Asgardeo(
config: OIDCUserConfig<AsgardeoProfile>
): OIDCConfig<AsgardeoProfile> {
return {
id: "asgardeo",
name: "Asgardeo",
type: "oidc",
wellKnown: `${config?.issuer}/oauth2/token/.well-known/openid-configuration`,
style: {
logo: "/asgardeo.svg",
logoDark: "/asgardeo-dark.svg",
bg: "#fff",
text: "#000",
bgDark: "#000",
textDark: "#fff",
},
options: config,
}
}

Some files were not shown because too many files have changed in this diff Show More