mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 07:35: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"];
|
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(
|
async function resolveUser(
|
||||||
identity: VerifiedIdentity,
|
identity: VerifiedIdentity,
|
||||||
deviceType: DeviceType,
|
deviceType: DeviceType,
|
||||||
@@ -253,6 +274,7 @@ export async function runLogin(
|
|||||||
): Promise<LoginResult> {
|
): Promise<LoginResult> {
|
||||||
const deviceType = parseUserAgent(req).type;
|
const deviceType = parseUserAgent(req).type;
|
||||||
const user = await resolveUser(identity, deviceType);
|
const user = await resolveUser(identity, deviceType);
|
||||||
|
assertMethodAllowedFor(user, identity, context.methodId);
|
||||||
|
|
||||||
await unlockUser(user, identity, deviceType);
|
await unlockUser(user, identity, deviceType);
|
||||||
if ("password" in identity && identity.password) {
|
if ("password" in identity && identity.password) {
|
||||||
|
|||||||
@@ -527,6 +527,46 @@ describe("external identities", () => {
|
|||||||
});
|
});
|
||||||
afterEach(() => disposeOidc());
|
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 () => {
|
it("provisions the first user as admin and links the identity", async () => {
|
||||||
const result = await runLogin(fakeRequest() as never, external, {
|
const result = await runLogin(fakeRequest() as never, external, {
|
||||||
methodId: "oidc",
|
methodId: "oidc",
|
||||||
|
|||||||
@@ -725,6 +725,10 @@ export function Auth({ onLogin }: AuthProps) {
|
|||||||
toast.error(t("auth.secondFactorUnavailable"));
|
toast.error(t("auth.secondFactorUnavailable"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (error?.response?.data?.code === "external_login_required") {
|
||||||
|
toast.error(t("auth.externalLoginRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
toast.error(
|
toast.error(
|
||||||
error?.response?.data?.error || error?.message || t(fallbackKey),
|
error?.response?.data?.error || error?.message || t(fallbackKey),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1027,6 +1027,7 @@
|
|||||||
"loadingServer": "Loading server...",
|
"loadingServer": "Loading server...",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"secondFactorUnavailable": "Your account uses a second factor that is not available right now. Contact an admin to reset it.",
|
"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.",
|
"secondFactorNoUI": "This second factor cannot be shown on this screen. Contact an admin.",
|
||||||
"serverAuthentication": "Server sign-in",
|
"serverAuthentication": "Server sign-in",
|
||||||
"minChars": "min. {{min}} characters",
|
"minChars": "min. {{min}} characters",
|
||||||
|
|||||||
Reference in new issue
Block a user