Compare commits

...

46 Commits

Author SHA1 Message Date
Balázs Orbán
f3be5e87f6 feat(middleware): introduce withAuth Next.js method (#3657)
* feat(middleware): introduce Middleware API to Next.js

* chore(app): upgrade Next.js in dev app

* chore(dev): add Middleware protected page to dev app

* chore(middleware): add `next/middleware` to `exports`

* fix(middleware): bail out redirect on custom pages

* fix(middleware): allow one-line export

* chore(middleware): simplify code

* fix(middleware): redirect back to page after succesful login

* feat(middleware): re-export `withAuth` as `default`

* chore: export middleware from `next-auth/middleware`

* chore: add `middleware` files to npm

* feat(middleware): handle chaining, fix some bugs

* chore(dev): showcase different middlewares

* chore(middleware): remove `@ts-expect-error` comments

* chore: update build clean script

* fix: bail out when NextAuth.js paths

* refactor: be more explicit about `initConfig` result

* refactor: simplify

* refactor: use `callbacks` similarily to `NextAuthOptions`

* refactor: use `nextauth` namespace when setting `token` on `req`

* refactor: don't allow passing `secret`

* addressing review
2022-02-03 18:07:26 +01:00
Dinil Fernando
844c9b147c feat(providers): add Trakt provider (#3771)
* added trakt provider

* fixed incorrect auth url

* Update src/providers/trakt.ts

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

* Update src/providers/trakt.ts

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

* Update trakt.ts

Co-authored-by: caidenwilson <caidenwilson@protonmail.com>
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-02-03 15:27:05 +01:00
Balázs Orbán
c9e16fb71e fix(core): only show Twitter OAuth 2 warning once 2022-02-02 16:06:14 +01:00
Balázs Orbán
a7d34f97c8 fix(providers): properly warn when using Twitter OAuth 2 (#3784)
* fix(providers): properly warn when using Twitter OAuth 2

* refactor(providers): move Twitter OAuth2 warning to `assert`

* fix: use proper warning code

* refactor: only set boolean
2022-02-02 12:37:17 +01:00
Balázs Orbán
f20d6790c8 feat(core): detect NEXTAUTH_SECRET (#3783)
* feat(core): detect `NEXTAUTH_SECRET` env variable

* chore(dev): use detected `NEXTAUTH_SECRET` in dev app
2022-02-02 02:08:56 +01:00
Norbert Szabó
53baf6d67d feat(ts): strongly type sign-in and error page errors (#3740)
* feat: added types for sign in errors

* feat: adding type to error prop

* chore: added documentation links to types
2022-02-02 02:08:44 +01:00
dependabot[bot]
255c822dfb chore(deps): bump node-fetch from 2.6.6 to 2.6.7 (#3777)
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.6 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.6...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-01 08:18:26 +01:00
inshatan
31c03c96d1 typo in redirect url for response with error (#3758) 2022-02-01 08:18:09 +01:00
dependabot[bot]
74df39a678 chore(deps-dev): bump next from 12.0.7 to 12.0.9 (#3764)
Bumps [next](https://github.com/vercel/next.js) from 12.0.7 to 12.0.9.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v12.0.7...v12.0.9)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-01 08:16:25 +01:00
Balázs Orbán
714d80a4f5 Update package.json 2022-01-25 18:39:58 +01:00
Balázs Orbán
3d5c669a05 Update bug_report.yaml 2022-01-25 18:37:30 +01:00
Balázs Orbán
29977f108f Update bug_report.yaml 2022-01-25 18:36:56 +01:00
Balázs Orbán
7d2e16a6bb Update bug_report.yaml 2022-01-25 18:35:54 +01:00
Seiji Takahashi
af157dac07 feat(react): add refetchOnWindowFocus option to SessionProvider (#3730) 2022-01-25 18:06:37 +01:00
Alex Johansson
1bf56a218e fix: Allow React 18 as peer dependency (#3728)
Avoid peer dependency warning when using React 18
2022-01-25 18:02:53 +01:00
David Chalifoux
4824f8c02a fix(providers): Check for valid profile picture response before converting to base64 (#3656)
* Fix: Add OpenID to authorization scope

* Fix: Check for valid profile picture response before converting to base64

* Update src/providers/azure-ad.ts

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

* Confirm that profile photo was returned

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-01-20 02:09:04 +01:00
PAKKU-Chan
a4d831d1b9 feat(providers): add authentik provider (#3625)
* Added authentik provider

* Removed idToken
2022-01-19 01:38:46 +01:00
David Chalifoux
59985264a2 fix(providers): use openid scopes by default (#3651) 2022-01-17 04:28:15 +01:00
Iftekhar Rifat
c844296982 fix: pass csrf & callbackUrl cookies in session api (#3607) 2022-01-17 00:41:16 +01:00
Jon Bellah
d1aa2a1a8e fix(ts): match GoogleProfile interface with Google docs (#3643) 2022-01-17 00:40:23 +01:00
Balázs Orbán
8139126f29 fix(core): detect Vercel without NEXTAUTH_URL (#3649)
* fix(core): detect Vercel without `NEXTAUTH_URL`

* chore(ts): use `any`

* chore: use `process.env.VERCEL` to detect Vercel
2022-01-17 00:37:30 +01:00
Laxmikanta Nayak
aa0e8200b3 docs: Updated the wrong link to providers list in readme (#3616)
The link to providers list was 404 so updated to the correct link in document.
2022-01-15 04:44:31 +01:00
Thang Vu
82447f8e3e fix: display inline errors when using custom error page. (#3576) 2022-01-10 11:57:27 +01:00
Balázs Orbán
a0b3814c81 feat: better out-of-the-box id_token detection (#3514)
* feat: better out-of-the-box `id_token` detection

* fix: check for `scope` on correct endpoint

* chore: simplify internal typing
2022-01-10 11:50:45 +01:00
Balázs Orbán
90c7d535c0 feat(providers): add support to Twitter OAuth 2.0 (#3446)
* feat(providers): add support to Twitter OAuth 2.0

* docs: add docs comment

* chore: cleanup

* chore: remove comments

* chore: give warning for OAuth 2 for now
2022-01-10 11:49:43 +01:00
Tetedeiench
0510c9b1ba feat(providers): add Patreon provider (#3581)
* Added patreon provider - tested and working

* Update src/providers/patreon.js

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

* Update src/providers/patreon.js

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

* Update src/providers/patreon.js

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

* Update src/providers/patreon.js

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

* Switched to TS, restore .env.local, restore package.json as per comments on the PR

* chore: ran Prettier

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-01-10 11:48:11 +01:00
Changsoon Bok
49e4af17e2 fix(providers): refactor naver provider profile (#3500)
* fix(providers): refactor naver provider profile

fix(providers): refactor naver provider profile

* fix(providers): convert typescript - naver provider profile

fix(providers): convert typescript - naver provider profile

* chore(providers): use nested interface for consistency

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-01-10 11:35:53 +01:00
Changsoon Bok
db65afe5ab fix(providers): fix url and auth method for Kakao provider (#3501)
* fix(providers): fix url and auth method for Kakao provider

* fix(providers): convert typescript - Kakao provider

fix(providers): convert typescript - Kakao provider

* chore(providers): use nested interface for consistency

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-01-10 11:31:55 +01:00
Changsoon Bok
36ca1f99e3 docs: update contributing guide link (#3595) 2022-01-10 11:22:58 +01:00
Thang Vu
9bec96784f chore(dev): add postinstall in app to remove next-auth (#3575) 2022-01-08 00:43:38 +01:00
Thang Vu
227ff2259f chore: add eslintIgnore in package.json (#3548)
* fix: add eslintIgnore in package.json

* Let eslint runs in app, config + js files

* Add a separate tsconfig.eslint.json file

We want to run the lint command on `app`, `src` and `config`, but at the same time want `tsc` to compile files in `src` only. A separate `tsconfig.eslint.json` is a suitable solution to satisfy both `eslint` and `tsc`: 04d1f3e549/packages/parser/README.md
2022-01-05 04:02:03 +01:00
Yves Fridelance
c71cb8457d fix(oauth): set httpOptions before issuer discovery (#3537)
* Update client.ts

Set custom.setHttpOptionsDefaults before Issuer.discover(.wellKnown). This allow discover the .wellKnown endpoint behind a proxy

* chore: address code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-01-02 00:04:23 +01:00
krautwigundrüben
a09a75be53 fix(providers): make Strava work again (#3520)
* Update strava.js

Auth with Strava was throwing errors before, this works.

* Update strava.js

changed according to commenters' suggestions

* chore: run linter

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-01-02 00:00:33 +01:00
Balázs Orbán
c4936991e5 chore(app): upgrade dev app dependencies 2021-12-31 00:41:59 +01:00
Thang Vu
e2add6a597 chore(dev): fix start email script (#3541) 2021-12-30 22:42:26 +01:00
Adam Kaczmarek
0e8be0c7d2 docs: fix OpenCollective link in README.md (#3494) 2021-12-22 00:42:21 +01:00
Ivan Esteban
d1d2d977fe fix(providers): use idToken by default in Cognito provider (#3448) 2021-12-18 02:21:20 +01:00
Kirankumar Ambati
48749d7320 fix(pages): remove default placeholder for credentials provider (#3451)
* fix #3449: removed default placeholder for credentials provider

* fix: formatting
2021-12-18 02:10:05 +01:00
Drew Miller
87d0beb70c fix(jwt): use authorization header as fallback (#3453)
If the `req` sent to `getToken` doesn't have the relevant cookies, use
the Bearer token in the Authorization header as a fallback.

Fixes #3452
2021-12-16 13:37:03 +01:00
Balázs Orbán
978e2eeb08 chore(dev): minor fixes on dev app 2021-12-11 21:19:12 +01:00
Balázs Orbán
8ab057ea33 chore(deps): ugprade dependencies (#3415) 2021-12-11 21:17:22 +01:00
Bogdan Soare
2c269a6a81 fix(providers): use id_token by default on Okta provider (#3418) 2021-12-11 12:52:40 +01:00
Alessandro Cuppari
8b9a109255 fix(providers): refactor FusionAuth to v4 (#3376)
* feat: updated fusionauth provider

* Updated fusionauth profile interface docstring

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

* Refactored openid well know logic

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

* Removed jwks endpoint property

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-12-09 21:48:01 +01:00
Etienne Martin
ac35d9f739 docs: Fix README.md typo (#3412) 2021-12-09 16:53:17 +01:00
Balázs Orbán
30a0fc6bc0 fix: properly handle callback URL fallback (#3402)
* fix: don't default to localhost on `host`

* fall back to `host` for `callbackUrl`

* use parsed host

* remove unnecessary type cast
2021-12-08 18:20:33 +01:00
Balázs Orbán
b0f6175cec chore(deps): upgrade next dev dependency 2021-12-08 17:50:25 +01:00
51 changed files with 4391 additions and 7449 deletions

View File

@@ -12,7 +12,12 @@ body:
value: |
Thanks for taking the time to fill out this bug report!
### Important :exclamation:
Please help us maintain this project more efficiently! Before creating the issue make sure you shouldn't be creating it in one the below repos instead:
Please help us maintain this project more efficiently!
Is this your first time contributing? See this video https://www.youtube.com/watch?v=cuoNzXFLitc
**Providing incorrect/insufficient information or skipping steps to reproduce the bug may result in closing the issue or converting to discussion without further explanation.**
Before creating the issue make sure you shouldn't be creating it in one the below repos instead:
- Docs related: https://github.com/nextauthjs/docs
- Adapter related: https://github.com/nextauthjs/adapters
@@ -50,8 +55,6 @@ body:
We encourage you to use one of the templates set up on **CodeSandbox** to reproduce your issue:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
🚧 _If you don't provide any way to reproduce the bug, the issue is at risk of being closed._
- type: textarea
id: logs

2
.gitignore vendored
View File

@@ -36,6 +36,8 @@ node_modules
/index.d.ts
/index.js
/next
/middleware.d.ts
/middleware.js
# Development app
app/src/css

View File

@@ -54,7 +54,7 @@ See [next-auth.js.org](https://next-auth.js.org) for more information and docume
### Flexible and easy to use
- Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
- Built-in support for [many popular sign-in services](https://next-auth.js.org/configuration/providers)
- Built-in support for [many popular sign-in services](https://next-auth.js.org/providers/overview)
- Supports email / passwordless authentication
- Supports stateless authentication with any backend (Active Directory, LDAP, etc)
- Supports both JSON Web Tokens and database sessions
@@ -149,7 +149,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.
Use the `<SessionProvider>` to allow 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"
@@ -179,7 +179,7 @@ export default function App({
### 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!
We're happy to announce we've recently created an [OpenCollective](https://opencollective.com/nextauth) for individuals and companies looking to contribute financially to the project!
<!--sponsors start-->
<table>
@@ -229,7 +229,7 @@ We're happy to announce we've recently created an [OpenCollective](https://openc
## 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).
our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md).
## License

View File

@@ -7,7 +7,7 @@ NEXTAUTH_URL=http://localhost:3000
# https://generate-secret.vercel.app/32 to generate a secret.
# Note: Changing a secret may invalidate existing sessions
# and/or verification tokens.
SECRET=secret
NEXTAUTH_SECRET=secret
AUTH0_ID=
AUTH0_SECRET=
@@ -33,6 +33,9 @@ TWITTER_SECRET=
LINE_ID=
LINE_SECRET=
TRAKT_ID=
TRAKT_SECRET=
# Example configuration for a Gmail account (will need SMTP enabled)
EMAIL_SERVER=smtps://user@gmail.com:password@smtp.gmail.com:465
EMAIL_FROM=user@gmail.com

View File

@@ -103,6 +103,11 @@ export default function Header() {
<a>Email</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href="/middleware-protected">
<a>Middleware protected</a>
</Link>
</li>
</ul>
</nav>
</header>

1
app/next-env.d.ts vendored
View File

@@ -1,5 +1,4 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited

View File

@@ -3,6 +3,7 @@ const path = require("path")
module.exports = {
webpack(config) {
config.experiments = {
...config.experiments,
topLevelAwait: true,
}
config.resolve = {

View File

@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"clean": "rm -rf .next",
"postinstall": "rm -rf node_modules/next-auth",
"dev": "npm-run-all --parallel dev:next watch:css copy:css ",
"dev:next": "next dev",
"build": "next build",
@@ -12,23 +13,23 @@
"watch:css": "cd .. && npm run watch:css",
"start": "next start",
"email": "npx fake-smtp-server",
"start:email": "email"
"start:email": "npm run email"
},
"license": "ISC",
"dependencies": {
"@next-auth/fauna-adapter": "0.2.2-next.4",
"@next-auth/prisma-adapter": "0.5.2-next.5",
"@prisma/client": "^2.29.1",
"@next-auth/fauna-adapter": "^1.0.1",
"@next-auth/prisma-adapter": "^1.0.1",
"@prisma/client": "^3.7.0",
"fake-smtp-server": "^0.8.0",
"faunadb": "^4.3.0",
"next": "^11.1.0",
"nodemailer": "^6.6.3",
"faunadb": "^4.4.1",
"next": "^12.0.8",
"nodemailer": "^6.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"cpx": "^1.5.0",
"npm-run-all": "^4.1.5",
"prisma": "^2.29.1"
"prisma": "^3.7.0"
}
}

View File

@@ -1,9 +1,11 @@
import NextAuth, { NextAuthOptions } from "next-auth"
import EmailProvider from "next-auth/providers/email"
// import EmailProvider from "next-auth/providers/email"
import GitHubProvider from "next-auth/providers/github"
import Auth0Provider from "next-auth/providers/auth0"
import KeycloakProvider from "next-auth/providers/keycloak"
import TwitterProvider from "next-auth/providers/twitter"
import TwitterProvider, {
TwitterLegacy as TwitterLegacyProvider,
} from "next-auth/providers/twitter"
import CredentialsProvider from "next-auth/providers/credentials"
import IDS4Provider from "next-auth/providers/identity-server4"
import Twitch from "next-auth/providers/twitch"
@@ -25,6 +27,8 @@ import Okta from "next-auth/providers/okta"
import AzureB2C from "next-auth/providers/azure-ad-b2c"
import OsuProvider from "next-auth/providers/osu"
import AppleProvider from "next-auth/providers/apple"
import PatreonProvider from "next-auth/providers/patreon"
import TraktProvider from "next-auth/providers/trakt"
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
// import { PrismaClient } from "@prisma/client"
@@ -71,11 +75,17 @@ export const authOptions: NextAuthOptions = {
},
}),
// OAuth 1
// TwitterLegacyProvider({
// clientId: process.env.TWITTER_LEGACY_ID,
// clientSecret: process.env.TWITTER_LEGACY_SECRET,
// }),
// OAuth 2 / OIDC
TwitterProvider({
// Opt-in to the new Twitter API for now. Should be default in the future.
version: "2.0",
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET,
}),
// OAuth 2 / OIDC
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
@@ -177,8 +187,15 @@ export const authOptions: NextAuthOptions = {
clientId: process.env.APPLE_ID,
clientSecret: process.env.APPLE_SECRET,
}),
PatreonProvider({
clientId: process.env.PATREON_ID,
clientSecret: process.env.PATREON_SECRET,
}),
TraktProvider({
clientId: process.env.TRAKT_ID,
clientSecret: process.env.TRAKT_SECRET,
}),
],
secret: process.env.SECRET,
debug: true,
theme: {
colorScheme: "auto",

View File

@@ -0,0 +1,44 @@
export { default } from "next-auth/middleware"
// Other ways to use this middleware
// import withAuth from "next-auth/middleware"
// import { withAuth } from "next-auth/middleware"
// export function middleware(req, ev) {
// return withAuth(req)
// }
// export function middleware(req, ev) {
// return withAuth(req, ev)
// }
// export function middleware(req, ev) {
// return withAuth(req, {
// callbacks: {
// authorized: ({ token }) => !!token,
// },
// })
// }
// export default withAuth(function middleware(req, ev) {
// console.log(req.nextauth.token)
// })
// export default withAuth(
// function middleware(req, ev) {
// console.log(req, ev)
// return undefined // NOTE: `NextMiddleware` should allow returning `void`
// },
// {
// callbacks: {
// authorized: ({ token }) => token.name === "Balázs Orbán",
// }
// }
// )
// export default withAuth({
// callbacks: {
// authorized: ({ token }) => !!token,
// },
// })

View File

@@ -0,0 +1,9 @@
import Layout from "components/layout"
export default function Page() {
return (
<Layout>
<h1>Page protected by Middleware</h1>
</Layout>
)
}

View File

@@ -16,6 +16,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {

10680
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -31,12 +31,13 @@
"./react": "./react/index.js",
"./core": "./core/index.js",
"./next": "./next/index.js",
"./middleware": "./middleware.js",
"./client/_utils": "./client/_utils.js",
"./providers/*": "./providers/*.js"
},
"scripts": {
"build": "npm run build:js && npm run build:css",
"clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts",
"clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts middleware.d.ts middleware.js",
"build:js": "npm run clean && npm run generate-providers && tsc && babel --config-file ./config/babel.config.js src --out-dir . --extensions \".tsx,.ts,.js,.jsx\"",
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir . && node config/wrap-css.js",
"dev:setup": "npm i && npm run generate-providers && npm run build:css && cd app && npm i",
@@ -61,24 +62,26 @@
"core",
"index.d.ts",
"index.js",
"adapters.d.ts"
"adapters.d.ts",
"middleware.d.ts",
"middleware.js"
],
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.15.4",
"@panva/hkdf": "^1.0.0",
"@babel/runtime": "^7.16.3",
"@panva/hkdf": "^1.0.1",
"cookie": "^0.4.1",
"jose": "^4.1.2",
"jose": "^4.3.7",
"oauth": "^0.9.15",
"openid-client": "^5.0.2",
"preact": "^10.5.14",
"openid-client": "^5.1.0",
"preact": "^10.6.3",
"preact-render-to-string": "^5.1.19",
"uuid": "^8.3.2"
},
"peerDependencies": {
"nodemailer": "^6.6.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^17.0.2 || ^18.0.0-0",
"react-dom": "^17.0.2 || ^18.0.0-0"
},
"peerDependenciesMeta": {
"nodemailer": {
@@ -87,50 +90,47 @@
},
"devDependencies": {
"@actions/core": "^1.6.0",
"@babel/cli": "^7.15.7",
"@babel/core": "^7.15.5",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.15.0",
"@babel/preset-env": "^7.15.6",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@testing-library/jest-dom": "^5.14.1",
"@babel/cli": "^7.16.0",
"@babel/core": "^7.16.0",
"@babel/plugin-proposal-optional-catch-binding": "^7.16.0",
"@babel/plugin-transform-runtime": "^7.16.4",
"@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0",
"@babel/preset-typescript": "^7.16.0",
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/user-event": "^13.2.1",
"@types/node": "^16.11.6",
"@testing-library/user-event": "^13.5.0",
"@types/node": "^16.11.12",
"@types/nodemailer": "^6.4.4",
"@types/oauth": "^0.9.1",
"@types/react": "^17.0.27",
"@types/react-dom": "^17.0.9",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@types/react": "^17.0.37",
"@types/react-dom": "^17.0.11",
"@typescript-eslint/parser": "^4.33.0",
"autoprefixer": "^10.3.7",
"babel-jest": "^27.3.0",
"autoprefixer": "^10.4.0",
"babel-jest": "^27.4.2",
"babel-plugin-jsx-pragmatic": "^1.0.2",
"babel-preset-preact": "^2.0.0",
"conventional-changelog-conventionalcommits": "4.6.1",
"cssnano": "^5.0.8",
"cssnano": "^5.0.12",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard-with-typescript": "^21.0.1",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jest": "^25.2.2",
"eslint-plugin-jest": "^25.3.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"fs-extra": "^10.0.0",
"husky": "^7.0.2",
"jest": "^27.3.0",
"husky": "^7.0.4",
"jest": "^27.4.3",
"jest-watch-typeahead": "^1.0.0",
"msw": "^0.35.0",
"next": "v11.1.3-canary.0",
"postcss-cli": "^9.0.1",
"msw": "^0.36.3",
"next": "12.0.9",
"postcss-cli": "^9.0.2",
"postcss-nested": "^5.0.6",
"prettier": "^2.4.1",
"pretty-quick": "^3.1.1",
"prettier": "2.4.1",
"pretty-quick": "^3.1.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript": "^4.4.3",
"typescript": "^4.5.2",
"whatwg-fetch": "^3.6.2"
},
"engines": {
@@ -142,9 +142,12 @@
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
"project": "./tsconfig.eslint.json"
},
"extends": ["standard-with-typescript", "prettier"],
"extends": [
"standard-with-typescript",
"prettier"
],
"ignorePatterns": [
"node_modules",
"next-env.d.ts",
@@ -168,15 +171,26 @@
},
"overrides": [
{
"files": ["./**/*test.js"],
"files": [
"./**/*test.js"
],
"env": {
"jest/globals": true
},
"extends": ["plugin:jest/recommended"],
"plugins": ["jest"]
"extends": [
"plugin:jest/recommended"
],
"plugins": [
"jest"
]
}
]
},
"eslintIgnore": [
"./*.d.ts",
"**/tests",
"**/__tests__"
],
"release": {
"branches": [
"+([0-9])?(.{+([0-9]),x}).x",

View File

@@ -12,7 +12,7 @@ import type { ErrorType } from "./pages/error"
export interface IncomingRequest {
/** @default "http://localhost:3000" */
host: string
host?: string
method?: string
cookies?: Record<string, string>
headers?: Record<string, any>
@@ -74,7 +74,7 @@ export async function NextAuthHandler<
action,
providerId,
host: req.host,
callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl ?? req.host,
callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl,
csrfToken: req.body?.csrfToken,
cookies: req.cookies,
isPost: method === "POST",
@@ -92,8 +92,11 @@ export async function NextAuthHandler<
switch (action) {
case "providers":
return (await routes.providers(options.providers)) as any
case "session":
return (await routes.session({ options, sessionStore })) as any
case "session": {
const session = await routes.session({ options, sessionStore })
if (session.cookies) cookies.push(...session.cookies)
return { ...session, cookies } as any
}
case "csrf":
return {
headers: [{ key: "Content-Type", value: "application/json" }],
@@ -135,15 +138,6 @@ export async function NextAuthHandler<
}
return render.verifyRequest()
case "error":
if (pages.error) {
return {
redirect: `${pages.error}${
pages.error.includes("?") ? "&" : "?"
}error=${error}`,
cookies,
}
}
// These error messages are displayed in line on the sign in page
if (
[
@@ -162,6 +156,15 @@ export async function NextAuthHandler<
return { redirect: `${options.url}/signin?error=${error}`, cookies }
}
if (pages.error) {
return {
redirect: `${pages.error}${
pages.error.includes("?") ? "&" : "?"
}error=${error}`,
cookies,
}
}
return render.error({ error: error as ErrorType })
default:
}

View File

@@ -100,7 +100,7 @@ export async function init({
// Callback functions
callbacks: { ...defaultCallbacks, ...userOptions.callbacks },
logger,
callbackUrl: "http://localhost:3000",
callbackUrl: url.origin,
}
// Init cookies

View File

@@ -16,6 +16,8 @@ type ConfigError =
| MissingAuthorize
| MissingAdapter
let twitterWarned = false
/**
* Verify that the user configured `next-auth` correctly.
* Good place to mention deprecations as well.
@@ -45,11 +47,13 @@ export function assertConfig(
if (!req.host) return "NEXTAUTH_URL"
let hasCredentials, hasEmail
let hasTwitterProvider
options.providers.forEach(({ type }) => {
if (type === "credentials") hasCredentials = true
else if (type === "email") hasEmail = true
})
for (const provider of options.providers) {
if (provider.type === "credentials") hasCredentials = true
else if (provider.type === "email") hasEmail = true
else if (provider.id === "twitter") hasTwitterProvider = true
}
if (hasCredentials) {
const dbStrategy = options.session?.strategy === "database"
@@ -75,4 +79,9 @@ export function assertConfig(
if (hasEmail && !options.adapter) {
return new MissingAdapter("E-mail login requires an adapter.")
}
if (!twitterWarned && hasTwitterProvider) {
twitterWarned = true
return "TWITTER_OAUTH_2_BETA"
}
}

View File

@@ -1,10 +1,11 @@
import { CallbackParamsType, TokenSet } from "openid-client"
import { TokenSet } from "openid-client"
import { openidClient } from "./client"
import { oAuth1Client } from "./client-legacy"
import { useState } from "./state-handler"
import { usePKCECodeVerifier } from "./pkce-handler"
import { OAuthCallbackError } from "../../errors"
import type { CallbackParamsType } from "openid-client"
import type { Account, LoggerInstance, Profile } from "../../.."
import type { OAuthChecks, OAuthConfig } from "../../../providers"
import type { InternalOptions } from "../../../lib/types"

View File

@@ -12,7 +12,9 @@ export async function openidClient(
options: InternalOptions<"oauth">
): Promise<Client> {
const provider = options.provider
if (provider.httpOptions) custom.setHttpOptionsDefaults(provider.httpOptions)
let issuer: Issuer
if (provider.wellKnown) {
issuer = await Issuer.discover(provider.wellKnown)
@@ -44,7 +46,5 @@ export async function openidClient(
// and https://github.com/nextauthjs/next-auth/issues/3067
client[custom.clock_tolerance] = 10
if (provider.httpOptions) custom.setHttpOptionsDefaults(provider.httpOptions)
return client
}

View File

@@ -1,7 +1,8 @@
import { InternalProvider } from "../../lib/types"
import { Provider } from "../../providers"
import { merge } from "../../lib/merge"
import { InternalUrl } from "../../lib/parse-url"
import type { InternalProvider } from "../../lib/types"
import type { Provider } from "../../providers"
import type { InternalUrl } from "../../lib/parse-url"
/**
* Adds `signinUrl` and `callbackUrl` to each provider
@@ -36,7 +37,7 @@ export default function parseProviders(params: {
function normalizeProvider(provider?: Provider) {
if (!provider) return
const normalizedProvider: InternalProvider = Object.entries(
const normalized: InternalProvider = Object.entries(
provider
).reduce<InternalProvider>((acc, [key, value]) => {
if (
@@ -44,25 +45,29 @@ function normalizeProvider(provider?: Provider) {
typeof value === "string"
) {
const url = new URL(value)
;(acc as any)[key] = {
acc[key] = {
url: `${url.origin}${url.pathname}`,
params: Object.fromEntries(url.searchParams ?? []),
}
} else {
acc[key as keyof InternalProvider] = value
acc[key] = value
}
return acc
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter, @typescript-eslint/consistent-type-assertions
}, {} as InternalProvider)
}, {} as any)
// Checks only work on OAuth 2.x + OIDC providers
if (
provider.type === "oauth" &&
!provider.version?.startsWith("1.") &&
!provider.checks
) {
;(normalizedProvider as InternalProvider<"oauth">).checks = ["state"]
if (normalized.type === "oauth" && !normalized.version?.startsWith("1.")) {
// If provider has as an "openid-configuration" well-known endpoint
// or an "openid" scope request, it will also likely be able to receive an `id_token`
normalized.idToken = Boolean(
normalized.idToken ??
normalized.wellKnown?.includes("openid-configuration") ??
// @ts-expect-error
normalized.authorization?.params?.scope?.includes("openid")
)
if (!normalized.checks) normalized.checks = ["state"]
}
return normalizedProvider
return normalized
}

View File

@@ -1,10 +1,20 @@
import { Theme } from "../.."
import { InternalUrl } from "../../lib/parse-url"
/**
* The following errors are passed as error query parameters to the default or overridden error page.
*
* [Documentation](https://next-auth.js.org/configuration/pages#error-page) */
export type ErrorType =
| "default"
| "configuration"
| "accessdenied"
| "verification"
export interface ErrorProps {
url?: InternalUrl
theme?: Theme
error?: string
error?: ErrorType
}
interface ErrorView {
@@ -14,12 +24,6 @@ interface ErrorView {
signin?: JSX.Element
}
export type ErrorType =
| "default"
| "configuration"
| "accessdenied"
| "verification"
/** Renders an error page. */
export default function ErrorPage(props: ErrorProps) {
const { url, error = "default", theme } = props
@@ -87,15 +91,17 @@ export default function ErrorPage(props: ErrorProps) {
status,
html: (
<div className="error">
{ theme?.brandColor && <style
dangerouslySetInnerHTML={{
__html: `
{theme?.brandColor && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--brand-color: ${theme?.brandColor}
}
`,
}}
/> }
}}
/>
)}
{theme?.logo && <img src={theme.logo} alt="Logo" className="logo" />}
<div className="card">
<h1>{heading}</h1>

View File

@@ -1,12 +1,29 @@
import { Theme } from "../.."
import { InternalProvider } from "../../lib/types"
/**
* The following errors are passed as error query parameters to the default or overridden sign-in page.
*
* [Documentation](https://next-auth.js.org/configuration/pages#sign-in-page) */
export type SignInErrorTypes =
| "Signin"
| "OAuthSignin"
| "OAuthCallback"
| "OAuthCreateAccount"
| "EmailCreateAccount"
| "Callback"
| "OAuthAccountNotLinked"
| "EmailSignin"
| "CredentialsSignin"
| "SessionRequired"
| "default"
export interface SignInServerPageParams {
csrfToken: string
providers: InternalProvider[]
callbackUrl: string
email: string
error: string
error: SignInErrorTypes
theme: Theme
}
@@ -39,7 +56,7 @@ export default function SigninPage(props: SignInServerPageParams) {
)
}
const errors: Record<string, string> = {
const errors: Record<SignInErrorTypes, string> = {
Signin: "Try signing in with a different account.",
OAuthSignin: "Try signing in with a different account.",
OAuthCallback: "Try signing in with a different account.",
@@ -48,7 +65,7 @@ export default function SigninPage(props: SignInServerPageParams) {
Callback: "Try signing in with a different account.",
OAuthAccountNotLinked:
"To confirm your identity, sign in with the same account you used originally.",
EmailSignin: "Check your email inbox.",
EmailSignin: "The e-mail could not be sent.",
CredentialsSignin:
"Sign in failed. Check the details you provided are correct.",
SessionRequired: "Please sign in to access this page.",
@@ -59,16 +76,17 @@ export default function SigninPage(props: SignInServerPageParams) {
return (
<div className="signin">
{ theme.brandColor && <style
dangerouslySetInnerHTML={{
__html: `
{theme.brandColor && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--brand-color: ${theme.brandColor}
}
`,
}}
/> }
}}
/>
)}
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
<div className="card">
{error && (
@@ -130,8 +148,7 @@ export default function SigninPage(props: SignInServerPageParams) {
id={`input-${credential}-for-${provider.id}-provider`}
type={provider.credentials[credential].type ?? "text"}
placeholder={
provider.credentials[credential].placeholder ??
"Password"
provider.credentials[credential].placeholder ?? ""
}
{...provider.credentials[credential]}
/>

View File

@@ -70,7 +70,7 @@ export default async function signin(params: {
return {
redirect: `${url}/error?${new URLSearchParams({
error: error as string,
})}}`,
})}`,
}
}

View File

@@ -27,9 +27,10 @@ export interface NextAuthOptions {
providers: Provider[]
/**
* 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.
* The default behavior is volatile, and **it is strongly recommended** you explicitly specify a value
* to avoid invalidating end user sessions when configuration changes are deployed.
* If not specified, it falls back to `jwt.secret` or `NEXTAUTH_SECRET` from environment vairables.
* Otherwise it will use a hash of all configuration options, including Client ID / Secrets for entropy.
*
* NOTE: The last behavior is extrmely volatile, and will throw an error in production.
* * **Default value**: `string` (SHA hash of the "options" object)
* * **Required**: No - **but strongly recommended**!
*

View File

@@ -13,11 +13,8 @@ const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
const now = () => (Date.now() / 1000) | 0
/** Issues a JWT. By default, the JWT is encrypted using "A256GCM". */
export async function encode({
token = {},
secret,
maxAge = DEFAULT_MAX_AGE,
}: JWTEncodeParams) {
export async function encode(params: JWTEncodeParams) {
const { token = {}, secret, maxAge = DEFAULT_MAX_AGE } = params
const encryptionSecret = await getDerivedEncryptionKey(secret)
return await new EncryptJWT(token)
.setProtectedHeader({ alg: "dir", enc: "A256GCM" })
@@ -28,10 +25,8 @@ export async function encode({
}
/** Decodes a NextAuth.js issued JWT. */
export async function decode({
token,
secret,
}: JWTDecodeParams): Promise<JWT | null> {
export async function decode(params: JWTDecodeParams): Promise<JWT | null> {
const { token, secret } = params
if (!token) return null
const encryptionSecret = await getDerivedEncryptionKey(secret)
const { payload } = await jwtDecrypt(token, encryptionSecret, {
@@ -42,7 +37,7 @@ export async function decode({
export interface GetTokenParams<R extends boolean = false> {
/** The request containing the JWT either in the cookies or in the `Authorization` header. */
req: NextApiRequest
req: NextApiRequest | Pick<NextApiRequest, "cookies" | "headers">
/**
* Use secure prefix for cookie name, unless URL in `NEXTAUTH_URL` is http://
* or not set (e.g. development or test instance) case use unprefixed name
@@ -55,7 +50,11 @@ export interface GetTokenParams<R extends boolean = false> {
* @default false
*/
raw?: R
secret: string
/**
* The same `secret` used in the `NextAuth` configuration.
* Defaults to the `NEXTAUTH_SECRET` environment variable.
*/
secret?: string
decode?: JWTOptions["decode"]
logger?: LoggerInstance | Console
}
@@ -71,13 +70,14 @@ export async function getToken<R extends boolean = false>(
const {
req,
secureCookie = process.env.NEXTAUTH_URL?.startsWith("https://") ??
!!process.env.VERCEL_URL,
!!process.env.VERCEL,
cookieName = secureCookie
? "__Secure-next-auth.session-token"
: "next-auth.session-token",
raw,
decode: _decode = decode,
logger = console,
secret = process.env.NEXTAUTH_SECRET,
} = params ?? {}
if (!req) throw new Error("Must pass `req` to JWT getToken()")
@@ -88,7 +88,13 @@ export async function getToken<R extends boolean = false>(
logger
)
const token = sessionStore.value
let token = sessionStore.value
if (!token && req.headers.authorization?.split(" ")[0] === "Bearer") {
const urlEncodedToken = req.headers.authorization.split(" ")[1]
token = decodeURIComponent(urlEncodedToken)
}
// @ts-expect-error
if (!token) return null
@@ -97,7 +103,7 @@ export async function getToken<R extends boolean = false>(
try {
// @ts-expect-error
return await _decode({ token, ...params })
return await _decode({ token, secret })
} catch {
// @ts-expect-error
return null

View File

@@ -34,7 +34,11 @@ export interface JWTDecodeParams {
}
export interface JWTOptions {
/** The secret used to encode/decode the NextAuth.js issued JWT. */
/**
* The secret used to encode/decode the NextAuth.js issued JWT.
* @deprecated Set the `NEXTAUTH_SECRET` environment vairable or
* use the top-level `secret` option instead
*/
secret: string
/**
* The maximum age of the NextAuth.js issued JWT in seconds.

View File

@@ -19,7 +19,7 @@ function hasErrorProperty(
return !!(x as any)?.error
}
export type WarningCode = "NEXTAUTH_URL" | "NO_SECRET"
export type WarningCode = "NEXTAUTH_URL" | "NO_SECRET" | "TWITTER_OAUTH_2_BETA"
/**
* Override any of the methods, and the rest will use the default logger.

View File

@@ -11,6 +11,7 @@ export interface InternalUrl {
toString: () => string
}
/** Returns an `URL` like object to make requests/redirects from server-side */
export default function parseUrl(url?: string): InternalUrl {
const defaultUrl = new URL("http://localhost:3000/api/auth")

View File

@@ -50,7 +50,7 @@ export type NextAuthAction =
export interface InternalOptions<T extends ProviderType = any> {
providers: InternalProvider[]
/**
* Parsed from `NEXTAUTH_URL` or `VERCEL_URL`.
* Parsed from `NEXTAUTH_URL` or `x-forwarded-host` on Vercel.
* @default "http://localhost:3000/api/auth"
*/
url: InternalUrl

2
src/middleware.ts Normal file
View File

@@ -0,0 +1,2 @@
export { default } from "./next/middleware"
export * from "./next/middleware"

View File

@@ -1,5 +1,5 @@
import { NextAuthHandler } from "../core"
import { setCookie } from "./cookie"
import { setCookie, detectHost } from "./utils"
import type {
GetServerSidePropsContext,
@@ -19,12 +19,13 @@ async function NextAuthNextHandler(
options: NextAuthOptions
) {
const { nextauth, ...query } = req.query
options.secret =
options.secret ?? options.jwt?.secret ?? process.env.NEXTAUTH_SECRET
const handler = await NextAuthHandler({
req: {
host:
process.env.NEXTAUTH_URL ??
process.env.VERCEL_URL ??
"http://localhost:3000",
host: detectHost(req.headers["x-forwarded-host"]),
body: req.body,
query,
cookies: req.cookies,
@@ -90,7 +91,7 @@ export async function getServerSession(
const session = await NextAuthHandler<Session | {}>({
options,
req: {
host: (process.env.NEXTAUTH_URL ?? process.env.VERCEL_URL) as string,
host: detectHost(context.req.headers["x-forwarded-host"]),
action: "session",
method: "GET",
cookies: context.req.cookies,
@@ -111,7 +112,7 @@ declare global {
namespace NodeJS {
interface ProcessEnv {
NEXTAUTH_URL?: string
VERCEL_URL?: string
VERCEL?: "1"
}
}
}

141
src/next/middleware.ts Normal file
View File

@@ -0,0 +1,141 @@
import type { NextMiddleware, NextFetchEvent } from "next/server"
import type { Awaitable, NextAuthOptions } from ".."
import type { JWT } from "../jwt"
import { NextResponse, NextRequest } from "next/server"
import { getToken } from "../jwt"
import parseUrl from "../lib/parse-url"
type AuthorizedCallback = (params: {
token: JWT | null
req: NextRequest
}) => Awaitable<boolean>
export interface NextAuthMiddlewareOptions {
/**
* Where to redirect the user in case of an error if they weren't logged in.
* Similar to `pages` in `NextAuth`.
*
* ---
* [Documentation](https://next-auth.js.org/configuration/pages)
*/
pages?: NextAuthOptions["pages"]
callbacks?: {
/**
* Callback that receives the user's JWT payload
* and returns `true` to allow the user to continue.
*
* This is similar to the `signIn` callback in `NextAuthOptions`.
*
* If it returns `false`, the user is redirected to the sign-in page instead
*
* The default is to let the user continue if they have a valid JWT (basic authentication).
*
* How to restrict a page and all of it's subpages for admins-only:
* @example
*
* ```js
* // `pages/admin/_middleware.js`
* import { withAuth } from "next-auth/middleware"
*
* export default withAuth({
* callbacks: {
* authorized: ({ token }) => token?.user.isAdmin
* }
* })
* ```
*
* ---
* [Documentation](https://next-auth.js.org/getting-started/nextjs/middleware#api) | [`signIn` callback](configuration/callbacks#sign-in-callback)
*/
authorized?: AuthorizedCallback
}
}
async function handleMiddleware(
req: NextRequest,
options: NextAuthMiddlewareOptions | undefined,
onSuccess?: (token: JWT | null) => Promise<any>
) {
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
const errorPage = options?.pages?.error ?? "/api/auth/error"
const basePath = parseUrl(process.env.NEXTAUTH_URL).path
// Avoid infinite redirect loop
if (
req.nextUrl.pathname.startsWith(basePath) ||
[signInPage, errorPage].includes(req.nextUrl.pathname)
) {
return
}
if (!process.env.NEXTAUTH_SECRET) {
console.error(
`[next-auth][error][NO_SECRET]`,
`\nhttps://next-auth.js.org/errors#no_secret`
)
return {
redirect: NextResponse.redirect(`${errorPage}?error=Configuration`),
}
}
const token = await getToken({ req: req as any })
const isAuthorized =
(await options?.callbacks?.authorized?.({ req, token })) ?? !!token
// the user is authorized, let the middleware handle the rest
if (isAuthorized) return await onSuccess?.(token)
// the user is not logged in, re-direct to the sign-in page
return NextResponse.redirect(
`${signInPage}?${new URLSearchParams({ callbackUrl: req.url })}`
)
}
export type WithAuthArgs =
| [NextRequest]
| [NextRequest, NextFetchEvent]
| [NextRequest, NextAuthMiddlewareOptions]
| [NextMiddleware]
| [NextMiddleware, NextAuthMiddlewareOptions]
| [NextAuthMiddlewareOptions]
/**
* Middleware that checks if the user is authenticated/authorized.
* If if they aren't, they will be redirected to the login page.
* Otherwise, continue.
*
* @example
*
* ```js
* // `pages/_middleware.js`
* export { default } from "next-auth/middleware"
* ```
*
* ---
* [Documentation](https://next-auth.js.org/getting-started/middleware)
*/
export function withAuth(...args: WithAuthArgs) {
if (args[0] instanceof NextRequest) {
// @ts-expect-error
return handleMiddleware(...args)
}
if (typeof args[0] === "function") {
const middleware = args[0]
const options = args[1] as NextAuthMiddlewareOptions | undefined
return async (...args: Parameters<NextMiddleware>) =>
await handleMiddleware(args[0], options, async (token) => {
;(args[0] as any).nextauth = { token }
return await middleware(...args)
})
}
const options = args[0]
return async (...args: Parameters<NextMiddleware>) =>
await handleMiddleware(args[0], options)
}
export default withAuth

View File

@@ -13,3 +13,11 @@ export function setCookie(res, cookie: Cookie) {
setCookieHeader.push(cookieHeader)
res.setHeader("Set-Cookie", setCookieHeader)
}
/** Extract the host from the environment */
export function detectHost(forwardedHost: any) {
// If we detect a Vercel environment, we can trust the host
if (process.env.VERCEL) return forwardedHost
// If `NEXTAUTH_URL` is `undefined` we fall back to "http://localhost:3000"
return process.env.NEXTAUTH_URL
}

View File

@@ -0,0 +1,44 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface AuthentikProfile {
iss: string,
sub: string,
aud: string,
exp: number,
iat: number,
auth_time: number,
acr: string,
c_hash: string,
nonce: string,
at_hash: string,
email: string,
email_verified: boolean,
name: string,
given_name: string,
family_name: string,
preferred_username: string,
nickname: string,
groups: string[]
}
export default function Authentik<
P extends Record<string, any> = AuthentikProfile
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
return {
id: "authentik",
name: "Authentik",
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
type: "oauth",
authorization: { params: { scope: "openid email profile" } },
checks: ["pkce", "state"],
profile(profile) {
return {
id: profile.sub,
name: profile.name ?? profile.preferred_username,
email: profile.email,
image: profile.picture,
}
},
options,
}
}

View File

@@ -28,7 +28,7 @@ export default function AzureAD<P extends Record<string, any> = AzureADProfile>(
wellKnown: `https://login.microsoftonline.com/${tenant}/v2.0/.well-known/openid-configuration`,
authorization: {
params: {
scope: "User.Read",
scope: "openid profile email",
},
},
async profile(profile, tokens) {
@@ -41,13 +41,23 @@ export default function AzureAD<P extends Record<string, any> = AzureADProfile>(
},
}
)
const pictureBuffer = await profilePicture.arrayBuffer()
const pictureBase64 = Buffer.from(pictureBuffer).toString("base64")
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: `data:image/jpeg;base64, ${pictureBase64}`,
// Confirm that profile photo was returned
if (profilePicture.ok) {
const pictureBuffer = await profilePicture.arrayBuffer()
const pictureBase64 = Buffer.from(pictureBuffer).toString("base64")
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: `data:image/jpeg;base64, ${pictureBase64}`,
}
} else {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
}
}
},
options,

View File

@@ -15,6 +15,7 @@ export default function Cognito<P extends Record<string, any> = CognitoProfile>(
name: "Cognito",
type: "oauth",
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
idToken: true,
profile(profile) {
return {
id: profile.sub,

View File

@@ -1,20 +0,0 @@
/** @type {import(".").OAuthProvider} */
export default function FusionAuth(options) {
return {
id: "fusionauth",
name: "FusionAuth",
type: "oauth",
authorization: `${options.issuer}oauth2/authorize`,
token: `${options.issuer}oauth2/token`,
userinfo: `${options.issuer}oauth2/userinfo`,
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
}
},
options,
}
}

View File

@@ -0,0 +1,52 @@
import { OAuthConfig, OAuthUserConfig } from "./oauth"
/** This is the default openid signature returned from FusionAuth
* it can be customized using [lambda functions](https://fusionauth.io/docs/v1/tech/lambdas)
*/
export interface FusionAuthProfile {
aud: string
exp: number
iat: number
iss: string
sub: string
jti: string
authenticationType: string
email: string
email_verified: boolean
preferred_username: string
at_hash: string
c_hash: string
scope: string
sid: string
}
export default function FusionAuth<
P extends Record<string, any> = FusionAuthProfile
>(
// tenantId only needed if there is more than one tenant configured on the server
options: OAuthUserConfig<P> & { tenantId?: string }
): OAuthConfig<P> {
return {
id: "fusionauth",
name: "FusionAuth",
type: "oauth",
wellKnown: options?.tenantId
? `${options.issuer}/.well-known/openid-configuration?tenantId=${options.tenantId}`
: `${options.issuer}/.well-known/openid-configuration`,
authorization: {
params: {
scope: "openid offline_access",
...(options?.tenantId && { tenantId: options.tenantId }),
},
},
checks: ["pkce", "state"],
profile(profile) {
return {
id: profile.sub,
email: profile.email,
name: profile?.preferred_username,
}
},
options,
}
}

View File

@@ -1,10 +1,21 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface GoogleProfile {
sub: string
name: string
aud: string
azp: string
email: string
email_verified: boolean
exp: number
family_name: string
given_name: string
hd: string
iat: number
iss: string
jti: string
name: string
nbf: number
picture: string
sub: string
}
export default function Google<P extends Record<string, any> = GoogleProfile>(

View File

@@ -1,20 +0,0 @@
/** @type {import(".").OAuthProvider} */
export default function Kakao(options) {
return {
id: "kakao",
name: "Kakao",
type: "oauth",
authorization: "https://kauth.kakao.com/oauth/authorize",
token: "https://kauth.kakao.com/oauth/token",
userinfo: "https://kapi.kakao.com/v2/user/me",
profile(profile) {
return {
id: profile.id,
name: profile.kakao_account?.profile.nickname,
email: profile.kakao_account?.email,
image: profile.kakao_account?.profile.profile_image_url,
}
},
options,
}
}

92
src/providers/kakao.ts Normal file
View File

@@ -0,0 +1,92 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export type DateTime = string
export type Gender = "female" | "male"
export type AgeRange =
| "1-9"
| "10-14"
| "15-19"
| "20-29"
| "30-39"
| "40-49"
| "50-59"
| "60-69"
| "70-79"
| "80-89"
| "90-"
/**
* https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info
* type from : https://gist.github.com/ziponia/cdce1ebd88f979b2a6f3f53416b56a77
*/
export interface KakaoProfile {
id: number
has_signed_up?: boolean
connected_at?: DateTime
synched_at?: DateTime
properties?: {
id?: string
status?: string
registered_at?: DateTime
msg_blocked?: boolean
nickname?: string
profile_image?: string
thumbnail_image?: string
}
kakao_account?: {
profile_needs_agreement?: boolean
profile_nickname_needs_agreement?: boolean
profile_image_needs_agreement?: boolean
profile?: {
nickname?: string
thumbnail_image_url?: string
profile_image_url?: string
is_default_image?: boolean
}
name_needs_agreement?: boolean
name?: string
email_needs_agreement?: boolean
is_email_valid?: boolean
is_email_verified?: boolean
email?: string
age_range_needs_agreement?: boolean
age_range?: AgeRange
birthyear_needs_agreement?: boolean
birthyear?: string
birthday_needs_agreement?: boolean
birthday?: string
birthday_type?: string
gender_needs_agreement?: boolean
gender?: Gender
phone_number_needs_agreement?: boolean
phone_number?: string
ci_needs_agreement?: boolean
ci?: string
ci_authenticated_at?: DateTime
}
}
export default function Kakao<P extends Record<string, any> = KakaoProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "kakao",
name: "Kakao",
type: "oauth",
authorization: "https://kauth.kakao.com/oauth/authorize?scope",
token: "https://kauth.kakao.com/oauth/token",
userinfo: "https://kapi.kakao.com/v2/user/me",
client: {
token_endpoint_auth_method: "client_secret_post",
},
profile(profile) {
return {
id: profile.id,
name: profile.kakao_account?.profile.nickname,
email: profile.kakao_account?.email,
image: profile.kakao_account?.profile.profile_image_url,
}
},
options,
}
}

View File

@@ -1,18 +0,0 @@
/** @type {import(".").OAuthProvider} */
export default function Naver(options) {
return {
id: "naver",
name: "Naver",
type: "oauth",
authorization: "https://nid.naver.com/oauth2.0/authorize",
token: "https://nid.naver.com/oauth2.0/token",
userinfo: "https://openapi.naver.com/v1/nid/me",
profile(profile) {
// REVIEW: By default, we only want to expose the
// "id", "name", "email" and "image" fields.
return profile.response
},
checks: ["state"],
options,
}
}

42
src/providers/naver.ts Normal file
View File

@@ -0,0 +1,42 @@
import type { OAuthConfig, OAuthUserConfig } from "."
/** https://developers.naver.com/docs/login/profile/profile.md */
export interface NaverProfile {
resultcode: string
message: string
response: {
id: string
nickname?: string
name?: string
email?: string
gender?: "F" | "M" | "U"
age?: string
birthday?: string
profile_image?: string
birthyear?: string
mobile?: string
}
}
export default function Naver<P extends Record<string, any> = NaverProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "naver",
name: "Naver",
type: "oauth",
authorization: "https://nid.naver.com/oauth2.0/authorize",
token: "https://nid.naver.com/oauth2.0/token",
userinfo: "https://openapi.naver.com/v1/nid/me",
profile(profile) {
return {
id: profile.response.id,
name: profile.response.name,
email: profile.response.email,
image: profile.response.profile_image,
}
},
checks: ["state"],
options,
}
}

View File

@@ -43,6 +43,7 @@ export default function Okta<P extends Record<string, any> = OktaProfile>(
type: "oauth",
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
authorization: { params: { scope: "openid email profile" } },
idToken: true,
profile(profile) {
return {
id: profile.sub,

34
src/providers/patreon.ts Normal file
View File

@@ -0,0 +1,34 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface PatreonProfile {
sub: string
nickname: string
email: string
picture: string
}
export default function Patreon<P extends Record<string, any> = PatreonProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "patreon",
name: "Patreon",
type: "oauth",
version: "2.0",
authorization: {
url: "https://www.patreon.com/oauth2/authorize",
params: { scope: "identity identity[email]" },
},
token: "https://www.patreon.com/api/oauth2/token",
userinfo: "https://www.patreon.com/api/oauth2/api/current_user",
profile(profile) {
return {
id: profile.data.id,
name: profile.data.attributes.full_name,
email: profile.data.attributes.email,
image: profile.data.attributes.image_url,
}
},
options,
}
}

View File

@@ -4,9 +4,23 @@ export default function Strava(options) {
id: "strava",
name: "Strava",
type: "oauth",
authorization: "https://www.strava.com/api/v3/oauth/authorize?scope=read",
token: "https://www.strava.com/api/v3/oauth/token",
authorization: {
url: "https://www.strava.com/api/v3/oauth/authorize",
params: {
scope: "read",
approval_prompt: "auto",
response_type: "code",
redirect_uri: "http://localhost:3000/api/auth/callback/strava",
},
},
token: {
url: "https://www.strava.com/api/v3/oauth/token",
},
userinfo: "https://www.strava.com/api/v3/athlete",
client: {
token_endpoint_auth_method: "client_secret_post",
},
profile(profile) {
return {
id: profile.id,

56
src/providers/trakt.ts Normal file
View File

@@ -0,0 +1,56 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface TraktUser {
username: string
private: boolean
name: string
vip: boolean
vip_ep: boolean
ids: { slug: string }
joined_at: string
location: string | null
about: string | null
gender: string | null
age: number | null
images: { avatar: { full: string } }
}
export default function Trakt<
P extends Record<string, any> = TraktUser
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
return {
id: "trakt",
name: "Trakt",
type: "oauth",
authorization: {
url: "https://trakt.tv/oauth/authorize",
params: { scope: "" }, // when default, trakt returns auth error
},
token: "https://api.trakt.tv/oauth/token",
userinfo: {
async request(context) {
const res = await fetch("https://api.trakt.tv/users/me?extended=full", {
headers: {
Authorization: `Bearer ${context.tokens.access_token}`,
"trakt-api-version": "2",
"trakt-api-key": context.provider.clientId as string,
},
})
if (res.ok) return await res.json()
throw new Error("Expected 200 OK from the userinfo endpoint")
},
},
profile(profile) {
return {
id: profile.ids.slug,
name: profile.name,
email: null, // trakt does not provide user emails
image: profile.images.avatar.full, // trakt does not allow hotlinking
}
},
options,
}
}

View File

@@ -1,6 +1,6 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface TwitterProfile {
export interface TwitterLegacyProfile {
id: number
id_str: string
name: string
@@ -95,12 +95,12 @@ export interface TwitterProfile {
needs_phone_verification: boolean
}
export default function Twitter<P extends Record<string, any> = TwitterProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
export function TwitterLegacy<
P extends Record<string, any> = TwitterLegacyProfile
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
return {
id: "twitter",
name: "Twitter",
name: "Twitter (Legacy)",
type: "oauth",
version: "1.0A",
authorization: "https://api.twitter.com/oauth/authenticate",
@@ -122,3 +122,93 @@ export default function Twitter<P extends Record<string, any> = TwitterProfile>(
options,
}
}
/**
* [Documentation](https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me)
*/
export interface TwitterProfile {
data: {
id: string
name: string
username: string
location?: string
entities?: {
url: {
urls: Array<{
start: number
end: number
url: string
expanded_url: string
display_url: string
}>
}
description: {
hashtags: Array<{
start: number
end: number
tag: string
}>
}
}
verified?: boolean
description?: string
url?: string
profile_image_url?: string
protected?: boolean
pinned_tweet_id?: string
created_at?: string
}
includes?: {
tweets?: Array<{
id: string
text: string
}>
}
}
export default function Twitter<
P extends Record<string, any> = TwitterLegacyProfile | TwitterProfile
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
if (options.version === "2.0") {
return {
id: "twitter",
name: "Twitter",
version: "2.0",
type: "oauth",
authorization: {
url: "https://twitter.com/i/oauth2/authorize",
params: { scope: "users.read tweet.read offline.access" },
},
token: {
url: "https://api.twitter.com/2/oauth2/token",
// TODO: Remove this
async request({ client, params, checks, provider }) {
const response = await client.oauthCallback(
provider.callbackUrl,
params,
checks,
{ exchangeBody: { client_id: options.clientId } }
)
return { tokens: response }
},
},
userinfo: {
url: "https://api.twitter.com/2/users/me",
params: { "user.fields": "profile_image_url" },
},
profile({ data }) {
return {
id: data.id,
name: data.name,
// NOTE: E-mail is currently unsupported by OAuth 2 Twitter.
email: null,
image: data.profile_image_url,
}
},
checks: ["pkce", "state"],
options,
}
}
return TwitterLegacy(options)
}

View File

@@ -379,16 +379,18 @@ export function SessionProvider(props: SessionProviderProps) {
}, [])
React.useEffect(() => {
const { refetchOnWindowFocus = true } = props
// Listen for when the page is visible, if the user switches tabs
// and makes our tab visible again, re-fetch the session.
// and makes our tab visible again, re-fetch the session, but only if
// this feature is not disabled.
const visibilityHandler = () => {
if (document.visibilityState === "visible")
if (refetchOnWindowFocus && document.visibilityState === "visible")
__NEXTAUTH._getSession({ event: "visibilitychange" })
}
document.addEventListener("visibilitychange", visibilityHandler, false)
return () =>
document.removeEventListener("visibilitychange", visibilityHandler, false)
}, [])
}, [props.refetchOnWindowFocus])
React.useEffect(() => {
const { refetchInterval } = props

View File

@@ -70,4 +70,9 @@ export interface SessionProviderProps {
* If set to `0` (default), the session is not polled.
*/
refetchInterval?: number
/**
* `SessionProvider` automatically refetches the session when the user switches between windows.
* This option activates this behaviour if set to `true` (default).
*/
refetchOnWindowFocus?: boolean
}

4
tsconfig.eslint.json Normal file
View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["./*.d.ts", "**/tests", "**/__tests__"]
}