Compare commits

...

51 Commits

Author SHA1 Message Date
Balázs Orbán
7c65bda6f1 feat: improve OAuth provider configuration (#2411)
> This touches on all OAuth providers, so there is a big potential for breaking by default. We have let new providers be added for contributors' specific needs, but from now on, we will require a more strict default on all new providers, so the basic behavior is predictable for everyone.
⚠ Unfortunately, we will not have the capacity to test each and every provider that has been added to the default providers, but we will do our best to test the most popular ones. (@ndom91 has worked on setting up the infrastructure for this). If you wish to make sure that the provider you are using will stay working, please reach out with your concerns and tell us how can you help us test that particular provider in the future. 🙏

That said, I will try my best to not break ANY of the currently built-in providers, or at least make the migration super easy. So hopefully, you won't have to change anything. It will most probably affect you if you defined a custom provider though.

We will monitor the default configuration much more closely, so the behavior will be more consistent across providers by default.

Closes #1846, Closes #1605, Closes #1607

BREAKING CHANGES:

Basecamp provider is removed. See the explanation [here](https://github.com/basecamp/api/blob/master/sections/authentication.md#on-authenticating-users-via-oauth)

**ALL** OAuth providers' `profile` callback is expected to only return these fields by default from now on: `id`, `name`, `email`, and `image` at most. Any of these missing values should be set to `null`.

The following new options are available:
1. `authorization` (replaces `authorizationUrl`, `authorizationParams`, `scope`)
2. `token` replaces (`accessTokenUrl`, `headers`, `params`)
3. `userinfo` (replaces `profileUrl`)

These three options map nicely to the OAuth spec's three endpoints for
1. initiating the login flow
2. retrieve OAuth tokens
3. retrieve user information

They all take the form of `EndpointHandler`:
```ts
type EndpointRequest<C, R> = (
  context: C & {
    /** `openid-client` Client */
    client: Client
    /** Provider is passed for convenience, ans also contains the `callbackUrl`. */
    provider: OAuthConfig & {
      signinUrl: string
      callbackUrl: string
    }
  }
) => Awaitable<R>

/** Gives granular control of the request to the given endpoint */
type AdvancedEndpointHandler<P extends UrlParams, C, R> = {
  /** Endpoint URL. Can contain parameters. Optionally, you can use `params`*/
  url?: string
  /** These will be prepended to the `url` */
  params?: P
  /**
   * Control the corresponding OAuth endpoint request completely.
   * Useful if your provider relies on some custom behavior
   * or it diverges from the OAuth spec.
   *
   * - ⚠ **This is an advanced option.**
   * You should **try to avoid using advanced options** unless you are very comfortable using them.
   */
  request?: EndpointRequest<C, R>
}

/** Either an URL (containing all the parameters) or an object with more granular control. */
type EndpointHandler<P extends UrlParams, C = any, R = any> =
  | string
  | AdvancedEndpointHandler<P, C, R>
```

In case of `authorization`, the `EndpointHandler` can define the `params` as [`AuthorizationParameters`](51dc47d9ac/types/index.d.ts (L108-L143))

> Note: `authorization` does not implement `request` yet. We will have to see if there is demand for it.

From now on, instead of using the `...` spread operator when adding a new built-in provider, the user is expected to add `options` as a property at the end of the default config. This way, we can deep merge the user config with the default one. This is needed  to let the user do something like this:

```js
MyProvider({
  clientId: "",
  clientSecret: "",
  authorization: { params: {scope: ""} }
})
```
So even if the default config defines anything in `authorization`, only the user-defined parts will be overridden.
2021-08-05 00:42:47 +02:00
Balázs Orbán
f06e4d286b refactor: replace node-oauth with openid-client (#1698)
* chore(deps): add openid-client

* chore: merge in next

* refactor(provider): remove redundant requestUrl param

* feat(provider): make profile callback optional

* refactor: use openid-client for OAuth2/OIDC

* refactor: use openidClient in oauth signin handler

* refactor: use openidClient in oauth callback handler

* docs(warn): add async issuer/old config warnings

* chore(deps): remove jsonwebtoken

* chore: add issuer property for testing locally

* chore(dev): import providers one-by-one

* fix(oauth): handle when no user in body/query

* chore(deps): remove pkce-challenge

* chore(dev): change Auth0 protection

* refactor(oauth): simplify pkce/state

* refactor: split OAuth1 client, reduce openid client

will improve API in another PR

* chore: change comment, dev app

* chore: mention OIDC client config discovery

* fix: add new operator when creating OIDC client

* refactor: delete req.query.nextauth after use

* docs(ts): use `TokenSet` from `openid-client`

* chore: simplify/type signin route

* refactor: rename to client-legacy to indicate intnet of maintenance

* chore(deps): try setting `oauth` as optional peer dep

* chore(deps): add `oauth` back as regular dependency

* chore(deps): add @types/oauth as dev dependency

* chore: remove params kept for backwards compatibility

* chore: don't make breaking changes in this PR

* chore(core): use correct TS declarations

* refactor: move files/add more accurate types internally

* chore: remove TODO comment

* chore: catch all errors in authorization URL generation
2021-07-20 14:52:35 +02:00
Lluis Agusti
bececbc200 Revert "refactor(providers): try Typescript"
This reverts commit 6d74da1f65.
2021-07-17 21:58:25 +02:00
Lluis Agusti
6d74da1f65 refactor(providers): try Typescript 2021-07-17 21:56:18 +02:00
Theo Browne
3312e53279 feat(events): include profile on signIn events (#2356)
* include profile on signIn events

* update docs

* Undefined profile on credentials, update docs
2021-07-13 18:12:24 +02:00
Nico Domino
ebf420c84a docs: clarify page protection (#2355) 2021-07-12 01:28:39 +02:00
Balázs Orbán
111d5fc572 feat(events): use named params for all event callbacks (#2342)
Unified API for all of our user-facing methods.

NOTE: `events.error` has been removed. This method has never been called in the core, so it did actually nothing. If you want to log errors to a third-party, check out the [`logger`](https://next-auth.js.org/configuration/options#logger) option instead.

BREAKING CHANGE:

Two event signatures changed to use named params, `signOut` and `updateUser`:
```diff
// [...nextauth].js
...
events: {
- signOut(tokenOrSession),
+ signOut({ token, session }), // token if using JWT, session if DB persisted sessions.
- updateUser(user)
+ updateUser({ user })
}
```
2021-07-12 00:30:24 +02:00
Balázs Orbán
acc9393560 feat(logger): simplify logger API (#2344)
Similar to #2342, this aims to unify the user-facing API and provide an easier way to extend in the future.

In addition, this PR also solves the problem when the `logger.error` method sometimes did not print results, because `Error` instances are not serializable and will be printed as empty objects `"{}"`.

After this PR, we make any `Error` instances serializable as described here: https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af

Closes #1602
Achieved by adding a `client: true` flag when logs are coming from the frontend.

BREAKING CHANGE:

The main change is that instead of an unknown number of parameters, the log events have at most two, where the second parameter is usually an object. In the case of the `error` event, it can also be an `Error` instance (that is serializable by `JSON.stringify`). If it is an object, an `Error` instance will be available on `metadata.error`, and `message` will default to `metadata.error.message`. This is done so that an error event always provides some kind of a stack to see where the error happened

```diff
// [...nextauth.js]
import log from "some-logger-service"
...
logger: {
- error(code, ...message) {},
+ error(code, metadata) {},
- warn(code, ...message) {},
+ warn(code) {}
- debug(code, ...message) {}
+ debug(code, metadata) {}
}
```
2021-07-12 00:17:32 +02:00
Balázs Orbán
6911dd9267 feat: rename protection to checks (#2255)
This change aligns the API with `openid-client`'s `checks` https://github.com/panva/node-openid-client/blob/main/docs/README.md#clientcallbackredirecturi-parameters-checks-extras, a library which we intend to migrate to in the future. Aligning our API early, so people get used to it.

Also, objectively the name `protection` might not have been as clear as I first thought. `checks` better describe the intention.

BREAKING CHANGE:

The `state` option on OAuth providers is now deprecated. Use `checks: ["state"]` instead.
`protections` is renamed to `checks`, here is an example:
```diff
- protection: ["pkce"]
+ checks: ["pkece"]
```

Furthermore, string values are not supported anymore. This is to be able to handle fewer cases internally.
```diff
- checks: "state"
+ checks: ["state"]
```
2021-07-10 23:55:20 +02:00
Balázs Orbán
cff153bd80 Merge main into next 2021-07-10 12:02:21 +02:00
Balázs Orbán
a2e5afa162 feat(react): make session requireable in useSession (#2236)
A living session could be a requirement for specific pages (like dashboards). If it doesn’t exist, the user should be redirected to a page asking them to sign in again.

Sometimes, a user might log out by accident, or by deleting cookies on purpose. If that happens (e.g. on a separate tab), then `useSession({ required: true })` should detect the absence of a session cookie and always return a non-nullable Session object type.

When `required: true` is set, the default behavior will be to redirect the user to the sign-in page. This can be overridden by an `action()` callback:

```js
const session = useSession({
  required: true,
  action() {
    // ....
  }
})
if (session.status === "Loading") return "Loading or not authenticated..."

// session.data is always defined here.
```

Co-authored-by: Kristóf Poduszló <kripod@protonmail.com>
Co-authored-by: Lluis Agusti <hi@llu.lu>

BREAKING CHANGE:

The `useSession` hook now returns an object. Here is how to accommodate for this change:

```diff
- const [ session, loading ] = useSession()
+ const { data: session, status } = useSession()
+ const loading = status === "loading"
```

With the new `status` option, you can test states much more clearly.
2021-07-05 16:03:55 +02:00
Nico Domino
53e5e37948 docs: update tutorials/faq structure (#2256)
* docs: cleanup css

* docs: upgrade deps

* docs: cleanup docusaurus config

* docs: reorganise tutorials page

* docs: fix github-counter css

* docs: update faq page structure

* docs(tutorials): copy

Co-authored-by: Lluis Agusti <hi@llu.lu>

* docs(tutorials): copy

Co-authored-by: Lluis Agusti <hi@llu.lu>

* docs(tutorials): copy

Co-authored-by: Lluis Agusti <hi@llu.lu>

* docs(tutorials): copy

Co-authored-by: Lluis Agusti <hi@llu.lu>

* docs(style): reorg css

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-07-05 11:15:45 +02:00
Balázs Orbán
8ff4b26014 Merge main into next 2021-07-02 13:10:34 +02:00
Balázs Orbán
2c35aa27f9 Merge main into next 2021-06-29 22:55:33 +02:00
Balázs Orbán
2833b661bd feat(core): use named params in callbacks (#2173)
Some of our user-facing callbacks come with a bunch of parameters, and it is not always the case that a user needs all of them. Picking out certain parameters from the end of the list would require the user to define params that they wouldn't even need.

Therefore this PR changes such callbacks so the user can only pick the necessary parameters.

This comes with the bonus of better TS support on the `session` and `signIn` callbacks, where some parameters historically could have been different types.

In the `session` callback, the second param could have been `token` (when using JWT sessions) or `user` (when using DB persisted sessions). Now they are separate parameters.

In the `signIn` callback, we now separate `profile` (OAuth), `email` (Email) and `credentials` (Credentials) provider params.

BREAKING CHANGE:

The `callbacks` method signatures are changing the following way:

```diff
- signIn(user, account, profileOrEmailOrCredentials)
+ signIn({ user, account, profile, email, credentials })
```
```diff
- redirect(url, baseUrl)
+ redirect({ url, baseUrl })
```
```diff
- session(session, tokenOrUser)
+ session({ session, token, user })
```
```diff
- jwt(token, user, account, OAuthProfile, isNewUser)
+ jwt({ token, user, account, profile, isNewUser })
```

> NOTE: You only need to define the params that you actually need (no more need  for `_` params.)

This way, if you only need `token` and `account` in the `jwt` callback, you can write:

```js
jwt({ token, account }) {
  if(account) {
    token.accessToken = account.access_token
  }
  return token
}
```
2021-06-26 14:54:13 +02:00
Balázs Orbán
6c1a0ec620 feat: bump dependencies (#2254)
Node 10 has reached end-of-life, Next.js stopped supporting it in Next 11. Since we are a Next.js library, it doesn't make sense for us to support anything lower than that either.

I also upgraded a bunch of dependencies and dropped some that weren't necessary anymore.

BREAKING CHANGE:

The lowest supported Node version is 12. (We still support IE11 in browsers, until that is not dropped by Next.js itself)
2021-06-26 11:39:18 +02:00
Balázs Orbán
988c9912b1 test(ts): fix ts tests 2021-06-26 00:32:35 +02:00
Balázs Orbán
a225324d4f Merge main into next 2021-06-25 22:10:09 +02:00
Nico Domino
3a48b8e467 docs: update errors page with more details (#2196) 2021-06-22 20:15:17 +02:00
Lluis Agusti
fb50b54466 test(client-provider): fix flaky test (#2216) 2021-06-20 20:04:34 +02:00
Sheldon Vaughn
fa89431573 docs(provider): import useState in WorkOS example (#2198)
Co-authored-by: Balázs Orbán <info@balazsorban.com>

* Add useState dependency

I went to execute the signin.js file and received an error that useState was undefined. Was able to reconcile this by adding the dependency.

Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Sangwon Park <dev.psw@gmail.com>
2021-06-17 01:23:50 +02:00
Balázs Orbán
3383857715 Merge branch 'main' into next 2021-06-17 01:21:49 +02:00
David Peherstorfer
bbc2d9b538 docs(provider): scope expects space separated string (#2188)
* fix(docs): scope expects space separated string

Currently the docs list string[] as possible type for scope. 
However, It only accepts a string (with space as separator).

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-16 07:38:39 +02:00
Hugh Boylan
d10bd9beba fix(react): publish react scripts with npm build (#2192) 2021-06-15 23:27:06 +02:00
Vikrant Bhat
c1c866f664 docs(provider): English language sentence fix (#2175)
* English language sentence fix

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-14 23:55:10 +02:00
Balázs Orbán
86ff89e296 fix(react): initialize session without loading state (#2180)
See commit dd12181378
2021-06-14 21:16:42 +02:00
Hugh Boylan
dd12181378 initialize session without loading state (#2180) 2021-06-14 20:11:56 +02:00
Balázs Orbán
47c17a89ae Merge branch 'main' into next
commit 665445818e
Author: Balázs Orbán <info@balazsorban.com>
Date:   Sat Jun 12 17:11:53 2021 +0200

    docs(config): link to next documentation instead of canary

commit 67cf2a11bb
Author: ndom91 <yo@ndo.dev>
Date:   Sat Jun 12 16:42:48 2021 +0200

    docs: fix alt client provider example
2021-06-12 17:15:35 +02:00
Nico Domino
c07fe1b9a7 docs: add versioned docs for unreleased v4.0.0-next.* (#2167)
* docs: add versioned docs for unreleased canary 4.x

* docs(fix): prettier broke custom image component without .mdx extension

* docs(feat): automatically fetch latest stable version label

* docs: cleanup script

* docs: cleanup next version name

* docs(chore): cleanup version leftovers

* docs: fix .gitignore

* docs: v4 default

* docs: remove auto version generation

* docs: fix missing import

* docs: rm node-fetch
2021-06-12 17:04:32 +02:00
Balázs Orbán
abaa5aed65 fix(react): don't use localStorage on server side (#2166) 2021-06-11 22:11:50 +02:00
Balázs Orbán
ca0ed1e2a8 feat(react): create client tailored to React (#1473)
**What**:

These changes ensure that we work more tightly with React that can also result in unforeseen performance boosts. In case we would decide on expanding to other libraries/frameworks, a new file per framework could be added.

**Why**:

Some performance issues (https://github.com/nextauthjs/next-auth/issues/844) could only be fixed by moving more of the client code into the `Provider`.

**How**:

Refactoring `next-auth/client`

Related: #1461, #1084, #1462

BREAKING CHANGE:
**1.** `next-auth/client` is renamed to `next-auth/react`.

**2.** In the past, we exposed most of the functions with different names for convenience. To simplify our source code, the new React specific client code exports only the following functions, listed with the necessary changes:

- `setOptions`: Not exposed anymore, use `SessionProvider` props
- `options`: Not exposed anymore, use `SessionProvider` props
- `session`: Rename to `getSession`
- `providers`: Rename to `getProviders`
- `csrfToken`: Rename to `getCsrfToken`
- `signin`: Rename to `signIn`
- `signout`: Rename to `signOut`
- `Provider`: Rename to `SessionProvider`

**3.** `Provider` changes.
- `Provider` is renamed to `SessionProvider`
- The `options` prop is now flattened as the props of `SessionProvider`.
- `clientMaxAge` has been renamed to `staleTime`.
- `keepAlive` has been renamed to `refetchInterval`.
An example of the changes:
```diff
- <Provider options={{clientMaxAge: 0, keepAlive: 0}}>{children}</Provider>
+ <SessionProvider staleTime={0} refetchInterval={0}>{children}</SessionProvider> 
```

**4.** It is now **required** to wrap the part of your application that uses `useSession` into a `SessionProvider`.

Usually, the best place for this is in your `pages/_app.jsx` file:

```jsx
import { SessionProvider } from "next-auth/react"

export default function App({
  Component,
  pageProps: { session, ...pageProps }
}) {
  return (
    // `session` comes from `getServerSideProps` or `getInitialProps`.
    // Avoids flickering/session loading on first load.
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  )
}
```
2021-06-11 21:59:36 +02:00
Balázs Orbán
ed345346db fix(ts): add AzureAD to OAuthProviderType 2021-06-10 20:11:12 +02:00
Ben
5ac1db741a feat(provider): refactor Azure AD, B2C providers (#1591)
BREAKING CHANGE: 

If you currently use `AzureADB2C`, you will need to update it to to `AzureAD` There should be no other changes needed.
2021-06-10 20:09:34 +02:00
Balázs Orbán
0c17af969e Merge branch 'main' into next 2021-06-10 14:49:36 +02:00
Tom Richter
ea9b6e37a9 fix(provider): convert github profile id from int to string (#2108) 2021-06-09 17:02:52 +02:00
Balázs Orbán
960bc1e9c0 feat(adapter): remove adapters from core (#1919)
* feat(adapter): remove built-in adapters and database

BREAKING CHANGE:

From now on, you will have to import your own adapter

Check out https://github.com/nextauthjs/adapters

The migration is super easy and has HUGE advantages for those not using TypeORM.

```diff
// [...nextauth].js
+ import TypeORMAdapter from "@next-auth/typeorm-legacy-adapter"
import NextAuth from "next-auth"

...
export default NextAuth({
-  database: "yourconnectionstring",
+ adapter: TypeORMAdapter("yourconnectionstring")
})
```


Co-authored-by: Lluis Agusti <hi@llu.lu>
Co-authored-by: Giovanni Carnel <479046+g10@users.noreply.github.com>
2021-06-09 14:45:13 +02:00
Balázs Orbán
d29e3e9c9d Merge branch 'main'
Conflicts:
	config/babel.config.json
	package-lock.json
	package.json
	src/server/index.js
	src/server/routes/callback.js
	src/server/routes/signin.js
2021-06-09 02:16:11 +02:00
Balázs Orbán
a388b44d0b Merge branch 'main' into next 2021-05-03 21:11:04 +02:00
Balázs Orbán
b6a3a72db4 Merge branch 'main' into next 2021-04-24 23:20:41 +02:00
Balázs Orbán
edcb10a823 Merge branch 'main' into next 2021-04-23 15:43:20 +02:00
Balázs Orbán
2acabe19e0 Merge main into next 2021-04-23 15:28:26 +02:00
Balázs Orbán
a6f5f4c184 fix: use upgraded require optional (#1743)
* chore(deps): switch back to (updated) require_optional

* fix: use @balazsorban/require-optional
2021-04-16 16:05:44 +02:00
Balázs Orbán
9fa93e3b5e fix(build): use optional-require dependency (#1736)
* chore(deps): add optional-require

* refactor: use optional-require
2021-04-16 00:23:29 +02:00
Balázs Orbán
cb4342fdda feat(build): modernize how we bundle next-auth (#1682)
* feat(build): optionally include TypeORM

If the user doesn't use databases,
it shouldn't be necessary to iclude it in the bundle.
This can more than half the package size!

* feat(build): clean up in dependencies

Remove unused dependencies, move optional ones to be optional

* feat(build): add exports field

* fix: use peerDependenciesMeta instead of non-standard peerOptionalDependecns field

* fix: ts-standard string quotes

* fix: ts-standard string quotes

* refactor: use asnyc/await for sendVerificationRequest

* chore(deps): upgrade mongodb, remove require_optional

Co-authored-by: ndom91 <yo@ndo.dev>

BREAKING CHANGE:
`typeorm`, and `nodemailer` are no longer dependencies added by default.
If you need any of them, you will have to install them yourself in your project directory.
TypeOrm is the default adapter, so if you only provide an `adapter` configuration or a `database`, you will need `typeorm`. You could also check out `@next-auth/typeorm-adapter`. In case you are using the Email provider, you will have to install `nodemailer` (or you can use the choice of your library in the `sendVerificationRequest` callback to send out the e-mail.)
2021-04-15 23:40:33 +02:00
Balázs Orbán
5f717b3914 chore: merge main into next 2021-04-12 00:46:27 +02:00
Balázs Orbán
d09a45ec7c chore: merge main into next 2021-03-26 16:23:35 +01:00
Balázs Orbán
930f58eba3 chore: merge main into next 2021-03-08 01:05:54 +01:00
Balázs Orbán
c20b7f2930 feat: use IE11 as client code bundle target (#1402) 2021-03-03 20:25:42 +01:00
Balázs Orbán
e418cddd96 chore: merge main into next 2021-03-03 20:25:42 +01:00
Balázs Orbán
111e7aabdf feat(provider): remove state property
BREAKING CHANGE: adding `state: true` is already redundant
as `protection: "state` is the default value. `state: false`
can be substituted with `protection: "state"`
2021-02-15 21:47:47 +01:00
Balázs Orbán
a113ef6fab feat: encourage returning strings instead of throwing
BREAKING CHANGE: We have supported throwing strings
for redirections, while we were showing a waring.
From now on, it is not possible. The user MUST return a string,
rather than throw it.
2021-02-15 21:47:35 +01:00
273 changed files with 26314 additions and 28631 deletions

1
.github/CODEOWNERS vendored
View File

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

1
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,4 @@
# https://docs.github.com/en/github/administering-a-repository/displaying-a-sponsor-button-in-your-repository
open_collective: nextauth
github: [balazsorban44]

View File

@@ -1,43 +0,0 @@
---
name: Bug report
about: Report a defect with NextAuth.js
labels: bug
assignees: ""
---
## Description 🐜
Please provide a clear and concise description of the bug in NextAuth.js.
🚧 _Do not report bugs with your own project here; ask for help [by raising a question instead](https://github.com/nextauthjs/next-auth/issues/new?assignees=&labels=question&template=question.md) - this helps us a lot with administration overhead._
## How to reproduce ☕️
We encourage you to use one of the templates set up on **CodeSandbox** to reproduce your issue:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
🚧 _If you don't provide any way to reproduce the bug, the issue is at risk of being closed._
## Screenshots / Logs 📽
**Help us help you**. We can address the bug you found much faster if you provide contextual screenshots or screen recordings showcasing the issue.
See [Kap](https://getkap.co/) for a good, easy-to-use, cross-platform screen recording tool.
## Environment 🖥
Please run this command:
```
$ npx envinfo --system --binaries --browsers --npmPackages "{next-auth}"
```
and paste the output here.
## Contributing 🙌🏽
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
In case you're willing to help fix this bug, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.

91
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@@ -0,0 +1,91 @@
name: Bug Report
description: File a bug report
labels: bug
# note: markdown sections will NOT appear as part of the issue as per documentation, rather they provide context to the user
# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#markdown
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Please provide the following information:
- type: textarea
id: description
attributes:
label: Description 🐜
description: Please provide a clear and concise description of the bug in NextAuth.js
validations:
required: true
- type: dropdown
id: ownproject
attributes:
label: Is this a bug in your own project?
description: 🚧 _Do not report bugs with your own project here; ask for help [by raising a question instead](https://github.com/nextauthjs/next-auth/issues/new?assignees=&labels=question&template=question.md) or use the [Discussions tab](https://github.com/nextauthjs/next-auth/discussions) - this helps us reduce the maintenance overhead._
multiple: false
options:
- "Yes"
- "No"
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: How to reproduce ☕️
description: Please provide a link or code snippets to a minimal reproduction of the bug
validations:
required: true
- type: markdown
attributes:
value: |
We encourage you to use one of the templates set up on **CodeSandbox** to reproduce your issue:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
🚧 _If you don't provide any way to reproduce the bug, the issue is at risk of being closed._
- type: textarea
id: logs
attributes:
label: Screenshots / Logs 📽
description: We can address the bug you found much faster if you provide contextual screenshots or screen recordings showcasing the issue.
- type: markdown
attributes:
value: |
See [Kap](https://getkap.co/) for a good, easy-to-use, cross-platform screen recording tool.
validations:
required: false
- type: textarea
id: environment
attributes:
label: Environment 🖥
validations:
required: true
- type: markdown
attributes:
value: |
Please run this command in your project's root folder:
```sh
npx envinfo --system --binaries --browsers --npmPackages "next,next-auth,react"
```
- type: dropdown
id: pr
attributes:
label: Contributing 🙌🏽
multiple: false
options:
- "Yes, I am willing to help solve this bug in a PR"
- "No, I am afraid I cannot help regarding this"
validations:
required: true
- type: markdown
attributes:
value: |
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚

View File

@@ -1,39 +0,0 @@
---
name: Feature request
about: Suggest an idea for NextAuth.js
labels: enhancement
assignees: ""
---
## Summary 💭
A clear and concise summary of the feature being proposed.
## Description 📓
Please provide a more in-depth description of the feature proposed.
Make sure you provide plenty of [links]() to external documentation and inline code examples like so:
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
Take time thinking about what you want to say and help us understand your proposal making sure that this description contains:
- **purpose of the feature**
- **potential problems**
- **potential alternatives**
You can use one of the templates set up on **CodeSandbox** to better illustrate your idea:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
## Contributing 🙌🏽
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
In case you're willing to help implement this feature, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.

View File

@@ -0,0 +1,68 @@
name: Feature Request
description: Suggest an idea for NextAuth.js
labels: enhancement
# note: markdown sections will NOT appear as part of the issue as per documentation, rather they provide context to the user
# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#markdown
body:
- type: markdown
attributes:
value: |
Thank you very much for reaching out to us regarding the awesome feature that you believe should be included in the NextAuth.js library. Please provide the following information:
- type: textarea
id: description
attributes:
label: Description 📓
description: Please provide a more in-depth description of the feature proposed.
validations:
required: true
- type: markdown
attributes:
value: |
Make sure you provide plenty of [links]() to external documentation and inline code examples like so:
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
Take time thinking about what you want to say and help us understand your proposal making sure that this description contains:
- **purpose of the feature**
- **potential problems**
- **potential alternatives**
- type: textarea
id: reproduction
attributes:
label: How to reproduce ☕️
description: If you have a CodeSandbox playground or some code snippets to help us visualize your idea better, please provide it here.
validations:
required: true
- type: markdown
attributes:
value: |
You can use one of the templates set up on **CodeSandbox** to better illustrate your idea:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
- type: dropdown
id: pr
attributes:
label: Contributing 🙌🏽
multiple: false
options:
- "Yes, I am willing to help implement this feature in a PR"
- "No, I am afraid I cannot help regarding this"
validations:
required: true
- type: markdown
attributes:
value: |
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚

View File

@@ -1,32 +0,0 @@
---
name: Question
about: Ask a question about NextAuth.js or for help using it
labels: question
assignees: ""
---
## Question 💬
Please provide an in-depth description of the question you have.
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
## How to reproduce ☕️
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
## Contributing 🙌🏽
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
In case you're willing to help answer this question, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.

62
.github/ISSUE_TEMPLATE/question.yaml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: Question
description: Ask a question about NextAuth.js or for help using it
labels: question
# note: markdown sections will NOT appear as part of the issue as per documentation, rather they provide context to the user
# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#markdown
body:
- type: markdown
attributes:
value: |
We are glad that you have a question about this library. Please provide the following information:
- type: textarea
id: question
attributes:
label: Question 💬
description: Please provide an in-depth description of the question you have.
validations:
required: true
- type: markdown
attributes:
value: |
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
- type: textarea
id: reproduction
attributes:
label: How to reproduce ☕️
description: Please provide a link to a minimal reproduction or code snippets that represents your question
validations:
required: true
- type: markdown
attributes:
value: |
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
- type: dropdown
id: pr
attributes:
label: Contributing 🙌🏽
multiple: false
options:
- "Yes, I am willing to help answer this question in a PR"
- "No, I am afraid I cannot help regarding this"
validations:
required: true
- type: markdown
attributes:
value: |
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚

View File

@@ -1,36 +0,0 @@
---
name: TypeScript
about: Ask a question about NextAuth.js TypeScript integration
labels:
- question
- TypeScript
assignees:
- lluia
- balazsorban44
---
## Question 💬
Please provide an in-depth description of the question you have when using NextAuth.js on a Typescript project or when consuming the built-in types for `next-auth`.
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
## How to reproduce ☕️
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
## Contributing 🙌🏽
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
In case you're willing to help answer this TypeScript question, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.

58
.github/ISSUE_TEMPLATE/typescript.yaml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: TypeScript
description: Ask a question about NextAuth.js TypeScript integration
labels: [question, TypeScript]
assignees: [lluia, balazsorban44]
# note: markdown sections will NOT appear as part of the issue as per documentation, rather they provide context to the user
# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#markdown
body:
- type: textarea
id: question
attributes:
label: Question 💬
description: Please provide an in-depth description of the question you have when using NextAuth.js on a Typescript project or when consuming the built-in types for `next-auth`.
validations:
required: true
- type: markdown
attributes:
value: |
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
- type: textarea
id: codesandbox
attributes:
label: How to reproduce ☕️
description: Please provide a link to a minimal reproduction or code snippets that represents your question
validations:
required: true
- type: markdown
attributes:
value: |
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
- type: dropdown
id: pr
attributes:
label: Contributing 🙌🏽
multiple: false
options:
- "Yes, I am willing to help answer this question in a PR"
- "No, I am afraid I cannot help regarding this"
validations:
required: true
- type: markdown
attributes:
value: |
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚

View File

@@ -1,4 +1,4 @@
name: Release Flow
name: Release
on:
push:
@@ -11,37 +11,66 @@ on:
jobs:
test:
name: Tests
name: Test
runs-on: ubuntu-latest
steps:
- name: Init
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 16
- name: Dependencies
uses: bahmutov/npm-install@v1
- name: Build
run: npm run build
- name: Run tests
run: npm test -- --coverage --verbose
run: npm test -- --coverage --verbose && npm run test:types
- name: Coverage
uses: codecov/codecov-action@v1
with:
directory: ./coverage
fail_ci_if_error: false
- name: Build
run: npm run build
release:
name: Release
needs: test
release-branch:
name: Publish branch
runs-on: ubuntu-latest
needs: test
if: ${{ github.event_name == 'push' }}
environment: Production
steps:
- name: Init
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 16
- name: Dependencies
uses: bahmutov/npm-install@v1
- name: Release
- name: Publish to npm and GitHub
run: npx semantic-release@17
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
release-pr:
name: Publish PR
runs-on: ubuntu-latest
needs: test
if: ${{ github.event_name == 'pull_request' }}
environment: Preview
steps:
- name: Init
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 16
- name: Dependencies
uses: bahmutov/npm-install@v1
- name: Publish to npm
run: |
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
npm run version:pr
npm publish --access public --tag experimental
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
PR_NUMBER: ${{ github.event.number }}

4
.gitignore vendored
View File

@@ -40,6 +40,8 @@ src/providers/index.js
/providers.js
/errors.js
/errors.d.ts
/react.js
/react.d.ts
# Development app
app/next-auth
@@ -61,4 +63,4 @@ app/yarn.lock
/prisma/migrations
# Tests
/coverage
/coverage

View File

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

View File

@@ -11,43 +11,49 @@ Please raise any significant new functionality or breaking change an issue for d
## For contributors
Anyone can be a contributor. Either you found a typo, or you have an awesome feature request you could implement, we encourage you to create a Pull Request.
### Pull Requests
* 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
* 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
- 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
- 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
### Setting up local environment
A quick guide on how to setup *next-auth* locally to work on it and test out any changes:
A quick guide on how to setup _next-auth_ locally to work on it and test out any changes:
The dev application requires you to use `npm@7`.
1. Clone the repo:
```sh
git clone git@github.com:nextauthjs/next-auth.git
cd next-auth
```
2. Install packages:
2. Install packages, set up the dev application:
```sh
npm i && npm run dev:setup
npm run dev:setup
```
3. Populate `.env.local`:
Copy `app/.env.local.example` to `app/.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`app/pages/api/auth/[...nextauth].js`.
1. Start the dev application/server:
```sh
npm run dev
```
Your dev application will be available on ```http://localhost:3000```
Your dev application will be available on `http://localhost:3000`
That's it! 🎉
@@ -64,6 +70,7 @@ When running `npm run dev`, you start a Next.js dev server on `http://localhost:
#### 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)
@@ -73,40 +80,27 @@ You can look at the existing built-in providers for inspiration.
#### Databases
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.
You can start them with `npm run db:start` and stop them with `npm run db:stop`.
You will need Docker and Docker Compose installed to be able to start / stop the databases.
When stopping the databases, it will reset their contents.
If you would like to contribute to an existing database adapter or help create a new one, head over to the [nextauthjs/adapters](https://www.github.com/nextauthjs/adapters) repository and follow the instructions provided there.
#### Testing
Tests can be run with `npm run test`.
Automated tests are currently crude and limited in functionality, but improvements are in development.
Currently, to run tests you need to first have started local test databases (e.g. using `npm run db:start`).
The databases can take a few seconds to start up, so you might need to give it a minute before running the tests.
## For maintainers
We use [semantic-release](https://github.com/semantic-release/semantic-release) together with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to automate releases. This makes the maintenance process easier and less error-prone to human error. Please study the "Conventional Commits" site to understand how to write a good commit message.
When accepting Pull Requests, make sure the following:
* Use "Squash and merge"
* Make sure you merge contributor PRs into `main`
* Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice.
* Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged. `semantic-release` will also comment back to connected issues and PRs, notifying the users that a feature is added/bug fixed, etc.)
- Use "Squash and merge"
- Make sure you merge contributor PRs into `main`
- Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice.
- Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged. `semantic-release` will also comment back to connected issues and PRs, notifying the users that a feature is added/bug fixed, etc.)
### Recommended Scopes
A typical conventional commit looks like this:
```
type(scope): title
@@ -121,9 +115,8 @@ Some recommended scopes are:
- **adapter** - Adapter related changes. (eg.: "feat(adapter): add X provider", "docs(provider): fix typo in X documentation"
- **db** - Database related changes. (eg.: "feat(db): add X database", "docs(db): fix typo in X documentation"
- **deps** - Adding/removing/updating a dependency (eg.: "chore(deps): add X")
> NOTE: If you are not sure which scope to use, you can simply ignore it. (eg.: "feat: add something"). Adding the correct type already helps a lot when analyzing the commit messages.
> NOTE: If you are not sure which scope to use, you can simply ignore it. (eg.: "feat: add something"). Adding the correct type already helps a lot when analyzing the commit messages.
### Skipping a release

112
README.md
View File

@@ -38,7 +38,7 @@ It is designed from the ground up to support Next.js and Serverless.
npm install --save next-auth
```
The easiest way to continue getting started, is to follow the [getting started](https://next-auth.js.org/getting-started/example) section in our docs.
The easiest way to continue getting started, is to follow the [getting started](https://next-auth.js.org/getting-started/example) section in our docs.
We also have a section of [tutorials](https://next-auth.js.org/tutorials) for those looking for more specific examples.
@@ -48,96 +48,116 @@ See [next-auth.js.org](https://next-auth.js.org) for more information and docume
### Flexible and easy to use
* Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
* Built-in support for [many popular sign-in services](https://next-auth.js.org/configuration/providers)
* Supports email / passwordless authentication
* Supports stateless authentication with any backend (Active Directory, LDAP, etc)
* Supports both JSON Web Tokens and database sessions
* Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
- Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
- Built-in support for [many popular sign-in services](https://next-auth.js.org/configuration/providers)
- Supports email / passwordless authentication
- Supports stateless authentication with any backend (Active Directory, LDAP, etc)
- Supports both JSON Web Tokens and database sessions
- Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
### Own your own data
NextAuth.js can be used with or without a database.
* An open source solution that allows you to keep control of your data
* Supports Bring Your Own Database (BYOD) and can be used with any database
* Built-in support for [MySQL, MariaDB, Postgres, Microsoft SQL Server, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
* Works great with databases from popular hosting providers
* Can also be used *without a database* (e.g. OAuth + JWT)
- An open source solution that allows you to keep control of your data
- Supports Bring Your Own Database (BYOD) and can be used with any database
- Built-in support for [MySQL, MariaDB, Postgres, Microsoft SQL Server, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
- Works great with databases from popular hosting providers
- Can also be used _without a database_ (e.g. OAuth + JWT)
### Secure by default
* Promotes the use of passwordless sign in mechanisms
* Designed to be secure by default and encourage best practice for safeguarding user data
* Uses Cross Site Request Forgery Tokens on POST routes (sign in, sign out)
* Default cookie policy aims for the most restrictive policy appropriate for each cookie
* When JSON Web Tokens are enabled, they are signed by default (JWS) with HS512
* Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
* Auto-generates symmetric signing and encryption keys for developer convenience
* Features tab/window syncing and keepalive messages to support short lived sessions
* Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org/)
- Promotes the use of passwordless sign-in mechanisms
- Designed to be secure by default and encourage best practices for safeguarding user data
- Uses Cross-Site Request Forgery (CSRF) Tokens on POST routes (sign in, sign out)
- Default cookie policy aims for the most restrictive policy appropriate for each cookie
- When JSON Web Tokens are enabled, they are signed by default (JWS) with HS512
- Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
- Auto-generates symmetric signing and encryption keys for developer convenience
- Features tab/window syncing and session polling to support short lived sessions
- Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org)
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
### TypeScript
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentation.
The package at `@types/next-auth` is now deprecated.
## Example
### Add API Route
```javascript
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
// pages/api/auth/[...nextauth].js
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
export default NextAuth({
providers: [
// OAuth authentication providers
Providers.Apple({
clientId: process.env.APPLE_ID,
clientSecret: process.env.APPLE_SECRET
clientSecret: process.env.APPLE_SECRET,
}),
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET
clientSecret: process.env.GOOGLE_SECRET,
}),
// Sign in with passwordless email link
Providers.Email({
server: process.env.MAIL_SERVER,
from: '<no-reply@example.com>'
from: "<no-reply@example.com>",
}),
],
// SQL or MongoDB database (or leave empty)
database: process.env.DATABASE_URL
})
```
### Add React Component
### Add React Hook
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
```javascript
import {
useSession, signIn, signOut
} from 'next-auth/client'
import { useSession, signIn, signOut } from "next-auth/react"
export default function Component() {
const [ session, loading ] = useSession()
if(session) {
return <>
Signed in as {session.user.email} <br/>
<button onClick={() => signOut()}>Sign out</button>
</>
const { data: session } = useSession()
if (session) {
return (
<>
Signed in as {session.user.email} <br />
<button onClick={() => signOut()}>Sign out</button>
</>
)
}
return <>
Not signed in <br/>
<button onClick={() => signIn()}>Sign in</button>
</>
return (
<>
Not signed in <br />
<button onClick={() => signIn()}>Sign in</button>
</>
)
}
```
## Acknowledgements
### Share/configure session state
Use the `<SessionProvider>` to allows instances of `useSession()` to share the session object across components. It also takes care of keeping the session updated and synced between tabs/windows.
```jsx title="pages/_app.js"
import { SessionProvider } from "next-auth/react"
export default function App({
Component,
pageProps: { session, ...pageProps }
}) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}
```
## Acknowledgments
[NextAuth.js is made possible thanks to all of its contributors.](https://next-auth.js.org/contributors)

View File

@@ -14,10 +14,10 @@ We request that you contact us directly to report serious issues that might impa
If you contact us regarding a serious issue:
* We will endeavor to get back to you within 72 hours.
* We will aim to publish a fix within 30 days.
* We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
* If 90 days has elapsed and we still don't have a fix, we will disclose the issue publicly.
- We will endeavor to get back to you within 72 hours.
- We will aim to publish a fix within 30 days.
- We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
- If 90 days has elapsed and we still don't have a fix, we will disclose the issue publicly.
Currently, the best way to report an issue is by emailing me@iaincollins.com

View File

@@ -10,12 +10,19 @@ NEXTAUTH_URL=http://localhost:3000
SECRET=
AUTH0_ID=
AUTH0_DOMAIN=
AUTH0_SECRET=
AUTH0_ISSUER=
IDS4_ID=
IDS4_SECRET=
IDS4_ISSUER=
GITHUB_ID=
GITHUB_SECRET=
TWITCH_ID=
TWITCH_SECRET=
TWITTER_ID=
TWITTER_SECRET=
@@ -23,8 +30,6 @@ TWITTER_SECRET=
EMAIL_SERVER=smtps://user@gmail.com:password@smtp.gmail.com:465
EMAIL_FROM=user@gmail.com
# You can use any of these as the "DATABASE_URL" for
# databases started with Docker using `npm run db:start`.
# Note: If using with Prisma adapter, you need to use a `.env`
# file rather than a `.env.local` file to configure env vars.
# Postgres: DATABASE_URL=postgres://nextauth:password@127.0.0.1:5432/nextauth?synchronize=true

View File

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

View File

@@ -1,17 +1,29 @@
import Link from 'next/link'
import styles from './footer.module.css'
import { version } from 'package.json'
import Link from "next/link"
import styles from "./footer.module.css"
import packageJSON from "package.json"
export default function Footer () {
export default function Footer() {
return (
<footer className={styles.footer}>
<hr />
<ul className={styles.navItems}>
<li className={styles.navItem}><a href='https://next-auth.js.org'>Documentation</a></li>
<li className={styles.navItem}><a href='https://www.npmjs.com/package/next-auth'>NPM</a></li>
<li className={styles.navItem}><a href='https://github.com/nextauthjs/next-auth-example'>GitHub</a></li>
<li className={styles.navItem}><Link href='/policy'><a>Policy</a></Link></li>
<li className={styles.navItem}><em>{version}</em></li>
<li className={styles.navItem}>
<a href="https://next-auth.js.org">Documentation</a>
</li>
<li className={styles.navItem}>
<a href="https://www.npmjs.com/package/next-auth">NPM</a>
</li>
<li className={styles.navItem}>
<a href="https://github.com/nextauthjs/next-auth-example">GitHub</a>
</li>
<li className={styles.navItem}>
<Link href="/policy">
<a>Policy</a>
</Link>
</li>
<li className={styles.navItem}>
<em>{packageJSON.version}</em>
</li>
</ul>
</footer>
)

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,57 +4,27 @@ import GitHubProvider from "next-auth/providers/github"
import Auth0Provider from "next-auth/providers/auth0"
import TwitterProvider from "next-auth/providers/twitter"
import CredentialsProvider from "next-auth/providers/credentials"
// import Adapters from 'next-auth/adapters'
// import { PrismaClient } from '@prisma/client'
// const prisma = new PrismaClient()
import IDS4Provider from "next-auth/providers/identity-server4"
import Twitch from "next-auth/providers/twitch"
import GoogleProvider from "next-auth/providers/google"
import FacebookProvider from "next-auth/providers/facebook"
import FoursquareProvider from "next-auth/providers/foursquare"
// import FreshbooksProvider from "next-auth/providers/freshbooks"
import GitlabProvider from "next-auth/providers/gitlab"
import InstagramProvider from "next-auth/providers/instagram"
import LineProvider from "next-auth/providers/line"
import LinkedInProvider from "next-auth/providers/linkedin"
import MailchimpProvider from "next-auth/providers/mailchimp"
import DiscordProvider from "next-auth/providers/discord"
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: [
// E-mail
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
}),
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Auth0Provider({
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",
}),
TwitterProvider({
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET,
}),
// Credentials
CredentialsProvider({
name: "Credentials",
credentials: {
@@ -72,20 +42,75 @@ export default NextAuth({
return null
},
}),
// OAuth 1
TwitterProvider({
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET,
}),
// OAuth 2 / OIDC
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Auth0Provider({
clientId: process.env.AUTH0_ID,
clientSecret: process.env.AUTH0_SECRET,
issuer: process.env.AUTH0_ISSUER,
}),
Twitch({
clientId: process.env.TWITCH_ID,
clientSecret: process.env.TWITCH_SECRET,
}),
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
FacebookProvider({
clientId: process.env.FACEBOOK_ID,
clientSecret: process.env.FACEBOOK_SECRET,
}),
FoursquareProvider({
clientId: process.env.FOURSQUARE_ID,
clientSecret: process.env.FOURSQUARE_SECRET,
}),
// FreshbooksProvider({
// clientId: process.env.FRESHBOOKS_ID,
// clientSecret: process.env.FRESHBOOKS_SECRET,
// }),
GitlabProvider({
clientId: process.env.GITLAB_ID,
clientSecret: process.env.GITLAB_SECRET,
}),
InstagramProvider({
clientId: process.env.INSTAGRAM_ID,
clientSecret: process.env.INSTAGRAM_SECRET,
}),
LineProvider({
clientId: process.env.LINE_ID,
clientSecret: process.env.LINE_SECRET,
}),
LinkedInProvider({
clientId: process.env.LINKEDIN_ID,
clientSecret: process.env.LINKEDIN_SECRET,
}),
MailchimpProvider({
clientId: process.env.MAILCHIMP_ID,
clientSecret: process.env.MAILCHIMP_SECRET,
}),
IDS4Provider({
clientId: process.env.IDS4_ID,
clientSecret: process.env.IDS4_SECRET,
issuer: process.env.IDS4_ISSUER,
}),
DiscordProvider({
clientId: process.env.DISCORD_ID,
clientSecret: process.env.DISCORD_SECRET,
}),
],
jwt: {
encryption: true,
secret: process.env.SECRET,
},
debug: false,
debug: true,
theme: "auto",
// Default Database Adapter (TypeORM)
// database: process.env.DATABASE_URL
// Prisma Database Adapter
// To configure this app to use the schema in `prisma/schema.prisma` run:
// npx prisma generate
// npx prisma migrate dev
// adapter: Adapters.Prisma.Adapter({ prisma })
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

17
config/version-pr.js Normal file
View File

@@ -0,0 +1,17 @@
const fs = require("fs-extra")
const path = require("path")
try {
const packageJSONPath = path.join(process.cwd(), "package.json")
const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath, "utf8"))
const sha8 = process.env.GITHUB_SHA.substr(0, 8)
const prNumber = process.env.PR_NUMBER
packageJSON.version = `0.0.0-pr.${prNumber}.${sha8}`
fs.writeFileSync(packageJSONPath, JSON.stringify(packageJSON))
} catch (error) {
console.error("Could not set PR version", error)
process.exit(1)
}

35137
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,8 +22,7 @@
"exports": {
".": "./dist/server/index.js",
"./jwt": "./dist/lib/jwt.js",
"./adapters": "./dist/adapters/index.js",
"./client": "./dist/client/index.js",
"./react": "./dist/client/react.js",
"./providers": "./dist/providers/index.js",
"./providers/*": "./dist/providers/*.js",
"./errors": "./dist/lib/errors.js"
@@ -32,7 +31,7 @@
"build": "npm run build:js && npm run build:css",
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.js src --out-dir dist",
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
"dev:setup": "npm run build:css && cd app && npm i",
"dev:setup": "npm i && 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.js --watch src --out-dir dist",
@@ -42,7 +41,9 @@
"test:types": "dtslint types --onlyTestTsNext",
"prepublishOnly": "npm run build",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
"lint:fix": "eslint . --fix",
"version:pr": "node ./config/version-pr",
"website": "cd www && npm run start"
},
"files": [
"dist",
@@ -52,8 +53,8 @@
"providers.d.ts",
"adapters.js",
"adapters.d.ts",
"client.js",
"client.d.ts",
"react.js",
"react.d.ts",
"errors.js",
"errors.d.ts",
"jwt.js",
@@ -62,73 +63,64 @@
],
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.14.0",
"@next-auth/prisma-legacy-adapter": "0.0.1-canary.127",
"@next-auth/typeorm-legacy-adapter": "0.0.2-canary.129",
"futoin-hkdf": "^1.3.2",
"@babel/runtime": "^7.14.6",
"futoin-hkdf": "^1.3.3",
"jose": "^1.27.2",
"jsonwebtoken": "^8.5.1",
"nodemailer": "^6.4.16",
"oauth": "^0.9.15",
"pkce-challenge": "^2.1.0",
"preact": "^10.4.1",
"preact-render-to-string": "^5.1.14",
"querystring": "^0.2.0"
"openid-client": "^4.7.4",
"preact": "^10.5.13",
"preact-render-to-string": "^5.1.19"
},
"peerDependencies": {
"react": "^16.13.1 || ^17",
"react-dom": "^16.13.1 || ^17"
"nodemailer": "^6.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"peerOptionalDependencies": {
"mongodb": "^3.5.9",
"mysql": "^2.18.1",
"mssql": "^6.2.1",
"pg": "^8.2.1",
"@prisma/client": "^2.16.1"
"peerDependenciesMeta": {
"nodemailer": {
"optional": true
}
},
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.13.13",
"@semantic-release/commit-analyzer": "^8.0.1",
"@semantic-release/github": "^7.2.0",
"@semantic-release/npm": "7.0.8",
"@semantic-release/release-notes-generator": "^9.0.1",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.6",
"@babel/cli": "^7.14.5",
"@babel/core": "^7.14.6",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.14.7",
"@babel/preset-react": "^7.14.5",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.1.9",
"@types/react": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"autoprefixer": "^9.7.6",
"babel-jest": "^26.6.3",
"@types/nodemailer": "^6.4.2",
"@types/oauth": "^0.9.1",
"@types/react": "^17.0.11",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"autoprefixer": "^10.2.6",
"babel-jest": "^27.0.5",
"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",
"conventional-changelog-conventionalcommits": "4.6.0",
"cssnano": "^5.0.6",
"dtslint": "^4.1.0",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard-with-typescript": "^20.0.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-promise": "^5.1.0",
"fs-extra": "^10.0.0",
"husky": "^6.0.0",
"jest": "^26.6.3",
"msw": "^0.28.2",
"next": "^10.0.5",
"postcss-cli": "^7.1.1",
"postcss-nested": "^4.2.1",
"prettier": "^2.2.1",
"pretty-quick": "^3.1.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"typescript": "^4.1.3",
"jest": "^27.0.5",
"msw": "^0.30.0",
"next": "^11.0.1",
"postcss-cli": "^8.3.1",
"postcss-nested": "^5.0.5",
"prettier": "^2.3.1",
"pretty-quick": "^3.1.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript": "^4.3.4",
"whatwg-fetch": "^3.6.2"
},
"prettier": {
@@ -157,6 +149,9 @@
"location": "readonly",
"fetch": "readonly"
},
"rules": {
"camelcase": "off"
},
"overrides": [
{
"files": [

View File

@@ -1,4 +1,4 @@
import { UnknownError } from "../lib/errors"
import { capitalize, UnknownError, upperSnake } from "../lib/errors"
/**
* Handles adapter induced errors.
@@ -9,7 +9,7 @@ import { UnknownError } from "../lib/errors"
export default function adapterErrorHandler(adapter, logger) {
return Object.keys(adapter).reduce((acc, method) => {
const name = capitalize(method)
const code = upperSnake(name, adapter.displayName)
const code = `${adapter.displayName ?? "ADAPTER"}_${upperSnake(name)}`
const adapterMethod = adapter[method]
acc[method] = async (...args) => {
@@ -26,11 +26,3 @@ export default function adapterErrorHandler(adapter, logger) {
return acc
}, {})
}
function capitalize(s) {
return `${s[0].toUpperCase()}${s.slice(1)}`
}
function upperSnake(s, prefix = "ADAPTER") {
return `${prefix}_${s.replace(/([A-Z])/g, "_$1")}`.toUpperCase()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

385
src/client/react.js vendored Normal file
View File

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

View File

@@ -33,66 +33,10 @@ export class AccountNotLinkedError extends UnknownError {
name = "AccountNotLinkedError"
}
export class CreateUserError extends UnknownError {
name = "CreateUserError"
export function upperSnake(s) {
return s.replace(/([A-Z])/g, "_$1").toUpperCase()
}
export class GetUserError extends UnknownError {
name = "GetUserError"
}
export class GetUserByEmailError extends UnknownError {
name = "GetUserByEmailError"
}
export class GetUserByIdError extends UnknownError {
name = "GetUserByIdError"
}
export class GetUserByProviderAccountIdError extends UnknownError {
name = "GetUserByProviderAccountIdError"
}
export class UpdateUserError extends UnknownError {
name = "UpdateUserError"
}
export class DeleteUserError extends UnknownError {
name = "DeleteUserError"
}
export class LinkAccountError extends UnknownError {
name = "LinkAccountError"
}
export class UnlinkAccountError extends UnknownError {
name = "UnlinkAccountError"
}
export class CreateSessionError extends UnknownError {
name = "CreateSessionError"
}
export class GetSessionError extends UnknownError {
name = "GetSessionError"
}
export class UpdateSessionError extends UnknownError {
name = "UpdateSessionError"
}
export class DeleteSessionError extends UnknownError {
name = "DeleteSessionError"
}
export class CreateVerificationRequestError extends UnknownError {
name = "CreateVerificationRequestError"
}
export class GetVerificationRequestError extends UnknownError {
name = "GetVerificationRequestError"
}
export class DeleteVerificationRequestError extends UnknownError {
name = "DeleteVerificationRequestError"
export function capitalize(s) {
return `${s[0].toUpperCase()}${s.slice(1)}`
}

View File

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

33
src/lib/merge.js Normal file
View File

@@ -0,0 +1,33 @@
// Source: https://stackoverflow.com/a/34749873/5364135
/**
* Simple object check.
* @param item
* @returns {boolean}
*/
function isObject(item) {
return item && typeof item === "object" && !Array.isArray(item)
}
/**
* Deep merge two objects.
* @param target
* @param ...sources
*/
export function merge(target, ...sources) {
if (!sources.length) return target
const source = sources.shift()
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} })
merge(target[key], source[key])
} else {
Object.assign(target, { [key]: source[key] })
}
}
}
return merge(target, ...sources)
}

View File

@@ -1,20 +1,19 @@
export default function FortyTwo(options) {
return {
id: '42-school',
name: '42 School',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.intra.42.fr/oauth/token',
authorizationUrl:
'https://api.intra.42.fr/oauth/authorize?response_type=code',
profileUrl: 'https://api.intra.42.fr/v2/me',
profile: (profile) => ({
id: profile.id,
email: profile.email,
image: profile.image_url,
name: profile.usual_full_name,
}),
...options,
id: "42-school",
name: "42 School",
type: "oauth",
authorization: "https://api.intra.42.fr/oauth/authorize",
token: "https://api.intra.42.fr/oauth/token",
userinfo: "https://api.intra.42.fr/v2/me",
profile(profile) {
return {
id: profile.id,
name: profile.usual_full_name,
email: profile.email,
image: profile.image_url,
}
},
options,
}
}

View File

@@ -3,32 +3,34 @@ export default function Apple(options) {
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,
authorization: {
url: "https://appleid.apple.com/auth/authorize",
params: {
scope: "name email",
response_type: "code",
id_token: "",
response_mode: "form_post",
},
},
token: {
url: "https://appleid.apple.com/auth/token",
idToken: true,
},
jwks_endpoint: "https://appleid.apple.com/auth/keys",
profile(profile) {
// The name of the user will only return on first login
// The name of the user will only be returned on first login
const name = profile.user
? profile.user.name.firstName + " " + profile.user.name.lastName
: null
return {
id: profile.sub,
name:
profile.user != null
? profile.user.name.firstName + " " + profile.user.name.lastName
: null,
name,
email: profile.email,
image: null,
}
},
clientId: null,
clientSecret: {
teamId: null,
privateKey: null,
keyId: null,
},
protection: "none", // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
...options,
checks: ["none"], // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
options,
}
}

View File

@@ -3,14 +3,15 @@ export default function Atlassian(options) {
id: "atlassian",
name: "Atlassian",
type: "oauth",
version: "2.0",
params: {
grant_type: "authorization_code",
authorization: {
url: "https://auth.atlassian.com/oauth/authorize",
params: {
audience: "api.atlassian.com",
prompt: "consent",
},
},
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",
token: "https://auth.atlassian.com/oauth/token",
userinfo: "https://api.atlassian.com/me",
profile(profile) {
return {
id: profile.account_id,
@@ -19,6 +20,6 @@ export default function Atlassian(options) {
image: profile.picture,
}
},
...options,
options,
}
}

View File

@@ -1,14 +1,13 @@
/** @type {import("types/providers").OAuthProvider} */
export default function Auth0(options) {
return {
id: "auth0",
name: "Auth0",
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
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`,
authorization: { params: { scope: "openid email profile" } },
checks: ["pkce", "state"],
idToken: true,
profile(profile) {
return {
id: profile.sub,
@@ -17,6 +16,6 @@ export default function Auth0(options) {
image: profile.picture,
}
},
...options,
options,
}
}

View File

@@ -1,24 +1,43 @@
export default function AzureADB2C(options) {
const tenant = options.tenantId ? options.tenantId : "common"
const { tenantName, primaryUserFlow } = options
return {
id: "azure-ad-b2c",
name: "Azure Active Directory B2C",
type: "oauth",
version: "2.0",
params: {
grant_type: "authorization_code",
authorization: {
url: `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${primaryUserFlow}/oauth2/v2.0/authorize`,
params: {
response_type: "code id_token",
response_mode: "query",
},
},
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/",
token: {
url: `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${primaryUserFlow}/oauth2/v2.0/token`,
idToken: true,
},
jwks_uri: `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${primaryUserFlow}}/discovery/v2.0/keys`,
profile(profile) {
let name = ""
if (profile.name) {
// B2C "Display Name"
name = profile.name
} else if (profile.given_name && profile.family_name) {
// B2C "Given Name" & "Surname"
name = `${profile.given_name} ${profile.family_name}`
} else if (profile.given_name) {
// B2C "Given Name"
name = `${profile.given_name}`
}
return {
id: profile.id,
name: profile.displayName,
email: profile.userPrincipalName,
id: profile.oid,
name,
email: profile.emails[0],
image: null,
}
},
...options,
options,
}
}

21
src/providers/azure-ad.js Normal file
View File

@@ -0,0 +1,21 @@
export default function AzureAD(options) {
const tenant = options.tenantId ?? "common"
return {
id: "azure-ad",
name: "Azure Active Directory",
type: "oauth",
authorization: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?response_mode=query`,
token: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
userinfo: "https://graph.microsoft.com/v1.0/me/",
profile(profile) {
return {
id: profile.id,
name: profile.displayName,
email: profile.userPrincipalName,
image: null,
}
},
options,
}
}

View File

@@ -1,22 +0,0 @@
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) {
return {
id: profile.identity.id,
name: `${profile.identity.first_name} ${profile.identity.last_name}`,
email: profile.identity.email_address,
image: null,
}
},
...options,
}
}

View File

@@ -1,21 +1,17 @@
export default function BattleNet(options) {
const { region } = options
const base =
region === "CN"
? "https://www.battlenet.com.cn/oauth"
: `https://${region}.battle.net/oauth`
return {
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"
: `https://${region}.battle.net/oauth/token`,
authorizationUrl:
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",
authorization: `${base}/authorize`,
token: `${base}/token`,
userinfo: "https://us.battle.net/oauth/userinfo",
profile(profile) {
return {
id: profile.id,
@@ -24,6 +20,6 @@ export default function BattleNet(options) {
image: null,
}
},
...options,
options,
}
}

View File

@@ -3,13 +3,9 @@ export default function Box(options) {
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",
authorization: "https://account.box.com/api/oauth2/authorize",
token: "https://api.box.com/oauth2/token",
userinfo: "https://api.box.com/2.0/users/me",
profile(profile) {
return {
id: profile.id,
@@ -18,6 +14,6 @@ export default function Box(options) {
image: profile.avatar_url,
}
},
...options,
options,
}
}

View File

@@ -3,14 +3,9 @@ export default function Bungie(options) {
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:
authorization: "https://www.bungie.net/en/OAuth/Authorize?reauth=true",
token: "https://www.bungie.net/platform/app/oauth/token/",
userinfo:
"https://www.bungie.net/platform/User/GetBungieAccount/{membershipId}/254/",
profile(profile) {
const { bungieNetUser: user } = profile.Response
@@ -18,17 +13,12 @@ export default function Bungie(options) {
return {
id: user.membershipId,
name: user.displayName,
email: null,
image: `https://www.bungie.net${
user.profilePicturePath.startsWith("/") ? "" : "/"
}${user.profilePicturePath}`,
email: null,
}
},
headers: {
"X-API-Key": null,
},
clientId: null,
clientSecret: null,
...options,
options,
}
}

View File

@@ -1,15 +1,11 @@
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" },
accessTokenUrl: `https://${domain}/oauth2/token`,
authorizationUrl: `https://${domain}/oauth2/authorize?response_type=code`,
profileUrl: `https://${domain}/oauth2/userInfo`,
authorization: `${options.issuer}oauth2/authorize?scope=openid+profile+email`,
token: `${options.issuer}oauth2/token`,
userinfo: `${options.issuer}oauth2/userInfo`,
profile(profile) {
return {
id: profile.sub,
@@ -18,6 +14,6 @@ export default function Cognito(options) {
image: null,
}
},
...options,
options,
}
}

View File

@@ -3,22 +3,18 @@ export default function Coinbase(options) {
id: "coinbase",
name: "Coinbase",
type: "oauth",
version: "2.0",
scope: "wallet:user:email wallet:user:read",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://api.coinbase.com/oauth/token",
requestTokenUrl: "https://api.coinbase.com/oauth/token",
authorizationUrl:
"https://www.coinbase.com/oauth/authorize?response_type=code",
profileUrl: "https://api.coinbase.com/v2/user",
authorization:
"https://www.coinbase.com/oauth/authorize?scope=wallet:user:email+wallet:user:read",
token: "https://api.coinbase.com/oauth/token",
userinfo: "https://api.coinbase.com/v2/user",
profile(profile) {
return {
id: profile.data.id,
name: profile.data.name,
email: profile.data.email,
email: profile.data.email,
image: profile.data.avatar_url,
}
},
...options,
options,
}
}

View File

@@ -5,6 +5,6 @@ export default function Credentials(options) {
type: "credentials",
authorize: null,
credentials: null,
...options,
options,
}
}

View File

@@ -3,13 +3,10 @@ export default function Discord(options) {
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",
authorization:
"https://discord.com/api/oauth2/authorize?scope=identify+email",
token: "https://discord.com/api/oauth2/token",
userinfo: "https://discord.com/api/users/@me",
profile(profile) {
if (profile.avatar === null) {
const defaultAvatarNumber = parseInt(profile.discriminator) % 5
@@ -21,10 +18,10 @@ export default function Discord(options) {
return {
id: profile.id,
name: profile.username,
image: profile.image_url,
email: profile.email,
image: profile.image_url,
}
},
...options,
options,
}
}

View File

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

View File

@@ -1,5 +1,5 @@
import nodemailer from "nodemailer"
import logger from "../lib/logger"
import nodemailer from "nodemailer"
export default function Email(options) {
return {
@@ -18,38 +18,31 @@ export default function Email(options) {
from: "NextAuth <no-reply@example.com>",
maxAge: 24 * 60 * 60,
sendVerificationRequest,
...options,
options,
}
}
const sendVerificationRequest = ({
async function sendVerificationRequest({
identifier: email,
url,
baseUrl,
provider,
}) => {
return new Promise((resolve, reject) => {
const { server, from } = provider
// Strip protocol from URL and use domain as site name
const site = baseUrl.replace(/^https?:\/\//, "")
nodemailer.createTransport(server).sendMail(
{
to: email,
from,
subject: `Sign in to ${site}`,
text: text({ url, site, email }),
html: html({ url, site, email }),
},
(error) => {
if (error) {
logger.error("SEND_VERIFICATION_EMAIL_ERROR", email, error)
return reject(new Error("SEND_VERIFICATION_EMAIL_ERROR", error))
}
return resolve()
}
)
})
}) {
const { server, from } = provider
// Strip protocol from URL and use domain as site name
const site = baseUrl.replace(/^https?:\/\//, "")
try {
await nodemailer.createTransport(server).sendMail({
to: email,
from,
subject: `Sign in to ${site}`,
text: text({ url, site, email }),
html: html({ url, site, email }),
})
} catch (error) {
logger.error("SEND_VERIFICATION_EMAIL_ERROR", email, error)
throw new Error("SEND_VERIFICATION_EMAIL_ERROR")
}
}
// Email HTML body

View File

@@ -3,20 +3,17 @@ export default function EVEOnline(options) {
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",
authorization: "https://login.eveonline.com/oauth/authorize",
token: "https://login.eveonline.com/oauth/token",
userinfo: "https://login.eveonline.com/oauth/verify",
profile(profile) {
return {
id: profile.CharacterID,
name: profile.CharacterName,
image: `https://image.eveonline.com/Character/${profile.CharacterID}_128.jpg`,
email: null,
image: `https://image.eveonline.com/Character/${profile.CharacterID}_128.jpg`,
}
},
...options,
options,
}
}

View File

@@ -1,14 +1,21 @@
/** @type {import("types/providers").OAuthProvider} */
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",
authorization: "https://www.facebook.com/v11.0/dialog/oauth?scope=email",
token: "https://graph.facebook.com/oauth/access_token",
userinfo: {
url: "https://graph.facebook.com/me",
// https://developers.facebook.com/docs/graph-api/reference/user/#fields
params: { fields: "id,name,email,picture" },
request({ tokens, client, provider }) {
return client.userinfo(tokens.access_token, {
params: provider.userinfo.params,
})
},
},
profile(profile) {
return {
id: profile.id,
@@ -17,6 +24,6 @@ export default function Facebook(options) {
image: profile.picture.data.url,
}
},
...options,
options,
}
}

View File

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

View File

@@ -1,23 +1,30 @@
/** @type {import("types/providers").OAuthProvider} */
export default function Foursquare(options) {
const { apiVersion } = options
const { apiVersion = "20210801" } = options
return {
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",
profileUrl: `https://api.foursquare.com/v2/users/self?v=${apiVersion}`,
profile(profile) {
authorization: "https://foursquare.com/oauth2/authenticate",
token: "https://foursquare.com/oauth2/access_token",
userinfo: {
url: `https://api.foursquare.com/v2/users/self?v=${apiVersion}`,
request({ tokens, client }) {
return client.userinfo(undefined, {
params: { oauth_token: tokens.access_token },
})
},
},
profile({ response: { profile } }) {
return {
id: profile.id,
name: `${profile.firstName} ${profile.lastName}`,
image: `${profile.prefix}original${profile.suffix}`,
email: profile.contact.email,
image: profile.photo
? `${profile.photo.prefix}original${profile.photo.suffix}`
: null,
}
},
...options,
options,
}
}

View File

@@ -1,19 +1,11 @@
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" },
accessTokenUrl: `https://${options.domain}/oauth2/token`,
authorizationUrl,
profileUrl: `https://${options.domain}/oauth2/userinfo`,
authorization: `${options.issuer}oauth2/authorize`,
token: `${options.issuer}oauth2/token`,
userinfo: `${options.issuer}oauth2/userinfo`,
profile(profile) {
return {
id: profile.sub,
@@ -22,6 +14,6 @@ export default function FusionAuth(options) {
image: profile.picture,
}
},
...options,
options,
}
}

View File

@@ -3,19 +3,17 @@ export default function GitHub(options) {
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",
authorization: "https://github.com/login/oauth/authorize?scope=user",
token: "https://github.com/login/oauth/access_token",
userinfo: "https://api.github.com/user",
profile(profile) {
return {
id: profile.id,
id: profile.id.toString(),
name: profile.name || profile.login,
email: profile.email,
image: profile.avatar_url,
}
},
...options,
options,
}
}

View File

@@ -1,14 +1,13 @@
/** @type {import("types/providers").OAuthProvider} */
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",
authorization: "https://gitlab.com/oauth/authorize?scope=read_user",
token: "https://gitlab.com/oauth/token",
userinfo: "https://gitlab.com/api/v4/user",
checks: ["pkce", "state"],
profile(profile) {
return {
id: profile.id,
@@ -17,6 +16,6 @@ export default function GitLab(options) {
image: profile.avatar_url,
}
},
...options,
options,
}
}

View File

@@ -3,23 +3,18 @@ export default function Google(options) {
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",
wellKnown: "https://accounts.google.com/.well-known/openid-configuration",
authorization: { params: { scope: "openid email profile" } },
idToken: true,
checks: ["pkce", "state"],
profile(profile) {
return {
id: profile.id,
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
}
},
...options,
options,
}
}

View File

@@ -1,17 +1,21 @@
/** @return {import("types/providers").OAuthConfig} */
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" },
accessTokenUrl: `https://${options.domain}/connect/token`,
authorizationUrl: `https://${options.domain}/connect/authorize?response_type=code`,
profileUrl: `https://${options.domain}/connect/userinfo`,
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
authorization: { params: { scope: "openid profile email" } },
checks: ["pkce", "state"],
idToken: true,
profile(profile) {
return { ...profile, id: profile.sub }
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: null,
}
},
...options,
options,
}
}

View File

@@ -15,7 +15,7 @@
* ...
*
* // pages/index
* import { signIn } from "next-auth/client"
* import { signIn } from "next-auth/react"
* ...
* <button onClick={() => signIn("instagram")}>
* Sign in
@@ -29,13 +29,10 @@ export default function Instagram(options) {
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:
authorization:
"https://api.instagram.com/oauth/authorize?scope=user_profile",
token: "https://api.instagram.com/oauth/access_token",
userinfo:
"https://graph.instagram.com/me?fields=id,username,account_type,name",
async profile(profile) {
return {
@@ -45,6 +42,6 @@ export default function Instagram(options) {
image: null,
}
},
...options,
options,
}
}

View File

@@ -3,12 +3,9 @@ export default function Kakao(options) {
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",
authorization: "https://kauth.kakao.com/oauth/authorize",
token: "https://kauth.kakao.com/oauth/token",
userinfo: "https://kapi.kakao.com/v2/user/me",
profile(profile) {
return {
id: profile.id,
@@ -17,6 +14,6 @@ export default function Kakao(options) {
image: profile.kakao_account?.profile.profile_image_url,
}
},
...options,
options,
}
}

View File

@@ -3,13 +3,10 @@ export default function LINE(options) {
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",
authorization:
"https://access.line.me/oauth2/v2.1/authorize?scope=openid+profile",
token: "https://api.line.me/oauth2/v2.1/token",
userinfo: "https://api.line.me/v2/profile",
profile(profile) {
return {
id: profile.userId,
@@ -18,6 +15,6 @@ export default function LINE(options) {
image: profile.pictureUrl,
}
},
...options,
options,
}
}

View File

@@ -3,26 +3,19 @@ export default function LinkedIn(options) {
id: "linkedin",
name: "LinkedIn",
type: "oauth",
version: "2.0",
scope: "r_liteprofile",
params: {
grant_type: "authorization_code",
client_id: options.clientId,
client_secret: options.clientSecret,
},
accessTokenUrl: "https://www.linkedin.com/oauth/v2/accessToken",
authorizationUrl:
"https://www.linkedin.com/oauth/v2/authorization?response_type=code",
profileUrl:
authorization:
"https://www.linkedin.com/oauth/v2/authorization?scope=r_liteprofile",
token: "https://www.linkedin.com/oauth/v2/accessToken",
userinfo:
"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,
}
},
...options,
options,
}
}

View File

@@ -1,22 +1,19 @@
export default function Mailchimp(options) {
return {
id: 'mailchimp',
name: 'Mailchimp',
type: 'oauth',
version: '2.0',
scope: '',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://login.mailchimp.com/oauth2/token',
authorizationUrl: 'https://login.mailchimp.com/oauth2/authorize?response_type=code',
profileUrl: 'https://login.mailchimp.com/oauth2/metadata',
profile: (profile) => {
id: "mailchimp",
name: "Mailchimp",
type: "oauth",
authorization: "https://login.mailchimp.com/oauth2/authorize",
token: "https://login.mailchimp.com/oauth2/token",
userinfo: "https://login.mailchimp.com/oauth2/metadata",
profile(profile) {
return {
id: profile.login.login_id,
name: profile.accountname,
email: profile.login.email,
image: null
image: null,
}
},
...options
options,
}
}

View File

@@ -3,15 +3,9 @@ export default function MailRu(options) {
id: "mailru",
name: "Mail.ru",
type: "oauth",
version: "2.0",
scope: "userinfo",
params: {
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",
authorization: "https://oauth.mail.ru/login?scope=userinfo",
token: "https://oauth.mail.ru/token",
userinfo: "https://oauth.mail.ru/userinfo",
profile(profile) {
return {
id: profile.id,
@@ -20,6 +14,6 @@ export default function MailRu(options) {
image: profile.image,
}
},
...options,
options,
}
}

View File

@@ -3,12 +3,9 @@ export default function Medium(options) {
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",
authorization: "https://medium.com/m/oauth/authorize?scope=basicProfile",
token: "https://api.medium.com/v1/tokens",
userinfo: "https://api.medium.com/v1/me",
profile(profile) {
return {
id: profile.data.id,
@@ -17,6 +14,6 @@ export default function Medium(options) {
image: profile.data.imageUrl,
}
},
...options,
options,
}
}

17
src/providers/naver.js Normal file
View File

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

View File

@@ -3,11 +3,9 @@ export default function Netlify(options) {
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",
authorization: "https://app.netlify.com/authorize",
token: "https://api.netlify.com/oauth/token",
userinfo: "https://api.netlify.com/api/v1/user",
profile(profile) {
return {
id: profile.id,
@@ -16,6 +14,6 @@ export default function Netlify(options) {
image: profile.avatar_url,
}
},
...options,
options,
}
}

View File

@@ -3,20 +3,17 @@ export default function Okta(options) {
id: "okta",
name: "Okta",
type: "oauth",
version: "2.0",
scope: "openid profile email",
params: {
grant_type: "authorization_code",
client_id: options.clientId,
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/`,
authorization: `${options.issuer}v1/authorize?scope=openid+profile+email`,
token: `${options.issuer}v1/token`,
userinfo: `${options.issuer}v1/userinfo`,
profile(profile) {
return { ...profile, id: profile.sub }
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: null,
}
},
...options,
options,
}
}

View File

@@ -1,20 +1,19 @@
export default function Osso(options) {
return {
id: "osso",
name: "SAML SSO",
name: "Osso",
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`,
authorization: `${options.issuer}oauth/authorize`,
token: `${options.issuer}oauth/token`,
userinfo: `${options.issuer}oauth/me`,
profile(profile) {
return {
id: profile.id,
name: profile.name || profile.email,
name: profile.name,
email: profile.email,
image: null,
}
},
...options,
options,
}
}

View File

@@ -3,21 +3,17 @@ export default function Reddit(options) {
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",
authorization: "https://www.reddit.com/api/v1/authorize?scope=identity",
token: " https://www.reddit.com/api/v1/access_token",
userinfo: "https://oauth.reddit.com/api/v1/me",
profile(profile) {
return {
id: profile.id,
name: profile.name,
image: null,
email: null,
image: null,
}
},
...options,
options,
}
}

View File

@@ -3,20 +3,19 @@ export default function Salesforce(options) {
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",
authorization:
"https://login.salesforce.com/services/oauth2/authorize?display=page",
token: "https://login.salesforce.com/services/oauth2/token",
userinfo: "https://login.salesforce.com/services/oauth2/userinfo",
profile(profile) {
return {
...profile,
id: profile.user_id,
name: null,
email: null,
image: profile.picture,
}
},
...options,
checks: ["none"],
options,
}
}

View File

@@ -3,24 +3,22 @@ export default function Slack(options) {
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",
authorization: {
url: "https://slack.com/oauth/v2/authorize",
params: {
user_scope: "identity.basic,identity.email,identity.avatar",
},
},
profileUrl: "https://slack.com/api/users.identity",
token: "https://slack.com/api/oauth.v2.access",
userinfo: "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,
id: profile.user.id,
name: profile.user.name,
email: profile.user.email,
image: profile.user.image_512,
}
},
...options,
options,
}
}

View File

@@ -3,13 +3,10 @@ export default function Spotify(options) {
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",
authorization:
"https://accounts.spotify.com/authorize?scope=user-read-email",
token: "https://accounts.spotify.com/api/token",
userinfo: "https://api.spotify.com/v1/me",
profile(profile) {
return {
id: profile.id,
@@ -18,6 +15,6 @@ export default function Spotify(options) {
image: profile.images?.[0]?.url,
}
},
...options,
options,
}
}

View File

@@ -3,20 +3,17 @@ export default function Strava(options) {
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",
authorization: "https://www.strava.com/api/v3/oauth/authorize?scope=read",
token: "https://www.strava.com/api/v3/oauth/token",
userinfo: "https://www.strava.com/api/v3/athlete",
profile(profile) {
return {
id: profile.id,
name: profile.firstname,
email: null,
image: profile.profile,
}
},
...options,
options,
}
}

View File

@@ -1,24 +1,27 @@
/** @return {import("types/providers").OAuthConfig} */
export default function Twitch(options) {
return {
wellKnown: "https://id.twitch.tv/oauth2/.well-known/openid-configuration",
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",
authorization: {
params: {
scope: "openid user:read:email",
claims: JSON.stringify({
id_token: { email: null, picture: null, preferred_username: null },
}),
},
},
idToken: true,
profile(profile) {
const data = profile.data[0]
return {
id: data.id,
name: data.display_name,
image: data.profile_image_url,
email: data.email,
id: profile.sub,
name: profile.preferred_username,
email: profile.email,
image: profile.picture,
}
},
...options,
options,
}
}

View File

@@ -4,10 +4,9 @@ export default function Twitter(options) {
name: "Twitter",
type: "oauth",
version: "1.0A",
scope: "",
authorization: "https://api.twitter.com/oauth/authenticate",
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) {
@@ -15,9 +14,12 @@ export default function Twitter(options) {
id: profile.id_str,
name: profile.name,
email: profile.email,
image: profile.profile_image_url_https.replace(/_normal\.(jpg|png|gif)$/, ".$1"),
image: profile.profile_image_url_https.replace(
/_normal\.(jpg|png|gif)$/,
".$1"
),
}
},
...options,
options,
}
}

View File

@@ -5,18 +5,11 @@ export default function VK(options) {
id: "vk",
name: "VK",
type: "oauth",
version: "2.0",
scope: "email",
params: {
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}`,
profileUrl: `https://api.vk.com/method/users.get?fields=photo_100&v=${apiVersion}`,
profile: (result) => {
authorization: `https://oauth.vk.com/authorize?scope=email&v=${apiVersion}`,
token: `https://oauth.vk.com/access_token?v=${apiVersion}`,
userinfo: `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(" "),
@@ -24,6 +17,6 @@ export default function VK(options) {
image: profile.photo_100,
}
},
...options,
options,
}
}

View File

@@ -3,13 +3,10 @@ export default function WordPress(options) {
id: "wordpress",
name: "WordPress.com",
type: "oauth",
version: "2.0",
scope: "auth",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://public-api.wordpress.com/oauth2/token",
authorizationUrl:
"https://public-api.wordpress.com/oauth2/authorize?response_type=code",
profileUrl: "https://public-api.wordpress.com/rest/v1/me",
authorization:
"https://public-api.wordpress.com/oauth2/authorize?scope=auth",
token: "https://public-api.wordpress.com/oauth2/token",
userinfo: "https://public-api.wordpress.com/rest/v1/me",
profile(profile) {
return {
id: profile.ID,
@@ -18,6 +15,6 @@ export default function WordPress(options) {
image: profile.avatar_URL,
}
},
...options,
options,
}
}

View File

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

View File

@@ -3,13 +3,10 @@ export default function Yandex(options) {
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",
authorization:
"https://oauth.yandex.ru/authorize?scope=login:email+login:info",
token: "https://oauth.yandex.ru/token",
userinfo: "https://login.yandex.ru/info?format=json",
profile(profile) {
return {
id: profile.id,
@@ -18,6 +15,6 @@ export default function Yandex(options) {
image: null,
}
},
...options,
options,
}
}

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