Skip to content

fix(oauth): fix OIDC and custom OAuth binding flows - #5216

Open
TomyJan wants to merge 15 commits into
QuantumNous:mainfrom
TomyJan:fix/oauth-oidc-custom-oauth
Open

fix(oauth): fix OIDC and custom OAuth binding flows#5216
TomyJan wants to merge 15 commits into
QuantumNous:mainfrom
TomyJan:fix/oauth-oidc-custom-oauth

Conversation

@TomyJan

@TomyJan TomyJan commented May 31, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

  • 请提供人工撰写的简洁摘要,避免直接粘贴未经整理的 AI 输出。

📝 变更描述 / Description

  • 修复 OIDC / 自定义 OAuth 回调地址生成逻辑:优先从请求头推导回调来源,再回退到 ServerAddress
  • 增强 OIDC 获取 token 失败时的诊断信息,记录并返回上游 token 接口错误详情
  • 修复自定义 OAuth 在用户中心绑定时错误使用 provider 数字 id 的问题,改为使用 provider slug
  • 修复自定义 OAuth 绑定流程,改为正常 OAuth 授权跳转并携带 state
  • 修复管理员“账户绑定管理”弹窗中自定义 OAuth 不显示外部用户 ID 的问题
  • 增加自定义 OAuth 自动关联已有用户策略:
    • none
    • email_verified
    • username
  • 删除用户时同步清理自定义 OAuth 绑定关系 已在 fix(oauth): clear bindings when hard deleting users #5582 修复
  • 增加自定义 OAuth 管理 UI 配置项、相关多语言翻译,以及内置 OIDC 使用说明

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

#5215

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

涉及流程问题, 不太好用截图表示. 已经自测相关修复全部生效

Summary by CodeRabbit

  • New Features

    • Added an “auto-link existing users” setting for Custom OAuth providers (disabled, email-verified, or username-based).
    • OAuth sign-in can now auto-link existing accounts for custom OAuth providers according to the selected policy.
    • Updated binding UI to display more provider details (including the linked provider user id) consistently.
  • Bug Fixes

    • Deleting a user now also removes their custom OAuth bindings.
    • OAuth/OIDC sign-in now surfaces clearer errors for invalid or incomplete provider responses.
  • Documentation

    • Added clearer in-app guidance and localized text about built-in OIDC vs. Custom OAuth.

- Derive OAuth redirect URIs from request headers before falling back to ServerAddress
- Improve OIDC token exchange diagnostics
- Fix custom OAuth profile binding to use provider slug and OAuth state
- Show custom OAuth provider user IDs in admin binding management
- Add configurable auto-link policy for custom OAuth providers
- Remove custom OAuth bindings when users are deleted
- Add custom OAuth UI controls, translations, and built-in OIDC guidance
@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a configurable auto_link_policy field to custom OAuth providers, enables backend auto-linking of OAuth logins to existing local users, introduces a shared redirect URI helper, refactors OIDC token decoding, updates user-binding cleanup and related frontend contracts, and adds UI text across six locales.

Changes

Custom OAuth Auto-Link Feature

