mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-24 16:11:53 -05:00
fix(saml): enforce trusted provider check (#6551)
This commit is contained in:
committed by
GitHub
parent
2495956502
commit
69db13bcce
@@ -1722,6 +1722,35 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
const account = 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 (!account) {
|
||||
const isTrustedProvider =
|
||||
ctx.context.options.account?.accountLinking?.trustedProviders?.includes(
|
||||
provider.providerId,
|
||||
) ||
|
||||
("domainVerified" in provider &&
|
||||
provider.domainVerified &&
|
||||
validateEmailDomain(userInfo.email, provider.domain));
|
||||
if (!isTrustedProvider) {
|
||||
const redirectUrl =
|
||||
RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
||||
throw ctx.redirect(`${redirectUrl}?error=account_not_linked`);
|
||||
}
|
||||
await ctx.context.internalAdapter.createAccount({
|
||||
userId: existingUser.id,
|
||||
providerId: provider.providerId,
|
||||
accountId: userInfo.id,
|
||||
accessToken: "",
|
||||
refreshToken: "",
|
||||
});
|
||||
}
|
||||
user = existingUser;
|
||||
} else {
|
||||
// if implicit sign up is disabled, we should not create a new user nor a new account.
|
||||
@@ -1737,19 +1766,6 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
|
||||
name: userInfo.name,
|
||||
emailVerified: userInfo.emailVerified,
|
||||
});
|
||||
}
|
||||
|
||||
// Create or update account link
|
||||
const account = await ctx.context.adapter.findOne<Account>({
|
||||
model: "account",
|
||||
where: [
|
||||
{ field: "userId", value: user.id },
|
||||
{ field: "providerId", value: provider.providerId },
|
||||
{ field: "accountId", value: userInfo.id },
|
||||
],
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
await ctx.context.internalAdapter.createAccount({
|
||||
userId: user.id,
|
||||
providerId: provider.providerId,
|
||||
|
||||
@@ -1182,6 +1182,171 @@ describe("SAML SSO", async () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should deny account linking when provider is not trusted and domain is not verified", async () => {
|
||||
const {
|
||||
auth: authUntrusted,
|
||||
signInWithTestUser,
|
||||
client,
|
||||
} = await getTestInstance({
|
||||
account: {
|
||||
accountLinking: {
|
||||
enabled: true,
|
||||
trustedProviders: [],
|
||||
},
|
||||
},
|
||||
plugins: [sso()],
|
||||
});
|
||||
|
||||
const { headers } = await signInWithTestUser();
|
||||
|
||||
await authUntrusted.api.registerSSOProvider({
|
||||
body: {
|
||||
providerId: "untrusted-saml-provider",
|
||||
issuer: "http://localhost:8081",
|
||||
domain: "http://localhost:8081",
|
||||
samlConfig: {
|
||||
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
||||
cert: certificate,
|
||||
callbackUrl: "http://localhost:3000/dashboard",
|
||||
wantAssertionsSigned: false,
|
||||
signatureAlgorithm: "sha256",
|
||||
digestAlgorithm: "sha256",
|
||||
idpMetadata: {
|
||||
metadata: idpMetadata,
|
||||
},
|
||||
spMetadata: {
|
||||
metadata: spMetadata,
|
||||
},
|
||||
identifierFormat:
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
},
|
||||
},
|
||||
headers,
|
||||
});
|
||||
|
||||
const ctx = await authUntrusted.$context;
|
||||
await ctx.adapter.create({
|
||||
model: "user",
|
||||
data: {
|
||||
id: "existing-user-id",
|
||||
email: "test@email.com",
|
||||
name: "Existing User",
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
let samlResponse: any;
|
||||
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
||||
onSuccess: async (context) => {
|
||||
samlResponse = await context.data;
|
||||
},
|
||||
});
|
||||
|
||||
const response = await authUntrusted.handler(
|
||||
new Request(
|
||||
"http://localhost:3000/api/auth/sso/saml2/callback/untrusted-saml-provider",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
SAMLResponse: samlResponse.samlResponse,
|
||||
RelayState: "http://localhost:3000/dashboard",
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(302);
|
||||
const redirectLocation = response.headers.get("location") || "";
|
||||
expect(redirectLocation).toContain("error=account_not_linked");
|
||||
});
|
||||
|
||||
it("should allow account linking when provider is in trustedProviders", async () => {
|
||||
const { auth: authWithTrusted, signInWithTestUser } = await getTestInstance(
|
||||
{
|
||||
account: {
|
||||
accountLinking: {
|
||||
enabled: true,
|
||||
trustedProviders: ["trusted-saml-provider"],
|
||||
},
|
||||
},
|
||||
plugins: [sso()],
|
||||
},
|
||||
);
|
||||
|
||||
const { headers } = await signInWithTestUser();
|
||||
|
||||
await authWithTrusted.api.registerSSOProvider({
|
||||
body: {
|
||||
providerId: "trusted-saml-provider",
|
||||
issuer: "http://localhost:8081",
|
||||
domain: "http://localhost:8081",
|
||||
samlConfig: {
|
||||
entryPoint: "http://localhost:8081/api/sso/saml2/idp/post",
|
||||
cert: certificate,
|
||||
callbackUrl: "http://localhost:3000/dashboard",
|
||||
wantAssertionsSigned: false,
|
||||
signatureAlgorithm: "sha256",
|
||||
digestAlgorithm: "sha256",
|
||||
idpMetadata: {
|
||||
metadata: idpMetadata,
|
||||
},
|
||||
spMetadata: {
|
||||
metadata: spMetadata,
|
||||
},
|
||||
identifierFormat:
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
},
|
||||
},
|
||||
headers,
|
||||
});
|
||||
|
||||
const ctx = await authWithTrusted.$context;
|
||||
await ctx.adapter.create({
|
||||
model: "user",
|
||||
data: {
|
||||
id: "existing-user-id-2",
|
||||
email: "test@email.com",
|
||||
name: "Existing User",
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
let samlResponse: any;
|
||||
await betterFetch("http://localhost:8081/api/sso/saml2/idp/post", {
|
||||
onSuccess: async (context) => {
|
||||
samlResponse = await context.data;
|
||||
},
|
||||
});
|
||||
|
||||
const response = await authWithTrusted.handler(
|
||||
new Request(
|
||||
"http://localhost:3000/api/auth/sso/saml2/callback/trusted-saml-provider",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
SAMLResponse: samlResponse.samlResponse,
|
||||
RelayState: "http://localhost:3000/dashboard",
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(302);
|
||||
const redirectLocation = response.headers.get("location") || "";
|
||||
expect(redirectLocation).not.toContain("error");
|
||||
expect(redirectLocation).toContain("dashboard");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SAML SSO with custom fields", () => {
|
||||
|
||||
Reference in New Issue
Block a user