Compare commits

..

3 Commits

Author SHA1 Message Date
Balázs Orbán
29b67b6647 Merge branch 'main' into next 2021-02-15 21:52:37 +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
22 changed files with 52 additions and 276 deletions

View File

@@ -2,7 +2,7 @@ name: Release
on:
push:
branches:
- 'main'
- 'master'
- 'next'
- '3.x'
pull_request:
@@ -23,4 +23,4 @@ jobs:
- run: npx semantic-release@17
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
NPM_TOKEN: ${{secrets.NPM_TOKEN}}

4
.gitignore vendored
View File

@@ -11,8 +11,6 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
# Dependencies
node_modules
@@ -39,4 +37,4 @@ www/providers.json
/_work
# Prisma migrations
/prisma/migrations
/prisma/migrations

View File

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

View File

@@ -7,25 +7,12 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<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" />
<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" />
</p>
</p>
@@ -167,3 +154,4 @@ We're open to all community contributions! If you'd like to contribute in any wa
## License
ISC

View File

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

View File

@@ -2,6 +2,9 @@ module.exports = {
branches: [
'+([0-9])?(.{+([0-9]),x}).x',
'main',
{ name: 'next', prerelease: true }
'next',
'next-major',
{ name: 'beta', prerelease: true },
{ name: 'alpha', prerelease: true }
]
}

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.avatar.startsWith('a_') ? 'gif' : 'png'
const format = profile.premium_type === 1 || profile.premium_type === 2 ? 'gif' : 'png'
profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`
}
return {

View File

@@ -72,11 +72,13 @@ async function NextAuthHandler (req, res, userOptions) {
const providers = parseProviders({ providers: userOptions.providers, baseUrl, basePath })
const provider = providers.find(({ id }) => id === providerId)
if (provider &&
provider.type === 'oauth' && provider.version?.startsWith('2') &&
(!provider.protection && provider.state !== false)
if (
provider?.type === 'oauth' &&
provider?.version?.startsWith('2') &&
!provider?.protection
) {
provider.protection = 'state' // Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
// Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
provider.protection = 'state'
}
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle

View File

@@ -108,7 +108,7 @@ async function getProfile ({ profileData, tokens, provider, user }) {
logger.debug('PROFILE_DATA', profileData)
const profile = await provider.profile(profileData, tokens)
const profile = await provider.profile(profileData)
// 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 = await provider.generateVerificationToken?.() ?? randomBytes(32).toString('hex')
const token = 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

@@ -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)}`)
}
// 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)}`)
}
// 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)}`)
}
// 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)}`)
}
// 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)}`)
}
return res.redirect(error)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
}
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)}`)
}
return res.redirect(error)
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
}
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 {

View File

@@ -328,7 +328,7 @@ export default NextAuth({
},
warn(code, ...message) {
log.warn(code, message)
},
}
debug(code, ...message) {
log.debug(code, message)
}

View File

@@ -78,10 +78,7 @@ 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, 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`
async profile(profile) {
return {
id: profile.id,
name: profile.name,

View File

@@ -5,14 +5,14 @@ title: Contributors
## Core Team
* [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)
* <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>
_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, however you can check out [this tutorial](/tutorials/refresh-token-rotation) if you want to implement it.
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.
### When I sign in with another account with the same email address, why are accounts not linked automatically?

View File

@@ -184,17 +184,3 @@ 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,8 +11,6 @@ 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
@@ -25,4 +23,4 @@ providers: [
})
]
...
```
```

View File

@@ -9,10 +9,6 @@ _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.
@@ -71,6 +67,7 @@ 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

@@ -1,139 +0,0 @@
---
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

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

View File

@@ -2,7 +2,7 @@
"name": "next-auth-docs",
"version": "0.1.1",
"scripts": {
"start": "npm run generate-providers && docusaurus start",
"start": "docusaurus start",
"build": "npm run generate-providers && docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",