Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c2eb9b3
fix(docs): fix default `maxAge` formula (#7406)
vixeven Apr 30, 2023
d739e8e
feat(adapters): add Account mapping before database write (#7369)
balazsorban44 Apr 30, 2023
5400645
chore: improve errors, add more docs (#7415)
balazsorban44 May 1, 2023
542c35d
fix: loosen profile types
balazsorban44 May 1, 2023
62e2ad1
chore: type fixes
balazsorban44 May 1, 2023
92a0fc4
fix: allow handling OAuth callback error response
balazsorban44 May 1, 2023
e3bdb38
fix(docs): remove extra heading
balazsorban44 May 3, 2023
6f5a503
chore: use `@ts-ignore`
balazsorban44 May 4, 2023
d6bc65f
chore: support release any package as experimental
balazsorban44 May 4, 2023
99247ce
chore: separate manual release job
balazsorban44 May 4, 2023
e0b5f18
chore: skip test for manual release
balazsorban44 May 4, 2023
eaf5080
chore: tweak
balazsorban44 May 4, 2023
8f416b6
chore: tweaks
balazsorban44 May 4, 2023
b96f013
chore: tweak manual release version
balazsorban44 May 5, 2023
f00ac78
chore: reduce breaking changes on Account mapping
balazsorban44 May 10, 2023
7ffd361
chore: add build to manual publish
balazsorban44 May 10, 2023
c336b10
chore: move build to root
balazsorban44 May 10, 2023
f5de6cf
docs(example): update broken link (#7504)
remirobichet May 10, 2023
cca94bf
feat: add update session to core (#7505)
balazsorban44 May 10, 2023
f62c016
chore: revert `picture` to `image`
balazsorban44 May 12, 2023
220ee41
chore: Move next.config.js file into the correct directory (#7580)
ghoshnirmalya May 18, 2023
4baf2c8
docs: Cypress.Cookies.defaults removed (#7574)
roy9495 May 18, 2023
8373bc9
feat: allow empty `account` mapper
balazsorban44 May 19, 2023
7711eb0
Merge branch 'main' of github.com:nextauthjs/next-auth
balazsorban44 May 19, 2023
527fff6
chore: Add Descope as a 🥉 bronze financial sponsor (#7615)
dorsha May 21, 2023
461b52e
chore(playgrounds): Nuxt 3.5.1 (#7626)
wobsoriano May 22, 2023
17e2c2f
docs: adapter card text color on hover when on dark mode (#7672)
anampartho May 29, 2023
a47b4ce
docs: fix info card rendering in oauth-tutorial.mdx (#7662)
grahampcharles May 29, 2023
26a1bc5
feat: introduce `@auth/prisma-adapter`
balazsorban44 Jun 1, 2023
ec933f4
chore: update lock file
balazsorban44 Jun 1, 2023
395020c
Merge branch 'main' into feat/authjs-prisma
balazsorban44 Jun 1, 2023
70f2982
update lock file
balazsorban44 Jun 1, 2023
e362e8f
update adapter test suite
balazsorban44 Jun 1, 2023
da8007f
chore: allow manual release of any `@auth/*` package
balazsorban44 Jun 1, 2023
d7b9879
revert line changes
balazsorban44 Jun 1, 2023
909956f
revert line changes
balazsorban44 Jun 1, 2023
39ffb90
revert line changes
balazsorban44 Jun 1, 2023
050e484
fix syntax error
balazsorban44 Jun 1, 2023
18b8ae2
Merge branch 'main' into feat/authjs-prisma
balazsorban44 Jun 1, 2023
210375b
chore: re-add pnpm caching
balazsorban44 Jun 1, 2023
f177471
Merge branch 'main' into feat/authjs-prisma
balazsorban44 Jun 1, 2023
137572d
Merge branch 'main' into feat/authjs-prisma
balazsorban44 Jun 1, 2023
87b2ff9
Merge branch 'main' into feat/authjs-prisma
balazsorban44 Jun 1, 2023
b3ca695
tweak turbo
balazsorban44 Jun 1, 2023
21286ea
Merge branch 'main' into feat/authjs-prisma
balazsorban44 Jun 1, 2023
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
Prev Previous commit
Next Next commit
chore: improve errors, add more docs (#7415)
* JWT Token -> JWT

* document some errors

* improve errors, docs
  • Loading branch information
balazsorban44 authored May 1, 2023
commit 5400645221de86fed01781428a8e6c8809305ec9
2 changes: 1 addition & 1 deletion docs/docs/concepts/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ Ultimately if your request is not accepted or is not actively in development, yo
</summary>
<p>

Auth.js by default uses JSON Web Tokens for saving the user's session. However, if you use a [database adapter](/guides/adapters/using-a-database-adapter), the database will be used to persist the user's session. You can force the usage of JWT when using a database [through the configuration options](/reference/configuration/auth-config#session). Since v4 all our JWT tokens are now encrypted by default with A256GCM.
Auth.js by default uses JSON Web Tokens for saving the user's session. However, if you use a [database adapter](/guides/adapters/using-a-database-adapter), the database will be used to persist the user's session. You can force the usage of JWT when using a database [through the configuration options](/reference/configuration/auth-config#session). Since v4 all our JWTs are now encrypted by default with A256GCM.

</p>
</details>
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/guides/basics/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Sent when the user signs out.

The message object will contain one of these depending on if you use JWT or database persisted sessions:

- `token`: The JWT token for this session.
- `token`: The JWT for this session.
- `session`: The session object from your adapter that is being ended

### createUser
Expand Down Expand Up @@ -60,5 +60,5 @@ Sent at the end of a request for the current session.

The message object will contain one of these depending on if you use JWT or database persisted sessions:

- `token`: The JWT token for this session.
- `token`: The JWT for this session.
- `session`: The session object from your adapter.
80 changes: 62 additions & 18 deletions packages/core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,6 @@ export class AuthError extends Error {
}
}

/**
* @todo
* Thrown when an Email address is already associated with an account
* but the user is trying an OAuth account that is not linked to it.
*/
export class AccountNotLinked extends AuthError {}

/**
* @todo
* One of the database `Adapter` methods failed.
Expand All @@ -37,8 +30,8 @@ export class AdapterError extends AuthError {}
export class AuthorizedCallbackError extends AuthError {}

/**
* There was an error while trying to finish up authenticating the user.
* Depending on the type of provider, this could be for multiple reasons.
* This error occurs when the user cannot finish the sign-in process.
* Depending on the provider type, this could have happened for multiple reasons.
*
* :::tip
* Check out `[auth][details]` in the error message to know which provider failed.
Expand All @@ -48,23 +41,23 @@ export class AuthorizedCallbackError extends AuthError {}
* ```
* :::
*
* For an **OAuth provider**, possible causes are:
* For an [OAuth provider](https://authjs.dev/reference/core/providers_oauth), 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:
* For an [Email provider](https://authjs.dev/reference/core/providers_email), 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:
* For a [Credentials provider](https://authjs.dev/reference/core/providers_credentials), 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:
Expand Down Expand Up @@ -107,31 +100,82 @@ export class MissingAPIRoute extends AuthError {}
/** @todo */
export class MissingAuthorize extends AuthError {}

/** @todo */
/**
* Auth.js requires a secret to be set, but none was not found. This is used to encrypt cookies, JWTs and other sensitive data.
*
* :::note
* If you are using a framework like Next.js, we try to automatically infer the secret from the `AUTH_SECRET` environment variable.
* Alternatively, you can also explicitly set the [`AuthConfig.secret`](https://authjs.dev/reference/core#secret).
* :::
*
*
* :::tip
* You can generate a good secret value:
* - On Unix systems: type `openssl rand -hex 32` in the terminal
* - Or generate one [online](https://generate-secret.vercel.app/32)
*
* :::
*/
export class MissingSecret extends AuthError {}

/** @todo */
export class OAuthSignInError extends AuthError {}
/**
* @todo
* Thrown when an Email address is already associated with an account
* but the user is trying an OAuth account that is not linked to it.
*/
export class OAuthAccountNotLinked extends AuthError {}

/** @todo */
export class OAuthCallbackError extends AuthError {}

/** @todo */
export class OAuthCreateUserError extends AuthError {}

/** @todo */
/**
* This error occurs during an OAuth sign in attempt when the provdier's
* response could not be parsed. This could for example happen if the provider's API
* changed, or the [`OAuth2Config.profile`](https://authjs.dev/reference/core/providers_oauth#profile) method is not implemented correctly.
*/
export class OAuthProfileParseError extends AuthError {}

/** @todo */
export class SessionTokenError extends AuthError {}

/** @todo */
/**
* This error occurs when the user cannot initiate the sign-in process.
* Depending on the provider type, this could have happened 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](https://authjs.dev/reference/core/providers_oauth), possible causes are:
* - The Authorization Server is not compliant with the [OAuth 2.0 specifcation](https://www.ietf.org/rfc/rfc6749.html)
* Check the details in the error message.
* - A runtime error occurred in Auth.js. This should be reported as a bug.
*
* For an [Email provider](https://authjs.dev/reference/core/providers_email), possible causes are:
* - The email sent from the client is invalid, could not be normalized by [`EmailConfig.normalizeIdentifier`](https://authjs.dev/reference/core/providers_email#normalizeidentifier)
* - 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.
*
*/
export class SignInError extends AuthError {}

/** @todo */
export class SignOutError extends AuthError {}

/** @todo */
/**
* Auth.js was requested to handle an operation that it does not support.
*
* See [`AuthAction`](https://authjs.dev/reference/core/types#authaction) for the supported actions.
*/
export class UnknownAction extends AuthError {}

/** @todo */
Expand Down
208 changes: 103 additions & 105 deletions packages/core/src/lib/callback-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AccountNotLinked } from "../errors.js"
import { OAuthAccountNotLinked } from "../errors.js"
import { fromDate } from "./utils/date.js"

import type {
Expand Down Expand Up @@ -122,118 +122,116 @@ export async function handleLogin(
})

return { session, user, isNewUser }
} else if (account.type === "oauth" || account.type === "oidc") {
// If signing in with OAuth account, check to see if the account exists already
const userByAccount = await getUserByAccount({
providerAccountId: account.providerAccountId,
provider: account.provider,
})
if (userByAccount) {
if (user) {
// If the user is already signed in with this account, we don't need to do anything
if (userByAccount.id === user.id) {
return { session, user, isNewUser }
}
// If the user is currently signed in, but the new account they are signing in
// with is already associated with another user, then we cannot link them
// and need to return an error.
throw new AccountNotLinked(
"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
// associated with a valid user then create session to sign the user in.
session = useJwtSession
? {}
: await createSession({
sessionToken: generateSessionToken(),
userId: userByAccount.id,
expires: fromDate(options.session.maxAge),
})

return { session, user: userByAccount, isNewUser }
} else {
const { provider: p } = options as InternalOptions<"oauth" | "oidc">
const { type, provider, providerAccountId, userId, ...tokenSet } = account
const defaults = { providerAccountId, provider, type, userId }
account = Object.assign(p.account(tokenSet), defaults)

if (user) {
// If the user is already signed in and the OAuth account isn't already associated
// with another user account then we can go ahead and link the accounts safely.
await linkAccount({ ...account, userId: user.id })
await events.linkAccount?.({ user, account, profile })

// As they are already signed in, we don't need to do anything after linking them
}

// If signing in with OAuth account, check to see if the account exists already
const userByAccount = await getUserByAccount({
providerAccountId: account.providerAccountId,
provider: account.provider,
})
if (userByAccount) {
if (user) {
// If the user is already signed in with this account, we don't need to do anything
if (userByAccount.id === user.id) {
return { session, user, isNewUser }
}
// If the user is currently signed in, but the new account they are signing in
// with is already associated with another user, then we cannot link them
// and need to return an error.
throw new OAuthAccountNotLinked(
"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
// associated with a valid user then create session to sign the user in.
session = useJwtSession
? {}
: await createSession({
sessionToken: generateSessionToken(),
userId: userByAccount.id,
expires: fromDate(options.session.maxAge),
})

// If the user is not signed in and it looks like a new OAuth account then we
// check there also isn't an user account already associated with the same
// email address as the one in the OAuth profile.
//
// This step is often overlooked in OAuth implementations, but covers the following cases:
//
// 1. It makes it harder for someone to accidentally create two accounts.
// e.g. by signin in with email, then again with an oauth account connected to the same email.
// 2. It makes it harder to hijack a user account using a 3rd party OAuth account.
// e.g. by creating an oauth account then changing the email address associated with it.
//
// It's quite common for services to automatically link accounts in this case, but it's
// better practice to require the user to sign in *then* link accounts to be sure
// someone is not exploiting a problem with a third party OAuth service.
//
// OAuth providers should require email address verification to prevent this, but in
// practice that is not always the case; this helps protect against that.
const userByEmail = profile.email
? await getUserByEmail(profile.email)
: null
if (userByEmail) {
const provider = options.provider as OAuthConfig<any>
if (provider?.allowDangerousEmailAccountLinking) {
// If you trust the oauth provider to correctly verify email addresses, you can opt-in to
// account linking even when the user is not signed-in.
user = userByEmail
} else {
// We end up here when we don't have an account with the same [provider].id *BUT*
// we do already have an account with the same email address as the one in the
// OAuth profile the user has just tried to sign in with.
//
// We don't want to have two accounts with the same email address, and we don't
// 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.
throw new AccountNotLinked(
"Another account already exists with the same e-mail address",
{ provider: account.provider }
)
}
} else {
// If the current user is not logged in and the profile isn't linked to any user
// accounts (by email or provider account id)...
//
// If no account matching the same [provider].id or .email exists, we can
// 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)
}
await events.createUser?.({ user })
return { session, user: userByAccount, isNewUser }
} else {
const { provider: p } = options as InternalOptions<"oauth" | "oidc">
const { type, provider, providerAccountId, userId, ...tokenSet } = account
const defaults = { providerAccountId, provider, type, userId }
account = Object.assign(p.account(tokenSet), defaults)

if (user) {
// If the user is already signed in and the OAuth account isn't already associated
// with another user account then we can go ahead and link the accounts safely.
await linkAccount({ ...account, userId: user.id })
await events.linkAccount?.({ user, account, profile })

session = useJwtSession
? {}
: await createSession({
sessionToken: generateSessionToken(),
userId: user.id,
expires: fromDate(options.session.maxAge),
})
// As they are already signed in, we don't need to do anything after linking them
return { session, user, isNewUser }
}

return { session, user, isNewUser: true }
// If the user is not signed in and it looks like a new OAuth account then we
// check there also isn't an user account already associated with the same
// email address as the one in the OAuth profile.
//
// This step is often overlooked in OAuth implementations, but covers the following cases:
//
// 1. It makes it harder for someone to accidentally create two accounts.
// e.g. by signin in with email, then again with an oauth account connected to the same email.
// 2. It makes it harder to hijack a user account using a 3rd party OAuth account.
// e.g. by creating an oauth account then changing the email address associated with it.
//
// It's quite common for services to automatically link accounts in this case, but it's
// better practice to require the user to sign in *then* link accounts to be sure
// someone is not exploiting a problem with a third party OAuth service.
//
// OAuth providers should require email address verification to prevent this, but in
// practice that is not always the case; this helps protect against that.
const userByEmail = profile.email
? await getUserByEmail(profile.email)
: null
if (userByEmail) {
const provider = options.provider as OAuthConfig<any>
if (provider?.allowDangerousEmailAccountLinking) {
// If you trust the oauth provider to correctly verify email addresses, you can opt-in to
// account linking even when the user is not signed-in.
user = userByEmail
} else {
// We end up here when we don't have an account with the same [provider].id *BUT*
// we do already have an account with the same email address as the one in the
// OAuth profile the user has just tried to sign in with.
//
// We don't want to have two accounts with the same email address, and we don't
// 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.
throw new OAuthAccountNotLinked(
"Another account already exists with the same e-mail address",
{ provider: account.provider }
)
}
} else {
// If the current user is not logged in and the profile isn't linked to any user
// accounts (by email or provider account id)...
//
// If no account matching the same [provider].id or .email exists, we can
// 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)
}
}
await events.createUser?.({ user })

await linkAccount({ ...account, userId: user.id })
await events.linkAccount?.({ user, account, profile })

throw new Error("Unsupported account type")
session = useJwtSession
? {}
: await createSession({
sessionToken: generateSessionToken(),
userId: user.id,
expires: fromDate(options.session.maxAge),
})

return { session, user, isNewUser: true }
}
}
Loading