Skip to content

chore: release v0.30.4 - #656

Merged
porcellus merged 42 commits into
0.30from
release/v0.30.4
Oct 21, 2026
Merged

porcellus merged 42 commits into
0.30from
release/v0.30.4

Conversation

@porcellus

Copy link
Copy Markdown
Collaborator

Releases 0.30.4 from the 0.30 line. The release notes are the ## [0.30.4] section of CHANGELOG.md.

Merging this starts the release pipeline as a backport: an upload to PyPI, a GitHub release that isn't marked latest, and no merge into master or sync of dev.

bcbogdan and others added 30 commits January 23, 2026 17:57
fix: Update phonenumbers version to cover failing validation edgecases
…with is_valid_frontend_redirect_uri (GHSA-3vrp-2rj4-wvw6)

Backport to 0.30: 0.30 has no pydantic recipe configs or plugins, so
is_valid_frontend_redirect_uri is a ThirdPartyConfig attribute passed through
init, ThirdPartyRecipe.init/__init__ and validate_and_normalise_user_input
(which rejects a value that isn't callable, as the pydantic config does on
0.31). The release note is a CHANGELOG.md entry under [unreleased].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…verified when verification is required (GHSA-4hf2-33qv-rq88)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
… target (GHSA-3vrp-2rj4-wvw6)

Resolve `.` and `..` path segments (also when written as %2e) in the
redirect target as in RFC 3986 section 5.2.4, for http(s) URLs and for
custom-scheme URLs whose path starts with /, and keep an empty ? or #,
as URL serialisation in node does. is_valid_frontend_redirect_uri and the
Location header both get this form, so HTTPS://App.Example.com/a/../cb
becomes https://app.example.com/cb. Document the normalised form, and say
in the release note to parse the URL instead of comparing strings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…nycode form in the Apple callback (GHSA-3vrp-2rj4-wvw6)

Convert a non-ASCII host in the configured origin / website_domain to its
IDNA A-label (punycode) form with the stdlib idna codec before comparing
it with the redirect target, whose host browsers send in that form, so
an app configured with https://bücher.example accepts redirects to
https://xn--bcher-kva.example. Redirect targets with a non-ASCII host are
still rejected. The debug log for a configured origin that can't be
compared now says it is not a valid http or https origin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…e with a 400 in the Apple callback (GHSA-3vrp-2rj4-wvw6)

A frontendRedirectURI containing a lone surrogate (e.g. \ud800 in the
JSON of the state) can't be encoded into a URL. Reject it while decoding
the state, so the callback answers with a 400 instead of a 500, in the
path, query and fragment alike, and is_valid_frontend_redirect_uri isn't
called for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
… targets and name userinfo in the error (GHSA-3vrp-2rj4-wvw6)

Document in TypeIsValidFrontendRedirectURIFunction that URLs with a
backslash, whitespace or a control character, and http(s) URLs with a
username or password or a non-ASCII host, are always rejected without
calling is_valid_frontend_redirect_uri, and tell hook authors to parse
the URL and compare its parts instead of comparing strings. An http(s)
redirect target with a username or password is now rejected with a
message saying so, instead of one about its scheme. The status code and
which targets are rejected are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…ert to another punycode form in the Apple callback (GHSA-3vrp-2rj4-wvw6)

Python's idna codec implements IDNA 2003, which maps ß to ss and ς to σ
and removes zero width (non-)joiners, while browsers keep them (UTS #46).
So a configured origin https://straße.example was compared as
strasse.example, a different host than the xn--strae-oqa.example that
browsers open. Treat a configured origin whose punycode form doesn't
decode back to its host (the conversion changed the host instead of only
encoding it) as not comparable, which covers these characters and the
other ones IDNA 2003 maps, so the callback answers with a 400 without a
custom check. Log at debug level that such an origin has to be
configured in its punycode (xn--) form, and correct the docstring and
the release note, which said the converted form is the one browsers use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…Apple callback (GHSA-3vrp-2rj4-wvw6)

Browsers percent-decode the host of http(s) URLs, so for a target like
https://app%2eexample.com/ they open app.example.com, while urllib (and
so is_valid_frontend_redirect_uri) sees the host app%2eexample.com.
Reject http(s) URLs with a % in the host in get_normalised_origin_tuple,
before is_valid_frontend_redirect_uri is called, and add them to the
documented list of always rejected targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…h prefix in the Apple callback docstrings (GHSA-3vrp-2rj4-wvw6)

Only C0 control characters, DEL and whitespace are rejected, not every
control character, so say that. Document that
normalise_frontend_redirect_uri adds /. in front of the path of a URL
without an authority when the path would otherwise start with //.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…ct_uri that isn't a function (GHSA-3vrp-2rj4-wvw6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…n sign in/up APIs (T1)

The thirdparty signinup, emailpassword signup/signin and passwordless
create code APIs now load the session and settle the tenant first, then
resolve the provider and run the form/email/phone validators for that
tenant. The session tenant still takes precedence over the path tenant.

Backport to 0.30: the test's async client fixture uses
pytest_asyncio.fixture, since 0.30 runs pytest-asyncio in strict mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
The GitHub provider's get_user_info now calls the configured
validate_access_token (the default checks the token against GitHub's
application token API) before fetching user info.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…nverified credentials (T4)

recover_account_post now loads the generated options before consuming
the token and rejects options generated for a different email with
INVALID_OPTIONS_ERROR. It runs is_sign_up_allowed before creating a new
webauthn recipe user, and if the recovered recipe user's email was not
verified before the recovery, it removes the user's existing credentials
and revokes its sessions before marking the email verified and linking.

