Compare commits

...

29 Commits

Author SHA1 Message Date
Praneeth
172e79cb04 fix(page): add character encoding and page titles (#1380)
* added character encoding fix

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

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

* use simple initialization

* be optimistic

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

* add yarn.lock to .gitignore

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

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

* feat: send all available tokens to provider.profile
2021-02-20 22:58:48 +01:00
Balázs Orbán
2d364f246a docs: tweak release badges 2021-02-17 19:14:45 +01:00
Balázs Orbán
564b342f69 fix(docs): generate providers on docosaurus start 2021-02-16 15:42:27 +01:00
Balázs Orbán
63638d81dc docs: add sponsoring information 2021-02-16 15:42:27 +01:00
Balázs Orbán
28683015f1 docs: add links to README badges 2021-02-16 10:34:54 +01:00
Balázs Orbán
726c49603d chore: make next a prerelease channel 2021-02-16 10:20:46 +01:00
Balázs Orbán
a7113c6d3e chore: trigger release on main branch 2021-02-16 09:50:19 +01:00
Balázs Orbán
910514c6e2 chore: trigger release action on next branch 2021-02-15 21:51:17 +01:00
Balázs Orbán
b7cca484cf docs(provider): mention re-exporting config 2021-02-15 13:28:20 +01:00
Balázs Orbán
e293e786a8 fix(page): fallback to default error when no query param (#1303) 2021-02-11 22:25:09 +01:00
Balázs Orbán
82dd6ba3e4 feat(logger): introduce user configurable logger (#1294) 2021-02-11 14:50:53 +01:00
Balázs Orbán
6e28a07746 fix(client): reload after login/logout when url contains hash (#1298)
Co-authored-by: Thew Dhanat
2021-02-11 12:18:54 +01:00
Balázs Orbán
61047e3c14 chore: add CodeQL code analysis action 2021-02-11 09:58:22 +01:00
Balázs Orbán
dc5f3f481d chore(deps): upgrade test dependencies 2021-02-10 21:04:36 +01:00
Balázs Orbán
0343344802 chore: add new PR labels 2021-02-10 20:56:22 +01:00
Balázs Orbán
134a95a4bd docs(provider): auto-generate providers list (#1295)
* docs: ignore providers.json

* chore: generate providers.json from front-matter

* chore: remove autogenerated file

* docs(provider): rename vk.com to VK

* docs: encourage adding new providers
2021-02-10 20:43:04 +01:00
Joshua Payette
52a4bd97cd docs(provider): Update azure-ad-b2c (#1288)
It seems that the AZURE_TENANT_NAME env var is not required for next-auth.  This update removes it.

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-02-10 00:57:43 +01:00
Benjamin Bender
87d43e4038 docs: Update client.md (#1289)
Fix typo on getProviders()
2021-02-10 00:37:27 +01:00
Balázs Orbán
68695af1f3 chore: update release flow 2021-02-09 22:08:52 +01:00
Balázs Orbán
76df2b5e70 chore: disable semantic releases temporarily 2021-02-09 20:52:01 +01:00
45 changed files with 1580 additions and 4605 deletions

14
.github/labeler.yml vendored
View File

@@ -19,3 +19,17 @@ databases:
- test/docker/databases/**/*
- www/docs/configuration/databases.md
- test/fixtures/**/*
core:
- src/**/*
style:
- src/css/**/*
client:
- src/client/**/*
- www/docs/getting-started/client.md
pages:
- src/server/pages/**/*
- www/docs/configuration/pages.md

View File

@@ -1,31 +1,30 @@
# Simple check that the build is valid and no linting errors.
# Currently is run as a seperate workflow as it's fast to fail.
name: Build Test
name: Lint/Build
on:
push:
branches:
- main
- canary
- next
pull_request:
branches:
branches:
- main
- canary
- next
jobs:
build:
lint-and-build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
node-version: [10, 12, 14]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build
- name: Install dependencies
uses: bahmutov/npm-install@v1
- run: npm run lint
- run: npm run build

67
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main, next ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '43 17 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -2,9 +2,10 @@ name: Integration Test
on:
push:
branches: [ main, canary ]
branches:
- main
- next
pull_request:
branches: [ main, canary ]
jobs:
test:
@@ -28,7 +29,7 @@ jobs:
strategy:
matrix:
node-version: [12.x]
node-version: [10, 12, 14]
steps:
- uses: actions/checkout@v2
@@ -37,8 +38,8 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
# Install dependencies
- run: npm ci
- name: Install dependencies
uses: bahmutov/npm-install@v1
# Run tests (build library, build + start test app in Docker, run tests)
- run: npm test

View File

@@ -2,29 +2,25 @@ name: Release
on:
push:
branches:
- main
- canary
- 'main'
- 'next'
- '3.x'
pull_request:
jobs:
release:
name: Release
runs-on: ubuntu-20.04
name: 'Release'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Release
uses: bahmutov/npm-install@v1
- run: npm run build
- run: npx semantic-release@17
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
NPM_TOKEN: ${{secrets.NPM_TOKEN}}

5
.gitignore vendored
View File

@@ -11,6 +11,8 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
# Dependencies
node_modules
@@ -24,6 +26,7 @@ node_modules
.docusaurus
.cache-loader
.next
www/providers.json
# VS
/.vs/slnx.sqlite-journal
@@ -36,4 +39,4 @@ node_modules
/_work
# Prisma migrations
/prisma/migrations
/prisma/migrations

View File

@@ -1,39 +0,0 @@
{
"branches": [
"main",
{ "name": "canary", "prerelease": true }
],
"plugins": [
["@semantic-release/commit-analyzer", {
"preset": "conventionalcommits",
"releaseRules": [
{ "breaking": true, "release": "major" },
{ "revert": true, "release": "patch" },
{ "type": "feat", "release": "minor" },
{ "type": "fix", "release": "patch" },
{ "type": "perf", "release": "patch" },
{ "type": "docs", "release": "patch" }
]
}],
["@semantic-release/release-notes-generator", {
"preset": "conventionalcommits",
"presetConfig": {
"types": [
{ "type": "feat", "section": "Features", "hidden": false },
{ "type": "fix", "section": "Bug Fixes", "hidden": false },
{ "type": "perf", "section": "Performance Improvements", "hidden": false },
{ "type": "revert", "section": "Reverts", "hidden": false },
{ "type": "docs", "section": "Documentation", "hidden": false },
{ "type": "style", "section": "Styles", "hidden": false },
{ "type": "chore", "section": "Miscellaneous Chores", "hidden": false },
{ "type": "refactor", "section": "Code Refactoring", "hidden": false },
{ "type": "test", "section": "Tests", "hidden": false },
{ "type": "build", "section": "Build System", "hidden": false },
{ "type": "ci", "section": "Continuous Integration", "hidden": false }
]
}
}],
"@semantic-release/github",
"@semantic-release/npm"
]
}

View File

@@ -13,10 +13,9 @@ Please raise any significant new functionality or breaking change an issue for d
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 `canary`, so please make your Pull Request against that branch.
* 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
* Rebasing in Pull Requests is preferred to keep a clean commit history (see below)
* Run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this extension](https://marketplace.visualstudio.com/items?itemName=chenxsan.vscode-standardjs) to fix lint issues in development)
* We 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
@@ -89,7 +88,7 @@ We use [semantic-release](https://github.com/semantic-release/semantic-release)
When accepting Pull Requests, make sure the following:
* Use "Squash and merge"
* Make sure you merge contributor PRs into `canary`
* 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.)

3
FUNDING.yml Normal file
View File

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

View File

@@ -7,12 +7,25 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/next-auth/workflows/Build%20Test/badge.svg" alt="Build Test" />
<img src="https://github.com/nextauthjs/next-auth/workflows/Integration%20Test/badge.svg" alt="Integration Test" />
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
<img src="https://img.shields.io/npm/dm/next-auth" alt="Downloads" />
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth" alt="Github Stars" />
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases" alt="Github Release" />
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3ARelease">
<img src="https://github.com/nextauthjs/next-auth/workflows/Release/badge.svg" alt="Release" />
</a>
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3A%22Integration+Test%22">
<img src="https://github.com/nextauthjs/next-auth/workflows/Integration%20Test/badge.svg" alt="Integration Test" />
</a>
<a href="https://bundlephobia.com/result?p=next-auth">
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
</a>
<a href="https://www.npmtrends.com/next-auth">
<img src="https://img.shields.io/npm/dm/next-auth" alt="Downloads" />
</a>
<a href="https://github.com/nextauthjs/next-auth/stargazers">
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth" alt="Github Stars" />
</a>
<a href="https://www.npmjs.com/package/next-auth">
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?label=latest" alt="Github Stable Release" />
</a>
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases&label=prerelease&sort=semver" alt="Github Prelease" />
</p>
</p>
@@ -154,4 +167,3 @@ We're open to all community contributions! If you'd like to contribute in any wa
## License
ISC

View File

@@ -54,7 +54,7 @@ export default function Header () {
className={styles.button}
onClick={(e) => {
e.preventDefault()
signOut({ redirect: false })
signOut()
}}
>
Sign out

View File

@@ -108,5 +108,11 @@
"globals": [
"fetch"
]
}
},
"funding": [
{
"type" : "github",
"url" : "https://github.com/sponsors/balazsorban44"
}
]
}

7
release.config.js Normal file
View File

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

View File

@@ -1,84 +1,83 @@
const Adapter = (config, options = {}) => {
async function getAdapter (appOptions) {
const { logger } = appOptions
// Display debug output if debug option enabled
function _debug (...args) {
if (appOptions.debug) {
console.log('[next-auth][debug]', ...args)
}
function debug (debugCode, ...args) {
logger.debug(`ADAPTER_${debugCode}`, ...args)
}
async function createUser (profile) {
_debug('createUser', profile)
debug('createUser', profile)
return null
}
async function getUser (id) {
_debug('getUser', id)
debug('getUser', id)
return null
}
async function getUserByEmail (email) {
_debug('getUserByEmail', email)
debug('getUserByEmail', email)
return null
}
async function getUserByProviderAccountId (providerId, providerAccountId) {
_debug('getUserByProviderAccountId', providerId, providerAccountId)
debug('getUserByProviderAccountId', providerId, providerAccountId)
return null
}
async function updateUser (user) {
_debug('updateUser', user)
debug('updateUser', user)
return null
}
async function deleteUser (userId) {
_debug('deleteUser', userId)
debug('deleteUser', userId)
return null
}
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
_debug('linkAccount', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
debug('linkAccount', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
return null
}
async function unlinkAccount (userId, providerId, providerAccountId) {
_debug('unlinkAccount', userId, providerId, providerAccountId)
debug('unlinkAccount', userId, providerId, providerAccountId)
return null
}
async function createSession (user) {
_debug('createSession', user)
debug('createSession', user)
return null
}
async function getSession (sessionToken) {
_debug('getSession', sessionToken)
debug('getSession', sessionToken)
return null
}
async function updateSession (session, force) {
_debug('updateSession', session)
debug('updateSession', session)
return null
}
async function deleteSession (sessionToken) {
_debug('deleteSession', sessionToken)
debug('deleteSession', sessionToken)
return null
}
async function createVerificationRequest (identifier, url, token, secret, provider) {
_debug('createVerificationRequest', identifier)
debug('createVerificationRequest', identifier)
return null
}
async function getVerificationRequest (identifier, token, secret, provider) {
_debug('getVerificationRequest', identifier, token)
debug('getVerificationRequest', identifier, token)
return null
}
async function deleteVerificationRequest (identifier, token, secret, provider) {
_debug('deleteVerification', identifier, token)
debug('deleteVerification', identifier, token)
return null
}

View File

@@ -1,7 +1,6 @@
import { createHash, randomBytes } from 'crypto'
import { CreateUserError } from '../../lib/errors'
import logger from '../../lib/logger'
const Adapter = (config) => {
const {
@@ -21,6 +20,7 @@ const Adapter = (config) => {
}
async function getAdapter (appOptions) {
const { logger } = appOptions
function debug (debugCode, ...args) {
logger.debug(`PRISMA_${debugCode}`, ...args)
}

View File

@@ -6,7 +6,7 @@ import { CreateUserError } from '../../lib/errors'
import adapterConfig from './lib/config'
import adapterTransform from './lib/transform'
import Models from './models'
import logger from '../../lib/logger'
import { updateConnectionEntities } from './lib/utils'
const Adapter = (typeOrmConfig, options = {}) => {
@@ -41,6 +41,12 @@ const Adapter = (typeOrmConfig, options = {}) => {
let connection = null
async function getAdapter (appOptions) {
const { logger } = appOptions
// Display debug output if debug option enabled
function debug (debugCode, ...args) {
logger.debug(`TYPEORM_${debugCode}`, ...args)
}
// Helper function to reuse / restablish connections
// (useful if they drop when after being idle)
async function _connect () {
@@ -77,12 +83,6 @@ const Adapter = (typeOrmConfig, options = {}) => {
// https://github.com/typeorm/typeorm/blob/master/docs/entity-manager-api.md
const { manager } = connection
// Display debug output if debug option enabled
// @TODO Refactor logger so is passed in appOptions
function debug (debugCode, ...args) {
logger.debug(`TYPEORM_${debugCode}`, ...args)
}
// The models are primarily designed for ANSI SQL database, but some
// flexiblity is required in the adapter to support non-SQL databases such
// as MongoDB which have different pragmas.

View File

@@ -11,7 +11,7 @@
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
import { useState, useEffect, useContext, createContext, createElement } from 'react'
import logger from '../lib/logger'
import _logger, { proxyLogger } from '../lib/logger'
import parseUrl from '../lib/parse-url'
// This behaviour mirrors the default behaviour for getting the site name that
@@ -37,6 +37,8 @@ const __NEXTAUTH = {
_getSession: () => {}
}
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
// Add event listners on load
if (typeof window !== 'undefined') {
if (__NEXTAUTH._eventListenersAdded === false) {
@@ -278,7 +280,11 @@ export async function signIn (provider, options = {}, authorizationParams = {})
const res = await fetch(_signInUrl, fetchOptions)
const data = await res.json()
if (redirect || !isCredentials) {
window.location = data.url ?? callbackUrl
const url = data.url ?? callbackUrl
window.location = url
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes('#')) window.location.reload()
return
}
@@ -324,7 +330,10 @@ export async function signOut (options = {}) {
const data = await res.json()
_sendMessage({ event: 'session', data: { trigger: 'signout' } })
if (redirect) {
window.location = data.url ?? callbackUrl
const url = data.url ?? callbackUrl
window.location = url
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes('#')) window.location.reload()
return
}

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

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

View File

@@ -1,4 +1,5 @@
const logger = {
/** @type {import("./logger").LoggerInstance} */
const _logger = {
error (code, ...message) {
console.error(
`[next-auth][error][${code.toLowerCase()}]`,
@@ -22,4 +23,60 @@ const logger = {
}
}
export default logger
/**
* Override the built-in logger.
* Any `undefined` level will use the default logger.
* @param {Partial<import("./logger").LoggerInstance>} newLogger
*/
export function setLogger (newLogger = {}) {
if (newLogger.error) _logger.error = newLogger.error
if (newLogger.warn) _logger.warn = newLogger.warn
if (newLogger.debug) _logger.debug = newLogger.debug
}
export default _logger
/**
* Serializes client-side log messages and sends them to the server
* @param {import("./logger").LoggerInstance} logger
* @param {string} basePath
* @return {import("./logger").LoggerInstance}
*/
export function proxyLogger (logger = _logger, basePath) {
try {
if (typeof window === 'undefined') {
return logger
}
const clientLogger = {}
for (const level in logger) {
clientLogger[level] = (code, ...message) => {
_logger[level](code, ...message) // Log on client as usual
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
}))
})
if (navigator.sendBeacon) {
return navigator.sendBeacon(url, body)
}
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body
})
}
}
return clientLogger
} catch {
return _logger
}
}

View File

@@ -14,7 +14,7 @@ export default (options) => {
const defaultAvatarNumber = parseInt(profile.discriminator) % 5
profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`
} else {
const format = profile.premium_type === 1 || profile.premium_type === 2 ? 'gif' : 'png'
const format = profile.avatar.startsWith('a_') ? 'gif' : 'png'
profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`
}
return {

View File

@@ -3,7 +3,7 @@ export default (options) => {
return {
id: 'vk',
name: 'vk.com',
name: 'VK',
type: 'oauth',
version: '2.0',
scope: 'email',

View File

@@ -1,4 +1,5 @@
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
import { LoggerInstance } from 'src/lib/logger'
import { CallbacksOptions } from './lib/callbacks'
import { CookiesOptions } from './lib/cookie'
import { EventsOptions } from './lib/events'
@@ -59,10 +60,12 @@ export interface NextAuthOptions {
useSecureCookies?: boolean
/** @docs https://next-auth.js.org/configuration/options#cookies */
cookies?: CookiesOptions
/** @docs https://next-auth.js.org/configuration/options#logger */
logger: LoggerInstance
}
/** Options that are the same both in internal and user provided options. */
export type NextAuthSharedOptions = 'pages' | 'jwt' | 'events' | 'callbacks' | 'cookies' | 'secret' | 'adapter' | 'theme' | 'debug'
export type NextAuthSharedOptions = 'pages' | 'jwt' | 'events' | 'callbacks' | 'cookies' | 'secret' | 'adapter' | 'theme' | 'debug' | 'logger'
export interface NextAuthInternalOptions extends Pick<NextAuthOptions, NextAuthSharedOptions> {
pkce?: {

View File

@@ -1,7 +1,7 @@
import adapters from '../adapters'
import jwt from '../lib/jwt'
import parseUrl from '../lib/parse-url'
import logger from '../lib/logger'
import logger, { setLogger } from '../lib/logger'
import * as cookie from './lib/cookie'
import * as defaultEvents from './lib/default-events'
import * as defaultCallbacks from './lib/default-callbacks'
@@ -27,6 +27,9 @@ if (!process.env.NEXTAUTH_URL) {
* @param {import(".").NextAuthOptions} userOptions
*/
async function NextAuthHandler (req, res, userOptions) {
if (userOptions.logger) {
setLogger(userOptions.logger)
}
// If debug enabled, set ENV VAR so that logger logs debug messages
if (userOptions.debug) {
process.env._NEXTAUTH_DEBUG = true
@@ -127,7 +130,8 @@ async function NextAuthHandler (req, res, userOptions) {
...defaultCallbacks,
...userOptions.callbacks
},
pkce: {}
pkce: {},
logger
}
await callbackUrlHandler(req, res)
@@ -220,6 +224,21 @@ async function NextAuthHandler (req, res, userOptions) {
return routes.callback(req, res)
}
break
case '_log':
try {
if (!userOptions.logger) return
const {
code = 'CLIENT_ERROR',
level = 'error',
message = '[]'
} = req.body
logger[level](code, ...JSON.parse(message))
} catch (error) {
// If logging itself failed...
logger.error('LOGGER_ERROR', error)
}
return res.end()
default:
}
}

View File

@@ -108,7 +108,7 @@ async function getProfile ({ profileData, tokens, provider, user }) {
logger.debug('PROFILE_DATA', profileData)
const profile = await provider.profile(profileData)
const profile = await provider.profile(profileData, tokens)
// Return profile, raw profile and auth provider details
return {
profile: {

View File

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

View File

@@ -1,8 +1,17 @@
// @ts-check
import { h } from 'preact' // eslint-disable-line no-unused-vars
import render from 'preact-render-to-string'
/** Renders an error page. */
export default function error ({ baseUrl, basePath, error, res }) {
/**
* Renders an error page.
* @param {{
* baseUrl: string
* basePath: string
* error?: string
* res: import("..").NextAuthResponse
* }} params
*/
export default function error ({ baseUrl, basePath, error = 'default', res }) {
const signinPageUrl = `${baseUrl}${basePath}/signin`
const errors = {
@@ -44,7 +53,7 @@ export default function error ({ baseUrl, basePath, error, res }) {
}
}
const { statusCode, heading, message, signin } = errors[error.toLowerCase()] || errors.default
const { statusCode, heading, message, signin } = errors[error.toLowerCase()]
res.status(statusCode)

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -11,8 +11,8 @@
"author": "Iain Collins <me@iaincollins.com>",
"license": "ISC",
"dependencies": {
"next": "^9.5.4",
"react": "^16.13.1",
"react-dom": "^16.13.1"
"next": "^10.0.6",
"react": "^17.0.1",
"react-dom": "^17.0.1"
}
}

View File

@@ -112,52 +112,6 @@ callbacks: {
The redirect callback may be invoked more than once in the same flow.
:::
## Session callback
The session callback is called whenever a session is checked.
e.g. `getSession()`, `useSession()`, `/api/auth/session`
* When using database sessions, the User object is passed as an argument.
* When using JSON Web Tokens for sessions, the JWT payload is provided instead.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
/**
* @param {object} session Session object
* @param {object} token User object (if using database sessions)
* JSON Web Token (if not using database sessions)
* @return {object} Session that will be returned to the client
*/
async session(session, token) {
if(token?.accessToken) {
// Add property to session, like an access_token from a provider
session.accessToken = token.accessToken
}
return session
}
}
...
```
:::tip
When using JSON Web Tokens the `jwt()` callback is invoked before the `session()` callback, so anything you add to the
JSON Web Token will be immediately available in the session callback, like for example an `access_token` from a provider.
:::
:::tip
To better represent its value, when using a JWT session, the second parameter should be called `token` (This is the same thing you return from the `jwt` callback). If you use a database, call it `user`.
:::
:::warning
The session object is not persisted server side, even when using database sessions - only data such as the session token, the user, and the expiry time is stored in the session table.
If you need to persist session data server side, you can use the `accessToken` returned for the session as a key - and connect to the database in the `session()` callback to access it. Session `accessToken` values do not rotate and are valid as long as the session is valid.
If using JSON Web Tokens instead of database sessions, you should use the User ID or a unique key stored in the token (you will need to generate a key for this yourself on sign in, as access tokens for sessions are not generated when using JSON Web Tokens).
:::
## JWT callback
This JSON Web Token callback is called whenever a JSON Web Token is created (i.e. at sign
@@ -206,3 +160,47 @@ NextAuth.js does not limit how much data you can store in a JSON Web Token, howe
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). You can store a key that can be used to look up that data in the `session()` callback.
:::
## Session callback
The session callback is called whenever a session is checked. By default, only a subset of the token is returned for increased security. If you want to make something available you added to the token through the `jwt()` callback, you have to explicitely forward it here to make it available to the client.
e.g. `getSession()`, `useSession()`, `/api/auth/session`
* When using database sessions, the User object is passed as an argument.
* When using JSON Web Tokens for sessions, the JWT payload is provided instead.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
/**
* @param {object} session Session object
* @param {object} token User object (if using database sessions)
* JSON Web Token (if not using database sessions)
* @return {object} Session that will be returned to the client
*/
async session(session, token) {
// Add property to session, like an access_token from a provider.
session.accessToken = token.accessToken
return session
}
}
...
```
:::tip
When using JSON Web Tokens the `jwt()` callback is invoked before the `session()` callback, so anything you add to the
JSON Web Token will be immediately available in the session callback, like for example an `access_token` from a provider.
:::
:::tip
To better represent its value, when using a JWT session, the second parameter should be called `token` (This is the same thing you return from the `jwt()` callback). If you use a database, call it `user`.
:::
:::warning
The session object is not persisted server side, even when using database sessions - only data such as the session token, the user, and the expiry time is stored in the session table.
If you need to persist session data server side, you can use the `accessToken` returned for the session as a key - and connect to the database in the `session()` callback to access it. Session `accessToken` values do not rotate and are valid as long as the session is valid.
If using JSON Web Tokens instead of database sessions, you should use the User ID or a unique key stored in the token (you will need to generate a key for this yourself on sign in, as access tokens for sessions are not generated when using JSON Web Tokens).
:::

View File

@@ -307,6 +307,42 @@ Set debug to `true` to enable debug messages for authentication and database ope
---
### logger
* **Default value**: `console`
* **Required**: *No*
#### Description
Override any of the logger levels (`undefined` levels will use the built-in logger), and intercept logs in NextAuth. You can use this to send NextAuth logs to a third-party logging service.
Example:
```js title="/pages/api/auth/[...nextauth].js"
import log from "logging-service"
export default NextAuth({
...
logger: {
error(code, ...message) {
log.error(code, message)
},
warn(code, ...message) {
log.warn(code, message)
},
debug(code, ...message) {
log.debug(code, message)
}
}
...
})
```
:::note
If the `debug` level is defined by the user, it will be called regardless of the `debug: false` [option](#debug).
:::
---
### theme
* **Default value**: `"auto"`

View File

@@ -7,7 +7,7 @@ NextAuth.js automatically creates simple, unbranded authentication pages for han
The options displayed on the sign up page are automatically generated based on the providers specified in the options passed to NextAuth.js.
To add a custom login page, for example. You can use the `pages` option:
To add a custom login page, you can use the `pages` option:
```javascript title="pages/api/auth/[...nextauth].js"
...
@@ -121,4 +121,4 @@ signIn('credentials', { username: 'jsmith', password: '1234' })
:::tip
Remember to put any custom pages in a folder outside **/pages/api** which is reserved for API code. As per the examples above, a location convention suggestion is `pages/auth/...`.
:::
:::

View File

@@ -78,7 +78,10 @@ As an example of what this looks like, this is the the provider object returned
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",
async profile(profile) {
async profile(profile, tokens) {
// You can use the tokens, in case you want to fetch more profile information
// For example several OAuth provider does not return e-mail by default.
// Depending on your provider, will have tokens like `access_token`, `id_token` and or `refresh_token`
return {
id: profile.id,
name: profile.name,
@@ -112,6 +115,16 @@ providers: [
...
```
:::tip
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 three changes:
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers)
2. Re-export your config: at [`src/providers/index.js`](https://github.com/nextauthjs/next-auth/blob/main/src/providers/index.js)
3. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
You can look at the existing built-in providers for inspiration.
:::
### OAuth provider options
| Name | Description | Type | Required |

View File

@@ -5,14 +5,14 @@ title: Contributors
## Core Team
* <a href="https://github.com/iaincollins">Iain Collins</a>
* <a href="https://github.com/LoriKarikari">Lori Karikari</a>
* <a href="https://github.com/ndom91">Nico Domino</a>
* <a href="https://github.com/Fumler">Fredrik Pettersen</a>
* <a href="https://github.com/geraldnolan">Gerald Nolan</a>
* <a href="https://github.com/lluia">Lluis Agusti</a>
* <a href="https://github.com/JeffersonBledsoe">Jefferson Bledsoe</a>
* <a href="https://github.com/balazsorban44">Balázs Orbán</a>
* [Iain Collins](https://github.com/iaincollins)
* [Lori Karikari](https://github.com/LoriKarikari)
* [Nico Domino](https://github.com/ndom91)
* [Fredrik Pettersen](https://github.com/Fumler)
* [Gerald Nolan](https://github.com/geraldnolan)
* [Lluis Agusti](https://github.com/lluia)
* [Jefferson Bledsoe](https://github.com/JeffersonBledsoe)
* [Balázs Orbán](https://github.com/sponsors/balazsorban44)
_Special thanks to Lori Karikari for creating most of the providers, to Nico Domino for creating this site, to Fredrik Pettersen for creating the Prisma adapter, to Gerald Nolan for adding support for Sign in with Apple, to Lluis Agusti for work to add TypeScript definitions and to Jefferson Bledsoe for working on automating testing._

View File

@@ -116,7 +116,7 @@ NextAuth.js records Refresh Tokens and Access Tokens on sign in (if supplied by
You can then look them up from the database or persist them to the JSON Web Token.
Note: NextAuth.js does not currently handle Access Token rotation for OAuth providers for you, if this is something you need, currently you will need to write the logic to handle that yourself.
Note: NextAuth.js does not currently handle Access Token rotation for OAuth providers for you, however you can check out [this tutorial](/tutorials/refresh-token-rotation) if you want to implement it.
### When I sign in with another account with the same email address, why are accounts not linked automatically?

View File

@@ -150,7 +150,7 @@ export default async (req, res) => {
```
:::note
Unlike `getSession()` and `getCsrfToken()`, when calling `getSession()` server side, you don't need to pass anything, just as calling it client side.
Unlike `getSession()` and `getCsrfToken()`, when calling `getProviders()` server side, you don't need to pass anything, just as calling it client side.
:::
---

View File

@@ -24,7 +24,6 @@ In `.env.local` create the follwing entries:
```
AZURE_CLIENT_ID=<copy Application (client) ID here>
AZURE_CLIENT_SECRET=<copy generated secret value here>
AZURE_TENANT_NAME=<copy the name of the tenant here>
AZURE_TENANT_ID=<copy the tenant id here>
```

View File

@@ -184,3 +184,17 @@ const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n`
:::tip
If you want to generate great looking email client compatible HTML with React, check out https://mjml.io
:::
## Customising the Verification Token
By default, we are generating a random verification token. You can define a `generateVerificationToken` method in your provider options if you want to override it:
```js title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
async generateVerificationToken() {
return "ABC123"
}
})
],

View File

@@ -11,6 +11,8 @@ https://dev.twitch.tv/docs/authentication
https://dev.twitch.tv/console/apps
Add the following redirect URL into the console `http://<your-next-app-url>/api/auth/callback/twitch`
## Example
```js
@@ -23,4 +25,4 @@ providers: [
})
]
...
```
```

View File

@@ -1,6 +1,6 @@
---
id: vk
title: vk.com
title: VK
---
## Documentation

View File

@@ -9,6 +9,10 @@ _These tutorials are contributed by the community and hosted on this site._
_New submissions and edits are welcome!_
### [Refresh Token Rotation](tutorials/refresh-token-rotation)
How to implement refresh token rotation.
### [Securing pages and API routes](tutorials/securing-pages-and-api-routes)
How to restrict access to pages and API routes.
@@ -67,7 +71,6 @@ This example shows how to implement a fullstack app in TypeScript with Next.js u
This `dev.to` tutorial walks one through adding NextAuth.js to an existing project. Including setting up the OAuth client id and secret, adding the API routes for authentication, protecting pages and api routes behind that authentication, etc.
### [Adding Sign in With Apple Next JS](https://thesiddd.com/blog/apple-auth)
This tutorial walks step by step on how to get Sign In with Apple working (both locally and on a deployed website) using NextAuth.js.

View File

@@ -0,0 +1,139 @@
---
id: refresh-token-rotation
title: Refresh Token Rotation
---
While NextAuth.js doesn't automatically handle access token rotation for OAuth providers yet, this functionality can be implemented using [callbacks](https://next-auth.js.org/configuration/callbacks).
## Source Code
_A working example can be accessed [here](https://github.com/lawrencecchen/next-auth-refresh-tokens)._
## Implementation
### Server Side
Using a [JWT callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) and a [session callback](https://next-auth.js.org/configuration/callbacks#session-callback), we can persist OAuth tokens and refresh them when they expire.
Below is a sample implementation using Google's Identity Provider. Please note that the OAuth 2.0 request in the `refreshAccessToken()` function will vary between different providers, but the core logic should remain similar.
```js title="pages/auth/[...nextauth.js]"
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
const GOOGLE_AUTHORIZATION_URL =
"https://accounts.google.com/o/oauth2/v2/auth?" +
new URLSearchParams({
prompt: "consent",
access_type: "offline",
response_type: "code",
});
/**
* Takes a token, and returns a new token with updated
* `accessToken` and `accessTokenExpires`. If an error occurs,
* returns the old token and an error property
*/
async function refreshAccessToken(token) {
try {
const url =
"https://oauth2.googleapis.com/token?" +
new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
grant_type: "refresh_token",
refresh_token: token.refreshToken,
});
const response = await fetch(url, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
method: "POST",
});
const refreshedTokens = await response.json();
if (!response.ok) {
throw refreshedTokens;
}
return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fall back to old refresh token
};
} catch (error) {
console.log(error);
return {
...token,
error: "RefreshAccessTokenError",
};
}
}
export default NextAuth({
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorizationUrl: GOOGLE_AUTHORIZATION_URL,
}),
],
callbacks: {
async jwt(token, user, account) {
// Initial sign in
if (account && user) {
return {
accessToken: account.accessToken,
accessTokenExpires: Date.now() + account.expires_in * 1000,
refreshToken: account.refresh_token,
user,
};
}
// Return previous token if the access token has not expired yet
if (Date.now() < token.accessTokenExpires) {
return token;
}
// Access token has expired, try to update it
return refreshAccessToken(token);
},
async session(session, token) {
if (token) {
session.user = token.user;
session.accessToken = token.accessToken;
session.error = token.error;
}
return session;
},
},
});
```
### Client Side
The `RefreshAccessTokenError` error that is caught in the `refreshAccessToken()` method is passed all the way to the client. This means that you can direct the user to the sign in flow if we cannot refresh their token.
We can handle this functionality as a side effect:
```js title="pages/auth/[...nextauth.js]"
import { signIn, useSession } from "next-auth/client";
import { useEffect } from "react";
const HomePage() {
const [session] = useSession();
useEffect(() => {
if (session?.error === "RefreshAccessTokenError") {
signIn(); // Force sign in to hopefully resolve error
}
}, [session]);
return (...)
}
```

View File

@@ -2,12 +2,13 @@
"name": "next-auth-docs",
"version": "0.1.1",
"scripts": {
"start": "docusaurus start",
"build": "docusaurus build",
"start": "npm run generate-providers && docusaurus start",
"build": "npm run generate-providers && docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"lint": "standard",
"lint:fix": "standard --fix"
"lint:fix": "standard --fix",
"generate-providers": "node ./scripts/generate-providers.js"
},
"dependencies": {
"@docusaurus/core": "^2.0.0-alpha.66",

View File

@@ -1,37 +0,0 @@
{
"apple": "Apple",
"atlassian": "Atlassian",
"auth0": "Auth0",
"azure-ad-b2c": "Azure Active Directory B2C",
"basecamp": "Basecamp",
"battle.net": "Battle.net",
"box": "Box",
"bungie": "Bungie",
"cognito": "Amazon Cognito",
"credentials": "Credentials",
"discord": "Discord",
"email": "Email",
"eveonline": "EVE Online",
"facebook": "Facebook",
"foursquare": "Foursquare",
"fusionauth": "FusionAuth",
"github": "GitHub",
"gitlab": "GitLab",
"google": "Google",
"identity-server4": "IdentityServer4",
"line": "LINE",
"linkedin": "LinkedIn",
"mailru": "Mail.ru",
"medium": "Medium",
"netlify": "Netlify",
"okta": "Okta",
"reddit": "Reddit",
"salesforce": "Salesforce",
"slack": "Slack",
"spotify": "Spotify",
"strava": "Strava",
"twitch": "Twitch",
"twitter": "Twitter",
"vk": "VK",
"yandex": "Yandex"
}

View File

@@ -0,0 +1,15 @@
const path = require('path')
const fs = require('fs')
const providersPath = path.join(process.cwd(), '/docs/providers')
const files = fs.readdirSync(providersPath, 'utf8')
const result = files.reduce((acc, file) => {
const provider = fs.readFileSync(path.join(providersPath, file), 'utf8')
const { id, title } = provider.match(/id: (?<id>.+)\ntitle: (?<title>.+)\n/).groups
acc[id] = title
return acc
}, {})
fs.writeFileSync(path.join(process.cwd(), 'providers.json'), JSON.stringify(result, null, 2))