Skip to content

[auth] SecretValue wrapper for secrets - #93

Open
ROHIT-JR wants to merge 1 commit into
nitrocloudofficial:developfrom
ROHIT-JR:feat/69-secret-value
Open

ROHIT-JR wants to merge 1 commit into
nitrocloudofficial:developfrom
ROHIT-JR:feat/69-secret-value

Conversation

@ROHIT-JR

Copy link
Copy Markdown
Collaborator

Closes #69

What this adds

SecretValue (nitrostack/auth/secret.py), exported from nitrostack. It keeps a secret out of logs, repr, tracebacks, pickles and serialized output. The raw value is only reachable through unwrap().

Ported from closed PR #4, with the snake_case API only (no camelCase aliases).

  • from_env(name), from_env_optional(name), from_value(value, allow_hardcoded=..., reason=...), unwrap(), is_from_environment().
  • str, repr, format, f-strings and logging all give [SecretValue: REDACTED]. There is no instance __dict__.
  • Pickle is redacted. A pickled secret loads back with no value, and unwrap() raises. I chose this over loading back a fake string, which a JWT or API key check could silently use as the real secret.
  • Pydantic v2: a field typed SecretValue accepts a SecretValue or a str, and model_dump(), model_dump_json() and the JSON schema never contain the value. A bare json.dumps(secret) raises TypeError instead of leaking.
  • == uses hmac.compare_digest. __hash__ is keyed per process, so it does not expose the value. copy and deepcopy keep the value.
  • is_secret_value(x) and unwrap_secret(x) (a plain str is returned as it is).

Modules

All of these still accept a plain str.

  • JWTModule.for_root(secret=...) / JWTService(secret=...): new optional explicit secret. It wins over the environment variable; with none given, behaviour is unchanged.
  • ApiKeyModule.for_root(keys=[...]) / ApiKeyService(keys=[...]): new optional explicit keys, used in addition to the environment ones.
  • OAuthService / OAuthModule.for_root: token_introspection_client_secret and static_client_secret accept a SecretValue. It is unwrapped only where it is really used: the Basic auth header for introspection and the RFC 7591 registration response.
  • ConfigService: defaults may hold SecretValue values. New get_secret() and get_secret_or_throw() return a SecretValue.

Note: JWTModule and ApiKeyModule took environment variable names before, not secret strings, so the new secret and keys arguments are how a secret reaches them in code.

Differences from the old PR #4 code

  • Dropped the camelCase aliases and the required argument of from_env (it never changed the behaviour; use from_env_optional instead).
  • Hardcoded-value and short-secret messages use warnings.warn instead of writing to stderr based on NODE_ENV.
  • unwrap_secret no longer prints a warning for plain strings, because the modules must keep accepting them.
  • repr is now fully redacted (the old one printed the source).

Checks

  • tests/test_secret_value.py: 40 tests covering each acceptance criterion (f-string, repr, model_dump_json, json.dumps(model.model_dump()), pickle.dumps at every protocol, log records, tracebacks with captured locals) and each module.
  • I broke the redaction on purpose four ways (leaking repr, a pickle that keeps the value, a Pydantic serializer that unwraps, and a plain ==). Each was caught by at least one test.
  • Full suite: 929 passed on top of current develop.
  • README has a short "Keeping secrets out of logs" section.

Not changed

JWTService.get_secret() still falls back to a built-in development secret when nothing is configured. That is existing behaviour and out of scope here, but it is probably worth its own issue.

SecretValue keeps a secret out of logs, repr, str, f-strings, tracebacks, pickles
and Pydantic/JSON output. The raw value is only available through unwrap().

- port of the SecretValue from closed PR nitrocloudofficial#4 with the snake_case API only
- pickling redacts: a pickled secret loads back with no value and unwrap() raises
- Pydantic v2 fields validate a SecretValue or str and serialize as the redacted marker
- equality uses hmac.compare_digest
- add is_secret_value() and unwrap_secret()
- JWTModule(secret=...), ApiKeyModule(keys=[...]), OAuthService client secrets and
  ConfigService defaults/get_secret() accept a SecretValue; plain str still works

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant