Compare commits

...

133 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
Balázs Orbán
1c7fe57edb fix: default to VERCEL_URL for callbackUrl 2021-12-08 17:43:49 +01:00
Balázs Orbán
59797bbdef fix: use VERCEL_URL by default for secureCookie (#3399) 2021-12-08 17:22:57 +01:00
Paul Büchner
9eb78a9de9 chore: fix typo in comment (#3388) 2021-12-08 03:07:26 +01:00
Balázs Orbán
2670bbb28f docs: match docs page wording for SECURITY.md 2021-12-06 21:05:41 +01:00
DmitryScaletta
0431c2a334 fix(ts): improve types for encode/decode functions (#3346)
* fix: improve types for encode/decode functions

* fix: use Awaitable type for encode/decode functions
2021-12-04 02:09:48 +01:00
Rraji Abdelbari
5ac688cc18 fix(providers): convert 42 School profile id to string (#3351) 2021-12-04 02:08:48 +01:00
Anthony Ringoet
8ea75f0c1c fix(ts): typo in Auth0Profile interface (#3347) 2021-12-04 02:06:23 +01:00
dnikomon
4dcdb62dca fix: remove nextauth from authorization params (#3332)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-12-02 19:43:42 +01:00
Nico Domino
1f4b7d8089 chore: add opencollective to package.json (#3333) 2021-12-02 12:50:14 +01:00
Balázs Orbán
fedb84872d docs: add top contributors to package.json 2021-12-01 16:44:20 +01:00
Balázs Orbán
c0dddfb77f docs: upgrade README 2021-12-01 16:40:21 +01:00
Balázs Orbán
50fe115df6 Release v4 2021-12-01 16:32:35 +01:00
Jameel Khan
cc17ddf8aa fix: Fallback to --color-text when no color-brand (#3313) 2021-12-01 15:01:11 +01:00
Balázs Orbán
8644e553ed Merge branch 'main' into beta 2021-11-30 19:20:56 +01:00
Nisala Kalupahana
d1d0db43ea feat(providers): ensure GitHub provider always gives an email (#3302)
* Ensure that GitHub provider always gives an email

* Update src/providers/github.js

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

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-11-29 23:58:49 +01:00
Balázs Orbán
b01f6805d3 chore(providers): TS improvements (#3295) 2021-11-28 17:52:56 +01:00
Balázs Orbán
c44b860b9e feat(providers): refactor Apple provider (#2875)
* chore: remove legacy code

* fix(providers): refactor Apple provider

* chore(dev): add Apple provider

* docs(providers): add `generateClientSecret` to JSDoc

* fix(providers): use `jose@4`

* fix(providers): use seconds since epoch, correct sign

* chore(providers): move secret generator into a script
2021-11-28 17:52:24 +01:00
Khánh Hoàng
22f74d7c4d fix(providers): correct authorization url for Atlassian (#2999)
* fix(provider): correct authorization for Atlassian

* feat(providers): use wellKnown for better configuration

* fix(atlassian): switch back to raw config

* fix(providers): pass generic to `OAuthUserConfig`

Co-authored-by: Lluis Agusti <hi@llu.lu>
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-11-28 15:49:24 +01:00
Balázs Orbán
2570168660 fix: add custom error message when session required (#3288) 2021-11-28 15:38:02 +01:00
Balázs Orbán
187a1474f5 feat(oauth): expose httpOptions (#3287) 2021-11-26 23:40:58 +01:00
Kevin McKernan
4dc76749f2 fix(providers): Rewrite EVEOnline in TS, fix default scopes (#2759)
* refactor EVEOnlineProvider into typescript, fix default scopes

* Update src/providers/eveonline.ts

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

* update to new OIDC SSO endpoints

* set idToken: true

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-11-26 19:20:40 +01:00
Torben
35ee608d59 feat(providers): add Osu! provider (#3234) 2021-11-20 14:49:51 +01:00
Estevan Jantsk
0f132de115 feat(providers): add Pipedrive provider (#3011)
* Add Pipedrive as a provider

* convert pipedrive provider to ts

* remove others interface

* refactor(pipedrive): run prettier

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-11-17 23:07:29 +01:00
Balázs Orbán
31426b9435 fix(providers): match filename with 42 Provider's id (#3225) 2021-11-17 23:03:56 +01:00
Balázs Orbán
64b2a2c43b fix: assert action when req.query isn't available (#3222)
* fix: assert `action` if `req.query` unavailable

* refactor: make `method` externally optional
2021-11-17 22:47:12 +01:00
Balázs Orbán
7beb3ff03b refactor(providers): cleanup 42 (#3221) 2021-11-17 10:15:59 +01:00
Richard van der Dys
432876c011 fix(providers): refactor Zoom
* Added support for zoom in beta

* Converted to typescript

* rename

* Now reflects response from Zoom

* chore: Prettier

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-11-16 23:37:03 +01:00
Balázs Orbán
15d1fab4c8 fix: correct assertion when Credentials only (#3217) 2021-11-16 23:16:12 +01:00
Rraji Abdelbari
5e803cd34c refactor(providers): convert 42 to TypeScript (#3211) 2021-11-16 21:57:53 +01:00
Kovacs Nicolas
78fa33312f docs(readme): opencollective domain (#3066)
I had 502 using `opencollective.org` for some time, also, the correct domain looks like `opencollective.com`
2021-11-04 08:16:30 +01:00
Tania
932d05da70 docs: mention other repos in readme and issue forms (#2989)
* Update bug_report.yaml

Add information about distributing issues to the correct repo

* fix yaml syntax

* remove new line

* improve content

* Import content

* remove one emoji

* Update feature_request.yaml

* Update README.md
2021-10-22 09:40:18 +02:00
Gianluca
e8a58a01b6 docs(contributing): fixed numeration type (#2624)
There was a numeration type error in the "For  contributors" section
2021-08-29 11:30:05 +02:00
Nico Domino
91de463a5e docs(providers): add tip about async provider code (#2443) 2021-08-26 23:45:07 +02:00
Nico Domino
4a9d871698 docs(www): add more algolia no-result terms (#2442) 2021-08-26 23:41:49 +02:00
Alex Vilchis
c2119b15de chore(docs): fix dependency name (#2607) 2021-08-26 19:42:20 +02:00
Alex Vilchis
0ce15c4a18 docs: Fix grammar (#2602) 2021-08-25 19:48:14 +02: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
85 changed files with 5187 additions and 7670 deletions

View File

@@ -10,7 +10,18 @@ body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Please provide the following information:
Thanks for taking the time to fill out this bug report!
### Important :exclamation:
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
If you are in the correct repo, then proceed by providing the following information:
- type: textarea
id: description
attributes:
@@ -44,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

View File

@@ -9,8 +9,14 @@ body:
- type: markdown
attributes:
value: |
Thank you very much for reaching out to us regarding the awesome feature that you believe should be included in the NextAuth.js library. Please provide the following information:
Thank you very much for reaching out to us regarding the awesome feature that you believe should be included in the NextAuth.js library.
### 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:
- Docs related: https://github.com/nextauthjs/docs
- Adapter related: https://github.com/nextauthjs/adapters
If you are in the correct repo, then proceed by providing the following information:
- type: textarea
id: description
attributes:
@@ -65,4 +71,3 @@ body:
attributes:
value: |
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚

7
.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
@@ -43,6 +45,8 @@ app/package-lock.json
app/yarn.lock
app/prisma/migrations
app/prisma/dev.db*
app/dist
app/next-auth
# VS
/.vs/slnx.sqlite-journal
@@ -50,6 +54,9 @@ app/prisma/dev.db*
/.vs
.vscode
# Jetbrains
.idea
# GitHub Actions runner
/actions-runner
/_work

View File

@@ -32,6 +32,11 @@ NextAuth.js is a complete open source authentication solution for [Next.js](http
It is designed from the ground up to support Next.js and Serverless.
This is the core repo for NextAuth.js. Check the repos below if you are interested in additional information:
- Docs related: https://github.com/nextauthjs/docs
- Adapter related: https://github.com/nextauthjs/adapters
## Getting Started
```
@@ -49,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
@@ -81,7 +86,8 @@ Advanced options allow you to define your own routines to handle controlling wha
### TypeScript
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.
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.
## Example
@@ -90,21 +96,24 @@ NextAuth.js comes with built-in types. For more information and usage, check out
```javascript
// pages/api/auth/[...nextauth].js
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import AppleProvider from "next-auth/providers/apple"
import GoogleProvider from "next-auth/providers/google"
import EmailProvider from "next-auth/providers/email"
export default NextAuth({
secret: process.env.SECRET,
providers: [
// OAuth authentication providers
Providers.Apple({
AppleProvider({
clientId: process.env.APPLE_ID,
clientSecret: process.env.APPLE_SECRET,
}),
Providers.Google({
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
// Sign in with passwordless email link
Providers.Email({
EmailProvider({
server: process.env.MAIL_SERVER,
from: "<no-reply@example.com>",
}),
@@ -140,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"
@@ -170,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>
@@ -219,7 +228,8 @@ 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).
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/main/CONTRIBUTING.md).
## License

View File

@@ -2,12 +2,6 @@
NextAuth.js practices responsible disclosure.
## Supported Versions
Security updates are only released for the current version.
Old releases are not maintained and do not receive updates.
## Reporting a Vulnerability
We request that you contact us directly to report serious issues that might impact the security of sites using NextAuth.js.
@@ -19,6 +13,12 @@ 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 contacting us via email at me@iaincollins.com or info@balazsorban.com and yo@ndo.dev.
The best way to report an issue is by contacting us via email at info@balazsorban.com or me@iaincollins.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. (Please do not disclose sensitive details publicly at this stage.)
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.
> For less serious issues (e.g. RFC compliance for unsupported flows or potential issues that may cause a problem in the future) 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.
## Supported Versions
Security updates are only released for the current version.
Old releases are not maintained and do not receive updates.

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

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

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"
@@ -23,6 +25,10 @@ import CognitoProvider from "next-auth/providers/cognito"
import SlackProvider from "next-auth/providers/slack"
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"
@@ -69,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,
@@ -167,8 +179,23 @@ export const authOptions: NextAuthOptions = {
tenantId: process.env.AZURE_B2C_TENANT_ID,
primaryUserFlow: process.env.AZURE_B2C_PRIMARY_USER_FLOW,
}),
OsuProvider({
clientId: process.env.OSU_CLIENT_ID,
clientSecret: process.env.OSU_CLIENT_SECRET,
}),
AppleProvider({
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

@@ -4,7 +4,7 @@ body {
max-width: 680px;
margin: 0 auto;
background: #fff;
color: #333;
color: var(--color-text);
}
li,

View File

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

10663
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,11 @@
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",
"author": "Iain Collins <me@iaincollins.com>",
"contributors": [
"Balázs Orbán <info@balazsorban.com>",
"Nico Domino <yo@ndo.dev>",
"Lluis Agusti <hi@llu.lu>"
],
"main": "index.js",
"module": "index.js",
"types": "index.d.ts",
@@ -26,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",
@@ -56,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": {
@@ -82,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": {
@@ -137,7 +142,7 @@
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
"project": "./tsconfig.eslint.json"
},
"extends": [
"standard-with-typescript",
@@ -181,6 +186,11 @@
}
]
},
"eslintIgnore": [
"./*.d.ts",
"**/tests",
"**/__tests__"
],
"release": {
"branches": [
"+([0-9])?(.{+([0-9]),x}).x",
@@ -211,6 +221,10 @@
{
"type": "github",
"url": "https://github.com/sponsors/balazsorban44"
},
{
"type": "opencollective",
"url": "https://opencollective.com/nextauth"
}
]
}

View File

@@ -12,8 +12,8 @@ import type { ErrorType } from "./pages/error"
export interface IncomingRequest {
/** @default "http://localhost:3000" */
host: string
method: string
host?: string
method?: string
cookies?: Record<string, string>
headers?: Record<string, any>
query?: Record<string, any>
@@ -67,7 +67,7 @@ export async function NextAuthHandler<
return render.error({ error: "configuration" })
}
const { action, providerId, error } = req
const { action, providerId, error, method = "GET" } = req
const { options, cookies } = await init({
userOptions,
@@ -77,7 +77,7 @@ export async function NextAuthHandler<
callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl,
csrfToken: req.body?.csrfToken,
cookies: req.cookies,
isPost: req.method === "POST",
isPost: method === "POST",
})
const sessionStore = new SessionStore(
@@ -86,14 +86,17 @@ export async function NextAuthHandler<
options.logger
)
if (req.method === "GET") {
if (method === "GET") {
const render = renderPage({ ...options, query: req.query, cookies })
const { pages } = options
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" }],
@@ -119,9 +122,9 @@ export async function NextAuthHandler<
const callback = await routes.callback({
body: req.body,
query: req.query,
method: req.method,
headers: req.headers,
cookies: req.cookies,
method,
options,
sessionStore,
})
@@ -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,10 +156,19 @@ 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:
}
} else if (req.method === "POST") {
} else if (method === "POST") {
switch (action) {
case "signin":
// Verified CSRF Token required for all sign in routes
@@ -201,9 +204,9 @@ export async function NextAuthHandler<
const callback = await routes.callback({
body: req.body,
query: req.query,
method: req.method,
headers: req.headers,
cookies: req.cookies,
method,
options,
sessionStore,
})
@@ -228,6 +231,6 @@ export async function NextAuthHandler<
return {
status: 400,
body: `Error: Action ${action} with HTTP ${req.method} is not supported by NextAuth.js` as any,
body: `Error: Action ${action} with HTTP ${method} is not supported by NextAuth.js` as any,
}
}

View File

@@ -100,7 +100,7 @@ export async function init({
// Callback functions
callbacks: { ...defaultCallbacks, ...userOptions.callbacks },
logger,
callbackUrl: process.env.NEXTAUTH_URL ?? "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.
@@ -27,7 +29,8 @@ export function assertConfig(
): ConfigError | WarningCode | undefined {
const { options, req } = params
if (!req.query?.nextauth) {
// req.query isn't defined when asserting `getServerSession` for example
if (!req.query?.nextauth && !req.action) {
return new MissingAPIRoute(
"Cannot find [...nextauth].{js,ts} in `/pages/api/auth`. Make sure the filename is written correctly."
)
@@ -44,18 +47,20 @@ 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"
const onlyCredentials = !options.providers.some(
(p) => p.type !== "credentials"
)
if (dbStrategy || onlyCredentials) {
if (dbStrategy && onlyCredentials) {
return new UnsupportedStrategy(
"Signin in with credentials only supported if JWT strategy is enabled"
)
@@ -74,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"
@@ -15,7 +16,7 @@ export default async function oAuthCallback(params: {
options: InternalOptions<"oauth">
query: IncomingRequest["query"]
body: IncomingRequest["body"]
method: IncomingRequest["method"]
method: Required<IncomingRequest>["method"]
cookies: IncomingRequest["cookies"]
}): Promise<GetProfileResult & { cookies?: OutgoingResponse["cookies"] }> {
const { options, query, body, method, cookies } = params
@@ -137,10 +138,6 @@ export default async function oAuthCallback(params: {
})
}
// If a user object is supplied (e.g. Apple provider) add it to the profile object
// TODO: Remove/extract to Apple provider?
profile.user = JSON.parse(body?.user ?? query?.user ?? null)
const profileResult = await getProfile({
profile,
provider,

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,12 +46,5 @@ export async function openidClient(
// and https://github.com/nextauthjs/next-auth/issues/3067
client[custom.clock_tolerance] = 10
// https://github.com/nextauthjs/next-auth/discussions/3186
if (typeof provider.httpOptions?.timeout === "number") {
custom.setHttpOptionsDefaults({
timeout: provider.httpOptions.timeout,
})
}
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">
<style
dangerouslySetInnerHTML={{
__html: `
{theme?.brandColor && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--brand-color: ${theme?.brandColor};
--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,9 +65,10 @@ 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.",
default: "Unable to sign in.",
}
@@ -58,15 +76,17 @@ export default function SigninPage(props: SignInServerPageParams) {
return (
<div className="signin">
<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 && (
@@ -128,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

@@ -12,7 +12,7 @@ export default function SignoutPage(props: SignoutProps) {
return (
<div className="signout">
<style
{ theme.brandColor && <style
dangerouslySetInnerHTML={{
__html: `
:root {
@@ -20,7 +20,7 @@ export default function SignoutPage(props: SignoutProps) {
}
`,
}}
/>
/> }
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
<div className="card">
<h1>Signout</h1>

View File

@@ -11,7 +11,7 @@ export default function VerifyRequestPage(props: VerifyRequestPageProps) {
return (
<div className="verify-request">
<style
{ theme.brandColor && <style
dangerouslySetInnerHTML={{
__html: `
:root {
@@ -19,7 +19,7 @@ export default function VerifyRequestPage(props: VerifyRequestPageProps) {
}
`,
}}
/>
/> }
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
<div className="card">
<h1>Check your email</h1>

View File

@@ -11,7 +11,7 @@ import type { User } from "../.."
export default async function callback(params: {
options: InternalOptions<"oauth" | "credentials" | "email">
query: IncomingRequest["query"]
method: IncomingRequest["method"]
method: Required<IncomingRequest>["method"]
body: IncomingRequest["body"]
headers: IncomingRequest["headers"]
cookies: IncomingRequest["cookies"]

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**!
*
@@ -298,7 +299,7 @@ export interface CallbacksOptions<
* 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)
* ⚠ By default, only a subset (email, name, image)
* of the token is returned for increased security.
*
* If you want to make something available you added to the token through the `jwt` callback,

View File

@@ -69,7 +69,7 @@ label {
text-align: left;
margin-bottom: 0.25rem;
display: block;
color: #666;
color: var(--color-text);
}
input[type] {
@@ -258,5 +258,5 @@ a.site {
}
.section-header {
color: var(--brand-color);
color: var(--brand-color, var(--color-text));
}

View File

@@ -3,7 +3,7 @@ import hkdf from "@panva/hkdf"
import { v4 as uuid } from "uuid"
import { SessionStore } from "../core/lib/cookie"
import type { NextApiRequest } from "next"
import type { JWT, JWTDecodeParams, JWTEncodeParams } from "./types"
import type { JWT, JWTDecodeParams, JWTEncodeParams, JWTOptions } from "./types"
import type { LoggerInstance } from ".."
export * from "./types"
@@ -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,8 +50,12 @@ export interface GetTokenParams<R extends boolean = false> {
* @default false
*/
raw?: R
secret: string
decode?: typeof decode
/**
* The same `secret` used in the `NextAuth` configuration.
* Defaults to the `NEXTAUTH_SECRET` environment variable.
*/
secret?: string
decode?: JWTOptions["decode"]
logger?: LoggerInstance | Console
}
@@ -70,16 +69,15 @@ export async function getToken<R extends boolean = false>(
): Promise<R extends true ? string : JWT | null> {
const {
req,
secureCookie = !(
!process.env.NEXTAUTH_URL ||
process.env.NEXTAUTH_URL.startsWith("http://")
),
secureCookie = process.env.NEXTAUTH_URL?.startsWith("https://") ??
!!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()")
@@ -90,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
@@ -99,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

@@ -1,4 +1,4 @@
import { decode, encode } from "."
import type { Awaitable } from ".."
export interface DefaultJWT extends Record<string, unknown> {
name?: string | null
@@ -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.
@@ -42,9 +46,9 @@ export interface JWTOptions {
*/
maxAge: number
/** Override this method to control the NextAuth.js issued JWT encoding. */
encode: typeof encode
encode: (params: JWTEncodeParams) => Awaitable<string>
/** Override this method to control the NextAuth.js issued JWT decoding. */
decode: typeof decode
decode: (params: JWTDecodeParams) => Awaitable<JWT | null>
}
export type Secret = string | Buffer

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,
@@ -18,17 +18,22 @@ async function NextAuthNextHandler(
res: NextApiResponse,
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) as string,
host: detectHost(req.headers["x-forwarded-host"]),
body: req.body,
query: req.query,
query,
cookies: req.cookies,
headers: req.headers,
method: req.method ?? "GET",
action: req.query.nextauth[0] as NextAuthAction,
providerId: req.query.nextauth[1],
error: (req.query.error as string | undefined) ?? req.query.nextauth[1],
method: req.method,
action: nextauth?.[0] as NextAuthAction,
providerId: nextauth?.[1],
error: (req.query.error as string | undefined) ?? nextauth?.[1],
},
options,
})
@@ -86,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,
@@ -107,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
}

179
src/providers/42-school.ts Normal file
View File

@@ -0,0 +1,179 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface UserData {
id: number
email: string
login: string
first_name: string
last_name: string
usual_full_name: null | string
usual_first_name: null | string
url: string
phone: "hidden" | string | null
displayname: string
image_url: string | null
"staff?": boolean
correction_point: number
pool_month: string | null
pool_year: string | null
location: string | null
wallet: number
anonymize_date: string
created_at: string
updated_at: string | null
alumni: boolean
"is_launched?": boolean
}
export interface CursusUser {
grade: string | null
level: number
skills: Array<{ id: number; name: string; level: number }>
blackholed_at: string | null
id: number
begin_at: string | null
end_at: string | null
cursus_id: number
has_coalition: boolean
created_at: string
updated_at: string | null
user: UserData
cursus: { id: number; created_at: string; name: string; slug: string }
}
export interface ProjectUser {
id: number
occurrence: number
final_mark: number | null
status: "in_progress" | "finished"
"validated?": boolean | null
current_team_id: number
project: {
id: number
name: string
slug: string
parent_id: number | null
}
cursus_ids: number[]
marked_at: string | null
marked: boolean
retriable_at: string | null
created_at: string
updated_at: string | null
}
export interface Achievement {
id: number
name: string
description: string
tier: "none" | "easy" | "medium" | "hard" | "challenge"
kind: "scolarity" | "project" | "pedagogy" | "scolarity"
visible: boolean
image: string | null
nbr_of_success: number | null
users_url: string
}
export interface LanguagesUser {
id: number
language_id: number
user_id: number
position: number
created_at: string
}
export interface TitlesUser {
id: number
user_id: number
title_id: number
selected: boolean
created_at: string
updated_at: string | null
}
export interface ExpertisesUser {
id: number
expertise_id: number
interested: boolean
value: number
contact_me: boolean
created_at: string
user_id: number
}
export interface Campus {
id: number
name: string
time_zone: string
language: {
id: number
name: string
identifier: string
created_at: string
updated_at: string | null
}
users_count: number
vogsphere_id: number
country: string
address: string
zip: string
city: string
website: string
facebook: string
twitter: string
active: boolean
email_extension: string
default_hidden_phone: boolean
}
export interface CampusUser {
id: number
user_id: number
campus_id: number
is_primary: boolean
created_at: string
updated_at: string | null
}
export interface FortyTwoProfile extends UserData {
groups: Array<{ id: string; name: string }>
cursus_users: CursusUser[]
projects_users: ProjectUser[]
languages_users: LanguagesUser[]
achievements: Achievement[]
titles: Array<{ id: string; name: string }>
titles_users: TitlesUser[]
partnerships: any[]
patroned: any[]
patroning: any[]
expertises_users: ExpertisesUser[]
roles: Array<{ id: string; name: string }>
campus: Campus[]
campus_users: CampusUser[]
user: any | null
}
export default function FortyTwo<
P extends Record<string, any> = FortyTwoProfile
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
return {
id: "42-school",
name: "42 School",
type: "oauth",
authorization: {
url: "https://api.intra.42.fr/oauth/authorize",
params: { scope: "public" },
},
token: "https://api.intra.42.fr/oauth/token",
userinfo: "https://api.intra.42.fr/v2/me",
profile(profile) {
return {
id: profile.id.toString(),
name: profile.usual_full_name,
email: profile.email,
image: profile.image_url,
}
},
options,
}
}

View File

@@ -1,23 +0,0 @@
/** @type {import(".").OAuthProvider} */
export default function FortyTwo(options) {
return {
id: "42-school",
name: "42 School",
type: "oauth",
authorization: {
url: "https://api.intra.42.fr/oauth/authorize",
params: { scope: "public" },
},
token: "https://api.intra.42.fr/oauth/token",
userinfo: "https://api.intra.42.fr/v2/me",
profile(profile) {
return {
id: profile.id,
name: profile.usual_full_name,
email: profile.email,
image: profile.image_url,
}
},
options,
}
}

View File

@@ -1,37 +0,0 @@
/** @type {import(".").OAuthProvider} */
export default function Apple(options) {
return {
id: "apple",
name: "Apple",
type: "oauth",
authorization: {
url: "https://appleid.apple.com/auth/authorize",
params: {
scope: "name email",
response_type: "code",
id_token: "",
response_mode: "form_post",
},
},
token: {
url: "https://appleid.apple.com/auth/token",
idToken: true,
},
jwks_endpoint: "https://appleid.apple.com/auth/keys",
profile(profile) {
// The name of the user will only be returned on first login
const name = profile.user
? profile.user.name.firstName + " " + profile.user.name.lastName
: null
return {
id: profile.sub,
name,
email: profile.email,
image: null,
}
},
checks: ["none"], // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
options,
}
}

122
src/providers/apple.ts Normal file
View File

@@ -0,0 +1,122 @@
import { OAuthConfig, OAuthUserConfig } from "."
/**
* See more at:
* [Retrieve the User's Information from Apple ID Servers
](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple#3383773)
*/
export interface AppleProfile {
/**
* The issuer registered claim identifies the principal that issued the identity token.
* Since Apple generates the token, the value is `https://appleid.apple.com`.
*/
iss: "https://appleid.apple.com"
/**
* The audience registered claim identifies the recipient for which the identity token is intended.
* Since the token is meant for your application, the value is the `client_id` from your developer account.
*/
aud: string
/**
* The issued at registered claim indicates the time at which Apple issued the identity token,
* in terms of the number of seconds since Epoch, in UTC.
*/
iat: number
/**
* The expiration time registered identifies the time on or after which the identity token expires,
* in terms of number of seconds since Epoch, in UTC.
* The value must be greater than the current date/time when verifying the token.
*/
exp: number
/**
* The subject registered claim identifies the principal that's the subject of the identity token.
* Since this token is meant for your application, the value is the unique identifier for the user.
*/
sub: string
/**
* A String value used to associate a client session and the identity token.
* This value mitigates replay attacks and is present only if passed during the authorization request.
*/
nonce: string
/**
* A Boolean value that indicates whether the transaction is on a nonce-supported platform.
* If you sent a nonce in the authorization request but don't see the nonce claim in the identity token,
* check this claim to determine how to proceed.
* If this claim returns true, you should treat nonce as mandatory and fail the transaction;
* otherwise, you can proceed treating the nonce as options.
*/
nonce_supported: boolean
/**
* A String value representing the user's email address.
* The email address is either the user's real email address or the proxy address,
* depending on their status private email relay service.
*/
email: string
/**
* A String or Boolean value that indicates whether the service has verified the email.
* The value of this claim is always true, because the servers only return verified email addresses.
* The value can either be a String (`"true"`) or a Boolean (`true`).
*/
email_verified: "true" | true
/**
* A String or Boolean value that indicates whether the email shared by the user is the proxy address.
* The value can either be a String (`"true"` or `"false"`) or a Boolean (`true` or `false`).
*/
is_private_email: boolean | "true" | "false"
/**
* An Integer value that indicates whether the user appears to be a real person.
* Use the value of this claim to mitigate fraud. The possible values are: 0 (or Unsupported), 1 (or Unknown), 2 (or LikelyReal).
* For more information, see [`ASUserDetectionStatus`](https://developer.apple.com/documentation/authenticationservices/asuserdetectionstatus).
* This claim is present only on iOS 14 and later, macOS 11 and later, watchOS 7 and later, tvOS 14 and later;
* the claim isn't present or supported for web-based apps.
*/
real_user_status: 0 | 1 | 2
/**
* A String value representing the transfer identifier used to migrate users to your team.
* This claim is present only during the 60-day transfer period after an you transfer an app.
* For more information, see [Bringing New Apps and Users into Your Team](https://developer.apple.com/documentation/sign_in_with_apple/bringing_new_apps_and_users_into_your_team).
*/
transfer_sub: string
at_hash: string
auth_time: number
}
export default function Apple<P extends Record<string, any> = AppleProfile>(
options: Omit<OAuthUserConfig<P>, "clientSecret"> & {
/**
* Apple requires the client secret to be a JWT. You can generate one using the following script:
* https://bal.so/apple-gen-secret
*
* Read more: [Creating the Client Secret
](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048)
*/
clientSecret: string
}
): OAuthConfig<P> {
return {
id: "apple",
name: "Apple",
type: "oauth",
wellKnown: "https://appleid.apple.com/.well-known/openid-configuration",
authorization: {
params: { scope: "name email", response_mode: "form_post" },
},
idToken: true,
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: null,
}
},
checks: ["pkce"],
options,
}
}

View File

@@ -1,11 +1,21 @@
/** @type {import(".").OAuthProvider} */
export default function Atlassian(options) {
import type { OAuthConfig, OAuthUserConfig } from "."
interface AtlassianProfile {
account_id: string
name: string
email: string
picture: string
}
export default function Atlassian<P extends AtlassianProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "atlassian",
name: "Atlassian",
type: "oauth",
authorization: {
url: "https://auth.atlassian.com/oauth/authorize",
url: "https://auth.atlassian.com/authorize",
params: {
audience: "api.atlassian.com",
prompt: "consent",

View File

@@ -1,8 +1,8 @@
import { OAuthConfig, OAuthUserConfig } from "./oauth"
import type { OAuthConfig, OAuthUserConfig } from "."
export interface Auth0Profile {
sub: string
nicname: string
nickname: string
email: string
picture: string
}

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

@@ -1,4 +1,4 @@
import { OAuthConfig, OAuthUserConfig } from "."
import type { OAuthConfig, OAuthUserConfig } from "."
export interface AzureB2CProfile {
exp: number

View File

@@ -1,4 +1,4 @@
import { OAuthConfig, OAuthUserConfig } from "./oauth"
import type { OAuthConfig, OAuthUserConfig } from "."
export interface AzureADProfile {
sub: string
@@ -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

@@ -1,4 +1,4 @@
import { OAuthConfig, OAuthUserConfig } from "."
import type { OAuthConfig, OAuthUserConfig } from "."
export interface CognitoProfile {
sub: string
@@ -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,6 +1,6 @@
import { IncomingRequest } from "src/core"
import { CommonProviderOptions } from "."
import { User, Awaitable } from ".."
import type { IncomingRequest } from "../core"
import type { CommonProviderOptions } from "."
import type { User, Awaitable } from ".."
export interface CredentialInput {
label?: string

View File

@@ -1,8 +1,8 @@
import { createTransport } from "nodemailer"
import { CommonProviderOptions } from "."
import { Options as SMTPConnectionOptions } from "nodemailer/lib/smtp-connection"
import { Awaitable } from ".."
import type { CommonProviderOptions } from "."
import type { Options as SMTPConnectionOptions } from "nodemailer/lib/smtp-connection"
import type { Awaitable } from ".."
export interface EmailConfig extends CommonProviderOptions {
type: "email"

View File

@@ -1,20 +0,0 @@
/** @type {import(".").OAuthProvider} */
export default function EVEOnline(options) {
return {
id: "eveonline",
name: "EVE Online",
type: "oauth",
authorization: "https://login.eveonline.com/oauth/authorize",
token: "https://login.eveonline.com/oauth/token",
userinfo: "https://login.eveonline.com/oauth/verify",
profile(profile) {
return {
id: profile.CharacterID,
name: profile.CharacterName,
email: null,
image: `https://image.eveonline.com/Character/${profile.CharacterID}_128.jpg`,
}
},
options,
}
}

View File

@@ -0,0 +1,38 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface EVEOnlineProfile {
CharacterID: number
CharacterName: string
ExpiresOn: string
Scopes: string
TokenType: string
CharacterOwnerHash: string
IntellectualProperty: string
}
export default function EVEOnline<
P extends Record<string, any> = EVEOnlineProfile
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
return {
id: "eveonline",
name: "EVE Online",
type: "oauth",
wellKnown:
"https://login.eveonline.com/.well-known/oauth-authorization-server",
authorization: {
params: {
scope: "publicData",
},
},
idToken: true,
profile(profile) {
return {
id: profile.CharacterID,
name: profile.CharacterName,
email: null,
image: `https://image.eveonline.com/Character/${profile.CharacterID}_128.jpg`,
}
},
options,
}
}

View File

@@ -1,7 +1,6 @@
import { Profile } from ".."
import { OAuthConfig, OAuthUserConfig } from "./oauth"
import type { OAuthConfig, OAuthUserConfig } from "."
export interface FacebookProfile extends Profile {
export interface FacebookProfile {
id: string
picture: { data: { url: string } }
}

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

@@ -6,7 +6,31 @@ export default function GitHub(options) {
type: "oauth",
authorization: "https://github.com/login/oauth/authorize?scope=read:user+user:email",
token: "https://github.com/login/oauth/access_token",
userinfo: "https://api.github.com/user",
userinfo: {
url: "https://api.github.com/user",
async request({ client, tokens }) {
// Get base profile
const profile = await client.userinfo(tokens)
// If user has email hidden, get their primary email from the GitHub API
if (!profile.email) {
const emails = await (
await fetch("https://api.github.com/user/emails", {
headers: { Authorization: `token ${tokens.access_token}` },
})
).json()
if (emails?.length > 0) {
// Get primary email
profile.email = emails.find(email => email.primary)?.email;
// And if for some reason it doesn't exist, just use the first
if (!profile.email) profile.email = emails[0].email;
}
}
return profile
},
},
profile(profile) {
return {
id: profile.id.toString(),

View File

@@ -1,10 +1,21 @@
import { OAuthConfig, OAuthUserConfig } from "./oauth"
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,8 +1,8 @@
import { OAuthConfig, OAuthProvider, OAuthProviderType } from "./oauth"
import type { OAuthConfig, OAuthProvider, OAuthProviderType } from "./oauth"
import { EmailConfig, EmailProvider, EmailProviderType } from "./email"
import type { EmailConfig, EmailProvider, EmailProviderType } from "./email"
import {
import type {
CredentialsConfig,
CredentialsProvider,
CredentialsProviderType,

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,4 +1,4 @@
import { OAuthConfig, OAuthUserConfig } from "."
import type { OAuthConfig, OAuthUserConfig } from "."
export interface KeycloakProfile {
exp: number

View File

@@ -1,20 +1,20 @@
import { OAuthConfig, OAuthUserConfig } from "."
import type { OAuthConfig, OAuthUserConfig } from "."
export interface LineProfile {
iss: string;
sub: string;
aud: string;
exp: number;
iat: number;
amr: string[];
name: string;
picture: string;
user: any;
iss: string
sub: string
aud: string
exp: number
iat: number
amr: string[]
name: string
picture: string
user: any
}
export default function LINE<
P extends Record<string, any> = LineProfile
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
export default function LINE<P extends Record<string, any> = LineProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "line",
name: "LINE",
@@ -33,6 +33,6 @@ export default function LINE<
client: {
id_token_signed_response_alg: "HS256",
},
options
options,
}
}

View File

@@ -1,4 +1,4 @@
import { OAuthConfig, OAuthUserConfig } from "."
import type { OAuthConfig, OAuthUserConfig } from "."
interface Identifier {
identifier: string

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

@@ -1,5 +1,5 @@
import { CommonProviderOptions } from "../providers"
import { Profile, TokenSet, User, Awaitable } from ".."
import type { CommonProviderOptions } from "../providers"
import type { Profile, TokenSet, User, Awaitable } from ".."
import type {
AuthorizationParameters,
@@ -115,10 +115,7 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
client?: Partial<ClientMetadata>
jwks?: { keys: JWK[] }
clientId?: string
clientSecret?:
| string
// TODO: only allow for Apple
| Record<"appleId" | "teamId" | "privateKey" | "keyId", string>
clientSecret?: string
/**
* If set to `true`, the user information will be extracted
* from the `id_token` claims, instead of
@@ -134,7 +131,7 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
// TODO: only allow for some
issuer?: string
/** Read more at: https://github.com/panva/node-openid-client/tree/main/docs#customizing-http-requests */
httpOptions?: Pick<HttpOptions, "timeout">
httpOptions?: HttpOptions
/**
* The options provided by the user.

View File

@@ -1,4 +1,4 @@
import { OAuthConfig, OAuthUserConfig } from "."
import type { OAuthConfig, OAuthUserConfig } from "."
export interface OktaProfile {
iss: string
@@ -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,

77
src/providers/osu.ts Normal file
View File

@@ -0,0 +1,77 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface OsuUserCompact {
avatar_url: string
country_code: string
default_group: string
id: string
is_active: boolean
is_bot: boolean
is_deleted: boolean
is_online: boolean
is_supporter: boolean
last_visit: Date | null
pm_friends_only: boolean
profile_colour: string | null
username: string
}
export interface OsuProfile extends OsuUserCompact {
discord: string | null
has_supported: boolean
interests: string | null
join_date: Date
kudosu: {
available: number
total: number
}
location: string | null
max_blocks: number
max_friends: number
occupation: string | null
playmode: string
playstyle: string[]
post_count: number
profile_order: string[]
title: string | null
title_url: string | null
twitter: string | null
website: string | null
country: {
code: string
name: string
}
cover: {
custom_url: string | null
url: string
id: number | null
}
is_restricted: boolean
}
export default function Osu<P extends Record<string, any> = OsuProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "osu",
name: "Osu!",
type: "oauth",
token: "https://osu.ppy.sh/oauth/token",
authorization: {
url: "https://osu.ppy.sh/oauth/authorize",
params: {
scope: "identify",
},
},
userinfo: "https://osu.ppy.sh/api/v2/me",
profile(profile) {
return {
id: profile.id,
email: null,
name: profile.username,
image: profile.avatar_url,
}
},
options,
}
}

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

@@ -0,0 +1,59 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface PipedriveProfile {
success: boolean
data: {
id: number
name: string
default_currency?: string
locale?: string
lang?: number
email: string
phone?: string
activated?: boolean
last_login?: Date
created?: Date
modified?: Date
signup_flow_variation?: string
has_created_company?: boolean
is_admin?: number
active_flag?: boolean
timezone_name?: string
timezone_offset?: string
role_id?: number
icon_url?: string
is_you?: boolean
company_id?: number
company_name?: string
company_domain?: string
company_country?: string
company_industry?: string
language?: {
language_code?: string
country_code?: string
}
}
}
export default function Pipedrive<
P extends Record<string, any> = PipedriveProfile
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
return {
id: "pipedrive",
name: "Pipedrive",
type: "oauth",
version: "2.0",
authorization: "https://oauth.pipedrive.com/oauth/authorize",
token: "https://oauth.pipedrive.com/oauth/token",
userinfo: "https://api.pipedrive.com/users/me",
profile: ({ data: profile }) => {
return {
id: profile.id,
name: profile.name,
email: profile.email,
image: profile.icon_url,
}
},
options,
}
}

View File

@@ -1,4 +1,4 @@
import { OAuthConfig, OAuthUserConfig } from "."
import type { OAuthConfig, OAuthUserConfig } from "."
export interface SlackProfile {
ok: boolean

View File

@@ -1,4 +1,4 @@
import { OAuthConfig, OAuthUserConfig } from "."
import type { OAuthConfig, OAuthUserConfig } from "."
export interface SpotifyImage {
url: string

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,4 +1,4 @@
import { OAuthConfig, OAuthUserConfig } from "."
import type { OAuthConfig, OAuthUserConfig } from "."
export interface TwitchProfile {
sub: string

View File

@@ -1,26 +0,0 @@
/** @type {import(".").OAuthProvider} */
export default function Twitter(options) {
return {
id: "twitter",
name: "Twitter",
type: "oauth",
version: "1.0A",
authorization: "https://api.twitter.com/oauth/authenticate",
accessTokenUrl: "https://api.twitter.com/oauth/access_token",
requestTokenUrl: "https://api.twitter.com/oauth/request_token",
profileUrl:
"https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true",
profile(profile) {
return {
id: profile.id_str,
name: profile.name,
email: profile.email,
image: profile.profile_image_url_https.replace(
/_normal\.(jpg|png|gif)$/,
".$1"
),
}
},
options,
}
}

214
src/providers/twitter.ts Normal file
View File

@@ -0,0 +1,214 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface TwitterLegacyProfile {
id: number
id_str: string
name: string
screen_name: string
location: string
description: string
url: string
entities: {
url: {
urls: Array<{
url: string
expanded_url: string
display_url: string
indices: number[]
}>
}
description: {
urls: any[]
}
}
protected: boolean
followers_count: number
friends_count: number
listed_count: number
created_at: string
favourites_count: number
utc_offset?: any
time_zone?: any
geo_enabled: boolean
verified: boolean
statuses_count: number
lang?: any
status: {
created_at: string
id: number
id_str: string
text: string
truncated: boolean
entities: {
hashtags: any[]
symbols: any[]
user_mentions: Array<{
screen_name: string
name: string
id: number
id_str: string
indices: number[]
}>
urls: any[]
}
source: string
in_reply_to_status_id: number
in_reply_to_status_id_str: string
in_reply_to_user_id: number
in_reply_to_user_id_str: string
in_reply_to_screen_name: string
geo?: any
coordinates?: any
place?: any
contributors?: any
is_quote_status: boolean
retweet_count: number
favorite_count: number
favorited: boolean
retweeted: boolean
lang: string
}
contributors_enabled: boolean
is_translator: boolean
is_translation_enabled: boolean
profile_background_color: string
profile_background_image_url: string
profile_background_image_url_https: string
profile_background_tile: boolean
profile_image_url: string
profile_image_url_https: string
profile_banner_url: string
profile_link_color: string
profile_sidebar_border_color: string
profile_sidebar_fill_color: string
profile_text_color: string
profile_use_background_image: boolean
has_extended_profile: boolean
default_profile: boolean
default_profile_image: boolean
following: boolean
follow_request_sent: boolean
notifications: boolean
translator_type: string
withheld_in_countries: any[]
suspended: boolean
needs_phone_verification: boolean
}
export function TwitterLegacy<
P extends Record<string, any> = TwitterLegacyProfile
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
return {
id: "twitter",
name: "Twitter (Legacy)",
type: "oauth",
version: "1.0A",
authorization: "https://api.twitter.com/oauth/authenticate",
accessTokenUrl: "https://api.twitter.com/oauth/access_token",
requestTokenUrl: "https://api.twitter.com/oauth/request_token",
profileUrl:
"https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true",
profile(profile) {
return {
id: profile.id_str,
name: profile.name,
email: profile.email,
image: profile.profile_image_url_https.replace(
/_normal\.(jpg|png|gif)$/,
".$1"
),
}
},
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

@@ -13,7 +13,9 @@ export default function Yandex(options) {
id: profile.id,
name: profile.real_name,
email: profile.default_email,
image: profile.is_avatar_empty ? null : `https://avatars.yandex.net/get-yapic/${profile.default_avatar_id}/islands-200`,
image: profile.is_avatar_empty
? null
: `https://avatars.yandex.net/get-yapic/${profile.default_avatar_id}/islands-200`,
}
},
options,

View File

@@ -1,20 +0,0 @@
/** @type {import(".").OAuthProvider} */
export default function Zoom(options) {
return {
id: "zoom",
name: "Zoom",
type: "oauth",
authorization: "https://zoom.us/oauth/authorize",
token: "https://zoom.us/oauth/token",
userinfo: "https://api.zoom.us/v2/users/me",
profile(profile) {
return {
id: profile.id,
name: `${profile.first_name} ${profile.last_name}`,
email: profile.email,
image: null,
}
},
options,
}
}

52
src/providers/zoom.ts Normal file
View File

@@ -0,0 +1,52 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface ZoomProfile {
id: string
first_name: string
last_name: string
email: string
type: number
role_name: string
pmi: number
use_pmi: boolean
vanity_url: string
personal_meeting_url: string
timezone: string
verified: number
dept: string
created_at: string
last_login_time: string
last_client_version: string
pic_url: string
host_key: string
jid: string
group_ids: string[]
im_group_ids: string[]
account_id: string
language: string
phone_country: string
phone_number: string
status: string
}
export default function Zoom<P extends Record<string, any> = ZoomProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "zoom",
name: "Zoom",
type: "oauth",
authorization: "https://zoom.us/oauth/authorize?scope",
token: "https://zoom.us/oauth/token",
userinfo: "https://api.zoom.us/v2/users/me",
profile(profile) {
return {
id: profile.id,
name: `${profile.first_name} ${profile.last_name}`,
email: profile.email,
image: profile.pic_url,
}
},
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__"]
}