Compare commits

..

67 Commits

Author SHA1 Message Date
Balázs Orbán
29db75ad28 chore(release): bump version [skip ci] 2022-07-25 12:31:40 +02:00
Balázs Orbán
d348ca1dc1 fix: reduce logger.error context 2022-07-25 11:14:49 +02:00
Balázs Orbán
d53e1ea6c4 chore: gitignore v4 files 2022-07-25 11:13:44 +02:00
Balázs Orbán
e701342b1a update package-lock.json 2022-07-05 13:49:23 +02:00
Balázs Orbán
8a133bf5fd fix: don't render email in email's HTML body 2022-07-05 13:47:28 +02:00
Balázs Orbán
35a3ea6620 fix: handle invalid email 2022-07-01 12:42:20 +02:00
Balázs Orbán
289800fbb4 chore: bump version 2022-06-23 12:05:39 +02:00
Sylvain Bellone
28eccc3e64 fix: ReferenceError: defaultCookies is not defined (#4711) 2022-06-23 10:50:28 +02:00
Balázs Orbán
e16bf939a9 chore: bump version 2022-06-20 10:07:04 +02:00
Balázs Orbán
9b078c92b2 fix: don't show error on relative callbackUrl 2022-06-20 10:05:36 +02:00
Balázs Orbán
87f6f576b1 fix: handle invalid callbackUrl 2022-06-10 15:11:41 +02:00
Balázs Orbán
50584bdc4c chore: bump version 2022-04-26 12:22:45 +02:00
Balázs Orbán
b4429235c0 fix: more strict default callback url handling 2022-04-26 12:22:11 +02:00
Balázs Orbán
e1b297d06d fix: update default callbacks.redirect 2022-04-14 11:32:16 +02:00
Balázs Orbán
ab764e3793 chore: bump release 2022-03-15 22:50:13 +01:00
Balázs Orbán
c8941e4b3e fix: remove action from bad request response 2022-03-15 22:45:10 +01:00
Nico Domino
ead715219a fix(deps): update built-in adapter dependencies (#2589)
* fix(deps): update prisma-legacy-adapter and typeorm-legacy-adapter dependencies

* chore: add missing package-lock update
2021-08-23 21:55:33 +02:00
Ashutosh Kumar
8faa7553dd docs: add suggestions for secret and encryption key generation (#2578) 2021-08-21 23:08:56 +02:00
Eduard Babinyan
90a6a0084b feat(provider): return image for Yandex by default (#2563)
Uploading an user image.
2021-08-20 09:37:30 +02:00
Aaron Powell
cb844a2436 docs(provider): remove en-us from Azure urls (#2554)
MS Docs has a lot of local language translations, so it's best to remove locale information from the URLs so that when someone follows them, they land on the right language version of the content.
2021-08-18 09:46:32 +02:00
Sercan Altundas
74558d6cc2 docs(email): remove duplicate CSS property from html (#2546)
- The CSS property 'text-decoration: none;' was duplicated in the example html code and is removed.
2021-08-17 12:17:54 +02:00
Jaye Hackett
d03125a77b docs(ts): mention module augmentation on callbacks (#2541) 2021-08-17 01:01:19 +02:00
Liam Tait
66d16f8bf4 fix(ts): allow scope as string array type (#2511) 2021-08-12 17:51:31 +02:00
Nico Domino
be74dd0e7e docs(security): email contact update (#2467)
* chore(docs): email contact update

* chore(docs): add me@iaincollins.com back
2021-08-02 17:18:17 +02:00
Aryan Beezadhur
9bf867ddcf docs: Update faq.md (#2458) 2021-07-30 22:34:32 +02:00
Nico Domino
0f460c22da docs(client): add text regarding 'logout' (#2432) 2021-07-28 20:10:08 +02:00
Sigurd Heggemsnes
887cb00877 docs(adapter): Typo in filepath for firebase auth in docs. (#2436) 2021-07-28 12:48:47 +02:00
Douglas
75ca097ff7 docs: Fix link to code (#2405) 2021-07-19 15:36:37 +02:00
Nicolas Azari
bcb9383aec docs: fix typos in options.md (#2393)
* Update options.md

* Update www/docs/configuration/options.md

Co-authored-by: Balázs Orbán <info@balazsorban.com>

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-07-17 22:21:45 +02:00
John Michael Kuhn Jr
b953963101 chore(core): fix typo in csrf-token-handler.js where 'strategy' is misspelled (#2391) 2021-07-17 12:02:38 +02:00
Nico Domino
4649f1968b docs(readme): add opencollective details to readme (#2388)
* docs(readme): add opencollective details to readme

* docs(www): add sponsors to docs footer

* docs(readme): move support under ack

* docs(www): dropped docusaurus link in footer
2021-07-16 18:05:15 +02:00
Angelo Annunziata
45f4a69a4e docs(configuration): remove comments in JWT example (#2378) 2021-07-16 09:28:19 +02:00
Prabhdeep Singh
2155c93a3c feat(providers): add OneLogin (#2345)
Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-07-14 11:07:56 +02:00
Angelo Annunziata
d5958571a4 docs(provider): fix typo (#2369) 2021-07-13 21:36:00 +02:00
James Q Quick
ebecaa6a4b docs(adapter): match Fauna index name with implementation(#2360)
* Update Fauna Adapter 

- added one-liner to explain how to use the setup scripts inside of the Fauna dashboard
- updated the `verification_request_by_token` index name to match what is expected inside of the SDK which is `verification_request_by_token_and_identifier`

* Update Typo

Co-authored-by: Balázs Orbán <info@balazsorban.com>

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-07-13 17:58:58 +02:00
Vincent Grafé
1c5173a818 docs(callbacks): fix typo (#2363) 2021-07-13 10:24:05 +02:00
Ben Goshow
35ce332cc6 feat(providers): add Freshbooks (#2322)
Contains the following squashed commits: 

* Create freshbooks.js
* Create freshbooks.md
* Update providers.d.ts
* Update freshbooks.md
* Update src/providers/freshbooks.js
* Update providers.test.ts
* Update freshbooks.md
2021-07-11 20:25:26 +02:00
Imamuzzaki Abu Salam
ec295287f1 docs: delete can word in "can can" (#2348) 2021-07-11 15:08:05 +02:00
Nick Arciero
46978ac02f docs(tutorial): Add link to blog post about integrating with Magic (#2340) 2021-07-10 09:56:13 +02:00
Pol
f546e550dd fix(oauth): correctly remove code_verifier cookie when used (#2325)
Co-authored-by: Pol Bonastre <pbonastre@plainconcepts.com>
2021-07-08 17:24:56 +02:00
Balázs Orbán
ac5b4db0f2 chore: add OpenCollective link to FUNDING.yml 2021-07-05 17:54:34 +02:00
Mahieyin Rahmun
8bbffdd08c docs(github): remove title property (#2308) 2021-07-04 13:23:44 +02:00
Mahieyin Rahmun
a22a0a36fd docs(github): remove title prefix and make reproductions required (#2306) 2021-07-04 11:19:13 +02:00
Mahieyin Rahmun
797272afe1 docs: use issue template forms (#2274)
* (docs) initial issue template forms as per #2271

* (typo) fix grammar and typo

* (forms) make the requested changes

* (chore) delete the old .md files

* (forms) fix type key
2021-07-02 21:13:03 +02:00
Mahieyin Rahmun
13e56bcf2f docs(adapters): update outdated documentation (#2296) 2021-07-02 12:50:27 +02:00
yokinist
b0f7f87c04 docs: update 'pages' option in example code (#2270) 2021-07-01 17:12:01 +02:00
Balázs Orbán
9c0851c0f9 chore(ci): shorten names in release.yml workflow 2021-06-30 21:36:28 +02:00
Andriy Komm
f5b3c29ab1 fix(ts): improve authorize typing on Credentials provider (#2227)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-30 15:49:38 +02:00
Nico Domino
b4f2a0106a chore(ci): add environment approval (#2214)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-30 15:28:12 +02:00
Balázs Orbán
9c095b0532 chore(dev): fix dev app when running locally (#2280)
* fix: fix console warning in dev app

* chore: add `npm i` to `dev:setup` script

* chore(deps): update dev dependencies (react+next)

* chore: update package-lock.json

* chore: use node 16 in actions
2021-06-29 22:11:55 +02:00
Nico Domino
0475964a0f chore(pages): typo in error messages (#2265) 2021-06-28 02:57:35 +02:00
Justin Forlenza
ad6c13cdc9 fix(ts): extend server type in Email provider from nodemailer (#2259)
* Added optional secure & TLS settings for SMTP

* Replaced custom interface with nodemailers

* Fix lockfile version

* Apply suggestions from code review

* Apply suggestions from code review

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-27 18:51:34 +02:00
Nico Domino
591aa7cc7e docs(adapter): rm @canary from adapters' install instructions (#2260) 2021-06-27 18:28:58 +02:00
ndom91
9abb392b4e chore: fix gh action typo 2021-06-27 03:39:38 +02:00
ndom91
b89ae87fb1 docs: respect color mode 2021-06-27 03:38:04 +02:00
ndom91
3687d17724 Merge branch 'main' of ssh://github.com/nextauthjs/next-auth 2021-06-27 03:11:07 +02:00
Balázs Orbán
b04ff82fb9 chore: clarify where to run envinfo in bug report template 2021-06-24 01:46:02 +02:00
Balázs Orbán
c11915ba9c chore: update bug report template 2021-06-24 01:44:33 +02:00
Balázs Orbán
24ee459f97 chore(ci): run tests and typechecks only 2021-06-24 00:38:17 +02:00
Balázs Orbán
ac4851d238 chore(ci): run test:ci (linting+test+typecheck) 2021-06-24 00:33:32 +02:00
can-mihci
84094b0ee7 docs(client): fix code block typo (#2217) 2021-06-22 20:11:18 +02:00
Vikrant Bhat
f09ab4a04f docs(providers): fix typo (#2220) 2021-06-22 20:08:43 +02:00
Vikrant Bhat
067364381b docs(providers): fix english sentence in Email provider section (#2222) 2021-06-22 09:28:47 +02:00
ndom91
6ee36b6842 ci: test release environment approval 2021-06-18 20:03:07 +02:00
Sangwon Park
5a89ab69d3 feat(provider): add Naver provider (#2172)
* add Naver provider

* fix typo

* Update src/providers/naver.js

Co-authored-by: Balázs Orbán <info@balazsorban.com>

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-16 00:46:41 +02:00
Balázs Orbán
665445818e docs(config): link to next documentation instead of canary 2021-06-12 17:11:53 +02:00
ndom91
67cf2a11bb docs: fix alt client provider example 2021-06-12 16:42:48 +02:00
213 changed files with 9685 additions and 18822 deletions

1
.github/CODEOWNERS vendored
View File

@@ -1,2 +1 @@
/types/ @balazsorban44 @lluia
/__tests__/ @lluia

7
.gitignore vendored
View File

@@ -40,8 +40,6 @@ src/providers/index.js
/providers.js
/errors.js
/errors.d.ts
/react.js
/react.d.ts
# Development app
app/next-auth
@@ -64,3 +62,8 @@ app/yarn.lock
# Tests
/coverage
# v4
packages
apps
docs/providers.json

View File

@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# npx pretty-quick --staged
npx pretty-quick --staged

View File

@@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
@@ -55,11 +55,11 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting me@iaincollins.com. All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and
appropriate to the circumstances. The project team is obligated to maintain
confidentiality with regard to the reporter of an incident. Further details of
specific enforcement policies may be posted separately.
reported by contacting me@iaincollins.com or info@balazsorban.com and yo@ndo.dev.
All complaints will be reviewed and investigated and will result in a response
that is deemed necessary and appropriate to the circumstances. The project team
is obligated to maintain confidentiality with regard to the reporter of an
incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other

View File

@@ -67,15 +67,15 @@ NextAuth.js can be used with or without a database.
### 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 (CSRF) Tokens on POST routes (sign in, sign out)
- Promotes the use of passwordless sign in mechanisms
- Designed to be secure by default and encourage best practice 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 signed by default (JWS) with HS512
- Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
- Auto-generates symmetric signing and encryption keys for developer convenience
- Features tab/window syncing and session polling to support short lived sessions
- Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org)
- 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 is able to sign in and how often sessions have to be re-validated.
@@ -83,12 +83,13 @@ Advanced options allow you to define your own routines to handle controlling wha
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentation.
The package at `@types/next-auth` is now deprecated.
## Example
### Add API Route
```javascript
// pages/api/auth/[...nextauth].js
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
@@ -109,18 +110,18 @@ export default NextAuth({
from: "<no-reply@example.com>",
}),
],
// SQL or MongoDB database (or leave empty)
database: process.env.DATABASE_URL,
})
```
### Add React Hook
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
### Add React Component
```javascript
import { useSession, signIn, signOut } from "next-auth/react"
import { useSession, signIn, signOut } from "next-auth/client"
export default function Component() {
const { data: session } = useSession()
const [session, loading] = useSession()
if (session) {
return (
<>
@@ -138,26 +139,7 @@ export default function Component() {
}
```
### Share/configure session state
Use the `<SessionProvider>` to allows instances of `useSession()` to share the session object across components. It also takes care of keeping the session updated and synced between tabs/windows.
```jsx title="pages/_app.js"
import { SessionProvider } from "next-auth/react"
export default function App({
Component,
pageProps: { session, ...pageProps }
}) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}
```
## Acknowledgments
## Acknowledgements
[NextAuth.js is made possible thanks to all of its contributors.](https://next-auth.js.org/contributors)
@@ -165,14 +147,44 @@ export default function App({
<img width="500px" src="https://contrib.rocks/image?repo=nextauthjs/next-auth" />
</a>
<div>
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss">
<img width="170px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/canary/www/static/img/powered-by-vercel.svg" alt="Powered By Vercel" />
</a>
</div>
<div>
<p align="left">Thanks to Vercel sponsoring this project by allowing it to be deployed for free for the entire NextAuth.js Team</p>
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss"></a>
</div>
### Support
We're happy to announce we've recently created an [OpenCollective](https://opencollective.org/nextauth) for individuals and companies looking to contribute financially to the project!
<!--sponsors start-->
<table>
<tbody>
<tr>
<td align="center" valign="top">
<a href="https://vercel.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/14985020?v=4" alt="Vercel Logo" />
</a><br />
<div>Vercel</div><br />
<sub>🥉 Bronze Financial Sponsor <br /> ☁️ Infrastructure Support</sub>
</td>
<td align="center" valign="top">
<a href="https://prisma.io" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/17219288?v=4" alt="Prisma Logo" />
</a><br />
<div>Prisma</div><br />
<sub>🥉 Bronze Financial Sponsor</sub>
</td>
<td align="center" valign="top">
<a href="https://checklyhq.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/25982255?v=4" alt="Checkly Logo" />
</a><br />
<div>Checkly</div><br />
<sub>☁️ Infrastructure Support</sub>
</td>
</tr><tr></tr>
</tbody>
</table>
<br />
<!--sponsors end-->
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please first read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/canary/CONTRIBUTING.md).

View File

@@ -19,6 +19,6 @@ If you contact us regarding a serious issue:
- We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
- If 90 days has elapsed and we still don't have a fix, we will disclose the issue publicly.
Currently, the best way to report an issue is by emailing me@iaincollins.com
Currently, the best way to report an issue is by contacting us via email at me@iaincollins.com or info@balazsorban.com and yo@ndo.dev.
For less serious issues (e.g. RFC compliance for unsupported flows or potential issues that may cause a problem future or default behaviour / options) it is appropriate to submit these these publically as bug reports or feature requests or to raise a question to open a discussion around them.

View File

@@ -1,18 +1,17 @@
import { signIn } from "next-auth/react"
import { signIn } from 'next-auth/client'
export default function AccessDenied() {
export default function AccessDenied () {
return (
<>
<h1>Access Denied</h1>
<p>
<a
href="/api/auth/signin"
href='/api/auth/signin'
onClick={(e) => {
e.preventDefault()
signIn()
}}
>
You must be signed in to view this page
>You must be signed in to view this page
</a>
</p>
</>

View File

@@ -1,22 +1,22 @@
import Link from "next/link"
import { signIn, signOut, useSession } from "next-auth/react"
import styles from "./header.module.css"
import Link from 'next/link'
import { signIn, signOut, useSession } from 'next-auth/client'
import styles from './header.module.css'
// The approach used in this component shows how to built a sign in and sign out
// component that works on pages which support both client and server side
// rendering, and avoids any flash incorrect content on initial page load.
export default function Header() {
const { data: session, status } = useSession()
export default function Header () {
const [session, loading] = useSession()
return (
<header>
<noscript>
<style>{".nojs-show { opacity: 1; top: 0; }"}</style>
<style>{'.nojs-show { opacity: 1; top: 0; }'}</style>
</noscript>
<div className={styles.signedInStatus}>
<p
className={`nojs-show ${
!session && status === "loading" ? styles.loading : styles.loaded
!session && loading ? styles.loading : styles.loaded
}`}
>
{!session && (
@@ -25,7 +25,7 @@ export default function Header() {
You are not signed in
</span>
<a
href="/api/auth/signin"
href='/api/auth/signin'
className={styles.buttonPrimary}
onClick={(e) => {
e.preventDefault()
@@ -50,7 +50,7 @@ export default function Header() {
<strong>{session.user.email || session.user.name}</strong>
</span>
<a
href="/api/auth/signout"
href='/api/auth/signout'
className={styles.button}
onClick={(e) => {
e.preventDefault()
@@ -66,42 +66,42 @@ export default function Header() {
<nav>
<ul className={styles.navItems}>
<li className={styles.navItem}>
<Link href="/">
<Link href='/'>
<a>Home</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href="/client">
<Link href='/client'>
<a>Client</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href="/server">
<Link href='/server'>
<a>Server</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href="/protected">
<Link href='/protected'>
<a>Protected</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href="/protected-ssr">
<Link href='/protected-ssr'>
<a>Protected(SSR)</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href="/api-example">
<Link href='/api-example'>
<a>API</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href="/credentials">
<Link href='/credentials'>
<a>Credentials</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href="/email">
<Link href='/email'>
<a>Email</a>
</Link>
</li>

View File

@@ -7,7 +7,7 @@ module.exports = {
alias: {
...config.resolve.alias,
"next-auth$": path.join(process.cwd(), "next-auth/server"),
"next-auth/react$": path.join(process.cwd(), "next-auth/client/react"),
"next-auth/client$": path.join(process.cwd(), "next-auth/client"),
"next-auth/jwt$": path.join(process.cwd(), "next-auth/lib/jwt"),
"next-auth/adapters": path.join(process.cwd(), "next-auth/adapters"),
"next-auth/providers": path.join(process.cwd(), "next-auth/providers"),

View File

@@ -15,7 +15,6 @@
"license": "ISC",
"dependencies": {
"next": "^11.0.1",
"nodemailer": "^6.6.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},

View File

@@ -1,13 +1,31 @@
import { SessionProvider } from "next-auth/react"
import { Provider } from "next-auth/client"
import "./styles.css"
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
// Use the <Provider> to improve performance and allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App({ Component, pageProps }) {
return (
<SessionProvider session={session}>
<Provider
// Provider options are not required but can be useful in situations where
// you have a short session maxAge time. Shown here with default values.
options={{
// Client Max Age controls how often the useSession in the client should
// contact the server to sync the session state. Value in seconds.
// e.g.
// * 0 - Disabled (always use cache value)
// * 60 - Sync session state with server if it's older than 60 seconds
clientMaxAge: 0,
// Keep Alive tells windows / tabs that are signed in to keep sending
// a keep alive request (which extends the current session expiry) to
// prevent sessions in open windows from expiring. Value in seconds.
//
// Note: If a session has expired when keep alive is triggered, all open
// windows / tabs will be updated to reflect the user is signed out.
keepAlive: 0,
}}
session={pageProps.session}
>
<Component {...pageProps} />
</SessionProvider>
</Provider>
)
}

View File

@@ -5,6 +5,10 @@ import Auth0Provider from "next-auth/providers/auth0"
import TwitterProvider from "next-auth/providers/twitter"
import CredentialsProvider from "next-auth/providers/credentials"
// import Adapters from 'next-auth/adapters'
// import { PrismaClient } from '@prisma/client'
// const prisma = new PrismaClient()
export default NextAuth({
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
// cookies: {
@@ -41,11 +45,11 @@ export default NextAuth({
clientSecret: process.env.AUTH0_SECRET,
domain: process.env.AUTH0_DOMAIN,
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
// checks: ["pkce", "state"],
// protection: ["pkce", "state"],
// authorizationParams: {
// response_mode: 'form_post'
// }
checks: ["pkce"],
protection: "pkce",
}),
TwitterProvider({
clientId: process.env.TWITTER_ID,
@@ -75,4 +79,13 @@ export default NextAuth({
},
debug: false,
theme: "auto",
// Default Database Adapter (TypeORM)
// database: process.env.DATABASE_URL
// Prisma Database Adapter
// To configure this app to use the schema in `prisma/schema.prisma` run:
// npx prisma generate
// npx prisma migrate dev
// adapter: Adapters.Prisma.Adapter({ prisma })
})

View File

@@ -1,5 +1,5 @@
// This is an example of how to read a JSON Web Token from an API route
import jwt from "next-auth/jwt"
import jwt from 'next-auth/jwt'
const secret = process.env.SECRET

View File

@@ -1,17 +1,12 @@
// This is an example of to protect an API route
import { getSession } from "next-auth/react"
import { getSession } from 'next-auth/client'
export default async (req, res) => {
const session = await getSession({ req })
if (session) {
res.send({
content:
"This is protected content. You can access this content because you are signed in.",
})
res.send({ content: 'This is protected content. You can access this content because you are signed in.' })
} else {
res.send({
error: "You must be sign in to view the protected content on this page.",
})
res.send({ error: 'You must be sign in to view the protected content on this page.' })
}
}

View File

@@ -1,5 +1,5 @@
// This is an example of how to access a session from an API route
import { getSession } from "next-auth/react"
import { getSession } from 'next-auth/client'
export default async (req, res) => {
const session = await getSession({ req })

View File

@@ -1,15 +1,15 @@
// eslint-disable-next-line no-use-before-define
import * as React from "react"
import { signIn, signOut, useSession } from "next-auth/react"
import Layout from "components/layout"
import * as React from 'react'
import { signIn, signOut, useSession } from 'next-auth/client'
import Layout from 'components/layout'
export default function Page() {
export default function Page () {
const [response, setResponse] = React.useState(null)
const handleLogin = (options) => async () => {
if (options.redirect) {
return signIn("credentials", options)
return signIn('credentials', options)
}
const response = await signIn("credentials", options)
const response = await signIn('credentials', options)
setResponse(response)
}
@@ -21,22 +21,18 @@ export default function Page() {
setResponse(response)
}
const { data: session } = useSession()
const [session] = useSession()
if (session) {
return (
<Layout>
<h1>Test different flows for Credentials logout</h1>
<span className="spacing">Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button>
<br />
<span className="spacing">No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button>
<br />
<span className='spacing'>Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
<span className='spacing'>No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
<p>Response:</p>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
</Layout>
)
}
@@ -44,24 +40,14 @@ export default function Page() {
return (
<Layout>
<h1>Test different flows for Credentials login</h1>
<span className="spacing">Default:</span>
<button onClick={handleLogin({ redirect: true, password: "password" })}>
Login
</button>
<br />
<span className="spacing">No redirect:</span>
<button onClick={handleLogin({ redirect: false, password: "password" })}>
Login
</button>
<br />
<span className="spacing">No redirect, wrong password:</span>
<button onClick={handleLogin({ redirect: false, password: "" })}>
Login
</button>
<span className='spacing'>Default:</span>
<button onClick={handleLogin({ redirect: true, password: 'password' })}>Login</button><br />
<span className='spacing'>No redirect:</span>
<button onClick={handleLogin({ redirect: false, password: 'password' })}>Login</button><br />
<span className='spacing'>No redirect, wrong password:</span>
<button onClick={handleLogin({ redirect: false, password: '' })}>Login</button>
<p>Response:</p>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
</Layout>
)
}

View File

@@ -1,11 +1,11 @@
// eslint-disable-next-line no-use-before-define
import * as React from "react"
import { signIn, signOut, useSession } from "next-auth/react"
import Layout from "components/layout"
import * as React from 'react'
import { signIn, signOut, useSession } from 'next-auth/client'
import Layout from 'components/layout'
export default function Page() {
export default function Page () {
const [response, setResponse] = React.useState(null)
const [email, setEmail] = React.useState("")
const [email, setEmail] = React.useState('')
const handleChange = (event) => {
setEmail(event.target.value)
@@ -15,9 +15,9 @@ export default function Page() {
event.preventDefault()
if (options.redirect) {
return signIn("email", options)
return signIn('email', options)
}
const response = await signIn("email", options)
const response = await signIn('email', options)
setResponse(response)
}
@@ -29,22 +29,18 @@ export default function Page() {
setResponse(response)
}
const { data: session } = useSession()
const [session] = useSession()
if (session) {
return (
<Layout>
<h1>Test different flows for Email logout</h1>
<span className="spacing">Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button>
<br />
<span className="spacing">No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button>
<br />
<span className='spacing'>Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
<span className='spacing'>No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
<p>Response:</p>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
</Layout>
)
}
@@ -52,29 +48,20 @@ export default function Page() {
return (
<Layout>
<h1>Test different flows for Email login</h1>
<label className="spacing">
Email address:{" "}
<input
type="text"
id="email"
name="email"
value={email}
onChange={handleChange}
/>
</label>
<br />
<label className='spacing'>
Email address:{' '}
<input type='text' id='email' name='email' value={email} onChange={handleChange} />
</label><br />
<form onSubmit={handleLogin({ redirect: true, email })}>
<span className="spacing">Default:</span>
<button type="submit">Sign in with Email</button>
<span className='spacing'>Default:</span>
<button type='submit'>Sign in with Email</button>
</form>
<form onSubmit={handleLogin({ redirect: false, email })}>
<span className="spacing">No redirect:</span>
<button type="submit">Sign in with Email</button>
<span className='spacing'>No redirect:</span>
<button type='submit'>Sign in with Email</button>
</form>
<p>Response:</p>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
</Layout>
)
}

View File

@@ -1,47 +1,37 @@
// This is an example of how to protect content using server rendering
import { getSession } from "next-auth/react"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"
import { getSession } from 'next-auth/client'
import Layout from '../components/layout'
import AccessDenied from '../components/access-denied'
export default function Page({ content, session }) {
export default function Page ({ content, session }) {
// If no session exists, display access denied message
if (!session) {
return (
<Layout>
<AccessDenied />
</Layout>
)
}
if (!session) { return <Layout><AccessDenied /></Layout> }
// If session exists, display content
return (
<Layout>
<h1>Protected Page</h1>
<p>
<strong>{content}</strong>
</p>
<p><strong>{content}</strong></p>
</Layout>
)
}
export async function getServerSideProps(context) {
export async function getServerSideProps (context) {
const session = await getSession(context)
let content = null
if (session) {
const hostname = process.env.NEXTAUTH_URL || "http://localhost:3000"
const hostname = process.env.NEXTAUTH_URL || 'http://localhost:3000'
const options = { headers: { cookie: context.req.headers.cookie } }
const res = await fetch(`${hostname}/api/examples/protected`, options)
const json = await res.json()
if (json.content) {
content = json.content
}
if (json.content) { content = json.content }
}
return {
props: {
session,
content,
},
content
}
}
}

View File

@@ -1,35 +1,33 @@
import { useState, useEffect } from "react"
import { useSession } from "next-auth/react"
import Layout from "../components/layout"
import { useState, useEffect } from 'react'
import { useSession } from 'next-auth/client'
import Layout from '../components/layout'
import AccessDenied from '../components/access-denied'
export default function Page() {
const { status } = useSession({
required: true,
})
export default function Page () {
const [session, loading] = useSession()
const [content, setContent] = useState()
// Fetch content from protected route
useEffect(() => {
if (status === "loading") return
const fetchData = async () => {
const res = await fetch("/api/examples/protected")
const res = await fetch('/api/examples/protected')
const json = await res.json()
if (json.content) {
setContent(json.content)
}
if (json.content) { setContent(json.content) }
}
fetchData()
}, [status])
}, [session])
if (status === "loading") return <Layout>Loading...</Layout>
// When rendering client side don't display anything until loading is complete
if (typeof window !== 'undefined' && loading) return null
// If no session exists, display access denied message
if (!session) { return <Layout><AccessDenied /></Layout> }
// If session exists, display content
return (
<Layout>
<h1>Protected Page</h1>
<p>
<strong>{content}</strong>
</p>
<p><strong>{content}</strong></p>
</Layout>
)
}

View File

@@ -1,7 +1,7 @@
import { getSession } from "next-auth/react"
import Layout from "../components/layout"
import { getSession } from 'next-auth/client'
import Layout from '../components/layout'
export default function Page() {
export default function Page () {
// As this page uses Server Side Rendering, the `session` will be already
// populated on render without needing to go through a loading stage.
// This is possible because of the shared context configured in `_app.js` that
@@ -11,31 +11,27 @@ export default function Page() {
<Layout>
<h1>Server Side Rendering</h1>
<p>
This page uses the universal <strong>getSession()</strong> method in{" "}
<strong>getServerSideProps()</strong>.
This page uses the universal <strong>getSession()</strong> method in <strong>getServerSideProps()</strong>.
</p>
<p>
Using <strong>getSession()</strong> in{" "}
<strong>getServerSideProps()</strong> is the recommended approach if you
need to support Server Side Rendering with authentication.
Using <strong>getSession()</strong> in <strong>getServerSideProps()</strong> is the recommended approach if you need to
support Server Side Rendering with authentication.
</p>
<p>
The advantage of Server Side Rendering is this page does not require
client side JavaScript.
The advantage of Server Side Rendering is this page does not require client side JavaScript.
</p>
<p>
The disadvantage of Server Side Rendering is that this page is slower to
render.
The disadvantage of Server Side Rendering is that this page is slower to render.
</p>
</Layout>
)
}
// Export the `session` prop to use sessions with Server Side Rendering
export async function getServerSideProps(context) {
export async function getServerSideProps (context) {
return {
props: {
session: await getSession(context),
},
session: await getSession(context)
}
}
}

View File

@@ -3,7 +3,7 @@
// https://nextjs.org/docs/basic-features/supported-browsers-features
module.exports = {
presets: [["@babel/preset-env", { targets: { node: "12" } }]],
presets: [["@babel/preset-env", { targets: { node: "10.13" } }]],
plugins: [
"@babel/plugin-proposal-optional-catch-binding",
"@babel/plugin-transform-runtime",
@@ -12,10 +12,7 @@ module.exports = {
overrides: [
{
test: ["../src/client/**"],
presets: [
["@babel/preset-env", { targets: { ie: "11" } }],
["@babel/preset-react", { runtime: "automatic" }],
],
presets: [["@babel/preset-env", { targets: { ie: "11" } }]],
},
{
test: ["../src/server/pages/**"],
@@ -23,7 +20,14 @@ module.exports = {
},
{
test: ["../src/**/*.test.js"],
presets: [["@babel/preset-react", { runtime: "automatic" }]],
presets: [
[
"@babel/preset-react",
{
runtime: "automatic",
},
],
],
},
],
}

View File

@@ -3,7 +3,7 @@ const path = require("path")
const MODULE_ENTRIES = {
SERVER: "index",
REACT: "react",
CLIENT: "client",
PROVIDERS: "providers",
ADAPTERS: "adapters",
JWT: "jwt",
@@ -13,16 +13,12 @@ const MODULE_ENTRIES = {
// Building submodule entries
const BUILD_TARGETS = {
[`${MODULE_ENTRIES.SERVER}.js`]:
"module.exports = require('./dist/server').default\n",
[`${MODULE_ENTRIES.REACT}.js`]:
"module.exports = require('./dist/client/react').default\n",
[`${MODULE_ENTRIES.PROVIDERS}.js`]:
"module.exports = require('./dist/providers').default\n",
[`${MODULE_ENTRIES.JWT}.js`]:
"module.exports = require('./dist/lib/jwt').default\n",
[`${MODULE_ENTRIES.ERRORS}.js`]:
"module.exports = require('./dist/lib/errors').default\n",
[`${MODULE_ENTRIES.SERVER}.js`]: "module.exports = require('./dist/server').default\n",
[`${MODULE_ENTRIES.CLIENT}.js`]: "module.exports = require('./dist/client').default\n",
[`${MODULE_ENTRIES.ADAPTERS}.js`]: "module.exports = require('./dist/adapters').default\n",
[`${MODULE_ENTRIES.PROVIDERS}.js`]: "module.exports = require('./dist/providers').default\n",
[`${MODULE_ENTRIES.JWT}.js`]: "module.exports = require('./dist/lib/jwt').default\n",
[`${MODULE_ENTRIES.ERRORS}.js`]: "module.exports = require('./dist/lib/errors').default\n",
}
Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
@@ -36,7 +32,7 @@ Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
const TYPES_TARGETS = [
`${MODULE_ENTRIES.SERVER}.d.ts`,
`${MODULE_ENTRIES.REACT}-client.d.ts`,
`${MODULE_ENTRIES.CLIENT}.d.ts`,
`${MODULE_ENTRIES.ADAPTERS}.d.ts`,
`${MODULE_ENTRIES.PROVIDERS}.d.ts`,
`${MODULE_ENTRIES.JWT}.d.ts`,
@@ -47,10 +43,7 @@ const TYPES_TARGETS = [
TYPES_TARGETS.forEach((target) => {
fs.copy(
path.resolve("types", target),
path.join(
process.cwd(),
target.startsWith("react-client") ? "react.d.ts" : target
),
path.join(process.cwd(), target),
(err) => {
if (err) throw err
console.log(`[build-types] copying "${target}" to root folder`)

View File

@@ -8,5 +8,4 @@ module.exports = {
collectCoverageFrom: ["!client/__tests__/**"],
testMatch: ["**/*.test.js"],
coverageDirectory: "../coverage",
testEnvironment: "jsdom",
}

11596
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "0.0.0-semantically-released",
"version": "3.29.9",
"description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",
@@ -22,7 +22,8 @@
"exports": {
".": "./dist/server/index.js",
"./jwt": "./dist/lib/jwt.js",
"./react": "./dist/client/react.js",
"./adapters": "./dist/adapters/index.js",
"./client": "./dist/client/index.js",
"./providers": "./dist/providers/index.js",
"./providers/*": "./dist/providers/*.js",
"./errors": "./dist/lib/errors.js"
@@ -42,8 +43,7 @@
"prepublishOnly": "npm run build",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"version:pr": "node ./config/version-pr",
"website": "cd www && npm run start"
"version:pr": "node ./config/version-pr"
},
"files": [
"dist",
@@ -53,8 +53,8 @@
"providers.d.ts",
"adapters.js",
"adapters.d.ts",
"react.js",
"react.d.ts",
"client.js",
"client.d.ts",
"errors.js",
"errors.d.ts",
"jwt.js",
@@ -63,67 +63,70 @@
],
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.14.6",
"futoin-hkdf": "^1.3.3",
"@babel/runtime": "^7.14.0",
"@next-auth/prisma-legacy-adapter": "0.1.2",
"@next-auth/typeorm-legacy-adapter": "0.1.4",
"futoin-hkdf": "^1.3.2",
"jose": "^1.27.2",
"jsonwebtoken": "^8.5.1",
"nodemailer": "^6.4.16",
"oauth": "^0.9.15",
"pkce-challenge": "^2.2.0",
"preact": "^10.5.13",
"preact-render-to-string": "^5.1.19"
"pkce-challenge": "^2.1.0",
"preact": "^10.4.1",
"preact-render-to-string": "^5.1.14",
"querystring": "^0.2.0"
},
"peerDependencies": {
"nodemailer": "^6.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^16.13.1 || ^17",
"react-dom": "^16.13.1 || ^17"
},
"peerOptionalDependencies": {
"nodemailer": "^6.6.2"
},
"peerDependenciesMeta": {
"nodemailer": {
"optional": true
}
"mongodb": "^3.5.9",
"mysql": "^2.18.1",
"mssql": "^6.2.1",
"pg": "^8.2.1",
"@prisma/client": "^2.16.1"
},
"devDependencies": {
"@babel/cli": "^7.14.5",
"@babel/core": "^7.14.6",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.14.7",
"@babel/preset-react": "^7.14.5",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.13.13",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^13.1.9",
"@types/nodemailer": "^6.4.2",
"@types/react": "^17.0.11",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"autoprefixer": "^10.2.6",
"babel-jest": "^27.0.5",
"@types/react": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"autoprefixer": "^9.7.6",
"babel-jest": "^26.6.3",
"babel-preset-preact": "^2.0.0",
"conventional-changelog-conventionalcommits": "4.6.0",
"cssnano": "^5.0.6",
"dtslint": "^4.1.0",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard-with-typescript": "^20.0.0",
"eslint-plugin-import": "^2.23.4",
"conventional-changelog-conventionalcommits": "4.4.0",
"cssnano": "^4.1.10",
"dotenv": "^8.2.0",
"dtslint": "^4.0.8",
"eslint": "^7.19.0",
"eslint-config-prettier": "^8.2.0",
"eslint-config-standard-with-typescript": "^19.0.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"fs-extra": "^10.0.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^5.0.0",
"husky": "^6.0.0",
"jest": "^27.0.5",
"msw": "^0.30.0",
"jest": "^26.6.3",
"msw": "^0.28.2",
"next": "^11.0.1",
"postcss-cli": "^8.3.1",
"postcss-nested": "^5.0.5",
"prettier": "^2.3.1",
"pretty-quick": "^3.1.1",
"postcss-cli": "^7.1.1",
"postcss-nested": "^4.2.1",
"prettier": "^2.2.1",
"pretty-quick": "^3.1.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript": "^4.3.4",
"typescript": "^4.1.3",
"whatwg-fetch": "^3.6.2"
},
"prettier": {

10
src/adapters/index.js Normal file
View File

@@ -0,0 +1,10 @@
import * as TypeORM from "./typeorm"
import * as Prisma from "./prisma"
export { TypeORM, Prisma }
export default {
Default: TypeORM.Adapter,
TypeORM,
Prisma,
}

6
src/adapters/prisma.js Normal file
View File

@@ -0,0 +1,6 @@
/*
* Source code can be found at:
* https://github.com/nextauthjs/adapters/tree/canary/packages/prisma-legacy
*/
export { PrismaLegacyAdapter as Adapter } from "@next-auth/prisma-legacy-adapter"

9
src/adapters/typeorm.js Normal file
View File

@@ -0,0 +1,9 @@
/*
* Source code can be found at:
* https://github.com/nextauthjs/adapters/tree/canary/packages/typeorm-legacy
*/
export {
TypeORMLegacyAdapter as Adapter,
Models,
} from "@next-auth/typeorm-legacy-adapter"

View File

@@ -1,7 +1,9 @@
import { useState } from "react"
import { rest } from "msw"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSession } from "./helpers/mocks"
import { SessionProvider, useSession } from "../react"
import { Provider, useSession } from ".."
import userEvent from "@testing-library/user-event"
beforeAll(() => {
server.listen()
@@ -16,22 +18,6 @@ afterAll(() => {
server.close()
})
test("it won't allow to fetch the session in isolation without a session context", () => {
function App() {
useSession()
return null
}
jest.spyOn(console, "error")
console.error.mockImplementation(() => {})
expect(() => render(<App />)).toThrow(
"useSession must be wrapped in a SessionProvider"
)
console.error.mockRestore()
})
test("fetches the session once and re-uses it for different consumers", async () => {
const sessionRouteCall = jest.fn()
@@ -44,9 +30,6 @@ test("fetches the session once and re-uses it for different consumers", async ()
render(<ProviderFlow />)
expect(screen.getByTestId("session-consumer-1")).toHaveTextContent("loading")
expect(screen.getByTestId("session-consumer-2")).toHaveTextContent("loading")
await waitFor(() => {
expect(sessionRouteCall).toHaveBeenCalledTimes(1)
@@ -57,44 +40,25 @@ test("fetches the session once and re-uses it for different consumers", async ()
})
})
test("when there's an existing session, it won't initialize as loading", async () => {
const sessionRouteCall = jest.fn()
server.use(
rest.get("/api/auth/session", (req, res, ctx) => {
sessionRouteCall()
res(ctx.status(200), ctx.json(mockSession))
})
)
render(<ProviderFlow session={mockSession} />)
expect(await screen.findByTestId("session-consumer-1")).not.toHaveTextContent(
"loading"
)
expect(screen.getByTestId("session-consumer-2")).not.toHaveTextContent(
"loading"
)
expect(sessionRouteCall).not.toHaveBeenCalled()
})
function ProviderFlow({ options = {} }) {
return (
<SessionProvider {...options}>
<SessionConsumer />
<SessionConsumer testId="2" />
</SessionProvider>
<>
<Provider options={options}>
<SessionConsumer />
<SessionConsumer testId="2" />
</Provider>
</>
)
}
function SessionConsumer({ testId = 1 }) {
const { data: session, status } = useSession()
const [session, loading] = useSession()
if (loading) return <span>loading</span>
return (
<div data-testid={`session-consumer-${testId}`}>
{status === "loading" ? "loading" : JSON.stringify(session)}
{JSON.stringify(session)}
</div>
)
}

View File

@@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockCSRFToken } from "./helpers/mocks"
import logger from "../../lib/logger"
import { getCsrfToken } from "../react"
import { getCsrfToken } from ".."
import { rest } from "msw"
jest.mock("../../lib/logger", () => ({
@@ -78,10 +78,11 @@ test("when the fetch fails it'll throw a client fetch error", async () => {
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
path: "csrf",
error: new SyntaxError("Unexpected token s in JSON at position 0"),
})
expect(logger.error).toBeCalledWith(
"CLIENT_FETCH_ERROR",
"csrf",
new SyntaxError("Unexpected token s in JSON at position 0")
)
})
})

View File

@@ -2,7 +2,7 @@ import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockProviders } from "./helpers/mocks"
import { getProviders } from "../react"
import { getProviders } from ".."
import logger from "../../lib/logger"
import { rest } from "msw"
@@ -56,10 +56,11 @@ test("when failing to fetch the providers, it'll log the error", async () => {
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
path: "providers",
error: new SyntaxError("Unexpected token s in JSON at position 0"),
})
expect(logger.error).toBeCalledWith(
"CLIENT_FETCH_ERROR",
"providers",
new SyntaxError("Unexpected token s in JSON at position 0")
)
})
})

View File

@@ -3,7 +3,7 @@ import { rest } from "msw"
import { server, mockSession } from "./helpers/mocks"
import logger from "../../lib/logger"
import { useState, useEffect } from "react"
import { getSession } from "../react"
import { getSession } from ".."
import { getBroadcastEvents } from "./helpers/utils"
jest.mock("../../lib/logger", () => ({
@@ -70,10 +70,11 @@ test("if there's an error fetching the session, it should log it", async () => {
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
path: "session",
error: new SyntaxError("Unexpected token S in JSON at position 0"),
})
expect(logger.error).toBeCalledWith(
"CLIENT_FETCH_ERROR",
"session",
new SyntaxError("Unexpected token S in JSON at position 0")
)
})
})

View File

@@ -8,7 +8,7 @@ import {
mockEmailResponse,
mockGithubResponse,
} from "./helpers/mocks"
import { signIn } from "../react"
import { signIn } from ".."
import { rest } from "msw"
const { location } = window
@@ -250,10 +250,11 @@ test("when it fails to fetch the providers, it redirected back to signin page",
expect(window.location.replace).toHaveBeenCalledWith(`/api/auth/error`)
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
error: "Error when retrieving providers",
path: "providers",
})
expect(logger.error).toBeCalledWith(
"CLIENT_FETCH_ERROR",
"providers",
errorMsg
)
})
})

View File

@@ -2,7 +2,7 @@ import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSignOutResponse } from "./helpers/mocks"
import { signOut } from "../react"
import { signOut } from ".."
import { rest } from "msw"
import { getBroadcastEvents } from "./helpers/utils"

418
src/client/index.js Normal file
View File

@@ -0,0 +1,418 @@
// Note about signIn() and signOut() methods:
//
// On signIn() and signOut() we pass 'json: true' to request a response in JSON
// instead of HTTP as redirect URLs on other domains are not returned to
// requests made using the fetch API in the browser, and we need to ask the API
// to return the response as a JSON object (the end point still defaults to
// returning an HTTP response with a redirect for non-JavaScript clients).
//
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
import {
useState,
useEffect,
useContext,
createContext,
createElement,
} from "react"
import _logger, { proxyLogger } from "../lib/logger"
import parseUrl from "../lib/parse-url"
// This behaviour mirrors the default behaviour for getting the site name that
// happens server side in server/index.js
// 1. An empty value is legitimate when the code is being invoked client side as
// relative URLs are valid in that context and so defaults to empty.
// 2. When invoked server side the value is picked up from an environment
// variable and defaults to 'http://localhost:3000'.
/** @type {import("types/internals/client").NextAuthConfig} */
const __NEXTAUTH = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
baseUrlServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL ||
process.env.NEXTAUTH_URL ||
process.env.VERCEL_URL
).baseUrl,
basePathServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL
).basePath,
keepAlive: 0,
clientMaxAge: 0,
// Properties starting with _ are used for tracking internal app state
_clientLastSync: 0,
_clientSyncTimer: null,
_eventListenersAdded: false,
_clientSession: undefined,
_getSession: () => {},
}
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
const broadcast = BroadcastChannel()
// Add event listners on load
if (typeof window !== "undefined" && !__NEXTAUTH._eventListenersAdded) {
__NEXTAUTH._eventListenersAdded = true
// Listen for storage events and update session if event fired from
// another window (but suppress firing another event to avoid a loop)
// Fetch new session data but tell it to not to fire another event to
// avoid an infinite loop.
// Note: We could pass session data through and do something like
// `setData(message.data)` but that can cause problems depending
// on how the session object is being used in the client; it is
// more robust to have each window/tab fetch it's own copy of the
// session object rather than share it across instances.
broadcast.receive(() => __NEXTAUTH._getSession({ event: "storage" }))
// Listen for document visibility change events and
// if visibility of the document changes, re-fetch the session.
document.addEventListener(
"visibilitychange",
() => {
!document.hidden && __NEXTAUTH._getSession({ event: "visibilitychange" })
},
false
)
}
// Context to store session data globally
/** @type {import("types/internals/client").SessionContext} */
const SessionContext = createContext()
export function useSession(session) {
const context = useContext(SessionContext)
if (context) return context
return _useSessionHook(session)
}
function _useSessionHook(session) {
const [data, setData] = useState(session)
const [loading, setLoading] = useState(!data)
useEffect(() => {
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
try {
const triggredByEvent = event !== null
const triggeredByStorageEvent = event === "storage"
const clientMaxAge = __NEXTAUTH.clientMaxAge
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
const currentTime = _now()
const clientSession = __NEXTAUTH._clientSession
// Updates triggered by a storage event *always* trigger an update and we
// always update if we don't have any value for the current session state.
if (!triggeredByStorageEvent && clientSession !== undefined) {
if (clientMaxAge === 0 && triggredByEvent !== true) {
// If there is no time defined for when a session should be considered
// stale, then it's okay to use the value we have until an event is
// triggered which updates it.
return
} else if (clientMaxAge > 0 && clientSession === null) {
// If the client doesn't have a session then we don't need to call
// the server to check if it does (if they have signed in via another
// tab or window that will come through as a triggeredByStorageEvent
// event and will skip this logic)
return
} else if (
clientMaxAge > 0 &&
currentTime < clientLastSync + clientMaxAge
) {
// If the session freshness is within clientMaxAge then don't request
// it again on this call (avoids too many invokations).
return
}
}
if (clientSession === undefined) {
__NEXTAUTH._clientSession = null
}
// Update clientLastSync before making response to avoid repeated
// invokations that would otherwise be triggered while we are still
// waiting for a response.
__NEXTAUTH._clientLastSync = _now()
// If this call was invoked via a storage event (i.e. another window) then
// tell getSession not to trigger an event when it calls to avoid an
// infinate loop.
const newClientSessionData = await getSession({
triggerEvent: !triggeredByStorageEvent,
})
// Save session state internally, just so we can track that we've checked
// if a session exists at least once.
__NEXTAUTH._clientSession = newClientSessionData
setData(newClientSessionData)
setLoading(false)
} catch (error) {
logger.error("CLIENT_USE_SESSION_ERROR", error)
setLoading(false)
}
}
__NEXTAUTH._getSession()
})
return [data, loading]
}
export async function getSession(ctx) {
const session = await _fetchData("session", ctx)
if (ctx?.triggerEvent ?? true) {
broadcast.post({ event: "session", data: { trigger: "getSession" } })
}
return session
}
export async function getCsrfToken(ctx) {
return (await _fetchData("csrf", ctx))?.csrfToken
}
export async function getProviders() {
return await _fetchData("providers")
}
export async function signIn(provider, options = {}, authorizationParams = {}) {
const { callbackUrl = window.location.href, redirect = true } = options
const baseUrl = _apiBaseUrl()
const providers = await getProviders()
if (!providers) {
return window.location.replace(`${baseUrl}/error`)
}
if (!(provider in providers)) {
return window.location.replace(
`${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
)
}
const isCredentials = providers[provider].type === "credentials"
const isEmail = providers[provider].type === "email"
const isSupportingReturn = isCredentials || isEmail
const signInUrl = isCredentials
? `${baseUrl}/callback/${provider}`
: `${baseUrl}/signin/${provider}`
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
const res = await fetch(_signInUrl, {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
...options,
csrfToken: await getCsrfToken(),
callbackUrl,
json: true,
}),
})
const data = await res.json()
if (redirect || !isSupportingReturn) {
const url = data.url ?? callbackUrl
window.location.replace(url)
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload()
return
}
const error = new URL(data.url).searchParams.get("error")
if (res.ok) {
await __NEXTAUTH._getSession({ event: "storage" })
}
return {
error,
status: res.status,
ok: res.ok,
url: error ? null : data.url,
}
}
export async function signOut(options = {}) {
const { callbackUrl = window.location.href, redirect = true } = options
const baseUrl = _apiBaseUrl()
const fetchOptions = {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
csrfToken: await getCsrfToken(),
callbackUrl,
json: true,
}),
}
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
const data = await res.json()
broadcast.post({ event: "session", data: { trigger: "signout" } })
if (redirect) {
const url = data.url ?? callbackUrl
window.location.replace(url)
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload()
return
}
await __NEXTAUTH._getSession({ event: "storage" })
return data
}
// Method to set options. The documented way is to use the provider, but this
// method is being left in as an alternative, that will be helpful if/when we
// expose a vanilla JavaScript version that doesn't depend on React.
export function setOptions({
baseUrl,
basePath,
clientMaxAge,
keepAlive,
} = {}) {
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
if (basePath) __NEXTAUTH.basePath = basePath
if (clientMaxAge) __NEXTAUTH.clientMaxAge = clientMaxAge
if (keepAlive) {
__NEXTAUTH.keepAlive = keepAlive
if (typeof window === "undefined") return
// Clear existing timer (if there is one)
if (__NEXTAUTH._clientSyncTimer !== null) {
clearTimeout(__NEXTAUTH._clientSyncTimer)
}
// Set next timer to trigger in number of seconds
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
// Only invoke keepalive when a session exists
if (!__NEXTAUTH._clientSession) return
await __NEXTAUTH._getSession({ event: "timer" })
}, keepAlive * 1000)
}
}
export function Provider({ children, session, options }) {
setOptions(options)
return createElement(
SessionContext.Provider,
{ value: useSession(session) },
children
)
}
/**
* If passed 'appContext' via getInitialProps() in _app.js
* then get the req object from ctx and use that for the
* req value to allow _fetchData to
* work seemlessly in getInitialProps() on server side
* pages *and* in _app.js.
*/
async function _fetchData(path, { ctx, req = ctx?.req } = {}) {
try {
const baseUrl = await _apiBaseUrl()
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const res = await fetch(`${baseUrl}/${path}`, options)
const data = await res.json()
if (!res.ok) throw data
return Object.keys(data).length > 0 ? data : null // Return null if data empty
} catch (error) {
logger.error("CLIENT_FETCH_ERROR", path, error)
return null
}
}
function _apiBaseUrl() {
if (typeof window === "undefined") {
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
if (!process.env.NEXTAUTH_URL) {
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
}
// Return absolute path when called server side
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
}
// Return relative path when called client side
return __NEXTAUTH.basePath
}
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
function _now() {
return Math.floor(Date.now() / 1000)
}
/**
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
* Only not using it directly, because Safari does not support it.
*
* https://caniuse.com/?search=broadcastchannel
*/
function BroadcastChannel(name = "nextauth.message") {
return {
/**
* Get notified by other tabs/windows.
* @param {(message: import("types/internals/client").BroadcastMessage) => void} onReceive
*/
receive(onReceive) {
if (typeof window === "undefined") return
window.addEventListener("storage", async (event) => {
if (event.key !== name) return
/** @type {import("types/internals/client").BroadcastMessage} */
const message = JSON.parse(event.newValue)
if (message?.event !== "session" || !message?.data) return
onReceive(message)
})
},
/** Notify other tabs/windows. */
post(message) {
if (typeof localStorage === "undefined") return
localStorage.setItem(
name,
JSON.stringify({ ...message, timestamp: _now() })
)
},
}
}
// Some methods are exported with more than one name. This provides some
// flexibility over how they can be invoked and backwards compatibility
// with earlier releases. These should be removed in a newer release, as it only
// creates problems for bundlers and adds confusion to users. TypeScript declarations
// will provide sufficient help when importing
export {
setOptions as options,
getSession as session,
getProviders as providers,
getCsrfToken as csrfToken,
signIn as signin,
signOut as signout,
}
export default {
getSession,
getCsrfToken,
getProviders,
useSession,
signIn,
signOut,
Provider,
/* Deprecated / unsupported features below this line */
// Use setOptions() set options globally in the app.
setOptions,
// Some methods are exported with more than one name. This provides some
// flexibility over how they can be invoked and backwards compatibility
// with earlier releases.
options: setOptions,
session: getSession,
providers: getProviders,
csrfToken: getCsrfToken,
signin: signIn,
signout: signOut,
}

385
src/client/react.js vendored
View File

@@ -1,385 +0,0 @@
// Note about signIn() and signOut() methods:
//
// On signIn() and signOut() we pass 'json: true' to request a response in JSON
// instead of HTTP as redirect URLs on other domains are not returned to
// requests made using the fetch API in the browser, and we need to ask the API
// to return the response as a JSON object (the end point still defaults to
// returning an HTTP response with a redirect for non-JavaScript clients).
//
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
// eslint-disable-next-line no-use-before-define
import * as React from "react"
import _logger, { proxyLogger } from "../lib/logger"
import parseUrl from "../lib/parse-url"
// This behaviour mirrors the default behaviour for getting the site name that
// happens server side in server/index.js
// 1. An empty value is legitimate when the code is being invoked client side as
// relative URLs are valid in that context and so defaults to empty.
// 2. When invoked server side the value is picked up from an environment
// variable and defaults to 'http://localhost:3000'.
/** @type {import("types/internals/react").NextAuthConfig} */
const __NEXTAUTH = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
baseUrlServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL ||
process.env.NEXTAUTH_URL ||
process.env.VERCEL_URL
).baseUrl,
basePathServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL
).basePath,
_lastSync: 0,
_session: undefined,
_getSession: () => {},
}
const broadcast = BroadcastChannel()
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
/** @type {import("types/internals/react").SessionContext} */
const SessionContext = React.createContext()
export function useSession(options = {}) {
const value = React.useContext(SessionContext)
if (process.env.NODE_ENV !== "production" && !value) {
throw new Error("useSession must be wrapped in a SessionProvider")
}
const { required, onUnauthenticated } = options
const requiredAndNotLoading = required && value.status === "unauthenticated"
React.useEffect(() => {
if (requiredAndNotLoading) {
const url = `/api/auth/signin?${new URLSearchParams({
error: "SessionRequired",
callbackUrl: window.location.href,
})}`
if (onUnauthenticated) onUnauthenticated()
else window.location.replace(url)
}
}, [requiredAndNotLoading, onUnauthenticated])
if (requiredAndNotLoading) {
return { data: value.data, status: "loading" }
}
return value
}
export async function getSession(ctx) {
const session = await _fetchData("session", ctx)
if (ctx?.broadcast ?? true) {
broadcast.post({ event: "session", data: { trigger: "getSession" } })
}
return session
}
export async function getCsrfToken(ctx) {
const response = await _fetchData("csrf", ctx)
return response?.csrfToken
}
export async function getProviders() {
return await _fetchData("providers")
}
export async function signIn(provider, options = {}, authorizationParams = {}) {
const { callbackUrl = window.location.href, redirect = true } = options
const baseUrl = _apiBaseUrl()
const providers = await getProviders()
if (!providers) {
return window.location.replace(`${baseUrl}/error`)
}
if (!(provider in providers)) {
return window.location.replace(
`${baseUrl}/signin?${new URLSearchParams({ callbackUrl })}`
)
}
const isCredentials = providers[provider].type === "credentials"
const isEmail = providers[provider].type === "email"
const isSupportingReturn = isCredentials || isEmail
const signInUrl = `${baseUrl}/${
isCredentials ? "callback" : "signin"
}/${provider}`
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
const res = await fetch(_signInUrl, {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
...options,
csrfToken: await getCsrfToken(),
callbackUrl,
json: true,
}),
})
const data = await res.json()
if (redirect || !isSupportingReturn) {
const url = data.url ?? callbackUrl
window.location.replace(url)
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload()
return
}
const error = new URL(data.url).searchParams.get("error")
if (res.ok) {
await __NEXTAUTH._getSession({ event: "storage" })
}
return {
error,
status: res.status,
ok: res.ok,
url: error ? null : data.url,
}
}
export async function signOut(options = {}) {
const { callbackUrl = window.location.href, redirect = true } = options
const baseUrl = _apiBaseUrl()
const fetchOptions = {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
csrfToken: await getCsrfToken(),
callbackUrl,
json: true,
}),
}
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
const data = await res.json()
broadcast.post({ event: "session", data: { trigger: "signout" } })
if (redirect) {
const url = data.url ?? callbackUrl
window.location.replace(url)
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload()
return
}
await __NEXTAUTH._getSession({ event: "storage" })
return data
}
/** @param {import("types/react-client").SessionProviderProps} props */
export function SessionProvider(props) {
const { children, baseUrl, basePath, staleTime = 0 } = props
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
if (basePath) __NEXTAUTH.basePath = basePath
/**
* If session was `null`, there was an attempt to fetch it,
* but it failed, but we still treat it as a valid initial value.
*/
const hasInitialSession = props.session !== undefined
/** If session was passed, initialize as already synced */
__NEXTAUTH._lastSync = hasInitialSession ? _now() : 0
const [session, setSession] = React.useState(() => {
if (hasInitialSession) __NEXTAUTH._session = props.session
return props.session
})
/** If session was passed, initialize as not loading */
const [loading, setLoading] = React.useState(!hasInitialSession)
React.useEffect(() => {
__NEXTAUTH._getSession = async ({ event } = {}) => {
try {
const storageEvent = event === "storage"
// We should always update if we don't have a client session yet
// or if there are events from other tabs/windows
if (storageEvent || __NEXTAUTH._session === undefined) {
__NEXTAUTH._lastSync = _now()
__NEXTAUTH._session = await getSession({
broadcast: !storageEvent,
})
setSession(__NEXTAUTH._session)
return
}
if (
// If there is no time defined for when a session should be considered
// stale, then it's okay to use the value we have until an event is
// triggered which updates it
(staleTime === 0 && !event) ||
// If the client doesn't have a session then we don't need to call
// the server to check if it does (if they have signed in via another
// tab or window that will come through as a "stroage" event
// event anyway)
(staleTime > 0 && __NEXTAUTH._session === null) ||
// Bail out early if the client session is not stale yet
(staleTime > 0 && _now() < __NEXTAUTH._lastSync + staleTime)
) {
return
}
// An event or session staleness occurred, update the client session.
__NEXTAUTH._lastSync = _now()
__NEXTAUTH._session = await getSession()
setSession(__NEXTAUTH._session)
} catch (error) {
logger.error("CLIENT_SESSION_ERROR", error)
} finally {
setLoading(false)
}
}
__NEXTAUTH._getSession()
}, [staleTime])
React.useEffect(() => {
// Listen for storage events and update session if event fired from
// another window (but suppress firing another event to avoid a loop)
// Fetch new session data but tell it to not to fire another event to
// avoid an infinite loop.
// Note: We could pass session data through and do something like
// `setData(message.data)` but that can cause problems depending
// on how the session object is being used in the client; it is
// more robust to have each window/tab fetch it's own copy of the
// session object rather than share it across instances.
const unsubscribe = broadcast.receive(
async () => await __NEXTAUTH._getSession({ event: "storage" })
)
return () => unsubscribe()
}, [])
React.useEffect(() => {
// Set up visibility change
// Listen for document visibility change events and
// if visibility of the document changes, re-fetch the session.
const visibilityHandler = () => {
!document.hidden && __NEXTAUTH._getSession({ event: "visibilitychange" })
}
document.addEventListener("visibilitychange", visibilityHandler, false)
return () =>
document.removeEventListener("visibilitychange", visibilityHandler, false)
}, [])
React.useEffect(() => {
const { refetchInterval } = props
// Set up polling
if (refetchInterval) {
const refetchIntervalTimer = setInterval(async () => {
if (__NEXTAUTH._session) {
await __NEXTAUTH._getSession({ event: "poll" })
}
}, refetchInterval * 1000)
return () => clearInterval(refetchIntervalTimer)
}
}, [props.refetchInterval])
const value = React.useMemo(
() => ({
data: session,
status: loading
? "loading"
: session
? "authenticated"
: "unauthenticated",
}),
[session, loading]
)
return (
<SessionContext.Provider value={value}>{children}</SessionContext.Provider>
)
}
/**
* If passed 'appContext' via getInitialProps() in _app.js
* then get the req object from ctx and use that for the
* req value to allow _fetchData to
* work seemlessly in getInitialProps() on server side
* pages *and* in _app.js.
*/
async function _fetchData(path, { ctx, req = ctx?.req } = {}) {
try {
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const res = await fetch(`${_apiBaseUrl()}/${path}`, options)
const data = await res.json()
if (!res.ok) throw data
return Object.keys(data).length > 0 ? data : null // Return null if data empty
} catch (error) {
logger.error("CLIENT_FETCH_ERROR", {
error,
path,
...(req ? { header: req.headers } : {}),
})
return null
}
}
function _apiBaseUrl() {
if (typeof window === "undefined") {
// Return absolute path when called server side
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
}
// Return relative path when called client side
return __NEXTAUTH.basePath
}
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
function _now() {
return Math.floor(Date.now() / 1000)
}
/**
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
* Only not using it directly, because Safari does not support it.
*
* https://caniuse.com/?search=broadcastchannel
*/
function BroadcastChannel(name = "nextauth.message") {
return {
/**
* Get notified by other tabs/windows.
* @param {(message: import("types/internals/react").BroadcastMessage) => void} onReceive
*/
receive(onReceive) {
const handler = (event) => {
if (event.key !== name) return
/** @type {import("types/internals/react").BroadcastMessage} */
const message = JSON.parse(event.newValue)
if (message?.event !== "session" || !message?.data) return
onReceive(message)
}
window.addEventListener("storage", handler)
return () => window.removeEventListener("storage", handler)
},
/** Notify other tabs/windows. */
post(message) {
if (typeof window === "undefined") return
localStorage.setItem(
name,
JSON.stringify({ ...message, timestamp: _now() })
)
},
}
}

View File

@@ -32,3 +32,67 @@ export class OAuthCallbackError extends UnknownError {
export class AccountNotLinkedError extends UnknownError {
name = "AccountNotLinkedError"
}
export class CreateUserError extends UnknownError {
name = "CreateUserError"
}
export class GetUserError extends UnknownError {
name = "GetUserError"
}
export class GetUserByEmailError extends UnknownError {
name = "GetUserByEmailError"
}
export class GetUserByIdError extends UnknownError {
name = "GetUserByIdError"
}
export class GetUserByProviderAccountIdError extends UnknownError {
name = "GetUserByProviderAccountIdError"
}
export class UpdateUserError extends UnknownError {
name = "UpdateUserError"
}
export class DeleteUserError extends UnknownError {
name = "DeleteUserError"
}
export class LinkAccountError extends UnknownError {
name = "LinkAccountError"
}
export class UnlinkAccountError extends UnknownError {
name = "UnlinkAccountError"
}
export class CreateSessionError extends UnknownError {
name = "CreateSessionError"
}
export class GetSessionError extends UnknownError {
name = "GetSessionError"
}
export class UpdateSessionError extends UnknownError {
name = "UpdateSessionError"
}
export class DeleteSessionError extends UnknownError {
name = "DeleteSessionError"
}
export class CreateVerificationRequestError extends UnknownError {
name = "CreateVerificationRequestError"
}
export class GetVerificationRequestError extends UnknownError {
name = "GetVerificationRequestError"
}
export class DeleteVerificationRequestError extends UnknownError {
name = "DeleteVerificationRequestError"
}

View File

@@ -1,37 +1,22 @@
import { UnknownError } from "./errors"
/** Makes sure that error is always serializable */
function formatError(o) {
if (o instanceof Error && !(o instanceof UnknownError)) {
return { message: o.message, stack: o.stack, name: o.name }
}
if (o?.error) {
o.error = formatError(o.error)
o.message = o.message ?? o.error.message
}
return o
}
/** @type {import("types").LoggerInstance} */
const _logger = {
error(code, metadata) {
metadata = formatError(metadata)
error(code, ...message) {
console.error(
`[next-auth][error][${code.toLowerCase()}]`,
`\nhttps://next-auth.js.org/errors#${code.toLowerCase()}`,
metadata.message,
metadata
...message
)
},
warn(code) {
warn(code, ...message) {
console.warn(
`[next-auth][warn][${code.toLowerCase()}]`,
`\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}`
`\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}`,
...message
)
},
debug(code, metadata) {
debug(code, ...message) {
if (!process?.env?._NEXTAUTH_DEBUG) return
console.log(`[next-auth][debug][${code.toLowerCase()}]`, metadata)
console.log(`[next-auth][debug][${code.toLowerCase()}]`, ...message)
},
}
@@ -62,19 +47,31 @@ export function proxyLogger(logger = _logger, basePath) {
const clientLogger = {}
for (const level in logger) {
clientLogger[level] = (code, metadata) => {
_logger[level](code, metadata) // Logs to console
clientLogger[level] = (code, ...message) => {
_logger[level](code, ...message) // Log on client as usual
if (level === "error") {
metadata = formatError(metadata)
}
metadata.client = true
const url = `${basePath}/_log`
const body = new URLSearchParams({ level, code, ...metadata })
const body = new URLSearchParams({
level,
code,
message: JSON.stringify(
message.map((m) => {
if (m instanceof Error) {
// Serializing errors: https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
return { name: m.name, message: m.message, stack: m.stack }
}
return m
})
),
})
if (navigator.sendBeacon) {
return navigator.sendBeacon(url, body)
}
return fetch(url, { method: "POST", body, keepalive: true })
return fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body,
})
}
}
return clientLogger

View File

@@ -28,7 +28,7 @@ export default function Apple(options) {
privateKey: null,
keyId: null,
},
checks: ["none"], // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
protection: "none", // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
...options,
}
}

View File

@@ -1,7 +1,5 @@
export default function AzureADB2C(options) {
const { tenantName, primaryUserFlow } = options
const authorizeUrl = `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${primaryUserFlow}/oauth2/v2.0/authorize`
const tokenUrl = `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${primaryUserFlow}/oauth2/v2.0/token`
const tenant = options.tenantId ? options.tenantId : "common"
return {
id: "azure-ad-b2c",
@@ -11,29 +9,14 @@ export default function AzureADB2C(options) {
params: {
grant_type: "authorization_code",
},
accessTokenUrl: tokenUrl,
requestTokenUrl: tokenUrl,
authorizationUrl: `${authorizeUrl}?response_type=code+id_token&response_mode=query`,
profileUrl: 'https://graph.microsoft.com/oidc/userinfo',
idToken: true,
profile: (profile) => {
let name = ''
if (profile.name) {
// B2C "Display Name"
name = profile.name
} else if (profile.given_name && profile.family_name) {
// B2C "Given Name" & "Surname"
name = `${profile.given_name} ${profile.family_name}`
} else if (profile.given_name) {
// B2C "Given Name"
name = `${profile.given_name}`
}
accessTokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query`,
profileUrl: "https://graph.microsoft.com/v1.0/me/",
profile(profile) {
return {
name,
id: profile.oid,
email: profile.emails[0]
id: profile.id,
name: profile.displayName,
email: profile.userPrincipalName,
}
},
...options,

View File

@@ -1,24 +0,0 @@
export default function AzureAD(options) {
const tenant = options.tenantId ?? 'common'
return {
id: 'azure-ad',
name: 'Azure Active Directory',
type: 'oauth',
version: '2.0',
params: {
grant_type: 'authorization_code'
},
accessTokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query`,
profileUrl: 'https://graph.microsoft.com/v1.0/me/',
profile: (profile) => {
return {
id: profile.id,
name: profile.displayName,
email: profile.userPrincipalName
}
},
...options
}
}

View File

@@ -15,7 +15,7 @@
* ...
*
* // pages/index
* import { signIn } from "next-auth/react"
* import { signIn } from "next-auth/client"
* ...
* <button onClick={() => signIn("dropbox")}>
* Sign in
@@ -29,26 +29,26 @@
*/
export default function Dropbox(options) {
return {
id: "dropbox",
name: "Dropbox",
type: "oauth",
version: "2.0",
scope: "account_info.read",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://api.dropboxapi.com/oauth2/token",
id: 'dropbox',
name: 'Dropbox',
type: 'oauth',
version: '2.0',
scope: 'account_info.read',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.dropboxapi.com/oauth2/token',
authorizationUrl:
"https://www.dropbox.com/oauth2/authorize?token_access_type=offline&response_type=code",
profileUrl: "https://api.dropboxapi.com/2/users/get_current_account",
'https://www.dropbox.com/oauth2/authorize?token_access_type=offline&response_type=code',
profileUrl: 'https://api.dropboxapi.com/2/users/get_current_account',
profile: (profile) => {
return {
id: profile.account_id,
name: profile.name.display_name,
email: profile.email,
image: profile.profile_photo_url,
email_verified: profile.email_verified,
email_verified: profile.email_verified
}
},
checks: ["state", "pkce"],
...options,
protection: ["state", "pkce"],
...options
}
}

View File

@@ -1,5 +1,5 @@
import logger from '../lib/logger'
import nodemailer from "nodemailer"
import logger from "../lib/logger"
export default function Email(options) {
return {
@@ -22,33 +22,42 @@ export default function Email(options) {
}
}
async function sendVerificationRequest ({ identifier: email, url, baseUrl, provider }) {
const { server, from } = provider
// Strip protocol from URL and use domain as site name
const site = baseUrl.replace(/^https?:\/\//, '')
try {
await nodemailer
.createTransport(server)
.sendMail({
const sendVerificationRequest = ({
identifier: email,
url,
baseUrl,
provider,
}) => {
return new Promise((resolve, reject) => {
const { server, from } = provider
// Strip protocol from URL and use domain as site name
const site = baseUrl.replace(/^https?:\/\//, "")
nodemailer.createTransport(server).sendMail(
{
to: email,
from,
subject: `Sign in to ${site}`,
text: text({ url, site, email }),
html: html({ url, site, email })
})
} catch (error) {
logger.error('SEND_VERIFICATION_EMAIL_ERROR', email, error)
throw new Error('SEND_VERIFICATION_EMAIL_ERROR')
}
html: html({ url, site, email }),
},
(error) => {
if (error) {
logger.error("SEND_VERIFICATION_EMAIL_ERROR", error)
return reject(new Error("SEND_VERIFICATION_EMAIL_ERROR", error))
}
return resolve()
}
)
})
}
// Email HTML body
const html = ({ url, site, email }) => {
// Insert invisible space into domains and email address to prevent both the
// email address and the domain from being turned into a hyperlink by email
const html = ({ url, site }) => {
// Insert invisible space into domains to prevent the
// the domain from being turned into a hyperlink by email
// clients like Outlook and Apple mail, as this is confusing because it seems
// like they are supposed to click on their email address to sign in.
const escapedEmail = `${email.replace(/\./g, "&#8203;.")}`
// like they are supposed to click it to sign in.
const escapedSite = `${site.replace(/\./g, "&#8203;.")}`
// Some simple styling options
@@ -63,17 +72,12 @@ const html = ({ url, site, email }) => {
<body style="background: ${backgroundColor};">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
<strong>${escapedSite}</strong>
<td align="center" style="padding: 10px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
Sign in to <strong>${escapedSite}</strong>
</td>
</tr>
</table>
<table width="100%" border="0" cellspacing="20" cellpadding="0" style="background: ${mainBackgroundColor}; max-width: 600px; margin: auto; border-radius: 10px;">
<tr>
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
Sign in as <strong>${escapedEmail}</strong>
</td>
</tr>
<tr>
<td align="center" style="padding: 20px 0;">
<table border="0" cellspacing="0" cellpadding="0">

View File

@@ -0,0 +1,20 @@
export default function Freshbooks(options) {
return {
id: 'freshbooks',
name: 'Freshbooks',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.freshbooks.com/auth/oauth/token',
authorizationUrl: 'https://auth.freshbooks.com/service/auth/oauth/authorize?response_type=code',
profileUrl: 'https://api.freshbooks.com/auth/api/v1/users/me',
async profile(profile) {
return {
id: profile.response.id,
name: `${profile.response.first_name} ${profile.response.last_name}`,
email: profile.response.email,
};
},
...options
};
}

View File

@@ -10,7 +10,7 @@ export default function GitHub(options) {
profileUrl: "https://api.github.com/user",
profile(profile) {
return {
id: profile.id.toString(),
id: profile.id,
name: profile.name || profile.login,
email: profile.email,
image: profile.avatar_url,

View File

@@ -15,7 +15,7 @@
* ...
*
* // pages/index
* import { signIn } from "next-auth/react"
* import { signIn } from "next-auth/client"
* ...
* <button onClick={() => signIn("instagram")}>
* Sign in

View File

@@ -5,7 +5,7 @@ export default function Naver(options) {
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
checks: ["state"],
protection: ["state"],
accessTokenUrl: "https://nid.naver.com/oauth2.0/token",
authorizationUrl:
"https://nid.naver.com/oauth2.0/authorize?response_type=code",

19
src/providers/onelogin.js Normal file
View File

@@ -0,0 +1,19 @@
export default function OneLogin(options) {
return {
id: "onelogin",
name: "OneLogin",
type: "oauth",
version: "2.0",
scope: "openid profile name email",
params: { grant_type: "authorization_code" },
// These will be different depending on the Org.
accessTokenUrl: `https://${options.domain}/oidc/2/token`,
requestTokenUrl: `https://${options.domain}/oidc/2/auth`,
authorizationUrl: `https://${options.domain}/oidc/2/auth?response_type=code`,
profileUrl: `https://${options.domain}/oidc/2/me`,
profile(profile) {
return { ...profile, id: profile.sub }
},
...options,
}
}

View File

@@ -9,7 +9,7 @@ export default function Salesforce(options) {
authorizationUrl:
"https://login.salesforce.com/services/oauth2/authorize?response_type=code",
profileUrl: "https://login.salesforce.com/services/oauth2/userinfo",
checks: ["none"],
protection: "none",
profile(profile) {
return {
...profile,

View File

@@ -15,10 +15,7 @@ export default function Twitter(options) {
id: profile.id_str,
name: profile.name,
email: profile.email,
image: profile.profile_image_url_https.replace(
/_normal\.(jpg|png|gif)$/,
".$1"
),
image: profile.profile_image_url_https.replace(/_normal\.(jpg|png|gif)$/, ".$1"),
}
},
...options,

View File

@@ -1,16 +1,16 @@
export default function WorkOS(options) {
const domain = options.domain || "api.workos.com"
const domain = options.domain || 'api.workos.com';
return {
id: "workos",
name: "WorkOS",
type: "oauth",
version: "2.0",
scope: "",
id: 'workos',
name: 'WorkOS',
type: 'oauth',
version: '2.0',
scope: '',
params: {
grant_type: "authorization_code",
grant_type: 'authorization_code',
client_id: options.clientId,
client_secret: options.clientSecret,
client_secret: options.clientSecret
},
accessTokenUrl: `https://${domain}/sso/token`,
authorizationUrl: `https://${domain}/sso/authorize?response_type=code`,
@@ -18,9 +18,9 @@ export default function WorkOS(options) {
profile: (profile) => {
return {
...profile,
name: `${profile.first_name} ${profile.last_name}`,
name: `${profile.first_name} ${profile.last_name}`
}
},
...options,
...options
}
}

View File

@@ -4,7 +4,7 @@ export default function Yandex(options) {
name: "Yandex",
type: "oauth",
version: "2.0",
scope: "login:email login:info",
scope: "login:email login:info login:avatar",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://oauth.yandex.ru/token",
requestTokenUrl: "https://oauth.yandex.ru/token",
@@ -15,7 +15,7 @@ export default function Yandex(options) {
id: profile.id,
name: profile.real_name,
email: profile.default_email,
image: null,
image: profile.is_avatar_empty ? null : `https://avatars.yandex.net/get-yapic/${profile.default_avatar_id}/islands-200`,
}
},
...options,

View File

@@ -1,3 +1,4 @@
import adapters from "../adapters"
import jwt from "../lib/jwt"
import parseUrl from "../lib/parse-url"
import logger, { setLogger } from "../lib/logger"
@@ -17,7 +18,17 @@ import * as state from "./lib/oauth/state-handler"
// To work properly in production with OAuth providers the NEXTAUTH_URL
// environment variable must be set.
if (!process.env.NEXTAUTH_URL) {
logger.warn("NEXTAUTH_URL")
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
}
function isValidHttpUrl(url, baseUrl) {
try {
return /^https?:/.test(
new URL(url, url.startsWith("/") ? baseUrl : undefined).protocol
)
} catch {
return false
}
}
/**
@@ -43,11 +54,11 @@ async function NextAuthHandler(req, res, userOptions) {
extendRes(req, res, resolve)
if (!req.query.nextauth) {
const message =
const error =
"Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly."
logger.error("MISSING_NEXTAUTH_API_ROUTE_ERROR", new Error(message))
return res.status(500).end(`Error: ${message}`)
logger.error("MISSING_NEXTAUTH_API_ROUTE_ERROR", error)
return res.status(500).end(`Error: ${error}`)
}
const {
@@ -70,6 +81,23 @@ async function NextAuthHandler(req, res, userOptions) {
...userOptions.cookies,
}
const errorPage = userOptions.pages?.error ?? `${baseUrl}${basePath}/error`
const callbackUrlParam = req.query?.callbackUrl
if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, baseUrl)) {
return res.redirect(`${errorPage}?error=Configuration`)
}
const { callbackUrl: defaultCallbackUrl } = cookie.defaultCookies(
userOptions.useSecureCookies ?? baseUrl.startsWith("https://")
)
const callbackUrlCookie =
req.cookies?.[cookies?.callbackUrl?.name ?? defaultCallbackUrl.name]
if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, baseUrl)) {
return res.redirect(`${errorPage}?error=Configuration`)
}
const secret = createSecret({ userOptions, basePath, baseUrl })
const providers = parseProviders({
@@ -77,20 +105,37 @@ async function NextAuthHandler(req, res, userOptions) {
baseUrl,
basePath,
})
const provider = providers.find(({ id }) => id === providerId)
// Checks only work on OAuth 2.x providers
if (
provider?.type === "oauth" &&
provider.version?.startsWith("2") &&
!provider.checks
) {
provider.checks = ["state"]
// Protection only works on OAuth 2.x providers
// TODO:
// - rename to `checks` in 4.x, so it is similar to `openid-client`
// - stop supporting `protection` as string
// - remove `state` property
if (provider?.type === "oauth" && provider.version?.startsWith("2")) {
// Priority: (protection array > protection string) > state > default
if (provider.protection) {
provider.protection = Array.isArray(provider.protection)
? provider.protection
: [provider.protection]
} else if (provider.state !== undefined) {
provider.protection = [provider.state ? "state" : "none"]
} else {
// Default to state, as we did in 3.1
// REVIEW: should we use "pkce" or "none" as default?
provider.protection = ["state"]
}
}
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle
// Parse database / adapter
// If adapter is provided, use it (advanced usage, overrides database)
// If database URI or config object is provided, use it (simple usage)
const adapter =
userOptions.adapter ??
(userOptions.database && adapters.Default(userOptions.database))
// User provided options are overriden by other options,
// except for the options with special handling above
req.options = {
@@ -101,6 +146,7 @@ async function NextAuthHandler(req, res, userOptions) {
...userOptions,
// These computed settings can have values in userOptions but we override them
// and are request-specific.
adapter,
baseUrl,
basePath,
action,
@@ -110,7 +156,7 @@ async function NextAuthHandler(req, res, userOptions) {
providers,
// Session options
session: {
jwt: !userOptions.adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
jwt: !adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
maxAge,
updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours)
...userOptions.session,
@@ -200,7 +246,6 @@ async function NextAuthHandler(req, res, userOptions) {
"OAuthAccountNotLinked",
"EmailSignin",
"CredentialsSignin",
"SessionRequired",
].includes(error)
) {
return res.redirect(`${baseUrl}${basePath}/signin?error=${error}`)
@@ -244,8 +289,13 @@ async function NextAuthHandler(req, res, userOptions) {
case "_log":
if (userOptions.logger) {
try {
const { code, level, ...metadata } = req.body
logger[level](code, metadata)
const {
code = "CLIENT_ERROR",
level = "error",
message = "[]",
} = req.body
logger[level](code, ...JSON.parse(message))
} catch (error) {
// If logging itself failed...
logger.error("LOGGER_ERROR", error)
@@ -257,7 +307,9 @@ async function NextAuthHandler(req, res, userOptions) {
}
return res
.status(400)
.end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
.end(
`Error: This action with HTTP ${req.method} is not supported by NextAuth.js`
)
})
}

View File

@@ -16,7 +16,7 @@ import adapterErrorHandler from "../../adapters/error-handler"
* @param {import("types").Session} sessionToken
* @param {import("types").Profile} profile
* @param {import("types").Account} account
* @param {import("types/internals").InternalOptions} options
* @param {import("types/internals").AppOptions} options
*/
export default async function callbackHandler(
sessionToken,

View File

@@ -1,42 +1,32 @@
// @ts-check
import * as cookie from "../lib/cookie"
import * as cookie from '../lib/cookie'
/**
* Get callback URL based on query param / cookie + validation,
* and add it to `req.options.callbackUrl`.
* @type {import("types/internals").NextAuthApiHandler}
* @note: `req.options` must already be defined when called.
*/
export default async function callbackUrlHandler(req, res) {
export default async function callbackUrlHandler (req, res) {
const { query } = req
const { body } = req
const { cookies, baseUrl, callbacks } = req.options
const { cookies, baseUrl, defaultCallbackUrl, callbacks } = req.options
let callbackUrl = baseUrl
// Handle preserving and validating callback URLs
// If no defaultCallbackUrl option specified, default to the homepage for the site
let callbackUrl = defaultCallbackUrl || baseUrl
// Try reading callbackUrlParamValue from request body (form submission) then from query param (get request)
const callbackUrlParamValue = body.callbackUrl || query.callbackUrl || null
const callbackUrlCookieValue = req.cookies[cookies.callbackUrl.name] || null
if (callbackUrlParamValue) {
// If callbackUrl form field or query parameter is passed try to use it if allowed
callbackUrl = await callbacks.redirect({
url: callbackUrlParamValue,
baseUrl,
})
callbackUrl = await callbacks.redirect(callbackUrlParamValue, baseUrl)
} else if (callbackUrlCookieValue) {
// If no callbackUrl specified, try using the value from the cookie if allowed
callbackUrl = await callbacks.redirect({
url: callbackUrlCookieValue,
baseUrl,
})
callbackUrl = await callbacks.redirect(callbackUrlCookieValue, baseUrl)
}
// Save callback URL in a cookie so that it can be used for subsequent requests in signin/signout/callback flow
if (callbackUrl && callbackUrl !== callbackUrlCookieValue) {
cookie.set(
res,
cookies.callbackUrl.name,
callbackUrl,
cookies.callbackUrl.options
)
// Save callback URL in a cookie so that can be used for subsequent requests in signin/signout/callback flow
if (callbackUrl && (callbackUrl !== callbackUrlCookieValue)) {
cookie.set(res, cookies.callbackUrl.name, callbackUrl, cookies.callbackUrl.options)
}
req.options.callbackUrl = callbackUrl

View File

@@ -8,115 +8,115 @@
* As only partial functionlity is required, only the code we need has been incorporated here
* (with fixes for specific issues) to keep dependancy size down.
*/
export function set (res, name, value, options = {}) {
export function set(res, name, value, options = {}) {
const stringValue =
typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value)
typeof value === "object" ? "j:" + JSON.stringify(value) : String(value)
if ('maxAge' in options) {
if ("maxAge" in options) {
options.expires = new Date(Date.now() + options.maxAge)
options.maxAge /= 1000
}
// Preserve any existing cookies that have already been set in the same session
let setCookieHeader = res.getHeader('Set-Cookie') || []
let setCookieHeader = res.getHeader("Set-Cookie") || []
// If not an array (i.e. a string with a single cookie) convert it into an array
if (!Array.isArray(setCookieHeader)) {
setCookieHeader = [setCookieHeader]
}
setCookieHeader.push(_serialize(name, String(stringValue), options))
res.setHeader('Set-Cookie', setCookieHeader)
res.setHeader("Set-Cookie", setCookieHeader)
}
function _serialize (name, val, options) {
function _serialize(name, val, options) {
const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/ // eslint-disable-line no-control-regex
const opt = options || {}
const enc = opt.encode || encodeURIComponent
if (typeof enc !== 'function') {
throw new TypeError('option encode is invalid')
if (typeof enc !== "function") {
throw new TypeError("option encode is invalid")
}
if (!fieldContentRegExp.test(name)) {
throw new TypeError('argument name is invalid')
throw new TypeError("argument name is invalid")
}
const value = enc(val)
if (value && !fieldContentRegExp.test(value)) {
throw new TypeError('argument val is invalid')
throw new TypeError("argument val is invalid")
}
let str = name + '=' + value
let str = name + "=" + value
if (opt.maxAge != null) {
const maxAge = opt.maxAge - 0
if (isNaN(maxAge) || !isFinite(maxAge)) {
throw new TypeError('option maxAge is invalid')
throw new TypeError("option maxAge is invalid")
}
str += '; Max-Age=' + Math.floor(maxAge)
str += "; Max-Age=" + Math.floor(maxAge)
}
if (opt.domain) {
if (!fieldContentRegExp.test(opt.domain)) {
throw new TypeError('option domain is invalid')
throw new TypeError("option domain is invalid")
}
str += '; Domain=' + opt.domain
str += "; Domain=" + opt.domain
}
if (opt.path) {
if (!fieldContentRegExp.test(opt.path)) {
throw new TypeError('option path is invalid')
throw new TypeError("option path is invalid")
}
str += '; Path=' + opt.path
str += "; Path=" + opt.path
} else {
str += '; Path=/'
str += "; Path=/"
}
if (opt.expires) {
let expires = opt.expires
if (typeof opt.expires.toUTCString === 'function') {
if (typeof opt.expires.toUTCString === "function") {
expires = opt.expires.toUTCString()
} else {
const dateExpires = new Date(opt.expires)
expires = dateExpires.toUTCString()
}
str += '; Expires=' + expires
str += "; Expires=" + expires
}
if (opt.httpOnly) {
str += '; HttpOnly'
str += "; HttpOnly"
}
if (opt.secure) {
str += '; Secure'
str += "; Secure"
}
if (opt.sameSite) {
const sameSite =
typeof opt.sameSite === 'string'
typeof opt.sameSite === "string"
? opt.sameSite.toLowerCase()
: opt.sameSite
switch (sameSite) {
case true:
str += '; SameSite=Strict'
str += "; SameSite=Strict"
break
case 'lax':
str += '; SameSite=Lax'
case "lax":
str += "; SameSite=Lax"
break
case 'strict':
str += '; SameSite=Strict'
case "strict":
str += "; SameSite=Strict"
break
case 'none':
str += '; SameSite=None'
case "none":
str += "; SameSite=None"
break
default:
throw new TypeError('option sameSite is invalid')
throw new TypeError("option sameSite is invalid")
}
}
@@ -134,46 +134,47 @@ function _serialize (name, val, options) {
* @TODO Review cookie settings (names, options)
* @return {import("types").CookiesOptions}
*/
export function defaultCookies (useSecureCookies) {
const cookiePrefix = useSecureCookies ? '__Secure-' : ''
export function defaultCookies(useSecureCookies) {
const cookiePrefix = useSecureCookies ? "__Secure-" : ""
return {
// default cookie options
sessionToken: {
name: `${cookiePrefix}next-auth.session-token`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
callbackUrl: {
name: `${cookiePrefix}next-auth.callback-url`,
options: {
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
csrfToken: {
// Default to __Host- for CSRF token for additional protection if using useSecureCookies
// NB: The `__Host-` prefix is stricter than the `__Secure-` prefix.
name: `${useSecureCookies ? '__Host-' : ''}next-auth.csrf-token`,
name: `${useSecureCookies ? "__Host-" : ""}next-auth.csrf-token`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
pkceCodeVerifier: {
name: `${cookiePrefix}next-auth.pkce.code_verifier`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
}
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
}
}

View File

@@ -3,7 +3,7 @@ import * as cookie from './cookie'
/**
* Ensure CSRF Token cookie is set for any subsequent requests.
* Used as part of the strateigy for mitigation for CSRF tokens.
* Used as part of the strategy for mitigation for CSRF tokens.
*
* Creates a cookie like 'next-auth.csrf-token' with the value 'token|hash',
* where 'token' is the CSRF token and 'hash' is a hash made of the token and

View File

@@ -1,24 +1,63 @@
// @ts-check
/** @type {import("types").CallbacksOptions["signIn"]} */
export function signIn() {
/**
* Use the signIn callback to control if a user is allowed to sign in or not.
*
* This is triggered before sign in flow completes, so the user profile may be
* a user object (with an ID) or it may be just their name and email address,
* depending on the sign in flow and if they have an account already.
*
* When using email sign in, this method is triggered both when the user
* requests to sign in and again when they activate the link in the sign in
* email.
*
* @param {object} profile User profile (e.g. user id, name, email)
* @param {object} account Account used to sign in (e.g. OAuth account)
* @param {object} metadata Provider specific metadata (e.g. OAuth Profile)
* @return {Promise<boolean|never>} Return `true` (or a modified JWT) to allow sign in
* Return `false` to deny access
*/
export async function signIn() {
return true
}
/** @type {import("types").CallbacksOptions["redirect"]} */
export function redirect({ url, baseUrl }) {
if (url.startsWith(baseUrl)) {
return url
}
/**
* Redirect is called anytime the user is redirected on signin or signout.
* By default, for security, only Callback URLs on the same URL as the site
* are allowed, you can use this callback to customise that behaviour.
*
* @param {string} url URL provided as callback URL by the client
* @param {string} baseUrl Default base URL of site (can be used as fallback)
* @return {Promise<string>} URL the client will be redirect to
*/
export async function redirect(url, baseUrl) {
if (url.startsWith("/")) return `${baseUrl}${url}`
else if (new URL(url).origin === baseUrl) return url
return baseUrl
}
/** @type {import("types").CallbacksOptions["session"]} */
export function session({ session }) {
/**
* The session callback is called whenever a session is checked.
* e.g. `getSession()`, `useSession()`, `/api/auth/session` (etc)
*
* @param {object} session Session object
* @param {object} token JSON Web Token (if enabled)
* @return {Promise<object>} Session that will be returned to the client
*/
export async function session(session) {
return session
}
/** @type {import("types").CallbacksOptions["jwt"]} */
export function jwt({ token }) {
/**
* This callback is called whenever a JSON Web Token is created / updated.
* e.g. On sign in, `getSession()`, `useSession()`, `/api/auth/session` (etc)
*
* On initial sign in, the raw OAuthProfile is passed if the user is signing in
* with an OAuth provider. It is not avalible on subsequent calls. You can
* take advantage of this to persist additional data you need to in the JWT.
*
* @param {object} token Decrypted JSON Web Token
* @param {object} oAuthProfile OAuth profile - only available on sign in
* @return {Promise<object>} JSON Web Token that will be saved
*/
export async function jwt(token) {
return token
}

View File

@@ -23,12 +23,14 @@ export default async function oAuthCallback(req) {
code = body.code
user = body.user != null ? JSON.parse(body.user) : null
} catch (error) {
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", {
logger.error(
"OAUTH_CALLBACK_HANDLER_ERROR",
error,
body: req.body,
providerId: provider.id,
code,
})
req.body,
provider.id,
code
)
logger.debug("OAUTH_CALLBACK_HANDLER_ERROR", req.body)
throw error
}
}
@@ -61,11 +63,7 @@ export default async function oAuthCallback(req) {
return getProfile({ profileData, provider, tokens, user })
} catch (error) {
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", {
error,
providerId: provider.id,
code,
})
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error, provider.id)
throw error
}
}
@@ -120,7 +118,7 @@ export default async function oAuthCallback(req) {
async function getProfile({ profileData, tokens, provider, user }) {
try {
// Convert profileData into an object if it's a string
if (typeof profileData === "string") {
if (typeof profileData === "string" || profileData instanceof String) {
profileData = JSON.parse(profileData)
}
@@ -129,7 +127,7 @@ async function getProfile({ profileData, tokens, provider, user }) {
profileData.user = user
}
logger.debug("PROFILE_DATA", { profile: profileData })
logger.debug("PROFILE_DATA", profileData)
const profile = await provider.profile(profileData, tokens)
// Return profile, raw profile and auth provider details
@@ -146,15 +144,15 @@ async function getProfile({ profileData, tokens, provider, user }) {
},
OAuthProfile: profileData,
}
} catch (error) {
} catch (exception) {
// 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.
logger.error("OAUTH_PARSE_PROFILE_ERROR", { error, profileData })
logger.error("OAUTH_PARSE_PROFILE_ERROR", exception)
return {
profile: null,
account: null,

View File

@@ -173,62 +173,51 @@ async function getOAuth2AccessToken(code, provider, codeVerifier) {
headers.Authorization = `Bearer ${code}`
}
if (provider.checks.includes("pkce")) {
if (provider.protection.includes("pkce")) {
params.code_verifier = codeVerifier
}
const postData = querystring.stringify(params)
return new Promise((resolve, reject) => {
this._request(
"POST",
url,
headers,
postData,
null,
(error, data, response) => {
if (error) {
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", {
error,
data,
response,
})
this._request("POST", url, headers, postData, null, (error, data) => {
if (error) {
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error)
return reject(error)
}
let raw
try {
// As of http://tools.ietf.org/html/draft-ietf-oauth-v2-07
// responses should be in JSON
raw = JSON.parse(data)
} catch {
// However both Facebook + Github currently use rev05 of the spec and neither
// seem to specify a content-type correctly in their response headers. :(
// Clients of these services suffer a minor performance cost.
raw = querystring.parse(data)
}
let accessToken
if (provider.id === "slack") {
const { ok, error } = raw
if (!ok) {
return reject(error)
}
let raw
try {
// As of http://tools.ietf.org/html/draft-ietf-oauth-v2-07
// responses should be in JSON
raw = JSON.parse(data)
} catch {
// However both Facebook + Github currently use rev05 of the spec and neither
// seem to specify a content-type correctly in their response headers. :(
// Clients of these services suffer a minor performance cost.
raw = querystring.parse(data)
}
let accessToken
if (provider.id === "slack") {
const { ok, error } = raw
if (!ok) {
return reject(error)
}
accessToken = raw.authed_user.access_token
} else {
accessToken = raw.access_token
}
resolve({
accessToken,
accessTokenExpires: null,
refreshToken: raw.refresh_token,
idToken: raw.id_token,
...raw,
})
accessToken = raw.authed_user.access_token
} else {
accessToken = raw.access_token
}
)
resolve({
accessToken,
accessTokenExpires: null,
refreshToken: raw.refresh_token,
idToken: raw.id_token,
...raw,
})
})
})
}

View File

@@ -1,11 +1,11 @@
import pkceChallenge from "pkce-challenge"
import * as cookie from "../cookie"
import jwt from "../../../lib/jwt"
import logger from "../../../lib/logger"
import { OAuthCallbackError } from "../../../lib/errors"
import pkceChallenge from 'pkce-challenge'
import * as cookie from '../cookie'
import jwt from '../../../lib/jwt'
import logger from '../../../lib/logger'
import { OAuthCallbackError } from '../../../lib/errors'
const PKCE_LENGTH = 64
const PKCE_CODE_CHALLENGE_METHOD = "S256" // can be 'plain', not recommended https://tools.ietf.org/html/rfc7636#section-4.2
const PKCE_CODE_CHALLENGE_METHOD = 'S256' // can be 'plain', not recommended https://tools.ietf.org/html/rfc7636#section-4.2
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
/**
@@ -13,36 +13,36 @@ const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export async function handleCallback(req, res) {
export async function handleCallback (req, res) {
const { cookies, provider, baseUrl, basePath } = req.options
try {
// Provider does not support PKCE, nothing to do.
if (!provider.checks?.includes("pkce")) {
if (!provider.protection?.includes('pkce')) {
return
}
if (!(cookies.pkceCodeVerifier.name in req.cookies)) {
throw new OAuthCallbackError("The code_verifier cookie was not found.")
throw new OAuthCallbackError('The code_verifier cookie was not found.')
}
const pkce = await jwt.decode({
...req.options.jwt,
token: req.cookies[cookies.pkceCodeVerifier.name],
maxAge: PKCE_MAX_AGE,
encryption: true,
encryption: true
})
req.options.pkce = pkce
logger.debug("PKCE_VERIFIER_FROM_COOKIE", {
logger.debug('OAUTH_CALLBACK_PROTECTION', 'Read PKCE verifier from cookie', {
code_verifier: pkce.code_verifier,
pkceLength: PKCE_LENGTH,
method: PKCE_CODE_CHALLENGE_METHOD,
method: PKCE_CODE_CHALLENGE_METHOD
})
// remove PKCE after it has been used
cookie.set(res, cookies.pkceCodeVerifier.name, "", {
cookie.set(res, cookies.pkceCodeVerifier.name, "", {
...cookies.pkceCodeVerifier.options,
maxAge: 0,
maxAge: 0
})
} catch (error) {
logger.error("CALLBACK_OAUTH_ERROR", error)
logger.error('CALLBACK_OAUTH_ERROR', error)
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
}
}
@@ -52,43 +52,42 @@ export async function handleCallback(req, res) {
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export async function handleSignin(req, res) {
export async function handleSignin (req, res) {
const { cookies, provider, baseUrl, basePath } = req.options
try {
if (!provider.checks?.includes("pkce")) {
// Provider does not support PKCE, nothing to do.
if (!provider.protection?.includes('pkce')) { // Provider does not support PKCE, nothing to do.
return
}
// Started login flow, add generated pkce to req.options and (encrypted) code_verifier to a cookie
const pkce = pkceChallenge(PKCE_LENGTH)
logger.debug("CREATE_PKCE_CHALLENGE_VERIFIER", {
logger.debug('OAUTH_SIGNIN_PROTECTION', 'Created PKCE challenge/verifier', {
...pkce,
pkceLength: PKCE_LENGTH,
method: PKCE_CODE_CHALLENGE_METHOD,
method: PKCE_CODE_CHALLENGE_METHOD
})
provider.authorizationParams = {
...provider.authorizationParams,
code_challenge: pkce.code_challenge,
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD
}
const encryptedCodeVerifier = await jwt.encode({
...req.options.jwt,
maxAge: PKCE_MAX_AGE,
token: { code_verifier: pkce.code_verifier },
encryption: true,
encryption: true
})
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + PKCE_MAX_AGE * 1000)
cookieExpires.setTime(cookieExpires.getTime() + (PKCE_MAX_AGE * 1000))
cookie.set(res, cookies.pkceCodeVerifier.name, encryptedCodeVerifier, {
expires: cookieExpires.toISOString(),
...cookies.pkceCodeVerifier.options,
...cookies.pkceCodeVerifier.options
})
logger.debug("PKCE_CODE_VERIFIER_SAVED", { encryptedCodeVerifier })
logger.debug('OAUTH_SIGNIN_PROTECTION', 'Created PKCE code_verifier saved in cookie')
} catch (error) {
logger.error("SIGNIN_OAUTH_ERROR", error)
logger.error('SIGNIN_OAUTH_ERROR', error)
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
}
}

View File

@@ -1,6 +1,6 @@
import { createHash } from "crypto"
import logger from "../../../lib/logger"
import { OAuthCallbackError } from "../../../lib/errors"
import { createHash } from 'crypto'
import logger from '../../../lib/logger'
import { OAuthCallbackError } from '../../../lib/errors'
/**
* For OAuth 2.0 flows, if the provider supports state,
@@ -9,23 +9,27 @@ import { OAuthCallbackError } from "../../../lib/errors"
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export async function handleCallback(req, res) {
export async function handleCallback (req, res) {
const { csrfToken, provider, baseUrl, basePath } = req.options
try {
// Provider does not support state, nothing to do.
if (!provider.checks?.includes("state")) {
if (!provider.protection?.includes('state')) {
return
}
const state = req.query.state || req.body.state
const expectedState = createHash("sha256").update(csrfToken).digest("hex")
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
logger.debug("STATE_CHECK", { state, expectedState })
logger.debug(
'OAUTH_CALLBACK_PROTECTION',
'Comparing received and expected state',
{ state, expectedState }
)
if (state !== expectedState) {
throw new OAuthCallbackError("Invalid state returned from OAuth provider")
throw new OAuthCallbackError('Invalid state returned from OAuth provider')
}
} catch (error) {
logger.error("STATE_ERROR", error)
logger.error('STATE_ERROR', error)
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
}
}
@@ -35,21 +39,31 @@ export async function handleCallback(req, res) {
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export async function handleSignin(req, res) {
export async function handleSignin (req, res) {
const { provider, baseUrl, basePath, csrfToken } = req.options
try {
if (!provider.checks?.includes("state")) {
// Provider does not support state, nothing to do.
if (!provider.protection?.includes('state')) { // Provider does not support state, nothing to do.
return
}
if ('state' in provider) {
logger.warn(
'STATE_OPTION_DEPRECATION',
'The `state` provider option is being replaced with `protection`. See the docs.'
)
}
// A hash of the NextAuth.js CSRF token is used as the state
const state = createHash("sha256").update(csrfToken).digest("hex")
const state = createHash('sha256').update(csrfToken).digest('hex')
provider.authorizationParams = { ...provider.authorizationParams, state }
logger.debug("STATE_ADDED_TO_PARAMS", { state })
logger.debug(
'OAUTH_CALLBACK_PROTECTION',
'Added state to authorization params',
{ state }
)
} catch (error) {
logger.error("SIGNIN_OAUTH_ERROR", error)
logger.error('SIGNIN_OAUTH_ERROR', error)
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
}
}

View File

@@ -5,7 +5,7 @@ import adapterErrorHandler from "../../../adapters/error-handler"
*
* @param {string} email
* @param {import("types/providers").EmailConfig} provider
* @param {import("types/internals").InternalOptions} options
* @param {import("types/internals").AppOptions} options
* @returns
*/
export default async function email(email, provider, options) {

View File

@@ -1,23 +1,23 @@
import oAuthClient from "../oauth/client"
import logger from "../../../lib/logger"
import oAuthClient from '../oauth/client'
import logger from '../../../lib/logger'
/** @param {import("types/internals").NextAuthRequest} req */
export default async function getAuthorizationUrl(req) {
export default async function getAuthorizationUrl (req) {
const { provider } = req.options
delete req.query?.nextauth
const params = {
...provider.authorizationParams,
...req.query,
...req.query
}
const client = oAuthClient(provider)
if (provider.version?.startsWith("2.")) {
if (provider.version?.startsWith('2.')) {
// Handle OAuth v2.x
let url = client.getAuthorizeUrl({
scope: provider.scope,
...params,
redirect_uri: provider.callbackUrl,
redirect_uri: provider.callbackUrl
})
// If the authorizationUrl specified in the config has query parameters on it
@@ -27,13 +27,13 @@ export default async function getAuthorizationUrl(req) {
// which inadvertantly strips them.
//
// https://github.com/ciaranj/node-oauth/pull/193
if (provider.authorizationUrl.includes("?")) {
if (provider.authorizationUrl.includes('?')) {
const parseUrl = new URL(provider.authorizationUrl)
const baseUrl = `${parseUrl.origin}${parseUrl.pathname}?`
url = url.replace(baseUrl, provider.authorizationUrl + "&")
url = url.replace(baseUrl, provider.authorizationUrl + '&')
}
logger.debug("GET_AUTHORIZATION_URL", { url, params })
logger.debug('GET_AUTHORIZATION_URL', url)
return url
}
@@ -42,12 +42,12 @@ export default async function getAuthorizationUrl(req) {
const url = `${provider.authorizationUrl}?${new URLSearchParams({
oauth_token: tokens.oauth_token,
oauth_token_secret: tokens.oauth_token_secret,
...tokens.params,
...tokens.params
})}`
logger.debug("GET_AUTHORIZATION_URL", { url, tokens })
logger.debug('GET_AUTHORIZATION_URL', url)
return url
} catch (error) {
logger.error("GET_AUTHORIZATION_URL_ERROR", error)
logger.error('GET_AUTHORIZATION_URL_ERROR', error)
throw error
}
}

View File

@@ -6,7 +6,8 @@ import adapterErrorHandler from "../../adapters/error-handler"
/**
* Handle callbacks from login services
* @type {import("types/internals").NextAuthApiHandler}
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export default async function callback(req, res) {
const {
@@ -71,12 +72,12 @@ export default async function callback(req, res) {
}
try {
const signInCallbackResponse = await callbacks.signIn({
user: userOrProfile,
const signInCallbackResponse = await callbacks.signIn(
userOrProfile,
account,
profile: OAuthProfile,
})
if (!signInCallbackResponse) {
OAuthProfile
)
if (signInCallbackResponse === false) {
return res.redirect(
`${baseUrl}${basePath}/error?error=AccessDenied`
)
@@ -84,11 +85,16 @@ export default async function callback(req, res) {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
// TODO: Remove in a future major release
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
return res.redirect(error)
}
// Sign user in
@@ -100,22 +106,22 @@ export default async function callback(req, res) {
)
if (useJwtSession) {
const defaultToken = {
const defaultJwtPayload = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const token = await callbacks.jwt({
token: defaultToken,
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
user,
account,
profile: OAuthProfile,
isNewUser,
})
OAuthProfile,
isNewUser
)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token })
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie expiry date
const cookieExpires = new Date()
@@ -215,22 +221,27 @@ export default async function callback(req, res) {
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn({
user: profile,
const signInCallbackResponse = await callbacks.signIn(
profile,
account,
email: { email },
})
if (!signInCallbackResponse) {
{ email }
)
if (signInCallbackResponse === false) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === "string") {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
// TODO: Remove in a future major release
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
return res.redirect(error)
}
// Sign user in
@@ -242,22 +253,22 @@ export default async function callback(req, res) {
)
if (useJwtSession) {
const defaultToken = {
const defaultJwtPayload = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const token = await callbacks.jwt({
token: defaultToken,
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
user,
account,
profile,
isNewUser,
})
isNewUser
)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token })
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie expiry date
const cookieExpires = new Date()
@@ -303,9 +314,7 @@ export default async function callback(req, res) {
if (!useJwtSession) {
logger.error(
"CALLBACK_CREDENTIALS_JWT_ERROR",
new Error(
"Signin in with credentials is only supported if JSON Web Tokens are enabled"
)
"Signin in with credentials is only supported if JSON Web Tokens are enabled"
)
return res
.status(500)
@@ -315,9 +324,7 @@ export default async function callback(req, res) {
if (!provider.authorize) {
logger.error(
"CALLBACK_CREDENTIALS_HANDLER_ERROR",
new Error(
"Must define an authorize() handler to use credentials authentication provider"
)
"Must define an authorize() handler to use credentials authentication provider"
)
return res
.status(500)
@@ -329,8 +336,7 @@ export default async function callback(req, res) {
let userObjectReturnedFromAuthorizeHandler
try {
userObjectReturnedFromAuthorizeHandler = await provider.authorize(
credentials,
{ ...req, options: {}, cookies: {} }
credentials, {...req, options: {}, cookies: {}}
)
if (!userObjectReturnedFromAuthorizeHandler) {
return res
@@ -340,53 +346,59 @@ export default async function callback(req, res) {
provider.id
)}`
)
} else if (typeof userObjectReturnedFromAuthorizeHandler === "string") {
return res.redirect(userObjectReturnedFromAuthorizeHandler)
}
} catch (error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`
)
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
return res.redirect(error)
}
const user = userObjectReturnedFromAuthorizeHandler
const account = { id: provider.id, type: "credentials" }
try {
const signInCallbackResponse = await callbacks.signIn({
const signInCallbackResponse = await callbacks.signIn(
user,
account,
credentials,
})
if (!signInCallbackResponse) {
credentials
)
if (signInCallbackResponse === false) {
return res
.status(403)
.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === "string") {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`
)
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
return res.redirect(error)
}
const defaultToken = {
const defaultJwtPayload = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const token = await callbacks.jwt({
token: defaultToken,
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
user,
account,
profile: userObjectReturnedFromAuthorizeHandler,
isNewUser: false,
})
userObjectReturnedFromAuthorizeHandler,
false
)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token })
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie expiry date
const cookieExpires = new Date()

View File

@@ -22,7 +22,7 @@ export default async function session(req, res) {
if (useJwtSession) {
try {
// Decrypt and verify token
const decodedToken = await jwt.decode({ ...jwt, token: sessionToken })
const decodedJwt = await jwt.decode({ ...jwt, token: sessionToken })
// Generate new session expiry date
const sessionExpiresDate = new Date()
@@ -33,35 +33,38 @@ export default async function session(req, res) {
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as…").
const defaultSession = {
const defaultSessionPayload = {
user: {
name: decodedToken.name || null,
email: decodedToken.email || null,
image: decodedToken.picture || null,
name: decodedJwt.name || null,
email: decodedJwt.email || null,
image: decodedJwt.picture || null,
},
expires: sessionExpires,
}
// Pass Session and JSON Web Token through to the session callback
const token = await callbacks.jwt({ token: decodedToken })
const session = await callbacks.session({
session: defaultSession,
token,
})
const jwtPayload = await callbacks.jwt(decodedJwt)
const sessionPayload = await callbacks.session(
defaultSessionPayload,
jwtPayload
)
// Return session payload as response
response = session
response = sessionPayload
// Refresh JWT expiry by re-signing it, with an updated expiry date
const newToken = await jwt.encode({ ...jwt, token })
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie, to also update expiry date on cookie
cookie.set(res, cookies.sessionToken.name, newToken, {
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
expires: sessionExpires,
...cookies.sessionToken.options,
})
await dispatchEvent(events.session, { session, token })
await dispatchEvent(events.session, {
session: sessionPayload,
jwt: jwtPayload,
})
} catch (error) {
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
logger.error("JWT_SESSION_ERROR", error)
@@ -85,7 +88,7 @@ export default async function session(req, res) {
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as…").
const defaultSession = {
const defaultSessionPayload = {
user: {
name: user.name,
email: user.email,
@@ -96,10 +99,10 @@ export default async function session(req, res) {
}
// Pass Session through to the session callback
const sessionPayload = await callbacks.session({
session: defaultSession,
user,
})
const sessionPayload = await callbacks.session(
defaultSessionPayload,
user
)
// Return session payload as response
response = sessionPayload

View File

@@ -40,16 +40,19 @@ export default async function signin(req, res) {
// complains about this we can make strict RFC 2821 compliance an option.
const email = req.body.email?.toLowerCase() ?? null
if (!email) {
return res.redirect(`${baseUrl}${basePath}/error?error=EmailSignin`)
}
// If is an existing user return a user object (otherwise use placeholder)
const user = (await getUserByEmail(email)) || { email }
const profile = (await getUserByEmail(email)) || { email }
const account = { id: provider.id, type: "email", providerAccountId: email }
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn({
user,
account,
email: { email, verificationRequest: true },
const signInCallbackResponse = await callbacks.signIn(profile, account, {
email,
verificationRequest: true,
})
if (signInCallbackResponse === false) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
@@ -57,9 +60,14 @@ export default async function signin(req, res) {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`
)
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`
)
}
// TODO: Remove in a future major release
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
return res.redirect(error)
}
try {

View File

@@ -6,9 +6,9 @@
"types": ["./types"],
"next-auth": ["./src/server"],
"next-auth/adapters": ["./src/adapters"],
"next-auth/react": ["./src/client/react"],
"next-auth/client": ["./src/client"],
"next-auth/jwt": ["./src/lib/jwt"],
"next-auth/providers": ["./src/providers"],
"next-auth/providers": ["./src/providers"]
},
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],

36
types/adapters.d.ts vendored
View File

@@ -1,7 +1,39 @@
import { InternalOptions } from "./internals"
import { AppOptions } from "./internals"
import { User, Profile, Session } from "."
import { EmailConfig } from "./providers"
/** Legacy */
export {
TypeORMAccountModel,
TypeORMSessionModel,
TypeORMUserModel,
TypeORMVerificationRequestModel,
} from "@next-auth/typeorm-legacy-adapter"
import {
TypeORMAdapter,
TypeORMAdapterModels,
} from "@next-auth/typeorm-legacy-adapter"
import { PrismaLegacyAdapter } from "@next-auth/prisma-legacy-adapter"
export const TypeORM: {
Models: TypeORMAdapterModels
Adapter: TypeORMAdapter
}
export const Prisma: {
Adapter: PrismaLegacyAdapter
}
declare const Adapters: {
Default: TypeORMAdapter
TypeORM: typeof TypeORM
Prisma: typeof Prisma
}
export default Adapters
/**
* Using a custom adapter you can connect to any database backend or even several different databases.
* Custom adapters created and maintained by our community can be found in the adapters repository.
@@ -120,5 +152,5 @@ export type Adapter<
client: C,
options?: O
) => {
getAdapter(appOptions: InternalOptions): Promise<AdapterInstance<U, P, S>>
getAdapter(appOptions: AppOptions): Promise<AdapterInstance<U, P, S>>
}

View File

@@ -2,7 +2,6 @@ import * as React from "react"
import { IncomingMessage } from "http"
import { Session } from "."
import { ProviderType } from "./providers"
import { SessionContextValue } from "internals/react"
export interface CtxOrReq {
req?: IncomingMessage
@@ -18,24 +17,29 @@ export type GetSessionOptions = CtxOrReq & {
triggerEvent?: boolean
}
export interface UseSessionOptions<R extends boolean> {
required: R
/** Defaults to `signIn` */
action?(): void
}
/**
* React Hook that gives you access
* to the logged in user's session data.
*
* [Documentation](https://next-auth.js.org/getting-started/client#usesession)
*/
export function useSession<R extends boolean>(
options?: UseSessionOptions<R>
): SessionContextValue<R>
export function useSession(): [Session | null, boolean]
/**
* Can be called client or server side to return a session asynchronously.
* It calls `/api/auth/session` and returns a promise with a session object,
* or null if no session exists.
*
* [Documentation](https://next-auth.js.org/getting-started/client#getsession)
*/
export function getSession(options?: GetSessionOptions): Promise<Session | null>
/**
* Alias for `getSession`
* @docs https://next-auth.js.org/getting-started/client#getsession
*/
export const session: typeof getSession
/*******************
* CSRF Token types
******************/
@@ -50,6 +54,12 @@ export function getSession(options?: GetSessionOptions): Promise<Session | null>
*/
export function getCsrfToken(ctxOrReq?: CtxOrReq): Promise<string | null>
/**
* Alias for `getCsrfToken`
* @docs https://next-auth.js.org/getting-started/client#getcsrftoken
*/
export const csrfToken: typeof getCsrfToken
/******************
* Providers types
*****************/
@@ -74,6 +84,12 @@ export function getProviders(): Promise<Record<
ClientSafeProvider
> | null>
/**
* Alias for `getProviders`
* @docs https://next-auth.js.org/getting-started/client#getproviders
*/
export const providers: typeof getProviders
/****************
* Sign in types
***************/
@@ -121,6 +137,12 @@ export function signIn<P extends SignInProvider = undefined>(
P extends RedirectableProvider ? SignInResponse | undefined : undefined
>
/**
* Alias for `signIn`
* @docs https://next-auth.js.org/getting-started/client#signin
*/
export const signin: typeof signIn
/****************
* Sign out types
****************/
@@ -147,25 +169,21 @@ export function signOut<R extends boolean = true>(
params?: SignOutParams<R>
): Promise<R extends true ? undefined : SignOutResponse>
/**
* @docs https://next-auth.js.org/getting-started/client#signout
* Alias for `signOut`
*/
export const signout: typeof signOut
/************************
* SessionProvider types
***********************/
/** @docs: https://next-auth.js.org/getting-started/client#options */
export interface SessionProviderProps {
session?: Session
export interface SessionProviderOptions {
baseUrl?: string
basePath?: string
/**
* The amount of time (in seconds) after a session should be considered stale.
* If set to `0` (default), the session will never be re-fetched.
*/
staleTime?: number
/**
* A time interval (in seconds) after which the session will be re-fetched.
* If set to `0` (default), the session is not polled.
*/
refetchInterval?: number
clientMaxAge?: number
keepAlive?: number
}
/**
@@ -173,6 +191,28 @@ export interface SessionProviderProps {
* Can also be used to throttle the number of requests to the endpoint
* `/api/auth/session`.
*
* [Documentation](https://next-auth.js.org/getting-started/client#sessionprovider)
* [Documentation](https://next-auth.js.org/getting-started/client#provider)
*/
export const SessionProvider: React.FC<SessionProviderProps>
export type SessionProvider = React.FC<{
children: React.ReactNode
session?: Session
options?: SessionProviderOptions
}>
/**
* Provider to wrap the app in to make session data available globally.
* Can also be used to throttle the number of requests to the endpoint
* `/api/auth/session`.
*
* [Documentation](https://next-auth.js.org/getting-started/client#provider)
*/
export const Provider: SessionProvider
/** @docs: https://next-auth.js.org/getting-started/client#options */
export function setOptions(options: SessionProviderOptions): void
/**
* Alias for `setOptions`
* @docs: https://next-auth.js.org/getting-started/client#options
*/
export const options: typeof setOptions

112
types/index.d.ts vendored
View File

@@ -2,9 +2,10 @@
/// <reference types="node" />
import { ConnectionOptions } from "typeorm"
import { Adapter } from "./adapters"
import { JWTOptions, JWT } from "./jwt"
import { AppProviders, Credentials } from "./providers"
import { AppProviders } from "./providers"
import {
Awaitable,
NextApiRequest,
@@ -28,6 +29,14 @@ export interface NextAuthOptions {
* [Documentation](https://next-auth.js.org/configuration/options#providers) | [Providers documentation](https://next-auth.js.org/configuration/providers)
*/
providers: AppProviders
/**
* A database connection string or configuration object.
* * **Default value**: `null`
* * **Required**: *No (unless using email provider)*
*
* [Documentation](https://next-auth.js.org/configuration/options#database) | [Databases](https://next-auth.js.org/configuration/databases)
*/
database?: string | Record<string, any> | ConnectionOptions
/**
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
* If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
@@ -89,7 +98,7 @@ export interface NextAuthOptions {
*
* [Documentation](https://next-auth.js.org/configuration/options#callbacks) | [Callbacks documentation](https://next-auth.js.org/configuration/callbacks)
*/
callbacks?: Partial<CallbacksOptions>
callbacks?: CallbacksOptions
/**
* Events are asynchronous functions that do not return a response, they are useful for audit logging.
* You can specify a handler for any of these events below - e.g. for debugging or to create an audit log.
@@ -104,11 +113,18 @@ export interface NextAuthOptions {
*/
events?: Partial<JWTEventCallbacks | SessionEventCallbacks>
/**
* You can use the adapter option to pass in your database adapter.
*
* By default NextAuth.js uses a database adapter that uses TypeORM and supports MySQL, MariaDB, Postgres and MongoDB and SQLite databases.
* An alternative adapter that uses Prisma, which currently supports MySQL, MariaDB and Postgres, is also included.
* You can use the adapter option to use the Prisma adapter - or pass in your own adapter
* if you want to use a database that is not supported by one of the built-in adapters.
* * **Default value**: TypeORM adapter
* * **Required**: *No*
*
* - ⚠ If the `adapter` option is specified it overrides the `database` option, only specify one or the other.
* - ⚠ Adapters are being migrated to their own home in a Community maintained repository.
*
* [Documentation](https://next-auth.js.org/configuration/options#adapter) |
* [Default adapter](https://next-auth.js.org/schemas/adapters#typeorm-adapter) |
* [Community adapters](https://github.com/nextauthjs/adapters)
*/
adapter?: ReturnType<Adapter>
@@ -196,7 +212,7 @@ export interface NextAuthOptions {
*
* [Documentation](https://next-auth.js.org/configuration/options#cookies) | [Usage example](https://next-auth.js.org/configuration/options#example)
*/
cookies?: Partial<CookiesOptions>
cookies?: CookiesOptions
}
/**
@@ -213,22 +229,9 @@ export type Theme = "auto" | "dark" | "light"
* [Documentation](https://next-auth.js.org/configuration/options#logger)
*/
export interface LoggerInstance {
warn(
code:
| "JWT_AUTO_GENERATED_SIGNING_KEY"
| "JWT_AUTO_GENERATED_ENCRYPTION_KEY"
| "NEXTAUTH_URL"
): void
error(
code: string,
/**
* Either an instance of (JSON serializable) Error
* or an object that contains some debug information.
* (Error is still available through `metadata.error`)
*/
metadata: Error | { error: Error; [key: string]: unknown }
): void
debug(code: string, metadata: unknown): void
warn(code: string, ...message: unknown[]): void
error(code: string, ...message: unknown[]): void
debug(code: string, ...message: unknown[]): void
}
/**
@@ -280,29 +283,7 @@ export interface CallbacksOptions<
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#sign-in-callback)
*/
signIn(params: {
user: User
account: A
/**
* If OAuth provider is used, it contains the full
* OAuth profile returned by your provider.
*/
profile: P & Record<string, unknown>
/**
* If Email provider is used, it contains the email, and optionally on the first call a
* `verificationRequest: true` property to indicate it is being triggered in the verification request flow.
* When the callback is invoked after a user has clicked on a sign in link,
* this property will not be present. You can check for the `verificationRequest` property
* to avoid sending emails to addresses or domains on a blocklist or to only explicitly generate them
* for email address in an allow list.
*/
email: {
email: string | null
verificationRequest?: boolean
}
/** If Credentials provider is used, it contains the user credentials */
credentials: Credentials
}): Awaitable<string | boolean>
signIn?(user: User, account: A, profile: P): Awaitable<string | boolean>
/**
* This callback is called anytime the user is redirected to a callback URL (e.g. on signin or signout).
* By default only URLs on the same URL as the site are allowed,
@@ -310,19 +291,12 @@ export interface CallbacksOptions<
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#redirect-callback)
*/
redirect(params: {
/** URL provided as callback URL by the client */
url: string
/** Default base URL of site (can be used as fallback) */
baseUrl: string
}): Awaitable<string>
redirect?(url: string, baseUrl: string): Awaitable<string>
/**
* This callback is called whenever a session is checked.
* (Eg.: invoking the `/api/session` endpoint, using `useSession` or `getSession`)
*
* ⚠ By default, only a subset (email, name, imgage)
* of the token is returned for increased security.
*
* - ⚠ By default, only a subset of the token is returned for increased security.
* If you want to make something available you added to the token through the `jwt` callback,
* you have to explicitely forward it here to make it available to the client.
*
@@ -332,11 +306,7 @@ export interface CallbacksOptions<
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
*
*/
session(params: {
session: Session
user: User
token: JWT
}): Awaitable<Session>
session?(session: Session, userOrToken: JWT | User): Awaitable<Session>
/**
* This callback is called whenever a JSON Web Token is created (i.e. at sign in)
* or updated (i.e whenever a session is accessed in the client).
@@ -344,18 +314,18 @@ export interface CallbacksOptions<
* where you can control what should be returned to the client.
* Anything else will be kept from your front-end.
*
* ⚠ By default the JWT is signed, but not encrypted.
* - ⚠ By default the JWT is signed, but not encrypted.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
*/
jwt(params: {
token: JWT
user?: User
account?: A
profile?: P
jwt?(
token: JWT,
user?: User,
account?: A,
profile?: P,
isNewUser?: boolean
}): Awaitable<JWT>
): Awaitable<JWT>
}
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
@@ -373,10 +343,10 @@ export interface CookieOption {
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
export interface CookiesOptions {
sessionToken: CookieOption
callbackUrl: CookieOption
csrfToken: CookieOption
pkceCodeVerifier: CookieOption
sessionToken?: CookieOption
callbackUrl?: CookieOption
csrfToken?: CookieOption
pkceCodeVerifier?: CookieOption
}
/** [Documentation](https://next-auth.js.org/configuration/events) */
@@ -458,11 +428,11 @@ export interface DefaultSession extends Record<string, unknown> {
/**
* Returned by `useSession`, `getSession`, returned by the `session` callback
* and also the shape received as a prop on the `SessionProvider` React Context
* and also the shape received as a prop on the `Provider` React Context
*
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
* [`SessionProvider`](https://next-auth.js.org/getting-started/client#sessionprovider) |
* [`Provider`](https://next-auth.js.org/getting-started/client#provider) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback)
*/
export interface Session extends Record<string, unknown>, DefaultSession {}

34
types/internals/client.d.ts vendored Normal file
View File

@@ -0,0 +1,34 @@
import * as React from "react"
import { Session } from ".."
export interface BroadcastMessage {
event?: "session"
data?: {
trigger?: "signout" | "getSession"
}
clientId: string
timestamp: number
}
export interface NextAuthConfig {
baseUrl: string
basePath: string
baseUrlServer: string
basePathServer: string
/** 0 means disabled (don't send); 60 means send every 60 seconds */
keepAlive: number
/** 0 means disabled (only use cache); 60 means sync if last checked > 60 seconds ago */
clientMaxAge: number
/** Used for timestamp since last sycned (in seconds) */
_clientLastSync: number
/** Stores timer for poll interval */
_clientSyncTimer: ReturnType<typeof setTimeout>
/** Tracks if event listeners have been added */
_eventListenersAdded: boolean
/** Stores last session response from hook */
_clientSession: Session | null | undefined
/** Used to store to function export by getSession() hook */
_getSession: any
}
export type SessionContext = React.Context<Session>

View File

@@ -1,18 +1,18 @@
import { Awaitable, NextApiRequest, NextApiResponse } from "./utils"
import {
CallbacksOptions,
CookiesOptions,
EventCallbacks,
LoggerInstance,
PagesOptions,
SessionOptions,
Theme,
} from ".."
import { NextApiRequest, NextApiResponse } from "./utils"
import { LoggerInstance, NextAuthOptions, SessionOptions, Theme } from ".."
import { AppProvider } from "../providers"
import { JWTOptions } from "next-auth/jwt"
import { Adapter } from "next-auth/adapters"
export interface InternalOptions {
/** Options that are the same both in internal and user provided options. */
export type NextAuthSharedOptions =
| "pages"
| "jwt"
| "events"
| "callbacks"
| "cookies"
| "adapter"
export interface AppOptions
extends Required<Pick<NextAuthOptions, NextAuthSharedOptions>> {
providers: AppProvider[]
baseUrl: string
basePath: string
@@ -42,22 +42,10 @@ export interface InternalOptions {
debug: boolean
logger: LoggerInstance
session: Required<SessionOptions>
pages: PagesOptions
jwt: JWTOptions
events: EventCallbacks
adapter: ReturnType<Adapter>
callbacks: CallbacksOptions
cookies: CookiesOptions
callbackUrl: string
}
export interface NextAuthRequest extends NextApiRequest {
options: InternalOptions
options: AppOptions
}
export type NextAuthResponse = NextApiResponse
export type NextAuthApiHandler = (
req: NextAuthRequest,
res: NextAuthResponse
) => Awaitable<void>

View File

@@ -1,37 +0,0 @@
import * as React from "react"
import { Session } from ".."
export interface BroadcastMessage {
event?: "session"
data?: {
trigger?: "signout" | "getSession"
}
clientId: string
timestamp: number
}
export interface NextAuthConfig {
baseUrl: string
basePath: string
baseUrlServer: string
basePathServer: string
/** Stores last session response */
_session?: Session | null
/** Used for timestamp since last sycned (in seconds) */
_lastSync: number
/**
* Stores the `SessionProvider`'s session update method to be able to
* trigger session updates from places like `signIn` or `signOut`
*/
_getSession: any
}
export type SessionContextValue<R extends boolean = false> = R extends true
?
| { data: Session; status: "authenticated" }
| { data: null; status: "loading" }
:
| { data: Session; status: "authenticated" }
| { data: null; status: "unauthenticated" | "loading" }
export type SessionContext = React.Context<SessionContextValue>

23
types/providers.d.ts vendored
View File

@@ -14,7 +14,7 @@ export interface CommonProviderOptions {
* OAuth Provider
*/
type ChecksType = "pkce" | "state" | "both" | "none"
type ProtectionType = "pkce" | "state" | "both" | "none"
/**
* OAuth provider options
@@ -27,20 +27,25 @@ export interface OAuthConfig<P extends Record<string, unknown> = Profile>
headers?: Record<string, any>
type: "oauth"
version: string
scope: string
scope: string | string[]
params: { grant_type: string }
accessTokenUrl: string
requestTokenUrl?: string
authorizationUrl: string
profileUrl: string
profile(profile: P, tokens: TokenSet): Awaitable<User & { id: string }>
checks?: ChecksType | ChecksType[]
protection?: ProtectionType | ProtectionType[]
clientId: string
clientSecret:
| string
// TODO: only allow for Apple
| Record<"appleId" | "teamId" | "privateKey" | "keyId", string>
idToken?: boolean
/**
* @deprecated Will be removed in an upcoming major release. Use `protection: ["state"]` instead.
*/
state?: boolean
// TODO: only allow for BattleNet
region?: string
// TODO: only allow for some
@@ -53,7 +58,6 @@ export type OAuthProviderType =
| "Apple"
| "Atlassian"
| "Auth0"
| "AzureAD"
| "AzureADB2C"
| "Basecamp"
| "BattleNet"
@@ -68,6 +72,7 @@ export type OAuthProviderType =
| "FACEIT"
| "FortyTwo"
| "Foursquare"
| "Freshbooks"
| "FusionAuth"
| "GitHub"
| "GitLab"
@@ -83,6 +88,7 @@ export type OAuthProviderType =
| "Naver"
| "Netlify"
| "Okta"
| "OneLogin"
| "Osso"
| "Reddit"
| "Salesforce"
@@ -111,16 +117,11 @@ interface CredentialInput {
placeholder?: string
}
export type Credentials = Record<string, CredentialInput>
interface CredentialsConfig<C extends Credentials = {}>
interface CredentialsConfig<C extends Record<string, CredentialInput> = {}>
extends CommonProviderOptions {
type: "credentials"
credentials: C
authorize(
credentials: Record<keyof C, string>,
req: NextApiRequest
): Awaitable<User | null>
authorize(credentials: Record<keyof C, string>, req: NextApiRequest): Awaitable<User | null>
}
export type CredentialsProvider = <C extends Record<string, CredentialInput>>(

View File

@@ -0,0 +1,26 @@
import Adapters from "next-auth/adapters"
// ExpectType TypeORMAdapter["Adapter"]
Adapters.Default({
type: "sqlite",
database: ":memory:",
synchronize: true,
})
// ExpectType TypeORMAdapter
Adapters.TypeORM.Adapter({
type: "sqlite",
database: ":memory:",
synchronize: true,
})
// ExpectType PrismaAdapter
Adapters.Prisma.Adapter({
prisma: {},
modelMapping: {
User: "foo",
Account: "bar",
Session: "session",
VerificationRequest: "foo",
},
})

View File

@@ -1,4 +1,4 @@
import * as client from "next-auth/react"
import * as client from "next-auth/client"
import { nextReq } from "./test-helpers"
const clientSession = {
@@ -11,89 +11,87 @@ const clientSession = {
expires: "1234",
}
/**
* $ExpectType
* | { data: Session; status: "authenticated"; }
* | { data: null; status: "unauthenticated" | "loading"; }
* | { //// data: Session; status: "authenticated"; }
* | { data: null; status: "loading"; }
*/
// $ExpectType [Session | null, boolean]
client.useSession()
// $ExpectType { data: Session; status: "authenticated"; } | { data: null; status: "loading"; }
const session = client.useSession({ required: true })
if (session.status === "loading") {
// $ExpectType null
session.data
} else {
// $ExpectType Session
session.data
}
// $ExpectType Promise<Session | null>
client.getSession({ req: nextReq })
// $ExpectType Promise<Session | null>
client.session({ req: nextReq })
// $ExpectType Promise<Record<string, ClientSafeProvider> | null>
client.getProviders()
// $ExpectType Promise<Record<string, ClientSafeProvider> | null>
client.providers()
// $ExpectType Promise<string | null>
client.getCsrfToken({ req: nextReq })
// $ExpectType Promise<string | null>
client.getCsrfToken({ ctx: { req: nextReq } })
client.csrfToken({ req: nextReq })
// $ExpectType Promise<string | null>
client.csrfToken({ ctx: { req: nextReq } })
// $ExpectType Promise<undefined>
client.signIn("github", { callbackUrl: "foo" }, { login: "username" })
client.signin("github", { callbackUrl: "foo" }, { login: "username" })
// $ExpectType Promise<SignInResponse | undefined>
client.signIn("credentials", { callbackUrl: "foo", redirect: true })
client.signin("credentials", { callbackUrl: "foo", redirect: true })
// $ExpectType Promise<SignInResponse | undefined>
client.signIn("credentials", { redirect: false })
client.signin("credentials", { redirect: false })
// $ExpectType Promise<SignInResponse | undefined>
client.signIn("email", { callbackUrl: "foo", redirect: false })
client.signin("email", { callbackUrl: "foo", redirect: false })
// $ExpectType Promise<SignInResponse | undefined>
client.signIn("email", { callbackUrl: "foo", redirect: true })
client.signin("email", { callbackUrl: "foo", redirect: true })
// $ExpectType Promise<undefined>
client.signOut()
client.signout()
// $ExpectType Promise<undefined>
client.signOut({ callbackUrl: "https://foo.com/callback", redirect: true })
client.signout({ callbackUrl: "https://foo.com/callback", redirect: true })
// $ExpectType Promise<SignOutResponse>
client.signOut({ callbackUrl: "https://foo.com/callback", redirect: false })
// $ExpectType ReactElement<any, any> | null
client.SessionProvider({
client.Provider({
children: null,
session: clientSession,
baseUrl: "https://foo.com",
basePath: "/",
staleTime: 1234,
options: {
baseUrl: "https://foo.com",
basePath: "/",
clientMaxAge: 1234,
},
})
// $ExpectType ReactElement<any, any> | null
client.SessionProvider({
client.Provider({
children: null,
session: clientSession,
})
// $ExpectType ReactElement<any, any> | null
client.SessionProvider({
client.Provider({
children: null,
options: {},
})
// $ExpectType ReactElement<any, any> | null
client.SessionProvider({
client.Provider({
children: null,
session: {
expires: "",
},
baseUrl: "https://foo.com",
basePath: "/",
staleTime: 1234,
refetchInterval: 4321,
options: {
baseUrl: "https://foo.com",
basePath: "/",
clientMaxAge: 1234,
keepAlive: 4321,
},
})

View File

@@ -33,7 +33,7 @@ Providers.Credentials({
type: "password",
},
},
authorize: async ({username, password}) => {
authorize: async ({ username, password }) => {
const user = {
/* fetched user */
}
@@ -152,6 +152,13 @@ Providers.Okta({
domain: "https://foo.auth0.com",
})
// $ExpectType OAuthConfig<Profile>
Providers.OneLogin({
clientId: "foo123",
clientSecret: "bar123",
domain: "foo.onelogin.com",
})
// $ExpectType OAuthConfig<Profile>
Providers.BattleNet({
clientId: "foo123",
@@ -257,3 +264,9 @@ Providers.Zoho({
clientId: "foo123",
clientSecret: "bar123",
})
// $ExpectType OAuthConfig<Profile>
Providers.Freshbooks({
clientId: "foo123",
clientSecret: "bar123",
})

View File

@@ -4,7 +4,7 @@ import NextAuth, * as NextAuthTypes from "next-auth"
import { IncomingMessage, ServerResponse } from "http"
import { Socket } from "net"
import { NextApiRequest, NextApiResponse } from "internals/utils"
import { InternalOptions } from "internals"
import { AppOptions } from "internals"
const req: NextApiRequest = Object.assign(new IncomingMessage(new Socket()), {
query: {},
@@ -62,7 +62,7 @@ const exampleVerificationRequest = {
const MyAdapter: Adapter<Record<string, unknown>> = () => {
return {
async getAdapter(appOptions: InternalOptions) {
async getAdapter(appOptions: AppOptions) {
return {
async createUser(profile) {
return exampleUser
@@ -135,6 +135,7 @@ const allConfig: NextAuthTypes.NextAuthOptions = {
clientSecret: "123",
}),
],
database: "path/to/db",
debug: true,
secret: "my secret",
session: {
@@ -153,16 +154,16 @@ const allConfig: NextAuthTypes.NextAuthOptions = {
},
pages: pageOptions,
callbacks: {
async signIn({ user, account, email, credentials, profile }) {
async signIn(user, account, profile) {
return true
},
async redirect({ url, baseUrl }) {
async redirect(url, baseUrl) {
return "path/to/foo"
},
async session({ session, user, token }) {
return session
async session(session, userOrToken) {
return { ...session }
},
async jwt({ token, user, account, profile, isNewUser }) {
async jwt(token, user, account, profile, isNewUser) {
return token
},
},

View File

@@ -15,7 +15,7 @@
"next-auth": ["."],
"next-auth/providers": ["./providers"],
"next-auth/adapters": ["./adapters"],
"next-auth/react": ["./react-client"],
"next-auth/client": ["./client"],
"next-auth/jwt": ["./jwt"]
}
}

View File

@@ -49,6 +49,8 @@ export default NextAuth({
## Schema
Run the following commands inside of the `Shell` tab in the Fauna dashboard to setup the appropriate collections and indexes.
```javascript
CreateCollection({ name: "accounts" })
CreateCollection({ name: "sessions" })
@@ -76,7 +78,7 @@ CreateIndex({
terms: [{ field: ["data", "email"] }],
})
CreateIndex({
name: "verification_request_by_token",
name: "verification_request_by_token_and_identifier",
source: Collection("verification_requests"),
unique: true,
terms: [{ field: ["data", "token"] }, { field: ["data", "identifier"] }],

View File

@@ -15,7 +15,7 @@ This is the Firebase Adapter for [`next-auth`](https://next-auth.js.org). This p
npm install next-auth @next-auth/firebase-adapter
```
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
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"

View File

@@ -11,6 +11,7 @@ All table/collection names in the built in models are plural, and all table name
You can [extend the built in models](/tutorials/typeorm-custom-models) and even [create your own database adapter](/tutorials/creating-a-database-adapter) if you want to use NextAuth.js with a database that is not supported out of the box.
:::
---
## User
@@ -29,7 +30,7 @@ If a user first signs in with OAuth then their email address is automatically po
This provides a way to contact users and for users to maintain access to their account and sign in using email in the event they are unable to sign in with the OAuth provider in future (if email sign in is configured).
:::
## Account
## Account
Table: `accounts`
@@ -59,4 +60,4 @@ The Verification Request model is used to store tokens for passwordless sign in
A single User can have multiple open Verification Requests (e.g. to sign in to different devices).
It has been designed to be extendable for other verification purposes in future (e.g. 2FA / short codes).
It has been designed to be extendable for other verification purposes in future (e.g. 2FA / short codes).

View File

@@ -15,7 +15,6 @@ There you can find the following adapters:
- [`fauna`](./fauna)
- [`dynamodb`](./dynamodb)
- [`firebase`](./firebase)
- [`pouchdb`](./pouchdb)
## Custom Adapter

View File

@@ -23,7 +23,7 @@ Configure your NextAuth.js to use the Prisma Adapter:
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import { PrismaLegacyAdapter } from "@next-auth/prisma-legacy-adapter"
import Adapters from "next-auth/adapters"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
@@ -35,7 +35,7 @@ export default NextAuth({
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
adapter: PrismaLegacyAdapter({ prisma }),
adapter: Adapters.Prisma.Adapter({ prisma }),
})
```
@@ -161,7 +161,7 @@ You can use custom model names by using the `modelMapping` option (shown here wi
```javascript title="pages/api/auth/[...nextauth].js"
...
adapter: PrismaLegacyAdapter({
adapter: Adapters.Prisma.Adapter({
prisma,
modelMapping: {
User: 'user',

View File

@@ -19,4 +19,4 @@ Objects stored in MongoDB use similar datatypes to SQL, with some differences:
4. A sparse index is used on the User `email` property to allow it to be optional, while still enforcing uniqueness if it is specified.
This is functionally equivalent to the ANSI SQL behaviour for a `unique` but `nullable` property.
This is functionally equivalent to the ANSI SQL behaviour for a `unique` but `nullable` property.

View File

@@ -1,88 +1,88 @@
---
id: mssql
title: Microsoft SQL Server
---
Schema for a Microsoft SQL Server (mssql) database.
:::note
When using a Microsoft SQL Server database with the default adapter (TypeORM) all properties of type `timestamp` are transformed to `datetime`.
This transform is also applied to any properties of type `timestamp` when using custom models.
:::
```sql
CREATE TABLE accounts
(
id int IDENTITY(1,1) NOT NULL,
compound_id varchar(255) NOT NULL,
user_id int NOT NULL,
provider_type varchar(255) NOT NULL,
provider_id varchar(255) NOT NULL,
provider_account_id varchar(255) NOT NULL,
refresh_token text NULL,
access_token text NULL,
access_token_expires datetime NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE sessions
(
id int IDENTITY(1,1) NOT NULL,
user_id int NOT NULL,
expires datetime NOT NULL,
session_token varchar(255) NOT NULL,
access_token varchar(255) NOT NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE users
(
id int IDENTITY(1,1) NOT NULL,
name varchar(255) NULL,
email varchar(255) NULL,
email_verified datetime NULL,
image varchar(255) NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE verification_requests
(
id int IDENTITY(1,1) NOT NULL,
identifier varchar(255) NOT NULL,
token varchar(255) NOT NULL,
expires datetime NOT NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE UNIQUE INDEX compound_id
ON accounts(compound_id);
CREATE INDEX provider_account_id
ON accounts(provider_account_id);
CREATE INDEX provider_id
ON accounts(provider_id);
CREATE INDEX user_id
ON accounts(user_id);
CREATE UNIQUE INDEX session_token
ON sessions(session_token);
CREATE UNIQUE INDEX access_token
ON sessions(access_token);
CREATE UNIQUE INDEX email
ON users(email);
CREATE UNIQUE INDEX token
ON verification_requests(token);
```
When using NextAuth.js with SQL Server for the first time, run NextAuth.js once against your database with `?synchronize=true` on the connection string and export the schema that is created.
:::
---
id: mssql
title: Microsoft SQL Server
---
Schema for a Microsoft SQL Server (mssql) database.
:::note
When using a Microsoft SQL Server database with the default adapter (TypeORM) all properties of type `timestamp` are transformed to `datetime`.
This transform is also applied to any properties of type `timestamp` when using custom models.
:::
```sql
CREATE TABLE accounts
(
id int IDENTITY(1,1) NOT NULL,
compound_id varchar(255) NOT NULL,
user_id int NOT NULL,
provider_type varchar(255) NOT NULL,
provider_id varchar(255) NOT NULL,
provider_account_id varchar(255) NOT NULL,
refresh_token text NULL,
access_token text NULL,
access_token_expires datetime NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE sessions
(
id int IDENTITY(1,1) NOT NULL,
user_id int NOT NULL,
expires datetime NOT NULL,
session_token varchar(255) NOT NULL,
access_token varchar(255) NOT NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE users
(
id int IDENTITY(1,1) NOT NULL,
name varchar(255) NULL,
email varchar(255) NULL,
email_verified datetime NULL,
image varchar(255) NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE verification_requests
(
id int IDENTITY(1,1) NOT NULL,
identifier varchar(255) NOT NULL,
token varchar(255) NOT NULL,
expires datetime NOT NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE UNIQUE INDEX compound_id
ON accounts(compound_id);
CREATE INDEX provider_account_id
ON accounts(provider_account_id);
CREATE INDEX provider_id
ON accounts(provider_id);
CREATE INDEX user_id
ON accounts(user_id);
CREATE UNIQUE INDEX session_token
ON sessions(session_token);
CREATE UNIQUE INDEX access_token
ON sessions(access_token);
CREATE UNIQUE INDEX email
ON users(email);
CREATE UNIQUE INDEX token
ON verification_requests(token);
```
When using NextAuth.js with SQL Server for the first time, run NextAuth.js once against your database with `?synchronize=true` on the connection string and export the schema that is created.
:::

View File

@@ -84,4 +84,4 @@ CREATE UNIQUE INDEX email
CREATE UNIQUE INDEX token
ON verification_requests(token);
```
```

View File

@@ -16,16 +16,16 @@ You can specify a handler for any of the callbacks below.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
async signIn({ user, account, profile, email, credentials }) {
async signIn(user, account, profile) {
return true
},
async redirect({ url, baseUrl }) {
async redirect(url, baseUrl) {
return baseUrl
},
async session({ session, user, token }) {
async session(session, user) {
return session
},
async jwt({ token, user, account, profile, isNewUser }) {
async jwt(token, user, account, profile, isNewUser) {
return token
}
...
@@ -41,7 +41,15 @@ Use the `signIn()` callback to control if a user is allowed to sign in.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
async signIn({ user, account, profile, email, credentials }) {
/**
* @param {object} user User object
* @param {object} account Provider account
* @param {object} profile Provider profile
* @return {boolean|string} Return `true` to allow sign in
* Return `false` to deny access
* Return `string` to redirect to (eg.: "/unauthorized")
*/
async signIn(user, account, profile) {
const isAllowedToSignIn = true
if (isAllowedToSignIn) {
return true
@@ -56,18 +64,18 @@ callbacks: {
...
```
- When using the **Email Provider** the `signIn()` callback is triggered both when the user makes a **Verification Request** (before they are sent email with a link that will allow them to sign in) and again _after_ they activate the link in the sign in email.
Email accounts do not have profiles in the same way OAuth accounts do. On the first call during email sign in the `email` object will include a property `verificationRequest: true` to indicate it is being triggered in the verification request flow. When the callback is invoked _after_ a user has clicked on a sign in link, this property will not be present.
* When using the **Email Provider** the `signIn()` callback is triggered both when the user makes a **Verification Request** (before they are sent email with a link that will allow them to sign in) and again *after* they activate the link in the sign in email.
Email accounts do not have profiles in the same way OAuth accounts do. On the first call during email sign in the `profile` object will include a property `verificationRequest: true` to indicate it is being triggered in the verification request flow. When the callback is invoked *after* a user has clicked on a sign in link, this property will not be present.
You can check for the `verificationRequest` property to avoid sending emails to addresses or domains on a blocklist (or to only explicitly generate them for email address in an allow list).
- When using the **Credentials Provider** the `user` object is the response returned from the `authorization` callback and the `credentials` object is the raw body of the `HTTP POST` submission.
* When using the **Credentials Provider** the `user` object is the response returned from the `authorize` callback and the `profile` object is the raw body of the `HTTP POST` submission.
:::note
When using NextAuth.js with a database, the User object will be either a user object from the database (including the User ID) if the user has signed in before or a simpler prototype user object (i.e. name, email, image) for users who have not signed in before.
When using NextAuth.js without a database, the user object will always be a prototype user object, with information extracted from the profile.
When using NextAuth.js without a database, the user object it will always be a prototype user object, with information extracted from the profile.
:::
:::note
@@ -85,36 +93,52 @@ By default only URLs on the same URL as the site are allowed, you can use the re
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
redirect({ url, baseUrl }) {
return url.startsWith(baseUrl) ? url : baseUrl
/**
* @param {string} url URL provided as callback URL by the client
* @param {string} baseUrl Default base URL of site (can be used as fallback)
* @return {string} URL the client will be redirect to
*/
async redirect(url, baseUrl) {
return url.startsWith(baseUrl)
? url
: baseUrl
}
}
...
```
:::note
The redirect callback may be invoked more than once in the same flow.
:::
## JWT callback
This callback is called whenever a JSON Web Token is created (i.e. at sign
in) or updated (i.e whenever a session is accessed in the client). The returned value will be [signed and optionally encrypted](/configuration/options#jwt), and it is stored in a cookie.
This JSON Web Token callback is called whenever a JSON Web Token is created (i.e. at sign
in) or updated (i.e whenever a session is accessed in the client).
Requests to `/api/auth/signin`, `/api/auth/session` and calls to `getSession()`, `useSession()` will invoke this function, but only if you are using a [JWT session](/configuration/options#session). This method is not invoked when you persist sessions in a database.
e.g. `/api/auth/signin`, `getSession()`, `useSession()`, `/api/auth/session`
- As with database persisted session expiry times, token expiry time is extended whenever a session is active.
- The arguments _user_, _account_, _profile_ and _isNewUser_ are only passed the first time this callback is called on a new session, after the user signs in. In subsequent calls, only `token` will be available.
* As with database session expiry times, token expiry time is extended whenever a session is active.
* The arguments *user*, *account*, *profile* and *isNewUser* are only passed the first time this callback is called on a new session, after the user signs in.
The contents _user_, _account_, _profile_ and _isNewUser_ will vary depending on the provider and on if you are using a database or not. You can persist data such as User ID, OAuth Access Token in this token. To make it available in the browser, check out the [`session()` callback](#session-callback) as well.
The contents *user*, *account*, *profile* and *isNewUser* will vary depending on the provider and on if you are using a database or not. If you want to pass data such as User ID, OAuth Access Token, etc. to the browser, you can persist it in the token and use the `session()` callback to return it.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
async jwt({ token, account }) {
// Persist the OAuth access_token to the token right after signin
if (account) {
token.accessToken = account.access_token
/**
* @param {object} token Decrypted JSON Web Token
* @param {object} user User object (only available on sign in)
* @param {object} account Provider account (only available on sign in)
* @param {object} profile Provider profile (only available on sign in)
* @param {boolean} isNewUser True if new user (only available on sign in)
* @return {object} JSON Web Token that will be saved
*/
async jwt(token, user, account, profile, isNewUser) {
// Add access_token to the token right after signin
if (account?.accessToken) {
token.accessToken = account.accessToken
}
return token
}
@@ -123,13 +147,18 @@ callbacks: {
```
:::tip
Use an if branch to check for the existence of parameters (apart from `token`). If they exist, this means that the callback is being invoked for the first time (i.e. the user is being signed in). This is a good place to persist additional data like an `access_token` in the JWT. Subsequent invocations will only contain the `token` parameter.
Use an if branch in jwt with checking for existence of any other params than token. If any of those exist, you call jwt for the first time.
This is a good place to add for example an `access_token` to your jwt, if you want to.
:::
:::tip
Check out the content of all the params in addition `token`, to see what info you have available on signin.
:::
:::warning
NextAuth.js does not limit how much data you can store in a JSON Web Token, however a ~**4096 byte limit** per cookie is commonly imposed by browsers.
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). A common solution is to store a key in the cookie that can be used to look up the remaining data in the database, for example, in the `session()` callback. Opt into database persisted sessions by setting [`session: {jwt: false}`](/configuration/options#session).
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). A common solution is to store a key in the cookie that can be used to look up the remaining data in the database, for example, in the `session()` callback.
:::
## Session callback
@@ -138,14 +167,20 @@ The session callback is called whenever a session is checked. By default, only a
e.g. `getSession()`, `useSession()`, `/api/auth/session`
- When using database sessions, the User object is passed as an argument.
- When using JSON Web Tokens for sessions, the JWT payload is provided instead.
* When using database sessions, the User object is passed as an argument.
* When using JSON Web Tokens for sessions, the JWT payload is provided instead.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
async session({ session, token, user }) {
// Send properties to the client, like an access_token from a provider.
/**
* @param {object} session Session object
* @param {object} token User object (if using database sessions)
* JSON Web Token (if not using database sessions)
* @return {object} Session that will be returned to the client
*/
async session(session, token) {
// Add property to session, like an access_token from a provider.
session.accessToken = token.accessToken
return session
}
@@ -153,11 +188,17 @@ callbacks: {
...
```
If you're using TypeScript, you will want to [augment the session type](/getting-started/typescript#module-augmentation).
:::tip
When using JSON Web Tokens the `jwt()` callback is invoked before the `session()` callback, so anything you add to the
JSON Web Token will be immediately available in the session callback, like for example an `access_token` from a provider.
:::
:::tip
To better represent its value, when using a JWT session, the second parameter should be called `token` (This is the same thing you return from the `jwt()` callback). If you use a database, call it `user`.
:::
:::warning
The session object is not persisted server side, even when using database sessions - only data such as the session token, the user, and the expiry time is stored in the session table.

View File

@@ -3,51 +3,47 @@ id: databases
title: Databases
---
NextAuth.js offers multiple database adapters:
NextAuth.js comes with multiple ways of connecting to a database:
- [`typeorm-legacy`](./../adapters/typeorm/typeorm-overview)
- [`prisma`](./../adapters/prisma)
- [`prisma-legacy`](./../adapters/prisma-legacy)
- [`fauna`](./../adapters/fauna)
- [`dynamodb`](./../adapters/dynamodb)
- [`firebase`](./../adapters/firebase)
- [`pouchdb`](./../adapters/pouchdb)
- **TypeORM** (default)<br/>
_The TypeORM adapter supports MySQL, PostgreSQL, MSSQL, SQLite and MongoDB databases._
- **Prisma**<br/>
_The Prisma 2 adapter supports MySQL, PostgreSQL and SQLite databases._
- **Fauna**<br/>
_The FaunaDB adapter only supports FaunaDB._
- **Custom Adapter**<br/>
_A custom Adapter can be used to connect to any database._
> As of **v4.0.0** NextAuth.js no longer ships with an adapter included by default. If you would like to persist any information, you need to install one of the many available adapters yourself. See the individual adapter documentation pages for more details.
> There are currently efforts in the [`nextauthjs/adapters`](https://github.com/nextauthjs/adapters) repository to get community-based DynamoDB, Sanity, PouchDB and Sequelize Adapters merged. If you are interested in any of the above, feel free to check out the PRs in the `nextauthjs/adapters` repository!
**This document covers the default adapter (TypeORM).**
See the [documentation for adapters](/adapters/overview) to learn more about using Prisma adapter or using a custom adapter.
To learn more about databases in NextAuth.js and how they are used, check out [databases in the FAQ](/faq#databases).
---
**The rest of this document covers the old default adapter (TypeORM).**
## How to use a database
## How to use a database
You can specify database credentials as as a connection string or a [TypeORM configuration](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md) object.
You can specify database credentials as a [TypeORM configuration](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md) object or connection string:
The following approaches are exactly equivalent:
```js title="pages/api/auth/[...nextauth].js"
import TypeORMAdapter from "@next-auth/typeorm-legacy-adapter"
import NextAuth from "next-auth"
export default NextAuth({
adapter: TypeORMAdapter(
"mysql://nextauth:password@127.0.0.1:3306/database_name"
),
// or...
adapter: TypeORMAdapter({
type: "mysql",
host: "127.0.0.1",
port: 3306,
username: "nextauth",
password: "password",
database: "database_name",
}),
})
```js
database: "mysql://nextauth:password@127.0.0.1:3306/database_name"
```
Both approaches are exactly equivalent:
```js
database: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'nextauth',
password: 'password',
database: 'database_name'
}
```
:::tip
You can pass in any valid [TypeORM configuration option](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md).
@@ -55,23 +51,22 @@ You can pass in any valid [TypeORM configuration option](https://github.com/type
_e.g. To set a prefix for all table names you can use the **entityPrefix** option as connection string parameter:_
```js
adapter: TypeORMAdapter(
"mysql://nextauth:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_"
)
"mysql://nextauth:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_"
```
_…or as a database configuration object:_
```js
adapter: TypeORMAdapter({
type: "mysql",
host: "127.0.0.1",
database: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: "nextauth",
password: "password",
database: "database_name",
entityPrefix: "nextauth_",
})
username: 'nextauth',
password: 'password',
database: 'database_name',
entityPrefix: 'nextauth_'
}
```
:::
@@ -92,21 +87,19 @@ _If you are running SQLite, MongoDB or a Document database you can skip this ste
Alternatively, you can also have your database configured automatically using the `synchronize: true` option:
```js
adapter: TypeORMAdapter(
"mysql://nextauth:password@127.0.0.1:3306/database_name?synchronize=true"
)
database: "mysql://nextauth:password@127.0.0.1:3306/database_name?synchronize=true"
```
```js
adapter: TypeORMAdapter({
type: "mysql",
host: "127.0.0.1",
database: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: "nextauth",
password: "password",
database: "database_name",
synchronize: true,
})
username: 'nextauth',
password: 'password',
database: 'database_name',
synchronize: true
}
```
:::warning
@@ -135,9 +128,7 @@ Install module:
#### Example
```js
adapter: TypeORMAdapter(
"mysql://username:password@127.0.0.1:3306/database_name"
)
database: "mysql://username:password@127.0.0.1:3306/database_name"
```
### MariaDB
@@ -148,9 +139,7 @@ Install module:
#### Example
```js
adapter: TypeORMAdapter(
"mariadb://username:password@127.0.0.1:3306/database_name"
)
database: "mariadb://username:password@127.0.0.1:3306/database_name"
```
### Postgres / CockroachDB
@@ -163,34 +152,30 @@ Install module:
PostgresDB
```js
adapter: TypeORMAdapter(
"postgres://username:password@127.0.0.1:5432/database_name"
)
database: "postgres://username:password@127.0.0.1:5432/database_name"
```
CockroachDB
```js
adapter: TypeORMAdapter(
"postgres://username:password@127.0.0.1:26257/database_name"
)
database: "postgres://username:password@127.0.0.1:26257/database_name"
```
If the node is using Self-signed cert
```js
adapter: TypeORMAdapter({
type: "cockroachdb",
host: process.env.DATABASE_HOST,
port: 26257,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
ssl: {
rejectUnauthorized: false,
ca: fs.readFileSync("/path/to/server-certificates/root.crt").toString(),
database: {
type: "cockroachdb",
host: process.env.DATABASE_HOST,
port: 26257,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
ssl: {
rejectUnauthorized: false,
ca: fs.readFileSync('/path/to/server-certificates/root.crt').toString()
},
},
})
```
Read more: [https://node-postgres.com/features/ssl](https://node-postgres.com/features/ssl)
@@ -205,7 +190,7 @@ Install module:
#### Example
```js
adapter: TypeORMAdapter("mssql://sa:password@localhost:1433/database_name")
database: "mssql://sa:password@localhost:1433/database_name"
```
### MongoDB
@@ -216,9 +201,7 @@ Install module:
#### Example
```js
adapter: TypeORMAdapter(
"mongodb://username:password@127.0.0.1:3306/database_name"
)
database: "mongodb://username:password@127.0.0.1:3306/database_name"
```
### SQLite
@@ -231,7 +214,7 @@ Install module:
#### Example
```js
adapter: TypeORMAdapter("sqlite://localhost/:memory:")
database: "sqlite://localhost/:memory:"
```
## Other databases

View File

@@ -123,27 +123,34 @@ jwt: {
// Defaults to NextAuth.js secret if not explicitly specified.
// This is used to generate the actual signingKey and produces a warning
// message if not defined explicitly.
// secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
// You can generate a secret be using `openssl rand -base64 64`
secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
// You can generate a signing key using `jose newkey -s 512 -t oct -a HS512`
// This gives you direct knowledge of the key used to sign the token so you can use it
// to authenticate indirectly (eg. to a database driver)
// signingKey: {"kty":"oct","kid":"Dl893BEV-iVE-x9EC52TDmlJUgGm9oZ99_ZL025Hc5Q","alg":"HS512","k":"K7QqRmJOKRK2qcCKV_pi9PSBv3XP0fpTu30TP8xn4w01xR3ZMZM38yL2DnTVPVw6e4yhdh0jtoah-i4c_pZagA"},
signingKey: {
kty: "oct",
kid: "Dl893BEV-iVE-x9EC52TDmlJUgGm9oZ99_ZL025Hc5Q",
alg: "HS512",
k: "K7QqRmJOKRK2qcCKV_pi9PSBv3XP0fpTu30TP8xn4w01xR3ZMZM38yL2DnTVPVw6e4yhdh0jtoah-i4c_pZagA"
},
// If you chose something other than the default algorithm for the signingKey (HS512)
// you also need to configure the algorithm
// verificationOptions: {
// algorithms: ['HS256']
// },
verificationOptions: {
algorithms: ['HS256']
},
// Set to true to use encryption. Defaults to false (signing only).
// encryption: true,
// encryptionKey: "",
// decryptionKey = encryptionKey,
// decryptionOptions = {
// algorithms: ['A256GCM']
// },
encryption: true,
// You can generate an encryption key by using `npx node-jose-tools newkey -s 256 -t oct -a A256GCM -u enc`
encryptionKey: "",
// decryptionKey: encryptionKey,
decryptionOptions: {
algorithms: ['A256GCM']
},
// You can define your own encode/decode functions for signing and encryption
// if you want to override the default behaviour.
// async encode({ secret, token, maxAge }) {},
// async decode({ secret, token, maxAge }) {},
async encode({ secret, token, maxAge }) {},
async decode({ secret, token, maxAge }) {},
}
```
@@ -252,16 +259,16 @@ You can specify a handler for any of the callbacks below.
```js
callbacks: {
async signIn({ user, account, profile, email, credentials }) {
async signIn(user, account, profile) {
return true
},
async redirect({ url, baseUrl }) {
async redirect(url, baseUrl) {
return baseUrl
},
async session({ session, token, user }) {
async session(session, user) {
return session
},
async jwt({ token, user, account, profile, isNewUser }) {
async jwt(token, user, account, profile, isNewUser) {
return token
}
}
@@ -300,12 +307,20 @@ events: {
### adapter
- **Default value**: none
- **Default value**: _Adapter.Default()_
- **Required**: _No_
#### Description
By default NextAuth.js does not include an adapter any longer. If you would like to persist user / account data, please install one of the many available adapters. More information can be found in the [adapter documentation](/adapters/overview).
By default NextAuth.js uses a database adapter that uses TypeORM and supports MySQL, MariaDB, Postgres and MongoDB and SQLite databases. An alternative adapter that uses Prisma, which currently supports MySQL, MariaDB and Postgres, is also included.
You can use the `adapter` option to use the Prisma adapter - or pass in your own adapter if you want to use a database that is not supported by one of the built-in adapters.
See the [adapter documentation](/adapters/overview) for more information.
:::note
If the `adapter` option is specified it overrides the `database` option, only specify one or the other.
:::
---
@@ -329,8 +344,6 @@ Set debug to `true` to enable debug messages for authentication and database ope
Override any of the logger levels (`undefined` levels will use the built-in logger), and intercept logs in NextAuth. You can use this to send NextAuth logs to a third-party logging service.
The `code` parameter for `error` and `warn` are explained in the [Warnings](/warnings) and [Errors](/errors) pages respectively.
Example:
```js title="/pages/api/auth/[...nextauth].js"
@@ -339,14 +352,14 @@ import log from "logging-service"
export default NextAuth({
...
logger: {
error(code, metadata) {
log.error(code, metadata)
error(code, ...message) {
log.error(code, message)
},
warn(code) {
log.warn(code)
warn(code, ...message) {
log.warn(code, message)
},
debug(code, metadata) {
log.debug(code, metadata)
debug(code, ...message) {
log.debug(code, message)
}
}
...

View File

@@ -22,11 +22,9 @@ To add a custom login page, you can use the `pages` option:
```
## Error codes
We purposefully restrict the returned error codes for increased security.
### Error page
The following errors are passed as error query parameters to the default or overriden error page:
- **Configuration**: There is a problem with the server configuration. Check if your [options](/configuration/options#options) is correct.
@@ -37,7 +35,6 @@ The following errors are passed as error query parameters to the default or over
Example: `/auth/error?error=Configuration`
### Sign-in page
The following errors are passed as error query parameters to the default or overriden sign-in page:
- **OAuthSignin**: Error in constructing an authorization URL ([1](https://github.com/nextauthjs/next-auth/blob/457952bb5abf08b09861b0e5da403080cd5525be/src/server/lib/signin/oauth.js), [2](https://github.com/nextauthjs/next-auth/blob/main/src/server/lib/oauth/pkce-handler.js), [3](https://github.com/nextauthjs/next-auth/blob/main/src/server/lib/oauth/state-handler.js)),
@@ -48,7 +45,6 @@ The following errors are passed as error query parameters to the default or over
- **OAuthAccountNotLinked**: If the email on the account is already linked, but not with this OAuth account
- **EmailSignin**: Sending the e-mail with the verification token failed
- **CredentialsSignin**: The `authorize` callback returned `null` in the [Credentials provider](/providers/credentials). We don't recommend providing information about which part of the credentials were wrong, as it might be abused by malicious hackers.
- **SessionRequired**: The content of this page requires you to be signed in at all times. See [useSession](/getting-started/client#require-session) for configuration.
- **Default**: Catch all, will apply, if none of the above matched
Example: `/auth/error?error=Default`
@@ -64,16 +60,14 @@ By default, the built-in pages will follow the system theme, utilizing the [`pre
In order to get the available authentication providers and the URLs to use for them, you can make a request to the API endpoint `/api/auth/providers`:
```jsx title="pages/auth/signin.js"
import { getProviders, signIn } from "next-auth/react"
import { getProviders, signIn } from 'next-auth/client'
export default function SignIn({ Providers }) {
export default function SignIn({ providers }) {
return (
<>
{Object.values(providers).map((provider) => (
{Object.values(providers).map(provider => (
<div key={provider.name}>
<button onClick={() => signIn(provider.id)}>
Sign in with {provider.name}
</button>
<button onClick={() => signIn(provider.id)}>Sign in with {provider.name}</button>
</div>
))}
</>
@@ -81,10 +75,10 @@ export default function SignIn({ Providers }) {
}
// This is the recommended way for Next.js 9.3 or newer
export async function getServerSideProps(context) {
export async function getServerSideProps(context){
const providers = await getProviders()
return {
props: { providers },
props: { providers }
}
}
@@ -103,26 +97,26 @@ SignIn.getInitialProps = async () => {
If you create a custom sign in form for email sign in, you will need to submit both fields for the **email** address and **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/signin/email**.
```jsx title="pages/auth/email-signin.js"
import { getCsrfToken } from "next-auth/react"
import { getCsrfToken } from 'next-auth/client'
export default function SignIn({ csrfToken }) {
return (
<form method="post" action="/api/auth/signin/email">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
<form method='post' action='/api/auth/signin/email'>
<input name='csrfToken' type='hidden' defaultValue={csrfToken}/>
<label>
Email address
<input type="email" id="email" name="email" />
<input type='email' id='email' name='email'/>
</label>
<button type="submit">Sign in with Email</button>
<button type='submit'>Sign in with Email</button>
</form>
)
}
// This is the recommended way for Next.js 9.3 or newer
export async function getServerSideProps(context) {
export async function getServerSideProps(context){
const csrfToken = await getCsrfToken(context)
return {
props: { csrfToken },
props: { csrfToken }
}
}
@@ -139,7 +133,7 @@ SignIn.getInitialProps = async (context) => {
You can also use the `signIn()` function which will handle obtaining the CSRF token for you:
```js
signIn("email", { email: "jsmith@example.com" })
signIn('email', { email: 'jsmith@example.com' })
```
### Credentials Sign in
@@ -147,21 +141,21 @@ signIn("email", { email: "jsmith@example.com" })
If you create a sign in form for credentials based authentication, you will need to pass a **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/callback/credentials**.
```jsx title="pages/auth/credentials-signin.js"
import { getCsrfToken } from "next-auth/react"
import { getCsrfToken } from 'next-auth/client'
export default function SignIn({ csrfToken }) {
return (
<form method="post" action="/api/auth/callback/credentials">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
<form method='post' action='/api/auth/callback/credentials'>
<input name='csrfToken' type='hidden' defaultValue={csrfToken}/>
<label>
Username
<input name="username" type="text" />
<input name='username' type='text'/>
</label>
<label>
Password
<input name="password" type="password" />
<input name='password' type='password'/>
</label>
<button type="submit">Sign in</button>
<button type='submit'>Sign in</button>
</form>
)
}
@@ -170,8 +164,8 @@ export default function SignIn({ csrfToken }) {
export async function getServerSideProps(context) {
return {
props: {
csrfToken: await getCsrfToken(context),
},
csrfToken: await getCsrfToken(context)
}
}
}
@@ -188,7 +182,7 @@ SignIn.getInitialProps = async (context) => {
You can also use the `signIn()` function which will handle obtaining the CSRF token for you:
```js
signIn("credentials", { username: "jsmith", password: "1234" })
signIn('credentials', { username: 'jsmith', password: '1234' })
```
:::tip

View File

@@ -83,7 +83,7 @@ providers: [
| name | Descriptive name for the provider | `string` | Yes |
| type | Type of provider, in this case `oauth` | `"oauth"` | Yes |
| version | OAuth version (e.g. '1.0', '1.0a', '2.0') | `string` | Yes |
| scope | OAuth access scopes (expects string with space as separator) | `string` | Yes |
| scope | OAuth access scopes (expects array or string) | `string` or `string[]` | Yes |
| params | Extra URL params sent when calling `accessTokenUrl` | `Object` | Yes |
| accessTokenUrl | Endpoint to retrieve an access token | `string` | Yes |
| authorizationUrl | Endpoint to request authorization from the user | `string` | Yes |
@@ -92,7 +92,8 @@ providers: [
| clientId | Client ID of the OAuth provider | `string` | Yes |
| clientSecret | Client Secret of the OAuth provider | `string` | Yes |
| profile | A callback returning an object with the user's info | `(profile, tokens) => Object` | Yes |
| checks | Additional security checks on OAuth providers (default: [`state`]) | `("pkce"|"state"|"none")[]` | No |
| protection | Additional security for OAuth login flows (defaults to `state`) | `"pkce"`,`"state"`,`"none"` | No |
| state | Same as `protection: "state"`. Being deprecated, use protection. | `boolean` | No |
| headers | Any headers that should be sent to the OAuth provider | `Object` | No |
| authorizationParams | Additional params to be sent to the authorization endpoint | `Object` | No |
| idToken | Set to `true` for services that use ID Tokens (e.g. OpenID) | `boolean` | No |
@@ -186,7 +187,7 @@ You only need to add two changes:
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
3. Add it to our [provider types](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts) (for TS projects)<br />
• you just need to add your new provider name to [this list](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts#L56-L97)<br />
• in case you new provider accepts some custom options, you can [add them here](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts#L48-L53)
• in case your new provider accepts some custom options, you can [add them here](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts#L48-L53)
That's it! 🎉 Others will be able to discover this provider much more easily now!
@@ -259,14 +260,14 @@ providers: [
// that is false/null if the credentials are invalid.
// e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
// You can also use the `req` object to obtain additional parameters
// (i.e., the request IP address)
// (i.e., the request IP address)
const res = await fetch("/your/endpoint", {
method: 'POST',
body: JSON.stringify(credentials),
headers: { "Content-Type": "application/json" }
})
const user = await res.json()
// If no error and we have user data, return it
if (res.ok && user) {
return user
@@ -287,10 +288,10 @@ The Credentials provider can only be used if JSON Web Tokens are enabled for ses
### Options
| Name | Description | Type | Required |
| Name | Description | Type | Required |
| :---------: | :-----------------------------------------------: | :-----------------------------------: | :------: |
| id | Unique ID for the provider | `string` | Yes |
| name | Descriptive name for the provider | `string` | Yes |
| type | Type of provider, in this case `credentials` | `"credentials"` | Yes |
| credentials | The credentials to sign-in with | `Object` | Yes |
| id | Unique ID for the provider | `string` | Yes |
| name | Descriptive name for the provider | `string` | Yes |
| type | Type of provider, in this case `credentials` | `"credentials"` | Yes |
| credentials | The credentials to sign-in with | `Object` | Yes |
| authorize | Callback to execute once user is to be authorized | `(credentials, req) => Promise<User>` | Yes |

View File

@@ -15,9 +15,9 @@ If you are seeing any of these errors in the console, something is wrong.
These errors are returned from the client. As the client is [Universal JavaScript (or "Isomorphic JavaScript")](https://en.wikipedia.org/wiki/Isomorphic_JavaScript) it can be run on the client or server, so these errors can occur in both in the terminal and in the browser console.
#### CLIENT_SESSION_ERROR
#### CLIENT_USE_SESSION_ERROR
This error occurs when the `SessionProvider` Context has a problem fetching session data.
This error occurs when the `useSession()` React Hook has a problem fetching session data.
#### CLIENT_FETCH_ERROR
@@ -33,62 +33,28 @@ These errors are displayed on the terminal.
#### OAUTH_GET_ACCESS_TOKEN_ERROR
This occurs when there was an error in the POST request to the OAuth provider and we were not able to retrieve the access token.
Please double check your provider settings.
#### OAUTH_V1_GET_ACCESS_TOKEN_ERROR
This error is explicitly related to older OAuth v1.x providers, if you are using one of these, please double check all available settings.
#### OAUTH_GET_PROFILE_ERROR
N/A
#### OAUTH_PARSE_PROFILE_ERROR
This error is a result of either a problem with the provider response or the user cancelling the action with the provider, unfortunately we can't discern which with the information we have.
This error should also log the exception and available `profileData` to further aid debugging.
#### OAUTH_CALLBACK_HANDLER_ERROR
This error will occur when there was an issue parsing the json request body, for example.
There should also be further details logged when this occurs, such as the error thrown, and the request body itself to aid in debugging.
---
### Signin / Callback
#### GET_AUTHORIZATION_URL_ERROR
This error can occur when we cannot get the OAuth v1 request token and generate the authorization URL.
Please double check your OAuth v1 provider settings, especially the OAuth token and OAuth token secret.
#### SIGNIN_OAUTH_ERROR
This error can occur in one of a few places, first during the redirect to the authorization URL of the provider. Next, in the signin flow while creating the PKCE code verifier. Finally, during the generation of the CSRF Token hash in internal state during signin.
Please check your OAuth provider settings and make sure your URLs and other options are correctly set on the provider side.
#### CALLBACK_OAUTH_ERROR
This can occur during handling of the callback if the `code_verifier` cookie was not found or an invalid state was returned from the OAuth provider.
#### SIGNIN_EMAIL_ERROR
This error can occur when a user tries to sign in via an email link; for example, if the email token could not be generated or the verification request failed.
Please double check your email settings.
#### CALLBACK_EMAIL_ERROR
This can occur during the email callback process. Specifically, if there was an error signing the user in via email, encoding the jwt, etc.
Please double check your Email settings.
#### EMAIL_REQUIRES_ADAPTER_ERROR
The Email authentication provider can only be used if a database is configured.
@@ -105,8 +71,6 @@ In _most cases_ it does not make sense to specify a database in NextAuth.js opti
#### CALLBACK_CREDENTIALS_HANDLER_ERROR
This error occurs when there was no `authorize()` handler defined on the credential authentication provider.
#### PKCE_ERROR
The provider you tried to use failed when setting [PKCE or Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636#section-4.2).
@@ -123,20 +87,20 @@ https://next-auth.js.org/errors#jwt_session_error JWKKeySupport: the key does no
The algorithm used for generating your key isn't listed as supported. You can generate a HS512 key using
```
````
jose newkey -s 512 -t oct -a HS512
```
````
If you are unable to use an HS512 key (for example to interoperate with other services) you can define what is supported using
```
````
jwt: {
signingKey: {"kty":"oct","kid":"--","alg":"HS256","k":"--"},
verificationOptions: {
algorithms: ["HS256"]
}
}
```
````
#### SESSION_ERROR
@@ -146,8 +110,6 @@ If you are unable to use an HS512 key (for example to interoperate with other se
#### SIGNOUT_ERROR
This error occurs when there was an issue deleting the session from the database, for example.
---
### Database
@@ -158,56 +120,30 @@ They all indicate a problem interacting with the database.
#### ADAPTER_CONNECTION_ERROR
This error can occur during the `createConnection()` function. Make sure your database connection string / settings are correct and the database is up and ready to receive connections.
#### CREATE_USER_ERROR
N/A
#### GET_USER_BY_ID_ERROR
N/A
#### GET_USER_BY_EMAIL_ERROR
N/A
#### GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR
N/A
#### LINK_ACCOUNT_ERROR
N/A
#### CREATE_SESSION_ERROR
N/A
#### GET_SESSION_ERROR
N/A
#### UPDATE_SESSION_ERROR
N/A
#### DELETE_SESSION_ERROR
N/A
#### CREATE_VERIFICATION_REQUEST_ERROR
N/A
#### GET_VERIFICATION_REQUEST_ERROR
N/A
#### DELETE_VERIFICATION_REQUEST_ERROR
N/A
---
### Other

View File

@@ -15,24 +15,13 @@ It is not commercial software and is not associated with a commercial organizati
## Compatibility
<details>
<summary>
<h3 style={{display:"inline-block"}}>What databases does NextAuth.js support?</h3>
</summary>
<p>
### What databases does NextAuth.js support?
You can use NextAuth.js with MySQL, MariaDB, Postgres, MongoDB and SQLite or without a database. (See also: [Databases](/configuration/databases))
You can use also NextAuth.js with any database using a custom database adapter, or by using a custom credentials authentication provider - e.g. to support signing in with a username and password stored in an existing database.
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What authentication services does NextAuth.js support?</h3>
</summary>
<p>
### What authentication services does NextAuth.js support?
<p>NextAuth.js includes built-in support for signing in with&nbsp;
{Object.values(require("../providers.json")).sort().join(", ")}.
@@ -43,14 +32,7 @@ NextAuth.js also supports email for passwordless sign in, which is useful for ac
You can also use a custom based provider to support signing in with a username and password stored in an external database and/or using two factor authentication.
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Does NextAuth.js support signing in with a username and password?</h3>
</summary>
<p>
### Does NextAuth.js support signing in with a username and password?
NextAuth.js is designed to avoid the need to store passwords for user accounts.
@@ -58,14 +40,7 @@ If you have an existing database of usernames and passwords, you can use a custo
_If you use a custom credentials provider user accounts will not be persisted in a database by NextAuth.js (even if one is configured). The option to use JSON Web Tokens for session tokens (which allow sign in without using a session database) must be enabled to use a custom credentials provider._
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Can I use NextAuth.js with a website that does not use Next.js?</h3>
</summary>
<p>
### Can I use NextAuth.js with a website that does not use Next.js?
NextAuth.js is designed for use with Next.js and Serverless.
@@ -75,55 +50,27 @@ If you use NextAuth.js on a website with a different subdomain then the rest of
NextAuth.js does not currently support automatically signing into sites on different top level domains (e.g. `www.example.com` vs `www.example.org`) using a single session.
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Can I use NextAuth.js with React Native?</h3>
</summary>
<p>
### Can I use NextAuth.js with React Native?
NextAuth.js is designed as a secure, confidential client and implements a server side authentication flow.
It is not intended to be used in native applications on desktop or mobile applications, which typically implement public clients (e.g. with client / secrets embedded in the application).
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Is NextAuth.js supporting TypeScript?</h3>
</summary>
<p>
### Is NextAuth.js supporting TypeScript?
Yes! Check out the [TypeScript docs](/getting-started/typescript)
</p>
</details>
---
## Databases
<details>
<summary>
<h3 style={{display:"inline-block"}}>What databases are supported by NextAuth.js?</h3>
</summary>
<p>
### What databases are supported by NextAuth.js?
NextAuth.js can be used with MySQL, Postgres, MongoDB, SQLite and compatible databases (e.g. MariaDB, Amazon Aurora, Amazon DocumentDB…) or with no database.
It also provides an Adapter API which allows you to connect it to any database.
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What does NextAuth.js use databases for?</h3>
</summary>
<p>
### What does NextAuth.js use databases for?
Databases in NextAuth.js are used for persisting users, OAuth accounts, email sign in tokens and sessions.
@@ -131,70 +78,35 @@ Specifying a database is optional if you don't need to persist user data or supp
If you are using a database with NextAuth.js, you can still explicitly enable JSON Web Tokens for sessions (instead of using database sessions).
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Should I use a database?</h3>
</summary>
<p>
### Should I use a database?
- Using NextAuth.js without a database works well for internal tools - where you need to control who is able to sign in, but when you do not need to create user accounts for them in your application.
- Using NextAuth.js with a database is usually a better approach for a consumer facing application where you need to persist accounts (e.g. for billing, to contact customers, etc).
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What database should I use?</h3>
</summary>
<p>
### What database should I use?
Managed database solutions for MySQL, Postgres and MongoDB (and compatible databases) are well supported from cloud providers such as Amazon, Google, Microsoft and Atlas.
If you are deploying directly to a particular cloud platform you may also want to consider serverless database offerings they have (e.g. [Amazon Aurora Serverless on AWS](https://aws.amazon.com/rds/aurora/serverless/)).
</p>
</details>
---
## Security
<details>
<summary>
<h3 style={{display:"inline-block"}}>I think I've found a security problem, what should I do?</h3>
</summary>
<p>
### I think I've found a security problem, what should I do?
Less serious or edge case issues (e.g. queries about compatibility with optional RFC specifications) can be raised as public issues on GitHub.
If you discover what you think may be a potentially serious security problem, please contact a core team member via a private channel (e.g. via email to me@iaincollins.com) or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details.
If you discover what you think may be a potentially serious security problem, please contact a core team member via a private channel (e.g. via email to me@iaincollins.com or info@balazsorban.com and yo@ndo.dev) or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details.
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What is the disclosure policy for NextAuth.js?</h3>
</summary>
<p>
### What is the disclosure policy for NextAuth.js?
We practice responsible disclosure.
If you contact us regarding a potentially serious issue, we will endeavor to get back to you within 72 hours and to publish a fix within 30 days. We will responsibly disclose the issue (and credit you with your consent) once a fix to resolve the issue has been released - or after 90 days, which ever is sooner.
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>How do I get Refresh Tokens and Access Tokens for an OAuth account?</h3>
</summary>
<p>
### How do I get Refresh Tokens and Access Tokens for an OAuth account?
NextAuth.js provides a solution for authentication, session management and user account creation.
@@ -207,14 +119,7 @@ You can then look them up from the database or persist them to the JSON Web Toke
Note: NextAuth.js does not currently handle Access Token rotation for OAuth providers for you, however you can check out [this tutorial](/tutorials/refresh-token-rotation) if you want to implement it.
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>When I sign in with another account with the same email address, why are accounts not linked automatically?</h3>
</summary>
<p>
### When I sign in with another account with the same email address, why are accounts not linked automatically?
Automatic account linking on sign in is not secure between arbitrary providers - with the exception of allowing users to sign in via an email addresses as a fallback (as they must verify their email address as part of the flow).
@@ -232,18 +137,11 @@ Automatic account linking is not a planned feature of NextAuth.js, however there
Providing support for secure account linking and unlinking of additional providers - which can only be done if a user is already signed in already - was originally a feature in v1.x but has not been present since v2.0, is planned to return in a future release.
</p>
</details>
---
## Feature Requests
<details>
<summary>
<h3 style={{display:"inline-block"}}>Why doesn't NextAuth.js support [a particular feature]?</h3>
</summary>
<p>
### Why doesn't NextAuth.js support [a particular feature]?
NextAuth.js is an open source project built by individual contributors who are volunteers writing code and providing support in their spare time.
@@ -251,14 +149,7 @@ If you would like NextAuth.js to support a particular feature, the best way to h
If you are not able to develop a feature yourself, you can offer to sponsor someone to work on it.
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>I disagree with a design decision, how can I change your mind?</h3>
</summary>
<p>
### I disagree with a design decision, how can I change your mind?
Product design decisions on NextAuth.js are made by core team members.
@@ -268,18 +159,11 @@ Requests that provide the detail requested in the template and follow the format
Ultimately if your request is not accepted or is not actively in development, you are always free to fork the project under the terms of the ISC License.
</p>
</details>
---
## JSON Web Tokens
<details>
<summary>
<h3 style={{display:"inline-block"}}>Does NextAuth.js use JSON Web Tokens?</h3>
</summary>
<p>
### Does NextAuth.js use JSON Web Tokens?
NextAuth.js supports both database session tokens and JWT session tokens.
@@ -288,14 +172,7 @@ NextAuth.js supports both database session tokens and JWT session tokens.
You can also choose to use JSON Web Tokens as session tokens with using a database, by explicitly setting the `session: { jwt: true }` option.
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What are the advantages of JSON Web Tokens?</h3>
</summary>
<p>
### What are the advantages of JSON Web Tokens?
JSON Web Tokens can be used for session tokens, but are also used for lots of other things, such as sending signed objects between services in authentication flows.
@@ -307,14 +184,7 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
- You can use JWT to securely store information you do not mind the client knowing even without encryption, as the JWT is stored in a server-readable-only-token so data in the JWT is not accessible to third party JavaScript running on your site.
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What are the disadvantages of JSON Web Tokens?</h3>
</summary>
<p>
### What are the disadvantages of JSON Web Tokens?
- You cannot as easily expire a JSON Web Token - doing so requires maintaining a server side blocklist of invalid tokens (at least until they expire) and checking every token against the list every time a token is presented.
@@ -332,18 +202,11 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
Avoid storing any data in a token that might be problematic if it were to be decrypted in the future.
- If you do not explicitly specify a secret for for NextAuth.js, existing sessions will be invalidated any time your NextAuth.js configuration changes, as NextAuth.js will default to an auto-generated secret.
- If you do not explicitly specify a secret for NextAuth.js, existing sessions will be invalidated any time your NextAuth.js configuration changes, as NextAuth.js will default to an auto-generated secret.
If using JSON Web Token you should at least specify a secret and ideally configure public/private keys.
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Are JSON Web Tokens secure?</h3>
</summary>
<p>
### Are JSON Web Tokens secure?
By default tokens are signed (JWS) but not encrypted (JWE), as encryption adds additional overhead and reduces the amount of space available to store data (total cookie size for a domain is limited to 4KB).
@@ -357,14 +220,7 @@ NextAuth.js will generate keys for you, but this will generate a warning at star
Using explicit public/private keys for signing is strongly recommended.
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What signing and encryption standards does NextAuth.js support?</h3>
</summary>
<p>
### What signing and encryption standards does NextAuth.js support?
NextAuth.js includes a largely complete implementation of JSON Object Signing and Encryption (JOSE):
@@ -379,6 +235,3 @@ This incorporates support for:
- [RFC 7638 - JSON Web Key Thumbprint](https://tools.ietf.org/html/rfc7638)
- [RFC 7787 - JSON JWS Unencoded Payload Option](https://tools.ietf.org/html/rfc7797)
- [RFC 8037 - CFRG Elliptic Curve ECDH and Signatures](https://tools.ietf.org/html/rfc8037)
</p>
</details>

View File

@@ -34,17 +34,17 @@ You can use the [session callback](/configuration/callbacks#session-callback) to
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
Make sure that [`<SessionProvider>`](#sessionprovider) is added to `pages/_app.js`.
It works best when the [`<Provider>`](#provider) is added to `pages/_app.js`.
#### Example
```jsx
import { useSession } from "next-auth/react"
import { useSession } from "next-auth/client"
export default function Component() {
const { data: session, status } = useSession()
const [session, loading] = useSession()
if (status === "authenticated") {
if (session) {
return <p>Signed in as {session.user.email}</p>
}
@@ -52,41 +52,6 @@ export default function Component() {
}
```
`useSession()` returns an object containing two values: `data` and `status`:
- **`data`**: This can be three values: [`Session`](https://github.com/nextauthjs/next-auth/blob/8ff4b260143458c5d8a16b80b11d1b93baa0690f/types/index.d.ts#L437-L444) / `undefined` / `null`.
- when the session hasn't been fetched yet, `data` will `undefined`
- in case it failed to retrieve the session, `data` will be `null`
- in case of success, `data` will be [`Session`](https://github.com/nextauthjs/next-auth/blob/8ff4b260143458c5d8a16b80b11d1b93baa0690f/types/index.d.ts#L437-L444).
- **`status`**: enum mapping to three possible session states: `"loading" | "authenticated" | "unauthenticated"`
### Require session
Due to the way how Next.js handles `getServerSideProps` and `getInitialProps`, every protected page load has to make a server-side request to check if the session is valid and then generate the requested page (SSR). This increases server load, and if you are good with making the requests from the client, there is an alternative. You can use `useSession` in a way that makes sure you always have a valid session. If after the initial loading state there was no session found, you can define the appropriate action to respond.
The default behavior is to redirect the user to the sign-in page, from where - after a successful login - they will be sent back to the page they started on. You can also define an `onFail()` callback, if you would like to do something else:
#### Example
```jsx title="pages/protected.jsx"
import { useSession } from "next-auth/react"
export default function Admin() {
const { status } = useSession({
required: true,
onUnauthenticated() {
// The user is not authenticated, handle it here.
}
})
const if (status === "loading") {
return "Loading or not authenticated..."
}
return "User is logged in"
}
```
---
## getSession()
@@ -110,7 +75,7 @@ async function myFunction() {
#### Server Side Example
```js
import { getSession } from "next-auth/react"
import { getSession } from "next-auth/client"
export default async (req, res) => {
const session = await getSession({ req })
@@ -148,7 +113,7 @@ async function myFunction() {
#### Server Side Example
```js
import { getCsrfToken } from "next-auth/react"
import { getCsrfToken } from "next-auth/client"
export default async (req, res) => {
const csrfToken = await getCsrfToken({ req })
@@ -175,7 +140,7 @@ It can be useful if you are creating a dynamic custom sign in page.
#### API Route
```jsx title="pages/api/example.js"
import { getProviders } from "next-auth/react"
import { getProviders } from "next-auth/client"
export default async (req, res) => {
const providers = await getProviders()
@@ -202,7 +167,7 @@ The `signIn()` method can be called from the client in different ways, as shown
#### Redirects to sign in page when clicked
```js
import { signIn } from "next-auth/react"
import { signIn } from "next-auth/client"
export default () => <button onClick={() => signIn()}>Sign in</button>
```
@@ -210,7 +175,7 @@ export default () => <button onClick={() => signIn()}>Sign in</button>
#### Starts Google OAuth sign-in flow when clicked
```js
import { signIn } from "next-auth/react"
import { signIn } from "next-auth/client"
export default () => (
<button onClick={() => signIn("google")}>Sign in with Google</button>
@@ -222,7 +187,7 @@ export default () => (
When using it with the email flow, pass the target `email` as an option.
```js
import { signIn } from "next-auth/react"
import { signIn } from "next-auth/client"
export default ({ email }) => (
<button onClick={() => signIn("email", { email })}>Sign in with Email</button>
@@ -241,7 +206,7 @@ e.g.
- `signIn('google', { callbackUrl: 'http://localhost:3000/foo' })`
- `signIn('email', { email, callbackUrl: 'http://localhost:3000/foo' })`
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect-callback). By default it requires the URL to be an absolute URL at the same host name, or else it will redirect to the homepage. You can define your own [redirect callback](/configuration/callbacks#redirect-callback) to allow other URLs, including supporting relative URLs.
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect-callback). By default it requires the URL to be an absolute URL at the same hostname, or else it will redirect to the homepage. You can define your own [redirect callback](/configuration/callbacks#redirect-callback) to allow other URLs, including supporting relative URLs.
#### Using the redirect: false option
@@ -283,7 +248,7 @@ e.g.
}
```
#### Additional parameters
#### Additional params
It is also possible to pass additional parameters to the `/authorize` endpoint through the third argument of `signIn()`.
@@ -291,7 +256,7 @@ See the [Authorization Request OIDC spec](https://openid.net/specs/openid-connec
e.g.
- `signIn("identity-server4", null, { prompt: "login" })` _always ask the user to re-authenticate_
- `signIn("identity-server4", null, { prompt: "login" })` _always ask the user to reauthenticate_
- `signIn("auth0", null, { login_hint: "info@example.com" })` _hints the e-mail address to the provider_
:::note
@@ -309,12 +274,12 @@ The following parameters are always overridden server-side: `redirect_uri`, `sta
- Client Side: **Yes**
- Server Side: No
Using the `signOut()` method ensures the user ends back on the page they started on after completing the sign out flow. It also handles CSRF tokens for you automatically.
In order to logout, use the `signOut()` method to ensure the user ends back on the page they started on after completing the sign out flow. It also handles CSRF tokens for you automatically.
It reloads the page in the browser when complete.
```js
import { signOut } from "next-auth/react"
import { signOut } from "next-auth/client"
export default () => <button onClick={() => signOut()}>Sign out</button>
```
@@ -325,7 +290,7 @@ As with the `signIn()` function, you can specify a `callbackUrl` parameter by pa
e.g. `signOut({ callbackUrl: 'http://localhost:3000/foo' })`
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect-callback). By default this means it must be an absolute URL at the same host name (or else it will default to the homepage); you can define your own custom [redirect callback](/configuration/callbacks#redirect-callback) to allow other URLs, including supporting relative URLs.
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect-callback). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom [redirect callback](/configuration/callbacks#redirect-callback) to allow other URLs, including supporting relative URLs.
#### Using the redirect: false option
@@ -334,36 +299,35 @@ If you pass `redirect: false` to `signOut`, the page will not reload. The sessio
:::tip
If you need to redirect to another page but you want to avoid a page reload, you can try:
`const data = await signOut({redirect: false, callbackUrl: "/foo"})`
where `data.url` is the validated URL you can redirect the user to without any flicker by using Next.js's `useRouter().push(data.url)`
where `data.url` is the validated url you can redirect the user to without any flicker by using Next.js's `useRouter().push(data.url)`
:::
---
## SessionProvider
## Provider
Using the supplied `<SessionProvider>` allows instances of `useSession()` to share the session object across components, by using [React Context](https://reactjs.org/docs/context.html) under the hood. It also takes care of keeping the session updated and synced between tabs/windows.
Using the supplied React `<Provider>` allows instances of `useSession()` to share the session object across components, by using [React Context](https://reactjs.org/docs/context.html) under the hood.
This improves performance, reduces network calls and avoids page flicker when rendering. It is highly recommended and can be easily added to all pages in Next.js apps by using `pages/_app.js`.
```jsx title="pages/_app.js"
import { SessionProvider } from "next-auth/react"
import { Provider } from "next-auth/client"
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
export default function App({ Component, pageProps }) {
return (
<SessionProvider session={session}>
<Provider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
</Provider>
)
}
```
If you pass the `session` page prop to the `<SessionProvider>` as in the example above you can avoid checking the session twice on pages that support both server and client side rendering.
If you pass the `session` page prop to the `<Provider>` as in the example above you can avoid checking the session twice on pages that support both server and client side rendering.
This only works on pages where you provide the correct `pageProps`, however. This is normally done in `getInitialProps` or `getServerSideProps` like so:
```js title="pages/index.js"
import { getSession } from "next-auth/react"
import { getSession } from "next-auth/client"
...
@@ -376,33 +340,30 @@ export async function getServerSideProps(ctx) {
}
```
If every one of your pages needs to be protected, you can do this in `_app`, otherwise you can do it on a page-by-page basis. Alternatively, you can do per page authentication checks client side, instead of having each authentication check be blocking (SSR) by using the method described below in [alternative client session handling](#custom-client-session-handling).
If every one of your pages needs to be protected, you can do this in `_app`, otherwise you can do it on a page-by-page basis. Alternatively, you can do per page authentication checks client side, instead of having each auth check be blocking (SSR) by using the method described below in [alternative client session handling](#custom-client-session-handling).
### Options
The session state is automatically synchronized across all open tabs/windows and they are all updated whenever they gain or lose focus or the state changes in any of them (e.g. a user signs in or out).
If you have session expiry times of 30 days (the default) or more then you probably don't need to change any of the default options in the Provider. If you need to, you can can trigger an update of the session object across all tabs/windows by calling `getSession()` from a client side function.
If you have session expiry times of 30 days (the default) or more then you probably don't need to change any of the default options in the Provider. If you need to, you can trigger an update of the session object across all tabs/windows by calling `getSession()` from a client side function.
However, if you need to customize the session behavior and/or are using short session expiry times, you can pass options to the provider to customize the behavior of the `useSession()` hook.
However, if you need to customise the session behaviour and/or are using short session expiry times, you can pass options to the provider to customise the behaviour of the `useSession()` hook.
```jsx title="pages/_app.js"
import { SessionProvider } from "next-auth/react"
import { Provider } from "next-auth/client"
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
export default function App({ Component, pageProps }) {
return (
<SessionProvider
session={session}
// Re-fetch session if cache is older than 60 seconds
staleTime={60}
// Re-fetch session every 5 minutes
refetchInterval={5 * 60}
<Provider
session={pageProps.session}
options={{
clientMaxAge: 60, // Re-fetch session if cache is older than 60 seconds
keepAlive: 5 * 60, // Send keepAlive message every 5 minutes
}}
>
<Component {...pageProps} />
</SessionProvider>
</Provider>
)
}
```
@@ -412,30 +373,30 @@ export default function App({
Every tab/window maintains its own copy of the local session state; the session is not stored in shared storage like localStorage or sessionStorage. Any update in one tab/window triggers a message to other tabs/windows to update their own session state.
Using low values for `staleTime` or `refetchInterval` will increase network traffic and load on authenticated clients and may impact hosting costs and performance.
Using low values for `clientMaxAge` or `keepAlive` will increase network traffic and load on authenticated clients and may impact hosting costs and performance.
:::
#### Stale time
#### Client Max Age
The `staleTime` option is the maximum age a session data can be on the client before it is considered stale.
The `clientMaxAge` option is the maximum age a session data can be on the client before it is considered stale.
When `staleTime` is set to `0` (the default) the cache will always be used when `useSession` is called and only explicit calls made to get the session status (i.e. `getSession()`) or event triggers, such as signing in or out in another tab/window, or a tab/window gaining or losing focus, will trigger an update of the session state.
When `clientMaxAge` is set to `0` (the default) the cache will always be used when useSession is called and only explicit calls made to get the session status (i.e. `getSession()`) or event triggers, such as signing in or out in another tab/window, or a tab/window gaining or losing focus, will trigger an update of the session state.
If set to any value other than zero, it specifies in seconds the maximum age of session data on the client before the `useSession()` hook will call the server again to sync the session state.
Unless you have a short session expiry time (e.g. < 24 hours) you probably don't need to change this option. Setting this option to too short a value will increase load (and potentially hosting costs).
The value for `staleTime` should always be lower than the value of the session `maxAge` [session option](/configuration/options#session).
The value for `clientMaxAge` should always be lower than the value of the session `maxAge` option.
#### Refetch interval
#### Keep Alive
The `refetchInterval` option can be used to contact the server to avoid a session expiring.
The `keepAlive` option is how often the client should contact the server to avoid a session expiring.
When `refetchInterval` is set to `0` (the default) there will be no session polling.
When `keepAlive` is set to `0` (the default) it will not send a keep alive message.
If set to any value other than zero, it specifies in seconds how often the client should contact the server to update the session state. If the session state has expired when it is triggered, all open tabs/windows will be updated to reflect this.
The value for `refetchInterval` should always be lower than the value of the session `maxAge` [session option](/configuration/options#session).
The value for `keepAlive` should always be lower than the value of the session `maxAge` option.
:::note
See [**the Next.js documentation**](https://nextjs.org/docs/advanced-features/custom-app) for more information on **\_app.js** in Next.js applications.
@@ -445,11 +406,11 @@ See [**the Next.js documentation**](https://nextjs.org/docs/advanced-features/cu
### Custom Client Session Handling
Due to the way Next.js handles `getServerSideProps` / `getInitialProps`, every protected page load has to make a server-side request to check if the session is valid and then generate the requested page. This alternative solution allows for showing a loading state on the initial check and every page transition afterward will be client-side, without having to check with the server and regenerate pages.
Due to the way Next.js handles `getServerSideProps` / `getInitialProps`, every protected page load has to make a server-side query to check if the session is valid and then generate the requested page. This alternative solution allows for showing a loading state on the initial check and every page transition afterward will be client-side, without having to check with the server and regenerate pages.
```js title="pages/admin.jsx"
export default function AdminDashboard() {
const { data: session } = useSession()
const [session] = useSession()
// session is always non-null inside this page, all the way down the React tree.
return "Some super secret dashboard"
}
@@ -458,12 +419,9 @@ AdminDashboard.auth = true
```
```jsx title="pages/_app.jsx"
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
export default function App({ Component, pageProps }) {
return (
<SessionProvider session={session}>
<Provider session={pageProps.session}>
{Component.auth ? (
<Auth>
<Component {...pageProps} />
@@ -471,12 +429,12 @@ export default function App({
) : (
<Component {...pageProps} />
)}
</SessionProvider>
</Provider>
)
}
function Auth({ children }) {
const { data: session, loading } = useSession()
const [session, loading] = useSession()
const isUser = !!session?.user
React.useEffect(() => {
if (loading) return // Do nothing while loading
@@ -503,14 +461,14 @@ AdminDashboard.auth = {
}
```
Because of how `_app` is written, it won't unnecessarily contact the `/api/auth/session` endpoint for pages that do not require authentication.
Because of how \_app is done, it won't unnecessarily contact the /api/auth/session endpoint for pages that do not require auth.
More information can be found in the following [GitHub Issue](https://github.com/nextauthjs/next-auth/issues/1210).
More information can be found in the following [Github Issue](https://github.com/nextauthjs/next-auth/issues/1210).
### NextAuth.js + React-Query
There is also an alternative client-side API library based upon [`react-query`](https://www.npmjs.com/package/react-query) available under [`nextauthjs/react-query`](https://github.com/nextauthjs/react-query).
If you use `react-query` in your project already, you can leverage it with NextAuth.js to handle the client-side session management for you as well. This replaces NextAuth.js's native `useSession` and `SessionProvider` from `next-auth/react`.
If you use `react-query` in your project already, you can leverage it with NextAuth.js to handle the client-side session management for you as well. This replaces NextAuth.js's native `useSession` and `Provider` from `next-auth/client`.
See repository [`README`](https://github.com/nextauthjs/react-query) for more details.

View File

@@ -18,22 +18,25 @@ To add NextAuth.js to a project create a file called `[...nextauth].js` in `page
[Read more about how to add authentication providers.](/configuration/providers)
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
export default NextAuth({
// Configure one or more authentication providers
providers: [
Providers.GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
clientSecret: process.env.GITHUB_SECRET
}),
// ...add more providers here
],
// A database is optional, but required to persist accounts in a database
database: process.env.DATABASE_URL,
})
```
All requests to `/api/auth/*` (`signIn`, callback, `signOut`, etc.) will automatically be handled by NextAuth.js.
All requests to `/api/auth/*` (signin, callback, signout, etc) will automatically be handed by NextAuth.js.
:::tip
See the [options documentation](/configuration/options) for how to configure providers, databases and other options.
@@ -41,27 +44,24 @@ See the [options documentation](/configuration/options) for how to configure pro
### Add React Hook
The [`useSession()`](http://localhost:3000/getting-started/client#usesession) React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
```javascript
import { useSession, signIn, signOut } from "next-auth/react"
```jsx title="pages/index.js"
import { signIn, signOut, useSession } from 'next-auth/client'
export default function Component() {
const { data: session } = useSession()
if (session) {
return (
<>
Signed in as {session.user.email} <br />
<button onClick={() => signOut()}>Sign out</button>
</>
)
}
return (
<>
Not signed in <br />
export default function Page() {
const [ session, loading ] = useSession()
return <>
{!session && <>
Not signed in <br/>
<button onClick={() => signIn()}>Sign in</button>
</>
)
</>}
{session && <>
Signed in as {session.user.email} <br/>
<button onClick={() => signOut()}>Sign out</button>
</>}
</>
}
```
@@ -69,28 +69,22 @@ export default function Component() {
You can use the `useSession` hook from anywhere in your application (e.g. in a header component).
:::
### Share/configure session state
### Add session state
To be able to use `useSession` first you'll need to expose the session context, [`<SessionProvider />`](http://localhost:3000/getting-started/client#sessionprovider), at the top level of your application:
To allow session state to be shared between pages - which improves performance, reduces network traffic and avoids component state changes while rendering - you can use the NextAuth.js Provider in `pages/_app.js`.
```javascript
// pages/_app.js
import { SessionProvider } from "next-auth/react"
```jsx title="pages/_app.js"
import { Provider } from 'next-auth/client'
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
export default function App ({ Component, pageProps }) {
return (
<SessionProvider session={session}>
<Provider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
</Provider>
)
}
```
In this way instances of `useSession` can have access to the session data and status, otherwise they'll throw an error... `<SessionProvider />` also takes care of keeping the session updated and synced between browser tabs and windows.
:::tip
Check out the [client documentation](/getting-started/client) to see how you can improve the user experience and page performance by using the NextAuth.js client.
:::

View File

@@ -19,7 +19,7 @@ The POST submission requires CSRF token from `/api/auth/csrf`.
Handles returning requests from OAuth services during sign in.
For OAuth 2.0 providers that support the `checks: ["state"]` option, the state parameter is checked against the one that was generated when the sign in flow was started - this uses a hash of the CSRF token which MUST match for both the POST and `GET` calls during sign in.
For OAuth 2.0 providers that support the `state` option, the value of the `state` parameter is checked against the one that was generated when the sign in flow was started - this uses a hash of the CSRF token which MUST match for both the POST and `GET` calls during sign in.
#### `GET` /api/auth/signout

View File

@@ -8,6 +8,10 @@ NextAuth.js comes with its own type definitions, so you can safely use it in you
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
:::warning
The types at [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) under the name of `@types/next-auth` are now deprecated, and not maintained anymore.
:::
---
## Adapters
@@ -60,19 +64,19 @@ import NextAuth from "next-auth"
export default NextAuth({
callbacks: {
session({ session, token, user }) {
return session // The return type will match the one returned in `useSession()`
session(session, token) {
return session // The type here should match the one returned in `useSession()`
},
},
})
```
```ts title="pages/index.ts"
import { useSession } from "next-auth/react"
import { useSession } from "next-auth/client"
export default function IndexPage() {
// `session` will match the returned value of `callbacks.session()` from `NextAuth()`
const { data: session } = useSession()
// `session` should match `callbacks.session()` in `NextAuth()`
const [session] = useSession()
return (
// Your component
@@ -87,7 +91,7 @@ import NextAuth from "next-auth"
declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
* Returned by `useSession`, `getSession` and received as a prop on the `Provider` React Context
*/
interface Session {
user: {

View File

@@ -5,7 +5,11 @@ title: Azure Active Directory B2C
## Documentation
https://docs.microsoft.com/en-us/azure/active-directory-b2c/
https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow
## Configuration
https://docs.microsoft.com/azure/active-directory-b2c/tutorial-create-tenant
## Options
@@ -15,86 +19,34 @@ The **Azure Active Directory Provider** comes with a set of default options:
You can override any of the options to suit your own use case.
## Configuration (Basic)
Basic configuration sets up Azure AD B2C to return an ID Token. This should be done as a prerequisite prior to running through the Advanced configuration.
Step 1: Azure AD B2C Tenant
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant
Step 2: App Registration
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-register-applications
Step 3: User Flow
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-user-flows
Note: For the step "User attributes and token claims" you might minimally:
- Collect attribute:
- Email Address
- Display Name
- Given Name
- Surname
- Return claim:
- Email Addresses
- Display Name
- Given Name
- Surname
- Identity Provider
- Identity Provider Access Token
- User's Object ID
## Example
- In https://portal.azure.com/ -> Azure Active Directory create a new App Registration.
- Make sure to remember / copy
- Application (client) ID
- Directory (tenant) ID
- When asked for a redirection URL, use http://localhost:3000/api/auth/callback/azure-ad-b2c
- Create a new secret and remember / copy its value immediately, it will disappear.
In `.env.local` create the following entries:
```
AZURE_AD_B2C_TENANT_NAME=<copy the B2C tenant name here from Step 1>
AZURE_AD_B2C_CLIENT_ID=<copy Application (client) ID here from Step 2>
AZURE_AD_B2C_CLIENT_SECRET=<copy generated secret value here from Step 2>
AZURE_AD_B2C_PRIMARY_USER_FLOW=<copy the name of the signin user flow you created from Step 3>
AZURE_CLIENT_ID=<copy Application (client) ID here>
AZURE_CLIENT_SECRET=<copy generated secret value here>
AZURE_TENANT_ID=<copy the tenant id here>
```
In `pages/api/auth/[...nextauth].js` find or add the AZURE_AD_B2C entries:
In `pages/api/auth/[...nextauth].js` find or add the AZURE entries:
```js
import Providers from 'next-auth/providers';
...
providers: [
Providers.AzureADB2C({
tenantName: process.env.AZURE_AD_B2C_TENANT_NAME,
clientId: process.env.AZURE_AD_B2C_CLIENT_ID,
clientSecret: process.env.AZURE_AD_B2C_CLIENT_SECRET,
primaryUserFlow: process.env.AZURE_AD_B2C_PRIMARY_USER_FLOW,
scope: `offline_access openid`,
}),
]
...
```
## Configuration (Advanced)
Advanced configuration sets up Azure AD B2C to return an Authorization Token. This builds on the steps completed in the Basic configuration above.
Step 4: Add a Web API application
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-single-page-app-webapi?tabs=app-reg-ga
Note: this is a second app registration (similar to Step 2) but with different setup and configuration.
## Example
Nothing in `.env.local` needs to change here. The only update is in `pages/api/auth/[...nextauth].js` where you will need to add the additional scopes that were created in Step 4 above:
```js
import Providers from 'next-auth/providers';
...
providers: [
Providers.AzureADB2C({
tenantName: process.env.AZURE_AD_B2C_TENANT_NAME,
clientId: process.env.AZURE_AD_B2C_CLIENT_ID,
clientSecret: process.env.AZURE_AD_B2C_CLIENT_SECRET,
primaryUserFlow: process.env.AZURE_AD_B2C_PRIMARY_USER_FLOW,
scope: `https://${process.env.AZURE_AD_B2C_TENANT_NAME}.onmicrosoft.com/api/demo.read https://${process.env.AZURE_AD_B2C_TENANT_NAME}.onmicrosoft.com/api/demo.write offline_access openid`,
clientId: process.env.AZURE_CLIENT_ID,
clientSecret: process.env.AZURE_CLIENT_SECRET,
scope: 'offline_access User.Read',
tenantId: process.env.AZURE_TENANT_ID,
}),
]
...

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