Compare commits

...

23 Commits

Author SHA1 Message Date
Balázs Orbán
eed0001524 fix(ts): adjust properties on default interfaces (#1794)
* fix(ts): adjust properties on default interfaces

* fix(ts): make expires also optional

* fix(ts): don't require default session/jwt fields

* fix(ts): make all default fields optional
2021-04-21 17:17:38 +02:00
Gabrijel Gavranović
a2705fb5b9 fix(client): export getCsrfToken directly to support Webpack 5
Fixes `Attempted import error: 'getCsrfToken' is not exported from 'next-auth/client' (imported as 'getCsrfToken’).`-error.
2021-04-21 17:14:12 +02:00
Balázs Orbán
cb1e5a7174 docs(dev): add readme to dev app 2021-04-21 00:00:57 +02:00
Balázs Orbán
8cba5d06b5 build(provider): filter index.js to be more forgiving 2021-04-20 23:17:18 +02:00
Balázs Orbán
c52ce57296 fix: add skypack recommended fields (#1791) 2021-04-20 22:40:12 +02:00
Balázs Orbán
4dae822806 chore: move dev app into its own folder (#1753)
* chore: move dev app to its own folder

* docs: update CONTRIBUTING.md

* docs: fix typos in CONTRIBUTING

* chore: gitignore dev app lock files

* chore: move release config into package.json
2021-04-20 22:25:51 +02:00
Lluis Agusti
901f6fb189 docs: mention TS example repo on the website (#1786)
* docs(www): mention TS example repo

* Update www/docs/getting-started/typescript.md

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-04-20 21:40:06 +02:00
Balázs Orbán
bb2237d0f9 fix(build): remove unnecessary build before release 2021-04-20 21:35:10 +02:00
Balázs Orbán
fab7ce8f94 fix(build): trigger re-release 2021-04-20 21:33:01 +02:00
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
133 changed files with 3949 additions and 2740 deletions

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

@@ -2,13 +2,14 @@ name: Release
on:
push:
branches:
- 'main'
- 'next'
- '3.x'
- "main"
- "beta"
- "next"
- "3.x"
pull_request:
jobs:
release:
name: 'Release'
name: "Release"
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -19,7 +20,6 @@ jobs:
node-version: 14
- name: Install dependencies
uses: bahmutov/npm-install@v1
- run: npm run build
- run: npx semantic-release@17
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

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

20
.gitignore vendored
View File

@@ -27,6 +27,24 @@ node_modules
.cache-loader
.next
www/providers.json
src/providers/index.js
internals
adapters.d.ts
adapters.js
client.d.ts
client.js
index.d.ts
index.js
jwt.d.ts
jwt.js
providers.d.ts
providers.js
# Development app
app/next-auth
app/dist/css
app/package-lock.json
app/yarn.lock
# VS
/.vs/slnx.sqlite-journal
@@ -39,4 +57,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
@@ -32,17 +32,17 @@ cd next-auth
2. Install packages:
```sh
npm i
npm i && npm dev:setup
```
3. Populate `.env.local`:
Copy `.env.local.example` to `.env.local`, and add your env variables for each provider you want to test.
Copy `app/.env.local.example` to `app/.env.local`, and add your env variables for each provider you want to test.
> 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`.
> You can find the next-auth config under`app/pages/api/auth/[...nextauth].js`.
1. Start the dev application/server and CSS watching:
1. Start the dev application/server:
```sh
npm run dev
```
@@ -57,11 +57,23 @@ If you need an example project to link to, you can use [next-auth-example](https
When running `npm run dev`, you start a Next.js dev server on `http://localhost:3000`, which includes hot reloading out of the box. Make changes on any of the files in `src` and see the changes immediately.
>NOTE: When working on CSS, you will need to manually refresh the page after changes. (Improving this through a PR is very welcome!)
> NOTE: When working on CSS, you will have to manually refresh the page after changes. The reason for this is our pages using CSS are server-side rendered. (Improving this through a PR is very welcome!)
> NOTE: The setup is as follows: The development application lives inside the `app` folder, and whenever you make a change to the `src` folder in the root (where next-auth is), it gets copied into `app` every time (gitignored), so Next.js can pick them up and apply hot reloading. This is to avoid some annoying issues with how symlinks are working with different React builds, and also to provide a super-fast feedback loop while developing core features.
#### 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.
Included is a Docker Compose file that starts up MySQL, PostgreSQL, and MongoDB databases on localhost.
It will use port `3306`, `5432`, and `27017` on localhost respectively; please make sure those ports are not used by other services on localhost.

View File

@@ -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

6
app/README.md Normal file
View File

@@ -0,0 +1,6 @@
# NextAuth.js Development App
This folder contains a Next.js app using NextAuth.js for local development. See the following section on how to start:
[Setting up local environment
](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md#setting-up-local-environment)

5
app/jsconfig.json Normal file
View File

@@ -0,0 +1,5 @@
{
"compilerOptions": {
"baseUrl": "."
}
}

View File

19
app/next.config.js Normal file
View File

@@ -0,0 +1,19 @@
const path = require("path")
module.exports = {
webpack(config) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
"next-auth$": path.join(process.cwd(), "next-auth/server"),
"next-auth/client$": path.join(process.cwd(), "next-auth/client"),
"next-auth/jwt$": path.join(process.cwd(), "next-auth/lib/jwt"),
"next-auth/adapters": path.join(process.cwd(), "next-auth/adapters"),
"next-auth/providers": path.join(process.cwd(), "next-auth/providers"),
},
}
return config
},
}

25
app/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "next-auth-app",
"version": "1.0.0",
"description": "NextAuth.js Developer app",
"private": true,
"scripts": {
"dev": "npm-run-all --parallel copy:app dev:css dev:next",
"dev:next": "next dev",
"copy:app": "cpx \"../src/**/*\" next-auth --watch",
"copy:css": "cpx \"../dist/css/**/*\" dist/css --watch",
"watch:css": "cd .. && npm run watch:css",
"dev:css": "npm-run-all --parallel watch:css copy:css",
"start": "next start"
},
"license": "ISC",
"dependencies": {
"next": "^10.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"cpx": "^1.5.0",
"npm-run-all": "^4.1.5"
}
}

View File

@@ -1,9 +1,9 @@
import { Provider } from 'next-auth/client'
import './styles.css'
import { Provider } from "next-auth/client"
import "./styles.css"
// Use the <Provider> to improve performance and allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App ({ Component, pageProps }) {
export default function App({ Component, pageProps }) {
return (
<Provider
// Provider options are not required but can be useful in situations where
@@ -21,7 +21,7 @@ export default function App ({ Component, pageProps }) {
//
// Note: If a session has expired when keep alive is triggered, all open
// windows / tabs will be updated to reflect the user is signed out.
keepAlive: 0
keepAlive: 0,
}}
session={pageProps.session}
>

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'

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'

View File

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

88
config/build.js Normal file
View File

@@ -0,0 +1,88 @@
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")
.filter((file) => file !== "index.js")
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

2253
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,39 +6,57 @@
"repository": "https://github.com/nextauthjs/next-auth.git",
"author": "Iain Collins <me@iaincollins.com>",
"main": "index.js",
"types": "./index.d.ts",
"keywords": ["react", "nodejs", "oauth", "jwt", "oauth2", "authentication", "nextjs", "csrf", "oidc", "nextauth"],
"exports": {
".": "./dist/server/index.js",
"./jwt": "./dist/lib/jwt.js",
"./adapters": "./dist/adapters/index.js",
"./client": "./dist/client/index.js",
"./providers": "./dist/providers/index.js",
"./providers/*": "./dist/providers/*.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:setup": "npm run build:css && cd app && npm i",
"dev": "cd app && npm run dev",
"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": {
@@ -57,7 +75,7 @@
},
"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 +94,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,25 +117,48 @@
"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": [
"localStorage",
"location",
"fetch"
"ignorePatterns": [
"node_modules",
"test",
"next-env.d.ts",
"types",
"www",
".next",
"dist"
],
"globals": {
"localStorage": "readonly",
"location": "readonly",
"fetch": "readonly"
}
},
"release": {
"branches": [
"+([0-9])?(.{+([0-9]),x}).x",
"main",
{ "name": "beta", "prerelease": true },
{ "name": "next", "prerelease": true }
]
},
"funding": [

View File

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

View File

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

103
src/client/index.d.ts vendored
View File

@@ -1,103 +0,0 @@
import * as React from 'react'
import { GetServerSidePropsContext } from 'next'
interface DefaultSession {
user: {
name: string | null
email: string | null
image: string | null
}
expires: Date | string
}
interface BroadcastMessage {
event?: 'session'
data?: {
trigger?: 'signout' | 'getSession'
}
clientId: string
timestamp: number
}
type GetSession<S extends Record<string, unknown> = DefaultSession> = (options: {
ctx?: GetServerSidePropsContext
req?: GetServerSidePropsContext['req']
event?: 'storage' | 'timer' | 'hidden' | string
triggerEvent?: boolean
}) => Promise<S>
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: DefaultSession | null | undefined
/** Used to store to function export by getSession() hook */
_getSession: any
}
export type GetCsrfToken = (
ctxOrReq: GetServerSidePropsContext & GetServerSidePropsContext['req']
) => Promise<string | null>
export interface SessionOptions {
baseUrl?: string
basePath?: string
clientMaxAge?: number
keepAlive?: number
}
export type Provider<S extends Record<string, unknown> = DefaultSession > = (options: {
children: React.ReactNode
session: S
options: SessionOptions
}) => React.ReactNode
export type SetOptions = (options: SessionOptions) => void
export type SessionContext = React.createContext<[DefaultSession | null, boolean]>
export type UseSession = () => [any, boolean]
export type GetProviders = () => Promise<any[]>
// Sign in types
export interface SignInOptions {
/** Defaults to the current URL. */
callbackUrl?: string
redirect?: boolean
}
export interface SignInResponse {
error: string | null
status: number
ok: boolean
url: string | null
}
export type SignIn<AuthorizationParams = Record<string, string>> = (
provider?: string,
options?: SignInOptions,
authorizationParams?: AuthorizationParams
) => SignInResponse
// Sign out types
interface SignOutResponse<RedirectType extends boolean=true> {
/** Defaults to the current URL. */
callbackUrl?: string
redirect?: RedirectType
}
export type SignOut<RedirectType extends boolean = true> = (params: SignOutResponse<RedirectType>) => RedirectType extends true ? Promise<{url?: string} | undefined> : undefined

View File

@@ -18,7 +18,7 @@ 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(".").NextAuthConfig} */
/** @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,
@@ -60,15 +60,9 @@ if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
}
// Context to store session data globally
/** @type {import("types/internals/client").SessionContext} */
const SessionContext = createContext()
/**
* React Hook that gives you access
* to the logged in user's session data.
*
* [Documentation](https://next-auth.js.org/getting-started/client#usesession)
* @type {import(".").UseSession}
*/
export function useSession (session) {
const context = useContext(SessionContext)
if (context) return context
@@ -143,14 +137,6 @@ function _useSessionHook (session) {
return [data, loading]
}
/**
* 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)
* @type {import(".").GetSession}
*/
export async function getSession (ctx) {
const session = await _fetchData('session', ctx)
if (ctx?.triggerEvent ?? true) {
@@ -159,39 +145,14 @@ export async function getSession (ctx) {
return session
}
/**
* 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)
* @type {import(".").GetCsrfToken}
*/
async function getCsrfToken (ctx) {
export async function getCsrfToken (ctx) {
return (await _fetchData('csrf', ctx))?.csrfToken
}
/**
* 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)
* @type {import(".").GetProviders}
*/
export async function getProviders () {
return _fetchData('providers')
}
/**
* 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)
* @type {import(".").SignIn}
*/
export async function signIn (provider, options = {}, authorizationParams = {}) {
const {
callbackUrl = window.location,
@@ -255,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.
*
* [Documentation](https://next-auth.js.org/getting-started/client#signout)
* @type {import(".").SignOut}
*/
export async function signOut (options = {}) {
const {
callbackUrl = window.location,
@@ -298,7 +252,6 @@ export async function signOut (options = {}) {
// 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.
/** @type {import(".").SetOptions} */
export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {}) {
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
if (basePath) __NEXTAUTH.basePath = basePath
@@ -321,14 +274,6 @@ export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {})
}
}
/**
* 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)
* @type {import(".").Provider}
*/
export function Provider ({ children, session, options }) {
setOptions(options)
return createElement(
@@ -387,13 +332,13 @@ function BroadcastChannel (name = 'nextauth.message') {
return {
/**
* Get notified by other tabs/windows.
* @param {(message: import(".").BroadcastMessage) => void} onReceive
* @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(".").BroadcastMessage} */
/** @type {import("types/internals/client").BroadcastMessage} */
const message = JSON.parse(event.newValue)
if (message?.event !== 'session' || !message?.data) return

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
}

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

@@ -1,10 +0,0 @@
export interface LoggerInstance {
warn: (code?: string, ...message: unknown[]) => void
error: (code?: string, ...message: unknown[]) => void
debug: (code?: string, ...message: unknown[]) => void
}
export declare function proxyLogger (logger: LoggerInstance, basePath: string): LoggerInstance
const _logger: LoggerInstance
export default _logger

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.avatar.startsWith('a_') ? '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,
}
}

View File

@@ -1,25 +1,28 @@
export default (options) => {
export default function FACEIT(options) {
return {
id: 'faceit',
name: 'FACEIT',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
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')}`
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) {
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
image,
}
},
...options
...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,83 +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 FACEIT from './faceit'
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 Instagram from './instagram'
import Kakao from './kakao'
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 Osso from './osso'
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'
import Zoho from './zoho'
export default {
Apple,
Atlassian,
Auth0,
AzureADB2C,
Basecamp,
BattleNet,
Box,
Bungie,
Cognito,
Credentials,
Discord,
Email,
EVEOnline,
Facebook,
FACEIT,
Foursquare,
FusionAuth,
GitHub,
GitLab,
Google,
IdentityServer4,
Instagram,
Kakao,
LINE,
LinkedIn,
MailRu,
Medium,
Netlify,
Okta,
Osso,
Reddit,
Salesforce,
Slack,
Spotify,
Strava,
Twitch,
Twitter,
VK,
Yandex,
Zoho
}

View File

@@ -1,5 +1,5 @@
/**
* @param {import("../server").Provider} options
* @type {import("types/providers").OAuthProvider} options
* @example
*
* ```js
@@ -22,30 +22,29 @@
* </button>
* ...
* ```
* *Resources:*
* - [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)
* [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) {
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) {
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
image: null,
}
},
...options
...options,
}
}

View File

@@ -1,21 +1,22 @@
export default (options) => {
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) => {
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
image: profile.kakao_account?.profile.profile_image_url,
}
},
...options
...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,
}
}

View File

@@ -1,20 +1,20 @@
export default (options) => {
export default function Osso(options) {
return {
id: 'osso',
name: 'SAML SSO',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
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) => {
profile(profile) {
return {
id: profile.id,
name: profile.name || profile.email,
email: profile.email
email: profile.email,
}
},
...options
...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,
}
}

View File

@@ -1,22 +1,23 @@
export default (options) => {
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) => {
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
image: null,
}
},
...options
...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-res'
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,20 +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?
}
if (typeof provider.protection === 'string') {
provider.protection = [provider.protection]
// 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
@@ -107,7 +105,6 @@ async function NextAuthHandler (req, res, userOptions) {
provider,
cookies,
secret,
csrfToken,
providers,
// Session options
session: {
@@ -138,6 +135,7 @@ async function NextAuthHandler (req, res, userOptions) {
logger
}
csrfTokenHandler(req, res)
await callbackUrlHandler(req, res)
const render = renderPage(req, res)
@@ -150,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}`
@@ -203,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)
@@ -212,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`)
}

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, 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) {
@@ -196,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
*/

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