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:
AuthorizedKeysPublic-key auth against an OpenSSH
authorized_keysfile (one file for everyone, or one per username). The recommended default.PasswordAuthPassword auth delegated to your callback (LDAP, a database, a hash check).
ChainAuthAccept if any of several policies accepts.
OpenAuthNo 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 aPasswordAuthcallback.
- wijjit_ssh.auth.load_authorized_keys(path)[source]
Load public keys from an OpenSSH
authorized_keysfile.- 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:
objectHow 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.
- async verify_password(username, password)[source]
Check a password.
- Parameters:
- Returns:
Whether the credential is valid. Implementations should compare in constant time (see
check_password()).- Return type:
- class wijjit_ssh.auth.OpenAuth[source]
Bases:
AuthPolicyNo 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.
WijjitSSHrefuses to run with this policy unlessallow_anonymous=Trueis also passed, and logs a warning when it does.
- class wijjit_ssh.auth.AuthorizedKeys(source=None, *, keys=None)[source]
Bases:
AuthPolicyPublic-key auth against OpenSSH
authorized_keysfiles.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_keysfile 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:
ValueError – If neither
sourcenorkeysis given.FileNotFoundError – If a named key file does not exist.
Examples
>>> AuthorizedKeys("~/.ssh/authorized_keys") >>> AuthorizedKeys({"alice": "keys/alice.pub"})
- class wijjit_ssh.auth.PasswordAuth(checker, *, keyboard_interactive=True)[source]
Bases:
AuthPolicyPassword 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
sshsession 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)
- async verify_password(username, password)[source]
Check a password.
- Parameters:
- Returns:
Whether the credential is valid. Implementations should compare in constant time (see
check_password()).- Return type:
- class wijjit_ssh.auth.ChainAuth(*policies)[source]
Bases:
AuthPolicyAccept 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.
OpenAuthis 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.- async verify_password(username, password)[source]
Check a password.
- Parameters:
- Returns:
Whether the credential is valid. Implementations should compare in constant time (see
check_password()).- Return type: