S2 Pairing Protocol Python Wrapper
Python helpers for S2 Connect pairing, authentication, and connection initiation.
This package implements client and server building blocks for the S2 communication-layer flows described in the official S2 specification: https://docs.s2standard.org/docs/communication-layer/discovery-pairing-authentication/.
Setup dev environment
Requires: pyenv with Python 3.10 installed on the system. Shell scripts are Linux-compatible.
ci/setup_dev_environment.sh
Install as regular python package
pip install .should just work
Use as a library
The package exposes a public client API, public protocol model re-exports, and HMAC helpers.
The CLI in this repository is a thin adapter around the same library API.
Pairing client API:
import asyncio
from s2auth.client import ClientSettings, PairingClient
async def run_pairing_flow() -> None:
settings = ClientSettings()
client = PairingClient.from_settings(settings)
pairing_result = await client.pair()
print(pairing_result.pairing_s2_node_id)
connect_result = await client.connect(pairing_s2_node_id=pairing_result.pairing_s2_node_id)
print(connect_result.success)
unpair_result = await client.unpair(pairing_s2_node_id=pairing_result.pairing_s2_node_id)
print(unpair_result.success)
asyncio.run(run_pairing_flow())
pair() returns PairingResult, connect() returns ConnectResult, and unpair() returns UnpairResult.
Hooks:
import logging
from s2auth.client import ClientSettings, PairingClient, PairingClientHooks
LOGGER = logging.getLogger(__name__)
def on_start(operation: str, pairing_s2_node_id: str | None) -> None:
LOGGER.info(f"start operation={operation} pairing_s2_node_id={pairing_s2_node_id}")
def on_success(operation: str, result: object) -> None:
LOGGER.info(f"success operation={operation} result_type={type(result).__name__}")
def on_error(operation: str, error: Exception) -> None:
LOGGER.error(f"error operation={operation} error={error}")
async def on_http_request(request) -> None:
LOGGER.debug(f"HTTP request {request.method} {request.url}")
async def on_http_response(response) -> None:
LOGGER.debug(f"HTTP response {response.status_code} {response.request.url}")
settings = ClientSettings()
hooks = PairingClientHooks(
on_operation_start=on_start,
on_operation_success=on_success,
on_operation_error=on_error,
http_request=on_http_request,
http_response=on_http_response,
)
client = PairingClient.from_settings(settings, hooks=hooks)
HTTP hook behavior contract:
- If you do not provide custom HTTP hooks, the library uses built-in request/response debug hooks.
- If you provide
http_requestand/orhttp_response, your hooks fully replace the built-in hooks. - The library does not merge, wrap, or interfere with custom HTTP hooks.
- If you replace the defaults and still want HTTP debug output, add that logging in your own hooks.
- Default hooks log at DEBUG level only; you only see them when logging is configured to DEBUG.
- Keep DEBUG logging off in production, because request/response debug logs may include sensitive values such as tokens.
Storage abstraction:
PairingClient is typed against the ConnectionStore interface. If you do not
provide a store, it uses the built-in Dao by default.
from s2auth.client import ClientSettings, ConnectionStore, PairingClient
from s2auth.client.dao import Dao
from typing import Any
settings = ClientSettings()
# Default storage (Dao)
client_default = PairingClient.from_settings(settings)
# Explicit Dao
client_with_dao = PairingClient.from_settings(settings, storage=Dao(settings.storage_db_url))
# Custom storage implementation can satisfy ConnectionStore
class MyCustomStorage:
def __init__(self) -> None:
self._data: dict[str, dict[str, Any]] = {}
def store_connection_details(self, s2_node_id: str, details: dict[str, Any]) -> None:
self._data[s2_node_id] = details
def load_connection_details(self, s2_node_id: str) -> dict[str, Any] | None:
return self._data.get(s2_node_id)
def remove_connection_details(self, s2_node_id: str) -> bool:
return self._data.pop(s2_node_id, None) is not None
client_with_custom_store = PairingClient.from_settings(settings, storage=MyCustomStorage())
Common protocol models:
from s2auth.common.model import Deployment, Role, HmacHashingAlgorithm
You can also import generated model submodules directly from s2auth.common.model
to access all symbols in the spec, including names that may appear in multiple specs
(for example different ErrorMessage enums):
from s2auth.common.model import s2_connect_common, s2_connect_pairing, s2_connect_session_init
pairing_error = s2_connect_pairing.ErrorMessage
session_error = s2_connect_session_init.ErrorMessage
HMAC helpers:
from s2auth.common import (
create_pairing_code,
create_challenge,
create_response,
verify_response,
get_supported_algorithms,
select_algorithm,
)
Call the client CLI
The pairing client is exposed as the Python module s2auth.client.main.
From a development checkout, run it by:
- first creating and activating a virtual environment:
python -m venv .venvandsource .venv/bin/activate - installing all dependencies:
ci/install_dependencies.sh(you may need to runci/setup_dev_environment.shif poetry is not yet installed) - then calling
client --help
There is also a helper script in the repository:
./run_client.sh
Client workflow:
- Run pairing first (
client ...) to store connection details for a target--pairing_s2_node_id. - After pairing is complete, run connect mode to initiate the S2 session and fetch communication details.
- If needed, run unpair mode to terminate the pairing.
Client configuration is loaded from .env by s2auth.client.settings.ClientSettings.
The CLI reads those values first and then lets you override them with command-line arguments.
Relevant client settings in .env are:
SERVER_URLPAIRING_TOKENPAIRING_S2_NODE_IDCLIENT_S2_NODE_IDCLIENT_ROLECLIENT_DEPLOYMENTDOMAIN_NAMEVERIFY_TLSSSL_CERTFILESTORAGE_DB_URLSUPPORTED_S2_VERSIONSSUPPORTED_COMMUNICATION_PROTOCOLSSUPPORTED_HMAC_HASHING_ALGORITHMSCLEINT_BRANDCLIENT_DEVICE_TYPECLIENT_MODEL_NAME
1. Run pairing
If you have configured .env, the simplest invocation is:
client
The examples below keep the same behavior but explicitly override values from .env on the command line.
WAN override example:
client \
--server_url https://localhost:8005/v1 \
--domain s2connect.example.com \
--pairing_token test \
--skip_cert_verify \
--deployment WAN \
--pairing_s2_node_id ninechars \
--s2_role RM \
--verbose
LAN override example:
client \
--server_url https://localhost:8005/v1 \
--deployment LAN \
--pairing_token test \
--pairing_s2_node_id ninechars \
--s2_role RM \
--verbose
Required input:
- Provide a
PAIRING_TOKENin.envor pass--pairing_tokento start the pairing flow. CLIENT_DEPLOYMENTin.envor--deploymenton the CLI is optional.- For WAN deployments, provide
DOMAIN_NAMEin.envor pass--domain, or let the client auto-detect the domain from--server_url. - For LAN deployments, a local certificate file is optional. The client computes the fingerprint from the TLS peer certificate in the pairing response.
- Set
SSL_CERTFILEin.envor pass--certificate_fileonly when you want to use an explicit CA/certificate bundle for TLS verification.
LAN security note:
- In LAN mode, pairing HMAC is bound to the certificate of the TLS peer seen by the client.
- This is a security feature: if a TLS-terminating proxy presents a different certificate, HMAC verification can fail with a signature mismatch.
- For LAN pairing behind intermediaries, prefer TLS passthrough so the client sees the endpoint certificate directly.
Useful optional arguments:
--server_urldefaults to the value fromSERVER_URL, orhttp://localhostif not configured.--client_S2_nodeIdand--pairing_S2_nodeIdlet you provide explicit node IDs instead of auto-generated ones.--pairing_s2_node_iddefaults toPAIRING_S2_NODE_IDwhen set and can be overridden on the CLI.--certificate_filepoints to a CA/certificate bundle file for TLS verification in local or test setups (optional).--skip_cert_verifydisables certificate verification for local or test setups.-vor--verboseenables debug logging.
Auto-detection behavior:
CLIENT_DEPLOYMENTor--deploymenttakes priority when set and disables deployment auto-detection.- If deployment is not set, the client infers it from the other effective settings.
DOMAIN_NAMEor--domainset: deployment is treated asWAN.SSL_CERTFILEor--certificate_fileset: deployment is treated asLAN.- If both domain and certificate settings are provided while deployment is unset, the client treats the connection as
WANbecause domain is checked first. - Otherwise the client inspects
SERVER_URLor--server_url. localhost,.local, and private/local IP addresses are treated asLAN.- Public hostnames or public IP addresses are treated as
WAN. - When deployment is auto-detected as
WANand no domain is set, the client also auto-detects the domain from the hostname inSERVER_URLor--server_url. - The client logs a warning whenever deployment or domain is auto-detected.
Test certificate:
- For local testing, a test certificate bundle is available at
tests/localhost.chain.pem. - This file is optional for LAN pairing fingerprinting and mainly useful when you want to force TLS verification against a specific local bundle.
- This file is intended for development and test scenarios only.
2. Connect after pairing:
client \
--connect \
--pairing_s2_node_id <pairing-node-id> \
--verbose
--connect uses the previously stored pairing data, calls /initiateSession, confirms the returned pending token, and stores/prints details such as selected protocol/version and server descriptions.
3. Unpair after pairing:
client \
--unpair \
--pairing_s2_node_id <pairing-node-id> \
--verbose
Please note: --connect and --unpair are dedicated modes and only accept --pairing_s2_node_id (or --pairing_S2_nodeId) plus optional --verbose.
Run the FastAPI server
The supported server entry point is the server console script defined in
pyproject.toml under [project.scripts].
1. Install dependencies
poetry install --all-extras
2. Create a .env file
An example file is provided at .env.example — copy it and fill in your values:
cp .env.example .env
The server reads its configuration from a .env file in the project root. All fields below are required:
PAIRING_NODE_ID=PAIR1234 # 8–12 character pairing node identifier
SERVER_S2_NODE_ID=<uuid> # UUID for the server-side S2 node
CEM_S2_NODE_ID=<uuid> # UUID for the CEM S2 node
CEM_TYPE=CEM
CEM_MODEL_NAME=My CEM
CEM_BRAND=MyBrand
HMAC_SALT=<your-domain-or-secret> # Salt used for HMAC verification
Optional fields:
CEM_URL=https://your-cem-host/connection/ # Exposed connection endpoint URL
DEFAULT_PAIRING_TOKEN=yourtoken # One-time startup pairing token; expires after PAIRING_TOKEN_TTL_SECONDS or first use
PAIRING_TOKEN_TTL_SECONDS=300 # Pairing token validity window (default: 5 minutes)
Pairing token behavior:
- Pairing tokens are one-time use tokens.
- A token is consumed by the next successful new pairing attempt and is then no longer valid.
- Pairing tokens expire after
PAIRING_TOKEN_TTL_SECONDS(default: 300 seconds / 5 minutes). DEFAULT_PAIRING_TOKENis the optional startup token. It is also one-time and TTL-bound.- If a one-time token has already expired, the server rejects pairing with an authentication error instead of silently accepting.
- If you want to start without a fixed startup token, set this in
.env:
DEFAULT_PAIRING_TOKEN=
When DEFAULT_PAIRING_TOKEN is empty, the server uses a generated pairing token flow.
You can still press P + Enter in the server console to override the next one-time token manually.
3. Start the server
After installation, run the configured console script directly:
server
This starts the development server with auto-reload enabled at http://0.0.0.0:8000.
The API documentation is available at:
- Swagger UI:
http://localhost:8000/docs - ReDoc:
http://localhost:8000/redoc
Note: Requires the server optional dependencies. Install with:
# For development (with Poetry)
poetry install --extras server
# Or install from PyPI
pip install s2auth[server]
Readding OpenAPI specs through swagger docs
./serve_specs.sh
Run Developer tooling
ci/lint.sh
ci/test_unit.sh
ci/typecheck.sh
Release to PyPI
This project uses dynamic versioning from Git tags. Create a tag like v0.1.1 on the
release commit; that tag becomes the published package version.
1. Prepare and verify
git fetch --tags --force
git status
poetry run pyright
poetry run ruff check .
poetry run pytest
2. Tag the release
git tag -a v0.1.1 -m "Release v0.1.1"
git push origin v0.1.1
3. Build artifacts
rm -rf dist
poetry build
ls -1 dist
4. Validate package metadata
python -m pip install --upgrade twine
python -m twine check dist/*
5. Publish (recommended: TestPyPI first)
export TWINE_USERNAME=__token__
export TWINE_PASSWORD=<your-pypi-token>
# TestPyPI
python -m twine upload --repository testpypi dist/*
# PyPI
python -m twine upload dist/*
6. Verify the published version
python -m pip install s2auth==0.1.1
python -m pip show s2auth
Always prefer python -m pip over pip so the command uses the intended interpreter.
Run python
python(with your virtual environment activated)
or
poetry shellpython
Update dependencies
poetry add <dependency>
or for a dev dependency
poetry add -G dev <dependency>
or for the server optional dependencies
poetry add --optional=server <dependency>
View installed dependencies
# List all installed packages
poetry show
# Show dependency tree
poetry show --tree
# Show specific package details
poetry show <package-name>
What to do on pre-commit errors
- If the error is auto fixed, you can just
git addthe changed files, and commit again. - If they are ruff errors, see https://docs.astral.sh/ruff/rules/ for the rule explanation
- If they are pyright errors, fix your typing
- If they are pytest errors, fix your code or the tests.
- Last case resort to skip the checks:
git commit --no-verifygit push --no-verify
Generate openapi client and server
ci/generate_s2_auth.sh
Relevant code is under src/s2auth/gen_protocol/{client,server}/{connection_init,pairing}
Code here is not moved automatically so moving the generated code to a usable location is manual for now.
Documentation
Comprehensive documentation is available in the docs/ directory. To browse it locally:
poetry run mkdocs serve
Automatic docs generation
Documentation is automatically built and deployed to GitHub Pages through the
workflow in .github/workflows/static.yml.
- Trigger: every push to
main - Manual run: GitHub Actions
Run workflow(workflow_dispatch) - Build command:
poetry run mkdocs build --strict - Published artifact:
docs_html/(frommkdocs.ymlsite_dir)
If the workflow fails, check the Build MkDocs site step first for strict-mode
warnings/errors and unresolved API doc imports.
Start with:
docs/index.mdfor the project overviewdocs/server/index.mdfor server integrationdocs/client/index.mdfor client usagedocs/api/for API reference pagesdocs/Development.mdfor development setup and maintenance notes
Key reference docs:
- Dependency Override Guide - How to override dependencies in the DI system (4 methods: decorator, setup(), function call, context manager)
- Context Storage Override - Specific guide for overriding context storage with Redis or other backends
- Pairing Token Override - How to customize pairing token generation (static tokens for testing, custom lengths, external sources)
- Dependency Injection Deployment Models - How the DI system works in different deployment scenarios (async, threaded, hybrid)
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file s2auth-0.1.0.tar.gz.
File metadata
- Download URL: s2auth-0.1.0.tar.gz
- Upload date:
- Size: 52.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ce087ec67ae56ea183f88dc4504c0c7fec0f9a016249bc4b34a9be66cbcd3b1c
|
|
| MD5 |
d49370b0bff43bc58f9e2832ae9d058b
|
|
| BLAKE2b-256 |
1bebf74990dc972b9eb76a9b968d7422618638ee9691033eb72934284cf04289
|
File details
Details for the file s2auth-0.1.0-py3-none-any.whl.
File metadata
- Download URL: s2auth-0.1.0-py3-none-any.whl
- Upload date:
- Size: 61.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2eac1407263ae362e5d5b722e2043b42a644bd8cf73d1f28f1afcf992e15629
|
|
| MD5 |
c9adfe64df09917ed5cf4237a60cea66
|
|
| BLAKE2b-256 |
64621440844842e04a0db656846030f6e79e4531913af777544b28e076669b58
|