wijjit_ssh.auth

Pluggable authentication for a Wijjit SSH server.

asyncssh drives authentication through a handful of callbacks on its SSHServer object. Wiring credentials straight into those callbacks works, but it couples every deployment to the server glue. Instead the server delegates to an AuthPolicy, so how a deployment authenticates is a value you pass in rather than code you edit.

Four presets ship:

AuthorizedKeys

Public-key auth against an OpenSSH authorized_keys file (one file for everyone, or one per username). The recommended default.

PasswordAuth

Password auth delegated to your callback (LDAP, a database, a hash check).

ChainAuth

Accept if any of several policies accepts.

OpenAuth

No authentication at all. Development only; the server logs a loud warning on startup and refuses to use it unless you also pass allow_anonymous.

Fail-closed. WijjitSSH raises if constructed with no auth policy, unless you explicitly pass allow_anonymous=True. Open auth has to be a decision someone typed, not a default they inherited.

Notes

A policy is consulted per connection attempt and may be shared across connections, so implementations should be stateless (or internally thread-safe/idempotent) and must not block the event loop - do slow work (database lookups, KDF verification) in async methods or an executor.

wijjit_ssh.auth.check_password(supplied, expected)[source]

Compare two passwords in constant time.

Comparing with == leaks the length of the matching prefix through timing, which is enough to recover a secret given enough attempts. Use this (or a real password hash such as argon2/bcrypt) inside a PasswordAuth callback.

Parameters:
  • supplied (str) – The password the client sent.

  • expected (str) – The password on record.

Returns:

Whether they match.

Return type:

bool

wijjit_ssh.auth.load_authorized_keys(path)[source]

Load public keys from an OpenSSH authorized_keys file.

Parameters:

path (str or Path) – Path to the file. ~ is expanded.

Returns:

Every key the file declares. Blank lines and # comments are skipped; lines carrying key options (no-pty,from="..." ssh-ed25519 AAAA...) have the options stripped. Unparseable lines are logged and skipped rather than failing the whole file - one bad line should not lock everyone out.

Return type:

list of SSHKey

Raises:

FileNotFoundError – If the file does not exist. This is fatal on purpose: silently treating a missing key file as “no authorized keys” would deny everyone, and a typo in a config path should be loud.

class wijjit_ssh.auth.AuthPolicy[source]

Bases: object

How a deployment authenticates SSH clients.

The base class denies everything: it requires authentication and supports no method, so a subclass that forgets to enable a method fails closed rather than open. Override only what you support.

auth_required(username)[source]

Whether this user must authenticate at all.

Parameters:

username (str) – The username the client offered.

Returns:

True to require authentication (the normal case). False lets the client straight in with no credential - only OpenAuth does that.

Return type:

bool

password_supported()[source]

Whether password authentication is offered.

Return type:

bool

async verify_password(username, password)[source]

Check a password.

Parameters:
  • username (str) – The username the client offered.

  • password (str) – The password the client sent.

Returns:

Whether the credential is valid. Implementations should compare in constant time (see check_password()).

Return type:

bool

public_key_supported()[source]

Whether public-key authentication is offered.

Return type:

bool

authorized_keys_for(username)[source]

Return the keys this user may authenticate with.

Parameters:

username (str) – The username the client offered.

Returns:

The user’s authorized keys, or None if the user is unknown (which denies them).

Return type:

list of SSHKey or None

kbdint_supported()[source]

Whether keyboard-interactive authentication is offered.

Return type:

bool

kbdint_prompts(username)[source]

The prompts to show for a keyboard-interactive challenge.

Parameters:

username (str) – The username the client offered.

Returns:

(prompt_text, echo) pairs. echo=False hides typing, as for a password.

Return type:

sequence of (str, bool)

async verify_kbdint(username, responses)[source]

Check the responses to a keyboard-interactive challenge.

Parameters:
Returns:

Whether the responses are valid.

Return type:

bool

class wijjit_ssh.auth.OpenAuth[source]

Bases: AuthPolicy

No authentication: anyone may connect as any username.

Development and demos only. There is no credential of any kind - the username is whatever the client typed and is not verified. Never expose this on an untrusted network.

WijjitSSH refuses to run with this policy unless allow_anonymous=True is also passed, and logs a warning when it does.

auth_required(username)[source]

Whether this user must authenticate at all.

Parameters:

username (str) – The username the client offered.

Returns:

True to require authentication (the normal case). False lets the client straight in with no credential - only OpenAuth does that.

Return type:

bool

class wijjit_ssh.auth.AuthorizedKeys(source=None, *, keys=None)[source]

Bases: AuthPolicy

Public-key auth against OpenSSH authorized_keys files.

The recommended policy for real deployments: no shared secret ever crosses the wire, and revoking access means deleting a line.

Parameters:
  • source (str, Path, or Mapping[str, str | Path], optional) – Either a single authorized_keys file whose keys authorize any username, or a mapping of username to that user’s key file. Files are read once, at construction, so a missing or malformed path fails at startup rather than at the first login attempt.

  • keys (Sequence[SSHKey], optional) – Authorized keys supplied directly rather than read from disk. Any username may use them. Mainly useful for tests and for deployments that source keys from somewhere other than a file.

