OAuth client secret candidate evidence¶
Task: d85fcf41-fbd3-4c13-99f2-7df01ef68ca2, frozen version 17.
Continues the previous local candidate
b1e6c09f68d5230ea3f7b8d3f705fd9059088c2d from task version 3.
The earlier Python-access blocker is resolved in this session.
Repository: h8v6/heyaira; branch: heyaira/d85fcf41.
Base: 4f487bbdf9006cf0a37390205f1721d7c6bbeb71.
This is a local candidate, not release acceptance. The result commit is recorded in the execution's HeyAira RESULT/receipt. No push, PR, merge, install, deployment, or production database operation was performed.
Changes and review evidence¶
src/heyaira/server.pysetsclient_secret_expiry_seconds=None, requesting the SDK's non-expiring secret representation, and installs the credential hashing adapter in the production app.src/heyaira/oauth.pyexcludesclient_secretfrom registration JSON while retainingclient_secret_hash. Loaded confidential clients expose the digest to the SDK. The adapter hashes incoming form/Basic credentials at/tokenand/revoke; presenting the stored digest as a password hashes it again. Basic passwords are URL-decoded exactly once before hashing, matching the installed SDK's RFC 6749 behavior. Registration still returns the original issued secret.- Loading a legacy row removes the plaintext key and retains its existing hash, deriving a hash if missing. A JSON snapshot condition prevents overwriting a concurrently replaced registration. The old expiry equal to issuance time is recognized and repaired on load; other finite expirations are preserved.
_client_for_token_exchangeis unchanged. Clients with authentication methodnone, including normalized ChatGPT, Claude, and HeyAira Bridge clients, retain public authentication. Original OAuth tests are unchanged.
Targeted regressions¶
tests/test_oauth_client_secrets_e2e.py reuses the real HTTP server/PostgreSQL
fixture from tests/test_project_resource_e2e.py. It skips through that fixture
when HEYAIRA_PROJECT_RESOURCE_E2E_DATABASE_URL is unset, and is enabled by the
existing scripts/test_postgres.py matrix without runner changes.
The new database tests register a client_secret_post client, read its stored
JSON/hash, wait 2.1 seconds, complete S256 PKCE and consent, and expect an access
token from /token. They reject wrong secrets and digest-as-password attempts,
then exercise revocation. Seeded legacy rows, with and without a preexisting
hash and with the old immediate-expiry value, are loaded, checked for plaintext
removal, and authenticated through the complete PKCE flow.
tests/test_oauth_client_secrets.py adds storage regressions, repeated legacy
loads, finite-expiry preservation, concurrent replacement, the four public
client shapes, and actual SDK authentication through an ASGI app using synthetic
client storage. An additional SDK regression covers encoded client IDs and
passwords containing reserved characters and Unicode. The Basic-auth SDK test
supplies the form client_id, as required by the installed SDK.
Actual local checks¶
The exact version-17 suite command ran with Python 3.12.7. The allowed Xcode Git executable was selected on PATH so deployment-fixture Git calls avoid the Apple launcher attempting to write a forbidden external cache. No dependencies were installed.
export PATH="/Applications/Xcode.app/Contents/Developer/usr/bin:$PATH"
PYTHONPATH=src python -m pytest -q -p no:cacheprovider \
--ignore=tests/test_server_beat_audit.py \
--ignore=tests/test_server_beat_corrections.py \
--deselect tests/test_deployment_guard.py::test_required_override_passes_preflight \
--deselect tests/test_deployment_guard.py::test_missing_extra_compose_file_is_rejected \
--deselect tests/test_ios_companion_contract.py::test_inbox_logic_check_passes_with_the_swift_toolchain
301 passed, 110 skipped, 3 deselected in 7.08s
exit code: 0
The two ignored modules and three deselections are precisely the unrelated sandbox-incompatible checks excluded by task v17; no extra exclusions were added. The skipped cases require opt-in isolated PostgreSQL databases.
Focused OAuth check:
PYTHONPATH=src python -m pytest -q -p no:cacheprovider \
tests/test_oauth_client_secrets.py tests/test_oauth_client_secrets_e2e.py tests/test_oauth.py
22 passed, 3 skipped in 1.96s
exit code: 0
The three focused skips are the new database regressions; their shared fixture
requires HEYAIRA_PROJECT_RESOURCE_E2E_DATABASE_URL.
Regression sensitivity was checked against an isolated copy of source from
4f487bbdf9006cf0a37390205f1721d7c6bbeb71, leaving the working source untouched:
test_registration_returns_original_secret_but_stores_only_its_hash: FAILED
original client_info still contains client_secret
test_sdk_accepts_the_secret_but_rejects_wrong_missing_or_hash_credentials[client_secret_post]: FAILED
original client_secret_expires_at equals issuance time instead of 0
2 failed in 1.49s (expected; exit code 1)
Before correcting Basic credential decoding, the added URL-encoding regression
also failed with invalid_client instead of authenticated invalid_grant
(1 failed in 1.40s). It passes with the final adapter in the successful suite.
git diff --check passed (exit 0). git diff --exit-code against the base for the original
tests/test_oauth.py, tests/test_oauth_instance_binding.py, and
tests/test_project_resource_e2e.py passed (exit 0), confirming they are unchanged.
An AST comparison of _client_for_token_exchange with the base commit also
passed, confirming the public-client normalization implementation is unchanged.
The version-17 local suite criterion passes with no failures, and the actual SDK authentication regressions execute locally. No PostgreSQL is available for this assignment, so the real database/PKCE regressions remain unexecuted locally. Independent verification and the complete zero-skip PostgreSQL CI gate remain required; Bridge owns subsequent publication.
Follow-up after running against PostgreSQL (PR #83)¶
- The legacy-row database test seeded a row without
scope, which SDK registration always fills with the default scopes, so/authorizeansweredinvalid_scopeand the test found norequest_id. The seed is now the exact record the oldregister_clientstored. - The body-rewriting middleware let the stored digest work as the password:
it hashed only UTF-8 urlencoded bodies (the SDK also reads
multipart/form-dataand latin-1 bytes), and it matched the full URL path, so behind a non-empty ASGIroot_pathit hashed nothing at all. The middleware is gone.HashedSecretClientAuthenticatoris the SDK's client authenticator with the presented secret hashed before the constant-time comparison, anduse_hashed_client_authenticationrebuilds the/tokenand/revokeroutes with the SDK's own handlers and wrappers around it, so the check happens where the client is authenticated, whatever the path prefix or body shape. - Rows are scrubbed on load only when the client returns. Migration
038_oauth_client_secret_scrub.sqlremoves the plaintext from every row.