Compare commits

...

66 Commits

Author SHA1 Message Date
Balázs Orbán
2becdad990 fix(logger): attempt at fixing infinite loop (#1789) 2021-04-20 21:22:20 +02:00
Pop Stefan
e3c2c7756d docs: add Class components tutorial (#1784) 2021-04-20 17:34:05 +02:00
Balázs Orbán
718f2537cb build(provider): auto-generate Providers submodule (#1782) 2021-04-20 17:33:24 +02:00
dogomedia-github
ae26df091d fix(provider): add sub to defaultJwtPayload for credentials provider. (#1725)
Co-authored-by: Joseph Chen <jchen@dogomedia.com>
2021-04-20 12:59:48 +02:00
dependabot[bot]
1cbf73b2f6 chore(deps): bump jose from 1.27.2 to 1.28.1 (#1772)
Bumps [jose](https://github.com/panva/jose) from 1.27.2 to 1.28.1.
- [Release notes](https://github.com/panva/jose/releases)
- [Changelog](https://github.com/panva/jose/blob/v1.28.1/CHANGELOG.md)
- [Commits](https://github.com/panva/jose/compare/v1.27.2...v1.28.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-20 12:56:07 +02:00
Balázs Orbán
46b62d723c feat(ts): expose types from main package (#1773)
* chore: add beta to release flow/GH actions

* feat(ts): expose types from the package (#1665)

* chore(types): move existing types to the repo
* feat(ts): expose types from the main package
* chore(deps): bring back `react-dom` version range
* chore(ts): cleanup deps and comments
* chore(ci): run types tests on a separate workflow

* chore(ci): fix typo on types workflow

* fix(ts): correctly export sub-module types (#1677)

* chore(types): build types script

Adds a script that moves the declaration files we have in `./types` to `./dist` relative to the files they intend to type.

This is the first step, we still need to change what we declare in `package.json`, add the script to the CI pipeline if we're happy with it and figure out how to type `next-auth/jwt`.

* refactor(lint): fix build-types script

* fix(ts): add .d.ts sub-module files to package.json

#1677 seemed to miss this

* fix(built): typo in package.json

* fix(build): fix release

* feat(ts): support module augmentation (#1681)

* chore(ts): remove unused imports

* refactor(ts): clean up CallbackOptions

* docs(ts): explain Module Augmentation

* docs(ts): don't use @ in folder name "types"

* test(ts): make jwt params optional

* docs(ts): fix typo (TypeScript -> NextAuth.js)

* style: replace ts-standard with eslint/prettier (#1724)

* style: move from ts-standard to eslint/prettier

* fix: install remaining eslint-config-standard peer deps

* fix: add remaining missing dependencies/config

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

* docs(lint): update contributing.md (#1760)

Regarding ESLint / Prettier use and link to their VSCode extensions

* refactor(ts): de-duplicate types (#1690)

* refactor(ts): deduplicate internal types

* refactor(ts): ease up providers typings

* test(ts): fix failing TS tests

* test(ts): rename TS property to fix test

* docs(ts): mention TS docs in README.md

* feat(ts): move/update client types

* refactor(TS): rename some types

* test(ts): fix client tests

* docs(ts): move function descriptions to .d.ts

* chore: fix lint error

* refactor(ts): separate internal types

* chore: simplify build-types script

* chore: update type import paths in src

* chore(build): create root files at build

* chore: remove unnecessary .npmignore

* chore: run prettier on types

* fix(ts): clean up jwt types

* fix(ts): make getToken return type depend on raw param

* docs(page): explain page errors, add theming note

* docs(ts): add JSDoc to NextAuthOptions props

* chore(ts): remove unused import

* docs(ts): change JSDOC docs notation

* refactor(build): extract module entries into enum

* chore(ts): move ClientSafeProvider

* chore(ts): simplify GetTokenParams generic

* style(lint): fix linting errors

* chore: re-add generic extension to GetTokenParams

* fix(ts): extract EmailConfigServerOptions to interface

* fix(ts): use relative imports

* Merge branch 'main' into beta

* Merge main into beta

* fix(ts): fix typos, add more links to documentation

* test(ts): update JWT getToken test

* fix(build): fix tsconfig.json formatting

* test(ts): use absolute imports in test files

* fix(ts): add missing callbacks JSDoc

* docs: mention TS in FAQ, fix typos

* docs: fix some typos in the docs

Co-authored-by: Lluis Agusti <hi@llu.lu>
Co-authored-by: Nico Domino <yo@ndo.dev>
2021-04-20 12:20:43 +02:00
Balázs Orbán
457952bb5a fix(jwt): make decode overrideable in getToken (#1751) 2021-04-17 12:40:50 +02:00
Balázs Orbán
17b789822d fix: make oauth_token_secret and oauth_token available (#1322)
* fix: add oauth_token_secret to requests

* chore: remove console.log

* refactor: follow casing from response
2021-04-14 21:26:15 +02:00
Ovidiu Dan
fd12194c0c docs(provider): Explain how to get access to LinkedIn authentication (#1706) 2021-04-12 18:46:20 +02:00
Balázs Orbán
1c662e9ddc fix(page): fall back to default error page (#1700) 2021-04-12 03:56:47 +02:00
Balázs Orbán
968903d227 fix(oauth): support response_mode=form_post (#1669)
* chore: alias dev script to next

* feat(core): fallback to body when reading state

* refactor: set csrfToken on req.options implicitly

Ensures we do this similarly than
in other handlers like pkce, state, extendRes, callbackUrlHandler etc.

* chore: add code comment for debugging
2021-04-12 00:24:05 +02:00
Balázs Orbán
3dedf6c26c fix(provider): proper check of protection property (#1694)
* fix(provider): proper check of protection property

* chore: add comment
2021-04-12 00:15:29 +02:00
Amauri Dias
d1dbfe1023 fix: truly replace .flat() to support Node <11 again (#1691) 2021-04-11 23:20:37 +02:00
David Colón
63171a0271 fix: validate provider existence before looking for protection property (#1687)
* Fix validation of provider existence before looking for protection property

* Use optional chaining
2021-04-11 15:20:01 +02:00
Amauri Dias
872e180339 fix: replace .flat() to support Node <11 again (#1684) 2021-04-11 10:57:25 +02:00
ifly7charlie
a7709df796 docs: Document the additional parameters in JWT (#1550)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-04-08 14:00:19 +02:00
Balázs Orbán
dbe283f0fa refactor: rename extend-res to extend-req 2021-04-07 22:26:54 +02:00
Balázs Orbán
727426bbec chore(ts): auto-label TypeScript related changes 2021-04-07 20:16:10 +02:00
Vinicius CR
5a3ee47337 feat(provider): accept array for protection to support multiple mechanisms (#1565)
* fix: add protection both option

* feat: update docs with new protection value

* fix: lint files

* refactor: change protection from string to array

* chore: reverting unespected change

* chore: lint files

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-04-06 19:20:56 +02:00
bhaveshmishra-code
8dd8f7c48a docs: fix typo in callbacks.md (#1657)
Fixed the spelling mistake.

existance -> existence
2021-04-05 19:35:26 +02:00
Jaime Martínez Rincón
072c59d85a docs: fix typo primsa (#1652) 2021-04-05 00:25:46 +02:00
dependabot[bot]
d0e8147a48 chore(deps): bump y18n from 4.0.0 to 4.0.1 (#1631)
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-05 00:24:39 +02:00
Jasper Moelker
5bc8f8b986 docs(page): correct getCsrfToken and input types (#1651)
This fixes the a mismatch between the import (`csrfToken`) and the method (`getCsrfToken`) used in `getInitialProps`/`getServerSideProps`.
In addition the form input fields now have their correct type: `email` for email input (for better autocomplete, virtual keyboard support and native validation) and `password` for the password input (to hide password while typing).
2021-04-04 22:01:53 +02:00
hoangbits
136361e1f4 docs: rename command to vercel cli, now cli is deprecated (#1647) 2021-04-04 11:02:38 +02:00
hoangbits
cc9869592c docs: fix typo in providers.md (#1641) 2021-04-02 17:18:40 +02:00
Jay Liew
073da60c3d docs: Update pages.md (#1592)
* Update pages.md

Updated Credentials Sign-In code example to indicate how to use `getServerSideProps` but still also showing the older `getInitialProps` example

* Update www/docs/configuration/pages.md

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

* update documentation to show example using getServerSideProps()

Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Jay Liew <jay@haute.tech>
2021-03-31 00:31:31 +02:00
ifly7charlie
aacc34bbfd docs(error): Add missing error message and technique to resolve (#1549)
* Add missing error message and technique to resolve

* Update errors.md

Correct with correct error message and more complete suggestions on resolving it
2021-03-26 23:09:21 +01:00
jgollhardt
074688d10e docs(provider): fix wrong param name in sendVerificationRequest example (#1595) 2021-03-26 23:01:25 +01:00
Macarse, Christian Ryan R
b3ffe50c03 docs(provider): removed misleading provider signin link (#1588) 2021-03-25 22:30:46 +01:00
Shubham Shukla
e6d063825d fix(provider): added options in instagram provider (#1570) 2021-03-23 22:28:54 +01:00
Balázs Orbán
985f7b3431 fix(logger): properly end request every time (#1557)
* fix(logger): properly end request every time

* chore: fix linting
2021-03-20 10:08:12 +01:00
Max
237b016378 fix(provider): reject access token if slack login flow was canceled (#1544)
* fix: reject access token if slack login flow was canceled

* style: fix lint errors in oauth client
2021-03-18 14:59:24 +01:00
Joshua Williams
776b9480da feat(provider): add Zoho provider (#1516)
* feat(provider): add zoho

* fix: use LF instead of CRLF

* fix: crlf to lf line endings

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-03-16 19:27:11 +01:00
Honman Yau
07a3f76cb3 docs: fix typos in REST API guide (#1528) 2021-03-16 19:25:24 +01:00
tclaude94
3726d68c49 feat(provider): add FACEIT provider (#1469) 2021-03-16 00:00:35 +01:00
dependabot[bot]
e31db1726a chore(deps): bump xmldom from 0.3.0 to 0.5.0 (#1510)
Bumps [xmldom](https://github.com/xmldom/xmldom) from 0.3.0 to 0.5.0.
- [Release notes](https://github.com/xmldom/xmldom/releases)
- [Changelog](https://github.com/xmldom/xmldom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xmldom/xmldom/compare/0.3.0...0.5.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-13 14:19:22 +01:00
James Perkins
a241199c11 docs(tutorials): Adding two more tutorials to the list.
[skip release]
2021-03-13 03:42:00 +00:00
dependabot[bot]
5385ec20a9 chore(deps): bump elliptic from 6.5.3 to 6.5.4 in /www (#1493)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-10 22:59:10 +01:00
Balázs Orbán
810d02e671 fix(deps): upgrade to latest preact-render-to-string (#1475) 2021-03-08 10:39:13 +01:00
Valentin Hervieu
e5535734f8 fix(client): set useSession loading state correctly (#1468)
This is fixing #1467.

The issue was due to doing the `setLoading(false)` in the finally:  as we can do an early return [here](a7e08e2a32/src/client/index.js (L100-L100)), we would still go to the finally and mark the session as being loaded.

I simply removed the `finally` block to only set the `loading` state to false when:
- the data is ready
- an error occures
2021-03-07 19:11:32 +01:00
Taehwan Noh
ba7aed1057 feat(provider): add Kakao provider (#1459) 2021-03-06 21:52:39 +01:00
Balázs Orbán
a7e08e2a32 fix: make sure useSession populates session correctly (#1462) 2021-03-06 20:02:43 +01:00
mcha
0d13040264 docs(client): fix client.md typos (#1453) 2021-03-06 10:50:29 +01:00
Balázs Orbán
582520f8ef chore: fix typo in feature request template 2021-03-06 00:35:09 +01:00
Sam Bauch
95942519a5 feat(provider): add Osso SAML provider (#1448)
Co-authored-by: @sbauch
2021-03-06 00:21:38 +01:00
Balázs Orbán
f3e64f04cc feat(client): introduce NEXTAUTH_URL_INTERNAL (#1449)
Co-authored-by: @gergelyke
2021-03-06 00:10:36 +01:00
Balázs Orbán
ed5cc4aa65 feat(provider): add Instagram provider (#1447)
Co-authored-by: @PolMrt pol@hey.com
2021-03-05 23:39:50 +01:00
Balázs Orbán
0e20b60229 docs(database): mention CockroachDB
Co-authored-by: @jukbot <jukbot@yellotalk.co>
2021-03-05 22:52:48 +01:00
Balázs Orbán
3aee24b5dc refactor: client improvements (#1428)
* docs(client): add TS definitions to client

* docs(client): add documentation links to public methods

* refactor(client): simplify window sync, simplify logic

* refactor(client): extract repeating logic to _fetchData

* refactor(client): remove clientId

* refactor(client): use session in Provider if passed
2021-03-05 22:47:32 +01:00
Baterka
960ca85907 fix: send only the error message in callback redirects (#1424)
Changed `encodeURIComponent(error)` to `encodeURIComponent(error.message)` to remove prefix (such as `Error: ` and possible stack trace).
Seems like better way of doing it and also safer if server throws some error with sensitive data.
2021-03-05 18:34:11 +01:00
Balázs Orbán
f960cc0f6f chore(docs): upgrade docs dependencies 2021-03-03 20:13:46 +01:00
Balázs Orbán
0f64f3eea7 chore: don't mark bugs as stale
Had a good laugh today 😄:

"This is not stale. Bread goes stale. Bugs don't. They don't just magically go away because time has passed" - Unknown
https://twitter.com/ericclemmons/status/1367000259046604803
2021-03-03 19:54:13 +01:00
yannicktian
71c78e8e24 feat(provider): allow disabling redirection on sign in with email (#1416)
* feat: allow to disable client-side redirect for email provider

* docs(client): mention that redirect can also be disabled for email provider

* feat: only display one email input in email page
2021-03-02 22:38:02 +01:00
dependabot[bot]
d86609a2dc chore(deps): bump prismjs from 1.22.0 to 1.23.0 in /www (#1409)
Bumps [prismjs](https://github.com/PrismJS/prism) from 1.22.0 to 1.23.0.
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.22.0...v1.23.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-01 22:52:46 +01:00
Joost van Wollingen
d0c3400d30 docs(page): Remove unnecessary context param (#1406)
...when calling providers in the docs
2021-03-01 21:39:03 +01:00
Praneeth
172e79cb04 fix(page): add character encoding and page titles (#1380)
* added character encoding fix

* changed multi-line to inline and added title param to send fn in src/server/pages/index.js

* modified the return object of renderPage in src/server/pages/index.js
2021-03-01 21:17:51 +01:00
Balázs Orbán
46d5c76605 docs: reword callbacks.md
Explain the `jwt()` callback before the `session()` callback, as it comes first in the flow.
2021-02-28 18:05:10 +01:00
Zach White
438efd8a9b docs: reword pages.md (#1386)
language edits
2021-02-27 23:43:45 +01:00
Balázs Orbán
d8d497cc91 feat(provider): call generateVerificationToken async (#1378) 2021-02-27 23:33:26 +01:00
Pop Stefan
6152c8afbb docs: added refresh token tutorial link in faq page (#1385) 2021-02-27 20:24:09 +01:00
Balázs Orbán
5ae6f6118c docs: add missing comma
Thx @followbl 😺
2021-02-25 23:29:33 +01:00
sid
96ff048b59 fix(provider): use correct file type for Discord profile img (#1365) 2021-02-23 21:39:27 +01:00
Ariel Weingarten
e80f6e936d docs(provider): Update twitch.md (#1353)
State what redirect URL to add to the Twitch console.
2021-02-22 20:04:38 +01:00
Lawrence Chen
6b5a215fb2 docs(tutorials): refresh token rotation (#1310)
* docs(tutorials): refresh token rotation

* use simple initialization

* be optimistic

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

* add yarn.lock to .gitignore

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-02-21 22:30:01 +01:00
Balázs Orbán
782482b9f4 feat: make tokens available in profile callback (#1329)
* feat: make access_token available in profile callback

* docs(provider): mention access_token param in profile callback

* feat: send all available tokens to provider.profile
2021-02-20 22:58:48 +01:00
Balázs Orbán
2d364f246a docs: tweak release badges 2021-02-17 19:14:45 +01:00
133 changed files with 10988 additions and 6491 deletions

View File

@@ -9,7 +9,7 @@ assignees: ''
A clear and concise description of the feature being proposed.
**Purpose of proposed feature**
A clear and concise description description of why this feature is necessary and what problems it solves.
A clear and concise description of why this feature is necessary and what problems it solves.
**Detail about proposed feature**
A detailed description of how the proposal might work (if you have one).

6
.github/labeler.yml vendored
View File

@@ -1,5 +1,6 @@
test:
- test/**/*
- types/tests/**/*
documentation:
- www/**/*
@@ -32,4 +33,7 @@ client:
pages:
- src/server/pages/**/*
- www/docs/configuration/pages.md
- www/docs/configuration/pages.md
TypeScript:
- types/**/*

1
.github/stale.yml vendored
View File

@@ -7,6 +7,7 @@ exemptLabels:
- pinned
- security
- priority
- bug
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable

View File

@@ -6,10 +6,12 @@ on:
push:
branches:
- main
- beta
- next
pull_request:
branches:
- main
- beta
- next
jobs:

View File

@@ -13,7 +13,7 @@ name: "CodeQL"
on:
push:
branches: [ main, next ]
branches: [ main, beta, next ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]

View File

@@ -2,9 +2,10 @@ name: Integration Test
on:
push:
branches:
- main
- next
branches:
- main
- beta
- next
pull_request:
jobs:
@@ -17,7 +18,7 @@ jobs:
if: github.event.pull_request.head.repo.full_name == github.repository
# We use self-hosted runners as cloud based runnners (e.g. AWS, GPC)
# fail due to IP Address checks done by providers, which enforce
# fail due to IP Address checks done by providers, which enforce
# CAPTCHA checks on login request from cloud compute IP addresses to
# prevent abuse.
runs-on: self-hosted
@@ -45,7 +46,7 @@ jobs:
- run: npm test
# TODO Tests should exit out if env vars not set (currently hangs)
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
NEXTAUTH_TWITTER_ID: ${{secrets.NEXTAUTH_TWITTER_ID}}
NEXTAUTH_TWITTER_SECRET: ${{secrets.NEXTAUTH_TWITTER_SECRET}}
NEXTAUTH_TWITTER_USERNAME: ${{secrets.NEXTAUTH_TWITTER_USERNAME}}

View File

@@ -3,6 +3,7 @@ on:
push:
branches:
- 'main'
- 'beta'
- 'next'
- '3.x'
pull_request:

25
.github/workflows/types.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Types
on:
push:
branches:
- main
- beta
- next
pull_request:
branches:
- main
- beta
- next
jobs:
lint-and-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
- name: Install dependencies
uses: bahmutov/npm-install@v1
- name: Check types
run: npm run test:types

5
.gitignore vendored
View File

@@ -11,6 +11,8 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
# Dependencies
node_modules
@@ -25,6 +27,7 @@ node_modules
.cache-loader
.next
www/providers.json
src/providers/index.js
# VS
/.vs/slnx.sqlite-journal
@@ -37,4 +40,4 @@ www/providers.json
/_work
# Prisma migrations
/prisma/migrations
/prisma/migrations

View File

@@ -16,7 +16,7 @@ Anyone can be a contributor. Either you found a typo, or you have an awesome fea
* The latest changes are always in `main`, so please make your Pull Request against that branch.
* Pull Requests should be raised for any change
* Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
* Run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this extension](https://marketplace.visualstudio.com/items?itemName=chenxsan.vscode-standardjs) to fix lint issues in development)
* We use ESLint/Prettier for linting/formatting, so please run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
* We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
* If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
@@ -42,7 +42,7 @@ npm i
> NOTE: You can add any environment variables to .env.local that you would like to use in your dev app.
> You can find the next-auth config under`pages/api/auth/[...nextauth].js`.
1. Start the dev application/server and CSS watching:
1. Start the dev application/server:
```sh
npm run dev
```
@@ -59,6 +59,16 @@ When running `npm run dev`, you start a Next.js dev server on `http://localhost:
>NOTE: When working on CSS, you will need to manually refresh the page after changes. (Improving this through a PR is very welcome!)
#### Providers
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily! You only need to add two changes:
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
That's it! 🎉 Others will be able to discover this provider much more easily now!
You can look at the existing built-in providers for inspiration.
#### Databases
Included is a Docker Compose file that starts up MySQL, Postgres, and MongoDB databases on localhost.

View File

@@ -23,9 +23,9 @@
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth" alt="Github Stars" />
</a>
<a href="https://www.npmjs.com/package/next-auth">
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth" alt="Github Stable Release" />
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?label=latest" alt="Github Stable Release" />
</a>
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases" alt="Github Prelease" />
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases&label=prerelease&sort=semver" alt="Github Prelease" />
</p>
</p>
@@ -84,13 +84,9 @@ Advanced options allow you to define your own routines to handle controlling wha
### TypeScript
You can install the appropriate types via the following command:
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 documentaion.
```
npm install --save-dev @types/next-auth
```
As of now, TypeScript is a community effort. If you encounter any problems with the types package, please create an issue at [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/next-auth). Alternatively, you can open a pull request directly with your fixes there. We welcome anyone to start a discussion on migrating this package to TypeScript, or how to improve the TypeScript experience in general.
The package at `@types/next-auth` is now deprecated.
## Example

View File

@@ -1 +0,0 @@
module.exports = require('./dist/adapters').default

View File

@@ -1 +0,0 @@
module.exports = require('./dist/client').default

View File

@@ -100,6 +100,11 @@ export default function Header () {
<a>Credentials</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/email'>
<a>Email</a>
</Link>
</li>
</ul>
</nav>
</header>

86
config/build.js Normal file
View File

@@ -0,0 +1,86 @@
const fs = require("fs-extra")
const path = require("path")
const MODULE_ENTRIES = {
SERVER: "index",
CLIENT: "client",
PROVIDERS: "providers",
ADAPTERS: "adapters",
JWT: "jwt",
}
// Building submodule entries
const BUILD_TARGETS = {
[`${MODULE_ENTRIES.SERVER}.js`]: "module.exports = require('./dist/server').default\n",
[`${MODULE_ENTRIES.CLIENT}.js`]: "module.exports = require('./dist/client').default\n",
[`${MODULE_ENTRIES.ADAPTERS}.js`]: "module.exports = require('./dist/adapters').default\n",
[`${MODULE_ENTRIES.PROVIDERS}.js`]: "module.exports = require('./dist/providers').default\n",
[`${MODULE_ENTRIES.JWT}.js`]: "module.exports = require('./dist/lib/jwt').default\n",
}
Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
fs.writeFile(path.join(process.cwd(), target), content, (err) => {
if (err) throw err
console.log(`[build] created "${target}" in root folder`)
})
})
// Building types
const TYPES_TARGETS = [
`${MODULE_ENTRIES.SERVER}.d.ts`,
`${MODULE_ENTRIES.CLIENT}.d.ts`,
`${MODULE_ENTRIES.ADAPTERS}.d.ts`,
`${MODULE_ENTRIES.PROVIDERS}.d.ts`,
`${MODULE_ENTRIES.JWT}.d.ts`,
"internals",
]
TYPES_TARGETS.forEach((target) => {
fs.copy(
path.resolve("types", target),
path.join(process.cwd(), target),
(err) => {
if (err) throw err
console.log(`[build-types] copying "${target}" to root folder`)
}
)
})
// Building providers
const providersDir = path.join(process.cwd(), "/src/providers")
const files = fs.readdirSync(providersDir, "utf8")
let importLines = ""
let exportLines = `export default {\n`
files.forEach((file) => {
const provider = fs.readFileSync(path.join(providersDir, file), "utf8")
try {
// NOTE: If this fails, the default export probably wasn't a named function.
// Always use a named function as default export.
// Eg.: export default function YourProvider ...
const { functionName } = provider.match(
/export default function (?<functionName>.+)\s?\(/
).groups
importLines += `import ${functionName} from "./${file}"\n`
exportLines += ` ${functionName},\n`
} catch (error) {
console.error(
[
`\nThe provider file '${file}' should have a single named default export`,
"Example: 'export default function YourProvider'\n\n",
].join("\n")
)
process.exit(1)
}
})
exportLines += `}\n`
fs.writeFile(
path.join(process.cwd(), "src/providers/index.js"),
[importLines, exportLines].join("\n")
)

View File

@@ -1 +0,0 @@
module.exports = require('./dist/server')

1
jwt.js
View File

@@ -1 +0,0 @@
module.exports = require('./dist/lib/jwt').default

2311
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,37 +8,45 @@
"main": "index.js",
"scripts": {
"build": "npm run build:js && npm run build:css",
"build:js": "babel --config-file ./config/babel.config.json src --out-dir dist",
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.json src --out-dir dist",
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
"dev": "next | npm run watch:css",
"dev:with-css": "next | npm run watch:css",
"dev": "next",
"watch": "npm run watch:js | npm run watch:css",
"watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist",
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
"test:app:start": "docker-compose -f test/docker/app.yml up -d",
"test:app:rebuild": "npm run build && docker-compose -f test/docker/app.yml up -d --build",
"test:app:stop": "docker-compose -f test/docker/app.yml down",
"test": "npm run test:app:rebuild && npm run test:integration && npm run test:app:stop",
"test": "npm run test:app:rebuild && npm run test:integration && npm run test:app:stop && npm run test:types",
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb && npm run test:db:mssql",
"test:db:mysql": "node test/mysql.js",
"test:db:postgres": "node test/postgres.js",
"test:db:mongodb": "node test/mongodb.js",
"test:db:mssql": "node test/mssql.js",
"test:integration": "mocha test/integration",
"test:types": "dtslint types",
"db:start": "docker-compose -f test/docker/databases.yml up -d",
"db:stop": "docker-compose -f test/docker/databases.yml down",
"prepublishOnly": "npm run build",
"publish:beta": "npm publish --tag beta",
"publish:canary": "npm publish --tag canary",
"lint": "ts-standard",
"lint:fix": "ts-standard --fix"
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"files": [
"dist",
"index.js",
"index.d.ts",
"providers.js",
"providers.d.ts",
"adapters.js",
"adapters.d.ts",
"client.js",
"jwt.js"
"client.d.ts",
"jwt.js",
"jwt.d.ts",
"internals"
],
"license": "ISC",
"dependencies": {
@@ -50,14 +58,14 @@
"oauth": "^0.9.15",
"pkce-challenge": "^2.1.0",
"preact": "^10.4.1",
"preact-render-to-string": "^5.1.7",
"preact-render-to-string": "^5.1.14",
"querystring": "^0.2.0",
"require_optional": "^1.0.1",
"typeorm": "^0.2.30"
},
"peerDependencies": {
"react": "^16.13.1 || ^17",
"react-dom": "^16.13.1 || ^17"
"react-dom": "16.13.1 || ^17"
},
"peerOptionalDependencies": {
"mongodb": "^3.5.9",
@@ -76,12 +84,21 @@
"@semantic-release/npm": "7.0.8",
"@semantic-release/release-notes-generator": "^9.0.1",
"@types/react": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"autoprefixer": "^9.7.6",
"babel-preset-preact": "^2.0.0",
"conventional-changelog-conventionalcommits": "4.4.0",
"cssnano": "^4.1.10",
"dotenv": "^8.2.0",
"dtslint": "^4.0.8",
"eslint": "^7.19.0",
"eslint-config-prettier": "^8.2.0",
"eslint-config-standard-with-typescript": "^19.0.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^5.0.0",
"mocha": "^8.1.3",
"mongodb": "^3.5.9",
"mssql": "^6.2.1",
@@ -90,29 +107,46 @@
"pg": "^8.2.1",
"postcss-cli": "^7.1.1",
"postcss-nested": "^4.2.1",
"prettier": "^2.2.1",
"prisma": "^2.16.1",
"puppeteer": "^5.2.1",
"puppeteer-extra": "^3.1.15",
"puppeteer-extra-plugin-stealth": "^2.6.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"ts-standard": "^10.0.0",
"typescript": "^4.1.3"
},
"ts-standard": {
"project": "./tsconfig.json",
"ignore": [
"test/",
"next-env.d.ts"
"prettier": {
"semi": false
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"extends": [
"standard-with-typescript",
"prettier"
],
"globals": [
"fetch"
]
"ignorePatterns": [
"node_modules",
"test",
"next-env.d.ts",
"types",
"www",
".next",
"dist"
],
"globals": {
"localStorage": "readonly",
"location": "readonly",
"fetch": "readonly"
}
},
"funding": [
{
"type" : "github",
"url" : "https://github.com/sponsors/balazsorban44"
"type": "github",
"url": "https://github.com/sponsors/balazsorban44"
}
]
}

View File

@@ -6,6 +6,27 @@ import Providers from 'next-auth/providers'
// const prisma = new PrismaClient()
export default NextAuth({
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
// cookies: {
// csrfToken: {
// name: 'next-auth.csrf-token',
// options: {
// httpOnly: true,
// sameSite: 'none',
// path: '/',
// secure: true
// }
// },
// pkceCodeVerifier: {
// name: 'next-auth.pkce.code_verifier',
// options: {
// httpOnly: true,
// sameSite: 'none',
// path: '/',
// secure: true
// }
// }
// },
providers: [
Providers.Email({
server: process.env.EMAIL_SERVER,
@@ -19,6 +40,11 @@ export default NextAuth({
clientId: process.env.AUTH0_ID,
clientSecret: process.env.AUTH0_SECRET,
domain: process.env.AUTH0_DOMAIN,
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
// protection: ["pkce", "state"],
// authorizationParams: {
// response_mode: 'form_post'
// }
protection: 'pkce'
}),
Providers.Twitter({

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line no-use-before-define
import * as React from 'react'
import { signIn, signOut, useSession } from 'next-auth/client'
import Layout from 'components/layout'

67
pages/email.js Normal file
View File

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

View File

@@ -1 +0,0 @@
module.exports = require('./dist/providers').default

View File

@@ -2,6 +2,7 @@ module.exports = {
branches: [
'+([0-9])?(.{+([0-9]),x}).x',
'main',
{ name: 'beta', prerelease: true },
{ name: 'next', prerelease: true }
]
}

View File

@@ -1,5 +1,3 @@
/// Note: fetch() is built in to Next.js 9.4
//
// Note about signIn() and signOut() methods:
//
// On signIn() and signOut() we pass 'json: true' to request a response in JSON
@@ -20,167 +18,75 @@ import parseUrl from '../lib/parse-url'
// relative URLs are valid in that context and so defaults to empty.
// 2. When invoked server side the value is picked up from an environment
// variable and defaults to 'http://localhost:3000'.
/** @type {import("types/internals/client").NextAuthConfig} */
const __NEXTAUTH = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
keepAlive: 0, // 0 == disabled (don't send); 60 == send every 60 seconds
clientMaxAge: 0, // 0 == disabled (only use cache); 60 == sync if last checked > 60 seconds ago
baseUrlServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePathServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL).basePath,
keepAlive: 0,
clientMaxAge: 0,
// Properties starting with _ are used for tracking internal app state
_clientLastSync: 0, // used for timestamp since last sycned (in seconds)
_clientSyncTimer: null, // stores timer for poll interval
_eventListenersAdded: false, // tracks if event listeners have been added,
_clientSession: undefined, // stores last session response from hook,
// Generate a unique ID to make it possible to identify when a message
// was sent from this tab/window so it can be ignored to avoid event loops.
_clientId: Math.random().toString(36).substring(2) + Date.now().toString(36),
// Used to store to function export by getSession() hook
_clientLastSync: 0,
_clientSyncTimer: null,
_eventListenersAdded: false,
_clientSession: undefined,
_getSession: () => {}
}
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
const broadcast = BroadcastChannel()
// Add event listners on load
if (typeof window !== 'undefined') {
if (__NEXTAUTH._eventListenersAdded === false) {
__NEXTAUTH._eventListenersAdded = true
if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
__NEXTAUTH._eventListenersAdded = true
// Listen for storage events and update session if event fired from
// another window (but suppress firing another event to avoid a loop)
// Fetch new session data but tell it to not to fire another event to
// avoid an infinite loop.
// Note: We could pass session data through and do something like
// `setData(message.data)` but that can cause problems depending
// on how the session object is being used in the client; it is
// more robust to have each window/tab fetch it's own copy of the
// session object rather than share it across instances.
broadcast.receive(() => __NEXTAUTH._getSession({ event: 'storage' }))
// Listen for storage events and update session if event fired from
// another window (but suppress firing another event to avoid a loop)
window.addEventListener('storage', async (event) => {
if (event.key === 'nextauth.message') {
const message = JSON.parse(event.newValue)
if (message?.event === 'session' && message.data) {
// Ignore storage events fired from the same window that created them
if (__NEXTAUTH._clientId === message.clientId) {
return
}
// Fetch new session data but pass 'true' to it not to fire an event to
// avoid an infinite loop.
//
// Note: We could pass session data through and do something like
// `setData(message.data)` but that can cause problems depending
// on how the session object is being used in the client; it is
// more robust to have each window/tab fetch it's own copy of the
// session object rather than share it across instances.
await __NEXTAUTH._getSession({ event: 'storage' })
}
}
})
// Listen for document visibilitychange events
let hidden, visibilityChange
if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support
hidden = 'hidden'
visibilityChange = 'visibilitychange'
} else if (typeof document.msHidden !== 'undefined') {
hidden = 'msHidden'
visibilityChange = 'msvisibilitychange'
} else if (typeof document.webkitHidden !== 'undefined') {
hidden = 'webkitHidden'
visibilityChange = 'webkitvisibilitychange'
}
const handleVisibilityChange = () => !document[hidden] && __NEXTAUTH._getSession({ event: visibilityChange })
document.addEventListener('visibilitychange', handleVisibilityChange, false)
}
}
// Method to set options. The documented way is to use the provider, but this
// method is being left in as an alternative, that will be helpful if/when we
// expose a vanilla JavaScript version that doesn't depend on React.
const setOptions = ({
baseUrl,
basePath,
clientMaxAge,
keepAlive
} = {}) => {
if (baseUrl) { __NEXTAUTH.baseUrl = baseUrl }
if (basePath) { __NEXTAUTH.basePath = basePath }
if (clientMaxAge) { __NEXTAUTH.clientMaxAge = clientMaxAge }
if (keepAlive) {
__NEXTAUTH.keepAlive = keepAlive
if (typeof window !== 'undefined' && keepAlive > 0) {
// Clear existing timer (if there is one)
if (__NEXTAUTH._clientSyncTimer !== null) { clearTimeout(__NEXTAUTH._clientSyncTimer) }
// Set next timer to trigger in number of seconds
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
// Only invoke keepalive when a session exists
if (__NEXTAUTH._clientSession) {
await __NEXTAUTH._getSession({ event: 'timer' })
}
}, keepAlive * 1000)
}
}
}
// Universal method (client + server)
// If passed 'appContext' via getInitialProps() in _app.js then get the req
// object from ctx and use that for the req value to allow getSession() to
// work seemlessly in getInitialProps() on server side pages *and* in _app.js.
export async function getSession ({ ctx, req = ctx?.req, triggerEvent = true } = {}) {
const baseUrl = _apiBaseUrl()
const fetchOptions = req ? { headers: { cookie: req.headers.cookie } } : {}
const session = await _fetchData(`${baseUrl}/session`, fetchOptions)
if (triggerEvent) {
_sendMessage({ event: 'session', data: { trigger: 'getSession' } })
}
return session
}
// Universal method (client + server)
// If passed 'appContext' via getInitialProps() in _app.js then get the req
// object from ctx and use that for the req value to allow getCsrfToken() to
// work seemlessly in getInitialProps() on server side pages *and* in _app.js.
async function getCsrfToken ({ ctx, req = ctx?.req } = {}) {
const baseUrl = _apiBaseUrl()
const fetchOptions = req ? { headers: { cookie: req.headers.cookie } } : {}
const data = await _fetchData(`${baseUrl}/csrf`, fetchOptions)
return data && data.csrfToken ? data.csrfToken : null
}
// Universal method (client + server); does not require request headers
const getProviders = async () => {
const baseUrl = _apiBaseUrl()
return _fetchData(`${baseUrl}/providers`)
// Listen for document visibility change events and
// if visibility of the document changes, re-fetch the session.
document.addEventListener('visibilitychange', () => {
!document.hidden && __NEXTAUTH._getSession({ event: 'visibilitychange' })
}, false)
}
// Context to store session data globally
/** @type {import("types/internals/client").SessionContext} */
const SessionContext = createContext()
// Client side method
export const useSession = (session) => {
// Try to use context if we can
const value = useContext(SessionContext)
// If we have no Provider in the tree, call the actual hook
if (value === undefined) {
return _useSessionHook(session)
}
return value
export function useSession (session) {
const context = useContext(SessionContext)
if (context) return context
return _useSessionHook(session)
}
// Internal hook for getting session from the api.
const _useSessionHook = (session) => {
function _useSessionHook (session) {
const [data, setData] = useState(session)
const [loading, setLoading] = useState(true)
const [loading, setLoading] = useState(!data)
useEffect(() => {
const _getSession = async ({ event = null } = {}) => {
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
try {
const triggredByEvent = (event !== null)
const triggeredByStorageEvent = !!((event && event === 'storage'))
const triggredByEvent = event !== null
const triggeredByStorageEvent = event === 'storage'
const clientMaxAge = __NEXTAUTH.clientMaxAge
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
const currentTime = Math.floor(new Date().getTime() / 1000)
const currentTime = _now()
const clientSession = __NEXTAUTH._clientSession
// Updates triggered by a storage event *always* trigger an update and we
// always update if we don't have any value for the current session state.
if (triggeredByStorageEvent === false && clientSession !== undefined) {
if (!triggeredByStorageEvent && clientSession !== undefined) {
if (clientMaxAge === 0 && triggredByEvent !== true) {
// If there is no time defined for when a session should be considered
// stale, then it's okay to use the value we have until an event is
@@ -204,13 +110,14 @@ const _useSessionHook = (session) => {
// Update clientLastSync before making response to avoid repeated
// invokations that would otherwise be triggered while we are still
// waiting for a response.
__NEXTAUTH._clientLastSync = Math.floor(new Date().getTime() / 1000)
__NEXTAUTH._clientLastSync = _now()
// If this call was invoked via a storage event (i.e. another window) then
// tell getSession not to trigger an event when it calls to avoid an
// infinate loop.
const triggerEvent = (triggeredByStorageEvent === false)
const newClientSessionData = await getSession({ triggerEvent })
const newClientSessionData = await getSession({
triggerEvent: !triggeredByStorageEvent
})
// Save session state internally, just so we can track that we've checked
// if a session exists at least once.
@@ -220,28 +127,32 @@ const _useSessionHook = (session) => {
setLoading(false)
} catch (error) {
logger.error('CLIENT_USE_SESSION_ERROR', error)
setLoading(false)
}
}
__NEXTAUTH._getSession = _getSession
_getSession()
__NEXTAUTH._getSession()
})
return [data, loading]
}
/**
* Client-side method to initiate a signin flow
* or send the user to the signin page listing all possible providers.
* (Automatically adds the CSRF token to the request)
* @see https://next-auth.js.org/getting-started/client#signin
* @param {string} [provider]
* @param {SignInOptions} [options]
* @param {object} [authorizationParams]
* @return {Promise<SignInResponse | undefined>}
* @typedef {{callbackUrl?: string; redirect?: boolean}} SignInOptions
* @typedef {{error: string | null; status: number; ok: boolean}} SignInResponse
*/
export async function getSession (ctx) {
const session = await _fetchData('session', ctx)
if (ctx?.triggerEvent ?? true) {
broadcast.post({ event: 'session', data: { trigger: 'getSession' } })
}
return session
}
async function getCsrfToken (ctx) {
return (await _fetchData('csrf', ctx))?.csrfToken
}
export async function getProviders () {
return _fetchData('providers')
}
export async function signIn (provider, options = {}, authorizationParams = {}) {
const {
callbackUrl = window.location,
@@ -258,6 +169,9 @@ export async function signIn (provider, options = {}, authorizationParams = {})
return
}
const isCredentials = providers[provider].type === 'credentials'
const isEmail = providers[provider].type === 'email'
const canRedirectBeDisabled = isCredentials || isEmail
const signInUrl = isCredentials
? `${baseUrl}/callback/${provider}`
: `${baseUrl}/signin/${provider}`
@@ -279,7 +193,7 @@ export async function signIn (provider, options = {}, authorizationParams = {})
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
const res = await fetch(_signInUrl, fetchOptions)
const data = await res.json()
if (redirect || !isCredentials) {
if (redirect || !canRedirectBeDisabled) {
const url = data.url ?? callbackUrl
window.location = url
// If url contains a hash, the browser does not reload the page. We reload manually
@@ -302,13 +216,6 @@ export async function signIn (provider, options = {}, authorizationParams = {})
}
}
/**
* Signs the user out, by removing the session cookie.
* (Automatically adds the CSRF token to the request)
* @param {SignOutOptions} [options]
* @returns {Promise<{url?: string} | undefined>}
* @typedef {{callbackUrl?: string; redirect?: boolean;}} SignOutOptions
*/
export async function signOut (options = {}) {
const {
callbackUrl = window.location,
@@ -328,7 +235,7 @@ export async function signOut (options = {}) {
}
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
const data = await res.json()
_sendMessage({ event: 'session', data: { trigger: 'signout' } })
broadcast.post({ event: 'session', data: { trigger: 'signout' } })
if (redirect) {
const url = data.url ?? callbackUrl
window.location = url
@@ -342,40 +249,109 @@ export async function signOut (options = {}) {
return data
}
// Provider to wrap the app in to make session data available globally
export const Provider = ({ children, session, options }) => {
setOptions(options)
return createElement(SessionContext.Provider, { value: useSession(session) }, children)
}
// Method to set options. The documented way is to use the provider, but this
// method is being left in as an alternative, that will be helpful if/when we
// expose a vanilla JavaScript version that doesn't depend on React.
export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {}) {
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
if (basePath) __NEXTAUTH.basePath = basePath
if (clientMaxAge) __NEXTAUTH.clientMaxAge = clientMaxAge
if (keepAlive) {
__NEXTAUTH.keepAlive = keepAlive
if (typeof window === 'undefined') return
const _fetchData = async (url, options = {}) => {
try {
const res = await fetch(url, options)
const data = await res.json()
return Promise.resolve(Object.keys(data).length > 0 ? data : null) // Return null if data empty
} catch (error) {
logger.error('CLIENT_FETCH_ERROR', url, error)
return Promise.resolve(null)
// Clear existing timer (if there is one)
if (__NEXTAUTH._clientSyncTimer !== null) {
clearTimeout(__NEXTAUTH._clientSyncTimer)
}
// Set next timer to trigger in number of seconds
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
// Only invoke keepalive when a session exists
if (!__NEXTAUTH._clientSession) return
await __NEXTAUTH._getSession({ event: 'timer' })
}, keepAlive * 1000)
}
}
const _apiBaseUrl = () => {
export function Provider ({ children, session, options }) {
setOptions(options)
return createElement(
SessionContext.Provider,
{ value: useSession(session) },
children
)
}
/**
* If passed 'appContext' via getInitialProps() in _app.js
* then get the req object from ctx and use that for the
* req value to allow _fetchData to
* work seemlessly in getInitialProps() on server side
* pages *and* in _app.js.
*/
async function _fetchData (path, { ctx, req = ctx?.req } = {}) {
try {
const baseUrl = await _apiBaseUrl()
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const res = await fetch(`${baseUrl}/${path}`, options)
const data = await res.json()
return Object.keys(data).length > 0 ? data : null // Return null if data empty
} catch (error) {
logger.error('CLIENT_FETCH_ERROR', path, error)
return null
}
}
function _apiBaseUrl () {
if (typeof window === 'undefined') {
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
if (!process.env.NEXTAUTH_URL) { logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set') }
if (!process.env.NEXTAUTH_URL) {
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
}
// Return absolute path when called server side
return `${__NEXTAUTH.baseUrl}${__NEXTAUTH.basePath}`
} else {
// Return relative path when called client side
return __NEXTAUTH.basePath
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
}
// Return relative path when called client side
return __NEXTAUTH.basePath
}
const _sendMessage = (message) => {
if (typeof localStorage !== 'undefined') {
const timestamp = Math.floor(new Date().getTime() / 1000)
localStorage.setItem('nextauth.message', JSON.stringify({ ...message, clientId: __NEXTAUTH._clientId, timestamp })) // eslint-disable-line
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
function _now () {
return Math.floor(Date.now() / 1000)
}
/**
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
* Only not using it directly, because Safari does not support it.
*
* https://caniuse.com/?search=broadcastchannel
*/
function BroadcastChannel (name = 'nextauth.message') {
return {
/**
* Get notified by other tabs/windows.
* @param {(message: import("types/internals/client").BroadcastMessage) => void} onReceive
*/
receive (onReceive) {
if (typeof window === 'undefined') return
window.addEventListener('storage', async (event) => {
if (event.key !== name) return
/** @type {import("types/internals/client").BroadcastMessage} */
const message = JSON.parse(event.newValue)
if (message?.event !== 'session' || !message?.data) return
onReceive(message)
})
},
/** Notify other tabs/windows. */
post (message) {
if (typeof localStorage === 'undefined') return
localStorage.setItem(name,
JSON.stringify({ ...message, timestamp: _now() })
)
}
}
}

View File

@@ -106,7 +106,8 @@ async function getToken (params) {
// or not set (e.g. development or test instance) case use unprefixed name
secureCookie = !(!process.env.NEXTAUTH_URL || process.env.NEXTAUTH_URL.startsWith('http://')),
cookieName = (secureCookie) ? '__Secure-next-auth.session-token' : 'next-auth.session-token',
raw = false
raw = false,
decode: _decode = decode
} = params
if (!req) throw new Error('Must pass `req` to JWT getToken()')
@@ -126,7 +127,7 @@ async function getToken (params) {
}
try {
return decode({ token, ...params })
return _decode({ token, ...params })
} catch {
return null
}

5
src/lib/logger.d.ts vendored
View File

@@ -1,5 +0,0 @@
export interface LoggerInstance {
warn: (code?: string, ...message: unknown[]) => void
error: (code?: string, ...message: unknown[]) => void
debug: (code?: string, ...message: unknown[]) => void
}

View File

@@ -1,34 +1,31 @@
/** @type {import("./logger").LoggerInstance} */
/** @type {import("types").LoggerInstance} */
const _logger = {
error (code, ...message) {
error(code, ...message) {
console.error(
`[next-auth][error][${code.toLowerCase()}]`,
`\nhttps://next-auth.js.org/errors#${code.toLowerCase()}`,
...message
)
},
warn (code, ...message) {
warn(code, ...message) {
console.warn(
`[next-auth][warn][${code.toLowerCase()}]`,
`\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}`,
...message
)
},
debug (code, ...message) {
debug(code, ...message) {
if (!process?.env?._NEXTAUTH_DEBUG) return
console.log(
`[next-auth][debug][${code.toLowerCase()}]`,
...message
)
}
console.log(`[next-auth][debug][${code.toLowerCase()}]`, ...message)
},
}
/**
* Override the built-in logger.
* Any `undefined` level will use the default logger.
* @param {Partial<import("./logger").LoggerInstance>} newLogger
* @param {Partial<import("types").LoggerInstance>} newLogger
*/
export function setLogger (newLogger = {}) {
export function setLogger(newLogger = {}) {
if (newLogger.error) _logger.error = newLogger.error
if (newLogger.warn) _logger.warn = newLogger.warn
if (newLogger.debug) _logger.debug = newLogger.debug
@@ -38,13 +35,13 @@ export default _logger
/**
* Serializes client-side log messages and sends them to the server
* @param {import("./logger").LoggerInstance} logger
* @param {import("types").LoggerInstance} logger
* @param {string} basePath
* @return {import("./logger").LoggerInstance}
* @return {import("types").LoggerInstance}
*/
export function proxyLogger (logger = _logger, basePath) {
export function proxyLogger(logger = _logger, basePath) {
try {
if (typeof window === 'undefined') {
if (typeof window === "undefined") {
return logger
}
@@ -57,21 +54,23 @@ export function proxyLogger (logger = _logger, basePath) {
const body = new URLSearchParams({
level,
code,
message: JSON.stringify(message.map(m => {
if (m instanceof Error) {
// Serializing errors: https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
return { name: m.name, message: m.message, stack: m.stack }
}
return m
}))
message: JSON.stringify(
message.map((m) => {
if (m instanceof Error) {
// Serializing errors: https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
return { name: m.name, message: m.message, stack: m.stack }
}
return m
})
),
})
if (navigator.sendBeacon) {
return navigator.sendBeacon(url, body)
}
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body
method: "POST",
headers: { "Content-Type": "application/json" },
body,
})
}
}

View File

@@ -1,30 +1,34 @@
export default (options) => {
export default function Apple(options) {
return {
id: 'apple',
name: 'Apple',
type: 'oauth',
version: '2.0',
scope: 'name email',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://appleid.apple.com/auth/token',
authorizationUrl: 'https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post',
id: "apple",
name: "Apple",
type: "oauth",
version: "2.0",
scope: "name email",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://appleid.apple.com/auth/token",
authorizationUrl:
"https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post",
profileUrl: null,
idToken: true,
profile: (profile) => {
profile(profile) {
// The name of the user will only return on first login
return {
id: profile.sub,
name: profile.user != null ? profile.user.name.firstName + ' ' + profile.user.name.lastName : null,
email: profile.email
name:
profile.user != null
? profile.user.name.firstName + " " + profile.user.name.lastName
: null,
email: profile.email,
}
},
clientId: null,
clientSecret: {
teamId: null,
privateKey: null,
keyId: null
keyId: null,
},
protection: 'none', // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
...options
protection: "none", // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
...options,
}
}

View File

@@ -1,24 +1,24 @@
export default (options) => {
export default function Atlassian(options) {
return {
id: 'atlassian',
name: 'Atlassian',
type: 'oauth',
version: '2.0',
id: "atlassian",
name: "Atlassian",
type: "oauth",
version: "2.0",
params: {
grant_type: 'authorization_code'
grant_type: "authorization_code",
},
accessTokenUrl: 'https://auth.atlassian.com/oauth/token',
accessTokenUrl: "https://auth.atlassian.com/oauth/token",
authorizationUrl:
'https://auth.atlassian.com/authorize?audience=api.atlassian.com&response_type=code&prompt=consent',
profileUrl: 'https://api.atlassian.com/me',
profile: (profile) => {
"https://auth.atlassian.com/authorize?audience=api.atlassian.com&response_type=code&prompt=consent",
profileUrl: "https://api.atlassian.com/me",
profile(profile) {
return {
id: profile.account_id,
name: profile.name,
email: profile.email,
image: profile.picture
image: profile.picture,
}
},
...options
...options,
}
}

View File

@@ -1,22 +1,22 @@
export default (options) => {
export default function Auth0(options) {
return {
id: 'auth0',
name: 'Auth0',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
scope: 'openid email profile',
id: "auth0",
name: "Auth0",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
scope: "openid email profile",
accessTokenUrl: `https://${options.domain}/oauth/token`,
authorizationUrl: `https://${options.domain}/authorize?response_type=code`,
profileUrl: `https://${options.domain}/userinfo`,
profile: (profile) => {
profile(profile) {
return {
id: profile.sub,
name: profile.nickname,
email: profile.email,
image: profile.picture
image: profile.picture,
}
},
...options
...options,
}
}

View File

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

View File

@@ -1,20 +1,22 @@
export default (options) => {
export default function Basecamp(options) {
return {
id: 'basecamp',
name: 'Basecamp',
type: 'oauth',
version: '2.0',
accessTokenUrl: 'https://launchpad.37signals.com/authorization/token?type=web_server',
authorizationUrl: 'https://launchpad.37signals.com/authorization/new?type=web_server',
profileUrl: 'https://launchpad.37signals.com/authorization.json',
profile: (profile) => {
id: "basecamp",
name: "Basecamp",
type: "oauth",
version: "2.0",
accessTokenUrl:
"https://launchpad.37signals.com/authorization/token?type=web_server",
authorizationUrl:
"https://launchpad.37signals.com/authorization/new?type=web_server",
profileUrl: "https://launchpad.37signals.com/authorization.json",
profile(profile) {
return {
id: profile.identity.id,
name: `${profile.identity.first_name} ${profile.identity.last_name}`,
email: profile.identity.email_address,
image: null
image: null,
}
},
...options
...options,
}
}

View File

@@ -1,29 +1,29 @@
export default (options) => {
export default function BattleNet(options) {
const { region } = options
return {
id: 'battlenet',
name: 'Battle.net',
type: 'oauth',
version: '2.0',
scope: 'openid',
params: { grant_type: 'authorization_code' },
id: "battlenet",
name: "Battle.net",
type: "oauth",
version: "2.0",
scope: "openid",
params: { grant_type: "authorization_code" },
accessTokenUrl:
region === 'CN'
? 'https://www.battlenet.com.cn/oauth/token'
region === "CN"
? "https://www.battlenet.com.cn/oauth/token"
: `https://${region}.battle.net/oauth/token`,
authorizationUrl:
region === 'CN'
? 'https://www.battlenet.com.cn/oauth/authorize?response_type=code'
region === "CN"
? "https://www.battlenet.com.cn/oauth/authorize?response_type=code"
: `https://${region}.battle.net/oauth/authorize?response_type=code`,
profileUrl: 'https://us.battle.net/oauth/userinfo',
profile: (profile) => {
profileUrl: "https://us.battle.net/oauth/userinfo",
profile(profile) {
return {
id: profile.id,
name: profile.battletag,
email: null,
image: null
image: null,
}
},
...options
...options,
}
}

View File

@@ -1,22 +1,23 @@
export default (options) => {
export default function Box(options) {
return {
id: 'box',
name: 'Box',
type: 'oauth',
version: '2.0',
scope: '',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.box.com/oauth2/token',
authorizationUrl: 'https://account.box.com/api/oauth2/authorize?response_type=code',
profileUrl: 'https://api.box.com/2.0/users/me',
profile: (profile) => {
id: "box",
name: "Box",
type: "oauth",
version: "2.0",
scope: "",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://api.box.com/oauth2/token",
authorizationUrl:
"https://account.box.com/api/oauth2/authorize?response_type=code",
profileUrl: "https://api.box.com/2.0/users/me",
profile(profile) {
return {
id: profile.id,
name: profile.name,
email: profile.login,
image: profile.avatar_url
image: profile.avatar_url,
}
},
...options
...options,
}
}

View File

@@ -1,30 +1,34 @@
export default (options) => {
export default function Bungie(options) {
return {
id: 'bungie',
name: 'Bungie',
type: 'oauth',
version: '2.0',
scope: '',
params: { reauth: 'true', grant_type: 'authorization_code' },
accessTokenUrl: 'https://www.bungie.net/platform/app/oauth/token/',
requestTokenUrl: 'https://www.bungie.net/platform/app/oauth/token/',
authorizationUrl: 'https://www.bungie.net/en/OAuth/Authorize?response_type=code',
profileUrl: 'https://www.bungie.net/platform/User/GetBungieAccount/{membershipId}/254/',
profile: (profile) => {
id: "bungie",
name: "Bungie",
type: "oauth",
version: "2.0",
scope: "",
params: { reauth: "true", grant_type: "authorization_code" },
accessTokenUrl: "https://www.bungie.net/platform/app/oauth/token/",
requestTokenUrl: "https://www.bungie.net/platform/app/oauth/token/",
authorizationUrl:
"https://www.bungie.net/en/OAuth/Authorize?response_type=code",
profileUrl:
"https://www.bungie.net/platform/User/GetBungieAccount/{membershipId}/254/",
profile(profile) {
const { bungieNetUser: user } = profile.Response
return {
id: user.membershipId,
name: user.displayName,
image: `https://www.bungie.net${user.profilePicturePath.startsWith('/') ? '' : '/'}${user.profilePicturePath}`,
email: null
image: `https://www.bungie.net${
user.profilePicturePath.startsWith("/") ? "" : "/"
}${user.profilePicturePath}`,
email: null,
}
},
headers: {
'X-API-Key': null
"X-API-Key": null,
},
clientId: null,
clientSecret: null,
...options
...options,
}
}

View File

@@ -1,23 +1,23 @@
export default (options) => {
export default function Cognito(options) {
const { domain } = options
return {
id: 'cognito',
name: 'Cognito',
type: 'oauth',
version: '2.0',
scope: 'openid profile email',
params: { grant_type: 'authorization_code' },
id: "cognito",
name: "Cognito",
type: "oauth",
version: "2.0",
scope: "openid profile email",
params: { grant_type: "authorization_code" },
accessTokenUrl: `https://${domain}/oauth2/token`,
authorizationUrl: `https://${domain}/oauth2/authorize?response_type=code`,
profileUrl: `https://${domain}/oauth2/userInfo`,
profile: (profile) => {
profile(profile) {
return {
id: profile.sub,
name: profile.username,
email: profile.email,
image: null
image: null,
}
},
...options
...options,
}
}

View File

@@ -1,10 +1,10 @@
export default (options) => {
export default function Credentials(options) {
return {
id: 'credentials',
name: 'Credentials',
type: 'credentials',
id: "credentials",
name: "Credentials",
type: "credentials",
authorize: null,
credentials: null,
...options
...options,
}
}

View File

@@ -1,29 +1,30 @@
export default (options) => {
export default function Discord(options) {
return {
id: 'discord',
name: 'Discord',
type: 'oauth',
version: '2.0',
scope: 'identify email',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://discord.com/api/oauth2/token',
authorizationUrl: 'https://discord.com/api/oauth2/authorize?response_type=code&prompt=none',
profileUrl: 'https://discord.com/api/users/@me',
profile: (profile) => {
id: "discord",
name: "Discord",
type: "oauth",
version: "2.0",
scope: "identify email",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://discord.com/api/oauth2/token",
authorizationUrl:
"https://discord.com/api/oauth2/authorize?response_type=code&prompt=none",
profileUrl: "https://discord.com/api/users/@me",
profile(profile) {
if (profile.avatar === null) {
const defaultAvatarNumber = parseInt(profile.discriminator) % 5
profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`
} else {
const format = profile.premium_type === 1 || profile.premium_type === 2 ? 'gif' : 'png'
const format = profile.avatar.startsWith("a_") ? "gif" : "png"
profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`
}
return {
id: profile.id,
name: profile.username,
image: profile.image_url,
email: profile.email
email: profile.email,
}
},
...options
...options,
}
}

View File

@@ -1,48 +1,54 @@
import nodemailer from 'nodemailer'
import logger from '../lib/logger'
import nodemailer from "nodemailer"
import logger from "../lib/logger"
export default (options) => {
export default function Email(options) {
return {
id: 'email',
type: 'email',
name: 'Email',
id: "email",
type: "email",
name: "Email",
// Server can be an SMTP connection string or a nodemailer config object
server: {
host: 'localhost',
host: "localhost",
port: 25,
auth: {
user: '',
pass: ''
}
user: "",
pass: "",
},
},
from: 'NextAuth <no-reply@example.com>',
maxAge: 24 * 60 * 60, // How long email links are valid for (default 24h)
from: "NextAuth <no-reply@example.com>",
maxAge: 24 * 60 * 60,
sendVerificationRequest,
...options
...options,
}
}
const sendVerificationRequest = ({ identifier: email, url, baseUrl, provider }) => {
const sendVerificationRequest = ({
identifier: email,
url,
baseUrl,
provider,
}) => {
return new Promise((resolve, reject) => {
const { server, from } = provider
// Strip protocol from URL and use domain as site name
const site = baseUrl.replace(/^https?:\/\//, '')
const site = baseUrl.replace(/^https?:\/\//, "")
nodemailer
.createTransport(server)
.sendMail({
nodemailer.createTransport(server).sendMail(
{
to: email,
from,
subject: `Sign in to ${site}`,
text: text({ url, site, email }),
html: html({ url, site, email })
}, (error) => {
html: html({ url, site, email }),
},
(error) => {
if (error) {
logger.error('SEND_VERIFICATION_EMAIL_ERROR', email, error)
return reject(new Error('SEND_VERIFICATION_EMAIL_ERROR', error))
logger.error("SEND_VERIFICATION_EMAIL_ERROR", email, error)
return reject(new Error("SEND_VERIFICATION_EMAIL_ERROR", error))
}
return resolve()
})
}
)
})
}
@@ -52,16 +58,16 @@ const html = ({ url, site, email }) => {
// email address and the domain from being turned into a hyperlink by email
// clients like Outlook and Apple mail, as this is confusing because it seems
// like they are supposed to click on their email address to sign in.
const escapedEmail = `${email.replace(/\./g, '&#8203;.')}`
const escapedSite = `${site.replace(/\./g, '&#8203;.')}`
const escapedEmail = `${email.replace(/\./g, "&#8203;.")}`
const escapedSite = `${site.replace(/\./g, "&#8203;.")}`
// Some simple styling options
const backgroundColor = '#f9f9f9'
const textColor = '#444444'
const mainBackgroundColor = '#ffffff'
const buttonBackgroundColor = '#346df1'
const buttonBorderColor = '#346df1'
const buttonTextColor = '#ffffff'
const backgroundColor = "#f9f9f9"
const textColor = "#444444"
const mainBackgroundColor = "#ffffff"
const buttonBackgroundColor = "#346df1"
const buttonBorderColor = "#346df1"
const buttonTextColor = "#ffffff"
return `
<body style="background: ${backgroundColor};">

View File

@@ -1,21 +1,22 @@
export default (options) => {
export default function EVEOnline(options) {
return {
id: 'eveonline',
name: 'EVE Online',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://login.eveonline.com/oauth/token',
authorizationUrl: 'https://login.eveonline.com/oauth/authorize?response_type=code',
profileUrl: 'https://login.eveonline.com/oauth/verify',
profile: (profile) => {
id: "eveonline",
name: "EVE Online",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://login.eveonline.com/oauth/token",
authorizationUrl:
"https://login.eveonline.com/oauth/authorize?response_type=code",
profileUrl: "https://login.eveonline.com/oauth/verify",
profile(profile) {
return {
id: profile.CharacterID,
name: profile.CharacterName,
image: `https://image.eveonline.com/Character/${profile.CharacterID}_128.jpg`,
email: null
email: null,
}
},
...options
...options,
}
}

View File

@@ -1,21 +1,22 @@
export default (options) => {
export default function Facebook(options) {
return {
id: 'facebook',
name: 'Facebook',
type: 'oauth',
version: '2.0',
scope: 'email',
accessTokenUrl: 'https://graph.facebook.com/oauth/access_token',
authorizationUrl: 'https://www.facebook.com/v7.0/dialog/oauth?response_type=code',
profileUrl: 'https://graph.facebook.com/me?fields=email,name,picture',
profile: (profile) => {
id: "facebook",
name: "Facebook",
type: "oauth",
version: "2.0",
scope: "email",
accessTokenUrl: "https://graph.facebook.com/oauth/access_token",
authorizationUrl:
"https://www.facebook.com/v7.0/dialog/oauth?response_type=code",
profileUrl: "https://graph.facebook.com/me?fields=email,name,picture",
profile(profile) {
return {
id: profile.id,
name: profile.name,
email: profile.email,
image: profile.picture.data.url
image: profile.picture.data.url,
}
},
...options
...options,
}
}

28
src/providers/faceit.js Normal file
View File

@@ -0,0 +1,28 @@
export default function FACEIT(options) {
return {
id: "faceit",
name: "FACEIT",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
headers: {
Authorization: `Basic ${Buffer.from(
`${options.clientId}:${options.clientSecret}`
).toString("base64")}`,
},
accessTokenUrl: "https://api.faceit.com/auth/v1/oauth/token",
authorizationUrl:
"https://accounts.faceit.com/accounts?redirect_popup=true&response_type=code",
profileUrl: "https://api.faceit.com/auth/v1/resources/userinfo",
profile(profile) {
const { guid: id, nickname: name, email, picture: image } = profile
return {
id,
name,
email,
image,
}
},
...options,
}
}

View File

@@ -1,22 +1,23 @@
export default ({ apiVersion, ...options }) => {
export default function Foursquare(options) {
const { apiVersion } = options
return {
id: 'foursquare',
name: 'Foursquare',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://foursquare.com/oauth2/access_token',
id: "foursquare",
name: "Foursquare",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://foursquare.com/oauth2/access_token",
authorizationUrl:
'https://foursquare.com/oauth2/authenticate?response_type=code',
"https://foursquare.com/oauth2/authenticate?response_type=code",
profileUrl: `https://api.foursquare.com/v2/users/self?v=${apiVersion}`,
profile: (profile) => {
profile(profile) {
return {
id: profile.id,
name: `${profile.firstName} ${profile.lastName}`,
image: `${profile.prefix}original${profile.suffix}`,
email: profile.contact.email
email: profile.contact.email,
}
},
...options
...options,
}
}

View File

@@ -1,27 +1,27 @@
export default (options) => {
export default function FusionAuth(options) {
let authorizationUrl = `https://${options.domain}/oauth2/authorize?response_type=code`
if (options.tenantId) {
authorizationUrl += `&tenantId=${options.tenantId}`
}
return {
id: 'fusionauth',
name: 'FusionAuth',
type: 'oauth',
version: '2.0',
scope: 'openid',
params: { grant_type: 'authorization_code' },
id: "fusionauth",
name: "FusionAuth",
type: "oauth",
version: "2.0",
scope: "openid",
params: { grant_type: "authorization_code" },
accessTokenUrl: `https://${options.domain}/oauth2/token`,
authorizationUrl,
profileUrl: `https://${options.domain}/oauth2/userinfo`,
profile: (profile) => {
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture
image: profile.picture,
}
},
...options
...options,
}
}

View File

@@ -1,21 +1,21 @@
export default (options) => {
export default function GitHub(options) {
return {
id: 'github',
name: 'GitHub',
type: 'oauth',
version: '2.0',
scope: 'user',
accessTokenUrl: 'https://github.com/login/oauth/access_token',
authorizationUrl: 'https://github.com/login/oauth/authorize',
profileUrl: 'https://api.github.com/user',
profile: (profile) => {
id: "github",
name: "GitHub",
type: "oauth",
version: "2.0",
scope: "user",
accessTokenUrl: "https://github.com/login/oauth/access_token",
authorizationUrl: "https://github.com/login/oauth/authorize",
profileUrl: "https://api.github.com/user",
profile(profile) {
return {
id: profile.id,
name: profile.name || profile.login,
email: profile.email,
image: profile.avatar_url
image: profile.avatar_url,
}
},
...options
...options,
}
}

View File

@@ -1,22 +1,22 @@
export default (options) => {
export default function GitLab(options) {
return {
id: 'gitlab',
name: 'GitLab',
type: 'oauth',
version: '2.0',
scope: 'read_user',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://gitlab.com/oauth/token',
authorizationUrl: 'https://gitlab.com/oauth/authorize?response_type=code',
profileUrl: 'https://gitlab.com/api/v4/user',
profile: (profile) => {
id: "gitlab",
name: "GitLab",
type: "oauth",
version: "2.0",
scope: "read_user",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://gitlab.com/oauth/token",
authorizationUrl: "https://gitlab.com/oauth/authorize?response_type=code",
profileUrl: "https://gitlab.com/api/v4/user",
profile(profile) {
return {
id: profile.id,
name: profile.username,
email: profile.email,
image: profile.avatar_url
image: profile.avatar_url,
}
},
...options
...options,
}
}

View File

@@ -1,23 +1,25 @@
export default (options) => {
export default function Google(options) {
return {
id: 'google',
name: 'Google',
type: 'oauth',
version: '2.0',
scope: 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://accounts.google.com/o/oauth2/token',
requestTokenUrl: 'https://accounts.google.com/o/oauth2/auth',
authorizationUrl: 'https://accounts.google.com/o/oauth2/auth?response_type=code',
profileUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
profile: (profile) => {
id: "google",
name: "Google",
type: "oauth",
version: "2.0",
scope:
"https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://accounts.google.com/o/oauth2/token",
requestTokenUrl: "https://accounts.google.com/o/oauth2/auth",
authorizationUrl:
"https://accounts.google.com/o/oauth2/auth?response_type=code",
profileUrl: "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
profile(profile) {
return {
id: profile.id,
name: profile.name,
email: profile.email,
image: profile.picture
image: profile.picture,
}
},
...options
...options,
}
}

View File

@@ -1,17 +1,17 @@
export default (options) => {
export default function IdentityServer4(options) {
return {
id: 'identity-server4',
name: 'IdentityServer4',
type: 'oauth',
version: '2.0',
scope: 'openid profile email',
params: { grant_type: 'authorization_code' },
id: "identity-server4",
name: "IdentityServer4",
type: "oauth",
version: "2.0",
scope: "openid profile email",
params: { grant_type: "authorization_code" },
accessTokenUrl: `https://${options.domain}/connect/token`,
authorizationUrl: `https://${options.domain}/connect/authorize?response_type=code`,
profileUrl: `https://${options.domain}/connect/userinfo`,
profile: (profile) => {
profile(profile) {
return { ...profile, id: profile.sub }
},
...options
...options,
}
}

View File

@@ -1,73 +0,0 @@
import Apple from './apple'
import Atlassian from './atlassian'
import Auth0 from './auth0'
import AzureADB2C from './azure-ad-b2c'
import Basecamp from './basecamp'
import BattleNet from './battlenet'
import Box from './box'
import Bungie from './bungie'
import Cognito from './cognito'
import Credentials from './credentials'
import Discord from './discord'
import Email from './email'
import EVEOnline from './eveonline'
import Facebook from './facebook'
import Foursquare from './foursquare'
import FusionAuth from './fusionauth'
import GitHub from './github'
import GitLab from './gitlab'
import Google from './google'
import IdentityServer4 from './identity-server4'
import LINE from './line'
import LinkedIn from './linkedin'
import MailRu from './mailru'
import Medium from './medium'
import Netlify from './netlify'
import Okta from './okta'
import Reddit from './reddit'
import Salesforce from './salesforce'
import Slack from './slack'
import Spotify from './spotify'
import Strava from './strava'
import Twitch from './twitch'
import Twitter from './twitter'
import VK from './vk'
import Yandex from './yandex'
export default {
Apple,
Atlassian,
Auth0,
AzureADB2C,
Basecamp,
BattleNet,
Box,
Bungie,
Cognito,
Credentials,
Discord,
Email,
EVEOnline,
Facebook,
Foursquare,
FusionAuth,
GitHub,
GitLab,
Google,
IdentityServer4,
LINE,
LinkedIn,
MailRu,
Medium,
Netlify,
Okta,
Reddit,
Salesforce,
Slack,
Spotify,
Strava,
Twitch,
Twitter,
VK,
Yandex
}

View File

@@ -0,0 +1,50 @@
/**
* @type {import("types/providers").OAuthProvider} options
* @example
*
* ```js
* // pages/api/auth/[...nextauth].js
* import Providers from `next-auth/providers`
* ...
* providers: [
* Providers.Instagram({
* clientId: process.env.INSTAGRAM_CLIENT_ID,
* clientSecret: process.env.INSTAGRAM_CLIENT_SECRET
* })
* ]
* ...
*
* // pages/index
* import { signIn } from "next-auth/client"
* ...
* <button onClick={() => signIn("instagram")}>
* Sign in
* </button>
* ...
* ```
* [NextAuth.js Documentation](https://next-auth.js.org/providers/instagram) | [Instagram Documentation](https://developers.facebook.com/docs/instagram-basic-display-api/getting-started) | [Configuration](https://developers.facebook.com/apps)
*/
export default function Instagram(options) {
return {
id: "instagram",
name: "Instagram",
type: "oauth",
version: "2.0",
scope: "user_profile",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://api.instagram.com/oauth/access_token",
authorizationUrl:
"https://api.instagram.com/oauth/authorize?response_type=code",
profileUrl:
"https://graph.instagram.com/me?fields=id,username,account_type,name",
async profile(profile) {
return {
id: profile.id,
name: profile.username,
email: null,
image: null,
}
},
...options,
}
}

22
src/providers/kakao.js Normal file
View File

@@ -0,0 +1,22 @@
export default function Kakao(options) {
return {
id: "kakao",
name: "Kakao",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://kauth.kakao.com/oauth/token",
authorizationUrl:
"https://kauth.kakao.com/oauth/authorize?response_type=code",
profileUrl: "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,
}
}

View File

@@ -1,22 +1,23 @@
export default (options) => {
export default function LINE(options) {
return {
id: 'line',
name: 'LINE',
type: 'oauth',
version: '2.0',
scope: 'profile openid',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.line.me/oauth2/v2.1/token',
authorizationUrl: 'https://access.line.me/oauth2/v2.1/authorize?response_type=code',
profileUrl: 'https://api.line.me/v2/profile',
profile: (profile) => {
id: "line",
name: "LINE",
type: "oauth",
version: "2.0",
scope: "profile openid",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://api.line.me/oauth2/v2.1/token",
authorizationUrl:
"https://access.line.me/oauth2/v2.1/authorize?response_type=code",
profileUrl: "https://api.line.me/v2/profile",
profile(profile) {
return {
id: profile.userId,
name: profile.displayName,
email: null,
image: profile.pictureUrl
image: profile.pictureUrl,
}
},
...options
...options,
}
}

View File

@@ -1,26 +1,28 @@
export default (options) => {
export default function LinkedIn(options) {
return {
id: 'linkedin',
name: 'LinkedIn',
type: 'oauth',
version: '2.0',
scope: 'r_liteprofile',
id: "linkedin",
name: "LinkedIn",
type: "oauth",
version: "2.0",
scope: "r_liteprofile",
params: {
grant_type: 'authorization_code',
grant_type: "authorization_code",
client_id: options.clientId,
client_secret: options.clientSecret
client_secret: options.clientSecret,
},
accessTokenUrl: 'https://www.linkedin.com/oauth/v2/accessToken',
authorizationUrl: 'https://www.linkedin.com/oauth/v2/authorization?response_type=code',
profileUrl: 'https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName)',
profile: (profile) => {
accessTokenUrl: "https://www.linkedin.com/oauth/v2/accessToken",
authorizationUrl:
"https://www.linkedin.com/oauth/v2/authorization?response_type=code",
profileUrl:
"https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName)",
profile(profile) {
return {
id: profile.id,
name: profile.localizedFirstName + ' ' + profile.localizedLastName,
name: profile.localizedFirstName + " " + profile.localizedLastName,
email: null,
image: null
image: null,
}
},
...options
...options,
}
}

View File

@@ -1,25 +1,25 @@
export default (options) => {
export default function MailRu(options) {
return {
id: 'mailru',
name: 'Mail.ru',
type: 'oauth',
version: '2.0',
scope: 'userinfo',
id: "mailru",
name: "Mail.ru",
type: "oauth",
version: "2.0",
scope: "userinfo",
params: {
grant_type: 'authorization_code'
grant_type: "authorization_code",
},
accessTokenUrl: 'https://oauth.mail.ru/token',
requestTokenUrl: 'https://oauth.mail.ru/token',
authorizationUrl: 'https://oauth.mail.ru/login?response_type=code',
profileUrl: 'https://oauth.mail.ru/userinfo',
profile: (profile) => {
accessTokenUrl: "https://oauth.mail.ru/token",
requestTokenUrl: "https://oauth.mail.ru/token",
authorizationUrl: "https://oauth.mail.ru/login?response_type=code",
profileUrl: "https://oauth.mail.ru/userinfo",
profile(profile) {
return {
id: profile.id,
name: profile.name,
email: profile.email,
image: profile.image
image: profile.image,
}
},
...options
...options,
}
}

View File

@@ -1,22 +1,22 @@
export default (options) => {
export default function Medium(options) {
return {
id: 'medium',
name: 'Medium',
type: 'oauth',
version: '2.0',
scope: 'basicProfile',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.medium.com/v1/tokens',
authorizationUrl: 'https://medium.com/m/oauth/authorize?response_type=code',
profileUrl: 'https://api.medium.com/v1/me',
profile: (profile) => {
id: "medium",
name: "Medium",
type: "oauth",
version: "2.0",
scope: "basicProfile",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://api.medium.com/v1/tokens",
authorizationUrl: "https://medium.com/m/oauth/authorize?response_type=code",
profileUrl: "https://api.medium.com/v1/me",
profile(profile) {
return {
id: profile.data.id,
name: profile.data.name,
email: null,
image: profile.data.imageUrl
image: profile.data.imageUrl,
}
},
...options
...options,
}
}

View File

@@ -1,21 +1,21 @@
export default (options) => {
export default function Netlify(options) {
return {
id: 'netlify',
name: 'Netlify',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.netlify.com/oauth/token',
authorizationUrl: 'https://app.netlify.com/authorize?response_type=code',
profileUrl: 'https://api.netlify.com/api/v1/user',
profile: (profile) => {
id: "netlify",
name: "Netlify",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://api.netlify.com/oauth/token",
authorizationUrl: "https://app.netlify.com/authorize?response_type=code",
profileUrl: "https://api.netlify.com/api/v1/user",
profile(profile) {
return {
id: profile.id,
name: profile.full_name,
email: profile.email,
image: profile.avatar_url
image: profile.avatar_url,
}
},
...options
...options,
}
}

View File

@@ -1,22 +1,22 @@
export default (options) => {
export default function Okta(options) {
return {
id: 'okta',
name: 'Okta',
type: 'oauth',
version: '2.0',
scope: 'openid profile email',
id: "okta",
name: "Okta",
type: "oauth",
version: "2.0",
scope: "openid profile email",
params: {
grant_type: 'authorization_code',
grant_type: "authorization_code",
client_id: options.clientId,
client_secret: options.clientSecret
client_secret: options.clientSecret,
},
// These will be different depending on the Org.
accessTokenUrl: `https://${options.domain}/v1/token`,
authorizationUrl: `https://${options.domain}/v1/authorize/?response_type=code`,
profileUrl: `https://${options.domain}/v1/userinfo/`,
profile: (profile) => {
profile(profile) {
return { ...profile, id: profile.sub }
},
...options
...options,
}
}

20
src/providers/osso.js Normal file
View File

@@ -0,0 +1,20 @@
export default function Osso(options) {
return {
id: "osso",
name: "SAML SSO",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
accessTokenUrl: `https://${options.domain}/oauth/token`,
authorizationUrl: `https://${options.domain}/oauth/authorize?response_type=code`,
profileUrl: `https://${options.domain}/oauth/me`,
profile(profile) {
return {
id: profile.id,
name: profile.name || profile.email,
email: profile.email,
}
},
...options,
}
}

View File

@@ -1,23 +1,23 @@
export default (options) => {
export default function Reddit(options) {
return {
id: 'reddit',
name: 'Reddit',
type: 'oauth',
version: '2.0',
scope: 'identity',
params: { grant_type: 'authorization_code' },
accessTokenUrl: ' https://www.reddit.com/api/v1/access_token',
id: "reddit",
name: "Reddit",
type: "oauth",
version: "2.0",
scope: "identity",
params: { grant_type: "authorization_code" },
accessTokenUrl: " https://www.reddit.com/api/v1/access_token",
authorizationUrl:
'https://www.reddit.com/api/v1/authorize?response_type=code',
profileUrl: 'https://oauth.reddit.com/api/v1/me',
profile: (profile) => {
"https://www.reddit.com/api/v1/authorize?response_type=code",
profileUrl: "https://oauth.reddit.com/api/v1/me",
profile(profile) {
return {
id: profile.id,
name: profile.name,
image: null,
email: null
email: null,
}
},
...options
...options,
}
}

View File

@@ -1,21 +1,22 @@
export default (options) => {
export default function Salesforce(options) {
return {
id: 'salesforce',
name: 'Salesforce',
type: 'oauth',
version: '2.0',
params: { display: 'page', grant_type: 'authorization_code' },
accessTokenUrl: 'https://login.salesforce.com/services/oauth2/token',
authorizationUrl: 'https://login.salesforce.com/services/oauth2/authorize?response_type=code',
profileUrl: 'https://login.salesforce.com/services/oauth2/userinfo',
protection: 'none', // REVIEW: Can we use "pkce" ?
profile: (profile) => {
id: "salesforce",
name: "Salesforce",
type: "oauth",
version: "2.0",
params: { display: "page", grant_type: "authorization_code" },
accessTokenUrl: "https://login.salesforce.com/services/oauth2/token",
authorizationUrl:
"https://login.salesforce.com/services/oauth2/authorize?response_type=code",
profileUrl: "https://login.salesforce.com/services/oauth2/userinfo",
protection: "none",
profile(profile) {
return {
...profile,
id: profile.user_id,
image: profile.picture
image: profile.picture,
}
},
...options
...options,
}
}

View File

@@ -1,24 +1,26 @@
export default (options) => {
export default function Slack(options) {
return {
id: 'slack',
name: 'Slack',
type: 'oauth',
version: '2.0',
id: "slack",
name: "Slack",
type: "oauth",
version: "2.0",
scope: [],
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://slack.com/api/oauth.v2.access',
authorizationUrl: 'https://slack.com/oauth/v2/authorize',
authorizationParams: { user_scope: 'identity.basic,identity.email,identity.avatar' },
profileUrl: 'https://slack.com/api/users.identity',
profile: (profile) => {
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://slack.com/api/oauth.v2.access",
authorizationUrl: "https://slack.com/oauth/v2/authorize",
authorizationParams: {
user_scope: "identity.basic,identity.email,identity.avatar",
},
profileUrl: "https://slack.com/api/users.identity",
profile(profile) {
const { user } = profile
return {
id: user.id,
name: user.name,
image: user.image_512,
email: user.email
email: user.email,
}
},
...options
...options,
}
}

View File

@@ -1,23 +1,23 @@
export default (options) => {
export default function Spotify(options) {
return {
id: 'spotify',
name: 'Spotify',
type: 'oauth',
version: '2.0',
scope: 'user-read-email',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://accounts.spotify.com/api/token',
id: "spotify",
name: "Spotify",
type: "oauth",
version: "2.0",
scope: "user-read-email",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://accounts.spotify.com/api/token",
authorizationUrl:
'https://accounts.spotify.com/authorize?response_type=code',
profileUrl: 'https://api.spotify.com/v1/me',
profile: (profile) => {
"https://accounts.spotify.com/authorize?response_type=code",
profileUrl: "https://api.spotify.com/v1/me",
profile(profile) {
return {
id: profile.id,
name: profile.display_name,
email: profile.email,
image: profile.images?.[0]?.url
image: profile.images?.[0]?.url,
}
},
...options
...options,
}
}

View File

@@ -1,22 +1,22 @@
export default (options) => {
export default function Strava(options) {
return {
id: 'strava',
name: 'Strava',
type: 'oauth',
version: '2.0',
scope: 'read',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://www.strava.com/api/v3/oauth/token',
id: "strava",
name: "Strava",
type: "oauth",
version: "2.0",
scope: "read",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://www.strava.com/api/v3/oauth/token",
authorizationUrl:
'https://www.strava.com/api/v3/oauth/authorize?response_type=code',
profileUrl: 'https://www.strava.com/api/v3/athlete',
profile: (profile) => {
"https://www.strava.com/api/v3/oauth/authorize?response_type=code",
profileUrl: "https://www.strava.com/api/v3/athlete",
profile(profile) {
return {
id: profile.id,
name: profile.firstname,
image: profile.profile
image: profile.profile,
}
},
...options
...options,
}
}

View File

@@ -1,24 +1,24 @@
export default (options) => {
export default function Twitch(options) {
return {
id: 'twitch',
name: 'Twitch',
type: 'oauth',
version: '2.0',
scope: 'user:read:email',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://id.twitch.tv/oauth2/token',
id: "twitch",
name: "Twitch",
type: "oauth",
version: "2.0",
scope: "user:read:email",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://id.twitch.tv/oauth2/token",
authorizationUrl:
'https://id.twitch.tv/oauth2/authorize?response_type=code',
profileUrl: 'https://api.twitch.tv/helix/users',
profile: (profile) => {
"https://id.twitch.tv/oauth2/authorize?response_type=code",
profileUrl: "https://api.twitch.tv/helix/users",
profile(profile) {
const data = profile.data[0]
return {
id: data.id,
name: data.display_name,
image: data.profile_image_url,
email: data.email
email: data.email,
}
},
...options
...options,
}
}

View File

@@ -1,23 +1,23 @@
export default (options) => {
export default function Twitter(options) {
return {
id: 'twitter',
name: 'Twitter',
type: 'oauth',
version: '1.0A',
scope: '',
accessTokenUrl: 'https://api.twitter.com/oauth/access_token',
requestTokenUrl: 'https://api.twitter.com/oauth/request_token',
authorizationUrl: 'https://api.twitter.com/oauth/authenticate',
id: "twitter",
name: "Twitter",
type: "oauth",
version: "1.0A",
scope: "",
accessTokenUrl: "https://api.twitter.com/oauth/access_token",
requestTokenUrl: "https://api.twitter.com/oauth/request_token",
authorizationUrl: "https://api.twitter.com/oauth/authenticate",
profileUrl:
'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true',
profile: (profile) => {
"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$/, '.jpg')
image: profile.profile_image_url_https.replace(/_normal\.jpg$/, ".jpg"),
}
},
...options
...options,
}
}

View File

@@ -1,30 +1,29 @@
export default (options) => {
const apiVersion = '5.126' // https://vk.com/dev/versions
export default function VK(options) {
const apiVersion = "5.126" // https://vk.com/dev/versions
return {
id: 'vk',
name: 'VK',
type: 'oauth',
version: '2.0',
scope: 'email',
id: "vk",
name: "VK",
type: "oauth",
version: "2.0",
scope: "email",
params: {
grant_type: 'authorization_code'
grant_type: "authorization_code",
},
accessTokenUrl: `https://oauth.vk.com/access_token?v=${apiVersion}`,
requestTokenUrl: `https://oauth.vk.com/access_token?v=${apiVersion}`,
authorizationUrl:
`https://oauth.vk.com/authorize?response_type=code&v=${apiVersion}`,
authorizationUrl: `https://oauth.vk.com/authorize?response_type=code&v=${apiVersion}`,
profileUrl: `https://api.vk.com/method/users.get?fields=photo_100&v=${apiVersion}`,
profile: (result) => {
const profile = result.response?.[0] ?? {}
return {
id: profile.id,
name: [profile.first_name, profile.last_name].filter(Boolean).join(' '),
name: [profile.first_name, profile.last_name].filter(Boolean).join(" "),
email: profile.email,
image: profile.photo_100
image: profile.photo_100,
}
},
...options
...options,
}
}

View File

@@ -1,23 +1,23 @@
export default (options) => {
export default function Yandex(options) {
return {
id: 'yandex',
name: 'Yandex',
type: 'oauth',
version: '2.0',
scope: 'login:email login:info',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://oauth.yandex.ru/token',
requestTokenUrl: 'https://oauth.yandex.ru/token',
authorizationUrl: 'https://oauth.yandex.ru/authorize?response_type=code',
profileUrl: 'https://login.yandex.ru/info?format=json',
profile: (profile) => {
id: "yandex",
name: "Yandex",
type: "oauth",
version: "2.0",
scope: "login:email login:info",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://oauth.yandex.ru/token",
requestTokenUrl: "https://oauth.yandex.ru/token",
authorizationUrl: "https://oauth.yandex.ru/authorize?response_type=code",
profileUrl: "https://login.yandex.ru/info?format=json",
profile(profile) {
return {
id: profile.id,
name: profile.real_name,
email: profile.default_email,
image: null
image: null,
}
},
...options
...options,
}
}

23
src/providers/zoho.js Normal file
View File

@@ -0,0 +1,23 @@
export default function Zoho(options) {
return {
id: "zoho",
name: "Zoho",
type: "oauth",
version: "2.0",
scope: "AaaServer.profile.Read",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://accounts.zoho.com/oauth/v2/token",
authorizationUrl:
"https://accounts.zoho.com/oauth/v2/auth?response_type=code",
profileUrl: "https://accounts.zoho.com/oauth/user/info",
profile(profile) {
return {
id: profile.ZUID,
name: `${profile.First_Name} ${profile.Last_Name}`,
email: profile.Email,
image: null,
}
},
...options,
}
}

94
src/server/index.d.ts vendored
View File

@@ -1,94 +0,0 @@
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
import { LoggerInstance } from 'src/lib/logger'
import { CallbacksOptions } from './lib/callbacks'
import { CookiesOptions } from './lib/cookie'
import { EventsOptions } from './lib/events'
export interface Provider {
id: string
name: string
type: string
version: string
params: Record<string, unknown>
scope: string
accessTokenUrl: string
authorizationUrl: string
profileUrl?: string
grant_type?: string
profile?: (profile: any) => Promise<any>
}
/** @docs https://next-auth.js.org/configuration/options */
export interface NextAuthOptions {
/** @docs https://next-auth.js.org/configuration/options#theme */
theme?: 'auto' | 'dark' | 'light'
/** @docs https://next-auth.js.org/configuration/options#providers */
providers: Provider[]
/** @docs https://next-auth.js.org/configuration/options#database */
database?: any
/** @docs https://next-auth.js.org/configuration/options#secret */
secret?: any
/** @docs https://next-auth.js.org/configuration/options#session */
session?: any
/** @docs https://next-auth.js.org/configuration/options#jwt */
jwt?: any
/** @docs https://next-auth.js.org/configuration/options#pages */
pages?: {
signIn?: string
signOut?: string
/** Error code passed in query string as ?error= */
error?: string
verifyRequest?: string
/** If set, new users will be directed here on first sign in */
newUser?: string
}
/**
* Callbacks are asynchronous functions you can use to control what happens when an action is performed.
* Callbacks are extremely powerful, especially in scenarios involving JSON Web Tokens as
* they allow you to implement access controls without a database and
* to integrate with external databases or APIs.
* @docs https://next-auth.js.org/configuration/options#callbacks
*/
callbacks?: CallbacksOptions
/** @docs https://next-auth.js.org/configuration/options#events */
events?: EventsOptions
/** @docs https://next-auth.js.org/configuration/options#adapter */
adapter?: any
/** @docs https://next-auth.js.org/configuration/options#debug */
debug?: boolean
/** @docs https://next-auth.js.org/configuration/options#usesecurecookies */
useSecureCookies?: boolean
/** @docs https://next-auth.js.org/configuration/options#cookies */
cookies?: CookiesOptions
/** @docs https://next-auth.js.org/configuration/options#logger */
logger: LoggerInstance
}
/** Options that are the same both in internal and user provided options. */
export type NextAuthSharedOptions = 'pages' | 'jwt' | 'events' | 'callbacks' | 'cookies' | 'secret' | 'adapter' | 'theme' | 'debug' | 'logger'
export interface NextAuthInternalOptions extends Pick<NextAuthOptions, NextAuthSharedOptions> {
pkce?: {
code_verifier?: string
/**
* Could be `"plain"`, but not recommended.
* We ignore it for now.
* @spec https://tools.ietf.org/html/rfc7636#section-4.2.
*/
code_challenge_method?: 'S256'
}
provider?: Provider
baseUrl?: string
basePath?: string
action?: string
csrfToken?: string
}
export interface NextAuthRequest extends NextApiRequest {
options: NextAuthInternalOptions
}
export interface NextAuthResponse extends NextApiResponse {}
export declare function NextAuthHandler (req: NextAuthRequest, res: NextAuthResponse, options: NextAuthOptions): ReturnType<NextApiHandler>
export declare function NextAuthHandler (options: NextAuthOptions): ReturnType<NextApiHandler>

View File

@@ -6,12 +6,12 @@ import * as cookie from './lib/cookie'
import * as defaultEvents from './lib/default-events'
import * as defaultCallbacks from './lib/default-callbacks'
import parseProviders from './lib/providers'
import callbackUrlHandler from './lib/callback-url-handler'
import extendRes from './lib/extend-req'
import * as routes from './routes'
import renderPage from './pages'
import csrfTokenHandler from './lib/csrf-token-handler'
import createSecret from './lib/create-secret'
import callbackUrlHandler from './lib/callback-url-handler'
import extendRes from './lib/extend-res'
import csrfTokenHandler from './lib/csrf-token-handler'
import * as pkce from './lib/oauth/pkce-handler'
import * as state from './lib/oauth/state-handler'
@@ -24,7 +24,7 @@ if (!process.env.NEXTAUTH_URL) {
/**
* @param {import("next").NextApiRequest} req
* @param {import("next").NextApiResponse} res
* @param {import(".").NextAuthOptions} userOptions
* @param {import("types").NextAuthOptions} userOptions
*/
async function NextAuthHandler (req, res, userOptions) {
if (userOptions.logger) {
@@ -67,16 +67,18 @@ async function NextAuthHandler (req, res, userOptions) {
const secret = createSecret({ userOptions, basePath, baseUrl })
const { csrfToken, csrfTokenVerified } = csrfTokenHandler(req, res, cookies, secret)
const providers = parseProviders({ providers: userOptions.providers, baseUrl, basePath })
const provider = providers.find(({ id }) => id === providerId)
if (provider &&
provider.type === 'oauth' && provider.version?.startsWith('2') &&
(!provider.protection && provider.state !== false)
) {
provider.protection = 'state' // Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
// Protection only works on OAuth 2.x providers
if (provider?.type === 'oauth' && provider.version?.startsWith('2')) {
// When provider.state is undefined, we still want this to pass
if (!provider.protection && provider.state !== false) {
// Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
provider.protection = ['state']
} else if (typeof provider.protection === 'string') {
provider.protection = [provider.protection]
}
}
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle
@@ -103,7 +105,6 @@ async function NextAuthHandler (req, res, userOptions) {
provider,
cookies,
secret,
csrfToken,
providers,
// Session options
session: {
@@ -134,6 +135,7 @@ async function NextAuthHandler (req, res, userOptions) {
logger
}
csrfTokenHandler(req, res)
await callbackUrlHandler(req, res)
const render = renderPage(req, res)
@@ -146,7 +148,7 @@ async function NextAuthHandler (req, res, userOptions) {
case 'session':
return routes.session(req, res)
case 'csrf':
return res.json({ csrfToken })
return res.json({ csrfToken: req.options.csrfToken })
case 'signin':
if (pages.signIn) {
let signinUrl = `${pages.signIn}${pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${req.options.callbackUrl}`
@@ -199,7 +201,7 @@ async function NextAuthHandler (req, res, userOptions) {
switch (action) {
case 'signin':
// Verified CSRF Token required for all sign in routes
if (csrfTokenVerified && provider) {
if (req.options.csrfTokenVerified && provider) {
if (await pkce.handleSignin(req, res)) return
if (await state.handleSignin(req, res)) return
return routes.signin(req, res)
@@ -208,14 +210,14 @@ async function NextAuthHandler (req, res, userOptions) {
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
case 'signout':
// Verified CSRF Token required for signout
if (csrfTokenVerified) {
if (req.options.csrfTokenVerified) {
return routes.signout(req, res)
}
return res.redirect(`${baseUrl}${basePath}/signout?csrf=true`)
case 'callback':
if (provider) {
// Verified CSRF Token required for credentials providers only
if (provider.type === 'credentials' && !csrfTokenVerified) {
if (provider.type === 'credentials' && !req.options.csrfTokenVerified) {
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
}
@@ -225,18 +227,19 @@ async function NextAuthHandler (req, res, userOptions) {
}
break
case '_log':
try {
if (!userOptions.logger) return
const {
code = 'CLIENT_ERROR',
level = 'error',
message = '[]'
} = req.body
if (userOptions.logger) {
try {
const {
code = 'CLIENT_ERROR',
level = 'error',
message = '[]'
} = req.body
logger[level](code, ...JSON.parse(message))
} catch (error) {
// If logging itself failed...
logger.error('LOGGER_ERROR', error)
logger[level](code, ...JSON.parse(message))
} catch (error) {
// If logging itself failed...
logger.error('LOGGER_ERROR', error)
}
}
return res.end()
default:

View File

@@ -1,7 +0,0 @@
export interface CallbacksOptions {
signIn?: (user: any, account: any, profile: any) => Promise<never | string>
jwt?: (token: any, user: any, account: any, profile: any, isNewUser?: boolean) => Promise<any>
session?: (session: any, userOrToken: any) => Promise<any>
redirect?: (url: string, baseUrl: string) => Promise<string>
}

View File

@@ -1,16 +0,0 @@
export interface CookieOption {
name: string
options: {
httpOnly: boolean
sameSite: string
path?: string
secure: boolean
}
}
export interface CookiesOptions {
sessionToken: CookieOption
callbackUrl: CookieOption
csrfToken: CookieOption
pkceCodeVerifier: CookieOption
}

View File

@@ -9,7 +9,8 @@
* (with fixes for specific issues) to keep dependancy size down.
*/
export function set (res, name, value, options = {}) {
const stringValue = typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value)
const stringValue =
typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value)
if ('maxAge' in options) {
options.expires = new Date(Date.now() + options.maxAge)
@@ -19,7 +20,9 @@ export function set (res, name, value, options = {}) {
// Preserve any existing cookies that have already been set in the same session
let setCookieHeader = res.getHeader('Set-Cookie') || []
// If not an array (i.e. a string with a single cookie) convert it into an array
if (!Array.isArray(setCookieHeader)) { setCookieHeader = [setCookieHeader] }
if (!Array.isArray(setCookieHeader)) {
setCookieHeader = [setCookieHeader]
}
setCookieHeader.push(_serialize(name, String(stringValue), options))
res.setHeader('Set-Cookie', setCookieHeader)
}
@@ -30,32 +33,44 @@ function _serialize (name, val, options) {
const opt = options || {}
const enc = opt.encode || encodeURIComponent
if (typeof enc !== 'function') { throw new TypeError('option encode is invalid') }
if (typeof enc !== 'function') {
throw new TypeError('option encode is invalid')
}
if (!fieldContentRegExp.test(name)) { throw new TypeError('argument name is invalid') }
if (!fieldContentRegExp.test(name)) {
throw new TypeError('argument name is invalid')
}
const value = enc(val)
if (value && !fieldContentRegExp.test(value)) { throw new TypeError('argument val is invalid') }
if (value && !fieldContentRegExp.test(value)) {
throw new TypeError('argument val is invalid')
}
let str = name + '=' + value
if (opt.maxAge != null) {
const maxAge = opt.maxAge - 0
if (isNaN(maxAge) || !isFinite(maxAge)) { throw new TypeError('option maxAge is invalid') }
if (isNaN(maxAge) || !isFinite(maxAge)) {
throw new TypeError('option maxAge is invalid')
}
str += '; Max-Age=' + Math.floor(maxAge)
}
if (opt.domain) {
if (!fieldContentRegExp.test(opt.domain)) { throw new TypeError('option domain is invalid') }
if (!fieldContentRegExp.test(opt.domain)) {
throw new TypeError('option domain is invalid')
}
str += '; Domain=' + opt.domain
}
if (opt.path) {
if (!fieldContentRegExp.test(opt.path)) { throw new TypeError('option path is invalid') }
if (!fieldContentRegExp.test(opt.path)) {
throw new TypeError('option path is invalid')
}
str += '; Path=' + opt.path
} else {
@@ -73,12 +88,19 @@ function _serialize (name, val, options) {
str += '; Expires=' + expires
}
if (opt.httpOnly) { str += '; HttpOnly' }
if (opt.httpOnly) {
str += '; HttpOnly'
}
if (opt.secure) { str += '; Secure' }
if (opt.secure) {
str += '; Secure'
}
if (opt.sameSite) {
const sameSite = typeof opt.sameSite === 'string' ? opt.sameSite.toLowerCase() : opt.sameSite
const sameSite =
typeof opt.sameSite === 'string'
? opt.sameSite.toLowerCase()
: opt.sameSite
switch (sameSite) {
case true:
@@ -110,7 +132,7 @@ function _serialize (name, val, options) {
* For more on prefixes see https://googlechrome.github.io/samples/cookie-prefixes/
*
* @TODO Review cookie settings (names, options)
* @return {import("./cookie").CookiesOptions}
* @return {import("types").CookiesOptions}
*/
export function defaultCookies (useSecureCookies) {
const cookiePrefix = useSecureCookies ? '__Secure-' : ''

View File

@@ -14,29 +14,30 @@ import * as cookie from './cookie'
* For more details, see the following OWASP links:
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
* https://owasp.org/www-chapter-london/assets/slides/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf
* @param {import("..").NextAuthRequest} req
* @param {import("..").NextAuthResponse} res
*/
export default function csrfTokenHandler (req, res, cookies, secret) {
const { csrfToken: csrfTokenFromRequest } = req.body
let csrfTokenFromCookie
let csrfTokenVerified = false
if (req.cookies[cookies.csrfToken.name]) {
const [csrfTokenValue, csrfTokenHash] = req.cookies[cookies.csrfToken.name].split('|')
if (csrfTokenHash === createHash('sha256').update(`${csrfTokenValue}${secret}`).digest('hex')) {
export default function csrfTokenHandler (req, res) {
const { cookies, secret } = req.options
if (cookies.csrfToken.name in req.cookies) {
const [csrfToken, csrfTokenHash] = req.cookies[cookies.csrfToken.name].split('|')
const expectedCsrfTokenHash = createHash('sha256').update(`${csrfToken}${secret}`).digest('hex')
if (csrfTokenHash === expectedCsrfTokenHash) {
// If hash matches then we trust the CSRF token value
csrfTokenFromCookie = csrfTokenValue
// If this is a POST request and the CSRF Token in the Post request matches
// the cookie we have already verified is one we have set, then token is verified!
if (req.method === 'POST' && csrfTokenFromCookie === csrfTokenFromRequest) { csrfTokenVerified = true }
// If this is a POST request and the CSRF Token in the POST request matches
// the cookie we have already verified is the one we have set, then the token is verified!
const csrfTokenVerified = req.method === 'POST' && csrfToken === req.body.csrfToken
req.options.csrfToken = csrfToken
req.options.csrfTokenVerified = csrfTokenVerified
return
}
}
if (!csrfTokenFromCookie) {
// If no csrfToken - because it's not been set yet, or because the hash doesn't match
// (e.g. because it's been modifed or because the secret has changed) create a new token.
csrfTokenFromCookie = randomBytes(32).toString('hex')
const newCsrfTokenCookie = `${csrfTokenFromCookie}|${createHash('sha256').update(`${csrfTokenFromCookie}${secret}`).digest('hex')}`
cookie.set(res, cookies.csrfToken.name, newCsrfTokenCookie, cookies.csrfToken.options)
}
return { csrfToken: csrfTokenFromCookie, csrfTokenVerified }
// If no csrfToken from cookie - because it's not been set yet,
// or because the hash doesn't match (e.g. because it's been modifed or because the secret has changed)
// create a new token.
const csrfToken = randomBytes(32).toString('hex')
const csrfTokenHash = createHash('sha256').update(`${csrfToken}${secret}`).digest('hex')
const csrfTokenCookie = `${csrfToken}|${csrfTokenHash}`
cookie.set(res, cookies.csrfToken.name, csrfTokenCookie, cookies.csrfToken.options)
req.options.csrfToken = csrfToken
}

View File

@@ -1,12 +0,0 @@
export type EventType=
| 'signIn'
| 'signOut'
| 'createUser'
| 'updateUser'
| 'linkAccount'
| 'session'
| 'error'
export type EventCallback = (message: any) => Promise<void>
export type EventsOptions = Partial<Record<EventType, EventCallback>>

View File

@@ -1,19 +1,19 @@
import { decode as jwtDecode } from 'jsonwebtoken'
import oAuthClient from './client'
import logger from '../../../lib/logger'
import { OAuthCallbackError } from '../../../lib/errors'
import { decode as jwtDecode } from "jsonwebtoken"
import oAuthClient from "./client"
import logger from "../../../lib/logger"
import { OAuthCallbackError } from "../../../lib/errors"
/** @param {import("../..").NextAuthRequest} req */
export default async function oAuthCallback (req) {
/** @param {import("types/internals").NextAuthRequest} req */
export default async function oAuthCallback(req) {
const { provider, pkce } = req.options
const client = oAuthClient(provider)
if (provider.version?.startsWith('2.')) {
if (provider.version?.startsWith("2.")) {
// The "user" object is specific to the Apple provider and is provided on first sign in
// e.g. {"name":{"firstName":"Johnny","lastName":"Appleseed"},"email":"johnny.appleseed@nextauth.com"}
let { code, user } = req.query // eslint-disable-line camelcase
if (req.method === 'POST') {
if (req.method === "POST") {
try {
const body = JSON.parse(JSON.stringify(req.body))
if (body.error) {
@@ -23,25 +23,35 @@ export default async function oAuthCallback (req) {
code = body.code
user = body.user != null ? JSON.parse(body.user) : null
} catch (error) {
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error, req.body, provider.id, code)
logger.error(
"OAUTH_CALLBACK_HANDLER_ERROR",
error,
req.body,
provider.id,
code
)
throw error
}
}
// REVIEW: Is this used by any of the providers?
// Pass authToken in header by default (unless 'useAuthTokenHeader: false' is set)
if (Object.prototype.hasOwnProperty.call(provider, 'useAuthTokenHeader')) {
if (Object.prototype.hasOwnProperty.call(provider, "useAuthTokenHeader")) {
client.useAuthorizationHeaderforGET(provider.useAuthTokenHeader)
} else {
client.useAuthorizationHeaderforGET(true)
}
try {
const tokens = await client.getOAuthAccessToken(code, provider, pkce.code_verifier)
const tokens = await client.getOAuthAccessToken(
code,
provider,
pkce.code_verifier
)
let profileData
if (provider.idToken) {
if (!tokens?.id_token) {
throw new OAuthCallbackError('Missing JWT ID Token')
throw new OAuthCallbackError("Missing JWT ID Token")
}
// Support services that use OpenID ID Tokens to encode profile data
@@ -52,26 +62,28 @@ export default async function oAuthCallback (req) {
return getProfile({ profileData, provider, tokens, user })
} catch (error) {
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, provider.id, code)
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error, provider.id, code)
throw error
}
}
try {
// Handle OAuth v1.x
const {
oauth_token: oauthToken, oauth_verifier: oauthVerifier
} = req.query
const tokens = await client.getOAuthAccessToken(oauthToken, null, oauthVerifier)
// eslint-disable-next-line camelcase
const { oauth_token, oauth_verifier } = req.query
// eslint-disable-next-line camelcase
const { token_secret } = await client.getOAuthRequestToken(provider.params)
const tokens = await client.getOAuthAccessToken(oauth_token, token_secret, oauth_verifier)
const profileData = await client.get(
provider.profileUrl,
tokens.accessToken,
tokens.refreshToken
tokens.oauth_token,
tokens.oauth_token_secret
)
return getProfile({ profileData, tokens, provider })
} catch (error) {
logger.error('OAUTH_V1_GET_ACCESS_TOKEN_ERROR', error)
logger.error("OAUTH_V1_GET_ACCESS_TOKEN_ERROR", error)
throw error
}
}
@@ -89,15 +101,19 @@ export default async function oAuthCallback (req) {
* expires_in?: string | Date | null
* refresh_token?: string
* id_token?: string
* token?: string
* token_secret?: string
* tokenSecret?: string
* params?: any
* }
* provider: import("../..").Provider
* user?: object
* }} profileParams
*/
async function getProfile ({ profileData, tokens, provider, user }) {
async function getProfile({ profileData, tokens, provider, user }) {
try {
// Convert profileData into an object if it's a string
if (typeof profileData === 'string' || profileData instanceof String) {
if (typeof profileData === "string" || profileData instanceof String) {
profileData = JSON.parse(profileData)
}
@@ -106,22 +122,22 @@ async function getProfile ({ profileData, tokens, provider, user }) {
profileData.user = user
}
logger.debug('PROFILE_DATA', profileData)
logger.debug("PROFILE_DATA", profileData)
const profile = await provider.profile(profileData)
const profile = await provider.profile(profileData, tokens)
// Return profile, raw profile and auth provider details
return {
profile: {
...profile,
email: profile.email?.toLowerCase() ?? null
email: profile.email?.toLowerCase() ?? null,
},
account: {
provider: provider.id,
type: provider.type,
id: profile.id,
...tokens
...tokens,
},
OAuthProfile: profileData
OAuthProfile: profileData,
}
} catch (exception) {
// If we didn't get a response either there was a problem with the provider
@@ -131,11 +147,11 @@ async function getProfile ({ profileData, tokens, provider, user }) {
// all providers, so we return an empty object; the user should then be
// redirected back to the sign up page. We log the error to help developers
// who might be trying to debug this when configuring a new provider.
logger.error('OAUTH_PARSE_PROFILE_ERROR', exception, profileData)
logger.error("OAUTH_PARSE_PROFILE_ERROR", exception, profileData)
return {
profile: null,
account: null,
OAuthProfile: profileData
OAuthProfile: profileData,
}
}
}

View File

@@ -7,7 +7,7 @@ import { sign as jwtSign } from 'jsonwebtoken'
* @TODO Refactor to remove dependancy on 'oauth' package
* It is already quite monkey patched, we don't use all the features and and it
* would be easier to maintain if all the code was native to next-auth.
* @param {import("../..").Provider} provider
* @param {import("types/providers").OAuthConfig} provider
*/
export default function oAuthClient (provider) {
if (provider.version?.startsWith('2.')) {
@@ -54,23 +54,36 @@ export default function oAuthClient (provider) {
const originalGetOAuth1AccessToken = oauth1Client.getOAuthAccessToken.bind(oauth1Client)
oauth1Client.getOAuthAccessToken = (...args) => {
return new Promise((resolve, reject) => {
originalGetOAuth1AccessToken(...args, (error, accessToken, refreshToken, results) => {
// eslint-disable-next-line camelcase
originalGetOAuth1AccessToken(...args, (error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
}
resolve({ accessToken, refreshToken, results })
resolve({
// TODO: Remove, this is only kept for backward compativility
// These are not in the OAuth 1.x spec
accessToken: oauth_token,
refreshToken: oauth_token_secret,
results: params,
oauth_token,
oauth_token_secret,
params
})
})
})
}
const originalGetOAuthRequestToken = oauth1Client.getOAuthRequestToken.bind(oauth1Client)
oauth1Client.getOAuthRequestToken = (...args) => {
oauth1Client.getOAuthRequestToken = (params = {}) => {
return new Promise((resolve, reject) => {
originalGetOAuthRequestToken(...args, (error, oauthToken) => {
// eslint-disable-next-line camelcase
originalGetOAuthRequestToken(params, (error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
}
resolve(oauthToken)
resolve({ oauth_token, oauth_token_secret, params })
})
})
}
@@ -88,7 +101,7 @@ export default function oAuthClient (provider) {
/**
* Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js
* @param {string} code
* @param {import("../..").Provider} provider
* @param {import("types/providers").OAuthConfig} provider
* @param {string | undefined} codeVerifier
*/
async function getOAuth2AccessToken (code, provider, codeVerifier) {
@@ -136,7 +149,7 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
headers.Authorization = `Bearer ${code}`
}
if (provider.protection === 'pkce') {
if (provider.protection.includes('pkce')) {
params.code_verifier = codeVerifier
}
@@ -167,9 +180,17 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
raw = querystring.parse(data)
}
const accessToken = provider.id === 'slack'
? raw.authed_user.access_token
: raw.access_token
let accessToken
if (provider.id === 'slack') {
const { ok, error } = raw
if (!ok) {
return reject(error)
}
accessToken = raw.authed_user.access_token
} else {
accessToken = raw.access_token
}
resolve({
accessToken,
@@ -188,7 +209,7 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
*
* 18/08/2020 @robertcraigie added results parameter to pass data to an optional request preparer.
* e.g. see providers/bungie
* @param {import("../..").Provider} provider
* @param {import("types/providers").OAuthConfig} provider
* @param {string} accessToken
* @param {any} results
*/

View File

@@ -10,13 +10,14 @@ const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
/**
* Adds `code_verifier` to `req.options.pkce`, and removes the corresponding cookie
* @param {import("../..").NextAuthRequest} req
* @param {import("../..").NextAuthResponse} res
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export async function handleCallback (req, res) {
const { cookies, provider, baseUrl, basePath } = req.options
try {
if (provider.protection !== 'pkce') { // Provider does not support PKCE, nothing to do.
// Provider does not support PKCE, nothing to do.
if (!provider.protection?.includes('pkce')) {
return
}
@@ -44,13 +45,13 @@ export async function handleCallback (req, res) {
/**
* Adds `code_challenge` and `code_challenge_method` to `req.options.pkce`.
* @param {import("../..").NextAuthRequest} req
* @param {import("../..").NextAuthResponse} res
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export async function handleSignin (req, res) {
const { cookies, provider, baseUrl, basePath } = req.options
try {
if (provider.protection !== 'pkce') { // Provider does not support PKCE, nothing to do.
if (!provider.protection?.includes('pkce')) { // Provider does not support PKCE, nothing to do.
return
}
// Started login flow, add generated pkce to req.options and (encrypted) code_verifier to a cookie

View File

@@ -6,17 +6,18 @@ import { OAuthCallbackError } from '../../../lib/errors'
* For OAuth 2.0 flows, if the provider supports state,
* check if state matches the one sent on signin
* (a hash of the NextAuth.js CSRF token).
* @param {import("../..").NextAuthRequest} req
* @param {import("../..").NextAuthResponse} res
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export async function handleCallback (req, res) {
const { csrfToken, provider, baseUrl, basePath } = req.options
try {
if (provider.protection !== 'state') { // Provider does not support state, nothing to do.
// Provider does not support state, nothing to do.
if (!provider.protection?.includes('state')) {
return
}
const { state } = req.query
const state = req.query.state || req.body.state
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
logger.debug(
@@ -35,13 +36,13 @@ export async function handleCallback (req, res) {
/**
* Adds CSRF token to the authorizationParams.
* @param {import("../..").NextAuthRequest} req
* @param {import("../..").NextAuthResponse} res
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export async function handleSignin (req, res) {
const { provider, baseUrl, basePath, csrfToken } = req.options
try {
if (provider.protection !== 'state') { // Provider does not support state, nothing to do.
if (!provider.protection?.includes('state')) { // Provider does not support state, nothing to do.
return
}

View File

@@ -10,7 +10,7 @@ export default async function email (email, provider, options) {
const secret = provider.secret || options.secret
// Generate token
const token = provider.generateVerificationToken?.() ?? randomBytes(32).toString('hex')
const token = await provider.generateVerificationToken?.() ?? randomBytes(32).toString('hex')
// Send email with link containing token (the unhashed version)
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`

View File

@@ -1,17 +1,21 @@
import oAuthClient from '../oauth/client'
import logger from '../../../lib/logger'
/** @param {import("../..").NextAuthRequest} req */
/** @param {import("types/internals").NextAuthRequest} req */
export default async function getAuthorizationUrl (req) {
const { provider } = req.options
delete req.query?.nextauth
const params = {
...provider.authorizationParams,
...req.query
}
const client = oAuthClient(provider)
if (provider.version?.startsWith('2.')) {
delete req.query?.nextauth
// Handle OAuth v2.x
let url = client.getAuthorizeUrl({
...provider.authorizationParams,
...req.query,
...params,
redirect_uri: provider.callbackUrl,
scope: provider.scope
})
@@ -34,8 +38,12 @@ export default async function getAuthorizationUrl (req) {
}
try {
const oAuthToken = await client.getOAuthRequestToken()
const url = `${provider.authorizationUrl}?oauth_token=${oAuthToken}`
const tokens = await client.getOAuthRequestToken(params)
const url = `${provider.authorizationUrl}?${new URLSearchParams({
oauth_token: tokens.oauth_token,
oauth_token_secret: tokens.oauth_token_secret,
...tokens.params
})}`
logger.debug('GET_AUTHORIZATION_URL', url)
return url
} catch (error) {

View File

@@ -1,6 +1,5 @@
// @ts-check
import { h } from 'preact' // eslint-disable-line no-unused-vars
import render from 'preact-render-to-string'
/**
* Renders an error page.
@@ -8,7 +7,7 @@ import render from 'preact-render-to-string'
* baseUrl: string
* basePath: string
* error?: string
* res: import("..").NextAuthResponse
* res: import("types/internals").NextAuthResponse
* }} params
*/
export default function error ({ baseUrl, basePath, error = 'default', res }) {
@@ -53,11 +52,11 @@ export default function error ({ baseUrl, basePath, error = 'default', res }) {
}
}
const { statusCode, heading, message, signin } = errors[error.toLowerCase()]
const { statusCode, heading, message, signin } = errors[error.toLowerCase()] ?? errors.default
res.status(statusCode)
return render(
return (
<div className='error'>
<h1>{heading}</h1>
<div className='message'>{message}</div>

View File

@@ -1,3 +1,4 @@
import renderToString from 'preact-render-to-string'
import signin from './signin'
import signout from './signout'
import verifyRequest from './verify-request'
@@ -9,14 +10,34 @@ export default function renderPage (req, res) {
const { baseUrl, basePath, callbackUrl, csrfToken, providers, theme } = req.options
res.setHeader('Content-Type', 'text/html')
function send (html) {
res.send(`<!DOCTYPE html><head><style type="text/css">${css()}</style><meta name="viewport" content="width=device-width, initial-scale=1"></head><body class="__next-auth-theme-${theme}"><div class="page">${html}</div></body></html>`)
function send ({ html, title }) {
res.send(`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>${css()}</style><title>${title}</title></head><body class="__next-auth-theme-${theme}"><div class="page">${renderToString(html)}</div></body></html>`)
}
return {
signin (props) { send(signin({ csrfToken, providers, callbackUrl, ...req.query, ...props })) },
signout (props) { send(signout({ csrfToken, baseUrl, basePath, ...props })) },
verifyRequest (props) { send(verifyRequest({ baseUrl, ...props })) },
error (props) { send(error({ basePath, baseUrl, res, ...props })) }
signin (props) {
send({
html: signin({ csrfToken, providers, callbackUrl, ...req.query, ...props }),
title: 'Sign In'
})
},
signout (props) {
send({
html: signout({ csrfToken, baseUrl, basePath, ...props }),
title: 'Sign Out'
})
},
verifyRequest (props) {
send({
html: verifyRequest({ baseUrl, ...props }),
title: 'Verify Request'
})
},
error (props) {
send({
html: error({ basePath, baseUrl, res, ...props }),
title: 'Error'
})
}
}
}

View File

@@ -1,5 +1,4 @@
import { h } from 'preact' // eslint-disable-line no-unused-vars
import render from 'preact-render-to-string'
export default function signin ({ csrfToken, providers, callbackUrl, email, error: errorType }) {
// We only want to render providers
@@ -30,7 +29,7 @@ export default function signin ({ csrfToken, providers, callbackUrl, email, erro
const error = errorType && (errors[errorType] ?? errors.default)
return render(
return (
<div className='signin'>
{error &&
<div className='error'>

View File

@@ -1,8 +1,7 @@
import { h } from 'preact' // eslint-disable-line no-unused-vars
import render from 'preact-render-to-string'
export default function signout ({ baseUrl, basePath, csrfToken }) {
return render(
return (
<div className='signout'>
<h1>Are you sure you want to sign out?</h1>
<form action={`${baseUrl}${basePath}/signout`} method='POST'>

View File

@@ -1,8 +1,7 @@
import { h } from 'preact' // eslint-disable-line no-unused-vars
import render from 'preact-render-to-string'
export default function verifyRequest ({ baseUrl }) {
return render(
return (
<div className='verify-request'>
<h1>Check your email</h1>
<p>A sign in link has been sent to your email address.</p>

View File

@@ -6,8 +6,8 @@ import dispatchEvent from '../lib/dispatch-event'
/**
* Handle callbacks from login services
* @param {import("..").NextAuthRequest} req
* @param {import("..").NextAuthResponse} res
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export default async function callback (req, res) {
const {
@@ -72,7 +72,7 @@ export default async function callback (req, res) {
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
@@ -168,7 +168,7 @@ export default async function callback (req, res) {
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
@@ -239,7 +239,7 @@ export default async function callback (req, res) {
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
return res.redirect(error)
}
@@ -254,7 +254,7 @@ export default async function callback (req, res) {
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
return res.redirect(error)
}
@@ -262,7 +262,8 @@ export default async function callback (req, res) {
const defaultJwtPayload = {
name: user.name,
email: user.email,
picture: user.image
picture: user.image,
sub: user.id?.toString()
}
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, userObjectReturnedFromAuthorizeHandler, false)

View File

@@ -2,8 +2,8 @@
* Return a JSON object with a list of all OAuth providers currently configured
* and their signin and callback URLs. This makes it possible to automatically
* generate buttons for all providers when rendering client side.
* @param {import("..").NextAuthRequest} req
* @param {import("..").NextAuthResponse} res
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export default function providers (req, res) {
const { providers } = req.options

View File

@@ -3,6 +3,9 @@
"strictNullChecks": true,
"baseUrl": ".",
"paths": {
"types": [
"./types"
],
"next-auth": [
"./src/server"
],
@@ -40,7 +43,8 @@
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
"**/*.tsx",
"**/*.js"
],
"exclude": [
"node_modules"

244
types/adapters.d.ts vendored Normal file
View File

@@ -0,0 +1,244 @@
import { AppOptions } from "./internals"
import { ConnectionOptions, EntitySchema } from "typeorm"
import { User } from "."
import { AppProvider } from "./internals/providers"
export interface Profile {
id: string
name: string
email: string | null
image?: string | null
}
export interface Session {
userId: string | number | object
expires: Date
sessionToken: string
accessToken: string
}
export interface VerificationRequest {
identifier: string
token: string
expires: Date
}
export interface SendVerificationRequestParams {
identifier: string
url: string
token: string
baseUrl: string
provider: AppProvider
}
export type EmailAppProvider = AppProvider & {
sendVerificationRequest: (
params: SendVerificationRequestParams
) => Promise<void>
maxAge: number | undefined
}
export interface AdapterInstance<
TUser,
TProfile,
TSession,
TVerificationRequest
> {
createUser: (profile: TProfile) => Promise<TUser>
getUser: (id: string) => Promise<TUser | null>
getUserByEmail: (email: string) => Promise<TUser | null>
getUserByProviderAccountId: (
providerId: string,
providerAccountId: string
) => Promise<TUser | null>
updateUser: (user: TUser) => Promise<TUser>
linkAccount: (
userId: string,
providerId: string,
providerType: string,
providerAccountId: string,
refreshToken: string,
accessToken: string,
accessTokenExpires: number
) => Promise<void>
createSession: (user: TUser) => Promise<TSession>
getSession: (sessionToken: string) => Promise<TSession | null>
updateSession: (session: TSession, force?: boolean) => Promise<TSession>
deleteSession: (sessionToken: string) => Promise<void>
createVerificationRequest?: (
email: string,
url: string,
token: string,
secret: string,
provider: EmailAppProvider,
options: AppOptions
) => Promise<TVerificationRequest>
getVerificationRequest?: (
email: string,
verificationToken: string,
secret: string,
provider: AppProvider
) => Promise<TVerificationRequest | null>
deleteVerificationRequest?: (
email: string,
verificationToken: string,
secret: string,
provider: AppProvider
) => Promise<void>
}
interface Adapter<
TUser extends User = any,
TProfile extends Profile = any,
TSession extends Session = any,
TVerificationRequest extends VerificationRequest = any
> {
getAdapter: (
appOptions: AppOptions
) => Promise<AdapterInstance<TUser, TProfile, TSession, TVerificationRequest>>
}
type Schema<T = any> = EntitySchema<T>["options"]
interface BuiltInAdapters {
Default: TypeORMAdapter["Adapter"]
TypeORM: TypeORMAdapter
Prisma: PrismaAdapter
}
/**
* TODO: fix auto-type schema
*/
interface TypeORMAdapter<
A extends TypeORMAccountModel = any,
U extends TypeORMUserModel = any,
S extends TypeORMSessionModel = any,
VR extends TypeORMVerificationRequestModel = any
> {
Adapter: (
typeOrmConfig: ConnectionOptions,
options?: {
models?: {
Account?: {
model: A
schema: Schema<A>
}
User?: {
model: U
schema: Schema<U>
}
Session?: {
model: S
schema: Schema<S>
}
VerificationRequest?: {
model: VR
schema: Schema<VR>
}
}
}
) => Adapter<U, Profile, S, VR>
Models: {
Account: {
model: TypeORMAccountModel
schema: Schema<TypeORMAccountModel>
}
User: {
model: TypeORMUserModel
schema: Schema<TypeORMUserModel>
}
Session: {
model: TypeORMSessionModel
schema: Schema<TypeORMSessionModel>
}
VerificationRequest: {
model: TypeORMVerificationRequestModel
schema: Schema<TypeORMVerificationRequestModel>
}
}
}
interface PrismaAdapter {
Adapter: (config: {
prisma: any
modelMapping?: {
User: string
Account: string
Session: string
VerificationRequest: string
}
}) => Adapter
}
declare class TypeORMAccountModel {
compoundId: string
userId: number
providerType: string
providerId: string
providerAccountId: string
refreshToken?: string
accessToken?: string
accessTokenExpires?: Date
constructor(
userId: number,
providerId: string,
providerType: string,
providerAccountId: string,
refreshToken?: string,
accessToken?: string,
accessTokenExpires?: Date
)
}
declare class TypeORMUserModel implements User {
name?: string
email?: string
image?: string
emailVerified?: Date
constructor(
name?: string,
email?: string,
image?: string,
emailVerified?: Date
)
}
declare class TypeORMSessionModel implements Session {
userId: number
expires: Date
sessionToken: string
accessToken: string
constructor(
userId: number,
expires: Date,
sessionToken?: string,
accessToken?: string
)
}
declare class TypeORMVerificationRequestModel implements VerificationRequest {
identifier: string
token: string
expires: Date
constructor(identifier: string, token: string, expires: Date)
}
declare const Adapters: BuiltInAdapters
export default Adapters
export {
Adapter,
BuiltInAdapters as Adapters,
TypeORMAdapter,
TypeORMAccountModel,
TypeORMUserModel,
TypeORMSessionModel,
TypeORMVerificationRequestModel,
PrismaAdapter,
}

218
types/client.d.ts vendored Normal file
View File

@@ -0,0 +1,218 @@
import * as React from "react"
import { IncomingMessage } from "http"
import { Session } from "."
import { ProviderType } from "./providers"
export interface CtxOrReq {
req?: IncomingMessage
ctx?: { req: IncomingMessage }
}
/***************
* Session types
**************/
export type GetSessionOptions = CtxOrReq & {
event?: "storage" | "timer" | "hidden" | string
triggerEvent?: boolean
}
/**
* React Hook that gives you access
* to the logged in user's session data.
*
* [Documentation](https://next-auth.js.org/getting-started/client#usesession)
*/
export function useSession(): [Session | null, boolean]
/**
* Can be called client or server side to return a session asynchronously.
* It calls `/api/auth/session` and returns a promise with a session object,
* or null if no session exists.
*
* [Documentation](https://next-auth.js.org/getting-started/client#getsession)
*/
export function getSession(options: GetSessionOptions): Promise<Session | null>
/**
* Alias for `getSession`
* @docs https://next-auth.js.org/getting-started/client#getsession
*/
export const session: typeof getSession
/*******************
* CSRF Token types
******************/
/**
* Returns the current Cross Site Request Forgery Token (CSRF Token)
* required to make POST requests (e.g. for signing in and signing out).
* You likely only need to use this if you are not using the built-in
* `signIn()` and `signOut()` methods.
*
* [Documentation](https://next-auth.js.org/getting-started/client#getcsrftoken)
*/
export function getCsrfToken(ctxOrReq: CtxOrReq): Promise<string | null>
/**
* Alias for `getCsrfToken`
* @docs https://next-auth.js.org/getting-started/client#getcsrftoken
*/
export const csrfToken: typeof getCsrfToken
/******************
* Providers types
*****************/
export interface ClientSafeProvider {
id: string
name: string
type: ProviderType
signinUrl: string
callbackUrl: string
}
/**
* It calls `/api/auth/providers` and returns
* a list of the currently configured authentication providers.
* It can be useful if you are creating a dynamic custom sign in page.
*
* [Documentation](https://next-auth.js.org/getting-started/client#getproviders)
*/
export function getProviders(): Promise<Record<
string,
ClientSafeProvider
> | null>
/**
* Alias for `getProviders`
* @docs https://next-auth.js.org/getting-started/client#getproviders
*/
export const providers: typeof getProviders
/****************
* Sign in types
***************/
export type RedirectableProvider = "email" | "credentials"
export type SignInProvider = RedirectableProvider | string | undefined
export interface SignInOptions extends Record<string, unknown> {
/**
* Defaults to the current URL.
* @docs https://next-auth.js.org/getting-started/client#specifying-a-callbackurl
*/
callbackUrl?: string
/** @docs https://next-auth.js.org/getting-started/client#using-the-redirect-false-option */
redirect?: boolean
}
export interface SignInResponse {
error: string | undefined
status: number
ok: boolean
url: string | null
}
/** Match `inputType` of `new URLSearchParams(inputType)` */
export type SignInAuthorisationParams =
| string
| string[][]
| Record<string, string>
| URLSearchParams
/**
* Client-side method to initiate a signin flow
* or send the user to the signin page listing all possible providers.
* Automatically adds the CSRF token to the request.
*
* [Documentation](https://next-auth.js.org/getting-started/client#signin)
*/
export function signIn<P extends SignInProvider = undefined>(
provider?: P,
options?: SignInOptions,
authorizationParams?: SignInAuthorisationParams
): Promise<
P extends RedirectableProvider ? SignInResponse | undefined : undefined
>
/**
* Alias for `signIn`
* @docs https://next-auth.js.org/getting-started/client#signin
*/
export const signin: typeof signIn
/****************
* Sign out types
****************/
/** @docs https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1 */
export interface SignOutResponse {
url: string
}
export interface SignOutParams<R extends boolean = true> {
/** @docs https://next-auth.js.org/getting-started/client#specifying-a-callbackurl-1 */
callbackUrl?: string
/** @docs https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1 */
redirect?: R
}
/**
* Signs the user out, by removing the session cookie.
* Automatically adds the CSRF token to the request.
*
* [Documentation](https://next-auth.js.org/getting-started/client#signout)
*/
export function signOut<R extends boolean = true>(
params?: SignOutParams<R>
): Promise<R extends true ? undefined : SignOutResponse>
/**
* @docs https://next-auth.js.org/getting-started/client#signout
* Alias for `signOut`
*/
export const signout: typeof signOut
/************************
* SessionProvider types
***********************/
/** @docs: https://next-auth.js.org/getting-started/client#options */
export interface SessionProviderOptions {
baseUrl?: string
basePath?: string
clientMaxAge?: number
keepAlive?: number
}
/**
* Provider to wrap the app in to make session data available globally.
* Can also be used to throttle the number of requests to the endpoint
* `/api/auth/session`.
*
* [Documentation](https://next-auth.js.org/getting-started/client#provider)
*/
export type SessionProvider = React.FC<{
children: React.ReactNode
session?: Session
options?: SessionProviderOptions
}>
/**
* Provider to wrap the app in to make session data available globally.
* Can also be used to throttle the number of requests to the endpoint
* `/api/auth/session`.
*
* [Documentation](https://next-auth.js.org/getting-started/client#provider)
*/
export const Provider: SessionProvider
/** @docs: https://next-auth.js.org/getting-started/client#options */
export function setOptions(options: SessionProviderOptions): void
/**
* Alias for `setOptions`
* @docs: https://next-auth.js.org/getting-started/client#options
*/
export const options: typeof setOptions

413
types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,413 @@
// Minimum TypeScript Version: 3.5
/// <reference types="node" />
import { ConnectionOptions } from "typeorm"
import { Adapter } from "./adapters"
import { JWTOptions, JWT } from "./jwt"
import { AppProviders } from "./providers"
import {
Awaitable,
NextApiRequest,
NextApiResponse,
NextApiHandler,
} from "./internals/utils"
/**
* Configure your NextAuth instance
*
* [Documentation](https://next-auth.js.org/configuration/options#options)
*/
export interface NextAuthOptions {
/**
* An array of authentication providers for signing in
* (e.g. Google, Facebook, Twitter, GitHub, Email, etc) in any order.
* This can be one of the built-in providers or an object with a custom provider.
* * **Default value**: `[]`
* * **Required**: *Yes*
*
* [Documentation](https://next-auth.js.org/configuration/options#providers) | [Providers documentation](https://next-auth.js.org/configuration/providers)
*/
providers: AppProviders
/**
* A database connection string or configuration object.
* * **Default value**: `null`
* * **Required**: *No (unless using email provider)*
*
* [Documentation](https://next-auth.js.org/configuration/options#database) | [Databases](https://next-auth.js.org/configuration/databases)
*/
database?: string | Record<string, any> | ConnectionOptions
/**
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
* If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
* 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.
* * **Default value**: `string` (SHA hash of the "options" object)
* * **Required**: No - **but strongly recommended**!
*
* [Documentation](https://next-auth.js.org/configuration/options#secret)
*/
secret?: string
/**
* Configure your session like if you want to use JWT or a database,
* how long until an idle session expires, or to throttle write operations in case you are using a database.
* * **Default value**: See the documentation page
* * **Required**: No
*
* [Documentation](https://next-auth.js.org/configuration/options#session)
*/
session?: SessionOptions
/**
* JSON Web Tokens can be used for session tokens if enabled with the `session: { jwt: true }` option.
* JSON Web Tokens are enabled by default if you have not specified a database.
* By default JSON Web Tokens are signed (JWS) but not encrypted (JWE),
* as JWT encryption adds additional overhead and comes with some caveats.
* You can enable encryption by setting `encryption: true`.
* * **Default value**: See the documentation page
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#jwt)
*/
jwt?: JWTOptions
/**
* Specify URLs to be used if you want to create custom sign in, sign out and error pages.
* Pages specified will override the corresponding built-in page.
* * **Default value**: `{}`
* * **Required**: *No*
* @example
*
* ```js
* pages: {
* signIn: '/auth/signin',
* signOut: '/auth/signout',
* error: '/auth/error',
* verifyRequest: '/auth/verify-request',
* newUser: null
* }
* ```
*
* [Documentation](https://next-auth.js.org/configuration/options#pages) | [Pages documentation](https://next-auth.js.org/configuration/pages)
*/
pages?: PagesOptions
/**
* Callbacks are asynchronous functions you can use to control what happens when an action is performed.
* Callbacks are *extremely powerful*, especially in scenarios involving JSON Web Tokens
* as they **allow you to implement access controls without a database** and to **integrate with external databases or APIs**.
* * **Default value**: See the Callbacks documentation
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#callbacks) | [Callbacks documentation](https://next-auth.js.org/configuration/callbacks)
*/
callbacks?: CallbacksOptions
/**
* Events are asynchronous functions that do not return a response, they are useful for audit logging.
* You can specify a handler for any of these events below - e.g. for debugging or to create an audit log.
* The content of the message object varies depending on the flow
* (e.g. OAuth or Email authentication flow, JWT or database sessions, etc),
* but typically contains a user object and/or contents of the JSON Web Token
* and other information relevant to the event.
* * **Default value**: `{}`
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#events) | [Events documentation](https://next-auth.js.org/configuration/events)
*/
events?: EventsOptions
/**
* By default NextAuth.js uses a database adapter that uses TypeORM and supports MySQL, MariaDB, Postgres and MongoDB and SQLite databases.
* An alternative adapter that uses Prisma, which currently supports MySQL, MariaDB and Postgres, is also included.
* You can use the adapter option to use the Prisma adapter - or pass in your own adapter
* if you want to use a database that is not supported by one of the built-in adapters.
* * **Default value**: TypeORM adapter
* * **Required**: *No*
*
* - ⚠ If the `adapter` option is specified it overrides the `database` option, only specify one or the other.
* - ⚠ Adapters are being migrated to their own home in a Community maintained repository.
*
* [Documentation](https://next-auth.js.org/configuration/options#adapter) |
* [Default adapter](https://next-auth.js.org/schemas/adapters#typeorm-adapter) |
* [Community adapters](https://github.com/nextauthjs/adapters)
*/
adapter?: Adapter
/**
* Set debug to true to enable debug messages for authentication and database operations.
* * **Default value**: `false`
* * **Required**: *No*
*
* - ⚠ If you added a custom `logger`, this setting is ignored.
*
* [Documentation](https://next-auth.js.org/configuration/options#debug) | [Logger documentation](https://next-auth.js.org/configuration/options#logger)
*/
debug?: boolean
/**
* Override any of the logger levels (`undefined` levels will use the built-in logger),
* and intercept logs in NextAuth. You can use this option to send NextAuth logs to a third-party logging service.
* * **Default value**: `console`
* * **Required**: *No*
*
* @example
*
* ```js
* // /pages/api/auth/[...nextauth].js
* import log from "logging-service"
* export default NextAuth({
* logger: {
* error(code, ...message) {
* log.error(code, message)
* },
* warn(code, ...message) {
* log.warn(code, message)
* },
* debug(code, ...message) {
* log.debug(code, message)
* }
* }
* })
* ```
*
* - ⚠ When set, the `debug` option is ignored
*
* [Documentation](https://next-auth.js.org/configuration/options#logger) |
* [Debug documentation](https://next-auth.js.org/configuration/options#debug)
*/
logger?: LoggerInstance
/**
* Changes the theme of pages.
* Set to `"light"` if you want to force pages to always be light.
* Set to `"dark"` if you want to force pages to always be dark.
* Set to `"auto"`, (or leave this option out)if you want the pages to follow the preferred system theme.
* * **Default value**: `"auto"`
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#theme) | [Pages documentation]("https://next-auth.js.org/configuration/pages")
*/
theme?: "auto" | "dark" | "light"
/**
* When set to `true` then all cookies set by NextAuth.js will only be accessible from HTTPS URLs.
* This option defaults to `false` on URLs that start with `http://` (e.g. http://localhost:3000) for developer convenience.
* You can manually set this option to `false` to disable this security feature and allow cookies
* to be accessible from non-secured URLs (this is not recommended).
* * **Default value**: `true` for HTTPS and `false` for HTTP sites
* * **Required**: No
*
* [Documentation](https://next-auth.js.org/configuration/options#usesecurecookies)
*
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
* but **may have complex implications** or side effects.
* You should **try to avoid using advanced options** unless you are very comfortable using them.
*/
useSecureCookies?: boolean
/**
* You can override the default cookie names and options for any of the cookies used by NextAuth.js.
* You can specify one or more cookies with custom properties,
* but if you specify custom options for a cookie you must provide all the options for that cookie.
* If you use this feature, you will likely want to create conditional behavior
* to support setting different cookies policies in development and production builds,
* as you will be opting out of the built-in dynamic policy.
* * **Default value**: `{}`
* * **Required**: No
*
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
* but **may have complex implications** or side effects.
* You should **try to avoid using advanced options** unless you are very comfortable using them.
*
* [Documentation](https://next-auth.js.org/configuration/options#cookies) | [Usage example](https://next-auth.js.org/configuration/options#example)
*/
cookies?: CookiesOptions
}
/**
* Override any of the methods, and the rest will use the default logger.
*
* [Documentation](https://next-auth.js.org/configuration/options#logger)
*/
export interface LoggerInstance {
warn(code: string, ...message: unknown[]): void
error(code: string, ...message: unknown[]): void
debug(code: string, ...message: unknown[]): void
}
/**
* Different tokens returned by OAuth Providers.
* Some of them are available with different casing,
* but they refer to the same value.
*/
export interface TokenSet {
accessToken: string
idToken?: string
refreshToken?: string
access_token: string
expires_in?: number | null
refresh_token?: string
id_token?: string
}
/**
* Usually contains information about the provider being used
* and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
*/
export interface Account extends TokenSet, Record<string, unknown> {
id: string
provider: string
type: string
}
/** The OAuth profile returned from your provider */
export interface Profile extends Record<string, unknown> {
sub?: string
name?: string
email?: string
image?: string
}
/** [Documentation](https://next-auth.js.org/configuration/callbacks) */
export interface CallbacksOptions<
P extends Record<string, unknown> = Profile,
A extends Record<string, unknown> = Account
> {
/**
* Use this callback to control if a user is allowed to sign in.
* Returning true will continue the sign-in flow.
* Throwing an error or returning a string will stop the flow, and redirect the user.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#sign-in-callback)
*/
signIn?(user: User, account: A, profile: P): Awaitable<string | boolean>
/**
* This callback is called anytime the user is redirected to a callback URL (e.g. on signin or signout).
* By default only URLs on the same URL as the site are allowed,
* you can use this callback to customise that behaviour.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#redirect-callback)
*/
redirect?(url: string, baseUrl: string): Awaitable<string>
/**
* This callback is called whenever a session is checked.
* (Eg.: invoking the `/api/session` endpoint, using `useSession` or `getSession`)
*
* - ⚠ By default, only a subset of the token is returned for increased security.
* If you want to make something available you added to the token through the `jwt` callback,
* you have to explicitely forward it here to make it available to the client.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#session-callback) |
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
*
*/
session?(session: Session, userOrToken: JWT | User): Awaitable<Session>
/**
* This callback is called whenever a JSON Web Token is created (i.e. at sign in)
* or updated (i.e whenever a session is accessed in the client).
* Its content is forwarded to the `session` callback,
* where you can control what should be returned to the client.
* Anything else will be kept from your front-end.
*
* - ⚠ By default the JWT is signed, but not encrypted.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
*/
jwt?(
token: JWT,
user?: User,
account?: A,
profile?: P,
isNewUser?: boolean
): Awaitable<JWT>
}
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
export interface CookieOption {
name: string
options: {
httpOnly: boolean
sameSite: true | "strict" | "lax" | "none"
path?: string
secure: boolean
maxAge?: number
domain?: string
}
}
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
export interface CookiesOptions {
sessionToken?: CookieOption
callbackUrl?: CookieOption
csrfToken?: CookieOption
pkceCodeVerifier?: CookieOption
}
/** [Documentation](https://next-auth.js.org/configuration/events) */
export type EventType =
| "signIn"
| "signOut"
| "createUser"
| "updateUser"
| "linkAccount"
| "session"
| "error"
/** [Documentation](https://next-auth.js.org/configuration/events) */
export type EventCallback = (message: any) => Promise<void>
/** [Documentation](https://next-auth.js.org/configuration/events) */
export type EventsOptions = Partial<Record<EventType, EventCallback>>
/** [Documentation](https://next-auth.js.org/configuration/pages) */
export interface PagesOptions {
signIn?: string
signOut?: string
/** Error code passed in query string as ?error= */
error?: string
verifyRequest?: string
/** If set, new users will be directed here on first sign in */
newUser?: string
}
/**
* Returned by `useSession`, `getSession`, returned by the `session` callback
* and also the shape received as a prop on the `Provider` React Context
*
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
* [`Provider`](https://next-auth.js.org/getting-started/client#provider) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback)
*/
export interface Session extends Record<string, unknown> {
user?: User
accessToken?: string
expires: string
}
/** [Documentation](https://next-auth.js.org/configuration/options#session) */
export interface SessionOptions {
jwt?: boolean
maxAge?: number
updateAge?: number
}
/**
* The shape of the returned object in the OAuth providers' `profile` callback,
* available in the `jwt` and `session` callbacks,
* or the second parameter of the `session` callback, when using a database.
*
* [`signIn` callback](https://next-auth.js.org/configuration/callbacks#sign-in-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`profile` OAuth provider callback](https://next-auth.js.org/configuration/providers#using-a-custom-provider)
*/
export interface User {
name?: string | null
email?: string | null
image?: string | null
}
declare function NextAuth(
req: NextApiRequest,
res: NextApiResponse,
options: NextAuthOptions
): ReturnType<NextApiHandler>
declare function NextAuth(options: NextAuthOptions): ReturnType<NextApiHandler>
export default NextAuth

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

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

50
types/internals/index.d.ts vendored Normal file
View File

@@ -0,0 +1,50 @@
import { NextApiRequest, NextApiResponse } from "./utils"
import { NextAuthOptions } from ".."
import { AppProvider } from "./providers"
/** Options that are the same both in internal and user provided options. */
export type NextAuthSharedOptions =
| "pages"
| "jwt"
| "events"
| "callbacks"
| "cookies"
| "secret"
| "adapter"
| "theme"
| "debug"
| "logger"
export interface AppOptions
extends Pick<NextAuthOptions, NextAuthSharedOptions> {
pkce?: {
code_verifier?: string
/**
* Could be `"plain"`, but not recommended.
* We ignore it for now.
* @spec https://tools.ietf.org/html/rfc7636#section-4.2.
*/
code_challenge_method?: "S256"
}
provider?: AppProvider
providers: AppProvider[]
baseUrl?: string
basePath?: string
action?:
| "providers"
| "session"
| "csrf"
| "signin"
| "signout"
| "callback"
| "verify-request"
| "error"
csrfToken?: string
csrfTokenVerified?: boolean
}
export interface NextAuthRequest extends NextApiRequest {
options: AppOptions
}
export type NextAuthResponse = NextApiResponse

40
types/internals/next.d.ts vendored Normal file
View File

@@ -0,0 +1,40 @@
import { IncomingMessage, ServerResponse } from "http"
// ------------------------------------------------------
// Types from next@10,
// see: https://github.com/microsoft/dtslint/issues/297
// ------------------------------------------------------
export interface NextApiRequest extends IncomingMessage {
query: {
[key: string]: string | string[]
}
cookies: {
[key: string]: string
}
body: any
env: any
preview?: boolean
previewData?: any
}
export type Send<T> = (body: T) => void
export type NextApiResponse<T = any> = ServerResponse & {
send: Send<T>
json: Send<T>
status: (statusCode: number) => NextApiResponse<T>
redirect: ((url: string) => NextApiResponse<T>) &
((status: number, url: string) => NextApiResponse<T>)
setPreviewData: (
data: object | string,
options?: {
maxAge?: number
}
) => NextApiResponse<T>
clearPreviewData: () => NextApiResponse<T>
}
export type NextApiHandler<T = any> = (
req: NextApiRequest,
res: NextApiResponse<T>
) => void | Promise<void>

6
types/internals/providers.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
import { CommonProviderOptions } from "../providers"
export interface AppProvider extends CommonProviderOptions {
signinUrl: string
callbackUrl: string
}

42
types/internals/utils.d.ts vendored Normal file
View File

@@ -0,0 +1,42 @@
import { IncomingMessage, ServerResponse } from "http"
export type Awaitable<T> = T | PromiseLike<T>
// ------------------------------------------------------
// Types from next@10,
// see: https://github.com/microsoft/dtslint/issues/297
// ------------------------------------------------------
export interface NextApiRequest extends IncomingMessage {
query: {
[key: string]: string | string[]
}
cookies: {
[key: string]: string
}
body: any
env: any
preview?: boolean
previewData?: any
}
export type Send<T> = (body: T) => void
export type NextApiResponse<T = any> = ServerResponse & {
send: Send<T>
json: Send<T>
status: (statusCode: number) => NextApiResponse<T>
redirect: ((url: string) => NextApiResponse<T>) &
((status: number, url: string) => NextApiResponse<T>)
setPreviewData: (
data: object | string,
options?: {
maxAge?: number
}
) => NextApiResponse<T>
clearPreviewData: () => NextApiResponse<T>
}
export type NextApiHandler<T = any> = (
req: NextApiRequest,
res: NextApiResponse<T>
) => void | Promise<void>

66
types/jwt.d.ts vendored Normal file
View File

@@ -0,0 +1,66 @@
import { JWT as JoseJWT, JWE } from "jose"
import { NextApiRequest } from "./internals/utils"
/**
* Returned by the `jwt` callback and `getToken`, when using JWT sessions
*
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) | [`getToken`](https://next-auth.js.org/tutorials/securing-pages-and-api-routes#using-gettoken)
*/
export interface JWT extends Record<string, unknown> {
name?: string | null
email?: string | null
picture?: string | null
}
export interface JWTEncodeParams {
token?: JWT
maxAge?: number
secret: string | Buffer
signingKey?: string
signingOptions?: JoseJWT.SignOptions
encryptionKey?: string
encryptionOptions?: object
encryption?: boolean
}
export function encode(params?: JWTEncodeParams): Promise<string>
export interface JWTDecodeParams {
token?: string
maxAge?: number
secret: string | Buffer
signingKey?: string
verificationKey?: string
verificationOptions?: JoseJWT.VerifyOptions<false>
encryptionKey?: string
decryptionKey?: string
decryptionOptions?: JWE.DecryptOptions<false>
encryption?: boolean
}
export function decode(params?: JWTDecodeParams): Promise<JWT>
export type GetTokenParams<R extends boolean = false> = {
req: NextApiRequest
secureCookie?: boolean
cookieName?: string
raw?: R
decode?: typeof decode
secret?: string
} & Omit<JWTDecodeParams, "secret">
/** [Documentation](https://next-auth.js.org/tutorials/securing-pages-and-api-routes#using-gettoken) */
export function getToken<R extends boolean = false>(
params?: GetTokenParams<R>
): Promise<R extends true ? string : JWT | null>
export interface JWTOptions {
secret?: string
maxAge?: number
encryption?: boolean
signingKey?: string
encryptionKey?: string
encode?: typeof encode
decode?: typeof decode
verificationOptions?: JoseJWT.VerifyOptions<false>
}

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