Skip to main content

pwfauth

Install 1.5.0

1.5.0 adds user variables: values your program keeps on the server for each customer (set_variable, get_variable, get_variables, delete_variable), editable in the panel. See CHANGELOG.md.

PWF Auth signs its replies with a new key. pwfauth 1.3.0 and newer pin it; 1.2.0 cannot talk to the current server, so update:

python -m pip install --upgrade pwfauth==1.5.0

1.4.0 works on networks that block the name pwfauth.com (Etisalat in the UAE): when it gets no reply at all, the same request goes to the same server as https://pwfauth.net. 1.3.1 sends a request once more when the connection drops before any reply (Windows error 10054). See CHANGELOG.md.

Server authentication update

All responses, including errors and clock-correction responses, must carry a valid RSA-SHA256 signature from the pinned PWFAuth public key. The private signing key stays on the server. Knowing the application secret is not enough to forge this signature. A fresh random nonce binds each response to its request, method, path, HTTP status and exact body bytes (PWF-REPLY-V1). Query strings are not part of the V1 signed path; production HTTPS protects them in transit.

The SDK accepts only https://pwfauth.com. Its default transport validates TLS certificates and refuses redirects. Missing/invalid signatures fail closed before JSON parsing or envelope decryption; unsigned proxy/CDN errors are security errors. There is no fallback to the old unsigned protocol. Update the server before clients. Applications must never unlock functionality after a security or transport error.

This addresses server emulation. It does not prevent an attacker who controls the client machine from modifying the application itself. Keep authoritative valuable operations on the server and keep real app secrets out of public repositories.

Official Python client for PWF Auth. It covers:

  • license keys and hardware-ID binding;
  • encrypted sessions with a server-driven kill switch;
  • self-service device moves;
  • accounts that run on license keys;
  • free trials, remote texts and slides, and OTA update checks.

PWF Auth is 100% free: every feature, unlimited, forever.

pip install pwfauth

Python 3.9+. One dependency, cryptography, because Python has no AES in its standard library. HTTP uses urllib from the standard library rather than requests.

What's new in 1.1.0

  • The kill switch cannot be dodged. Only an encrypted reply counts as the server's answer.
    • A plain refusal no longer keeps the session alive forever. The commonest is the server rejecting a clock that was moved more than 5 minutes.
    • Neither does a fake {"success": true}.
    • Bans, pauses and expiry end the session with the server's own code (BANNED, PAUSED, EXPIRED…) and message.
  • A wrong PC clock repairs itself. When the server refuses the time, the client shifts by the server's clock and retries once. Opt out with auto_correct_clock=False.
  • Move a license to a new PC: reset_hardware_id(key, reason).
  • Accounts that run on license keys: register_account_with_key() and redeem_key().
  • Fixed:
    • check_update(), change_account_password() and track_social_click() were refused by the server in 1.0.x.
    • The heartbeat now uses the server's interval.
  • See CHANGELOG.md for the details and the few behaviour changes.

Quick start

import os
import sys
from pwfauth import PwfClient

client = PwfClient(os.environ["PWFAUTH_SECRET"])

def on_session_ended(error_code, message):
    """Ban, pause, expiry, HWID reset, revoke, maintenance, or a server that stopped
    answering. Actually stop here — logging alone leaves the licence unenforceable.
    This runs on the heartbeat thread, so os._exit (sys.exit would only end it)."""
    print(f"Session ended ({error_code}): {message}", file=sys.stderr)
    os._exit(1)

client.on_session_ended = on_session_ended

login = client.login("XXXXX-XXXXX-XXXXX-XXXXX")
if not login.success:
    print(login.message, file=sys.stderr)   # safe to show the user
    sys.exit(1)

client.start_heartbeat()   # keeps the session alive AND enforces the kill switch

start_heartbeat() is what turns a one-time check into a live session. Revoke the key in your dashboard and on_session_ended fires on the user's machine within one heartbeat. It does not wait for the next launch. A console app or service can call client.run_heartbeat() instead, which blocks until the session ends.

GUI apps

on_session_ended runs on the heartbeat's background thread. Hand the work to your toolkit's own thread:

client.on_session_ended = lambda code, msg: root.after(0, sign_out, code, msg)   # Tkinter

Moving a license to a new PC

from pwfauth import PwfErrorCodes

login = client.login(key)
if login.error_code in (PwfErrorCodes.HWID_MISMATCH, PwfErrorCodes.DEVICE_LIMIT):
    if ask_user("This key is used on another computer. Move it here?"):
        moved = client.reset_hardware_id(key, "New laptop")
        if moved.success:
            login = client.login(key)
        else:
            show(moved.message)   # e.g. the cooldown: "try again in 11 hours"

The move unbinds the key from every machine and ends its sessions. In App Settings you decide whether customers may move keys themselves, and the cooldown between two moves (12 hours by default).

Accounts

