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: 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 @pytest.mark.asyncio
async def test( async def test(
self, self,
@ -16,6 +21,7 @@ class Test:
wg_c_ed25519_pubkey: Path, wg_c_ed25519_pubkey: Path,
timeout, timeout,
shared_wg: WarpgateProcess, shared_wg: WarpgateProcess,
include_pk: bool,
): ):
ssh_port = processes.start_ssh_server( ssh_port = processes.start_ssh_server(
trusted_keys=[wg_c_ed25519_pubkey.read_text()] trusted_keys=[wg_c_ed25519_pubkey.read_text()]
@ -32,13 +38,24 @@ class Test:
api.create_password_credential( api.create_password_credential(
user.id, sdk.NewPasswordCredential(password="123") 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.add_user_role(user.id, role.id)
api.update_user( api.update_user(
user.id, user.id,
sdk.UserDataRequest( sdk.UserDataRequest(
username=user.username, username=user.username,
credential_policy=sdk.UserRequireCredentialsPolicy( 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", f"{user.username}:{ssh_target.name}@localhost",
"-p", "-p",
str(shared_wg.ssh_port), str(shared_wg.ssh_port),
"-i", "-o",
"/dev/null", "IdentityFile=ssh-keys/id_ed25519",
"ls", "ls",
"/bin/sh", "/bin/sh",
password="123",
) )
msg = await ws.receive(5) msg = await ws.receive(5)

View file

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

View file

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

View file

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

View file

@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { Input } from '@sveltestrap/sveltestrap' import { Input } from '@sveltestrap/sveltestrap'
import { CredentialKind, type UserRequireCredentialsPolicy } from './lib/api' import { CredentialKind, type UserRequireCredentialsPolicy } from './lib/api'
import type { ExistingCredential } from './CredentialEditor.svelte' import type { ExistingCredential } from './CredentialEditor.svelte'
import Fa from 'svelte-fa'; import Fa from 'svelte-fa'
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'
type ProtocolID = 'http' | 'ssh' | 'mysql' | 'postgres' type ProtocolID = 'http' | 'ssh' | 'mysql' | 'postgres'
@ -29,7 +29,7 @@ const labels = {
WebUserApproval: 'In-browser auth', WebUserApproval: 'In-browser auth',
} }
const tips: Record<ProtocolID, Map<[CredentialKind, boolean], string> | undefined> = { const tips: Record<ProtocolID, Map<[CredentialKind, boolean], string>> = {
postgres: new Map([ postgres: new Map([
[ [
[CredentialKind.Password, false], [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.', '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(() => { let activeTips: string[] = $derived.by(() => {