Layer / File(s) Summary
Data model & controller contracts
model/custom_oauth_provider.go, controller/custom_oauth.go
AutoLinkPolicy is added to the custom OAuth provider model, validated against allowed values, and passed through custom provider create/update/response mapping.
OAuth auto-link engine
controller/oauth.go, oauth/generic.go
OAuth user resolution now attempts policy-based auto-linking for generic OAuth providers before user creation; the helper queries by verified email or username and creates or updates the binding when a matching enabled user is found.
Redirect URI & OIDC token handling
oauth/redirect_uri.go, oauth/generic.go, oauth/oidc.go
OAuth redirect URIs are built from request context with fallback to server settings; OIDC token exchange now reads raw response bodies, surfaces OAuth error fields, and decodes user info with the shared JSON helper.
User deletion cleanup & tests
model/user.go, model/user_oauth_binding_delete_test.go
User deletion now removes OAuth bindings first, and tests cover binding removal for both soft and hard delete paths.
Frontend API types & contracts
web/default/src/features/profile/api.ts, web/default/src/features/users/api.ts, web/default/src/features/system-settings/auth/custom-oauth/types.ts
OAuth binding types now use numeric provider IDs and new provider metadata fields; custom OAuth provider types and form schema include auto_link_policy plus selectable options.
Admin UI: provider form
web/default/src/features/system-settings/auth/custom-oauth/components/provider-form-dialog.tsx, web/default/src/features/system-settings/auth/oauth-section.tsx
The custom OAuth provider form adds an auto_link_policy select with create/edit defaults, and the OIDC settings tab adds descriptive text about built-in OIDC vs. Custom OAuth.
Account binding UI & client OAuth flow
web/default/src/features/profile/components/tabs/account-bindings-tab.tsx, web/default/src/features/users/components/dialogs/user-binding-dialog.tsx
The custom OAuth bind flow now builds an authorization URL client-side, and binding displays switch to provider_user_id with numeric provider IDs in dialog typing.
i18n strings
web/default/src/i18n/locales/*.json
New translation keys are added for auto-link policy labels and guidance text across en, fr, ja, ru, vi, and zh.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • QuantumNous/new-api#2857: Introduced the custom OAuth provider CRUD that this PR extends with auto_link_policy fields and validation.
  • QuantumNous/new-api#5582: Shares the user-deletion path change that removes UserOAuthBinding records before deleting a user.

Suggested reviewers

  • seefs001
  • Calcium-Ion

Poem

🐇 Hop-hop, the bindings now know their way,
By email or username, they may stay.
The redirect trail is built with care,
And OIDC clues are caught mid-air.
This bunny says: “Linked, cleaned, and neat!” 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main OAuth/OIDC binding flow fixes in this change set.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
oauth/oidc.go (1)

158-158: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use common.DecodeJson() instead of json.NewDecoder() for consistency.

Line 158 directly uses json.NewDecoder(res.Body).Decode(&oidcUser), which violates the coding guideline requiring wrapper functions from common/json.go. The same file's ExchangeToken method was updated to use common.Unmarshal (line 101), but GetUserInfo was left unchanged.

♻️ Proposed fix to align with coding guidelines
-	var oidcUser oidcUser
-	err = json.NewDecoder(res.Body).Decode(&oidcUser)
-	if err != nil {
+	body, err := io.ReadAll(res.Body)
+	if err != nil {
+		logger.LogError(ctx, fmt.Sprintf("[OAuth-OIDC] GetUserInfo read body error: %s", err.Error()))
+		return nil, err
+	}
+
+	var oidcUser oidcUser
+	if err := common.DecodeJson(io.NopCloser(strings.NewReader(string(body))), &oidcUser); err != nil {
 		logger.LogError(ctx, fmt.Sprintf("[OAuth-OIDC] GetUserInfo decode error: %s", err.Error()))
 		return nil, err
 	}

Or more simply, following the pattern used in ExchangeToken:

+	body, err := io.ReadAll(res.Body)
+	if err != nil {
+		logger.LogError(ctx, fmt.Sprintf("[OAuth-OIDC] GetUserInfo read body error: %s", err.Error()))
+		return nil, err
+	}
+
 	var oidcUser oidcUser
-	err = json.NewDecoder(res.Body).Decode(&oidcUser)
-	if err != nil {
+	if err := common.Unmarshal(body, &oidcUser); err != nil {
 		logger.LogError(ctx, fmt.Sprintf("[OAuth-OIDC] GetUserInfo decode error: %s", err.Error()))
 		return nil, err
 	}

As per coding guidelines: All JSON marshal/unmarshal operations MUST use wrapper functions from common/json.go: common.Marshal(), common.Unmarshal(), common.UnmarshalJsonStr(), common.DecodeJson(), or common.GetJsonType(). Do NOT directly import or call encoding/json in business code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@oauth/oidc.go` at line 158, GetUserInfo currently decodes the HTTP response
with json.NewDecoder(res.Body).Decode(&oidcUser) which breaks the guideline;
replace that call with the wrapper common.DecodeJson(res.Body, &oidcUser)
(matching the pattern used in ExchangeToken which uses common.Unmarshal) and
remove any direct encoding/json usage from this function so all JSON decoding
uses the common package.
web/default/src/features/users/components/dialogs/user-binding-dialog.tsx (1)

250-278: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Type error: label fallback now yields string | number.

Changing custom_oauth_providers[].id to number (Line 88) breaks the two label assignments below, since BindingItem.label is typed string (Line 71):

  • Line 255: provider.name || provider.idstring | number
  • Line 269: binding.provider_name || binding.provider_idstring | number (provider_id is now number)

bun run typecheck will fail on both. Wrap the numeric fallback in String(...).

🐛 Proposed fix
-        label: provider.name || provider.id,
+        label: provider.name || String(provider.id),
-          label: binding.provider_name || binding.provider_id,
+          label: binding.provider_name || String(binding.provider_id),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/users/components/dialogs/user-binding-dialog.tsx`
around lines 250 - 278, The label fallbacks can be number|string now because
custom_oauth_providers[].id and binding.provider_id are numbers; update the two
label assignments so they always produce strings: in the loop over
customProviders set label to provider.name || String(provider.id), and in the
loop over oauthBindings set label to binding.provider_name ||
String(binding.provider_id). Locate the code that builds the items array (the
customProviders loop and the oauthBindings loop that push into items,
referencing variables customProviders, oauthBindings, oauthBindingMap, items and
the BindingItem.label type) and wrap the numeric fallbacks with String(...)
accordingly.
web/default/src/features/system-settings/auth/custom-oauth/components/provider-form-dialog.tsx (1)

134-154: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add missing auto_link_policy field to new provider reset.

When resetting the form for a new provider (no existing provider), the auto_link_policy field is missing from the reset object. For consistency with the editing flow (line 130) and other fields like auth_style (line 151), this field should be explicitly set.

Proposed fix
       well_known: '',
       auth_style: 0,
+      auto_link_policy: 'none',
       access_policy: '',
       access_denied_message: '',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/system-settings/auth/custom-oauth/components/provider-form-dialog.tsx`
around lines 134 - 154, The form reset for creating a new provider in the
provider-form-dialog component omits the auto_link_policy field; update the
branch that calls form.reset when props.open && !props.provider to include an
explicit auto_link_policy entry (matching the edited-provider default used when
props.provider exists), so add auto_link_policy alongside fields like auth_style
in the object passed to form.reset to ensure consistent defaults between create
and edit flows.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/default/src/features/profile/components/tabs/account-bindings-tab.tsx`:
- Around line 118-137: The popup is opened only after an await (getOAuthState),
which can cause browsers to block it; modify handleBindCustomOAuth so it opens
the popup synchronously before any awaits (call window.open immediately to get a
popup window reference), then perform getOAuthState() and build the
authorization URL and set popup.location.href (or fallback to
popup.opener.location) to the final URL; ensure you still return early if
provider lacks required fields but do this check before opening the popup, and
reference handleBindCustomOAuth, getOAuthState, provider.authorization_endpoint,
provider.client_id, provider.slug and window.open when making the change.

In `@web/default/src/features/system-settings/auth/custom-oauth/types.ts`:
- Around line 228-232: AUTO_LINK_POLICY_OPTIONS uses full-sentence labelKey
strings; change each labelKey to hierarchical i18n keys (e.g., replace 'Do not
auto-link existing users' -> 'auth.oauth.autoLink.none', 'Auto-link by email' ->
'auth.oauth.autoLink.emailVerified', 'Auto-link by username' ->
'auth.oauth.autoLink.username') and update the corresponding locale files
(en/fr/ja/ru/vi/zh) to add these keys and translations; also search usages of
AUTO_LINK_POLICY_OPTIONS and any direct references to the old sentence keys and
update them to use the new hierarchical keys so i18n lookups continue to work.

In `@web/default/src/features/system-settings/auth/oauth-section.tsx`:
- Around line 451-455: Replace the hard-coded English sentence passed to the
translation function in the FormDescription (the t(...) call inside the
component rendering the OIDC description) with a hierarchical translation key
like 'auth.oauth.oidc.builtInDescription', and add that key to the project's
locale files (all supported locales) with the original full descriptive text as
the value so the UI shows the same message via
t('auth.oauth.oidc.builtInDescription').

---

Outside diff comments:
In `@oauth/oidc.go`:
- Line 158: GetUserInfo currently decodes the HTTP response with
json.NewDecoder(res.Body).Decode(&oidcUser) which breaks the guideline; replace
that call with the wrapper common.DecodeJson(res.Body, &oidcUser) (matching the
pattern used in ExchangeToken which uses common.Unmarshal) and remove any direct
encoding/json usage from this function so all JSON decoding uses the common
package.

In
`@web/default/src/features/system-settings/auth/custom-oauth/components/provider-form-dialog.tsx`:
- Around line 134-154: The form reset for creating a new provider in the
provider-form-dialog component omits the auto_link_policy field; update the
branch that calls form.reset when props.open && !props.provider to include an
explicit auto_link_policy entry (matching the edited-provider default used when
props.provider exists), so add auto_link_policy alongside fields like auth_style
in the object passed to form.reset to ensure consistent defaults between create
and edit flows.

In `@web/default/src/features/users/components/dialogs/user-binding-dialog.tsx`:
- Around line 250-278: The label fallbacks can be number|string now because
custom_oauth_providers[].id and binding.provider_id are numbers; update the two
label assignments so they always produce strings: in the loop over
customProviders set label to provider.name || String(provider.id), and in the
loop over oauthBindings set label to binding.provider_name ||
String(binding.provider_id). Locate the code that builds the items array (the
customProviders loop and the oauthBindings loop that push into items,
referencing variables customProviders, oauthBindings, oauthBindingMap, items and
the BindingItem.label type) and wrap the numeric fallbacks with String(...)
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0adf57eb-4acf-4757-966f-73d429d41b2d

📥 Commits

Reviewing files that changed from the base of the PR and between 0c7aceb and 10e5667.

📒 Files selected for processing (22)
  • controller/custom_oauth.go
  • controller/oauth.go
  • model/custom_oauth_provider.go
  • model/task_cas_test.go
  • model/user.go
  • model/user_oauth_binding_delete_test.go
  • oauth/generic.go
  • oauth/oidc.go
  • oauth/redirect_uri.go
  • web/default/src/features/profile/api.ts
  • web/default/src/features/profile/components/tabs/account-bindings-tab.tsx
  • web/default/src/features/system-settings/auth/custom-oauth/components/provider-form-dialog.tsx
  • web/default/src/features/system-settings/auth/custom-oauth/types.ts
  • web/default/src/features/system-settings/auth/oauth-section.tsx
  • web/default/src/features/users/api.ts
  • web/default/src/features/users/components/dialogs/user-binding-dialog.tsx
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json

Comment thread web/default/src/features/profile/components/tabs/account-bindings-tab.tsx Outdated
Comment thread web/src/features/system-settings/auth/custom-oauth/types.ts
Comment thread web/src/features/system-settings/auth/oauth-section.tsx
TomyJan added 2 commits May 31, 2026 22:47
- Open the custom OAuth popup synchronously before fetching OAuth state so browsers do not block the binding window
- Decode OIDC userinfo responses through common.DecodeJson instead of encoding/json directly
- Preserve auto_link_policy defaults when resetting the custom OAuth provider create form
- Normalize custom provider IDs to strings when rendering admin binding labels
@TomyJan

TomyJan commented Jun 8, 2026

Copy link
Copy Markdown
Author

Anyone review?

@t0ng7u

@IsHPDuwu

Copy link
Copy Markdown

Is this PR still progressing?

@TomyJan

TomyJan commented Jun 24, 2026

Copy link
Copy Markdown
Author

Is this PR still in progress?

Done. Some new conflicts have been resolved.

@Ark-Aak

Ark-Aak commented Jul 12, 2026

Copy link
Copy Markdown

I'm waiting for this PR to be merged.

@LobsterEnigma

Copy link
Copy Markdown

No one resolved the conflicts? The issue described as “修复自定义 OAuth 在用户中心绑定时错误使用 provider 数字 ID 的问题,改为使用 provider slug” still occurs in version rc.21

@TomyJan

TomyJan commented Jul 27, 2026

Copy link
Copy Markdown
Author

No one resolved the conflicts? The issue described as "Fix custom OAuth incorrectly using provider numeric ID when binding in user center, changed to use provider slug" still occurs in version rc.21

It's been handled. If needed, you can download the binary from the release on my fork.

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.

4 participants