client.register_account("bob", "S3cret!pass", email="bob@example.com")
client.register_account_with_key("bob", "S3cret!pass", key)   # when the app wants a key at sign-up
client.account_login("bob", "S3cret!pass")                    # opens a session, like login()
client.redeem_key(another_key)                                # adds the key's time to the account
client.change_account_password("bob", "S3cret!pass", "N3w!pass")
  • Keys on accounts: a key used for sign-up or redeemed onto an account is used up. It can no longer sign in on its own, and login with it answers KEY_REDEEMED.
  • Password changes: changing the password signs the account out on every device.

Reading the licence

lic = login.license
if lic.is_lifetime:
    print("Lifetime licence")
else:
    print(f"Expires {lic.expires_at:%Y-%m-%d} — {lic.days_remaining} day(s) left")

expires_at is None for lifetime keys, because the server sends expires_at: null. Treating that as a date is the most common integration bug against this API, and the is_lifetime flag exists so you never have to.

Feature flags

if login.get_boolean("pro_export"):
    enable_export()

Handles the 1 / 0 / "true" forms the dashboard can store.

Checking a key without using a device seat

status = client.check_key(key)   # no session opened, no seat consumed

Updates

upd = client.check_update("1.4.2")            # channel="beta" / "alpha" also work
if upd.data.get("update_available"):
    print("New version:", upd.data["update"]["version"], upd.data["update"]["download_url"])

Hardware ID

from pwfauth import get_hardware_id
print(get_hardware_id())

The ID is derived the same way as in the .NET and Node clients. So the same machine counts as one device under any SDK, and switching languages does not use up a second seat against the licence's device limit.

Everything else

Method What it does
login(key) / logout() Open and close a session (logout does not unbind the key)
check_key(key) Read a key's state without a session
heartbeat() One manual beat
start_heartbeat() / stop_heartbeat() / run_heartbeat() The heartbeat loop
reset_hardware_id(key, reason) Move a license to this PC (self-service)
create_trial() Issue a free trial for this machine
get_app_info() / get_texts() / get_slides() Remote app content
set_variable(name, value) / get_variable(name) / get_variables() / delete_variable(name) Values kept on the server for this customer (raise PwfRefusedError when refused)
check_update(version, channel) OTA update check
track_social_click(link_id) Count a click on one of the app's social links
register_account() / register_account_with_key() / account_login() User accounts
redeem_key(key) / change_account_password() Account keys and passwords
post_envelope() / get_envelope() / post_plain() Raw transports

Options

PwfClient takes these keyword arguments:

Option Effect
base_url Fixed to https://pwfauth.com; other origins are rejected
heartbeat_seconds Interval between heartbeats; the server's by default
max_heartbeat_failures Beats in a row without an encrypted answer before NETWORK_LOST; 3 by default
max_rate_limited_beats Separate budget for HTTP 429; 10 by default
timeout Request timeout
hardware_id Use your own hardware ID
user_agent The User-Agent header to send
auto_correct_clock Repair a wrong PC clock automatically

Errors

from pwfauth import PwfErrorCodes, PwfHttpError, PwfSecurityError, ends_session

try:
    login = client.login(key)
except PwfSecurityError:
    ...   # an unencrypted "success": not the license server — never unlock on it
except PwfHttpError as e:
    ...   # no usable reply; e.status is 401 for a wrong app secret, 0 for no connection

ends_session(code) says whether a code means the licence is finished on this machine. It returns False for a code it does not recognise, on purpose. An unknown code is far more likely to be a new transient condition than a new way of being banned, and locking a paying user out on a guess is the worse failure.

MIT

Metadata

Release files for pwfauth 1.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pwfauth 1.5.0
File Size Uploaded
pwfauth-1.5.0.tar.gz 35.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pwfauth 1.5.0
File Interpreter ABI Platform
pwfauth-1.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 63.6 kB

Release files / pwfauth-1.5.0.tar.gz

Download URL pwfauth-1.5.0.tar.gz
Size 35.8 kB
Tags Source
SHA-256 checksum
How to use checksums
674afdee10c916249fbac8bf5ac61c279c0f38833f9d61942d94f2da75447965
BLAKE2b-256 checksum
How to use checksums
d293cb095d0258afcb9d2069bed09e955f802b6b8e772b770a19e9b0be54887b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.0

Release files / pwfauth-1.5.0-py3-none-any.whl

Download URL pwfauth-1.5.0-py3-none-any.whl
Size 27.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
42605b5c4a118e71f9029978a379179bf399eaf7f4aa7b7a700eb4a4e51424b5
BLAKE2b-256 checksum
How to use checksums
85f3e6930d0122daf6d21b75d3ab30a91c169ce26f7776909f1cd48317c04695
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.0

Release history Release notifications | RSS feed

This release

1.5.0 This release

2 release files

1.4.0

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.0

1 release file

1.1.0

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page