Backport to 0.30: 0.30 has no list or remove credential APIs, and its
register credential API loads the session with
load_session_in_auth_api_if_needed, so the database check added there
covers it. The tests for the list and remove credential APIs are left out,
and the release note names only the register credential API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…r api key strictly (T5)

An empty or whitespace-only dashboard api_key is now normalised to None, so
the dashboard uses dashboard sessions, matching the reported auth mode.
validate_api_key now requires the "Bearer " prefix, rejects an empty key and
compares keys in constant time.

Backport to 0.30: validate_and_normalise_user_input takes api_key and
admins as arguments on 0.30 (no pydantic DashboardConfig), so the
normalisation and its test use those.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
… per-request providers (T6)

merge_config, merge_providers_from_core_and_static, create_provider and
GenericProvider._normalize_input now work on copies of the provider config
(including the user info map and clients), so per-tenant overrides and the
defaults filled in by built-in providers no longer change the static config
passed to the thirdparty recipe. The dashboard third party config GET API
also copies the static providers and builds new client objects instead of
writing query params into them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…verified device matches (T8)

remove_device_post now requires completed MFA whenever the user has a
verified device, unless the name exactly matches an unverified device and
does not match any verified device under case-insensitive, NFKC normalised
comparison.

Backport to 0.30: the test imports the 0.30 names of the multifactorauth and
totp override configs (OverrideConfig).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
is_fake_email now trims and lowercases the email before checking the
fake email domains, matching how emails are compared elsewhere. The
emailpassword sign in API also checks the stored email of the matched
login method.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
… all codes (T10)

revoke_all_codes(phone_number=...) now sends the phone number to the core
under phoneNumber instead of email, so the codes of that phone number are
revoked. When both email and phone_number are passed, both are sent and
the core rejects the request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
porcellus and others added 12 commits October 16, 2026 18:35
should_allow_access now also applies the admins check when the request is
routed to the unlink API, which is a GET request that modifies the user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
… type (M1)

validate_access_token_structure now rejects payloads that have an stt
value other than 0, and stt is a protected access token payload prop.

Backport to 0.30: the new test module sets pytestmark = pytest.mark.asyncio,
since 0.30 doesn't run pytest with asyncio_mode = "auto".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…without a verified id token (M2)

When a specific hd is configured, the Google Workspaces provider now
requires either a verified id token (whose hd is checked by
validate_id_token_payload) or a matching hd value in the user info
response, and rejects the sign in otherwise.

Backport to 0.30: the new test module sets pytestmark = mark.asyncio, since
0.30 doesn't run pytest with asyncio_mode = "auto".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…ms (M4)

A token whose kid is not in a fresh JWKS cache now only triggers a new
fetch from the core if the last fetch was at least 500ms ago; otherwise
verification fails straight away with TRY_REFRESH_TOKEN. Fetches are
serialised with a separate lock, so the cache read/write lock is no
longer held during the HTTP request.

Backport to 0.30: get_latest_keys keeps taking the 0.30 SessionConfig, and
the new test module sets pytestmark = pytest.mark.asyncio, since 0.30
doesn't run pytest with asyncio_mode = "auto".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…_claims_for_session_handle (M7)

Pass the session's tenant when handle-based claim validation refetches a claim value, and drop protected props from the payload update before merging it.

Backport to 0.30: the new test module sets pytestmark = mark.asyncio, since
0.30 doesn't run pytest with asyncio_mode = "auto".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…allowed first factors (M9)

is_valid_first_factor now only falls back to the static or all available
first factors when the tenant's first_factors is None, so a tenant with
an empty first_factors list rejects every first factor.

Backport to 0.30: FactorIds has no get_all_factors on 0.30, so the test
lists the factor ids itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
… in the release note (M1)

Tell apps that set a custom access token claim named stt to rename it
before upgrading, and that existing sessions carrying it get a 401 and
can't be refreshed or signed out until they are repaired with
merge_into_access_token_payload(session_handle, {}), which removes the
now-protected claim, or revoked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…ck without an id token (M2)

With a specific hd configured, a Google Workspaces sign in without an id
token is now only accepted if the user info response has the same hd.
Sign ins with an id token are checked against its hd claim as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
… in the release note (M1)

stt can still be set with merge_into_access_token_payload: it only
removes an existing stt claim, and stores one passed in its update.
create_new_session and claim validation drop it. Say that instead of
saying stt can no longer be set as a custom claim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…ession's tenant in validate_claims_for_session_handle (M7)

Note under Changed that validate_claims_for_session_handle now refetches
claim values for the session's own tenant and drops protected claims
from the payload update it merges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
…ipe.reset (M4)

The JWKS cache and its refetch time are module level. Without this, a test (or a
test server init) that switches to a new core app keeps the previous app's keys, and
a token from the new app that arrives within 500ms of the previous fetch is rejected
with try-refresh instead of fetching the new keys. That made unrelated session tests
fail at random. Node and golang already reset their JWKS cache on session reset.

test_session_verification_of_jwt_with_dynamic_signing_key_mode_works_as_expected
re-initialised on a new core app and only found the old token's key because the
cache leaked across reset(); it now re-initialises on the same core app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174ThwKR7SF8P4J7E79RigE

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@porcellus
porcellus merged commit 43b9742 into 0.30 Oct 21, 2026
15 of 19 checks passed
@porcellus
porcellus deleted the release/v0.30.4 branch October 21, 2026 14:49
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.

2 participants