Skip to content

Commit

Permalink
feat: add update session to core (#7505)
Browse files Browse the repository at this point in the history
* feat: add update session to core

Integrates #7056 into `@auth/core`

* resolve default user after jwt callback
  • Loading branch information
balazsorban44 authored May 10, 2023
1 parent f5de6cf commit cca94bf
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 34 deletions.
18 changes: 17 additions & 1 deletion packages/core/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export async function AuthInternal<
case "providers":
return (await routes.providers(options.providers)) as any
case "session": {
const session = await routes.session(sessionStore, options)
const session = await routes.session({ sessionStore, options })
if (session.cookies) cookies.push(...session.cookies)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
return { ...session, cookies } as any
Expand Down Expand Up @@ -177,6 +177,22 @@ export async function AuthInternal<
return { ...callback, cookies }
}
break
case "session": {
if (options.csrfTokenVerified) {
const session = await routes.session({
options,
sessionStore,
newSession: request.body?.data,
isUpdate: true,
})
if (session.cookies) cookies.push(...session.cookies)
return { ...session, cookies } as any
}

// If CSRF token is invalid, return a 400 status code
// we should not redirect to a page as this is an API route
return { status: 400, cookies }
}
default:
}
}
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/lib/routes/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export async function callback(params: {
account,
profile: OAuthProfile,
isNewUser,
trigger: isNewUser ? "signUp" : "signIn",
})

// Clear cookies if token is null
Expand Down Expand Up @@ -244,6 +245,7 @@ export async function callback(params: {
user: loggedInUser,
account,
isNewUser,
trigger: isNewUser ? "signUp" : "signIn",
})

// Clear cookies if token is null
Expand Down Expand Up @@ -340,6 +342,7 @@ export async function callback(params: {
// @ts-expect-error
account,
isNewUser: false,
trigger: "signIn",
})

// Clear cookies if token is null
Expand Down
29 changes: 19 additions & 10 deletions packages/core/src/lib/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import type { InternalOptions, ResponseInternal, Session } from "../../types.js"
import type { SessionStore } from "../cookie.js"

