Compare commits

..

3 Commits

Author SHA1 Message Date
Balázs Orbán
c8a201a247 Add missing installation requirements 2023-01-01 16:12:41 +01:00
Didi Keke
5586f07480 Add missing DynamoDBClientConfig interface import 2023-01-01 16:09:02 +01:00
Didi Keke
85ccd78977 Add @aws-sdk/client-dynamodb as peer dependency 2023-01-01 16:08:48 +01:00
18 changed files with 153 additions and 245 deletions

View File

@@ -3,10 +3,9 @@ import { authOptions } from "./api/auth/[...nextauth]"
import Layout from "../components/layout"
import type { GetServerSidePropsContext } from "next"
import { useSession } from "next-auth/react"
import type { Session } from "next-auth"
export default function ServerSidePage() {
const { data: session } = useSession()
export default function ServerSidePage({ session }: { session: Session }) {
// As this page uses Server Side Rendering, the `session` will be already
// populated on render without needing to go through a loading stage.
return (

View File

@@ -2,203 +2,119 @@
title: Refresh token rotation
---
Refresh token rotation is the practice of updating an `access_token` on behalf of the user, without requiring interaction (eg.: re-sign in). `access_token`s are usually issued for a limited time. After they expire, the service verifying them will ignore the value. Instead of asking the user to sign in again to obtain a new `access_token`, certain providers support exchanging a `refresh_token` for a new `access_token`, renewing the expiry time. Let's see how this can be achieved.
While Auth.js doesn't automatically handle access token rotation for [OAuth providers](/reference/providers/oauth-builtin) yet, this functionality can be implemented using [callbacks](/guides/basics/callbacks).
:::note
Our goal is to add zero-config support for built-in providers eventually. Let us know if you would like to help.
:::
## Source Code
A working example can be accessed [here](https://github.com/nextauthjs/next-auth-refresh-token-example).
## Implementation
First, make sure that the provider you want to use supports `refresh_token`'s. Check out [The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749#section-6) spec for more details.
### Server Side
Depending on the session strategy, `refresh_token` can be persisted either in a database, or in a cookie, in an encrypted JWT.
:::info
Using a JWT to store the `refresh_token` is less secure than saving it in a database, and you need to evaluate based on your requirements which strategy you choose.
:::
#### JWT strategy
Using the [jwt](../../reference/03-core/interfaces/types.CallbacksOptions.md#jwt) and [session](../../reference/03-core/interfaces/types.CallbacksOptions.md#session) callbacks, we can persist OAuth tokens and refresh them when they expire.
Using a [JWT callback](https://authjs.dev/guides/basics/callbacks#jwt-callback) and a [session callback](https://authjs.dev/guides/basics/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.
```ts
import { Auth } from "@auth/core"
import { type TokenSet } from "@auth/core/types"
import Google from "@auth/core/providers/google"
```js title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
export default Auth(new Request("https://example.com"), {
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_at * 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: [
Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
authorization: { params: { access_type: "offline", prompt: "consent" } },
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: GOOGLE_AUTHORIZATION_URL,
}),
],
callbacks: {
async jwt({ token, account }) {
if (account) {
// Save the access token and refresh token in the JWT on the initial login
async jwt({ token, user, account }) {
// Initial sign in
if (account && user) {
return {
access_token: account.access_token,
expires_at: Date.now() + account.expires_in * 1000,
refresh_token: account.refresh_token,
}
} else if (Date.now() < token.expires_at) {
// If the access token has not expired yet, return it
return token
} else {
// If the access token has expired, try to refresh it
try {
// https://accounts.google.com/.well-known/openid-configuration
// We need the `token_endpoint`.
const response = await fetch("https://oauth2.googleapis.com/token", {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: process.env.GOOGLE_ID,
client_secret: process.env.GOOGLE_SECRET,
grant_type: "refresh_token",
refresh_token: token.refresh_token,
}),
method: "POST",
})
const tokens: TokenSet = await response.json()
if (!response.ok) throw tokens
return {
...token, // Keep the previous token properties
access_token: tokens.access_token,
expires_at: Date.now() + tokens.expires_in * 1000,
// Fall back to old refresh token, but note that
// many providers may only allow using a refresh token once.
refresh_token: tokens.refresh_token ?? token.refresh_token,
}
} catch (error) {
console.error("Error refreshing access token", error)
// The error property will be used client-side to handle the refresh token error
return { ...token, error: "RefreshAccessTokenError" as const }
accessToken: account.access_token,
accessTokenExpires: Date.now() + account.expires_at * 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 }) {
session.user = token.user
session.accessToken = token.accessToken
session.error = token.error
return session
},
},
})
declare module "@auth/core/types" {
interface Session {
error?: "RefreshAccessTokenError"
}
}
declare module "@auth/core/jwt" {
interface JWT {
access_token: string
expires_at: number
refresh_token: string
error?: "RefreshAccessTokenError"
}
}
```
#### Database strategy
Using the database strategy is very similar, but instead of preserving the `access_token` and `refresh_token`, we save it, well, in the database.
```ts
import { Auth } from "@auth/core"
import { type TokenSet } from "@auth/core/types"
import Google from "@auth/core/providers/google"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
export default Auth(new Request("https://example.com"), {
adapter: PrismaAdapter(prisma),
providers: [
Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
authorization: { params: { access_type: "offline", prompt: "consent" } },
}),
],
callbacks: {
async session({ session, user }) {
const [google] = await prisma.account.findMany({
where: { userId: user.id, provider: "google" },
})
if (google.expires_at >= Date.now()) {
// If the access token has expired, try to refresh it
try {
// https://accounts.google.com/.well-known/openid-configuration
// We need the `token_endpoint`.
const response = await fetch("https://oauth2.googleapis.com/token", {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: process.env.GOOGLE_ID,
client_secret: process.env.GOOGLE_SECRET,
grant_type: "refresh_token",
refresh_token: google.refresh_token,
}),
method: "POST",
})
const tokens: TokenSet = await response.json()
if (!response.ok) throw tokens
await prisma.account.update({
data: {
access_token: tokens.access_token,
expires_at: Date.now() + tokens.expires_in * 1000,
refresh_token: tokens.refresh_token ?? google.refresh_token,
},
where: {
provider_providerAccountId: {
provider: "google",
providerAccountId: google.providerAccountId,
},
},
})
} catch (error) {
console.error("Error refreshing access token", error)
// The error property will be used client-side to handle the refresh token error
session.error = "RefreshAccessTokenError"
}
}
return session
},
},
})
declare module "@auth/core/types" {
interface Session {
error?: "RefreshAccessTokenError"
}
}
declare module "@auth/core/jwt" {
interface JWT {
access_token: string
expires_at: number
refresh_token: string
error?: "RefreshAccessTokenError"
}
}
```
### Client Side
The `RefreshAccessTokenError` error that is caught in the `refreshAccessToken()` method is passed to the client. This means that you can direct the user to the sign-in flow if we cannot refresh their token.
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:
@@ -218,8 +134,3 @@ const HomePage() {
return (...)
}
```
## Source Code
A working example can be accessed [here](https://github.com/nextauthjs/next-auth-refresh-token-example).

View File

@@ -1,14 +1,14 @@
---
title: Available OAuth providers
sidebar_label: OAuth providers
sidebar_label: Oauth providers
---
Authentication Providers in **Auth.js** are services that can be used to sign a user in.
Authentication Providers in **Auth.js** are services that can be used to sign in a user.
Auth.js comes with a set of built-in providers. You can find them [here](https://github.com/nextauthjs/next-auth/tree/main/packages/core/src/providers). Each built-in provider has its own documentation page:
:::note
Auth.js supports any **2.x** and **OpenID Connect (OIDC)** compliant providers and has built-in support for the most popular services.
Auth.js is designed to work with any OAuth service, it supports **OAuth 1.0**, **1.0A**, **2.0** and **OpenID Connect (OIDC)** and has built-in support for most popular sign-in services.
:::
<ul>

View File

@@ -11,7 +11,7 @@ http://developers.strava.com/docs/reference/
The **Strava Provider** comes with a set of default options:
- [Strava Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/strava.ts)
- [Strava Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/strava.js)
You can override any of the options to suit your own use case.

View File

@@ -22,7 +22,6 @@
"classnames": "^2.3.2",
"mdx-mermaid": "1.2.2",
"mermaid": "9.0.1",
"next-auth": "workspace:*",
"prism-react-renderer": "1.3.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",

View File

@@ -5,7 +5,6 @@
"repository": "https://github.com/nextauthjs/next-auth.git",
"scripts": {
"build:app": "turbo run build --filter=next-auth-app",
"build:docs": "turbo run build --filter=docs",
"build": "turbo run build --filter=next-auth --filter=@next-auth/* --filter=@auth/* --no-deps",
"test": "turbo run test --concurrency=1 --filter=[HEAD^1] --filter=./packages/* --filter=!*pouchdb-* --filter=!@*upstash* --filter=!*dynamodb-*",
"clean": "turbo run clean --no-cache",

View File

@@ -1,7 +1,7 @@
{
"name": "@next-auth/dynamodb-adapter",
"repository": "https://github.com/nextauthjs/next-auth",
"version": "1.0.6",
"version": "1.0.5",
"description": "AWS DynamoDB adapter for next-auth.",
"keywords": [
"next-auth",
@@ -44,4 +44,4 @@
"jest": "^27.4.3",
"next-auth": "workspace:*"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@auth/core",
"version": "0.2.5",
"version": "0.2.4",
"description": "Authentication for the Web.",
"keywords": [
"authentication",
@@ -20,7 +20,7 @@
"Balázs Orbán <info@balazsorban.com>",
"Nico Domino <yo@ndo.dev>",
"Lluis Agusti <hi@llu.lu>",
"Thang Huu Vu <hi@thvu.dev>",
"Thang Huu Vu <thvu@hey.com>",
"Iain Collins <me@iaincollins.com"
],
"type": "module",

View File

@@ -22,8 +22,7 @@ export async function getAuthorizationUrl(
let url = provider.authorization?.url
let as: o.AuthorizationServer | undefined
// Falls back to authjs.dev if the user only passed params
if (!url || url.host === "authjs.dev") {
if (!url) {
// If url is undefined, we assume that issuer is always defined
// We check this in assert.ts
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -49,9 +48,9 @@ export async function getAuthorizationUrl(
redirect_uri: provider.callbackUrl,
// @ts-expect-error TODO:
...provider.authorization?.params,
},
Object.fromEntries(provider.authorization?.url.searchParams ?? []),
query
}, // Defaults
Object.fromEntries(authParams), // From provider config
query // From `signIn` call
)
for (const k in params) authParams.set(k, params[k])

View File

@@ -31,12 +31,7 @@ export async function handleOAuth(
const { logger, provider } = options
let as: o.AuthorizationServer
const { token, userinfo } = provider
// Falls back to authjs.dev if the user only passed params
if (
(!token?.url || token.url.host === "authjs.dev") &&
(!userinfo?.url || userinfo.url.host === "authjs.dev")
) {
if (!provider.token?.url && !provider.userinfo?.url) {
// We assume that issuer is always defined as this has been asserted earlier
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const issuer = new URL(provider.issuer!)
@@ -59,9 +54,9 @@ export async function handleOAuth(
as = discoveredAs
} else {
as = {
issuer: provider.issuer ?? "https://authjs.dev", // TODO: review fallback issuer
token_endpoint: token?.url.toString(),
userinfo_endpoint: userinfo?.url.toString(),
issuer: provider.issuer ?? "https://a", // TODO: review fallback issuer
token_endpoint: provider.token?.url.toString(),
userinfo_endpoint: provider.userinfo?.url.toString(),
}
}
@@ -148,9 +143,9 @@ export async function handleOAuth(
throw new Error("TODO: Handle OAuth 2.0 response body error")
}
if (userinfo?.request) {
profile = await userinfo.request({ tokens, provider })
} else if (userinfo?.url) {
if (provider.userinfo?.request) {
profile = await provider.userinfo.request({ tokens, provider })
} else if (provider.userinfo?.url) {
const userinfoResponse = await o.userInfoRequest(
as,
client,

View File

@@ -45,11 +45,11 @@ export default function parseProviders(params: {
}
}
// TODO: Also add discovery here, if some endpoints/config are missing.
// We should return both a client and authorization server config.
function normalizeOAuth(
c: OAuthConfig<any> | OAuthUserConfig<any>
c?: OAuthConfig<any> | OAuthUserConfig<any>
): OAuthConfigInternal<any> | {} {
if (!c) return {}
if (c.issuer) c.wellKnown ??= `${c.issuer}/.well-known/openid-configuration`
const authorization = normalizeEndpoint(c.authorization, c.issuer)
@@ -84,18 +84,18 @@ function normalizeEndpoint(
e?: OAuthConfig<any>[OAuthEndpointType],
issuer?: string
): OAuthConfigInternal<any>[OAuthEndpointType] {
if (!e && issuer) return
if (!e || issuer) return
if (typeof e === "string") {
return { url: new URL(e) }
}
// If e.url is undefined, it's because the provider config
// If v.url is undefined, it's because the provider config
// assumes that we will use the issuer endpoint.
// The existence of either e.url or provider.issuer is checked in
// assert.ts. We fallback to "https://authjs.dev" to be able to pass around
// a valid URL even if the user only provided params.
// NOTE: This need to be checked when constructing the URL
// for the authorization, token and userinfo endpoints.
const url = new URL(e?.url ?? "https://authjs.dev")
for (const k in e?.params) url.searchParams.set(k, e?.params[k])
return { url, request: e?.request }
// The existence of either v.url or provider.issuer is checked in
// assert.ts
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const url = new URL(e.url!)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
for (const k in e.params) url.searchParams.set(k, e.params[k] as any)
return { ...e, url }
}

View File

@@ -2,7 +2,7 @@ import { handleLogin } from "../callback-handler.js"
import { CallbackRouteError } from "../../errors.js"
import { handleOAuth } from "../oauth/callback.js"
import { createHash } from "../web.js"
import { getAdapterUserFromEmail, handleAuthorized } from "./shared.js"
import { handleAuthorized } from "./shared.js"
import type { AdapterSession } from "../../adapters.js"
import type {
@@ -10,7 +10,6 @@ import type {
ResponseInternal,
User,
InternalOptions,
Account,
} from "../../types.js"
import type { Cookie, SessionStore } from "../cookie.js"
@@ -173,40 +172,40 @@ export async function callback(params: {
}
// @ts-expect-error -- Verified in `assertConfig`.
const user = await getAdapterUserFromEmail(identifier, adapter)
const profile = await getAdapterUserFromEmail(identifier, adapter)
const account: Account = {
providerAccountId: user.email,
userId: user.id,
const account = {
providerAccountId: profile.email,
type: "email" as const,
provider: provider.id,
}
// Check if user is allowed to sign in
const unauthorizedOrError = await handleAuthorized(
{ user, account },
{ user: profile, account },
options
)
if (unauthorizedOrError) return { ...unauthorizedOrError, cookies }
// Sign user in
const {
user: loggedInUser,
session,
isNewUser,
} = await handleLogin(sessionStore.value, user, account, options)
const { user, session, isNewUser } = await handleLogin(
sessionStore.value,
profile,
account,
options
)
if (useJwtSession) {
const defaultToken = {
name: loggedInUser.name,
email: loggedInUser.email,
picture: loggedInUser.image,
sub: loggedInUser.id?.toString(),
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const token = await callbacks.jwt({
token: defaultToken,
user: loggedInUser,
user,
account,
isNewUser,
})
@@ -234,7 +233,7 @@ export async function callback(params: {
})
}
await events.signIn?.({ user: loggedInUser, account, isNewUser })
await events.signIn?.({ user, account, isNewUser })
// Handle first logins on new accounts
// e.g. option to send users to a new account landing page on initial login

View File

@@ -33,7 +33,7 @@ export async function signin(
const account: Account = {
providerAccountId: email,
userId: user.id,
userId: email,
type: "email",
provider: provider.id,
}

View File

@@ -1,6 +1,6 @@
{
"name": "@auth/sveltekit",
"version": "0.1.12",
"version": "0.1.11",
"description": "Authentication for SvelteKit.",
"keywords": [
"authentication",

View File

@@ -18,7 +18,7 @@
* ## Usage
*
* ```ts title="src/hooks.server.ts"
* import { SvelteKitAuth } from "@auth/sveltekit"
* import SvelteKitAuth from "@auth/sveltekit"
* import GitHub from "@auth/core/providers/github"
* import { GITHUB_ID, GITHUB_SECRET } from "$env/static/private"
*

View File

@@ -9,7 +9,7 @@
"Balázs Orbán <info@balazsorban.com>",
"Nico Domino <yo@ndo.dev>",
"Lluis Agusti <hi@llu.lu>",
"Thang Huu Vu <hi@thvu.dev>"
"Thang Huu Vu <thvu@hey.com>"
],
"main": "index.js",
"module": "index.js",

2
pnpm-lock.yaml generated
View File

@@ -258,7 +258,6 @@ importers:
docusaurus-plugin-typedoc: ^0.18.0
mdx-mermaid: 1.2.2
mermaid: 9.0.1
next-auth: workspace:*
prism-react-renderer: 1.3.5
react: ^18.2.0
react-dom: ^18.2.0
@@ -273,7 +272,6 @@ importers:
classnames: 2.3.2
mdx-mermaid: 1.2.2_mermaid@9.0.1+react@18.2.0
mermaid: 9.0.1
next-auth: link:../packages/next-auth
prism-react-renderer: 1.3.5_react@18.2.0
react: 18.2.0
react-dom: 18.2.0_react@18.2.0

View File

@@ -1,19 +1,28 @@
{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"docs#build": {
"dependsOn": [
"^build",
"next-auth#build",
"@auth/core#build",
"@auth/sveltekit#build"
]
},
"build": {
"dependsOn": ["^build"]
},
"next-auth#build": {
"dependsOn": ["^build"],
"outputs": [
"client/**",
"core/**",
"lib/**",
"css/**",
"jwt/**",
"next/**",
"providers/**",
"react/**",
"next/**",
"client/**",
"providers/**",
"core/**",
"index.d.ts",
"index.js",
"adapters.d.ts",