Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added signinInfo when signing in #7234

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/playgrounds/nuxt/lib/auth/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function signIn<
options?: SignInOptions,
authorizationParams?: SignInAuthorizationParams
) {
const { callbackUrl = window.location.href, redirect = true } = options ?? {}
const { callbackUrl = window.location.href, redirect = true, signinInfo } = options ?? {}

// TODO: Support custom providers
const isCredentials = providerId === "credentials"
Expand Down Expand Up @@ -58,6 +58,7 @@ export async function signIn<
...options,
csrfToken,
callbackUrl,
signinInfo: signinInfo === undefined ? undefined : encodeURIComponent(signinInfo)
}),
})

Expand Down
4 changes: 4 additions & 0 deletions apps/playgrounds/nuxt/lib/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export interface SignInOptions extends Record<string, unknown> {
callbackUrl?: string
/** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option) */
redirect?: boolean
/**
* Information that will be passed to the `signIn` callback, the `createUser` adapter function, and the `createUser` event.
*/
signinInfo?: string
}
export interface SignInResponse {
error: string | undefined
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/getting-started/upgrade-to-v4.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ The signatures for the callback methods now look like this:

```diff
- signIn(user, account, profileOrEmailOrCredentials)
+ signIn({ user, account, profile, email, credentials })
+ signIn({ user, account, profile, email, credentials, signinInfo })
```

```diff
Expand Down
6 changes: 3 additions & 3 deletions docs/docs/guides/basics/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ If you want to pass data such as an Access Token or User ID to the browser when

You can specify a handler for any of the callbacks below.

```js title="pages/api/auth/[...nextauth].js"s
```js title="pages/api/auth/[...nextauth].js"
callbacks: {
async signIn({ user, account, profile, email, credentials }) {
async signIn({ user, account, profile, email, credentials, signinInfo }) {
return true
},
async redirect({ url, baseUrl }) {
Expand All @@ -37,7 +37,7 @@ Use the `signIn()` callback to control if a user is allowed to sign in.

```js title="pages/api/auth/[...nextauth].js"
callbacks: {
async signIn({ user, account, profile, email, credentials }) {
async signIn({ user, account, profile, email, credentials, signinInfo }) {
const isAllowedToSignIn = true
if (isAllowedToSignIn) {
return true
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/guides/basics/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The message object will contain one of these depending on if you use JWT or data

Sent when the adapter is told to create a new user.

The message object will contain the user.
The message object will contain the user and `signinInfo`.

### updateUser

Expand Down
11 changes: 11 additions & 0 deletions docs/docs/reference/nextjs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,14 @@ title: Client
:::warning WIP
`@auth/nextjs/client` is work in progress. For now, please use [NextAuth.js Client API](https://next-auth.js.org/getting-started/client).
:::

### Using the `signinInfo` option

A string may be passed to the `signinInfo` parameter of the client's `signIn()` method. That string will be available in the `signIn` callback, the `createUser` adapter function, and the `createUser` event. This may be helpful in determining if a user can login (e.g. users must enter an invitation code or beta key) or in creating a user (e.g. the primary key is a user-supplied "DisplayName" - you'll likely need to write your own adapter).

`signinInfo` must be a string. You are in charge of de/serialization.

Example usage:

- `signIn("google", { signinInfo: "Bill Johnson" })`

10 changes: 10 additions & 0 deletions docs/docs/reference/solidstart/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ signIn()
signIn("provider") // example: signIn("github")
```

### Using the `signinInfo` option

A string may be passed to the `signinInfo` parameter of the client's `signIn()` method. That string will be available in the `signIn` callback, the `createUser` adapter function, and the `createUser` event. This may be helpful in determining if a user can login (e.g. users must enter an invitation code or beta key) or in creating a user (e.g. the primary key is a user-supplied "DisplayName" - you'll likely need to write your own adapter).

`signinInfo` must be a string. You are in charge of de/serialization.

Example usage:

- `signIn("google", { signinInfo: "Bill Johnson" })`

## Signing out

```ts
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
*/

import { ProviderType } from "./providers/index.js"
import type { Account, Awaitable, User } from "./types.js"
import type { Account, Awaitable, SigninInfo, User } from "./types.js"
// TODO: Discuss if we should expose methods to serialize and deserialize
// the data? Many adapters share this logic, so it could be useful to
// have a common implementation.
Expand Down Expand Up @@ -216,7 +216,7 @@ export interface VerificationToken {
* :::
*/
export interface Adapter {
createUser?(user: Omit<AdapterUser, "id">): Awaitable<AdapterUser>
createUser?(user: Omit<AdapterUser, "id">, info?: { signinInfo?: SigninInfo }): Awaitable<AdapterUser>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I add it as an optional param so it may be extended in the future with other data. Also shouldn't break exiting adapters.

getUser?(id: string): Awaitable<AdapterUser | null>
getUserByEmail?(email: string): Awaitable<AdapterUser | null>
/** Using the provider id and the id of the user for a specific account, get the user. */
Expand Down
13 changes: 7 additions & 6 deletions packages/core/src/lib/callback-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
AdapterSession,
AdapterUser,
} from "../adapters.js"
import type { Account, InternalOptions, User } from "../types.js"
import type { Account, SigninInfo, InternalOptions, User } from "../types.js"
import type { JWT } from "../jwt.js"
import type { OAuthConfig } from "../providers/index.js"
import type { SessionToken } from "./cookie.js"
Expand All @@ -27,7 +27,8 @@ export async function handleLogin(
sessionToken: SessionToken,
_profile: User | AdapterUser | { email: string },
_account: AdapterAccount | Account | null,
options: InternalOptions
options: InternalOptions,
signinInfo: SigninInfo | undefined
) {
// Input validation
if (!_account?.providerAccountId || !_account.type)
Expand Down Expand Up @@ -107,8 +108,8 @@ export async function handleLogin(
} else {
const { id: _, ...newUser } = { ...profile, emailVerified: new Date() }
// Create user account if there isn't one for the email address already
user = await createUser(newUser)
await events.createUser?.({ user })
user = await createUser(newUser, { signinInfo })
await events.createUser?.({ user, signinInfo })
isNewUser = true
}

Expand Down Expand Up @@ -211,9 +212,9 @@ export async function handleLogin(
// create a new account for the user, link it to the OAuth account and
// create a new session for them so they are signed in with it.
const { id: _, ...newUser } = { ...profile, emailVerified: null }
user = await createUser(newUser)
user = await createUser(newUser, { signinInfo })
}
await events.createUser?.({ user })
await events.createUser?.({ user, signinInfo })

await linkAccount({ ...account, userId: user.id })
await events.linkAccount?.({ user, account, profile })
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/lib/cookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
maxAge: 60 * 15, // 15 minutes in seconds
},
},
signinInfo: {
name: `${cookiePrefix}next-auth.signin-info`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
nonce: {
name: `${cookiePrefix}next-auth.nonce`,
options: {
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type {
ResponseInternal,
} from "../types.js"

const SIGNIN_INFO_MAX_AGE = 60 * 15 // 15 minutes in seconds

/** @internal */
export async function AuthInternal<
Body extends string | Record<string, any> | any[]
Expand Down Expand Up @@ -139,6 +141,18 @@ export async function AuthInternal<
} else {
switch (action) {
case "signin":
if (request.body?.signinInfo !== undefined) {
const expires = new Date()
expires.setTime(expires.getTime() + SIGNIN_INFO_MAX_AGE * 1000)
cookies.push({
name: options.cookies.signinInfo.name,
value: request.body.signinInfo,
options: {
...options.cookies.signinInfo.options,
expires,
},
})
}
Comment on lines +144 to +155
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely no idea if this is the right place to put it... but it works!

if ((csrfDisabled || options.csrfTokenVerified) && options.provider) {
const signin = await routes.signin(
request.query,
Expand Down
33 changes: 23 additions & 10 deletions packages/core/src/lib/routes/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
ResponseInternal,
InternalOptions,
Account,
SigninInfo,
} from "../../types.js"
import type { Cookie, SessionStore } from "../cookie.js"

Expand Down Expand Up @@ -41,6 +42,20 @@ export async function callback(params: {

const useJwtSession = sessionStrategy === "jwt"

let signinInfo: undefined | SigninInfo = undefined
const encodedSigninInfo = params.cookies?.[options.cookies.signinInfo.name]
if (encodedSigninInfo !== undefined) {
signinInfo = decodeURIComponent(encodedSigninInfo)
// clear signinInfo cookie
cookies.push({
name: options.cookies.signinInfo.name,
value: "",
options: {
...options.cookies.signinInfo.options,
maxAge: 0,
},
})
}
try {
if (provider.type === "oauth" || provider.type === "oidc") {
const authorizationResult = await handleOAuth(
Expand Down Expand Up @@ -82,7 +97,7 @@ export async function callback(params: {
}

const unauthorizedOrError = await handleAuthorized(
{ user: userOrProfile, account, profile: OAuthProfile },
{ user: userOrProfile, account, profile: OAuthProfile, signinInfo },
options
)

Expand All @@ -93,7 +108,8 @@ export async function callback(params: {
sessionStore.value,
profile,
account,
options
options,
signinInfo
)

if (useJwtSession) {
Expand Down Expand Up @@ -194,7 +210,7 @@ export async function callback(params: {

// Check if user is allowed to sign in
const unauthorizedOrError = await handleAuthorized(
{ user, account },
{ user, account, signinInfo },
options
)

Expand All @@ -205,7 +221,7 @@ export async function callback(params: {
user: loggedInUser,
session,
isNewUser,
} = await handleLogin(sessionStore.value, user, account, options)
} = await handleLogin(sessionStore.value, user, account, options, signinInfo)

if (useJwtSession) {
const defaultToken = {
Expand Down Expand Up @@ -288,15 +304,14 @@ export async function callback(params: {
}
}

/** @type {import("src").Account} */
const account = {
const account: Account = {
providerAccountId: user.id,
type: "credentials",
type: "credentials" as const,
provider: provider.id,
}

const unauthorizedOrError = await handleAuthorized(
{ user, account, credentials },
{ user, account, credentials, signinInfo },
options
)

Expand All @@ -312,7 +327,6 @@ export async function callback(params: {
const token = await callbacks.jwt({
token: defaultToken,
user,
// @ts-expect-error
account,
isNewUser: false,
})
Expand All @@ -335,7 +349,6 @@ export async function callback(params: {
cookies.push(...sessionCookies)
}

// @ts-expect-error
await events.signIn?.({ user, account })

return { redirect: callbackUrl, cookies }
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib/routes/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AuthorizedCallbackError } from "../../errors.js"
import { InternalOptions } from "../../types.js"

export async function handleAuthorized(
params: any,
params: Parameters<InternalOptions["callbacks"]["signIn"]>[0],
{ url, logger, callbacks: { signIn } }: InternalOptions
) {
try {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/lib/routes/signin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ export async function signin(
provider: provider.id,
}

const signinInfo = body?.signinInfo === undefined ? undefined : decodeURIComponent(body.signinInfo)
const unauthorizedOrError = await handleAuthorized(
{ user, account, email: { verificationRequest: true } },
{ user, account, email: { verificationRequest: true }, signinInfo },
options
)

Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ export interface CallbacksOptions<P = Profile, A = Account> {
}
/** If Credentials provider is used, it contains the user credentials */
credentials?: Record<string, CredentialInput>
/** Info from the client's `signIn()` function. */
signinInfo?: SigninInfo
}) => Awaitable<boolean>
/**
* This callback is called anytime the user is redirected to a callback URL (e.g. on signin or signout).
Expand Down Expand Up @@ -237,9 +239,12 @@ export interface CookiesOptions {
csrfToken: CookieOption
pkceCodeVerifier: CookieOption
state: CookieOption
signinInfo: CookieOption
nonce: CookieOption
}

export type SigninInfo = string
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it SigninInfo or SignInInfo 🤔🤔 Choosing the former arbitrarily because less stuttering.


/**
* The various event callbacks you can register for from next-auth
*
Expand Down Expand Up @@ -269,7 +274,7 @@ export interface EventCallbacks {
| { session: Awaited<ReturnType<Required<Adapter>["deleteSession"]>> }
| { token: Awaited<ReturnType<JWTOptions["decode"]>> }
) => Awaitable<void>
createUser: (message: { user: User }) => Awaitable<void>
createUser: (message: { user: User, signinInfo?: SigninInfo }) => Awaitable<void>
updateUser: (message: { user: User }) => Awaitable<void>
linkAccount: (message: {
user: User | AdapterUser
Expand Down
3 changes: 2 additions & 1 deletion packages/frameworks-solid-start/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function signIn<
options?: SignInOptions,
authorizationParams?: SignInAuthorizationParams
) {
const { callbackUrl = window.location.href, redirect = true } = options ?? {}
const { callbackUrl = window.location.href, redirect = true, signinInfo } = options ?? {}

// TODO: Support custom providers
const isCredentials = providerId === "credentials"
Expand Down Expand Up @@ -56,6 +56,7 @@ export async function signIn<
...options,
csrfToken,
callbackUrl,
signinInfo: signinInfo === undefined ? undefined : encodeURIComponent(signinInfo)
}),
})

Expand Down
3 changes: 2 additions & 1 deletion packages/frameworks-sveltekit/src/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function signIn<
options?: SignInOptions,
authorizationParams?: SignInAuthorizationParams
) {
const { callbackUrl = window.location.href, redirect = true } = options ?? {}
const { callbackUrl = window.location.href, redirect = true, signinInfo } = options ?? {}

// TODO: Support custom providers
const isCredentials = providerId === "credentials"
Expand Down Expand Up @@ -57,6 +57,7 @@ export async function signIn<
...options,
csrfToken,
callbackUrl,
signinInfo: signinInfo === undefined ? undefined : encodeURIComponent(signinInfo)
}),
})

Expand Down
Loading