fixed #972 - SSH server not offering keyboard-interactive when only OOB or SSO auth is enabled for a user

This commit is contained in:
Eugene 2025-05-21 20:20:10 +02:00
parent 5ee29b9ad0
commit 2381f55696
No known key found for this signature in database
GPG key ID: 5896FCBBDD1CF4F4
5 changed files with 52 additions and 18 deletions

View file

@ -9,6 +9,11 @@ from .util import wait_port
class Test:
# When include_pk is False, we're testing for
# https://github.com/warp-tech/warpgate/issues/972
# where the SSH server fails to offer keyboard-interactive authentication
# when no OTP credential is present.
@pytest.mark.parametrize("include_pk", [True, False])
@pytest.mark.asyncio
async def test(
self,
@ -16,6 +21,7 @@ class Test:
wg_c_ed25519_pubkey: Path,
timeout,
shared_wg: WarpgateProcess,
include_pk: bool,
):
ssh_port = processes.start_ssh_server(
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
@ -32,13 +38,24 @@ class Test:
api.create_password_credential(
user.id, sdk.NewPasswordCredential(password="123")
)
if include_pk:
api.create_public_key_credential(
user.id,
sdk.NewPublicKeyCredential(
label="Public Key",
openssh_public_key=open("ssh-keys/id_ed25519.pub").read().strip()
),
)
api.add_user_role(user.id, role.id)
api.update_user(
user.id,
sdk.UserDataRequest(
username=user.username,
credential_policy=sdk.UserRequireCredentialsPolicy(
ssh=[sdk.CredentialKind.WEBUSERAPPROVAL],
ssh=[sdk.CredentialKind.WEBUSERAPPROVAL] if not include_pk else [
sdk.CredentialKind.PUBLICKEY,
sdk.CredentialKind.WEBUSERAPPROVAL,
],
),
),
)
@ -80,11 +97,10 @@ class Test:
f"{user.username}:{ssh_target.name}@localhost",
"-p",
str(shared_wg.ssh_port),
"-i",
"/dev/null",
"-o",
"IdentityFile=ssh-keys/id_ed25519",
"ls",
"/bin/sh",
password="123",
)
msg = await ws.receive(5)

View file

@ -2,6 +2,7 @@ from asyncio import subprocess
from base64 import b64decode
from uuid import uuid4
import pyotp
import pytest
from pathlib import Path
from textwrap import dedent
@ -32,13 +33,14 @@ class Test:
sdk.RoleDataRequest(name=f"role-{uuid4()}"),
)
user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}"))
api.create_public_key_credential(
user.id,
sdk.NewPublicKeyCredential(
label="Public Key",
openssh_public_key=open("ssh-keys/id_ed25519.pub").read().strip()
),
)
if include_public_key:
api.create_public_key_credential(
user.id,
sdk.NewPublicKeyCredential(
label="Public Key",
openssh_public_key=open("ssh-keys/id_ed25519.pub").read().strip(),
),
)
api.create_otp_credential(
user.id,
sdk.NewOtpCredential(

View file

@ -109,12 +109,21 @@ impl ConfigProvider for DatabaseConfigProvider {
let user = user_model.load_details(&db).await?;
let supported_credential_types: HashSet<CredentialKind> = user
let mut available_credential_types = user
.credentials
.iter()
.map(|x| x.kind())
.filter(|x| supported_credential_types.contains(x))
.collect();
.collect::<HashSet<_>>();
available_credential_types.insert(CredentialKind::WebUserApproval);
let supported_credential_types = supported_credential_types
.iter()
.copied()
.collect::<HashSet<_>>()
.intersection(&available_credential_types)
.copied()
.collect::<HashSet<_>>();
let default_policy = Box::new(AnySingleCredentialPolicy {
supported_credential_types: supported_credential_types.clone(),
}) as Box<dyn CredentialPolicy + Sync + Send>;

View file

@ -1497,6 +1497,10 @@ impl ServerSession {
CredentialKind::Sso => m.push(MethodKind::KeyboardInteractive),
}
}
if m.contains(&MethodKind::KeyboardInteractive) {
// Ensure keyboard-interactive is always the last method
m.push(MethodKind::KeyboardInteractive);
}
m
}

View file

@ -1,9 +1,9 @@
<script lang="ts">
import { Input } from '@sveltestrap/sveltestrap'
import { CredentialKind, type UserRequireCredentialsPolicy } from './lib/api'
import type { ExistingCredential } from './CredentialEditor.svelte'
import Fa from 'svelte-fa';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import type { ExistingCredential } from './CredentialEditor.svelte'
import Fa from 'svelte-fa'
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'
type ProtocolID = 'http' | 'ssh' | 'mysql' | 'postgres'
@ -29,7 +29,7 @@ const labels = {
WebUserApproval: 'In-browser auth',
}
const tips: Record<ProtocolID, Map<[CredentialKind, boolean], string> | undefined> = {
const tips: Record<ProtocolID, Map<[CredentialKind, boolean], string>> = {
postgres: new Map([
[
[CredentialKind.Password, false],
@ -40,6 +40,9 @@ const tips: Record<ProtocolID, Map<[CredentialKind, boolean], string> | undefine
'Not all clients will show the 2FA auth prompt. The user might need to log in to the Warpgate UI to see the prompt.',
],
]),
http: new Map(),
mysql: new Map(),
ssh: new Map(),
}
let activeTips: string[] = $derived.by(() => {