mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
fix: sso only accounts cannot sign in with a passkey or other local method
This commit is contained in:
1 parent
2e3c242e34
commit
2ba11652eb
4 files changed
+67
No files matched your search
@@ -171,6 +171,27 @@ async function isTrustedDevice(req: Request, userId: string): Promise<boolean> {
|
||||
|
||||
type DeviceType = ReturnType<typeof parseUserAgent>["type"];
|
||||
|
||||
/**
|
||||
* An account that only signs in through SSO or LDAP must keep doing so. A
|
||||
* passkey or any other local method would skip the identity provider, so a
|
||||
* user disabled there could still get in.
|
||||
*/
|
||||
export function assertMethodAllowedFor(
|
||||
user: Pick<UserRecord, "isExternal" | "passwordHash">,
|
||||
identity: VerifiedIdentity,
|
||||
methodId: string,
|
||||
): void {
|
||||
if (identity.kind === "external" || !user.isExternal) return;
|
||||
if (user.passwordHash && user.passwordHash.trim() !== "") return;
|
||||
ensureCoreLoginProviders();
|
||||
if (getLoginMethod(methodId)?.external) return;
|
||||
throw new LoginMethodError(
|
||||
"This account signs in through its identity provider",
|
||||
403,
|
||||
"external_login_required",
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveUser(
|
||||
identity: VerifiedIdentity,
|
||||
deviceType: DeviceType,
|
||||
@@ -253,6 +274,7 @@ export async function runLogin(
|
||||
): Promise<LoginResult> {
|
||||
const deviceType = parseUserAgent(req).type;
|
||||
const user = await resolveUser(identity, deviceType);
|
||||
assertMethodAllowedFor(user, identity, context.methodId);
|
||||
|
||||
await unlockUser(user, identity, deviceType);
|
||||
if ("password" in identity && identity.password) {
|
||||
|
||||
@@ -527,6 +527,46 @@ describe("external identities", () => {
|
||||
});
|
||||
afterEach(() => disposeOidc());
|
||||
|
||||
function addSsoUser(passwordHash = "") {
|
||||
addUser({ passwordHash });
|
||||
h.state.identities.push({
|
||||
userId: "u1",
|
||||
providerId: "3",
|
||||
subject: "sub-1",
|
||||
} as never);
|
||||
}
|
||||
|
||||
it("refuses a passkey sign in for an account that only uses SSO", async () => {
|
||||
addSsoUser();
|
||||
await expect(
|
||||
runLogin(
|
||||
fakeRequest() as never,
|
||||
{ kind: "user", userId: "u1", mfaSatisfied: true },
|
||||
{ methodId: "passkey", rememberMe: false },
|
||||
),
|
||||
).rejects.toMatchObject({ status: 403, code: "external_login_required" });
|
||||
});
|
||||
|
||||
it("still lets an SSO account with a password use a passkey", async () => {
|
||||
addSsoUser(bcrypt.hashSync(PASSWORD, 4));
|
||||
const result = await runLogin(
|
||||
fakeRequest() as never,
|
||||
{ kind: "user", userId: "u1", mfaSatisfied: true },
|
||||
{ methodId: "passkey", rememberMe: false },
|
||||
);
|
||||
expect(result.kind).toBe("session");
|
||||
});
|
||||
|
||||
it("lets an external method sign an SSO account in by user id", async () => {
|
||||
addSsoUser();
|
||||
const result = await runLogin(
|
||||
fakeRequest() as never,
|
||||
{ kind: "user", userId: "u1" },
|
||||
{ methodId: "oidc", rememberMe: false },
|
||||
);
|
||||
expect(result.kind).toBe("session");
|
||||
});
|
||||
|
||||
it("provisions the first user as admin and links the identity", async () => {
|
||||
const result = await runLogin(fakeRequest() as never, external, {
|
||||
methodId: "oidc",
|
||||
|
||||
@@ -725,6 +725,10 @@ export function Auth({ onLogin }: AuthProps) {
|
||||
toast.error(t("auth.secondFactorUnavailable"));
|
||||
return;
|
||||
}
|
||||
if (error?.response?.data?.code === "external_login_required") {
|
||||
toast.error(t("auth.externalLoginRequired"));
|
||||
return;
|
||||
}
|
||||
toast.error(
|
||||
error?.response?.data?.error || error?.message || t(fallbackKey),
|
||||
);
|
||||
|
||||
@@ -1027,6 +1027,7 @@
|
||||
"loadingServer": "Loading server...",
|
||||
"password": "Password",
|
||||
"secondFactorUnavailable": "Your account uses a second factor that is not available right now. Contact an admin to reset it.",
|
||||
"externalLoginRequired": "This account signs in through single sign-on. Use that button to sign in.",
|
||||
"secondFactorNoUI": "This second factor cannot be shown on this screen. Contact an admin.",
|
||||
"serverAuthentication": "Server sign-in",
|
||||
"minChars": "min. {{min}} characters",
|
||||
|
||||
Reference in new issue
Block a user