Raises:

Examples

>>> AuthorizedKeys("~/.ssh/authorized_keys")
>>> AuthorizedKeys({"alice": "keys/alice.pub"})
auth_required(username)[source]

Whether this user must authenticate at all.

Parameters:

username (str) – The username the client offered.

Returns:

True to require authentication (the normal case). False lets the client straight in with no credential - only OpenAuth does that.

Return type:

bool

public_key_supported()[source]

Whether public-key authentication is offered.

Return type:

bool

authorized_keys_for(username)[source]

Return the keys this user may authenticate with.

Parameters:

username (str) – The username the client offered.

Returns:

The user’s authorized keys, or None if the user is unknown (which denies them).

Return type:

list of SSHKey or None

class wijjit_ssh.auth.PasswordAuth(checker, *, keyboard_interactive=True)[source]

Bases: AuthPolicy

Password auth delegated to a callback.

Parameters:
  • checker (Callable[[str, str], bool | Awaitable[bool]]) – (username, password) -> bool. May be sync or async; async is preferred for anything that talks to a database or computes a KDF, since a blocking checker stalls the whole server’s event loop.

  • keyboard_interactive (bool, optional) – Also offer the same check over keyboard-interactive (default True). Some clients prefer it, and it is what an interactive ssh session typically falls back to.

Notes

The callback owns credential comparison and must not leak timing: use check_password() for a constant-time compare of a plaintext secret, or a real password hash (argon2, bcrypt, scrypt) for anything stored at rest.

Examples

>>> from wijjit_ssh.auth import PasswordAuth, check_password
>>> USERS = {"alice": "correct-horse"}
>>> async def check(username, password):
...     expected = USERS.get(username)
...     return expected is not None and check_password(password, expected)
>>> policy = PasswordAuth(check)
auth_required(username)[source]

Whether this user must authenticate at all.

Parameters:

username (str) – The username the client offered.

Returns:

True to require authentication (the normal case). False lets the client straight in with no credential - only OpenAuth does that.

Return type:

bool

password_supported()[source]

Whether password authentication is offered.

Return type:

bool

async verify_password(username, password)[source]

Check a password.

Parameters:
  • username (str) – The username the client offered.

  • password (str) – The password the client sent.

Returns:

Whether the credential is valid. Implementations should compare in constant time (see check_password()).

Return type:

bool

kbdint_supported()[source]

Whether keyboard-interactive authentication is offered.

Return type:

bool

kbdint_prompts(username)[source]

The prompts to show for a keyboard-interactive challenge.

Parameters:

username (str) – The username the client offered.

Returns:

(prompt_text, echo) pairs. echo=False hides typing, as for a password.

Return type:

sequence of (str, bool)

async verify_kbdint(username, responses)[source]

Check the responses to a keyboard-interactive challenge.

Parameters:
Returns:

Whether the responses are valid.

Return type:

bool

class wijjit_ssh.auth.ChainAuth(*policies)[source]

Bases: AuthPolicy

Accept a client if any of several policies accepts.

Lets a deployment offer, say, public keys for engineers and passwords for everyone else, without writing a bespoke policy.

Parameters:

*policies (AuthPolicy) – The policies to try. A method is offered if any policy offers it, and a credential is accepted if any policy that offers that method accepts it.

Raises:

ValueError – If no policies are given (which would deny everyone, silently).

Notes

If any policy does not require authentication (i.e. OpenAuth is in the chain), the chain does not either - “accept if any accepts” applies to the no-credential case too. Chaining OpenAuth therefore makes every other policy in the chain irrelevant; it is almost certainly a mistake, and is logged as a warning.

auth_required(username)[source]

Whether this user must authenticate at all.

Parameters:

username (str) – The username the client offered.

Returns:

True to require authentication (the normal case). False lets the client straight in with no credential - only OpenAuth does that.

Return type:

bool

password_supported()[source]

Whether password authentication is offered.

Return type:

bool

async verify_password(username, password)[source]

Check a password.

Parameters:
  • username (str) – The username the client offered.

  • password (str) – The password the client sent.

Returns:

Whether the credential is valid. Implementations should compare in constant time (see check_password()).

Return type:

bool

public_key_supported()[source]

Whether public-key authentication is offered.

Return type:

bool

authorized_keys_for(username)[source]

Return the keys this user may authenticate with.

Parameters:

username (str) – The username the client offered.

Returns:

The user’s authorized keys, or None if the user is unknown (which denies them).

Return type:

list of SSHKey or None

kbdint_supported()[source]

Whether keyboard-interactive authentication is offered.

Return type:

bool

kbdint_prompts(username)[source]

The prompts to show for a keyboard-interactive challenge.

Parameters:

username (str) – The username the client offered.

Returns:

(prompt_text, echo) pairs. echo=False hides typing, as for a password.

Return type:

sequence of (str, bool)

async verify_kbdint(username, responses)[source]

Check the responses to a keyboard-interactive challenge.

Parameters:
Returns:

Whether the responses are valid.

Return type:

bool