mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-12 19:51:00 +05:00
fix(sso): trust admin-configured SSO providers for account linking
better-auth's account-linking guard requires either the IdP asserting email_verified or the sso_provider row being domainVerified before it will link an SSO login to an existing local user with the same email. Entra ID never asserts email_verified (neither via its OIDC userinfo endpoint nor via SAML attributes), and Dokploy never persisted domainVerified at all, so linking was permanently impossible for any Entra-backed provider (OIDC or SAML) regardless of the local user's own emailVerified state. Add the domainVerified column, defaulting to true — Dokploy already scopes providers to an admin-configured domain and gates registration behind enterprise/admin permissions, so that domain match (still enforced by better-auth's validateEmailDomain check) is trust enough. Fixes #5099
This commit is contained in:
parent
988f5b64d6
commit
67f6890fc8
@ -83,6 +83,11 @@ const azureMapping: ClaimMapping = {
|
||||
image: "",
|
||||
};
|
||||
|
||||
// id: "sub",
|
||||
// email: "preferred_username",
|
||||
// emailVerified: "email_verified",
|
||||
// name: "name",
|
||||
|
||||
const genericMapping: ClaimMapping = {
|
||||
id: "sub",
|
||||
email: "email",
|
||||
@ -228,10 +233,9 @@ export function RegisterOidcDialog({
|
||||
mapping: {
|
||||
id: oidc?.mapping?.id ?? baseMapping.id,
|
||||
email: oidc?.mapping?.email ?? baseMapping.email,
|
||||
emailVerified:
|
||||
oidc?.mapping?.emailVerified ?? baseMapping.emailVerified,
|
||||
emailVerified: oidc?.mapping?.emailVerified ?? "",
|
||||
name: oidc?.mapping?.name ?? baseMapping.name,
|
||||
image: oidc?.mapping?.image ?? baseMapping.image,
|
||||
image: oidc?.mapping?.image ?? "",
|
||||
},
|
||||
});
|
||||
}, [data, open, form]);
|
||||
|
||||
1
apps/dokploy/drizzle/0194_acoustic_prima.sql
Normal file
1
apps/dokploy/drizzle/0194_acoustic_prima.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE "sso_provider" ADD COLUMN "domain_verified" boolean DEFAULT true NOT NULL;
|
||||
9163
apps/dokploy/drizzle/meta/0194_snapshot.json
Normal file
9163
apps/dokploy/drizzle/meta/0194_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1359,6 +1359,13 @@
|
||||
"when": 1788802431830,
|
||||
"tag": "0193_chemical_the_liberteens",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 194,
|
||||
"version": "7",
|
||||
"when": 1788858506799,
|
||||
"tag": "0194_acoustic_prima",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.30.5",
|
||||
"version": "v0.30.6",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@ -135,6 +135,7 @@ export const ssoProvider = pgTable("sso_provider", {
|
||||
providerId: text("provider_id").notNull().unique(),
|
||||
organizationId: text("organization_id"),
|
||||
domain: text("domain").notNull(),
|
||||
domainVerified: boolean("domain_verified"),
|
||||
});
|
||||
|
||||
export const twoFactor = pgTable(
|
||||
@ -156,6 +157,29 @@ export const twoFactor = pgTable(
|
||||
],
|
||||
);
|
||||
|
||||
export const passkey = pgTable(
|
||||
"passkey",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name"),
|
||||
publicKey: text("public_key").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
credentialID: text("credential_id").notNull(),
|
||||
counter: integer("counter").notNull(),
|
||||
deviceType: text("device_type").notNull(),
|
||||
backedUp: boolean("backed_up").notNull(),
|
||||
transports: text("transports"),
|
||||
createdAt: timestamp("created_at"),
|
||||
aaguid: text("aaguid"),
|
||||
},
|
||||
(table) => [
|
||||
index("passkey_userId_idx").on(table.userId),
|
||||
index("passkey_credentialID_idx").on(table.credentialID),
|
||||
],
|
||||
);
|
||||
|
||||
export const organization = pgTable(
|
||||
"organization",
|
||||
{
|
||||
@ -242,6 +266,7 @@ export const userRelations = relations(user, ({ many }) => ({
|
||||
accounts: many(account),
|
||||
ssoProviders: many(ssoProvider),
|
||||
twoFactors: many(twoFactor),
|
||||
passkeys: many(passkey),
|
||||
members: many(member),
|
||||
invitations: many(invitation),
|
||||
}));
|
||||
@ -274,6 +299,13 @@ export const twoFactorRelations = relations(twoFactor, ({ one }) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
export const passkeyRelations = relations(passkey, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [passkey.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const organizationRelations = relations(organization, ({ many }) => ({
|
||||
organizationRoles: many(organizationRole),
|
||||
members: many(member),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { z } from "zod";
|
||||
import { organization } from "./account";
|
||||
import { user } from "./user";
|
||||
@ -15,6 +15,7 @@ export const ssoProvider = pgTable("sso_provider", {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
domain: text("domain").notNull(),
|
||||
domainVerified: boolean("domain_verified").notNull().default(true),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
|
||||
@ -32,7 +32,12 @@ export const auth = betterAuth({
|
||||
},
|
||||
plugins: [
|
||||
apiKey({ enableMetadata: true, references: "user" }),
|
||||
sso(),
|
||||
sso({
|
||||
trustEmailVerified: true,
|
||||
domainVerification: {
|
||||
enabled: true,
|
||||
},
|
||||
}),
|
||||
twoFactor(),
|
||||
passkey(),
|
||||
organization({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user