feat(sso): support disabling setting email verified from a provider (#3551)

* feat: support disabling setting email verified

* Update docs/content/docs/plugins/sso.mdx

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix: update account handling in SSO to support trusted providers

* default to not setting email verified

* docs: update documentation

* add attribute map

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
Bereket Engida
2025-07-22 18:19:25 -07:00
committed by GitHub
parent 8fbe8f03a5
commit dea6419e09
2 changed files with 68 additions and 3 deletions

View File

@@ -1,5 +1,6 @@
import {
generateState,
type Account,
type BetterAuthPlugin,
type OAuth2Tokens,
type Session,
@@ -148,6 +149,11 @@ export interface SSOOptions {
* @default 10
*/
providersLimit?: number | ((user: User) => Promise<number> | number);
/**
* Trust the email verified flag from the provider.
* @default false
*/
trustEmailVerified?: boolean;
}
export const sso = (options?: SSOOptions) => {
@@ -1093,7 +1099,9 @@ export const sso = (options?: SSOOptions) => {
),
id: idToken[mapping.id || "sub"],
email: idToken[mapping.email || "email"],
emailVerified: idToken[mapping.emailVerified || "email_verified"],
emailVerified: options?.trustEmailVerified
? idToken[mapping.emailVerified || "email_verified"]
: false,
name: idToken[mapping.name || "name"],
image: idToken[mapping.image || "picture"],
} as {
@@ -1149,7 +1157,9 @@ export const sso = (options?: SSOOptions) => {
name: userInfo.name || userInfo.email,
id: userInfo.id,
image: userInfo.image,
emailVerified: userInfo.emailVerified || false,
emailVerified: options?.trustEmailVerified
? userInfo.emailVerified || false
: false,
},
account: {
idToken: tokenResponse.idToken,
@@ -1325,6 +1335,9 @@ export const sso = (options?: SSOOptions) => {
.filter(Boolean)
.join(" ") || parsedResponse.extract.attributes?.displayName,
attributes: parsedResponse.extract.attributes,
emailVerified: options?.trustEmailVerified
? ((attributes?.[mapping.emailVerified] || false) as boolean)
: false,
};
let user: User;
@@ -1340,6 +1353,37 @@ export const sso = (options?: SSOOptions) => {
});
if (existingUser) {
const accounts = await ctx.context.adapter.findOne<Account>({
model: "account",
where: [
{ field: "userId", value: existingUser.id },
{ field: "providerId", value: provider.providerId },
{ field: "accountId", value: userInfo.id },
],
});
if (!accounts) {
const isTrustedProvider =
ctx.context.options.account?.accountLinking?.trustedProviders?.includes(
provider.providerId,
);
if (!isTrustedProvider) {
throw ctx.redirect(
`${parsedSamlConfig.callbackUrl}?error=account_not_found`,
);
}
await ctx.context.adapter.create<Account>({
model: "account",
data: {
userId: existingUser.id,
providerId: provider.providerId,
accountId: userInfo.id,
createdAt: new Date(),
updatedAt: new Date(),
accessToken: "",
refreshToken: "",
},
});
}
user = existingUser;
} else {
user = await ctx.context.adapter.create({
@@ -1347,7 +1391,26 @@ export const sso = (options?: SSOOptions) => {
data: {
email: userInfo.email,
name: userInfo.name,
emailVerified: true,
emailVerified: options?.trustEmailVerified
? userInfo.emailVerified || false
: false,
createdAt: new Date(),
updatedAt: new Date(),
},
});
await ctx.context.adapter.create<Account>({
model: "account",
data: {
userId: user.id,
providerId: provider.providerId,
accountId: userInfo.id,
accessToken: "",
refreshToken: "",
accessTokenExpiresAt: new Date(),
refreshTokenExpiresAt: new Date(),
scope: "",
createdAt: new Date(),
updatedAt: new Date(),
},
});
}