Compare commits

...

25 Commits

Author SHA1 Message Date
Balázs Orbán
24945895e9 chore(release): bump package version(s) [skip ci] 2022-09-28 18:10:38 +02:00
Balázs Orbán
6deccf610f fix(core): return JSON for non-HTML server route errors (#5442)
* fix(core): return JSON for non-HTML server route errors

* refactor: throw in `unstable_getServerSession`

* test: expect `unstable_getServerSession` to throw

* refactor: destructure

* fix unrelated test formatting

* catch error page
2022-09-28 17:01:39 +01:00
Etienne Martin
f770b90219 fix(react): safe use of localStorage API (#5444)
fix: safe use of localstorage

Co-authored-by: Etienne <>
2022-09-28 16:54:07 +01:00
Balázs Orbán
87f4786917 chore: bump release package 2022-09-28 13:51:41 +02:00
Balázs Orbán
191ef06471 chore(release): bump package version(s) [skip ci] 2022-09-28 13:00:32 +02:00
Philip
75e6d8f0aa docs(adapters): Update prisma.md (#5366)
* Update prisma.md

The referenced official doc page describes how to fix the `warn(prisma-client) There are already 10 instances of Prisma Client actively running.` error in development mode.

* Update prisma.md

Implemented best practice for Prisma Client creation.

* Fixed typo in Prisma db filename.
2022-09-28 11:15:55 +01:00
Yixuan Xu
17999edd30 chore(example): fix hydrate problem in react18 (#5439) 2022-09-28 10:50:40 +02:00
Tom Freudenberg
54b1845e58 fix(core): don't lock next in peerDependencies #5427 (#5430)
* Update peerDependencies #5427

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-09-27 00:04:50 +01:00
Tomas Pozo
879faf9fab docs(middleware): add tip on additional matcher patterns (#5404)
* docs(middleware): add tip on additional matcher patterns

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-09-26 13:39:32 +02:00
Balázs Orbán
3e3c36891e docs(example): use generic type in AppProps
closes #5401
2022-09-25 10:57:44 +01:00
Balázs Orbán
ac5d8a9795 chore(release): bump package version(s) [skip ci] 2022-09-25 11:42:17 +02:00
Matt Oliver
965c6267e2 feat(core): make session token with DB session strategy customizable (#5328)
* Add option for custom generateSessionToken

* Apply suggestions from code review

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-09-25 10:26:59 +01:00
Sébastien Vanvelthem
bfc429d20b fix: update jose to fix nextjs edge error with middleware (#5372)
fix: update jose to fix nextjs edge error
2022-09-25 15:46:02 +07:00
Balázs Orbán
2d8e910a19 chore(release): bump package version(s) [skip ci] 2022-09-25 10:29:56 +02:00
voinik
d16e04848e fix(adapters): check token during email verification in Upstash Adapter (#5377)
* Check token during email verification

* Undo accidental linter fix

* Update index.ts

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-09-25 09:10:55 +01:00
Balázs Orbán
ff3a52895b chore(release): bump package version(s) [skip ci] 2022-09-25 09:42:51 +02:00
Balázs Orbán
e6e03e8842 feat(adapters): update Mikro ORM adapter schema
BREAKING CHANGE:

See https://github.com/nextauthjs/next-auth/pull/5316
2022-09-25 09:29:17 +02:00
Thomas Large
715aad9474 chore: Add Next to peerDeps & bump to 12.2.5 in devDeps (#5384) 2022-09-24 13:39:07 +07:00
Jonas Strassel
902bf92a85 fix(mikro-orm): re-enable tests (#5316) 2022-09-21 23:36:23 +07:00
Eng Zer Jun
44f2a47e6e fix(middleware): use includes() for NextAuth pages (#5104)
* fix(middleware): use `includes()` for NextAuth pages

Some users could be setting their `signIn` and `error` pages option to
`/` to disable the automatically generated pages, as suggested in [1].

This commit reverts the behaviour for matching `signIn` and `error`
pages in `handleMiddleware` to pre-v4.10.3.

```
const signInPage = "/"
const errorPage = "/"
const publicPaths = [signInPage, errorPage, "/_next", "/favicon.ico"]

// pathname = "/" will return true
publicPaths.some((p) => pathname.startsWith(p))
```

Fixes: aedabc8d ("fix: avoid redirect on always public paths")
Reference [1]: https://github.com/nextauthjs/next-auth/discussions/2330#discussioncomment-1678298
Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>

* test(middleware): add tests for public paths

Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>

Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
Co-authored-by: Thang Vu <thvu@hey.com>
2022-09-18 11:07:46 +07:00
dependabot[bot]
a3b92dbaec chore(deps): bump jose from 4.5.0 to 4.9.3 in /apps/playground-sveltekit (#5359)
Bumps [jose](https://github.com/panva/jose) from 4.5.0 to 4.9.3.
- [Release notes](https://github.com/panva/jose/releases)
- [Changelog](https://github.com/panva/jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/jose/compare/v4.5.0...v4.9.3)

---
updated-dependencies:
- dependency-name: jose
  dependency-type: indirect
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-17 01:55:54 +02:00
Steve Burtenshaw
bdd3ab2816 docs(middleware): remove reference to nested (#5355)
Fixes #5180
2022-09-16 10:30:58 +02:00
Dulmandakh
ba55f06585 chore(deps): bump cookie to 0.5.0 (#5339) 2022-09-14 18:31:00 +02:00
Steve Burtenshaw
d2b877fb28 docs(client): onUnauthenticated reference (#5340) 2022-09-14 17:26:02 +02:00
Yuriy Gromchenko
658b22d9fb docs(atlassian): update provider scope (#5337) 2022-09-14 17:23:01 +02:00
32 changed files with 1984 additions and 299 deletions

View File

@@ -2,12 +2,16 @@ import { SessionProvider } from "next-auth/react"
import "./styles.css"
import type { AppProps } from "next/app"
import type { Session } from "next-auth"
// Use of the <SessionProvider> is mandatory to allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App({ Component, pageProps }: AppProps) {
export default function App({
Component,
pageProps: { session, ...pageProps },
}: AppProps<{ session: Session }>) {
return (
<SessionProvider session={pageProps.session} refetchInterval={0}>
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)

View File

@@ -4,8 +4,7 @@ import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"
export default function ProtectedPage() {
const { data: session, status } = useSession()
const loading = status === "loading"
const { data: session } = useSession()
const [content, setContent] = useState()
// Fetch content from protected route
@@ -19,9 +18,7 @@ export default function ProtectedPage() {
}
fetchData()
}, [session])
// 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) {

View File

@@ -1161,9 +1161,9 @@ isexe@^2.0.0:
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
jose@^4.1.4, jose@^4.3.7:
version "4.5.0"
resolved "https://registry.yarnpkg.com/jose/-/jose-4.5.0.tgz#92829d8cf846351eb55aaaf94f252fb1d191f2d5"
integrity sha512-GFcVFQwYQKbQTUOo2JlpFGXTkgBw26uzDsRMD2q1WgSKNSnpKS9Ug7bdQ8dS+p4sZHNH6iRPu6WK2jLIjspaMA==
version "4.9.3"
resolved "https://registry.yarnpkg.com/jose/-/jose-4.9.3.tgz#890abd3f26725fe0f2aa720bc2f7835702b624db"
integrity sha512-f8E/z+T3Q0kA9txzH2DKvH/ds2uggcw0m3vVPSB9HrSkrQ7mojjifvS7aR8cw+lQl2Fcmx9npwaHpM/M3GD8UQ==
js-yaml@^4.1.0:
version "4.1.0"

View File

@@ -12,15 +12,24 @@ npm install next-auth @prisma/client @next-auth/prisma-adapter
npm install prisma --save-dev
```
Create a file with your Prisma Client:
```javascript title="lib/prismadb.js"
import { PrismaClient } from "@prisma/client"
const client = globalThis.prisma || new PrismaClient()
if (process.env.NODE_ENV !== "production") globalThis.prisma = client
export default client
```
Configure your NextAuth.js to use the Prisma Adapter:
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
import prisma from "../../../lib/prismadb"
export default NextAuth({
adapter: PrismaAdapter(prisma),

View File

@@ -114,6 +114,12 @@ session: {
// Use it to limit write operations. Set to 0 to always update the database.
// Note: This option is ignored if using JSON Web Tokens
updateAge: 24 * 60 * 60, // 24 hours
// The session token is usually either a random UUID or string, however if you
// need a more customized session token string, you can define your own generate function.
generateSessionToken: () => {
return randomUUID?.() ?? randomBytes(32).toString("hex")
}
}
```

View File

@@ -67,7 +67,7 @@ export default function Component() {
Due to the way how Next.js handles `getServerSideProps` and `getInitialProps`, every protected page load has to make a server-side request to check if the session is valid and then generate the requested page (SSR). This increases server load, and if you are good with making the requests from the client, there is an alternative. You can use `useSession` in a way that makes sure you always have a valid session. If after the initial loading state there was no session found, you can define the appropriate action to respond.
The default behavior is to redirect the user to the sign-in page, from where - after a successful login - they will be sent back to the page they started on. You can also define an `onFail()` callback, if you would like to do something else:
The default behavior is to redirect the user to the sign-in page, from where - after a successful login - they will be sent back to the page they started on. You can also define an `onUnauthenticated()` callback, if you would like to do something else:
#### Example

View File

@@ -24,7 +24,11 @@ providers: [
AtlassianProvider({
clientId: process.env.ATLASSIAN_CLIENT_ID,
clientSecret: process.env.ATLASSIAN_CLIENT_SECRET,
scope: "write:jira-work read:jira-work read:jira-user offline_access read:me"
authorization: {
params: {
scope: "write:jira-work read:jira-work read:jira-user offline_access read:me"
}
}
})
]
...

View File

@@ -42,18 +42,30 @@ export default function Page() {
### Next.js (Middleware)
With NextAuth.js 4.2.0 and Next.js 12, you can now protect your pages via the middleware pattern more easily. If you would like to protect all pages, you can create a `_middleware.js` file in your root `pages` directory which looks like this.
With NextAuth.js 4.2.0 and Next.js 12, you can now protect your pages via the middleware pattern more easily. If you would like to protect all pages, you can create a `middleware.js` file in your root `pages` directory which looks like this:
```js title="/middleware.js"
export { default } from "next-auth/middleware"
```
Otherwise, if you only want to protect a subset of pages, you could put it in a subdirectory as well, for example in `/pages/admin/_middleware.js` would protect all pages under `/admin`.
If you only want to secure certain pages, export a `config` object with a `matcher`:
```js
export { default } from "next-auth/middleware"
export const config = { matcher: ["/dashboard"] }
```
For the time being, the `withAuth` middleware only supports `"jwt"` as [session strategy](https://next-auth.js.org/configuration/options#session).
More details can be found [here](https://next-auth.js.org/configuration/nextjs#middleware).
:::tip
To inclue all `dashboard` nested routes (sub pages like `/dashboard/settings`, `/dashboard/profile`) you can pass `matcher: "/dashboard/:path*"` to `config`.
For other patterns check out the [Next.js Middleware documentation](https://nextjs.org/docs/advanced-features/middleware#matcher).
:::
### Server Side
You can protect server side rendered pages using the `unstable_getServerSession` method. This is different from the old `getSession()` method, in that it does not do an extra fetch out over the internet to confirm data from itself, increasing performance significantly.

View File

@@ -7,7 +7,7 @@
"build:app": "turbo run build --filter=next-auth-app --include-dependencies",
"build": "turbo run build --filter=next-auth --filter=@next-auth/* --no-deps",
"lint": "turbo run lint --filter=!next-auth-docs --parallel",
"test": "turbo run test --concurrency=1 --filter=!@next-auth/pouchdb-adapter --filter=!@next-auth/mikro-orm-adapter --filter=!@next-auth/upstash-redis-adapter --filter=!next-auth-* --filter=[HEAD^1]",
"test": "turbo run test --concurrency=1 --filter=!@next-auth/pouchdb-adapter --filter=!@next-auth/upstash-redis-adapter --filter=!next-auth-* --filter=[HEAD^1]",
"clean": "turbo run clean --no-cache",
"dev:app": "turbo run dev --parallel --continue --filter=next-auth-app...",
"dev:docs": "turbo run dev --filter=next-auth-docs",
@@ -18,7 +18,7 @@
},
"devDependencies": {
"@actions/core": "^1.6.0",
"@balazsorban/monorepo-release": "0.0.4",
"@balazsorban/monorepo-release": "0.0.5",
"@types/jest": "^28.1.3",
"@types/node": "^17.0.25",
"@typescript-eslint/eslint-plugin": "^5.10.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/mikro-orm-adapter",
"version": "2.0.1",
"version": "3.0.0",
"description": "MikroORM adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -32,22 +32,22 @@
"dist"
],
"peerDependencies": {
"@mikro-orm/core": "^5.0.2",
"@mikro-orm/core": "^5",
"next-auth": "^4"
},
"devDependencies": {
"@mikro-orm/core": "^5.0.2",
"@mikro-orm/sqlite": "^5.0.2",
"@mikro-orm/core": "^5",
"@mikro-orm/sqlite": "^5",
"@next-auth/adapter-test": "workspace:*",
"@next-auth/tsconfig": "workspace:*",
"@types/uuid": "^8.3.3",
"jest": "^27.4.3",
"@types/uuid": ">=8",
"jest": "^29",
"next-auth": "workspace:*"
},
"dependencies": {
"uuid": "^9"
},
"jest": {
"preset": "@next-auth/adapter-test/jest"
},
"dependencies": {
"uuid": "^8.3.2"
}
}
}

View File

@@ -9,6 +9,7 @@ import {
OneToMany,
Collection,
ManyToOne,
types,
} from "@mikro-orm/core"
import type { DefaultAccount } from "next-auth"
@@ -29,55 +30,56 @@ export class User implements RemoveIndex<AdapterUser> {
@PrimaryKey()
id: string = randomUUID()
@Property({ nullable: true })
@Property({ type: types.string, nullable: true })
name?: string
@Property({ nullable: true })
@Property({ type: types.string, nullable: true })
@Unique()
email?: string
@Property({ type: "Date", nullable: true })
@Property({ type: types.datetime, nullable: true })
emailVerified: Date | null = null
@Property({ nullable: true })
@Property({ type: types.string, nullable: true })
image?: string
@OneToMany({
entity: () => Session,
mappedBy: (session) => session.user,
entity: 'Session',
mappedBy: (session: Session) => session.user,
hidden: true,
orphanRemoval: true,
})
sessions = new Collection<Session>(this)
sessions = new Collection<Session, object>(this)
@OneToMany({
entity: () => Account,
mappedBy: (account) => account.user,
entity: 'Account',
mappedBy: (account: Account) => account.user,
hidden: true,
orphanRemoval: true,
})
accounts = new Collection<Account>(this)
accounts = new Collection<Account, object>(this)
}
@Entity()
export class Session implements AdapterSession {
@PrimaryKey()
@Property({ type: types.string })
id: string = randomUUID()
@ManyToOne({
entity: () => User,
entity: 'User',
hidden: true,
onDelete: "cascade",
})
user!: User
@Property({ persist: false })
@Property({ type: types.string, persist: false })
userId!: string
@Property()
@Property({ type: 'Date' })
expires!: Date
@Property()
@Property({ type: types.string })
@Unique()
sessionToken!: string
}
@@ -86,46 +88,47 @@ export class Session implements AdapterSession {
@Unique({ properties: ["provider", "providerAccountId"] })
export class Account implements RemoveIndex<DefaultAccount> {
@PrimaryKey()
@Property({ type: types.string })
id: string = randomUUID()
@ManyToOne({
entity: () => User,
entity: 'User',
hidden: true,
onDelete: "cascade",
})
user!: User
@Property({ persist: false })
@Property({ type: types.string, persist: false })
userId!: string
@Enum()
@Property({ type: types.string })
type!: ProviderType
@Property()
@Property({ type: types.string })
provider!: string
@Property()
@Property({ type: types.string })
providerAccountId!: string
@Property({ nullable: true })
@Property({ type: types.string, nullable: true })
refresh_token?: string
@Property({ nullable: true })
@Property({ type: types.string, nullable: true })
access_token?: string
@Property({ nullable: true })
@Property({ type: types.integer, nullable: true })
expires_at?: number
@Property({ nullable: true })
@Property({ type: types.string, nullable: true })
token_type?: string
@Property({ nullable: true })
@Property({ type: types.string, nullable: true })
scope?: string
@Property({ nullable: true })
@Property({ type: types.text, nullable: true })
id_token?: string
@Property({ nullable: true })
@Property({ type: types.string, nullable: true })
session_state?: string
}
@@ -133,12 +136,12 @@ export class Account implements RemoveIndex<DefaultAccount> {
@Unique({ properties: ["token", "identifier"] })
export class VerificationToken implements AdapterVerificationToken {
@PrimaryKey()
@Property()
@Property({ type: types.string })
token!: string
@Property()
@Property({ type: 'Date' })
expires!: Date
@Property()
@Property({ type: types.string })
identifier!: string
}

View File

@@ -10,7 +10,7 @@ import { MikroORM, wrap } from "@mikro-orm/core"
import * as defaultEntities from "./entities"
export * as defaultEntities from "./entities"
export { defaultEntities }
/**
* The MikroORM adapter accepts a MikroORM configuration and returns a NextAuth adapter.

View File

@@ -0,0 +1,591 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`run migrations: createSchemaSQL 1`] = `
"pragma foreign_keys = off;
create table \`user\` (\`id\` text not null, \`name\` text null, \`email\` text null, \`email_verified\` datetime null, \`image\` text null, primary key (\`id\`));
create unique index \`user_email_unique\` on \`user\` (\`email\`);
create table \`session\` (\`id\` text not null, \`user_id\` text not null, \`expires\` datetime not null, \`session_token\` text not null, constraint \`session_user_id_foreign\` foreign key(\`user_id\`) references \`user\`(\`id\`) on delete cascade on update cascade, primary key (\`id\`));
create index \`session_user_id_index\` on \`session\` (\`user_id\`);
create unique index \`session_session_token_unique\` on \`session\` (\`session_token\`);
create table \`account\` (\`id\` text not null, \`user_id\` text not null, \`type\` text not null, \`provider\` text not null, \`provider_account_id\` text not null, \`refresh_token\` text null, \`access_token\` text null, \`expires_at\` integer null, \`token_type\` text null, \`scope\` text null, \`id_token\` text null, \`session_state\` text null, constraint \`account_user_id_foreign\` foreign key(\`user_id\`) references \`user\`(\`id\`) on delete cascade on update cascade, primary key (\`id\`));
create index \`account_user_id_index\` on \`account\` (\`user_id\`);
create unique index \`account_provider_provider_account_id_unique\` on \`account\` (\`provider\`, \`provider_account_id\`);
create table \`verification_token\` (\`token\` text not null, \`expires\` datetime not null, \`identifier\` text not null, primary key (\`token\`));
create unique index \`verification_token_token_identifier_unique\` on \`verification_token\` (\`token\`, \`identifier\`);
pragma foreign_keys = on;
"
`;
exports[`run migrations: targetSchema 1`] = `
{
"name": undefined,
"namespaces": [],
"tables": [
{
"checks": [],
"columns": {
"email": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "email",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"email_verified": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": 0,
"mappedType": "datetime",
"name": "email_verified",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "datetime",
"unsigned": false,
},
"id": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "id",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"image": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "image",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"name": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "name",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
},
"comment": undefined,
"foreignKeys": {},
"indexes": [
{
"columnNames": [
"email",
],
"composite": false,
"keyName": "user_email_unique",
"primary": false,
"unique": true,
},
{
"columnNames": [
"id",
],
"composite": false,
"expression": undefined,
"keyName": "primary",
"primary": true,
"type": undefined,
"unique": true,
},
],
"name": "user",
"schema": undefined,
},
{
"checks": [],
"columns": {
"expires": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": 0,
"mappedType": "datetime",
"name": "expires",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "datetime",
"unsigned": false,
},
"id": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "id",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"session_token": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "session_token",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"user_id": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "user_id",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
},
"comment": undefined,
"foreignKeys": {
"session_user_id_foreign": {
"columnNames": [
"user_id",
],
"constraintName": "session_user_id_foreign",
"deleteRule": "cascade",
"localTableName": "session",
"referencedColumnNames": [
"id",
],
"referencedTableName": "user",
"updateRule": "cascade",
},
},
"indexes": [
{
"columnNames": [
"user_id",
],
"composite": false,
"keyName": "session_user_id_index",
"primary": false,
"unique": false,
},
{
"columnNames": [
"session_token",
],
"composite": false,
"keyName": "session_session_token_unique",
"primary": false,
"unique": true,
},
{
"columnNames": [
"id",
],
"composite": false,
"expression": undefined,
"keyName": "primary",
"primary": true,
"type": undefined,
"unique": true,
},
],
"name": "session",
"schema": undefined,
},
{
"checks": [],
"columns": {
"access_token": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "access_token",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"expires_at": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "integer",
"name": "expires_at",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "integer",
"unsigned": false,
},
"id": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "id",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"id_token": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "id_token",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"provider": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "provider",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"provider_account_id": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "provider_account_id",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"refresh_token": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "refresh_token",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"scope": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "scope",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"session_state": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "session_state",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"token_type": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "token_type",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"type": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "type",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"user_id": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "user_id",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
},
"comment": undefined,
"foreignKeys": {
"account_user_id_foreign": {
"columnNames": [
"user_id",
],
"constraintName": "account_user_id_foreign",
"deleteRule": "cascade",
"localTableName": "account",
"referencedColumnNames": [
"id",
],
"referencedTableName": "user",
"updateRule": "cascade",
},
},
"indexes": [
{
"columnNames": [
"user_id",
],
"composite": false,
"keyName": "account_user_id_index",
"primary": false,
"unique": false,
},
{
"columnNames": [
"provider",
"provider_account_id",
],
"composite": true,
"expression": undefined,
"keyName": "account_provider_provider_account_id_unique",
"primary": false,
"type": undefined,
"unique": true,
},
{
"columnNames": [
"id",
],
"composite": false,
"expression": undefined,
"keyName": "primary",
"primary": true,
"type": undefined,
"unique": true,
},
],
"name": "account",
"schema": undefined,
},
{
"checks": [],
"columns": {
"expires": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": 0,
"mappedType": "datetime",
"name": "expires",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "datetime",
"unsigned": false,
},
"identifier": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "identifier",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"token": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "token",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
},
"comment": undefined,
"foreignKeys": {},
"indexes": [
{
"columnNames": [
"token",
"identifier",
],
"composite": true,
"expression": undefined,
"keyName": "verification_token_token_identifier_unique",
"primary": false,
"type": undefined,
"unique": true,
},
{
"columnNames": [
"token",
],
"composite": false,
"expression": undefined,
"keyName": "primary",
"primary": true,
"type": undefined,
"unique": true,
},
],
"name": "verification_token",
"schema": undefined,
},
],
}
`;

View File

@@ -1,10 +1,69 @@
import type { Options } from "@mikro-orm/core"
import { Options, types } from "@mikro-orm/core"
import type { SqliteDriver } from "@mikro-orm/sqlite"
import { MikroORM, wrap } from "@mikro-orm/core"
import { runBasicTests } from "@next-auth/adapter-test"
import { MikroOrmAdapter, defaultEntities } from "../src"
import { User, VeryImportantEntity } from "./testEntities"
import {
Cascade,
Collection,
Entity,
OneToMany,
PrimaryKey,
Property,
Unique,
} from "@mikro-orm/core"
import { randomUUID } from "@next-auth/adapter-test"
@Entity()
export class User implements defaultEntities.User {
@PrimaryKey()
@Property({ type: types.string })
id: string = randomUUID()
@Property({ type: types.string, nullable: true })
name?: string
@Property({ type: types.string, nullable: true })
@Unique()
email?: string
@Property({ type: 'Date', nullable: true })
emailVerified: Date | null = null
@Property({ type: types.string, nullable: true })
image?: string
@OneToMany({
entity: 'Session',
mappedBy: (session: defaultEntities.Session) => session.user,
hidden: true,
orphanRemoval: true,
cascade: [Cascade.ALL],
})
sessions = new Collection<defaultEntities.Session>(this)
@OneToMany({
entity: 'Account',
mappedBy: (account: defaultEntities.Account) => account.user,
hidden: true,
orphanRemoval: true,
cascade: [Cascade.ALL],
})
accounts = new Collection<defaultEntities.Account>(this)
@Property({ type: types.string, hidden: true })
role = "ADMIN"
}
@Entity()
export class VeryImportantEntity {
@PrimaryKey()
@Property({ type: types.string })
id: string = randomUUID()
@Property({ type: types.boolean })
important = true
}
let _init: MikroORM

View File

@@ -0,0 +1,28 @@
import { MikroORM, Options } from "@mikro-orm/core";
import { SqliteDriver } from "@mikro-orm/sqlite";
import { defaultEntities } from "../src";
const config: Options<SqliteDriver> = {
dbName: "./db.sqlite",
type: "sqlite",
entities: [
defaultEntities.User,
defaultEntities.Account,
defaultEntities.Session,
defaultEntities.VerificationToken,
],
}
it("run migrations", async () => {
const orm = await MikroORM.init(config)
await orm.getSchemaGenerator().dropSchema()
const createSchemaSQL = await orm.getSchemaGenerator().getCreateSchemaSQL()
expect(createSchemaSQL).toMatchSnapshot('createSchemaSQL')
const targetSchema = await orm.getSchemaGenerator().getTargetSchema()
expect(targetSchema).toMatchSnapshot('targetSchema')
await orm.getSchemaGenerator().dropSchema()
await orm.close().catch(() => null)
})

View File

@@ -1,61 +0,0 @@
import {
Cascade,
Collection,
Entity,
OneToMany,
PrimaryKey,
Property,
Unique,
} from "@mikro-orm/core"
import { randomUUID } from "@next-auth/adapter-test"
import type { defaultEntities } from "../src"
import { Account, Session } from "../src/entities"
@Entity()
export class User implements defaultEntities.User {
@PrimaryKey()
id: string = randomUUID()
@Property({ nullable: true })
name?: string
@Property({ nullable: true })
@Unique()
email?: string
@Property({ type: "Date", nullable: true })
emailVerified: Date | null = null
@Property({ nullable: true })
image?: string
@OneToMany({
entity: () => Session,
mappedBy: (session) => session.user,
hidden: true,
orphanRemoval: true,
cascade: [Cascade.ALL],
})
sessions = new Collection<Session>(this)
@OneToMany({
entity: () => Account,
mappedBy: (account) => account.user,
hidden: true,
orphanRemoval: true,
cascade: [Cascade.ALL],
})
accounts = new Collection<Account>(this)
@Property({ hidden: true })
role = "ADMIN"
}
@Entity()
export class VeryImportantEntity {
@PrimaryKey()
id: string = randomUUID()
@Property()
important = true
}

View File

@@ -5,4 +5,4 @@
"./*.js",
"./*.d.ts",
]
}
}

View File

@@ -8,5 +8,6 @@
"outDir": "dist",
"stripInternal": true
},
"exclude": ["tests", "dist", "jest.config.js"]
"include": ["src"],
"exclude": ["dist", "test", "node_modules"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/mongodb-adapter",
"version": "1.0.4",
"version": "1.1.0",
"description": "mongoDB adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/upstash-redis-adapter",
"version": "3.0.1",
"version": "3.0.2",
"description": "Upstash adapter for next-auth. It uses Upstash's connectionless (HTTP based) Redis client.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",

View File

@@ -165,13 +165,20 @@ export function UpstashRedisAdapter(
},
async createVerificationToken(verificationToken) {
await setObjectAsJson(
verificationTokenKeyPrefix + verificationToken.identifier,
verificationTokenKeyPrefix +
verificationToken.identifier +
":" +
verificationToken.token,
verificationToken
)
return verificationToken
},
async useVerificationToken(verificationToken) {
const tokenKey = verificationTokenKeyPrefix + verificationToken.identifier
const tokenKey =
verificationTokenKeyPrefix +
verificationToken.identifier +
":" +
verificationToken.token
const token = await client.get<VerificationToken>(tokenKey)
if (!token) return null

View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "4.10.3",
"version": "4.12.2",
"description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",
@@ -69,8 +69,8 @@
"dependencies": {
"@babel/runtime": "^7.16.3",
"@panva/hkdf": "^1.0.1",
"cookie": "^0.4.1",
"jose": "^4.3.7",
"cookie": "^0.5.0",
"jose": "^4.9.3",
"oauth": "^0.9.15",
"openid-client": "^5.1.0",
"preact": "^10.6.3",
@@ -78,6 +78,7 @@
"uuid": "^8.3.2"
},
"peerDependencies": {
"next": "^12.2.5",
"nodemailer": "^6.6.5",
"react": "^17.0.2 || ^18",
"react-dom": "^17.0.2 || ^18"
@@ -118,7 +119,7 @@
"jest-environment-jsdom": "^28.1.1",
"jest-watch-typeahead": "^1.1.0",
"msw": "^0.42.3",
"next": "12.2.0",
"next": "12.2.5",
"postcss": "^8.4.14",
"postcss-cli": "^9.1.0",
"postcss-nested": "^5.0.6",
@@ -129,4 +130,4 @@
"engines": {
"node": "^12.19.0 || ^14.15.0 || ^16.13.0"
}
}
}

View File

@@ -94,10 +94,18 @@ export function BroadcastChannel(name = "nextauth.message") {
/** Notify other tabs/windows. */
post(message: Record<string, unknown>) {
if (typeof window === "undefined") return
localStorage.setItem(
name,
JSON.stringify({ ...message, timestamp: now() })
)
try {
localStorage.setItem(
name,
JSON.stringify({ ...message, timestamp: now() })
)
} catch {
/**
* The localStorage API isn't always available.
* It won't work in private mode prior to Safari 11 for example.
* Notifications are simply dropped if an error is encountered.
*/
}
},
}
}

View File

@@ -94,13 +94,21 @@ export async function NextAuthHandler<
assertionResult.forEach(logger.warn)
} else if (assertionResult instanceof Error) {
// Bail out early if there's an error in the user config
const { pages, theme } = userOptions
logger.error(assertionResult.code, assertionResult)
const htmlPages = ["signin", "signout", "error", "verify-request"]
if (!htmlPages.includes(req.action) || req.method !== "GET") {
const message = `There is a problem with the server configuration. Check the server logs for more information.`
return {
status: 500,
headers: [{ key: "Content-Type", value: "application/json" }],
body: { message } as any,
}
}
const { pages, theme } = userOptions
const authOnErrorPage =
pages?.error &&
req.action === "signin" &&
req.query?.callbackUrl.startsWith(pages.error)
pages?.error && req.query?.callbackUrl?.startsWith(pages.error)
if (!pages?.error || authOnErrorPage) {
if (authOnErrorPage) {

View File

@@ -1,3 +1,4 @@
import { randomBytes, randomUUID } from "crypto"
import { NextAuthOptions } from ".."
import logger from "../utils/logger"
import parseUrl from "../utils/parse-url"
@@ -86,6 +87,10 @@ export async function init({
strategy: userOptions.adapter ? "database" : "jwt",
maxAge,
updateAge: 24 * 60 * 60,
generateSessionToken: () => {
// Use `randomUUID` if available. (Node 15.6+)
return randomUUID?.() ?? randomBytes(32).toString("hex")
},
...userOptions.session,
},
// JWT options

View File

@@ -1,4 +1,3 @@
import { randomBytes, randomUUID } from "crypto"
import { AccountNotLinkedError } from "../errors"
import { fromDate } from "./utils"
@@ -37,7 +36,7 @@ export default async function callbackHandler(params: {
adapter,
jwt,
events,
session: { strategy: sessionStrategy },
session: { strategy: sessionStrategy, generateSessionToken },
} = options
// If no adapter is configured then we don't have a database and cannot
@@ -219,8 +218,3 @@ export default async function callbackHandler(params: {
}
}
}
function generateSessionToken() {
// Use `randomUUID` if available. (Node 15.6++)
return randomUUID?.() ?? randomBytes(32).toString("hex")
}

View File

@@ -468,6 +468,13 @@ export interface SessionOptions {
* @default 86400 // 1 day
*/
updateAge: number
/**
* Generate a custom session token for database-based sessions.
* By default, a random UUID or string is generated depending on the Node.js version.
* However, you can specify your own custom string (such as CUID) to be used.
* @default `randomUUID` or `randomBytes.toHex` depending on the Node.js version
*/
generateSessionToken: () => string
}
export interface DefaultUser {

View File

@@ -118,12 +118,14 @@ export async function unstable_getServerSession(
},
})
const { body, cookies } = session
const { body, cookies, status = 200 } = session
cookies?.forEach((cookie) => setCookie(res, cookie))
if (body && typeof body !== "string" && Object.keys(body).length)
return body as Session
if (body && typeof body !== "string" && Object.keys(body).length) {
if (status === 200) return body as Session
throw new Error((body as any).message)
}
return null
}

View File

@@ -106,12 +106,13 @@ async function handleMiddleware(
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
const errorPage = options?.pages?.error ?? "/api/auth/error"
const basePath = parseUrl(process.env.NEXTAUTH_URL).path
const publicPaths = [signInPage, errorPage, "/_next", "/favicon.ico"]
const publicPaths = ["/_next", "/favicon.ico"]
// Avoid infinite redirects/invalid response
// on paths that never require authentication
if (
pathname.startsWith(basePath) ||
[signInPage, errorPage].includes(pathname) ||
publicPaths.some((p) => pathname.startsWith(p))
) {
return

View File

@@ -47,17 +47,19 @@ describe("Treat secret correctly", () => {
})
it("Error if missing NEXTAUTH_SECRET and secret", async () => {
const session = await unstable_getServerSession(req, res, {
providers: [],
logger,
})
const configError = new Error(
"There is a problem with the server configuration. Check the server logs for more information."
)
await expect(
unstable_getServerSession(req, res, { providers: [], logger })
).rejects.toThrowError(configError)
expect(session).toEqual(null)
expect(logger.error).toBeCalledTimes(1)
expect(logger.error).toBeCalledWith("NO_SECRET", expect.any(MissingSecret))
})
it("Only logs warning once and in development", async () => {
process.env.NEXTAUTH_SECRET = "secret"
// Expect console.warn to NOT be called due to NODE_ENV=production
await unstable_getServerSession(req, res, { providers: [], logger })
expect(console.warn).toBeCalledTimes(0)
@@ -71,6 +73,7 @@ describe("Treat secret correctly", () => {
// Expect console.warn to be still only be called ONCE
await unstable_getServerSession(req, res, { providers: [], logger })
expect(console.warn).toBeCalledTimes(1)
delete process.env.NEXTAUTH_SECRET
})
})

View File

@@ -0,0 +1,40 @@
import { NextMiddleware } from "next/server"
import { NextAuthMiddlewareOptions, withAuth } from "../src/next/middleware"
it("should not match pages as public paths", async () => {
const options: NextAuthMiddlewareOptions = {
pages: {
signIn: "/",
error: "/",
},
secret: "secret",
}
const nextUrl: any = {
pathname: "/protected/pathA",
search: "",
origin: "http://127.0.0.1",
}
const req: any = { nextUrl, headers: { authorization: "" } }
const handleMiddleware = withAuth(options) as NextMiddleware
const res = await handleMiddleware(req, null as any)
expect(res).toBeDefined()
expect(res?.status).toBe(307)
})
it("should not redirect on public paths", async () => {
const options: NextAuthMiddlewareOptions = {
secret: "secret",
}
const nextUrl: any = {
pathname: "/_next/foo",
search: "",
origin: "http://127.0.0.1",
}
const req: any = { nextUrl, headers: { authorization: "" } }
const handleMiddleware = withAuth(options) as NextMiddleware
const res = await handleMiddleware(req, null as any)
expect(res).toBeUndefined()
})

1238
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff