Skip to content

feat: extend JWT direct SSO modes#3447

Open
MisonL wants to merge 13 commits intoQuantumNous:mainfrom
MisonL:feat/auth-jwt-sso-mvp-submit
Open

feat: extend JWT direct SSO modes#3447
MisonL wants to merge 13 commits intoQuantumNous:mainfrom
MisonL:feat/auth-jwt-sso-mvp-submit

Conversation

@MisonL
Copy link

@MisonL MisonL commented Mar 26, 2026

📝 变更描述 / Description

  • 增加 JWT Direct SSO 的后端与前端流程,支持浏览器登录与直连 JWT 登录。
  • 扩展 ticket_exchangeticket_validate 两种票据换 JWT 模式。
  • 支持 claims / userinfo 两类身份解析与映射。
  • 收紧回调地址与绑定流程的安全边界,避免未配置回调或禁用用户状态下的错误放行。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)
  • ✨ 新功能 (New feature)
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自撰写此描述,去除了 AI 原始输出的冗余。
  • 深度理解: 我已完全理解这些更改的工作原理及潜在影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过了测试或手动验证。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

  • go test ./oauth ./controller ./middleware -count=1
  • cd web && bun run build
  • git diff --check

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 26, 2026

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 new "JWT-direct" custom OAuth provider path (ticket-exchange/validation, JWKS/PEM verification, userinfo parsing), a JWT login/bind controller and API route, expands custom provider schema/validation/registry handling, refactors session identity syncing and bind/login flows, adds frontend support and many tests across layers.

Changes

Cohort / File(s) Summary
JWT Direct implementation & tests
oauth/jwt_direct.go, oauth/jwt_direct_test.go, oauth/provider.go
Introduces JWTDirectProvider, ticket acquire/validate flows, JWKS/PEM verification, claim→user/group/role mapping, and tests; adds CustomBindingProvider interface. Review cryptography, JWKS selection, claim mapping, and HTTP payload modes.
JWT login controller, route & tests
controller/custom_oauth_jwt.go, controller/custom_oauth_jwt_test.go, router/api-router.go
New HandleCustomOAuthJWTLogin handler + POST /auth/external/:provider/jwt/login, state/session validation, identity resolution, bind/login branching, auditing, and extensive tests. Check session/state validation, audit redaction, and bind vs login paths.
Custom provider model, controller & update tests
model/custom_oauth_provider.go, controller/custom_oauth.go, controller/custom_oauth_update_test.go
Expands CustomOAuthProvider schema (kind, JWT/ticket, mapping, flags), adds getters/validation, pointer-based update shape, response mapping, and cache invalidation on create/update/delete. Review validation rules and pointer-update behavior.
Registry behavior & tests
oauth/registry.go, oauth/registry_test.go
Registry now registers only oauth_code providers and removes non-oauth_code entries; tests validate register/remove semantics. Check provider lifecycle and slug tracking.
OAuth controller, bind/session tests & bind handlers
controller/oauth.go, controller/bind_session_test.go, controller/wechat.go, controller/telegram.go, controller/user.go
Refactors find-or-create flow, email-merge, bind-after-status-check; bind handlers now use getSessionUser; session-bound bind tests added. Review merge/conflict handling and session access changes.
Middleware session sync & tests
middleware/auth.go, middleware/auth_test.go
Middleware now loads authoritative user identity and syncs session fields on each request; new helpers and tests for immediate session invalidation/refresh. Review session-save error handling and performance implications.
Status API caching & misc
controller/misc.go
Status endpoint now serves expanded provider payload using an RW-locked cache with invalidation helper; added error handling for provider load failures. Review cache concurrency and invalidation triggers.
Frontend: OAuth UI, callback, settings & helpers
web/src/components/auth/*, web/src/components/settings/*, web/src/helpers/api.js
Adds provider kind handling, JWT/ticket UI and validation, browser-login gating, JWT callback flow (ticket/direct) with retrying, and defensive data handling. Review client-side URL/token building, encoding, and i18n changes.
Frontend minor UX & hardening
web/src/components/settings/personal/cards/AccountManagement.jsx, web/src/components/table/tokens/.../EditTokenModal.jsx
Bind button reflects browser_login_supported (API-only); defensive handling for malformed model/group API responses.
Misc: json alias, i18n, user helper
common/json.go, i18n/keys.go, i18n/locales/*, model/user.go
Adds common.RawMessage alias, new i18n keys (oauth.ticket_missing, oauth.jwt_missing) and locales, and GetUserIdentityById helper used for session sync. Review translations and helper semantics.
Tests: added integration/unit suites
controller/custom_oauth_update_test.go, controller/bind_session_test.go, middleware/auth_test.go, oauth/registry_test.go, plus many new JWT/provider tests
Large set of new tests covering provider validation, JWT direct flows, session-bound binds, middleware session-sync, and registry behavior. Review DB isolation, migrations, and test determinism.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Browser as Browser (cookies)
    participant Controller as JWT Login Controller
    participant Session as Session Store
    participant Provider as JWTDirectProvider
    participant DB as Database
    participant Auditor as Audit Logger

    Client->>Controller: POST /api/auth/external/:provider/jwt/login (state, token/ticket)
    Controller->>Session: validate state == session.oauth_state
    Controller->>Provider: ResolveIdentityFromInput(token, ticket, callbackURL, state)

    alt Ticket exchange/validate
        Provider->>Provider: HTTP exchange/validate (external ticket endpoint)
        Provider->>Provider: extract/normalize token or claims
    else Direct token
        Provider->>Provider: parse & verify JWT (PEM/JWKS)
    end

    Provider->>Controller: resolved identity (external id, user info, groups/roles)
    alt existing session user
        Controller->>DB: bind OAuth identity to session user
        Controller->>Auditor: record bind audit
        Controller-->>Client: {action: "bind", success:true}
    else new or merged user
        Controller->>DB: findOrCreate user (auto-register/email-merge)
        Controller->>DB: sync group/role if enabled
        Controller->>Auditor: record login audit
        Controller-->>Client: {action: "login", success:true}
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • Calcium-Ion
  • seefs001

🐰 I hopped through tokens, tickets, and keys,
Swapping claims into groups with the greatest of ease,
Bindings and audits stitched neat like a seam,
I nibble the bugs and dream the login dream,
Hop on — your SSO now sings in one gleam!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.03% 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
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: extending JWT direct SSO modes, which is the core objective of this changeset.
Linked Issues check ✅ Passed The PR implements JWT direct integration with JWKS/public key configuration, JWT signature verification, claims extraction and mapping, and group/role synchronization. These core requirements from issue #3334 are fully addressed in code.
Out of Scope Changes check ✅ Passed All changes are directly related to JWT direct SSO implementation: new JWT provider, auth handlers, UI components, middleware updates for session consistency, and i18n support. No unrelated changes detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (7)
oauth/jwt_direct.go (1)

3-30: Replace direct encoding/json usage with common wrappers.

Line 11 imports encoding/json directly, and it's used at lines 544 and 569. Per coding guidelines, all JSON operations should use the wrapper functions in common/json.go.

Proposed fix

Remove the direct import:

 import (
 	"bytes"
 	"context"
 	"crypto/ecdsa"
 	"crypto/ed25519"
 	"crypto/elliptic"
 	"crypto/rsa"
 	"encoding/base64"
-	"encoding/json"
 	"encoding/xml"

Update the usages:

-	if err := json.Unmarshal(body, &payload); err != nil {
+	if err := common.Unmarshal(body, &payload); err != nil {
 		return nil, fmt.Errorf("parse ticket validation json failed: %w", err)
-	var payload map[string]any
-	if err := json.Unmarshal(body, &payload); err == nil {
+	var payload any
+	if err := common.Unmarshal(body, &payload); err == nil {
 		if str, ok := payload.(string); ok {

As per coding guidelines: "All JSON marshal/unmarshal operations MUST use wrapper functions in common/json.go."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@oauth/jwt_direct.go` around lines 3 - 30, The file jwt_direct.go currently
imports encoding/json and uses json.Marshal/json.Unmarshal; remove the direct
encoding/json import and replace those calls with the project JSON wrapper
functions from common/json.go (use the common package's marshal/unmarshal
helpers — e.g., common.MarshalJSON/common.UnmarshalJSON or the actual wrapper
names in common/json.go) at the places where json.Marshal and json.Unmarshal are
invoked in jwt_direct.go, updating error handling to match the wrapper
signatures and keeping the common package import.
middleware/auth_test.go (1)

3-20: Use common JSON wrappers instead of encoding/json.

The test file imports encoding/json directly (line 4) and uses it for JSON decoding (lines 155, 179). Per coding guidelines, all JSON operations should use the wrapper functions in common/json.go.

Proposed fix
 import (
-	"encoding/json"
 	"fmt"
 	"net/http"

Then update the JSON decoding calls:

-	if err := json.NewDecoder(response.Body).Decode(&payload); err != nil {
+	if err := common.DecodeJson(response.Body, &payload); err != nil {
 		t.Fatalf("failed to decode session response: %v", err)
 	}
-	if err := json.NewDecoder(response.Body).Decode(&payload); err != nil {
+	if err := common.DecodeJson(response.Body, &payload); err != nil {
 		t.Fatalf("failed to decode api response: %v", err)
 	}

As per coding guidelines: "All JSON marshal/unmarshal operations MUST use wrapper functions in common/json.go."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@middleware/auth_test.go` around lines 3 - 20, Remove the direct import of
"encoding/json" and switch to the JSON wrapper helpers provided in the common
package (the helpers from common/json.go). Replace any usage of encoding/json
functions such as json.Unmarshal and json.NewDecoder(...).Decode with the
corresponding common helpers (use the common package's JSON decode/unmarshal
functions) in the test file, and update the imports to include the common
package instead of encoding/json so all JSON operations use the common wrapper.
oauth/registry.go (1)

130-134: Consider adding a log when non-oauth_code providers are removed in incremental updates.

The delete-and-return path is correct, but currently silent; a short log would improve traceability during provider kind transitions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@oauth/registry.go` around lines 130 - 134, When config.IsOAuthCode() returns
false and you remove the provider from providers and customProviderSlugs, add a
short log message that records the provider slug and that it was removed due to
non-oauth_code kind; e.g., call the module's logger (or log.Printf) before the
deletes to emit something like "removing provider <config.Slug>: not oauth_code"
so incremental updates are traceable for config.IsOAuthCode(), providers,
customProviderSlugs, and config.Slug.
oauth/registry_test.go (1)

9-28: Cover the unregister-on-update branch too.

This test starts from an empty registry, so it never exercises the delete(...) path in RegisterOrUpdateCustomProvider when an existing oauth_code provider is updated to jwt_direct. A regression there would still pass.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@oauth/registry_test.go` around lines 9 - 28, The test currently only covers
adding a jwt_direct provider from an empty registry; extend
TestRegisterOrUpdateCustomProviderSkipsJWTDirect to also exercise the
“unregister-on-update” branch by first registering a provider with Kind set to
model.CustomOAuthProviderKindOAuthCode (using RegisterOrUpdateCustomProvider
with the same slug), asserting it exists via GetProvider/IsCustomProvider, then
calling RegisterOrUpdateCustomProvider again with Kind
model.CustomOAuthProviderKindJWTDirect and asserting the provider is removed
(GetProvider returns nil and IsCustomProvider is false); keep
UnregisterCustomProvider cleanup calls to reset state.
controller/misc.go (1)

133-136: Move the provider DB lookup out of OptionMapRWMutex.

GetEnabledCustomOAuthProviders() does DB I/O while the read lock taken at Line 43 is still held. That makes every /status request keep common.OptionMap writers blocked behind database latency. Fetch the providers before taking the lock, or narrow the lock to the actual map reads.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/misc.go` around lines 133 - 136, The DB call
GetEnabledCustomOAuthProviders() is being made while holding
common.OptionMapRWMutex (read lock) which can block writers; move the call to
GetEnabledCustomOAuthProviders() so it runs before acquiring OptionMapRWMutex,
or alternatively reduce the locked section to only the map access (i.e., call
GetEnabledCustomOAuthProviders() first, then lock OptionMapRWMutex only around
reads of common.OptionMap and any use of customProviders), ensuring symbols
mentioned (GetEnabledCustomOAuthProviders, customProviders,
common.OptionMapRWMutex, common.OptionMap) are updated accordingly.
controller/bind_session_test.go (1)

92-105: Assert that unauthenticated WeChat binds never hit the upstream server.

Right now the mock always returns success, so this test still passes if WeChatBind contacts WeChat before checking the session. Tracking zero upstream requests would lock in the intended “fail before external I/O” behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/bind_session_test.go` around lines 92 - 105, The test must assert
that an unauthenticated WeChat bind does not contact the upstream server: change
the httptest.NewServer handler for wechatServer to record/atomically count
incoming requests (e.g., increment an int32 or send to a channel) instead of
just returning success, keep common.WeChatAuthEnabled,
common.WeChatServerAddress and common.WeChatServerToken as-is, call
performBindSessionTestRequest(...) and then assert the recorded request count is
zero; reference the wechatServer handler and performBindSessionTestRequest so
the assertion guarantees WeChatBind didn't call the upstream before session
checks.
web/src/components/settings/CustomOAuthSetting.jsx (1)

1378-1396: Consider simplifying the nested ternary for readability.

The rules definition uses nested ternaries that could be flattened.

Suggested simplification
                   <Form.Input
                     field='issuer'
                     label={t('Issuer')}
                     placeholder='https://issuer.example.com'
                     rules={
-                      isJWTUserInfoMode
-                        ? []
-                        : isJWTTicketValidateMode
-                          ? []
-                        : [{ required: true, message: t('请输入 Issuer') }]
+                      (isJWTUserInfoMode || isJWTTicketValidateMode)
+                        ? []
+                        : [{ required: true, message: t('请输入 Issuer') }]
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/CustomOAuthSetting.jsx` around lines 1378 - 1396,
The nested ternary in the Form.Input for field 'issuer' (in
CustomOAuthSetting.jsx) is hard to read; extract the rules logic into a small
boolean or helper (e.g., compute a variable like issuerRules or a function
shouldRequireIssuer using isJWTUserInfoMode and isJWTTicketValidateMode) and
pass that variable to the rules prop instead of the nested ternary, keeping the
existing behavior (empty array when either isJWTUserInfoMode or
isJWTTicketValidateMode is true, otherwise [{ required: true, message: t('请输入
Issuer') }]); update the extraText usage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/custom_oauth_jwt_test.go`:
- Around line 3-28: Remove the direct encoding/json import from
controller/custom_oauth_jwt_test.go and replace all usages of
json.NewDecoder().Decode(...) (in the helper functions that parse JSON
responses) with the project's wrapper common.DecodeJson(...); if the code uses
json.RawMessage, switch to common.RawMessage if available otherwise keep the
type but still parse via common.DecodeJson; update the imports to drop
encoding/json and ensure helper functions (the ones at the JSON decode call
sites) call common.DecodeJson with the same target types.

In `@controller/custom_oauth_jwt.go`:
- Around line 118-119: The audit success is being recorded before the session is
actually persisted; move the call to recordCustomOAuthJWTAudit(audit) so it runs
only after setupLogin(result.User, c) completes successfully (i.e., after
setupLogin returns without error and the session save inside it has succeeded).
Update the flow in the function containing recordCustomOAuthJWTAudit and
setupLogin to call setupLogin(result.User, c) first, check its error/return
status, and then call recordCustomOAuthJWTAudit(audit) only on success; keep the
same audit variable and context (audit, result.User, c) so the audit entry
remains identical when moved.
- Around line 67-74: The JWT-login business failures (e.g., invalid oauth_state
and other 400/403 branches) must be returned in the normal JSON envelope so
submitJWTLogin() doesn’t treat them as transport errors and retry; change the
HTTP status for these error responses from non-2xx (e.g., http.StatusForbidden /
http.StatusBadRequest) to http.StatusOK while keeping the JSON body
(success:false, message: i18n.T(...)) and still calling
recordCustomOAuthJWTAudit(audit). Update the error-response sites in this file
(the invalid state branch around the state/session check and the other 400/403
branch around lines ~125-130) to return 200 with the existing JSON payload
instead of 4xx.

In `@controller/custom_oauth.go`:
- Around line 41-48: The provider response is exposing sensitive JSON blobs
TicketExchangeHeaders and TicketExchangeExtraParams; update the serialization so
toCustomOAuthProviderResponse() (and any code paths used for read/create/update
responses) omits TicketExchangeHeaders and TicketExchangeExtraParams from
returned structs while still preserving them in storage; locate references to
the struct fields TicketExchangeHeaders and TicketExchangeExtraParams and change
the response mapping to exclude them (or explicitly whitelist safe fields) so
admin UI/read APIs no longer receive those secrets.
- Line 409: The update path currently ignores empty client_secret so it can
never be cleared; change the request model's ClientSecret from string to *string
(ClientSecret *string `json:"client_secret"`) and update the code that applies
the secret (the block that currently checks the ClientSecret string for
non-emptiness) to check for nil vs non-nil: if ClientSecret != nil then assign
the stored secret to *ClientSecret (allowing an empty string to clear it), and
if ClientSecret == nil then leave the existing secret untouched; update any
validation/serialization accordingly so API callers can send null (to keep) or
an explicit "" (to clear).

In `@middleware/auth.go`:
- Around line 126-133: The response for a failed syncSessionUserIdentity uses
http.StatusOK (200) which is inconsistent with other auth failures; change the
response status to http.StatusUnauthorized (401) and keep the same JSON body and
abort logic so it matches the handling used for loadSessionUserIdentity
failures; update the HTTP status in the block that calls
syncSessionUserIdentity(session, user) (the same branch that currently returns
200) so all authentication/authorization failures return 401.

In `@oauth/registry.go`:
- Around line 108-111: The summary log still reports the total DB rows even
after skipping configs that return false from config.IsOAuthCode(), causing a
misleading loaded-provider count; fix by introducing a loadedCount (or increment
an existing counter) inside the loop only when a provider is actually registered
(i.e., after the continue is not hit) and use that loadedCount in the final
summary log instead of the total DB rows, referencing the existing
config.IsOAuthCode(), config.Name and config.Slug skip logic so the increment
happens where providers are truly loaded.

In `@web/src/components/auth/LoginForm.jsx`:
- Around line 135-137: The code calls .some and .filter on
status.custom_oauth_providers which may not be an array; guard these accesses
with Array.isArray checks: for the browser-login check (the expression using
(status.custom_oauth_providers || []).some(...)) update it to first test
Array.isArray(status.custom_oauth_providers) and only call .some when true
(otherwise return false), and likewise for the other block that uses
status.custom_oauth_providers.filter(...) (only call .filter when
Array.isArray(...) is true, otherwise treat as empty array/result); reference
the symbol status.custom_oauth_providers and the functions using .some/.filter
to locate and fix both occurrences.

In `@web/src/components/auth/OAuth2Callback.jsx`:
- Around line 170-201: The branch handling providerKind === 'jwt_direct' only
treats jwt_acquire_mode === 'ticket_exchange' as a ticket callback; update the
jwtAcquireMode check to also treat 'ticket_validate' as a ticket callback so
ticket params are extracted and handled correctly. Specifically, in the
jwtAcquireMode conditional around pickFirstParamValue([... 'ticket','st']) and
submitJWTLogin({ state, ticket }), change the condition to include
'ticket_validate' (e.g. jwtAcquireMode === 'ticket_exchange' || jwtAcquireMode
=== 'ticket_validate') so that pickFirstParamValue, the ticket presence check,
showError/navigation, and submitJWTLogin are executed for both modes; leave the
JWT token extraction path (pickFirstParamValue([...
'id_token','token','jwt','access_token']) and submitJWTLogin({ state, id_token:
jwtToken })) unchanged.

In `@web/src/components/auth/RegisterForm.jsx`:
- Around line 133-135: Guard against non-array shapes for
status.custom_oauth_providers before calling array methods: replace direct uses
like (status.custom_oauth_providers || []).some(...) and .filter(...) with a
safe check/coercion such as const customProviders =
Array.isArray(status.custom_oauth_providers) ? status.custom_oauth_providers :
[]; then call customProviders.some(...) and customProviders.filter(...). Update
all occurrences in RegisterForm (references: status.custom_oauth_providers, the
some/filter usages) to use this Array.isArray guard so malformed but truthy
values won't throw.

In `@web/src/helpers/api.js`:
- Around line 55-71: The helper supportsCustomProviderBrowserLogin currently
only treats jwt_acquire_mode === 'ticket_exchange' as the ticket flow; update
the condition(s) in supportsCustomProviderBrowserLogin (and the similar block
around the other occurrence) to treat both 'ticket_exchange' and
'ticket_validate' as ticket-based flows so that providers with jwt_acquire_mode
=== 'ticket_validate' will return true when authorization_endpoint is present;
locate getCustomProviderKind(provider) usage and change the check from ===
'ticket_exchange' to check for either mode (e.g., provider?.jwt_acquire_mode ===
'ticket_exchange' || provider?.jwt_acquire_mode === 'ticket_validate') so the
browser login and authorize URL generation follow the ticket flow for both
modes.
- Around line 75-79: The hard-coded Chinese error strings in
web/src/helpers/api.js should be localized; replace direct Error messages (e.g.,
the throws for missing/invalid auth endpoint URL and the other occurrences noted
at lines 103-108 and 398-430) to use the shared i18n instance or return
translation keys instead of literal text. Update the helper (the validation
function that throws these Errors) to call the shared i18n translate function
(or throw an object containing a stable translation key like
"error.authEndpoint.missing"/"error.authEndpoint.invalid") and ensure callers or
UI components resolve the key via react-i18next so locale switching works
consistently. Ensure all other similar throws in this file follow the same
pattern.

---

Nitpick comments:
In `@controller/bind_session_test.go`:
- Around line 92-105: The test must assert that an unauthenticated WeChat bind
does not contact the upstream server: change the httptest.NewServer handler for
wechatServer to record/atomically count incoming requests (e.g., increment an
int32 or send to a channel) instead of just returning success, keep
common.WeChatAuthEnabled, common.WeChatServerAddress and
common.WeChatServerToken as-is, call performBindSessionTestRequest(...) and then
assert the recorded request count is zero; reference the wechatServer handler
and performBindSessionTestRequest so the assertion guarantees WeChatBind didn't
call the upstream before session checks.

In `@controller/misc.go`:
- Around line 133-136: The DB call GetEnabledCustomOAuthProviders() is being
made while holding common.OptionMapRWMutex (read lock) which can block writers;
move the call to GetEnabledCustomOAuthProviders() so it runs before acquiring
OptionMapRWMutex, or alternatively reduce the locked section to only the map
access (i.e., call GetEnabledCustomOAuthProviders() first, then lock
OptionMapRWMutex only around reads of common.OptionMap and any use of
customProviders), ensuring symbols mentioned (GetEnabledCustomOAuthProviders,
customProviders, common.OptionMapRWMutex, common.OptionMap) are updated
accordingly.

In `@middleware/auth_test.go`:
- Around line 3-20: Remove the direct import of "encoding/json" and switch to
the JSON wrapper helpers provided in the common package (the helpers from
common/json.go). Replace any usage of encoding/json functions such as
json.Unmarshal and json.NewDecoder(...).Decode with the corresponding common
helpers (use the common package's JSON decode/unmarshal functions) in the test
file, and update the imports to include the common package instead of
encoding/json so all JSON operations use the common wrapper.

In `@oauth/jwt_direct.go`:
- Around line 3-30: The file jwt_direct.go currently imports encoding/json and
uses json.Marshal/json.Unmarshal; remove the direct encoding/json import and
replace those calls with the project JSON wrapper functions from common/json.go
(use the common package's marshal/unmarshal helpers — e.g.,
common.MarshalJSON/common.UnmarshalJSON or the actual wrapper names in
common/json.go) at the places where json.Marshal and json.Unmarshal are invoked
in jwt_direct.go, updating error handling to match the wrapper signatures and
keeping the common package import.

In `@oauth/registry_test.go`:
- Around line 9-28: The test currently only covers adding a jwt_direct provider
from an empty registry; extend TestRegisterOrUpdateCustomProviderSkipsJWTDirect
to also exercise the “unregister-on-update” branch by first registering a
provider with Kind set to model.CustomOAuthProviderKindOAuthCode (using
RegisterOrUpdateCustomProvider with the same slug), asserting it exists via
GetProvider/IsCustomProvider, then calling RegisterOrUpdateCustomProvider again
with Kind model.CustomOAuthProviderKindJWTDirect and asserting the provider is
removed (GetProvider returns nil and IsCustomProvider is false); keep
UnregisterCustomProvider cleanup calls to reset state.

In `@oauth/registry.go`:
- Around line 130-134: When config.IsOAuthCode() returns false and you remove
the provider from providers and customProviderSlugs, add a short log message
that records the provider slug and that it was removed due to non-oauth_code
kind; e.g., call the module's logger (or log.Printf) before the deletes to emit
something like "removing provider <config.Slug>: not oauth_code" so incremental
updates are traceable for config.IsOAuthCode(), providers, customProviderSlugs,
and config.Slug.

In `@web/src/components/settings/CustomOAuthSetting.jsx`:
- Around line 1378-1396: The nested ternary in the Form.Input for field 'issuer'
(in CustomOAuthSetting.jsx) is hard to read; extract the rules logic into a
small boolean or helper (e.g., compute a variable like issuerRules or a function
shouldRequireIssuer using isJWTUserInfoMode and isJWTTicketValidateMode) and
pass that variable to the rules prop instead of the nested ternary, keeping the
existing behavior (empty array when either isJWTUserInfoMode or
isJWTTicketValidateMode is true, otherwise [{ required: true, message: t('请输入
Issuer') }]); update the extraText usage unchanged.
🪄 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: 08412f6e-9eab-4985-b0de-cac63d6c2c83

📥 Commits

Reviewing files that changed from the base of the PR and between 22b6b16 and 7f38d8a.

📒 Files selected for processing (26)
  • controller/bind_session_test.go
  • controller/custom_oauth.go
  • controller/custom_oauth_jwt.go
  • controller/custom_oauth_jwt_test.go
  • controller/custom_oauth_update_test.go
  • controller/misc.go
  • controller/oauth.go
  • controller/telegram.go
  • controller/user.go
  • controller/wechat.go
  • middleware/auth.go
  • middleware/auth_test.go
  • model/custom_oauth_provider.go
  • oauth/jwt_direct.go
  • oauth/jwt_direct_test.go
  • oauth/provider.go
  • oauth/registry.go
  • oauth/registry_test.go
  • router/api-router.go
  • web/src/components/auth/LoginForm.jsx
  • web/src/components/auth/OAuth2Callback.jsx
  • web/src/components/auth/RegisterForm.jsx
  • web/src/components/settings/CustomOAuthSetting.jsx
  • web/src/components/settings/personal/cards/AccountManagement.jsx
  • web/src/components/table/tokens/modals/EditTokenModal.jsx
  • web/src/helpers/api.js

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
web/src/components/settings/CustomOAuthSetting.jsx (1)

164-212: ⚠️ Potential issue | 🟡 Minor

Use Chinese source keys for the new translations.

These additions pass English strings to t() (Query, OAuth 2.0 / OIDC Code Flow, OAuth Code, Configuration, etc.). In this repo that won’t line up with the locale JSON structure, so the new UI text will stay untranslated or create a second key style.

As per coding guidelines, "Translation files in web/src/i18n/locales/{lang}.json must be flat JSON with Chinese source strings as keys. Use useTranslation() hook and call t('中文key') in components."

Also applies to: 258-294, 779-785, 929-953

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/CustomOAuthSetting.jsx` around lines 164 - 212,
The option and label constants (CUSTOM_OAUTH_KIND_OPTIONS, JWT_SOURCE_OPTIONS,
JWT_ACQUIRE_MODE_OPTIONS, JWT_IDENTITY_MODE_OPTIONS,
TICKET_EXCHANGE_METHOD_OPTIONS, TICKET_EXCHANGE_PAYLOAD_MODE_OPTIONS,
JWT_MAPPING_MODE_OPTIONS, and DISCOVERY_FIELD_LABELS) currently pass English
strings directly to t(); change each label value to the corresponding Chinese
source key used in the locale JSON and update component usage to call t('中文键名')
instead of t('English string') so translations resolve correctly (ensure each
new Chinese key matches the keys in web/src/i18n/locales/{lang}.json and update
any occurrences referenced later in the file at the noted ranges as well).
🧹 Nitpick comments (4)
controller/misc.go (1)

43-46: Cache the enabled-provider payload instead of querying it in GetStatus.

GetStatus is a bootstrap/public endpoint, and model.GetEnabledCustomOAuthProviders() is a direct database query. That makes every status load depend on the DB for data that only changes on provider CRUD. Consider caching the serialized provider list and invalidating it on create/update/delete/reload.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/misc.go` around lines 43 - 46, GetStatus currently calls
model.GetEnabledCustomOAuthProviders() (assigning to customProviders) on every
request; change this to read a cached serialized provider payload instead and
only fall back to loading+serializing once on cache miss. Implement a small
in-memory cache (or use existing cache utility) keyed for the enabled-provider
payload, store the serialized result the controller expects, and return that
from GetStatus. Ensure all provider lifecycle operations
(create/update/delete/reload functions) clear or refresh the same cache key so
subsequent GetStatus calls see updates without hitting the DB on every
bootstrap/public request.
controller/custom_oauth_jwt.go (2)

56-62: Use i18n for user-facing error messages.

The error message on line 60 is hardcoded in Chinese. For consistency with the rest of the controller (which uses common.ApiErrorI18n), consider using an i18n key.

♻️ Proposed fix
 	var req customOAuthJWTLoginRequest
 	if err := c.ShouldBind(&req); err != nil {
 		audit.FailureReason = "invalid_request"
 		recordCustomOAuthJWTAudit(audit)
-		common.ApiErrorMsg(c, "无效的请求参数: "+err.Error())
+		common.ApiErrorI18n(c, i18n.MsgInvalidRequestParams)
 		return
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/custom_oauth_jwt.go` around lines 56 - 62, Replace the hardcoded
Chinese error string in the c.ShouldBind error path with an i18n call: in the
block where req is bound (customOAuthJWTLoginRequest) and on error you already
set audit.FailureReason and call recordCustomOAuthJWTAudit, call
common.ApiErrorI18n(c, "error.invalid_request_params",
map[string]interface{}{"detail": err.Error()}) (or the project's existing i18n
key pattern) instead of common.ApiErrorMsg so the user-facing message uses the
translation system; keep the audit and return behavior unchanged.

147-159: Hardcoded Chinese error messages in validation logic.

The error messages on lines 152 and 158 are hardcoded in Chinese. These errors are passed through handleCustomOAuthJWTLoginError and may be displayed to users. Consider using i18n-compatible error handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/custom_oauth_jwt.go` around lines 147 - 159, The validation branch
in the login flow uses hardcoded Chinese error strings (fmt.Errorf("未提供票据") and
fmt.Errorf("未提供 JWT 令牌")) which are passed into handleCustomOAuthJWTLoginError;
replace these with i18n-aware errors or error constants and return a
machine-readable error value (e.g., an error variable or code) that
handleCustomOAuthJWTLoginError can translate for display. Update the
providerConfig.RequiresTicketAcquire() branch and the rawToken check to return
standardized errors (or call your localization helper) instead of literal
Chinese text, and ensure any audit.FailureReason remains unchanged so the audit
uses the same failure keys.
web/src/components/auth/OAuth2Callback.jsx (1)

85-88: Redundant localStorage.setItem call.

Line 86 manually sets localStorage.setItem('user', ...), but setUserData(data) on line 87 already performs the same operation. Remove the redundant call.

♻️ Proposed fix
     userDispatch({ type: 'login', payload: data });
-    localStorage.setItem('user', JSON.stringify(data));
     setUserData(data);
     updateAPI();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/auth/OAuth2Callback.jsx` around lines 85 - 88, Remove the
redundant localStorage write: the explicit localStorage.setItem('user',
JSON.stringify(data)) call is unnecessary because setUserData(data) already
persists the user; in the OAuth2 callback flow, keep userDispatch({ type:
'login', payload: data }); call setUserData(data); and then call updateAPI();
but delete the localStorage.setItem(...) line so only setUserData handles
storage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/custom_oauth_jwt.go`:
- Around line 64-71: Replace the unsafe type assertion on
session.Get("oauth_state") with a safe comma-ok check: retrieve session :=
sessions.Default(c), read state := strings.TrimSpace(req.State) as before, then
do val, ok := session.Get("oauth_state").(string) and treat any non-string or
missing value as invalid (set audit.FailureReason = "invalid_state", call
recordCustomOAuthJWTAudit(audit), call common.ApiErrorI18n(c,
i18n.MsgOAuthStateInvalid) and return). Ensure you also compare state != val
only when ok is true so corrupted/non-string session values don't cause a panic.

In `@middleware/auth_test.go`:
- Around line 35-60: The helper setupAuthMiddlewareTestDB mutates package
globals (model.DB, model.LOG_DB and
common.UsingSQLite/UsingMySQL/UsingPostgreSQL/common.RedisEnabled) but only
closes the temporary DB; capture the previous values before changing them and
restore them in the t.Cleanup closure. Specifically, save oldDB := model.DB,
oldLogDB := model.LOG_DB and the prior booleans for common.UsingSQLite,
common.UsingMySQL, common.UsingPostgreSQL, common.RedisEnabled at the start of
setupAuthMiddlewareTestDB, then in the existing t.Cleanup first close the temp
sqlite connection and then set model.DB = oldDB, model.LOG_DB = oldLogDB and
restore the common.* flags so later tests see the original state.

In `@middleware/auth.go`:
- Around line 33-42: The current loadSessionUserIdentity function always calls
model.GetUserById, causing a DB lookup on every authenticated request; change it
to fetch only the minimal identity fields (e.g., id, username, role, status,
group) via a lightweight method (e.g., model.GetUserIdentityById or a new method
that selects only those columns) or implement a short-lived cache keyed by
session id with explicit invalidation on revoke; update loadSessionUserIdentity
to call that new identity-only function or cache lookup instead of
model.GetUserById to avoid the heavy hot-path DB read while preserving
revocation semantics.

In `@web/src/components/settings/CustomOAuthSetting.jsx`:
- Around line 421-442: The current setFormValues call spreads ...provider last
which lets empty legacy provider fields overwrite the computed defaults (e.g.,
jwt_identity_mode, jwt_acquire_mode, authorization_service_field,
ticket_exchange_*), so move the spread to be first (i.e., spread provider before
the explicit normalized fields) in the setFormValues invocation to ensure the
explicit fallback/normalized values (in setFormValues) take precedence over
possibly empty properties on provider; adjust the setFormValues call around the
provider object to place ...provider at the beginning (and optionally filter
undefined/null if needed) so normalized values in this form are not clobbered by
legacy empty strings.

---

Outside diff comments:
In `@web/src/components/settings/CustomOAuthSetting.jsx`:
- Around line 164-212: The option and label constants
(CUSTOM_OAUTH_KIND_OPTIONS, JWT_SOURCE_OPTIONS, JWT_ACQUIRE_MODE_OPTIONS,
JWT_IDENTITY_MODE_OPTIONS, TICKET_EXCHANGE_METHOD_OPTIONS,
TICKET_EXCHANGE_PAYLOAD_MODE_OPTIONS, JWT_MAPPING_MODE_OPTIONS, and
DISCOVERY_FIELD_LABELS) currently pass English strings directly to t(); change
each label value to the corresponding Chinese source key used in the locale JSON
and update component usage to call t('中文键名') instead of t('English string') so
translations resolve correctly (ensure each new Chinese key matches the keys in
web/src/i18n/locales/{lang}.json and update any occurrences referenced later in
the file at the noted ranges as well).

---

Nitpick comments:
In `@controller/custom_oauth_jwt.go`:
- Around line 56-62: Replace the hardcoded Chinese error string in the
c.ShouldBind error path with an i18n call: in the block where req is bound
(customOAuthJWTLoginRequest) and on error you already set audit.FailureReason
and call recordCustomOAuthJWTAudit, call common.ApiErrorI18n(c,
"error.invalid_request_params", map[string]interface{}{"detail": err.Error()})
(or the project's existing i18n key pattern) instead of common.ApiErrorMsg so
the user-facing message uses the translation system; keep the audit and return
behavior unchanged.
- Around line 147-159: The validation branch in the login flow uses hardcoded
Chinese error strings (fmt.Errorf("未提供票据") and fmt.Errorf("未提供 JWT 令牌")) which
are passed into handleCustomOAuthJWTLoginError; replace these with i18n-aware
errors or error constants and return a machine-readable error value (e.g., an
error variable or code) that handleCustomOAuthJWTLoginError can translate for
display. Update the providerConfig.RequiresTicketAcquire() branch and the
rawToken check to return standardized errors (or call your localization helper)
instead of literal Chinese text, and ensure any audit.FailureReason remains
unchanged so the audit uses the same failure keys.

In `@controller/misc.go`:
- Around line 43-46: GetStatus currently calls
model.GetEnabledCustomOAuthProviders() (assigning to customProviders) on every
request; change this to read a cached serialized provider payload instead and
only fall back to loading+serializing once on cache miss. Implement a small
in-memory cache (or use existing cache utility) keyed for the enabled-provider
payload, store the serialized result the controller expects, and return that
from GetStatus. Ensure all provider lifecycle operations
(create/update/delete/reload functions) clear or refresh the same cache key so
subsequent GetStatus calls see updates without hitting the DB on every
bootstrap/public request.

In `@web/src/components/auth/OAuth2Callback.jsx`:
- Around line 85-88: Remove the redundant localStorage write: the explicit
localStorage.setItem('user', JSON.stringify(data)) call is unnecessary because
setUserData(data) already persists the user; in the OAuth2 callback flow, keep
userDispatch({ type: 'login', payload: data }); call setUserData(data); and then
call updateAPI(); but delete the localStorage.setItem(...) line so only
setUserData handles storage.
🪄 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: 107c31e5-9955-4885-8781-374785cd5325

📥 Commits

Reviewing files that changed from the base of the PR and between 7f38d8a and c1eb220.

📒 Files selected for processing (18)
  • controller/bind_session_test.go
  • controller/custom_oauth.go
  • controller/custom_oauth_jwt.go
  • controller/custom_oauth_jwt_test.go
  • controller/custom_oauth_update_test.go
  • controller/misc.go
  • controller/user.go
  • controller/wechat.go
  • middleware/auth.go
  • middleware/auth_test.go
  • oauth/jwt_direct.go
  • oauth/registry.go
  • oauth/registry_test.go
  • web/src/components/auth/LoginForm.jsx
  • web/src/components/auth/OAuth2Callback.jsx
  • web/src/components/auth/RegisterForm.jsx
  • web/src/components/settings/CustomOAuthSetting.jsx
  • web/src/helpers/api.js
✅ Files skipped from review due to trivial changes (2)
  • web/src/components/auth/LoginForm.jsx
  • controller/custom_oauth_jwt_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • web/src/components/auth/RegisterForm.jsx
  • controller/bind_session_test.go
  • oauth/registry_test.go
  • controller/custom_oauth.go
  • controller/custom_oauth_update_test.go
  • oauth/jwt_direct.go

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
web/src/components/settings/CustomOAuthSetting.jsx (1)

507-510: ⚠️ Potential issue | 🟡 Minor

Use field labels here, not internal keys.

field is values like client_id and authorization_endpoint, so this toast shows untranslated implementation names and t() can't resolve stable locale entries. Map the required fields to the existing human-readable labels before building the message.

As per coding guidelines, translation files in web/src/i18n/locales/{lang}.json must be flat JSON with Chinese source strings as keys.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/CustomOAuthSetting.jsx` around lines 507 - 510,
The toast currently displays internal keys (from requiredFields/currentValues)
like client_id which are not user-friendly or translatable; update the
validation in the loop that calls showError (the block iterating over
requiredFields) to map each internal field key to its human-readable label (use
the component's existing label mapping or add a fieldLabels object) and call
showError(t('请填写 {fieldLabel}', { fieldLabel: fieldLabels[field] } )) or
equivalent so the message uses the localized/human label instead of the raw key;
ensure the string passed to t() is the flat Chinese source string required by
web/src/i18n/locales/*.json.
♻️ Duplicate comments (2)
middleware/auth_test.go (1)

34-72: ⚠️ Potential issue | 🟡 Minor

Restore Gin mode in cleanup.

gin.SetMode(gin.TestMode) is process-global. Leaving it set after this helper returns leaks test-only Gin state into later tests.

💡 Suggested fix
 func setupAuthMiddlewareTestDB(t *testing.T) {
 	t.Helper()
+	oldGinMode := gin.Mode()
 
 	oldDB := model.DB
 	oldLogDB := model.LOG_DB
@@
 	t.Cleanup(func() {
+		gin.SetMode(oldGinMode)
 		sqlDB, err := db.DB()
 		if err == nil {
 			_ = sqlDB.Close()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@middleware/auth_test.go` around lines 34 - 72, The helper
setupAuthMiddlewareTestDB sets gin.SetMode(gin.TestMode) but never restores the
previous Gin mode; capture the current mode before changing (e.g., oldGinMode :=
gin.Mode()), and in the t.Cleanup closure restore it with
gin.SetMode(oldGinMode) alongside the existing cleanup steps so that
setupAuthMiddlewareTestDB leaves Gin's global mode unchanged after tests.
web/src/components/settings/CustomOAuthSetting.jsx (1)

548-550: ⚠️ Potential issue | 🟠 Major

The edit flow still can’t express “clear this client_secret”.

The backend now distinguishes null (keep existing) from "" (clear), but the form still starts blank with “留空则保持原有密钥” semantics and this submit branch strips empty strings. That leaves no path for an admin to intentionally remove a stored secret, including after switching a provider to jwt_direct. Track field dirtiness or add an explicit clear action so "" only reaches the API when the user really chose to clear it.

Also applies to: 1165-1188

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/CustomOAuthSetting.jsx` around lines 548 - 550,
The current submit branch in CustomOAuthSetting.jsx removes
payload.client_secret when editingProvider and the input is an empty string,
preventing an admin from intentionally clearing a stored secret; change this by
tracking whether the client_secret field was modified (e.g., introduce a
clientSecretDirty flag or an explicit "clear secret" control) and use that flag
in the submit logic instead of payload.client_secret === ''. Update the code
that currently checks editingProvider and payload.client_secret (the delete
payload.client_secret branch) to only delete the key when the field was not
dirtied/left untouched, and send an explicit empty string ("" ) when the user
intentionally cleared the secret (or send null/omit otherwise) so the backend
can distinguish keep vs clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/custom_oauth_jwt.go`:
- Around line 175-177: Replace direct use of identity.User.ProviderUserID when
assigning audit fields with a redacted or hashed representation: compute a
deterministic, non-reversible identifier (e.g., HMAC-SHA256 or a one-way hash
with a server-side salt) via a helper like redactOAuthID(...) and assign that to
audit.ExternalID (and any other audit fields currently set from
identity.User.ProviderUserID in the later block covering the other occurrences).
Update places using audit.ExternalID in SysLog/RecordLog to persist the
redacted/hash value so raw PII is never written to logs or DB; keep use of
safeOAuthAuditValue(identity.Group) and oauthRoleLabel(identity.Role) unchanged.
Ensure the helper is deterministic so records for the same provider ID can be
correlated without exposing the plain value.

In `@web/src/components/auth/OAuth2Callback.jsx`:
- Around line 134-148: The !success branch in submitJWTLogin currently only
shows a toast and returns, leaving the OAuth2Callback page stuck on the <Loading
/> spinner; update that branch in submitJWTLogin (the function that calls
handleCallbackSuccess and uses MAX_RETRIES, retry) to immediately
showError(message || t('授权失败')), then navigate away (e.g.,
navigate('/console/personal')) and return to stop further processing; ensure
this behavior mirrors the catch branch so business-level failures
(success:false) do not hang the callback spinner and do not trigger retries.

---

Outside diff comments:
In `@web/src/components/settings/CustomOAuthSetting.jsx`:
- Around line 507-510: The toast currently displays internal keys (from
requiredFields/currentValues) like client_id which are not user-friendly or
translatable; update the validation in the loop that calls showError (the block
iterating over requiredFields) to map each internal field key to its
human-readable label (use the component's existing label mapping or add a
fieldLabels object) and call showError(t('请填写 {fieldLabel}', { fieldLabel:
fieldLabels[field] } )) or equivalent so the message uses the localized/human
label instead of the raw key; ensure the string passed to t() is the flat
Chinese source string required by web/src/i18n/locales/*.json.

---

Duplicate comments:
In `@middleware/auth_test.go`:
- Around line 34-72: The helper setupAuthMiddlewareTestDB sets
gin.SetMode(gin.TestMode) but never restores the previous Gin mode; capture the
current mode before changing (e.g., oldGinMode := gin.Mode()), and in the
t.Cleanup closure restore it with gin.SetMode(oldGinMode) alongside the existing
cleanup steps so that setupAuthMiddlewareTestDB leaves Gin's global mode
unchanged after tests.

In `@web/src/components/settings/CustomOAuthSetting.jsx`:
- Around line 548-550: The current submit branch in CustomOAuthSetting.jsx
removes payload.client_secret when editingProvider and the input is an empty
string, preventing an admin from intentionally clearing a stored secret; change
this by tracking whether the client_secret field was modified (e.g., introduce a
clientSecretDirty flag or an explicit "clear secret" control) and use that flag
in the submit logic instead of payload.client_secret === ''. Update the code
that currently checks editingProvider and payload.client_secret (the delete
payload.client_secret branch) to only delete the key when the field was not
dirtied/left untouched, and send an explicit empty string ("" ) when the user
intentionally cleared the secret (or send null/omit otherwise) so the backend
can distinguish keep vs clear.
🪄 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: a2f6f61c-706c-4507-a9b7-2daba69f8e36

📥 Commits

Reviewing files that changed from the base of the PR and between c1eb220 and 3143e46.

📒 Files selected for processing (14)
  • common/json.go
  • controller/custom_oauth.go
  • controller/custom_oauth_jwt.go
  • controller/custom_oauth_jwt_test.go
  • controller/misc.go
  • i18n/keys.go
  • i18n/locales/en.yaml
  • i18n/locales/zh-CN.yaml
  • i18n/locales/zh-TW.yaml
  • middleware/auth.go
  • middleware/auth_test.go
  • model/user.go
  • web/src/components/auth/OAuth2Callback.jsx
  • web/src/components/settings/CustomOAuthSetting.jsx
✅ Files skipped from review due to trivial changes (6)
  • common/json.go
  • i18n/locales/en.yaml
  • i18n/locales/zh-CN.yaml
  • i18n/locales/zh-TW.yaml
  • i18n/keys.go
  • controller/custom_oauth_jwt_test.go

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
web/src/components/settings/CustomOAuthSetting.jsx (1)

509-512: ⚠️ Potential issue | 🟡 Minor

Avoid dynamic translation keys in required-field errors

t(\请填写 ${field}`)generates runtime keys (e.g.,请填写 issuer`) and can show untranslated/internal field IDs to users. Use a static i18n key plus a localized field-label map.

Suggested fix
+  const REQUIRED_FIELD_LABELS = {
+    name: '显示名称',
+    slug: '标识符 (Slug)',
+    client_id: '客户端 ID',
+    client_secret: '客户端密钥',
+    authorization_endpoint: '授权端点',
+    token_endpoint: '令牌端点',
+    user_info_endpoint: '用户信息端点',
+    issuer: '发行者',
+  };
...
     for (const field of requiredFields) {
       if (!currentValues[field]) {
-        showError(t(`请填写 ${field}`));
+        const fieldLabel = REQUIRED_FIELD_LABELS[field] || field;
+        showError(t('请填写 {{field}}', { field: t(fieldLabel) }));
         return;
       }
     }

As per coding guidelines web/src/**/*.{ts,tsx,js,jsx}: "Use useTranslation() hook and call t('中文key') in components."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/CustomOAuthSetting.jsx` around lines 509 - 512,
The validation currently builds dynamic translation keys with t(`请填写 ${field}`)
which may expose internal IDs; instead create a static i18n key for the
required-field message and a localized field-label map, then use the static key
with a translated field label. Update the block that iterates requiredFields
(referencing requiredFields, currentValues, showError, and t) to look up a
human-friendly label for each field (e.g., fieldLabelMap[field] translated via t
or using t('issuer_label')) and call t with the fixed message key (e.g.,
t('请填写字段', { field: translatedLabel })) before calling showError. Ensure
useTranslation() is used in the component and remove any string interpolation
inside t(...) so all keys are static.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/components/settings/CustomOAuthSetting.jsx`:
- Around line 500-506: The check that ensures ticket_exchange_url is present for
acquireMode values 'ticket_exchange'/'ticket_validate' is missing URL format
validation, so update the validation in the CustomOAuthSetting component (the
branch using acquireMode and currentValues.ticket_exchange_url) to also verify
ticket_exchange_url is a well-formed HTTP/HTTPS URL (e.g., use the URL
constructor or a strict regex) before proceeding; if the URL is invalid, call
showError(t('票据处理模式必须填写有效的 Ticket Processing URL')) and return. Apply the same
format validation to the other ticket-mode block (the logic referenced around
lines 516-526) so both code paths validate presence and scheme before
continuing.
- Around line 1102-1104: The label prop for the slug field is hardcoded as
label='Slug'; replace it with a localized string using the component's i18n
instance (e.g., label={t('your中文key_for_slug')}) and ensure the component calls
useTranslation() (const { t } = useTranslation()) and imports it if not already
present; update the locale files to include the matching Chinese source key for
the slug label so the t(...) lookup resolves.

---

Outside diff comments:
In `@web/src/components/settings/CustomOAuthSetting.jsx`:
- Around line 509-512: The validation currently builds dynamic translation keys
with t(`请填写 ${field}`) which may expose internal IDs; instead create a static
i18n key for the required-field message and a localized field-label map, then
use the static key with a translated field label. Update the block that iterates
requiredFields (referencing requiredFields, currentValues, showError, and t) to
look up a human-friendly label for each field (e.g., fieldLabelMap[field]
translated via t or using t('issuer_label')) and call t with the fixed message
key (e.g., t('请填写字段', { field: translatedLabel })) before calling showError.
Ensure useTranslation() is used in the component and remove any string
interpolation inside t(...) so all keys are static.
🪄 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: 239095f0-ee8e-41ec-8698-3f03bc16e652

📥 Commits

Reviewing files that changed from the base of the PR and between 3143e46 and fe896c0.

📒 Files selected for processing (1)
  • web/src/components/settings/CustomOAuthSetting.jsx

@MisonL
Copy link
Author

MisonL commented Mar 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
web/src/components/settings/CustomOAuthSetting.jsx (1)

579-616: ⚠️ Potential issue | 🟠 Major

Switching away from JWT mode does not actually clear the stored JWT config.

controller/custom_oauth.go only updates pointer fields that are present in the request. Deleting these keys here preserves the old DB values, and several hidden JWT-only fields (jwt_source, jwt_header, issuer, mappings, sync flags, etc.) are still sent anyway. After toggling back to jwt_direct, stale ticket/JWKS/group-role settings can silently reappear.

💡 Suggested fix
-      if (providerKind !== 'jwt_direct') {
-        delete payload.jwt_identity_mode;
-        delete payload.jwt_acquire_mode;
-        delete payload.authorization_service_field;
-        delete payload.ticket_exchange_url;
-        delete payload.ticket_exchange_method;
-        delete payload.ticket_exchange_payload_mode;
-        delete payload.ticket_exchange_ticket_field;
-        delete payload.ticket_exchange_token_field;
-        delete payload.ticket_exchange_service_field;
-        delete payload.ticket_exchange_extra_params;
-        delete payload.ticket_exchange_headers;
-      }
+      if (providerKind !== 'jwt_direct') {
+        Object.assign(payload, {
+          issuer: '',
+          audience: '',
+          jwks_url: '',
+          public_key: '',
+          jwt_source: '',
+          jwt_header: '',
+          jwt_identity_mode: '',
+          jwt_acquire_mode: '',
+          authorization_service_field: '',
+          ticket_exchange_url: '',
+          ticket_exchange_method: '',
+          ticket_exchange_payload_mode: '',
+          ticket_exchange_ticket_field: '',
+          ticket_exchange_token_field: '',
+          ticket_exchange_service_field: '',
+          ticket_exchange_extra_params: '',
+          ticket_exchange_headers: '',
+          group_field: '',
+          role_field: '',
+          group_mapping: '',
+          role_mapping: '',
+          auto_register: false,
+          auto_merge_by_email: false,
+          sync_group_on_login: false,
+          sync_role_on_login: false,
+          group_mapping_mode: '',
+          role_mapping_mode: '',
+        });
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/CustomOAuthSetting.jsx` around lines 579 - 616,
When switching away from jwt_direct you must send explicit nulls for
JWT-specific pointer fields so the controller will clear them; instead of
deleting keys from payload in the providerKind !== 'jwt_direct' branch, set each
JWT/ticket field to null (e.g. jwt_identity_mode, jwt_acquire_mode,
authorization_service_field, ticket_exchange_url, ticket_exchange_method,
ticket_exchange_payload_mode, ticket_exchange_ticket_field,
ticket_exchange_token_field, ticket_exchange_service_field,
ticket_exchange_extra_params, ticket_exchange_headers) and also ensure hidden
JWT fields and config like jwt_source, jwt_header, issuer, any mapping/sync
flags and group/role mappings are set to null when editingProvider and
providerKind !== 'jwt_direct' so the server receives present-but-null pointers
and will clear DB values in controller/custom_oauth.go.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/custom_oauth_jwt_test.go`:
- Around line 46-75: setupCustomOAuthJWTControllerTestDB mutates global state
(model.DB, model.LOG_DB and common.* flags) without restoring them; capture the
previous values at the start of the function and restore them in the existing
t.Cleanup closure after closing the sqlite handle. Specifically, save the
current model.DB and model.LOG_DB and each flag: common.UsingSQLite,
common.UsingMySQL, common.UsingPostgreSQL, common.RedisEnabled,
common.RegisterEnabled, common.QuotaForNewUser, common.QuotaForInvitee,
common.QuotaForInviter, then in t.Cleanup assign those saved values back so
later tests don't inherit the modified/closed DB or altered flags.

In `@controller/custom_oauth_jwt.go`:
- Around line 84-89: Replace direct assignments of audit.FailureReason =
err.Error() with a bounded redacted value (e.g., a short machine-readable code
plus minimal safe message) produced by a helper (create or reuse a helper such
as RedactAuditFailure(err) that returns a safe string), and keep the existing
calls to recordCustomOAuthJWTAudit(audit) and handleCustomOAuthJWTLoginError(c,
err) unchanged; update every occurrence in this file where audit.FailureReason
is set from err (including the sites around
recordCustomOAuthJWTAudit/handleCustomOAuthJWTLoginError and the other
occurrences noted) so the audit trail never contains raw upstream error text or
payloads.
- Around line 74-82: The code currently picks the JWT credential using a fixed
order (firstNonEmpty(req.Token, req.IDToken, req.JWT)), which forces token-first
and breaks claims mode; add a helper like
selectJWTLoginCredential(providerConfig, req) that checks
providerConfig.GetJWTIdentityMode() (compare to
model.CustomJWTIdentityModeUserInfo) and returns firstNonEmpty(req.Token,
req.IDToken, req.JWT) for userinfo mode or firstNonEmpty(req.IDToken, req.JWT,
req.Token) for claims mode, then call completeCustomOAuthJWTLogin using that
helper instead of the fixed firstNonEmpty call to ensure id_token/jwt are
preferred when appropriate.

In `@web/src/components/settings/CustomOAuthSetting.jsx`:
- Around line 818-824: The new Chinese strings introduced in
CustomOAuthSetting.jsx (e.g., the Tag labels rendered in render(kind): 'JWT 直连'
and 'OAuth 授权码', plus other strings like '添加身份提供商' and the new banner copy
around lines where the component renders) are not present in the zh-TW locale
JSON, causing fallback to Simplified Chinese; run the i18n toolchain (bun run
i18n:extract, bun run i18n:sync, then bun run i18n:lint) to extract and sync
these flat JSON keys into web/src/i18n/locales/zh-TW.json, ensuring every new
t('...') key from CustomOAuthSetting.jsx (and the block around 883-910) is added
with proper zh-TW translations so the UI no longer falls back.

---

Outside diff comments:
In `@web/src/components/settings/CustomOAuthSetting.jsx`:
- Around line 579-616: When switching away from jwt_direct you must send
explicit nulls for JWT-specific pointer fields so the controller will clear
them; instead of deleting keys from payload in the providerKind !== 'jwt_direct'
branch, set each JWT/ticket field to null (e.g. jwt_identity_mode,
jwt_acquire_mode, authorization_service_field, ticket_exchange_url,
ticket_exchange_method, ticket_exchange_payload_mode,
ticket_exchange_ticket_field, ticket_exchange_token_field,
ticket_exchange_service_field, ticket_exchange_extra_params,
ticket_exchange_headers) and also ensure hidden JWT fields and config like
jwt_source, jwt_header, issuer, any mapping/sync flags and group/role mappings
are set to null when editingProvider and providerKind !== 'jwt_direct' so the
server receives present-but-null pointers and will clear DB values in
controller/custom_oauth.go.
🪄 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: 17d55034-da14-44a3-a488-3deee19a5f94

📥 Commits

Reviewing files that changed from the base of the PR and between fe896c0 and 60ed091.

📒 Files selected for processing (7)
  • controller/custom_oauth_jwt.go
  • controller/custom_oauth_jwt_test.go
  • web/src/components/auth/OAuth2Callback.jsx
  • web/src/components/settings/CustomOAuthSetting.jsx
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/zh-CN.json
  • web/src/i18n/locales/zh-TW.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • web/src/i18n/locales/zh-CN.json
  • web/src/i18n/locales/en.json
  • web/src/components/auth/OAuth2Callback.jsx

@MisonL
Copy link
Author

MisonL commented Mar 26, 2026

Fixes Applied Successfully

Fixed 5 file(s) based on 4 unresolved CodeRabbit review comment(s).

Files modified:

  • controller/custom_oauth_jwt.go
  • controller/custom_oauth_jwt_test.go
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/zh-CN.json
  • web/src/i18n/locales/zh-TW.json

Commit: a17e5d97

Validation:

  • go test ./controller ./middleware -count=1
  • cd web && bun run build
  • git diff --check

Note:

  • cd web && bun run i18n:lint still reports the repository's pre-existing global i18n backlog and was not expanded in this review-fix patch.

The latest autofix changes are on the feat/auth-jwt-sso-mvp-submit branch.

@MisonL
Copy link
Author

MisonL commented Mar 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
controller/custom_oauth_jwt.go (1)

107-111: ⚠️ Potential issue | 🟠 Major

Route deferred bind failures through the JWT-login error mapper.

Line 111 bypasses handleCustomOAuthJWTLoginError(c, err). If bindOAuthIdentityToUser() returns OAuthAlreadyBoundError or another OAuth business error, this branch skips the dedicated i18n/error mapping used everywhere else in this handler and falls back to the generic API error path instead.

↩️ Proposed fix
 	if result.BindAfterStatusCheck {
 		if err := bindOAuthIdentityToUser(result.User, provider, result.ProviderUserID); err != nil {
 			audit.FailureReason = oauthAuditFailureReason(err)
 			recordCustomOAuthJWTAudit(audit)
-			common.ApiError(c, err)
+			handleCustomOAuthJWTLoginError(c, err)
 			return
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/custom_oauth_jwt.go` around lines 107 - 111, The branch that
handles deferred binding (when result.BindAfterStatusCheck is true) calls
bindOAuthIdentityToUser and on error currently sets audit fields, records the
audit via recordCustomOAuthJWTAudit and calls common.ApiError(c, err), which
bypasses the localized/business error mapping; replace the common.ApiError call
with a call to handleCustomOAuthJWTLoginError(c, err) so OAuth business errors
(e.g., OAuthAlreadyBoundError) are mapped and translated consistently while
keeping the existing audit update via oauthAuditFailureReason and
recordCustomOAuthJWTAudit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/custom_oauth_jwt.go`:
- Around line 164-169: The callback URL for ticket modes is currently derived
from request headers via buildCustomOAuthJWTCallbackURL when calling
ResolveIdentityFromInput, which allows caller-controlled values; change this to
require and validate a configured external base URL (ServerAddress) and fail
closed for ticket_exchange/ticket_validate: update
buildCustomOAuthJWTCallbackURL to accept and validate ServerAddress (parse and
ensure a well-formed base URL) and return an error if empty/malformed, stop
using X-Forwarded-* / Request.Host as fallback, and in the controller where
ResolveIdentityFromInput is called (the ResolveIdentityFromInput(...)
invocation) propagate and handle that error by returning a failure for ticket
modes instead of deriving the callback from request headers. Ensure
ResolveIdentityFromInput gets only the validated callback URL and that any code
paths referenced for ticket modes enforce the same validation.

---

Duplicate comments:
In `@controller/custom_oauth_jwt.go`:
- Around line 107-111: The branch that handles deferred binding (when
result.BindAfterStatusCheck is true) calls bindOAuthIdentityToUser and on error
currently sets audit fields, records the audit via recordCustomOAuthJWTAudit and
calls common.ApiError(c, err), which bypasses the localized/business error
mapping; replace the common.ApiError call with a call to
handleCustomOAuthJWTLoginError(c, err) so OAuth business errors (e.g.,
OAuthAlreadyBoundError) are mapped and translated consistently while keeping the
existing audit update via oauthAuditFailureReason and recordCustomOAuthJWTAudit.
🪄 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: 4668100e-b1d1-4514-a836-dfebd7a733c4

📥 Commits

Reviewing files that changed from the base of the PR and between 60ed091 and a17e5d9.

📒 Files selected for processing (5)
  • controller/custom_oauth_jwt.go
  • controller/custom_oauth_jwt_test.go
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/zh-CN.json
  • web/src/i18n/locales/zh-TW.json
✅ Files skipped from review due to trivial changes (4)
  • web/src/i18n/locales/zh-CN.json
  • web/src/i18n/locales/zh-TW.json
  • controller/custom_oauth_jwt_test.go
  • web/src/i18n/locales/en.json

@MisonL
Copy link
Author

MisonL commented Mar 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/custom_oauth_jwt.go`:
- Around line 192-207: When handling the existing-session bind path, ensure you
check the user's account status immediately after getSessionUser(c) and before
calling bindOAuthIdentityToCurrentUser so disabled users cannot link identities;
specifically, after currentUser, currentUserErr := getSessionUser(c) and before
bindOAuthIdentityToCurrentUser(c, provider, identity.User) add a status gate
that sets audit.FailureReason via oauthAuditFailureReason (when audit != nil)
and returns the appropriate error/result if the user is disabled or locked. Keep
existing audit field handling (audit.Action = "bind", audit.TargetUserID =
currentUser.Id) only after the status check passes.
- Around line 107-112: The delayed bind branch uses common.ApiError which
bypasses the OAuth-specific error formatting; after bindOAuthIdentityToUser(...)
returns an error (including OAuthAlreadyBoundError), set audit.FailureReason =
oauthAuditFailureReason(err) and recordCustomOAuthJWTAudit(audit) as you do now,
but replace the common.ApiError(c, err) call with the OAuth error handler (e.g.
handleOAuthError(c, err) or the existing OAuth-specific handler your controller
uses elsewhere) so OAuth-specific errors are returned with the correct
localized/business response, then return.
🪄 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: c08bc5bb-1d34-4620-80a7-785c6c246ac9

📥 Commits

Reviewing files that changed from the base of the PR and between a17e5d9 and 524e4dc.

📒 Files selected for processing (2)
  • controller/custom_oauth_jwt.go
  • controller/custom_oauth_jwt_test.go
✅ Files skipped from review due to trivial changes (1)
  • controller/custom_oauth_jwt_test.go

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.

feat(auth): 支持 CAS / JWT 企业统一认证接入

1 participant