Summary
In raw-username mode (OIDC_USE_RAW_USERNAME=true), Yuxi stores the OIDC binding placeholder as:
oidc:{sub}:{target_user_id}
but later resolves the bound user by splitting that string on : and reading a fixed segment as the numeric user id.
If the IdP emits a sub that itself contains :, the placeholder no longer resolves back to the real bound user.
Affected code
backend/package/yuxi/services/oidc_service.py
find_user_by_oidc_sub(...)
find_deleted_oidc_user_by_sub(...)
_create_oidc_binding_placeholder(...)
Why this is a real bug
The docs describe the placeholder record as the mechanism that preserves the binding between an OIDC identity and a local Yuxi account in raw-username mode.
That mechanism works when sub does not contain :, but breaks when it does.
So this is not only a parsing edge case — it can cause a previously bound account to fail login with a false conflict.
Minimal repro
I reproduced this locally on current main (c47a0192) with the current implementation.
Repro 1: direct binding lookup
- Create a local user
alice
- Create the OIDC binding placeholder for
sub = tenant:user
- Call
find_user_by_oidc_sub(db, 'tenant:user')
Actual result
It returns the deleted placeholder record:
resolved_user_id = oidc:tenant:user:1
resolved_is_deleted = 1
instead of the real bound user.
Expected result
It should resolve back to the original bound user alice.
User-visible impact
I also reproduced the callback path with a minimal mocked OIDC exchange/userinfo flow.
When the existing bound account uses a sub containing :, the callback redirects to:
/login?oidc_error=OIDC标识已绑定到其他账号,请联系管理员处理绑定冲突
So an already-bound account is treated as if it were bound to a different user.
Root cause
The placeholder format is suffix-based (...:{target_user_id}), but the reader treats it as if every : before the numeric suffix were structural.
That assumption is unsafe for sub values containing :.
Suggested fix direction
A minimal fix would be to keep the existing placeholder format and parse the numeric user id from the right-hand side (for example, right-splitting once), instead of reading a fixed segment after a full split.
Summary
In raw-username mode (
OIDC_USE_RAW_USERNAME=true), Yuxi stores the OIDC binding placeholder as:oidc:{sub}:{target_user_id}but later resolves the bound user by splitting that string on
:and reading a fixed segment as the numeric user id.If the IdP emits a
subthat itself contains:, the placeholder no longer resolves back to the real bound user.Affected code
backend/package/yuxi/services/oidc_service.pyfind_user_by_oidc_sub(...)find_deleted_oidc_user_by_sub(...)_create_oidc_binding_placeholder(...)Why this is a real bug
The docs describe the placeholder record as the mechanism that preserves the binding between an OIDC identity and a local Yuxi account in raw-username mode.
That mechanism works when
subdoes not contain:, but breaks when it does.So this is not only a parsing edge case — it can cause a previously bound account to fail login with a false conflict.
Minimal repro
I reproduced this locally on current
main(c47a0192) with the current implementation.Repro 1: direct binding lookup
alicesub = tenant:userfind_user_by_oidc_sub(db, 'tenant:user')Actual result
It returns the deleted placeholder record:
resolved_user_id = oidc:tenant:user:1resolved_is_deleted = 1instead of the real bound user.
Expected result
It should resolve back to the original bound user
alice.User-visible impact
I also reproduced the callback path with a minimal mocked OIDC exchange/userinfo flow.
When the existing bound account uses a
subcontaining:, the callback redirects to:/login?oidc_error=OIDC标识已绑定到其他账号,请联系管理员处理绑定冲突So an already-bound account is treated as if it were bound to a different user.
Root cause
The placeholder format is suffix-based (
...:{target_user_id}), but the reader treats it as if every:before the numeric suffix were structural.That assumption is unsafe for
subvalues containing:.Suggested fix direction
A minimal fix would be to keep the existing placeholder format and parse the numeric user id from the right-hand side (for example, right-splitting once), instead of reading a fixed segment after a full split.