Compare commits

..

21 Commits

Author SHA1 Message Date
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
Wilkins Fernandez
ad4709764a docs: update import for providers (#1823)
Updates the names export from `providers` to `getProviders`.
2021-04-23 14:58:53 +02:00
Michał Bundyra
55a2932973 fix(ts): add Mailchimp provider (#1821) 2021-04-23 13:11:13 +02:00
Michał Bundyra
49cb7e5bd7 feat(provider): add Mailchimp provider (#1781) 2021-04-23 12:15:25 +02:00
Balázs Orbán
b95182ded7 fix(ts): expose errors type delcarations (#1817) 2021-04-22 23:45:23 +02:00
Balázs Orbán
be28672fd4 fix(errors): expose custom errors (#1816)
* chore(deps): add class-properties babel plugin

* feat(errors): expand list of custom error classes

* build(errors): expose errors as a submodule
2021-04-22 23:28:38 +02:00
Balázs Orbán
e26c5fc905 fix(ts): adjust AppOptions (#1815) 2021-04-22 23:04:27 +02:00
Balázs Orbán
543f812eb3 fix(build): export functions in jwt (#1814) 2021-04-22 19:28:17 +02:00
Joël Galeran
0c9f9777c5 docs(adapter): Remove --preview-feature flag (#1807)
* Remove --preview-feature flag

* Update [...nextauth].js
2021-04-22 18:11:30 +02:00
Balázs Orbán
34f334a71d fix(ts): make Profile/User interfaces overridable (#1801)
* fix(ts): create DefaultUser interface

* fix(ts): fix TypeORMUserModel

* fix(ts): create DefaultProfile
2021-04-22 01:04:23 +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
26 changed files with 1757 additions and 1207 deletions

27
.gitignore vendored
View File

@@ -25,20 +25,21 @@ node_modules
# Generated files
.docusaurus
.cache-loader
.next
www/providers.json
src/providers/index.js
internals
adapters.d.ts
adapters.js
client.d.ts
client.js
index.d.ts
index.js
jwt.d.ts
jwt.js
providers.d.ts
providers.js
/internals
/adapters.d.ts
/adapters.js
/client.d.ts
/client.js
/index.d.ts
/index.js
/jwt.d.ts
/jwt.js
/providers.d.ts
/providers.js
/errors.js
/errors.d.ts
# Development app
app/next-auth
@@ -57,4 +58,4 @@ app/yarn.lock
/_work
# Prisma migrations
/prisma/migrations
/prisma/migrations

View File

@@ -82,6 +82,6 @@ export default NextAuth({
// Prisma Database Adapter
// To configure this app to use the schema in `prisma/schema.prisma` run:
// npx prisma generate
// npx prisma migrate dev --preview-feature
// npx prisma migrate dev
// adapter: Adapters.Prisma.Adapter({ prisma })
})

View File

@@ -1,12 +1,25 @@
{
"presets": [
["@babel/preset-env", { "targets": { "esmodules": true } }]
["@babel/preset-env", { "targets": { "node": "10" } }]
],
"plugins": [
"@babel/plugin-proposal-class-properties"
],
"plugins": [
"@babel/plugin-proposal-class-properties"
],
"comments": false,
"overrides": [
{
"test": ["../src/client/**"],
"comments": true,
"presets": [
["@babel/preset-env", { "targets": { "ie": "11" } }]
]
},
{
"test": ["../src/server/pages/**"],
"presets": ["preact"]
}
]
}
}

View File

@@ -7,6 +7,7 @@ const MODULE_ENTRIES = {
PROVIDERS: "providers",
ADAPTERS: "adapters",
JWT: "jwt",
ERRORS: "errors",
}
// Building submodule entries
@@ -17,6 +18,7 @@ const BUILD_TARGETS = {
[`${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",
}
Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
@@ -34,6 +36,7 @@ const TYPES_TARGETS = [
`${MODULE_ENTRIES.ADAPTERS}.d.ts`,
`${MODULE_ENTRIES.PROVIDERS}.d.ts`,
`${MODULE_ENTRIES.JWT}.d.ts`,
`${MODULE_ENTRIES.ERRORS}.d.ts`,
"internals",
]

2264
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,14 +7,26 @@
"author": "Iain Collins <me@iaincollins.com>",
"main": "index.js",
"types": "./index.d.ts",
"keywords": ["react", "nodejs", "oauth", "jwt", "oauth2", "authentication", "nextjs", "csrf", "oidc", "nextauth"],
"keywords": [
"react",
"nodejs",
"oauth",
"jwt",
"oauth2",
"authentication",
"nextjs",
"csrf",
"oidc",
"nextauth"
],
"exports": {
".": "./dist/server/index.js",
"./jwt": "./dist/lib/jwt.js",
"./adapters": "./dist/adapters/index.js",
"./client": "./dist/client/index.js",
"./providers": "./dist/providers/index.js",
"./providers/*": "./dist/providers/*.js"
"./providers/*": "./dist/providers/*.js",
"./errors": "./dist/lib/errors.js"
},
"scripts": {
"build": "npm run build:js && npm run build:css",
@@ -54,39 +66,61 @@
"adapters.d.ts",
"client.js",
"client.d.ts",
"errors.js",
"errors.d.ts",
"jwt.js",
"jwt.d.ts",
"internals"
],
"license": "ISC",
"dependencies": {
"crypto-js": "^4.0.0",
"@balazsorban/require-optional": "^1.0.0",
"futoin-hkdf": "^1.3.2",
"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",
"require_optional": "^1.0.1",
"typeorm": "^0.2.30"
"preact-render-to-string": "^5.1.14"
},
"peerDependencies": {
"react": "^16.13.1 || ^17",
"react-dom": "16.13.1 || ^17"
},
"peerOptionalDependencies": {
"mongodb": "^3.5.9",
"react-dom": "^16.13.1 || ^17",
"mongodb": "^3.6.6",
"mysql": "^2.18.1",
"mssql": "^6.2.1",
"pg": "^8.2.1",
"@prisma/client": "^2.16.1"
"@prisma/client": "^2.16.1",
"nodemailer": "^6.4.16",
"typeorm": "^0.2.30"
},
"peerDependenciesMeta": {
"mongodb": {
"optional": true
},
"mysql": {
"optional": true
},
"mssql": {
"optional": true
},
"pg": {
"optional": true
},
"@prisma/client": {
"optional": true
},
"nodemailer": {
"optional": true
},
"typeorm": {
"optional": true
}
},
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/preset-env": "^7.9.6",
"@prisma/client": "^2.16.1",
"@semantic-release/commit-analyzer": "^8.0.1",
@@ -110,7 +144,7 @@
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^5.0.0",
"mocha": "^8.1.3",
"mongodb": "^3.5.9",
"mongodb": "^3.6.6",
"mssql": "^6.2.1",
"mysql": "^2.18.1",
"next": "^10.0.5",
@@ -157,8 +191,14 @@
"branches": [
"+([0-9])?(.{+([0-9]),x}).x",
"main",
{ "name": "beta", "prerelease": true },
{ "name": "next", "prerelease": true }
{
"name": "beta",
"prerelease": true
},
{
"name": "next",
"prerelease": true
}
]
},
"funding": [

View File

@@ -1,6 +1,5 @@
import { createConnection, getConnection } from 'typeorm'
import { createHash } from 'crypto'
import require_optional from 'require_optional' // eslint-disable-line camelcase
import { CreateUserError } from '../../lib/errors'
import adapterConfig from './lib/config'
@@ -9,6 +8,8 @@ import Models from './models'
import { updateConnectionEntities } from './lib/utils'
import requireOptional from '@balazsorban/require-optional'
const Adapter = (typeOrmConfig, options = {}) => {
// Ensure typeOrmConfigObject is normalized to an object
const typeOrmConfigObject = (typeof typeOrmConfig === 'string')
@@ -94,12 +95,12 @@ const Adapter = (typeOrmConfig, options = {}) => {
let ObjectId
if (config.type === 'mongodb') {
idKey = '_id'
// Using a dynamic import causes problems for some compilers/bundlers
// that don't handle dynamic imports. To try and work around this we are
// using the same method mongodb uses to load Object ID type, which is to
// use the require_optional loader.
const mongodb = require_optional('mongodb')
ObjectId = mongodb.ObjectId
// We should/could use dynamic import here, but
// bundlers like webpack will try to import the module,
// even if this conditional branch is never entered.
// We work around this with requireOptional.
const mongodb = requireOptional('mongodb')
ObjectId = mongodb.ObjectID
}
// These values are stored as seconds, but to use them with dates in

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

@@ -0,0 +1,103 @@
import * as React from 'react'
import { GetServerSidePropsContext } from 'next'
interface DefaultSession {
user: {
name: string | null
email: string | null
image: string | null
}
expires: Date | string
}
interface BroadcastMessage {
event?: 'session'
data?: {
trigger?: 'signout' | 'getSession'
}
clientId: string
timestamp: number
}
type GetSession<S extends Record<string, unknown> = DefaultSession> = (options: {
ctx?: GetServerSidePropsContext
req?: GetServerSidePropsContext['req']
event?: 'storage' | 'timer' | 'hidden' | string
triggerEvent?: boolean
}) => Promise<S>
export interface NextAuthConfig {
baseUrl: string
basePath: string
baseUrlServer: string
basePathServer: string
/** 0 means disabled (don't send); 60 means send every 60 seconds */
keepAlive: number
/** 0 means disabled (only use cache); 60 means sync if last checked > 60 seconds ago */
clientMaxAge: number
/** Used for timestamp since last sycned (in seconds) */
_clientLastSync: number
/** Stores timer for poll interval */
_clientSyncTimer: ReturnType<typeof setTimeout>
/** Tracks if event listeners have been added */
_eventListenersAdded: boolean
/** Stores last session response from hook */
_clientSession: DefaultSession | null | undefined
/** Used to store to function export by getSession() hook */
_getSession: any
}
export type GetCsrfToken = (
ctxOrReq: GetServerSidePropsContext & GetServerSidePropsContext['req']
) => Promise<string | null>
export interface SessionOptions {
baseUrl?: string
basePath?: string
clientMaxAge?: number
keepAlive?: number
}
export type Provider<S extends Record<string, unknown> = DefaultSession > = (options: {
children: React.ReactNode
session: S
options: SessionOptions
}) => React.ReactNode
export type SetOptions = (options: SessionOptions) => void
export type SessionContext = React.createContext<[DefaultSession | null, boolean]>
export type UseSession = () => [any, boolean]
export type GetProviders = () => Promise<any[]>
// Sign in types
export interface SignInOptions {
/** Defaults to the current URL. */
callbackUrl?: string
redirect?: boolean
}
export interface SignInResponse {
error: string | null
status: number
ok: boolean
url: string | null
}
export type SignIn<AuthorizationParams = Record<string, string>> = (
provider?: string,
options?: SignInOptions,
authorizationParams?: AuthorizationParams
) => SignInResponse
// Sign out types
interface SignOutResponse<RedirectType extends boolean=true> {
/** Defaults to the current URL. */
callbackUrl?: string
redirect?: RedirectType
}
export type SignOut<RedirectType extends boolean = true> = (params: SignOutResponse<RedirectType>) => RedirectType extends true ? Promise<{url?: string} | undefined> : undefined

View File

@@ -1,39 +1,98 @@
/**
* Same as the default `Error`, but it is JSON serializable.
* @source https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
*/
export class UnknownError extends Error {
constructor (message) {
super(message)
this.name = 'UnknownError'
constructor(error) {
// Support passing error or string
super(error?.message ?? error)
this.name = "UnknownError"
if (error instanceof Error) {
this.stack = error.stack
}
}
toJSON () {
toJSON() {
return {
error: {
name: this.name,
message: this.message
// stack: this.stack
}
name: this.name,
message: this.message,
stack: this.stack,
}
}
}
export class CreateUserError extends UnknownError {
constructor (message) {
super(message)
this.name = 'CreateUserError'
}
}
// Thrown when an Email address is already associated with an account
// but the user is trying an OAuth account that is not linked to it.
export class AccountNotLinkedError extends UnknownError {
constructor (message) {
super(message)
this.name = 'AccountNotLinkedError'
}
}
export class OAuthCallbackError extends UnknownError {
constructor (message) {
super(message)
this.name = 'OAuthCallbackError'
}
name = "OAuthCallbackError"
}
/**
* Thrown when an Email address is already associated with an account
* but the user is trying an OAuth account that is not linked to it.
*/
export class AccountNotLinkedError extends UnknownError {
name = "AccountNotLinkedError"
}
export class CreateUserError extends UnknownError {
name = "CreateUserError"
}
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"
}

View File

@@ -1,33 +1,33 @@
import crypto from 'crypto'
import jose from 'jose'
import logger from './logger'
import crypto from "crypto"
import jose from "jose"
import logger from "./logger"
// Set default algorithm to use for auto-generated signing key
const DEFAULT_SIGNATURE_ALGORITHM = 'HS512'
const DEFAULT_SIGNATURE_ALGORITHM = "HS512"
// Set default algorithm for auto-generated symmetric encryption key
const DEFAULT_ENCRYPTION_ALGORITHM = 'A256GCM'
const DEFAULT_ENCRYPTION_ALGORITHM = "A256GCM"
// Use encryption or not by default
const DEFAULT_ENCRYPTION_ENABLED = false
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
async function encode ({
export async function encode({
token = {},
maxAge = DEFAULT_MAX_AGE,
secret,
signingKey,
signingOptions = {
expiresIn: `${maxAge}s`
expiresIn: `${maxAge}s`,
},
encryptionKey,
encryptionOptions = {
alg: 'dir',
alg: "dir",
enc: DEFAULT_ENCRYPTION_ALGORITHM,
zip: 'DEF'
zip: "DEF",
},
encryption = DEFAULT_ENCRYPTION_ENABLED
encryption = DEFAULT_ENCRYPTION_ENABLED,
} = {}) {
// Signing Key
const _signingKey = signingKey
@@ -49,7 +49,7 @@ async function encode ({
return signedToken
}
async function decode ({
export async function decode({
secret,
token,
maxAge = DEFAULT_MAX_AGE,
@@ -57,14 +57,14 @@ async function decode ({
verificationKey = signingKey, // Optional (defaults to encryptionKey)
verificationOptions = {
maxTokenAge: `${maxAge}s`,
algorithms: [DEFAULT_SIGNATURE_ALGORITHM]
algorithms: [DEFAULT_SIGNATURE_ALGORITHM],
},
encryptionKey,
decryptionKey = encryptionKey, // Optional (defaults to encryptionKey)
decryptionOptions = {
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM]
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM],
},
encryption = DEFAULT_ENCRYPTION_ENABLED
encryption = DEFAULT_ENCRYPTION_ENABLED,
} = {}) {
if (!token) return null
@@ -77,8 +77,12 @@ async function decode ({
: getDerivedEncryptionKey(secret)
// Decrypt token
const decryptedToken = jose.JWE.decrypt(token, _encryptionKey, decryptionOptions)
tokenToVerify = decryptedToken.toString('utf8')
const decryptedToken = jose.JWE.decrypt(
token,
_encryptionKey,
decryptionOptions
)
tokenToVerify = decryptedToken.toString("utf8")
}
// Signing Key
@@ -99,17 +103,22 @@ async function decode ({
* raw?: boolean
* }} params
*/
async function getToken (params) {
export async function getToken(params) {
const {
req,
// Use secure prefix for cookie name, unless URL is NEXTAUTH_URL is http://
// or not set (e.g. development or test instance) case use unprefixed name
secureCookie = !(!process.env.NEXTAUTH_URL || process.env.NEXTAUTH_URL.startsWith('http://')),
cookieName = (secureCookie) ? '__Secure-next-auth.session-token' : 'next-auth.session-token',
secureCookie = !(
!process.env.NEXTAUTH_URL ||
process.env.NEXTAUTH_URL.startsWith("http://")
),
cookieName = secureCookie
? "__Secure-next-auth.session-token"
: "next-auth.session-token",
raw = false,
decode: _decode = decode
decode: _decode = decode,
} = params
if (!req) throw new Error('Must pass `req` to JWT getToken()')
if (!req) throw new Error("Must pass `req` to JWT getToken()")
// Try to get token from cookie
let token = req.cookies[cookieName]
@@ -117,8 +126,8 @@ async function getToken (params) {
// If cookie not found in cookie look for bearer token in authorization header.
// This allows clients that pass through tokens in headers rather than as
// cookies to use this helper function.
if (!token && req.headers.authorization?.split(' ')[0] === 'Bearer') {
const urlEncodedToken = req.headers.authorization.split(' ')[1]
if (!token && req.headers.authorization?.split(" ")[0] === "Bearer") {
const urlEncodedToken = req.headers.authorization.split(" ")[1]
token = decodeURIComponent(urlEncodedToken)
}
@@ -138,7 +147,7 @@ let DERIVED_SIGNING_KEY_WARNING = false
let DERIVED_ENCRYPTION_KEY_WARNING = false
// Do the better hkdf of Node.js one added in `v15.0.0` and Third Party one
function hkdf (secret, { byteLength, encryptionInfo, digest = 'sha256' }) {
function hkdf(secret, { byteLength, encryptionInfo, digest = "sha256" }) {
if (crypto.hkdfSync) {
return Buffer.from(
crypto.hkdfSync(
@@ -150,39 +159,50 @@ function hkdf (secret, { byteLength, encryptionInfo, digest = 'sha256' }) {
)
)
}
return require('futoin-hkdf')(secret, byteLength, { info: encryptionInfo, hash: digest })
return require("futoin-hkdf")(secret, byteLength, {
info: encryptionInfo,
hash: digest,
})
}
function getDerivedSigningKey (secret) {
function getDerivedSigningKey(secret) {
if (!DERIVED_SIGNING_KEY_WARNING) {
logger.warn('JWT_AUTO_GENERATED_SIGNING_KEY')
logger.warn("JWT_AUTO_GENERATED_SIGNING_KEY")
DERIVED_SIGNING_KEY_WARNING = true
}
const buffer = hkdf(secret, {
byteLength: 64,
encryptionInfo: 'NextAuth.js Generated Signing Key'
encryptionInfo: "NextAuth.js Generated Signing Key",
})
const key = jose.JWK.asKey(buffer, {
alg: DEFAULT_SIGNATURE_ALGORITHM,
use: "sig",
kid: "nextauth-auto-generated-signing-key",
})
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_SIGNATURE_ALGORITHM, use: 'sig', kid: 'nextauth-auto-generated-signing-key' })
return key
}
function getDerivedEncryptionKey (secret) {
function getDerivedEncryptionKey(secret) {
if (!DERIVED_ENCRYPTION_KEY_WARNING) {
logger.warn('JWT_AUTO_GENERATED_ENCRYPTION_KEY')
logger.warn("JWT_AUTO_GENERATED_ENCRYPTION_KEY")
DERIVED_ENCRYPTION_KEY_WARNING = true
}
const buffer = hkdf(secret, {
byteLength: 32,
encryptionInfo: 'NextAuth.js Generated Encryption Key'
encryptionInfo: "NextAuth.js Generated Encryption Key",
})
const key = jose.JWK.asKey(buffer, {
alg: DEFAULT_ENCRYPTION_ALGORITHM,
use: "enc",
kid: "nextauth-auto-generated-encryption-key",
})
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_ENCRYPTION_ALGORITHM, use: 'enc', kid: 'nextauth-auto-generated-encryption-key' })
return key
}
export default {
encode,
decode,
getToken
getToken,
}

View File

@@ -1,5 +1,5 @@
import nodemailer from "nodemailer"
import logger from "../lib/logger"
import logger from '../lib/logger'
import requireOptional from '@balazsorban/require-optional'
export default function Email(options) {
return {
@@ -22,34 +22,25 @@ export default function Email(options) {
}
}
const sendVerificationRequest = ({
identifier: email,
url,
baseUrl,
provider,
}) => {
return new Promise((resolve, reject) => {
const { server, from } = provider
// Strip protocol from URL and use domain as site name
const site = baseUrl.replace(/^https?:\/\//, "")
nodemailer.createTransport(server).sendMail(
{
async function sendVerificationRequest ({ identifier: email, url, baseUrl, provider }) {
const { server, from } = provider
// Strip protocol from URL and use domain as site name
const site = baseUrl.replace(/^https?:\/\//, '')
try {
const nodemailer = requireOptional('nodemailer')
await 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()
}
)
})
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

@@ -0,0 +1,22 @@
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) => {
return {
id: profile.login.login_id,
name: profile.accountname,
email: profile.login.email,
image: null
}
},
...options
}
}

View File

@@ -1,4 +1,3 @@
import adapters from '../adapters'
import jwt from '../lib/jwt'
import parseUrl from '../lib/parse-url'
import logger, { setLogger } from '../lib/logger'
@@ -15,6 +14,8 @@ import csrfTokenHandler from './lib/csrf-token-handler'
import * as pkce from './lib/oauth/pkce-handler'
import * as state from './lib/oauth/state-handler'
import optionalRequire from '@balazsorban/require-optional'
// To work properly in production with OAuth providers the NEXTAUTH_URL
// environment variable must be set.
if (!process.env.NEXTAUTH_URL) {
@@ -73,7 +74,7 @@ async function NextAuthHandler (req, res, userOptions) {
// Protection only works on OAuth 2.x providers
if (provider?.type === 'oauth' && provider.version?.startsWith('2')) {
// When provider.state is undefined, we still want this to pass
if (!provider.protection && provider.state !== false) {
if (!provider.protection) {
// Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
provider.protection = ['state']
} else if (typeof provider.protection === 'string') {
@@ -86,7 +87,11 @@ async function NextAuthHandler (req, res, userOptions) {
// Parse database / adapter
// If adapter is provided, use it (advanced usage, overrides database)
// If database URI or config object is provided, use it (simple usage)
const adapter = userOptions.adapter ?? (userOptions.database && adapters.Default(userOptions.database))
let adapter = userOptions.adapter
if ((!adapter && !!userOptions.database)) {
const TypeOrm = optionalRequire('../adapters/typeorm')
adapter = TypeOrm.Adapter(userOptions.database)
}
// User provided options are overriden by other options,
// except for the options with special handling above

View File

@@ -65,18 +65,13 @@ export default async function callback (req, res) {
try {
const signInCallbackResponse = await callbacks.signIn(userOrProfile, account, OAuthProfile)
if (signInCallbackResponse === false) {
if (!signInCallbackResponse) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === 'string') {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
return res.redirect(error)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
// Sign user in
@@ -161,18 +156,13 @@ export default async function callback (req, res) {
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn(profile, account, { email })
if (signInCallbackResponse === false) {
if (!signInCallbackResponse) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === 'string') {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
return res.redirect(error)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
// Sign user in
@@ -236,12 +226,11 @@ export default async function callback (req, res) {
userObjectReturnedFromAuthorizeHandler = await provider.authorize(credentials)
if (!userObjectReturnedFromAuthorizeHandler) {
return res.status(401).redirect(`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`)
} else if (typeof userObjectReturnedFromAuthorizeHandler === 'string') {
return res.redirect(userObjectReturnedFromAuthorizeHandler)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
return res.redirect(error)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
const user = userObjectReturnedFromAuthorizeHandler
@@ -249,14 +238,13 @@ export default async function callback (req, res) {
try {
const signInCallbackResponse = await callbacks.signIn(user, account, credentials)
if (signInCallbackResponse === false) {
if (!signInCallbackResponse) {
return res.status(403).redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === 'string') {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
return res.redirect(error)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
const defaultJwtPayload = {

View File

@@ -45,18 +45,13 @@ export default async function signin (req, res) {
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn(profile, account, { email, verificationRequest: true })
if (signInCallbackResponse === false) {
if (!signInCallbackResponse) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === 'string') {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
return res.redirect(error)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
}
try {

1
types/adapters.d.ts vendored
View File

@@ -204,6 +204,7 @@ declare class TypeORMUserModel implements User {
image?: string,
emailVerified?: Date
)
[x: string]: unknown
}
declare class TypeORMSessionModel implements Session {

23
types/errors.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
/**
* Same as the default `Error`, but it is JSON serializable.
* @source https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
*/
export class UnknownError extends Error {}
export class OAuthCallbackError extends UnknownError {}
export class AccountNotLinkedError extends UnknownError {}
export class CreateUserError extends UnknownError {}
export class GetUserError extends UnknownError {}
export class GetUserByEmailError extends UnknownError {}
export class GetUserByIdError extends UnknownError {}
export class GetUserByProviderAccountIdError extends UnknownError {}
export class UpdateUserError extends UnknownError {}
export class DeleteUserError extends UnknownError {}
export class LinkAccountError extends UnknownError {}
export class UnlinkAccountError extends UnknownError {}
export class CreateSessionError extends UnknownError {}
export class GetSessionError extends UnknownError {}
export class UpdateSessionError extends UnknownError {}
export class DeleteSessionError extends UnknownError {}
export class CreateVerificationRequestError extends UnknownError {}
export class GetVerificationRequestError extends UnknownError {}
export class DeleteVerificationRequestError extends UnknownError {}

18
types/index.d.ts vendored
View File

@@ -251,14 +251,16 @@ export interface Account extends TokenSet, Record<string, unknown> {
type: string
}
/** The OAuth profile returned from your provider */
export interface Profile extends Record<string, unknown> {
export interface DefaultProfile {
sub?: string
name?: string
email?: string
image?: string
}
/** The OAuth profile returned from your provider */
export interface Profile extends Record<string, unknown>, DefaultProfile {}
/** [Documentation](https://next-auth.js.org/configuration/callbacks) */
export interface CallbacksOptions<
P extends Record<string, unknown> = Profile,
@@ -391,6 +393,12 @@ export interface SessionOptions {
updateAge?: number
}
export interface DefaultUser {
name?: string | null
email?: string | null
image?: string | null
}
/**
* The shape of the returned object in the OAuth providers' `profile` callback,
* available in the `jwt` and `session` callbacks,
@@ -401,11 +409,7 @@ export interface SessionOptions {
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`profile` OAuth provider callback](https://next-auth.js.org/configuration/providers#using-a-custom-provider)
*/
export interface User {
name?: string | null
email?: string | null
image?: string | null
}
export interface User extends Record<string, unknown>, DefaultUser {}
declare function NextAuth(
req: NextApiRequest,

View File

@@ -14,9 +14,22 @@ export type NextAuthSharedOptions =
| "theme"
| "debug"
| "logger"
| "session"
export interface AppOptions
extends Pick<NextAuthOptions, NextAuthSharedOptions> {
extends Required<Pick<NextAuthOptions, NextAuthSharedOptions>> {
providers: AppProvider[]
baseUrl: string
basePath: string
action:
| "providers"
| "session"
| "csrf"
| "signin"
| "signout"
| "callback"
| "verify-request"
| "error"
pkce?: {
code_verifier?: string
/**
@@ -27,18 +40,6 @@ export interface AppOptions
code_challenge_method?: "S256"
}
provider?: AppProvider
providers: AppProvider[]
baseUrl?: string
basePath?: string
action?:
| "providers"
| "session"
| "csrf"
| "signin"
| "signout"
| "callback"
| "verify-request"
| "error"
csrfToken?: string
csrfTokenVerified?: boolean
}

View File

@@ -77,6 +77,7 @@ export type OAuthProviderType =
| "Kakao"
| "LINE"
| "LinkedIn"
| "Mailchimp"
| "MailRu"
| "Medium"
| "Netlify"

View File

@@ -60,7 +60,7 @@ By default, the built-in pages will follow the system theme, utilizing the [`pre
In order to get the available authentication providers and the URLs to use for them, you can make a request to the API endpoint `/api/auth/providers`:
```jsx title="pages/auth/signin.js"
import { providers, signIn } from 'next-auth/client'
import { getProviders, signIn } from 'next-auth/client'
export default function SignIn({ providers }) {
return (
@@ -76,7 +76,7 @@ export default function SignIn({ providers }) {
// This is the recommended way for Next.js 9.3 or newer
export async function getServerSideProps(context){
const providers = await providers()
const providers = await getProviders()
return {
props: { providers }
}
@@ -86,7 +86,7 @@ export async function getServerSideProps(context){
// If older than Next.js 9.3
SignIn.getInitialProps = async () => {
return {
providers: await providers()
providers: await getProviders()
}
}
*/

View File

@@ -76,7 +76,6 @@ In _most cases_ it does not make sense to specify a database in NextAuth.js opti
The provider you tried to use failed when setting [PKCE or Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636#section-4.2).
The `code_verifier` is saved in a cookie called (by default) `__Secure-next-auth.pkce.code_verifier` which expires after 15 minutes.
Check if `cookies.pkceCodeVerifier` is configured correctly. The default `code_challenge_method` is `"S256"`. This is currently not configurable to `"plain"`, as it is not recommended, and in most cases it is only supported for backward compatibility.
---
### Session Handling

View File

@@ -0,0 +1,26 @@
---
id: mailchimp
title: Mailchimp
---
## Documentation
https://mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/
## Configuration
https://admin.mailchimp.com/account/oauth2/client/
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.Mailchimp({
clientId: process.env.MAILCHIMP_CLIENT_ID,
clientSecret: process.env.MAILCHIMP_CLIENT_SECRET
})
]
...
```

View File

@@ -188,7 +188,7 @@ npx prisma generate
To configure you database to use the new schema (i.e. create tables and columns) use the `prisma migrate` command:
```
npx prisma migrate dev --preview-feature
npx prisma migrate dev
```
To generate a schema in this way with the above example code, you will need to specify your datbase connection string in the environment variable `DATABASE_URL`. You can do this by setting it in a `.env` file at the root of your project.

View File

@@ -11,7 +11,7 @@ _New submissions and edits are welcome!_
### [NextJS Authentication Crash Course with NextAuth.js](https://youtu.be/o_wZIVmWteQ)
This tutorial dives in to the ins and outs of NextAuth including email, GitHub, Twitter and integrating with Auth0 in under hour.
This tutorial dives in to the ins and outs of NextAuth including email, Github, Twitter and integrating with Auth0 in under hour.
### [Create your own NextAuth.js Login Pages](https://youtu.be/kB6YNYZ63fw)

View File

@@ -46,32 +46,4 @@ You can use [node-jose-tools](https://www.npmjs.com/package/node-jose-tools) to
**Option 2**: Specify custom encode/decode functions on the jwt object. This gives you complete control over signing / verification / etc.
#### JWT_AUTO_GENERATED_ENCRYPTION_KEY
#### SIGNIN_CALLBACK_REJECT_REDIRECT
You returned something in the `signIn` callback, that is being deprecated.
You probably had something similar in the callback:
```js
return Promise.reject("/some/url")
```
or
```js
throw "/some/url"
```
To remedy this, simply return the url instead:
```js
return "/some/url"
```
#### STATE_OPTION_DEPRECATION
You provided `state: true` or `state: false` as a provider option. This is being deprecated in a later release in favour of `protection: "state"` and `protection: "none"` respectively. To remedy this warning:
- If you use `state: true`, just simply remove it. The default is `protection: "state"` already..
- If you use `state: false`, set `protection: "none"`.
#### JWT_AUTO_GENERATED_ENCRYPTION_KEY