/** Return a session object filtered via `callbacks.session` */
export async function session(
sessionStore: SessionStore,
export async function session(params: {
options: InternalOptions
): Promise<ResponseInternal<Session | null>> {
sessionStore: SessionStore
isUpdate?: boolean
newSession?: any
}): Promise<ResponseInternal<Session | null>> {
const { options, sessionStore, newSession, isUpdate } = params
const {
adapter,
jwt,
Expand All @@ -33,22 +36,26 @@ export async function session(
try {
const decodedToken = await jwt.decode({ ...jwt, token: sessionToken })

const token = await callbacks.jwt({
// @ts-expect-error
token: decodedToken,
...(isUpdate && { trigger: "update" }),
session: newSession,
})

const newExpires = fromDate(sessionMaxAge)

// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as...").
const session = {
user: {
name: decodedToken?.name,
email: decodedToken?.email,
image: decodedToken?.picture,
name: token?.name,
email: token?.email,
image: token?.picture,
},
expires: newExpires.toISOString(),
}

// @ts-expect-error
const token = await callbacks.jwt({ token: decodedToken })

if (token !== null) {
// @ts-expect-error
const newSession = await callbacks.session({ session, token })
Expand Down Expand Up @@ -128,11 +135,13 @@ export async function session(
user: {
name: user.name,
email: user.email,
image: user.image,
picture: user.image,
},
expires: session.expires.toISOString(),
},
user,
newSession,
...(isUpdate ? { trigger: "update" } : {}),
})

// Return session payload as response
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/providers/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ export interface CredentialsConfig<
* @example
* ```ts
* //...
* async authorize(, request) {
* async authorize(credentials, request) {
* if(!isValidCredentials(credentials)) return null
* const response = await fetch(request)
* if(!response.ok) return null
* return await response.json() ?? null
* }
* //...
* ```
*/
authorize: (
/**
Expand Down
92 changes: 70 additions & 22 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,40 +238,86 @@ export interface CallbacksOptions<P = Profile, A = Account> {
* If you want to make something available you added to the token through the `jwt` callback,
* you have to explicitly forward it here to make it available to the client.
*
* [Documentation](https://authjs.dev/guides/basics/callbacks#session-callback) |
* [`jwt` callback](https://authjs.dev/guides/basics/callbacks#jwt-callback) |
* [`useSession`](https://authjs.dev/reference/react/#usesession) |
* [`getSession`](https://authjs.dev/reference/utilities/#getsession) |
*
* @see [`jwt` callback](https://authjs.dev/reference/core/types#jwt)
*/
session: (params: {
session: Session
user: User | AdapterUser
token: JWT
}) => Awaitable<Session>
session: (
params:
| {
session: Session
/** Available when {@link AuthConfig.session} is set to `strategy: "jwt"` */
token: JWT
/** Available when {@link AuthConfig.session} is set to `strategy: "database"`. */
user: AdapterUser
} & {
/**
* Available when using {@link AuthConfig.session} `strategy: "database"` and an update is triggered for the session.
*
* :::note
* You should validate this data before using it.
* :::
*/
newSession: any
trigger: "update"
}
) => Awaitable<Session | DefaultSession>
/**
* This callback is called whenever a JSON Web Token is created (i.e. at sign in)
* or updated (i.e whenever a session is accessed in the client).
* Its content is forwarded to the `session` callback,
* where you can control what should be returned to the client.
* Anything else will be kept inaccessible from the client.
* Anything else will be kept from your front-end.
*
* Returning `null` will invalidate the JWT session by clearing
* the user's cookies. You'll still have to monitor and invalidate
* unexpired tokens from future requests yourself to prevent
* unauthorized access.
* The JWT is encrypted by default.
*
* By default the JWT is encrypted.
*
* [Documentation](https://authjs.dev/guides/basics/callbacks#jwt-callback) |
* [`session` callback](https://authjs.dev/guides/basics/callbacks#session-callback)
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
*/
jwt: (params: {
/**
* When `trigger` is `"signIn"` or `"signUp"`, it will be a subset of {@link JWT},
* `name`, `email` and `image` will be included.
*
* Otherwise, it will be the full {@link JWT} for subsequent calls.
*/
token: JWT
user?: User | AdapterUser
account?: A | null
/**
* Either the result of the {@link OAuthConfig.profile} or the {@link CredentialsConfig.authorize} callback.
* @note available when `trigger` is `"signIn"` or `"signUp"`.
*
* Resources:
* - [Credentials Provider](https://authjs.dev/reference/core/providers_credentials)
* - [User database model](https://authjs.dev/reference/adapters#user)
*/
user: User | AdapterUser
/**
* Contains information about the provider that was used to sign in.
* Also includes {@link TokenSet}
* @note available when `trigger` is `"signIn"` or `"signUp"`
*/
account: A | null
/**
* The OAuth profile returned from your provider.
* (In case of OIDC it will be the decoded ID Token or /userinfo response)
* @note available when `trigger` is `"signIn"`.
*/
profile?: P
/**
* Check why was the jwt callback invoked. Possible reasons are:
* - user sign-in: First time the callback is invoked, `user`, `profile` and `account` will be present.
* - user sign-up: a user is created for the first time in the database (when {@link AuthConfig.session}.strategy is set to `"database"`)
* - update event: Triggered by the [`useSession().update`](https://next-auth.js.org/getting-started/client#update-session) method.
* In case of the latter, `trigger` will be `undefined`.
*/
trigger?: "signIn" | "signUp" | "update"
/** @deprecated use `trigger === "signUp"` instead */
isNewUser?: boolean
/**
* When using {@link AuthConfig.session} `strategy: "jwt"`, this is the data
* sent from the client via the [`useSession().update`](https://next-auth.js.org/getting-started/client#update-session) method.
*
* ⚠ Note, you should validate this data before using it.
*/
session?: any
}) => Awaitable<JWT | null>
}

Expand Down Expand Up @@ -451,7 +497,9 @@ export type InternalProvider<T = ProviderType> = (T extends "oauth"
* :::
* - **`"error"`**: Renders the built-in error page.
* - **`"providers"`**: Returns a client-safe list of all configured providers.
* - **`"session"`**: Returns the user's session if it exists, otherwise `null`.
* - **`"session"`**:
* - **`GET**`: Returns the user's session if it exists, otherwise `null`.
* - **`POST**`: Updates the user's session and returns the updated session.
* - **`"signin"`**:
* - **`GET`**: Renders the built-in sign-in page.
* - **`POST`**: Initiates the sign-in flow.
Expand Down

0 comments on commit cca94bf

Please sign in to comment.