Compare commits

..

4 Commits

Author SHA1 Message Date
Balázs Orbán
f42ef7cc62 apply suggestion 2023-01-06 18:44:48 +01:00
Balázs Orbán
35a708a2c3 remove prettier-ignore 2023-01-03 07:36:59 +01:00
Balázs Orbán
4f919ca76f fix error name 2023-01-03 07:35:46 +01:00
Balázs Orbán
036e34b4b6 fix(core): improve stack traces 2023-01-03 07:24:12 +01:00
15 changed files with 240 additions and 279 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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:*"
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

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( 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 }
} }

View File

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

View File

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

View File

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

View File

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