mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
4 Commits
@auth/core
...
fix/improv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f42ef7cc62 | ||
|
|
35a708a2c3 | ||
|
|
4f919ca76f | ||
|
|
036e34b4b6 |
@@ -3,10 +3,9 @@ import { authOptions } from "./api/auth/[...nextauth]"
|
|||||||
import Layout from "../components/layout"
|
import Layout from "../components/layout"
|
||||||
|
|
||||||
import type { GetServerSidePropsContext } from "next"
|
import type { GetServerSidePropsContext } from "next"
|
||||||
import { useSession } from "next-auth/react"
|
import type { Session } from "next-auth"
|
||||||
|
|
||||||
export default function ServerSidePage() {
|
export default function ServerSidePage({ session }: { session: Session }) {
|
||||||
const { data: session } = useSession()
|
|
||||||
// As this page uses Server Side Rendering, the `session` will be already
|
// As this page uses Server Side Rendering, the `session` will be already
|
||||||
// populated on render without needing to go through a loading stage.
|
// populated on render without needing to go through a loading stage.
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,203 +2,119 @@
|
|||||||
title: Refresh token rotation
|
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
|
## Source Code
|
||||||
Our goal is to add zero-config support for built-in providers eventually. Let us know if you would like to help.
|
|
||||||
:::
|
A working example can be accessed [here](https://github.com/nextauthjs/next-auth-refresh-token-example).
|
||||||
|
|
||||||
## Implementation
|
## 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
|
### Server Side
|
||||||
|
|
||||||
Depending on the session strategy, `refresh_token` can be persisted either in a database, or in a cookie, in an encrypted JWT.
|
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.
|
||||||
|
|
||||||
:::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.
|
|
||||||
|
|
||||||
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.
|
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
|
```js title="pages/api/auth/[...nextauth].js"
|
||||||
import { Auth } from "@auth/core"
|
import NextAuth from "next-auth"
|
||||||
import { type TokenSet } from "@auth/core/types"
|
import GoogleProvider from "next-auth/providers/google"
|
||||||
import Google from "@auth/core/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: [
|
providers: [
|
||||||
Google({
|
GoogleProvider({
|
||||||
clientId: process.env.GOOGLE_ID,
|
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||||
clientSecret: process.env.GOOGLE_SECRET,
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
authorization: { params: { access_type: "offline", prompt: "consent" } },
|
authorization: GOOGLE_AUTHORIZATION_URL,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, account }) {
|
async jwt({ token, user, account }) {
|
||||||
if (account) {
|
// Initial sign in
|
||||||
// Save the access token and refresh token in the JWT on the initial login
|
if (account && user) {
|
||||||
return {
|
return {
|
||||||
access_token: account.access_token,
|
accessToken: account.access_token,
|
||||||
expires_at: Date.now() + account.expires_in * 1000,
|
accessTokenExpires: Date.now() + account.expires_at * 1000,
|
||||||
refresh_token: account.refresh_token,
|
refreshToken: account.refresh_token,
|
||||||
}
|
user,
|
||||||
} 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 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 }) {
|
async session({ session, token }) {
|
||||||
|
session.user = token.user
|
||||||
|
session.accessToken = token.accessToken
|
||||||
session.error = token.error
|
session.error = token.error
|
||||||
|
|
||||||
return session
|
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
|
### 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:
|
We can handle this functionality as a side effect:
|
||||||
|
|
||||||
@@ -218,8 +134,3 @@ const HomePage() {
|
|||||||
return (...)
|
return (...)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Source Code
|
|
||||||
|
|
||||||
A working example can be accessed [here](https://github.com/nextauthjs/next-auth-refresh-token-example).
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
---
|
---
|
||||||
title: Available OAuth providers
|
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:
|
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
|
:::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>
|
<ul>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ http://developers.strava.com/docs/reference/
|
|||||||
|
|
||||||
The **Strava Provider** comes with a set of default options:
|
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.
|
You can override any of the options to suit your own use case.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-auth/dynamodb-adapter",
|
"name": "@next-auth/dynamodb-adapter",
|
||||||
"repository": "https://github.com/nextauthjs/next-auth",
|
"repository": "https://github.com/nextauthjs/next-auth",
|
||||||
"version": "1.0.6",
|
"version": "1.0.5",
|
||||||
"description": "AWS DynamoDB adapter for next-auth.",
|
"description": "AWS DynamoDB adapter for next-auth.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"next-auth",
|
"next-auth",
|
||||||
@@ -44,4 +44,4 @@
|
|||||||
"jest": "^27.4.3",
|
"jest": "^27.4.3",
|
||||||
"next-auth": "workspace:*"
|
"next-auth": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@auth/core",
|
"name": "@auth/core",
|
||||||
"version": "0.2.5",
|
"version": "0.2.4",
|
||||||
"description": "Authentication for the Web.",
|
"description": "Authentication for the Web.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"authentication",
|
"authentication",
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
|
interface ErrorCause extends Record<string, unknown> {}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export class AuthError extends Error {
|
export class AuthError extends Error {
|
||||||
metadata?: Record<string, unknown>
|
constructor(message: string | Error | ErrorCause, cause?: ErrorCause) {
|
||||||
constructor(message: Error | string, metadata?: Record<string, unknown>) {
|
|
||||||
if (message instanceof Error) {
|
if (message instanceof Error) {
|
||||||
super(message.message)
|
super(undefined, {
|
||||||
this.stack = message.stack
|
cause: { err: message, ...(message.cause as any), ...cause },
|
||||||
} else super(message)
|
})
|
||||||
this.name = this.constructor.name
|
} else if (typeof message === "string") {
|
||||||
this.metadata = metadata
|
if (cause instanceof Error) {
|
||||||
|
cause = { err: cause, ...(cause.cause as any) }
|
||||||
|
}
|
||||||
|
super(message, cause)
|
||||||
|
} else {
|
||||||
|
super(undefined, message)
|
||||||
|
}
|
||||||
Error.captureStackTrace?.(this, this.constructor)
|
Error.captureStackTrace?.(this, this.constructor)
|
||||||
|
this.name =
|
||||||
|
message instanceof AuthError ? message.name : this.constructor.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +37,45 @@ export class AdapterError extends AuthError {}
|
|||||||
/** @todo */
|
/** @todo */
|
||||||
export class AuthorizedCallbackError extends AuthError {}
|
export class AuthorizedCallbackError extends AuthError {}
|
||||||
|
|
||||||
/** @todo */
|
/**
|
||||||
|
* There was an error while trying to finish up authenticating the user.
|
||||||
|
* Depending on the type of provider, this could be for multiple reasons.
|
||||||
|
*
|
||||||
|
* :::tip
|
||||||
|
* Check out `[auth][details]` in the error message to know which provider failed.
|
||||||
|
* @example
|
||||||
|
* ```sh
|
||||||
|
* [auth][details]: { "provider": "github" }
|
||||||
|
* ```
|
||||||
|
* :::
|
||||||
|
*
|
||||||
|
* For an **OAuth provider**, possible causes are:
|
||||||
|
* - The user denied access to the application
|
||||||
|
* - There was an error parsing the OAuth Profile:
|
||||||
|
* Check out the provider's `profile` or `userinfo.request` method to make sure
|
||||||
|
* it correctly fetches the user's profile.
|
||||||
|
* - The `signIn` or `jwt` callback methods threw an uncaught error:
|
||||||
|
* Check the callback method implementations.
|
||||||
|
*
|
||||||
|
* For an **Email provider**, possible causes are:
|
||||||
|
* - The provided email/token combination was invalid/missing:
|
||||||
|
* Check if the provider's `sendVerificationRequest` method correctly sends the email.
|
||||||
|
* - The provided email/token combination has expired:
|
||||||
|
* Ask the user to log in again.
|
||||||
|
* - There was an error with the database:
|
||||||
|
* Check the database logs.
|
||||||
|
*
|
||||||
|
* For a **Credentials provider**, possible causes are:
|
||||||
|
* - The `authorize` method threw an uncaught error:
|
||||||
|
* Check the provider's `authorize` method.
|
||||||
|
* - The `signIn` or `jwt` callback methods threw an uncaught error:
|
||||||
|
* Check the callback method implementations.
|
||||||
|
*
|
||||||
|
* :::tip
|
||||||
|
* Check out `[auth][cause]` in the error message for more details.
|
||||||
|
* It will show the original stack trace.
|
||||||
|
* :::
|
||||||
|
*/
|
||||||
export class CallbackRouteError extends AuthError {}
|
export class CallbackRouteError extends AuthError {}
|
||||||
|
|
||||||
/** @todo */
|
/** @todo */
|
||||||
@@ -93,3 +140,10 @@ export class UnsupportedStrategy extends AuthError {}
|
|||||||
|
|
||||||
/** @todo */
|
/** @todo */
|
||||||
export class UntrustedHost extends AuthError {}
|
export class UntrustedHost extends AuthError {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's email/token combination was invalid.
|
||||||
|
* This could be because the email/token combination was not found in the database,
|
||||||
|
* or because it token has expired. Ask the user to log in again.
|
||||||
|
*/
|
||||||
|
export class Verification extends AuthError {}
|
||||||
|
|||||||
@@ -133,7 +133,8 @@ export async function handleLogin(
|
|||||||
// with is already associated with another user, then we cannot link them
|
// with is already associated with another user, then we cannot link them
|
||||||
// and need to return an error.
|
// and need to return an error.
|
||||||
throw new AccountNotLinked(
|
throw new AccountNotLinked(
|
||||||
"The account is already associated with another user"
|
"The account is already associated with another user",
|
||||||
|
{ provider: account.provider }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// If there is no active session, but the account being signed in with is already
|
// If there is no active session, but the account being signed in with is already
|
||||||
@@ -193,7 +194,8 @@ export async function handleLogin(
|
|||||||
// want to link them in case it's not safe to do so, so instead we prompt the user
|
// want to link them in case it's not safe to do so, so instead we prompt the user
|
||||||
// to sign in via email to verify their identity and then link the accounts.
|
// to sign in via email to verify their identity and then link the accounts.
|
||||||
throw new AccountNotLinked(
|
throw new AccountNotLinked(
|
||||||
"Another account already exists with the same e-mail address"
|
"Another account already exists with the same e-mail address",
|
||||||
|
{ provider: account.provider }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ export async function getAuthorizationUrl(
|
|||||||
let url = provider.authorization?.url
|
let url = provider.authorization?.url
|
||||||
let as: o.AuthorizationServer | undefined
|
let as: o.AuthorizationServer | undefined
|
||||||
|
|
||||||
// Falls back to authjs.dev if the user only passed params
|
if (!url) {
|
||||||
if (!url || url.host === "authjs.dev") {
|
|
||||||
// If url is undefined, we assume that issuer is always defined
|
// If url is undefined, we assume that issuer is always defined
|
||||||
// We check this in assert.ts
|
// We check this in assert.ts
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
@@ -49,9 +48,9 @@ export async function getAuthorizationUrl(
|
|||||||
redirect_uri: provider.callbackUrl,
|
redirect_uri: provider.callbackUrl,
|
||||||
// @ts-expect-error TODO:
|
// @ts-expect-error TODO:
|
||||||
...provider.authorization?.params,
|
...provider.authorization?.params,
|
||||||
},
|
}, // Defaults
|
||||||
Object.fromEntries(provider.authorization?.url.searchParams ?? []),
|
Object.fromEntries(authParams), // From provider config
|
||||||
query
|
query // From `signIn` call
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const k in params) authParams.set(k, params[k])
|
for (const k in params) authParams.set(k, params[k])
|
||||||
|
|||||||
@@ -31,12 +31,7 @@ export async function handleOAuth(
|
|||||||
const { logger, provider } = options
|
const { logger, provider } = options
|
||||||
let as: o.AuthorizationServer
|
let as: o.AuthorizationServer
|
||||||
|
|
||||||
const { token, userinfo } = provider
|
if (!provider.token?.url && !provider.userinfo?.url) {
|
||||||
// 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")
|
|
||||||
) {
|
|
||||||
// We assume that issuer is always defined as this has been asserted earlier
|
// We assume that issuer is always defined as this has been asserted earlier
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const issuer = new URL(provider.issuer!)
|
const issuer = new URL(provider.issuer!)
|
||||||
@@ -59,9 +54,9 @@ export async function handleOAuth(
|
|||||||
as = discoveredAs
|
as = discoveredAs
|
||||||
} else {
|
} else {
|
||||||
as = {
|
as = {
|
||||||
issuer: provider.issuer ?? "https://authjs.dev", // TODO: review fallback issuer
|
issuer: provider.issuer ?? "https://a", // TODO: review fallback issuer
|
||||||
token_endpoint: token?.url.toString(),
|
token_endpoint: provider.token?.url.toString(),
|
||||||
userinfo_endpoint: userinfo?.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")
|
throw new Error("TODO: Handle OAuth 2.0 response body error")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userinfo?.request) {
|
if (provider.userinfo?.request) {
|
||||||
profile = await userinfo.request({ tokens, provider })
|
profile = await provider.userinfo.request({ tokens, provider })
|
||||||
} else if (userinfo?.url) {
|
} else if (provider.userinfo?.url) {
|
||||||
const userinfoResponse = await o.userInfoRequest(
|
const userinfoResponse = await o.userInfoRequest(
|
||||||
as,
|
as,
|
||||||
client,
|
client,
|
||||||
|
|||||||
@@ -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(
|
function normalizeOAuth(
|
||||||
c: OAuthConfig<any> | OAuthUserConfig<any>
|
c?: OAuthConfig<any> | OAuthUserConfig<any>
|
||||||
): OAuthConfigInternal<any> | {} {
|
): OAuthConfigInternal<any> | {} {
|
||||||
|
if (!c) return {}
|
||||||
|
|
||||||
if (c.issuer) c.wellKnown ??= `${c.issuer}/.well-known/openid-configuration`
|
if (c.issuer) c.wellKnown ??= `${c.issuer}/.well-known/openid-configuration`
|
||||||
|
|
||||||
const authorization = normalizeEndpoint(c.authorization, c.issuer)
|
const authorization = normalizeEndpoint(c.authorization, c.issuer)
|
||||||
@@ -84,18 +84,18 @@ function normalizeEndpoint(
|
|||||||
e?: OAuthConfig<any>[OAuthEndpointType],
|
e?: OAuthConfig<any>[OAuthEndpointType],
|
||||||
issuer?: string
|
issuer?: string
|
||||||
): OAuthConfigInternal<any>[OAuthEndpointType] {
|
): OAuthConfigInternal<any>[OAuthEndpointType] {
|
||||||
if (!e && issuer) return
|
if (!e || issuer) return
|
||||||
if (typeof e === "string") {
|
if (typeof e === "string") {
|
||||||
return { url: new URL(e) }
|
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.
|
// assumes that we will use the issuer endpoint.
|
||||||
// The existence of either e.url or provider.issuer is checked in
|
// The existence of either v.url or provider.issuer is checked in
|
||||||
// assert.ts. We fallback to "https://authjs.dev" to be able to pass around
|
// assert.ts
|
||||||
// a valid URL even if the user only provided params.
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
// NOTE: This need to be checked when constructing the URL
|
const url = new URL(e.url!)
|
||||||
// for the authorization, token and userinfo endpoints.
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||||
const url = new URL(e?.url ?? "https://authjs.dev")
|
for (const k in e.params) url.searchParams.set(k, e.params[k] as any)
|
||||||
for (const k in e?.params) url.searchParams.set(k, e?.params[k])
|
|
||||||
return { url, request: e?.request }
|
return { ...e, url }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { handleLogin } from "../callback-handler.js"
|
import { handleLogin } from "../callback-handler.js"
|
||||||
import { CallbackRouteError } from "../../errors.js"
|
import { CallbackRouteError, Verification } from "../../errors.js"
|
||||||
import { handleOAuth } from "../oauth/callback.js"
|
import { handleOAuth } from "../oauth/callback.js"
|
||||||
import { createHash } from "../web.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 { AdapterSession } from "../../adapters.js"
|
||||||
import type {
|
import type {
|
||||||
RequestInternal,
|
RequestInternal,
|
||||||
ResponseInternal,
|
ResponseInternal,
|
||||||
User,
|
|
||||||
InternalOptions,
|
InternalOptions,
|
||||||
Account,
|
|
||||||
} from "../../types.js"
|
} from "../../types.js"
|
||||||
import type { Cookie, SessionStore } from "../cookie.js"
|
import type { Cookie, SessionStore } from "../cookie.js"
|
||||||
|
|
||||||
@@ -155,9 +153,13 @@ export async function callback(params: {
|
|||||||
const token = query?.token as string | undefined
|
const token = query?.token as string | undefined
|
||||||
const identifier = query?.email as string | undefined
|
const identifier = query?.email as string | undefined
|
||||||
|
|
||||||
// If these are missing, the sign-in URL was manually opened without these params or the `sendVerificationRequest` method did not send the link correctly in the email.
|
|
||||||
if (!token || !identifier) {
|
if (!token || !identifier) {
|
||||||
return { redirect: `${url}/error?error=configuration`, cookies }
|
const e = new TypeError(
|
||||||
|
"Missing token or email. The sign-in URL was manually opened without token/identifier or the link was not sent correctly in the email.",
|
||||||
|
{ cause: { hasToken: !!token, hasEmail: !!identifier } }
|
||||||
|
)
|
||||||
|
e.name = "Configuration"
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = provider.secret ?? options.secret
|
const secret = provider.secret ?? options.secret
|
||||||
@@ -167,46 +169,46 @@ export async function callback(params: {
|
|||||||
token: await createHash(`${token}${secret}`),
|
token: await createHash(`${token}${secret}`),
|
||||||
})
|
})
|
||||||
|
|
||||||
const invalidInvite = !invite || invite.expires.valueOf() < Date.now()
|
const hasInvite = !!invite
|
||||||
if (invalidInvite) {
|
const expired = invite ? invite.expires.valueOf() < Date.now() : undefined
|
||||||
return { redirect: `${url}/error?error=Verification`, cookies }
|
const invalidInvite = !hasInvite || expired
|
||||||
}
|
if (invalidInvite) throw new Verification({ hasInvite, expired })
|
||||||
|
|
||||||
// @ts-expect-error -- Verified in `assertConfig`.
|
// @ts-expect-error -- Verified in `assertConfig`.
|
||||||
const user = await getAdapterUserFromEmail(identifier, adapter)
|
const profile = await getAdapterUserFromEmail(identifier, adapter)
|
||||||
|
|
||||||
const account: Account = {
|
const account = {
|
||||||
providerAccountId: user.email,
|
providerAccountId: profile.email,
|
||||||
userId: user.id,
|
|
||||||
type: "email" as const,
|
type: "email" as const,
|
||||||
provider: provider.id,
|
provider: provider.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is allowed to sign in
|
// Check if user is allowed to sign in
|
||||||
const unauthorizedOrError = await handleAuthorized(
|
const unauthorizedOrError = await handleAuthorized(
|
||||||
{ user, account },
|
{ user: profile, account },
|
||||||
options
|
options
|
||||||
)
|
)
|
||||||
|
|
||||||
if (unauthorizedOrError) return { ...unauthorizedOrError, cookies }
|
if (unauthorizedOrError) return { ...unauthorizedOrError, cookies }
|
||||||
|
|
||||||
// Sign user in
|
// Sign user in
|
||||||
const {
|
const { user, session, isNewUser } = await handleLogin(
|
||||||
user: loggedInUser,
|
sessionStore.value,
|
||||||
session,
|
profile,
|
||||||
isNewUser,
|
account,
|
||||||
} = await handleLogin(sessionStore.value, user, account, options)
|
options
|
||||||
|
)
|
||||||
|
|
||||||
if (useJwtSession) {
|
if (useJwtSession) {
|
||||||
const defaultToken = {
|
const defaultToken = {
|
||||||
name: loggedInUser.name,
|
name: user.name,
|
||||||
email: loggedInUser.email,
|
email: user.email,
|
||||||
picture: loggedInUser.image,
|
picture: user.image,
|
||||||
sub: loggedInUser.id?.toString(),
|
sub: user.id?.toString(),
|
||||||
}
|
}
|
||||||
const token = await callbacks.jwt({
|
const token = await callbacks.jwt({
|
||||||
token: defaultToken,
|
token: defaultToken,
|
||||||
user: loggedInUser,
|
user,
|
||||||
account,
|
account,
|
||||||
isNewUser,
|
isNewUser,
|
||||||
})
|
})
|
||||||
@@ -234,7 +236,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
|
// Handle first logins on new accounts
|
||||||
// e.g. option to send users to a new account landing page on initial login
|
// e.g. option to send users to a new account landing page on initial login
|
||||||
@@ -253,33 +255,22 @@ export async function callback(params: {
|
|||||||
} else if (provider.type === "credentials" && method === "POST") {
|
} else if (provider.type === "credentials" && method === "POST") {
|
||||||
const credentials = body
|
const credentials = body
|
||||||
|
|
||||||
let user: User | null
|
// TODO: Forward the original request as is, instead of reconstructing it
|
||||||
|
Object.entries(query ?? {}).forEach(([k, v]) =>
|
||||||
try {
|
url.searchParams.set(k, v)
|
||||||
// TODO: Forward the original request as is, instead of reconstructing it
|
)
|
||||||
|
const user = await provider.authorize(
|
||||||
|
credentials,
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
Object.entries(query ?? {}).forEach(([k, v]) => url.searchParams.set(k, v))
|
new Request(url, { headers, method, body: JSON.stringify(body) })
|
||||||
user = await provider.authorize(
|
)
|
||||||
credentials,
|
if (!user) {
|
||||||
// prettier-ignore
|
|
||||||
new Request(url, { headers, method, body: JSON.stringify(body) })
|
|
||||||
)
|
|
||||||
if (!user) {
|
|
||||||
return {
|
|
||||||
status: 401,
|
|
||||||
redirect: `${url}/error?${new URLSearchParams({
|
|
||||||
error: "CredentialsSignin",
|
|
||||||
provider: provider.id,
|
|
||||||
})}`,
|
|
||||||
cookies,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
return {
|
||||||
status: 401,
|
status: 401,
|
||||||
redirect: `${url}/error?error=${encodeURIComponent(
|
redirect: `${url}/error?${new URLSearchParams({
|
||||||
(e as Error).message
|
error: "CredentialsSignin",
|
||||||
)}`,
|
provider: provider.id,
|
||||||
|
})}`,
|
||||||
cookies,
|
cookies,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export async function signin(
|
|||||||
|
|
||||||
const account: Account = {
|
const account: Account = {
|
||||||
providerAccountId: email,
|
providerAccountId: email,
|
||||||
userId: user.id,
|
userId: email,
|
||||||
type: "email",
|
type: "email",
|
||||||
provider: provider.id,
|
provider: provider.id,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,21 @@ const reset = "\x1b[0m"
|
|||||||
export const logger: LoggerInstance = {
|
export const logger: LoggerInstance = {
|
||||||
error(error: AuthError) {
|
error(error: AuthError) {
|
||||||
const url = `https://errors.authjs.dev#${error.name.toLowerCase()}`
|
const url = `https://errors.authjs.dev#${error.name.toLowerCase()}`
|
||||||
console.error(error.stack)
|
|
||||||
console.error(
|
console.error(
|
||||||
`${red}[auth][error][${error.name}]${reset}: Read more at ${url}`
|
`${red}[auth][error][${error.name}]${reset}:${
|
||||||
|
error.message ? ` ${error.message}.` : ""
|
||||||
|
} Read more at ${url}`
|
||||||
)
|
)
|
||||||
error.metadata && console.error(JSON.stringify(error.metadata, null, 2))
|
if (error.cause) {
|
||||||
|
const { err, ...data } = error.cause as any
|
||||||
|
console.error(`${red}[auth][cause]${reset}:`, (err as Error).stack)
|
||||||
|
console.error(
|
||||||
|
`${red}[auth][details]${reset}:`,
|
||||||
|
JSON.stringify(data, null, 2)
|
||||||
|
)
|
||||||
|
} else if (error.stack) {
|
||||||
|
console.error(error.stack.replace(/.*/, "").substring(1))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
warn(code) {
|
warn(code) {
|
||||||
const url = `https://errors.authjs.dev#${code}`
|
const url = `https://errors.authjs.dev#${code}`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@auth/sveltekit",
|
"name": "@auth/sveltekit",
|
||||||
"version": "0.1.12",
|
"version": "0.1.11",
|
||||||
"description": "Authentication for SvelteKit.",
|
"description": "Authentication for SvelteKit.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"authentication",
|
"authentication",
|
||||||
|
|||||||
Reference in New Issue
Block a user