-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
(WIP) feat: oauth2 && sso #1815
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughImplements a full OAuth2/OIDC stack: an in-process OAuth server (JWT/JWKS, key management, grants), API controllers/endpoints (authorize, token, introspect, revoke, userinfo, discovery), models and revocation store, middleware, router wiring, admin UIs, demos/examples, and dependency updates. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Browser
participant ClientApp as OAuth Client
participant API as Server (/api)
participant OAuth as OAuth Core
participant Store as KV/Revocation
rect rgb(245,248,255)
ClientApp->>API: GET /.well-known/openid-configuration
API->>OAuth: EnsureInitialized()
API-->>ClientApp: discovery document (auth/token/userinfo, jwks)
end
rect rgb(245,255,245)
note right of User: Authorization Code + PKCE flow
ClientApp->>Browser: open auth URL (code + code_challenge)
Browser->>API: GET /oauth/authorize?client_id&code_challenge...
API->>OAuth: HandleAuthorizeRequest (prepare/consent)
OAuth->>Store: store auth_code (TTL)
OAuth-->>Browser: 302 redirect with code
Browser->>ClientApp: redirect_uri?code=...
ClientApp->>API: POST /oauth/token (grant_type=authorization_code + code_verifier)
API->>OAuth: HandleTokenRequest (exchange code)
OAuth->>Store: consume code, issue tokens (access/id/refresh)
API-->>ClientApp: token response
end
rect rgb(255,248,240)
note right of ClientApp: Resource access with Bearer JWT
ClientApp->>API: GET /protected (Authorization: Bearer <jwt>)
API->>Middleware: OAuthJWTAuth / fallback
Middleware->>OAuth: GetPublicKeyByKid(kid)
Middleware->>Store: IsTokenRevoked(jti)
Middleware-->>API: auth OK (claims in context)
API-->>ClientApp: protected resource
end
sequenceDiagram
autonumber
participant Admin
participant API
participant OAuth as OAuth Core
participant Store as JWKS/Store
rect rgb(255,245,245)
Admin->>API: POST /oauth/keys/rotate
API->>OAuth: RotateSigningKey(newKid?)
OAuth->>Store: update JWKS/current kid
OAuth-->>API: { success, kid }
API-->>Admin: response
end
rect rgb(245,245,255)
Admin->>API: POST /oauth/revoke (token)
API->>Store: RevokeToken(jti, exp)
API-->>Admin: success
Admin->>API: POST /oauth/introspect (token)
API->>OAuth: verify signature(by kid)
API->>Store: IsTokenRevoked(jti)
API-->>Admin: introspection result (active/claims)
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal). Please share your feedback with us on this Discord post. 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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 22
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
go.mod (1)
4-4
: Invalid go directive; must be major.minor.The go directive should be “go 1.23” (not 1.23.4). Use a separate toolchain directive if you need a specific patch.
-go 1.23.4 +go 1.23 +toolchain go1.23.4model/main.go (1)
276-301
: Fast migration path misses OAuthClient.If migrateDBFast is ever used, OAuthClient won’t be migrated.
migrations := []struct { model interface{} name string }{ @@ {&TwoFA{}, "TwoFA"}, {&TwoFABackupCode{}, "TwoFABackupCode"}, + {&OAuthClient{}, "OAuthClient"}, }
main.go (1)
139-147
: Critical: session cookie SameSite=Strict + Secure=false — breaks SSO and is unsafe.SameSite=Strict prevents the browser sending the session cookie on cross‑site top‑level redirects (e.g. OAuth/OIDC authorize), causing forced re‑logins; Secure=false allows cookies over plain HTTP.
Action: update main.go store.Options (lines 139–147) to set Secure=true in TLS/production (driven by env/config) and use SameSite=Lax by default (or SameSite=None with Secure=true if you require true cross‑site iframe usage). Test a third‑party app redirect to /api/oauth/authorize to confirm SSO works.
Locations: main.go:139–147 (store.Options); controller/oidc.go:104–106 (session oauth_state) — OAuth/OIDC flows rely on session cookies.
🧹 Nitpick comments (65)
web/src/pages/OAuth/Consent.jsx (1)
51-56
: Optional: auto-redirect to login on 401/login_required.Current UX shows a banner and waits for manual click. Consider redirecting to /login with next back to consent for smoother flows.
- } catch (e) { - // 401 login required or other error - setError(e?.response?.data?.error || 'failed'); + } catch (e) { + const code = e?.response?.status; + const err = e?.response?.data?.error; + if (code === 401 || err === 'login_required') { + const u = new URL(window.location.origin + '/login'); + u.searchParams.set('next', '/oauth/consent' + window.location.search); + window.location.href = u.toString(); + return; + } + setError(err || 'failed'); } finally { setLoading(false); }web/src/App.jsx (1)
47-47
: Lazy‑load Consent page to avoid inflating the initial bundle.Other pages are lazy; align Consent for consistency and perf.
- import OAuthConsent from './pages/OAuth/Consent'; + const OAuthConsent = React.lazy(() => import('./pages/OAuth/Consent'));<Route path='/oauth/consent' element={ - <Suspense fallback={<Loading></Loading>}> + <Suspense fallback={<Loading></Loading>} key={location.pathname}> <OAuthConsent /> </Suspense> } />Also applies to: 202-209
src/oauth/util.go (1)
44-59
: Build redirect with url.Parse and merge query params safely.String concatenation with “?”/“&” can mis-handle existing queries, encoding, and duplicates. Prefer url.Parse + Query().
func writeOAuthRedirectError(c *gin.Context, redirectURI, errCode, description, state string) { - writeNoStore(c) - q := "error=" + url.QueryEscape(errCode) - if description != "" { - q += "&error_description=" + url.QueryEscape(description) - } - if state != "" { - q += "&state=" + url.QueryEscape(state) - } - sep := "?" - if strings.Contains(redirectURI, "?") { - sep = "&" - } - c.Redirect(http.StatusFound, redirectURI+sep+q) + writeNoStore(c) + u, err := url.Parse(redirectURI) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "invalid_redirect_uri"}) + return + } + q := u.Query() + q.Set("error", errCode) + if description != "" { + q.Set("error_description", description) + } + if state != "" { + q.Set("state", state) + } + u.RawQuery = q.Encode() + c.Redirect(http.StatusFound, u.String()) }model/oauth_revoked_token.go (1)
18-27
: Fallback TTL is very short when exp is unknown.1 minute may allow quick reuse of a revoked token if its exp is far out and Redis briefly misses the write. Consider a safer default (e.g., 10–30 minutes) aligned with max JWT lifetime in your system.
web/src/components/modals/oauth2/EditOAuth2ClientModal.jsx (3)
171-200
: De-duplicate URI validation logic across Create/Edit modals.Both modals inline nearly identical isValidRedirectUri. Extract a shared helper (e.g., helpers/oauth2.js) to keep behavior consistent.
20-33
: Remove unused UI imports.TextArea and Switch from top-level imports aren’t used (you use Form.TextArea/Form.Switch). Trim to reduce bundle size and lint noise.
-import { - Modal, - Form, - Input, - Select, - TextArea, - Switch, - Space, - Typography, - Divider, - Button, -} from '@douyinfe/semi-ui'; +import { Modal, Form, Input, Select, Space, Typography, Divider, Button } from '@douyinfe/semi-ui';
368-411
: Ensure redirect_uris are unique and trimmed before submit.Prevent accidental duplicates and trailing spaces.
- const validRedirectUris = redirectUris - .map((u) => (u || '').trim()) - .filter((u) => u.length > 0); + const validRedirectUris = Array.from( + new Set( + redirectUris.map((u) => (u || '').trim()).filter((u) => u.length > 0) + ) + );examples/oauth/oauth2_test_client.go (4)
91-123
: Avoid DefaultServeMux; use a local ServeMux to prevent handler leakage.Apply this diff:
- srv := &http.Server{Addr: ":9999"} - http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + mux := http.NewServeMux() + mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { ... }) + srv := &http.Server{Addr: ":9999", Handler: mux}
82-89
: Rename local variable ‘url’ to avoid shadowing the imported package name.Apply this diff:
- url := conf.AuthCodeURL( + authURL := conf.AuthCodeURL( @@ - fmt.Printf("Visit this URL to authorize:\n%s\n\n", url) + fmt.Printf("Visit this URL to authorize:\n%s\n\n", authURL)
172-179
: Simplify userinfo URL derivation; the conditional is redundant.Apply this diff:
- // 将最后一个路径段 authorize 替换为 userinfo - dir := path.Dir(u.Path) - if strings.HasSuffix(u.Path, "/authorize") { - u.Path = path.Join(dir, "userinfo") - } else { - // 回退:追加默认 /oauth/userinfo - u.Path = path.Join(dir, "userinfo") - } + // 将最后一个路径段替换/拼接为 userinfo + dir := path.Dir(u.Path) + u.Path = path.Join(dir, "userinfo")
145-147
: Don’t print full access tokens; mask in logs.Suggestion:
- fmt.Printf("Access Token: %s\n", token.AccessToken) + masked := token.AccessToken + if len(masked) > 12 { masked = masked[:6] + "..." + masked[len(masked)-6:] } + fmt.Printf("Access Token: %s\n", masked)Also applies to: 163-163
web/src/components/modals/oauth2/OAuth2QuickStartModal.jsx (3)
98-124
: Only call onDone on success, not on failure.Currently onDone fires even when client creation fails.
Apply this diff:
- } else { - showError(res?.data?.message || '创建失败'); - } - onDone && onDone(); + } else { + showError(res?.data?.message || '创建失败'); + return; + } + onDone && onDone();
133-136
: Validate issuer format before saving.Add a basic check:
- <Form.Input ... value={issuer} onChange={setIssuer} ... /> + <Form.Input ... value={issuer} onChange={setIssuer} ... /> + {/* 简单校验 */} + {issuer && !/^https?:\/\/[^ ]+$/i.test(issuer) && <Text type="danger">Issuer 格式不正确</Text>}
57-69
: Batch update options with one API if backend supports it.Multiple sequential PUTs increase latency and failure surface.
If supported, add an endpoint to accept an array and send once.
web/src/pages/Setting/OAuth2/OAuth2ServerSettings.jsx (2)
60-97
: Use Promise.allSettled and update baseline after save.Partial-failure detection via includes(undefined) is unreliable with Promise.all; also baseline isn’t updated on success.
Apply this diff:
- Promise.all(requestQueue) - .then((res) => { - if (requestQueue.length === 1) { - if (res.includes(undefined)) return; - } else if (requestQueue.length > 1) { - if (res.includes(undefined)) - return showError(t('部分保存失败,请重试')); - } - showSuccess(t('保存成功')); + Promise.allSettled(requestQueue) + .then((results) => { + const failed = results.filter(r => r.status === 'rejected'); + if (failed.length) return showError(t('部分保存失败,请重试')); + showSuccess(t('保存成功')); + // 更新基线,避免重复提交相同值 + setInputsRow(structuredClone(inputs)); if (props && props.refresh) { props.refresh(); } })
265-275
: HS256 should be discouraged unless you have strong key management.Default to RS256; if exposing HS256, add an admin‑level warning tooltip.
web/src/components/modals/oauth2/CreateOAuth2ClientModal.jsx (1)
78-86
: Safer default when allowedGrantTypes is empty.Current fallback may produce an empty array. Provide a deterministic default.
Apply this diff:
- const computeDefaultGrantTypes = (type, allowed) => { + const computeDefaultGrantTypes = (type, allowed) => { const cand = type === 'public' ? ['authorization_code', 'refresh_token'] : ['client_credentials', 'authorization_code', 'refresh_token']; const subset = cand.filter((g) => allowed.includes(g)); - return subset.length ? subset : [allowed[0]].filter(Boolean); + if (subset.length) return subset; + // fallback if server returns empty allowed list + return type === 'public' ? ['authorization_code'] : ['client_credentials']; };web/src/pages/Setting/OAuth2/OAuth2QuickStart.jsx (1)
74-75
: Use showSuccess for consistency with other flows.Apply this diff:
- Toast.success('客户端已创建:' + res.data.client_id); + showSuccess('客户端已创建:' + res.data.client_id);examples/oauth/oauth-demo.html (3)
177-190
: Discovery should try standard path; also persist endpoints for callback resilience.Your fetch hardcodes
/api/.well-known/openid-configuration
, but the spec path is/.well-known/openid-configuration
. Add a fallback and persist discovered endpoints so the callback works after page reloads.- const res = await fetch(iss.replace(/\/$/,'') + '/api/.well-known/openid-configuration'); - const d = await res.json(); - $('authorization_endpoint').value = d.authorization_endpoint || ''; - $('token_endpoint').value = d.token_endpoint || ''; - $('userinfo_endpoint').value = d.userinfo_endpoint || ''; + const base = iss.replace(/\/$/,''); + let res = await fetch(base + '/api/.well-known/openid-configuration'); + if (!res.ok) { + // fallback to spec path + res = await fetch(base + '/.well-known/openid-configuration'); + } + const d = await res.json(); + $('authorization_endpoint').value = d.authorization_endpoint || ''; + $('token_endpoint').value = d.token_endpoint || ''; + $('userinfo_endpoint').value = d.userinfo_endpoint || ''; + // persist for callback flow + if (d.authorization_endpoint) persist('authorization_endpoint', d.authorization_endpoint); + if (d.token_endpoint) persist('token_endpoint', d.token_endpoint); + if (d.userinfo_endpoint) persist('userinfo_endpoint', d.userinfo_endpoint);
263-268
: Token POST helper: add Accept header and friendlier error body handling.Safer to declare you expect JSON and include response text on failures.
- const res = await fetch(url, { method:'POST', headers:{ 'Content-Type':'application/x-www-form-urlencoded' }, body }); - if(!res.ok){ const t = await res.text(); throw new Error(`HTTP ${res.status} ${t}`); } + const res = await fetch(url, { + method:'POST', + headers:{ 'Content-Type':'application/x-www-form-urlencoded', 'Accept':'application/json' }, + body + }); + if(!res.ok){ + const t = await res.text(); + throw new Error(`HTTP ${res.status}: ${t || res.statusText}`); + } return res.json();
301-307
: UserInfo: check HTTP status to surface failures.Currently errors during non-2xx responses may be masked.
- const res = await fetch(ep, { headers:{ Authorization: 'Bearer '+at } }); - const data = await res.json(); $('userinfoOut').textContent = JSON.stringify(data, null, 2); + const res = await fetch(ep, { headers:{ Authorization: 'Bearer '+at, Accept:'application/json' } }); + if (!res.ok) { + const t = await res.text(); + throw new Error(`HTTP ${res.status}: ${t || res.statusText}`); + } + const data = await res.json(); + $('userinfoOut').textContent = JSON.stringify(data, null, 2);web/src/components/settings/OAuth2Setting.jsx (3)
49-51
: Pass the Axios error object to showError to trigger auth redirects and richer messages.Currently 401s won’t redirect to login because a plain string is passed.
- } catch (error) { - showError('获取OAuth2设置失败'); + } catch (error) { + showError(error); } finally {
82-82
: Harden window.open against tab‑nabbing.- <Button onClick={()=>window.open('/oauth-demo.html','_blank')}>前端 Demo</Button> + <Button onClick={()=>window.open('/oauth-demo.html','_blank','noopener,noreferrer')}>前端 Demo</Button>
21-21
: Remove unused import Spin or use it to show loading state.-import { Card, Spin, Space, Button } from '@douyinfe/semi-ui'; +import { Card, Space, Button } from '@douyinfe/semi-ui';web/src/pages/Setting/OAuth2/JWKSManager.jsx (2)
21-23
: Propagate Axios errors to showError; don’t mask 401/429/500 handlers.- } catch (e) { - showError('获取密钥列表失败'); + } catch (e) { + showError(e); } finally {- } catch (e) { - showError('密钥轮换失败'); + } catch (e) { + showError(e); } finally {- } catch (e) { - showError('删除失败'); + } catch (e) { + showError(e); } finally {Also applies to: 38-40, 55-57
50-50
: Use showSuccess for consistency with helpers.- Toast.success('已删除:' + kid); + showSuccess('已删除:' + kid);web/src/pages/Setting/OAuth2/OAuth2Tools.jsx (3)
60-73
: Only attach PKCE params for authorization code flow.- const u = new URL(values.authorization_endpoint || (server.issuer + '/oauth/authorize')); - u.searchParams.set('response_type', values.response_type || 'code'); + const u = new URL(values.authorization_endpoint || (server.issuer + '/api/oauth/authorize')); + const rt = values.response_type || 'code'; + u.searchParams.set('response_type', rt); @@ - if (values.code_challenge) { + if (rt === 'code' && values.code_challenge) { u.searchParams.set('code_challenge', values.code_challenge); u.searchParams.set('code_challenge_method', values.code_challenge_method || 'S256'); }
3-3
: Surface server‑info fetch errors via showError.-import { API } from '../../../helpers'; +import { API, showError } from '../../../helpers';- } catch {} + } catch (e) { showError(e); }Also applies to: 43-58
1-1
: Drop unused useMemo import.-import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react';web/src/components/modals/oauth2/JWKSManagerModal.jsx (3)
18-18
: Pass Axios errors to showError so 401s redirect and server messages appear.- } catch { showError('获取密钥列表失败'); } finally { setLoading(false); } + } catch (e) { showError(e); } finally { setLoading(false); }- } catch { showError('密钥轮换失败'); } finally { setLoading(false); } + } catch (e) { showError(e); } finally { setLoading(false); }- } catch { showError('删除失败'); } finally { setLoading(false); } + } catch (e) { showError(e); } finally { setLoading(false); }Also applies to: 27-27, 36-36
34-34
: Use showSuccess helper for consistency.- if (res?.data?.success) { Toast.success('已删除:' + kid); await load(); } + if (res?.data?.success) { showSuccess('已删除:' + kid); await load(); }- Toast.success('已生成并生效:' + res.data.path); + showSuccess('已生成并生效:' + res.data.path);Also applies to: 80-80
2-2
: Remove unused imports TextArea, Input (using Form.TextArea/Form.Input).-import { Modal, Table, Button, Space, Tag, Typography, Popconfirm, Toast, Form, TextArea, Divider, Input } from '@douyinfe/semi-ui'; +import { Modal, Table, Button, Space, Tag, Typography, Popconfirm, Toast, Form, Divider } from '@douyinfe/semi-ui';middleware/auth.go (1)
333-348
: Add aud (and alg) enforcement to prevent token confusion; optional but recommended.- // issuer check when configured + // issuer check when configured if iss, ok2 := claims["iss"].(string); !ok2 || (settings.Issuer != "" && iss != settings.Issuer) { return false } + // optional audience check if your AS issues aud + if settings.Audience != "" { + switch aud := claims["aud"].(type) { + case string: + if aud != settings.Audience { return false } + case []interface{}: + found := false + for _, v := range aud { if s, _ := v.(string); s == settings.Audience { found = true; break } } + if !found { return false } + } + }setting/system_setting/oauth2.go (2)
24-42
: Clarify defaults and ensure they are enforced end-to-end
- JWTSigningAlgorithm and JWTKeyID appear configurable here, but server code hard-codes "RS256" and auto-generates KIDs. Either wire these settings into signing/JWKS or remove to avoid dead config.
- JWTPrivateKeyFile vs DefaultPrivateKeyPath are overlapping and may confuse users; consider a single field with clear semantics.
- DefaultUserRole uses a magic number (1). Prefer a constant (e.g., common.RoleUser) to avoid drift.
Can you confirm whether src/oauth/server.go consumes JWTSigningAlgorithm/JWTKeyID and any plan to align KID generation with this default?
8-21
: Prefer duration types or documented units; validate inputsAccessTokenTTL/RefreshTokenTTL are ints in minutes. Consider time.Duration or string durations ("10m", "12h") for clarity, or keep minutes but validate bounds (e.g., 1–1440) at update time to prevent overly long-lived tokens.
web/public/oauth-demo.html (3)
62-63
: Mask confidential inputsThe Client Secret input should not be plain text.
Apply this diff:
- <div class="col"><label>Client Secret(可选,机密客户端)</label><input id="client_secret" placeholder="留空表示公开客户端" /></div> + <div class="col"><label>Client Secret(可选,机密客户端)</label><input id="client_secret" type="password" placeholder="留空表示公开客户端" /></div>
149-149
: Improve discovery compatibility (fallback to standard well‑known path)Not all deployments serve discovery under /api/.well-known. Try /.well-known/openid-configuration if the first fetch fails.
Apply this diff:
- $('btnDiscover').onclick = async () => { const iss=$('issuer').value.trim(); if(!iss){ alert('请填写 Issuer'); return; } try{ persist('issuer', iss); const res=await fetch(iss.replace(/\/$/,'')+'/api/.well-known/openid-configuration'); const d=await res.json(); $('authorization_endpoint').value=d.authorization_endpoint||''; $('token_endpoint').value=d.token_endpoint||''; $('userinfo_endpoint').value=d.userinfo_endpoint||''; if(d.issuer){ $('issuer').value=d.issuer; persist('issuer', d.issuer);} $('conf_json').value=JSON.stringify(d,null,2); setAuthInfo('已从发现文档加载端点', true);}catch(e){ setAuthInfo('自动发现失败:'+e,false);} }; + $('btnDiscover').onclick = async () => { + const iss=$('issuer').value.trim(); if(!iss){ alert('请填写 Issuer'); return; } + try{ + persist('issuer', iss); + const base = iss.replace(/\/$/,''); + let res = await fetch(base + '/api/.well-known/openid-configuration'); + if (!res.ok) res = await fetch(base + '/.well-known/openid-configuration'); + const d=await res.json(); + $('authorization_endpoint').value=d.authorization_endpoint||''; + $('token_endpoint').value=d.token_endpoint||''; + $('userinfo_endpoint').value=d.userinfo_endpoint||''; + if(d.issuer){ $('issuer').value=d.issuer; persist('issuer', d.issuer); } + $('conf_json').value=JSON.stringify(d,null,2); + setAuthInfo('已从发现文档加载端点', true); + }catch(e){ setAuthInfo('自动发现失败:'+e,false); } + };
160-160
: Base64URL decode may fail without paddingAdd padding for JWT part decoding to avoid atob errors.
Apply this diff:
+ function padB64(s){ const pad = s.length % 4; return pad ? s + '='.repeat(4 - pad) : s; } @@ - $('btnDecodeJWT').onclick = ()=>{ const t=$('id_token').value.trim(); if(!t){ $('jwtClaims').textContent='(空)'; return;} const parts=t.split('.'); if(parts.length<2){ $('jwtClaims').textContent='格式错误'; return;} try{ const json=JSON.parse(atob(parts[1].replace(/-/g,'+').replace(/_/g,'/'))); $('jwtClaims').textContent=JSON.stringify(json,null,2);}catch(e){ $('jwtClaims').textContent='解码失败:'+e; } }; + $('btnDecodeJWT').onclick = ()=>{ const t=$('id_token').value.trim(); if(!t){ $('jwtClaims').textContent='(空)'; return;} const parts=t.split('.'); if(parts.length<2){ $('jwtClaims').textContent='格式错误'; return;} try{ const b64=parts[1].replace(/-/g,'+').replace(/_/g,'/'); const json=JSON.parse(atob(padB64(b64))); $('jwtClaims').textContent=JSON.stringify(json,null,2);}catch(e){ $('jwtClaims').textContent='解码失败:'+e; } };controller/oauth_keys.go (3)
51-56
: Return more precise status codes for delete failuresMap “unknown kid” to 404 and “cannot delete current signing key” to 409.
Apply this diff:
- if err := oauth.DeleteSigningKey(kid); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + if err := oauth.DeleteSigningKey(kid); err != nil { + status := http.StatusBadRequest + msg := err.Error() + switch { + case strings.Contains(msg, "unknown kid"): + status = http.StatusNotFound + case strings.Contains(msg, "cannot delete current signing key"): + status = http.StatusConflict + } + c.JSON(status, gin.H{"success": false, "message": msg}) return }Note: add strings to imports.
import ( "github.com/gin-gonic/gin" "net/http" "one-api/logger" "one-api/src/oauth" + "strings" )
66-73
: Differentiate “file exists” from server errorsWhen generating a key file with overwrite=false and file exists, respond 409 Conflict.
Apply this diff:
- kid, err := oauth.GenerateAndPersistKey(req.Path, req.Kid, req.Overwrite) + kid, err := oauth.GenerateAndPersistKey(req.Path, req.Kid, req.Overwrite) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + status := http.StatusInternalServerError + if strings.Contains(err.Error(), "file exists") { + status = http.StatusConflict + } + c.JSON(status, gin.H{"success": false, "message": err.Error()}) return }
59-73
: Path safety considerationsThis endpoint writes a private key to an arbitrary filesystem path. It’s root‑only, but still recommend restricting to a configured safe directory or requiring absolute paths under DefaultPrivateKeyPath, and rejecting traversal.
web/src/components/modals/oauth2/OAuth2ToolsModal.jsx (3)
1-1
: Remove unused importuseMemo isn’t used.
Apply this diff:
-import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react';
100-119
: Discovery: add fallback to standard well‑known pathMirror the static demo fallback so tools work against deployments that expose /.well-known at the root (no /api).
Apply this diff:
- const url = iss.replace(/\/$/, '') + '/api/.well-known/openid-configuration'; - const res = await fetch(url); + const base = iss.replace(/\/$/, ''); + let res = await fetch(base + '/api/.well-known/openid-configuration'); + if (!res.ok) res = await fetch(base + '/.well-known/openid-configuration');
185-186
: Pad Base64URL when decoding ID TokenAvoid atob failures on unpadded payloads.
Apply this diff:
- const json = JSON.parse(atob(parts[1].replace(/-/g,'+').replace(/_/g,'/'))); + const pad = (s)=>{ const m=s.length%4; return m? s + '='.repeat(4-m) : s; }; + const json = JSON.parse(atob(pad(parts[1].replace(/-/g,'+').replace(/_/g,'/'))));router/api-router.go (3)
35-44
: Add spec-friendly /.well-known aliasesTo improve interoperability with standard OIDC/OAuth clients, expose root-level well-known endpoints in addition to the /api variants.
Apply this diff:
// OAuth2 Server endpoints apiRouter.GET("/.well-known/jwks.json", controller.GetJWKS) apiRouter.GET("/.well-known/openid-configuration", controller.OAuthOIDCConfiguration) apiRouter.GET("/.well-known/oauth-authorization-server", controller.OAuthServerInfo) + // Root-level aliases for standard discovery + apiRouter.GET("/../.well-known/openid-configuration", controller.OAuthOIDCConfiguration) // alias at site root if router mounted at /api + apiRouter.GET("/../.well-known/oauth-authorization-server", controller.OAuthServerInfo) + apiRouter.GET("/../.well-known/jwks.json", controller.GetJWKS)Note: If router is mounted at /api, consider registering these at the top-level engine (router.GET) instead of the apiRouter group for clean paths.
39-41
: Consider rate limiting /oauth/authorizeUnlike token and revoke, authorize isn’t rate-limited here. Add a conservative limiter to reduce abuse.
Apply this diff:
- apiRouter.GET("/oauth/authorize", controller.OAuthAuthorizeEndpoint) + apiRouter.GET("/oauth/authorize", middleware.CriticalRateLimit(), controller.OAuthAuthorizeEndpoint)
58-67
: Scope gate on a root-only group is redundantThis group requires RootAuth; RequireOAuthScopeIfPresent("admin") doesn’t add protection and may block root-only OAuth flows if the token lacks the “admin” scope. Either drop it or switch to a “root” scope if you model that.
Would you like this group to allow admin (non-root) via OAuth scope instead? If not, remove the scope check for clarity.
src/oauth/server.go (3)
268-318
: Validate PEM import more thoroughly.The
ImportPEMKey
function should validate key size and algorithm parameters before accepting the key.Add validation for minimum key size:
if err != nil { return "", err } +// Validate minimum key size for security +if key.N.BitLen() < 2048 { + return "", fmt.Errorf("RSA key must be at least 2048 bits") +} signingKeys[kid] = key
631-688
: Consider addingazp
claim for authorization code flows.When generating access tokens for authorization code flows, consider adding the
azp
(authorized party) claim as recommended by OpenID Connect for scenarios where the audience contains multiple values.claims := jwt.MapClaims{ "iss": iss, "sub": func() string { if subject != "" { return subject } return clientID }(), "aud": "one-api", + "azp": clientID, // authorized party for OIDC compliance "iat": now.Unix(), "nbf": now.Unix(), "exp": exp.Unix(), "scope": scope, "client_id": clientID, "grant_type": grantType, "jti": jti, }
747-999
: Large function should be refactored.The
HandleAuthorizeRequest
function is 252 lines long and handles multiple responsibilities. Consider splitting it into smaller, focused functions.Consider extracting the following into separate functions:
- Parameter validation logic (lines 752-826)
- Prepare mode handling (lines 855-923)
- Authorization code flow (lines 941-969)
- Implicit token flow (lines 972-998)
Example extraction:
func validateAuthorizeParams(c *gin.Context) (*authorizeParams, error) { // Extract lines 752-826 validation logic } func handlePrepareMode(c *gin.Context, params *authorizeParams) { // Extract lines 855-923 }controller/oauth_client.go (1)
148-156
: Extract grant types to use system settings.The valid grant types are hardcoded here, but should use the system settings to ensure consistency with the OAuth2 server configuration.
Use system settings for validation:
+import "one-api/setting/system_setting" // 验证授权类型 -validGrantTypes := []string{"client_credentials", "authorization_code", "refresh_token"} +settings := system_setting.GetOAuth2Settings() +validGrantTypes := settings.AllowedGrantTypes for _, grantType := range req.GrantTypes {model/oauth_client.go (8)
18-21
: Normalize grant types/scopes to avoid silent mismatches.CSV parsing without trimming leads to hard‑to‑debug false negatives (e.g., "authorization_code, refresh_token"). Also ensure comparisons are trimmed.
Apply:
func (c *OAuthClient) GetGrantTypes() []string { if c.GrantTypes == "" { return []string{"client_credentials"} } - return strings.Split(c.GrantTypes, ",") + parts := strings.Split(c.GrantTypes, ",") + out := make([]string, 0, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + if p != "" { + out = append(out, p) + } + } + return out } func (c *OAuthClient) GetScopes() []string { if c.Scopes == "" { return []string{"api:read"} } - return strings.Split(c.Scopes, ",") + parts := strings.Split(c.Scopes, ",") + out := make([]string, 0, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + if p != "" { + out = append(out, p) + } + } + return out } func (c *OAuthClient) ValidateGrantType(grantType string) bool { allowedTypes := c.GetGrantTypes() for _, allowedType := range allowedTypes { - if allowedType == grantType { + if strings.TrimSpace(allowedType) == strings.TrimSpace(grantType) { return true } } return false } func (c *OAuthClient) ValidateScope(scope string) bool { allowedScopes := c.GetScopes() requestedScopes := strings.Split(scope, " ") for _, requestedScope := range requestedScopes { requestedScope = strings.TrimSpace(requestedScope) if requestedScope == "" { continue } found := false for _, allowedScope := range allowedScopes { - if allowedScope == requestedScope { + if strings.TrimSpace(allowedScope) == requestedScope { found = true break } } if !found { return false } } return true }Also applies to: 57-63, 69-76, 94-102, 105-126
46-54
: Validate redirect URIs on write.
SetRedirectURIs
accepts arbitrary strings; validate withurl.Parse
, enforce https (except for loopback per RFC 8252), dedupe, and reject fragments.I can add a small helper to parse/normalize and enforce RFC 8252/6749 rules if you confirm the DB can reject invalid URIs at creation/update time.
83-91
: Redirect URI check ignoresDomain
and lacks normalization.
- The
Domain
field is documented as "allowed redirect domain" but is unused here.- Exact string equality may be fine (fail‑closed), but consider normalizing and being explicit about policy. If
Domain
is not intended, remove it to avoid confusion.Do you plan to support domain‑level whitelisting, or should we rely solely on exact pre‑registered URIs?
142-146
: Avoid coupling to user status; define client status constants.
common.UserStatusEnabled
leaks user semantics into client model. Define client‑specific constants to prevent accidental coupling.Apply elsewhere:
const ( OAuthClientStatusEnabled = 1 OAuthClientStatusDisabled = 2 )Then:
- err := DB.Where("id = ? AND status = ?", id, common.UserStatusEnabled).First(&client).Error + err := DB.Where("id = ? AND status = ?", id, OAuthClientStatusEnabled).First(&client).Error
149-153
: Consistent status filtering and pagination defaults.
GetAllOAuthClients
returns disabled clients too. Align withGetOAuthClientByID
or make it explicit via a parameter.Apply:
- err := DB.Order("created_time desc").Limit(num).Offset(startIdx).Find(&clients).Error + err := DB.Where("status = ?", OAuthClientStatusEnabled). + Order("created_time desc"). + Limit(num). + Offset(startIdx). + Find(&clients).Error
156-161
: Search can be unbounded; add status filter and sensible limit/sort.Prevent large scans and keep results consistent.
- err := DB.Where("name LIKE ? OR id LIKE ? OR description LIKE ?", - "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%").Find(&clients).Error + err := DB.Where("status = ?", OAuthClientStatusEnabled). + Where("name LIKE ? OR id LIKE ? OR description LIKE ?", + "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%"). + Order("created_time desc"). + Limit(100). + Find(&clients).Error
24-29
: Consider usingtime.Time
with GORM auto timestamps.
int64
epoch works, buttime.Time
withautoCreateTime
/autoUpdateTime
is more idiomatic and reduces manual handling.Example:
CreatedAt time.Time `json:"created_time" gorm:"autoCreateTime"` LastUsedAt time.Time `json:"last_used_time"`And convert to/from epoch at the API boundary if needed.
178-183
: Clarify whetherCountOAuthClients
should include disabled clients.If most listings hide disabled clients, consider counting enabled ones for UX consistency.
- err := DB.Model(&OAuthClient{}).Count(&count).Error + err := DB.Model(&OAuthClient{}).Where("status = ?", OAuthClientStatusEnabled).Count(&count).Errorweb/src/pages/Setting/OAuth2/OAuth2ClientSettings.jsx (6)
92-93
: URL‑encode path parameters used in API calls.Protect against IDs containing reserved URL characters.
- const res = await API.delete(`/api/oauth_clients/${client.id}`); + const res = await API.delete(`/api/oauth_clients/${encodeURIComponent(client.id)}`);- const res = await API.post(`/api/oauth_clients/${client.id}/regenerate_secret`); + const res = await API.post(`/api/oauth_clients/${encodeURIComponent(client.id)}/regenerate_secret`);Also applies to: 107-108
164-172
: Handle status types robustly.Backends sometimes return booleans or strings. Coerce to number for consistent rendering.
- render: (status) => ( - <Tag color={status === 1 ? 'green' : 'red'}> - {status === 1 ? '启用' : '禁用'} - </Tag> - ), + render: (status) => { + const enabled = Number(status) === 1; + return ( + <Tag color={enabled ? 'green' : 'red'}> + {enabled ? '启用' : '禁用'} + </Tag> + ); + },
174-178
: Make created_time rendering tolerant to seconds/milliseconds and fallback to alternate field.Avoid incorrect dates if the unit or field name varies.
- render: (time) => new Date(time * 1000).toLocaleString(), + render: (time, record) => { + const raw = time ?? record?.creation_time; + const n = Number(raw); + const date = isNaN(n) ? new Date(raw) : new Date(n > 1e12 ? n : n * 1000); + return isNaN(date.getTime()) ? '-' : date.toLocaleString(); + },
315-371
: Gate JWKS/Server‑info views to admins only (verify).Ensure this page is admin‑only and that endpoints don’t leak sensitive config in non‑prod builds.
248-381
: Apply i18n to user‑facing strings.Most strings are hardcoded in Chinese; wrap them with
t(...)
to localize.
56-66
: Preserve search filter on reload; keep endpoint pathBackend registers /oauth_clients with GET("/") and callers consistently use '/api/oauth_clients/' — no path change needed. Reapply the current searchKeyword when populating filteredClients after fetching.
File: web/src/pages/Setting/OAuth2/OAuth2ClientSettings.jsx — update loadClients result handling:
- const res = await API.get('/api/oauth_clients/'); - if (res.data.success) { - setClients(res.data.data || []); - setFilteredClients(res.data.data || []); + const res = await API.get('/api/oauth_clients/'); + if (res.data.success) { + const data = res.data.data || []; + setClients(data); + setFilteredClients( + searchKeyword + ? data.filter(c => + c.name?.toLowerCase().includes(searchKeyword.toLowerCase()) || + c.id?.toLowerCase().includes(searchKeyword.toLowerCase()) || + c.description?.toLowerCase().includes(searchKeyword.toLowerCase()) + ) + : data + );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sum
is excluded by!**/*.sum
📒 Files selected for processing (32)
controller/oauth.go
(1 hunks)controller/oauth_client.go
(1 hunks)controller/oauth_keys.go
(1 hunks)examples/oauth/oauth-demo.html
(1 hunks)examples/oauth/oauth2_test_client.go
(1 hunks)go.mod
(5 hunks)main.go
(2 hunks)middleware/auth.go
(4 hunks)middleware/oauth_jwt.go
(1 hunks)model/main.go
(1 hunks)model/oauth_client.go
(1 hunks)model/oauth_revoked_token.go
(1 hunks)router/api-router.go
(8 hunks)setting/system_setting/oauth2.go
(1 hunks)src/oauth/server.go
(1 hunks)src/oauth/store.go
(1 hunks)src/oauth/util.go
(1 hunks)web/public/oauth-demo.html
(1 hunks)web/src/App.jsx
(2 hunks)web/src/components/modals/oauth2/CreateOAuth2ClientModal.jsx
(1 hunks)web/src/components/modals/oauth2/EditOAuth2ClientModal.jsx
(1 hunks)web/src/components/modals/oauth2/JWKSManagerModal.jsx
(1 hunks)web/src/components/modals/oauth2/OAuth2QuickStartModal.jsx
(1 hunks)web/src/components/modals/oauth2/OAuth2ToolsModal.jsx
(1 hunks)web/src/components/settings/OAuth2Setting.jsx
(1 hunks)web/src/pages/OAuth/Consent.jsx
(1 hunks)web/src/pages/Setting/OAuth2/JWKSManager.jsx
(1 hunks)web/src/pages/Setting/OAuth2/OAuth2ClientSettings.jsx
(1 hunks)web/src/pages/Setting/OAuth2/OAuth2QuickStart.jsx
(1 hunks)web/src/pages/Setting/OAuth2/OAuth2ServerSettings.jsx
(1 hunks)web/src/pages/Setting/OAuth2/OAuth2Tools.jsx
(1 hunks)web/src/pages/Setting/index.jsx
(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (25)
model/main.go (1)
model/oauth_client.go (1)
OAuthClient
(13-30)
web/src/pages/Setting/index.jsx (1)
web/src/components/settings/OAuth2Setting.jsx (1)
OAuth2Setting
(30-92)
web/src/App.jsx (2)
web/src/components/common/ui/Loading.jsx (1)
Loading
(23-29)web/src/pages/OAuth/Consent.jsx (1)
OAuthConsent
(14-199)
controller/oauth.go (5)
src/oauth/server.go (6)
GetJWKS
(114-116)EnsureInitialized
(68-94)HandleTokenRequest
(321-344)HandleAuthorizeRequest
(748-999)GetPublicKeyByKid
(127-132)GetRSAPublicKey
(119-124)setting/system_setting/oauth2.go (1)
GetOAuth2Settings
(49-51)model/oauth_revoked_token.go (2)
IsTokenRevoked
(35-57)RevokeToken
(12-33)middleware/oauth_jwt.go (1)
GetOAuthClaims
(278-286)model/user.go (1)
GetUserById
(287-299)
web/src/components/modals/oauth2/EditOAuth2ClientModal.jsx (2)
web/src/components/modals/oauth2/CreateOAuth2ClientModal.jsx (9)
redirectUris
(43-43)grantTypes
(45-45)allowedGrantTypes
(46-50)handleSubmit
(130-224)isValidRedirectUri
(111-127)addRedirectUri
(243-245)removeRedirectUri
(248-250)updateRedirectUri
(253-257)handleGrantTypesChange
(260-270)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
web/src/pages/Setting/OAuth2/OAuth2ServerSettings.jsx (1)
web/src/helpers/utils.jsx (4)
compareObjects
(294-311)showWarning
(149-151)showError
(118-147)showSuccess
(153-155)
web/src/pages/Setting/OAuth2/JWKSManager.jsx (2)
web/src/components/modals/oauth2/JWKSManagerModal.jsx (7)
Typography
(6-6)loading
(9-9)keys
(10-10)load
(12-19)rotate
(21-28)del
(30-37)columns
(88-101)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
web/src/components/settings/OAuth2Setting.jsx (6)
web/src/components/modals/oauth2/JWKSManagerModal.jsx (2)
loading
(9-9)JWKSManagerModal
(8-148)web/src/components/modals/oauth2/OAuth2QuickStartModal.jsx (2)
loading
(10-10)OAuth2QuickStartModal
(7-230)web/src/pages/Setting/OAuth2/OAuth2ClientSettings.jsx (2)
loading
(46-46)OAuth2ClientSettings
(44-437)web/src/pages/Setting/OAuth2/OAuth2ServerSettings.jsx (2)
loading
(33-33)OAuth2ServerSettings
(31-404)web/src/components/modals/oauth2/OAuth2ToolsModal.jsx (3)
res
(19-19)data
(9-9)OAuth2ToolsModal
(26-324)web/src/helpers/utils.jsx (1)
showError
(118-147)
model/oauth_revoked_token.go (1)
common/redis.go (3)
RedisEnabled
(17-17)RedisSet
(64-70)RedisGet
(72-79)
controller/oauth_keys.go (2)
src/oauth/server.go (5)
RotateSigningKey
(135-156)ListSigningKeys
(202-210)DeleteSigningKey
(213-227)GenerateAndPersistKey
(159-193)ImportPEMKey
(268-318)logger/logger.go (1)
LogInfo
(55-57)
web/src/pages/Setting/OAuth2/OAuth2QuickStart.jsx (2)
web/src/components/modals/oauth2/OAuth2QuickStartModal.jsx (4)
Typography
(5-5)origin
(8-8)applyRecommended
(57-78)createClient
(98-124)web/src/helpers/utils.jsx (2)
showSuccess
(153-155)showError
(118-147)
web/src/pages/Setting/OAuth2/OAuth2Tools.jsx (1)
web/src/components/modals/oauth2/OAuth2ToolsModal.jsx (15)
enc
(8-8)data
(9-9)hash
(10-10)bytes
(11-11)binary
(12-12)i
(13-13)i
(22-22)charset
(18-18)res
(19-19)array
(20-20)server
(27-27)values
(39-52)buildAuthorizeURL
(74-88)copy
(90-92)genVerifier
(94-98)
src/oauth/store.go (1)
common/redis.go (4)
RedisSet
(64-70)RedisGet
(72-79)RedisDel
(91-97)RedisEnabled
(17-17)
main.go (2)
src/oauth/server.go (1)
InitOAuthServer
(37-65)common/sys_log.go (1)
SysLog
(10-13)
web/src/components/modals/oauth2/OAuth2QuickStartModal.jsx (2)
web/src/pages/Setting/OAuth2/OAuth2QuickStart.jsx (4)
Typography
(5-5)origin
(9-9)applyRecommended
(17-37)createClient
(58-84)web/src/helpers/utils.jsx (2)
showSuccess
(153-155)showError
(118-147)
web/src/components/modals/oauth2/OAuth2ToolsModal.jsx (1)
web/src/components/modals/oauth2/OAuth2QuickStartModal.jsx (1)
issuer
(14-14)
web/src/components/modals/oauth2/JWKSManagerModal.jsx (2)
web/src/pages/Setting/OAuth2/JWKSManager.jsx (7)
Typography
(6-6)loading
(9-9)keys
(10-10)load
(12-26)rotate
(28-43)del
(45-60)columns
(66-99)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
middleware/oauth_jwt.go (5)
setting/system_setting/oauth2.go (1)
GetOAuth2Settings
(49-51)src/oauth/server.go (1)
GetPublicKeyByKid
(127-132)model/oauth_client.go (1)
GetOAuthClientByID
(142-146)common/constants.go (1)
UserStatusEnabled
(174-174)model/oauth_revoked_token.go (1)
IsTokenRevoked
(35-57)
middleware/auth.go (7)
setting/system_setting/oauth2.go (1)
GetOAuth2Settings
(49-51)src/oauth/server.go (1)
GetRSAPublicKey
(119-124)model/oauth_revoked_token.go (1)
IsTokenRevoked
(35-57)model/user_cache.go (1)
GetUserCache
(73-112)common/constants.go (1)
UserStatusEnabled
(174-174)common/gin.go (1)
SetContextKey
(53-55)constant/context_key.go (1)
ContextKeyUsingGroup
(46-46)
src/oauth/server.go (7)
setting/system_setting/oauth2.go (2)
GetOAuth2Settings
(49-51)OAuth2Settings
(5-21)common/sys_log.go (1)
SysLog
(10-13)common/utils.go (2)
GenerateKey
(190-193)GetUUID
(155-159)controller/oauth.go (1)
GetJWKS
(19-58)model/oauth_client.go (1)
GetOAuthClientByID
(142-146)model/user.go (1)
GetUserById
(287-299)logger/logger.go (1)
LogInfo
(55-57)
web/src/components/modals/oauth2/CreateOAuth2ClientModal.jsx (2)
web/src/components/modals/oauth2/EditOAuth2ClientModal.jsx (12)
Typography
(36-36)Select
(37-37)formApi
(40-40)loading
(41-41)redirectUris
(42-42)grantTypes
(43-43)allowedGrantTypes
(44-48)handleSubmit
(146-222)addRedirectUri
(225-227)removeRedirectUri
(230-232)updateRedirectUri
(235-239)handleGrantTypesChange
(242-252)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
model/oauth_client.go (3)
common/sys_log.go (1)
SysLog
(10-13)model/main.go (1)
DB
(63-63)common/constants.go (1)
UserStatusEnabled
(174-174)
controller/oauth_client.go (2)
model/oauth_client.go (7)
GetAllOAuthClients
(149-153)SearchOAuthClients
(156-161)GetOAuthClientByID
(142-146)CreateOAuthClient
(164-166)OAuthClient
(13-30)UpdateOAuthClient
(169-171)DeleteOAuthClient
(174-176)common/constants.go (1)
UserStatusEnabled
(174-174)
web/src/pages/Setting/OAuth2/OAuth2ClientSettings.jsx (3)
web/src/components/modals/oauth2/CreateOAuth2ClientModal.jsx (3)
loading
(42-42)grantTypes
(45-45)CreateOAuth2ClientModal
(40-444)web/src/components/modals/oauth2/EditOAuth2ClientModal.jsx (3)
loading
(41-41)grantTypes
(43-43)EditOAuth2ClientModal
(39-415)web/src/helpers/utils.jsx (3)
showError
(118-147)showSuccess
(153-155)showInfo
(157-159)
router/api-router.go (6)
controller/oauth.go (8)
GetJWKS
(19-58)OAuthOIDCConfiguration
(156-190)OAuthServerInfo
(117-153)OAuthTokenEndpoint
(61-96)OAuthAuthorizeEndpoint
(99-114)OAuthIntrospect
(193-263)OAuthRevoke
(266-336)OAuthUserInfo
(339-375)middleware/auth.go (2)
AdminAuth
(165-169)RootAuth
(171-175)middleware/oauth_jwt.go (3)
OAuthJWTAuth
(18-63)OptionalOAuthAuth
(233-247)RequireOAuthScopeIfPresent
(250-275)controller/oauth_keys.go (5)
RotateOAuthSigningKey
(26-36)ListOAuthSigningKeys
(39-42)DeleteOAuthSigningKey
(45-57)GenerateOAuthSigningKeyFile
(60-73)ImportOAuthSigningKey
(76-89)controller/oauth_client.go (7)
GetAllOAuthClients
(39-73)SearchOAuthClients
(76-104)GetOAuthClient
(107-133)CreateOAuthClient
(136-210)UpdateOAuthClient
(213-272)DeleteOAuthClient
(275-298)RegenerateOAuthClientSecret
(301-346)model/oauth_client.go (5)
GetAllOAuthClients
(149-153)SearchOAuthClients
(156-161)CreateOAuthClient
(164-166)UpdateOAuthClient
(169-171)DeleteOAuthClient
(174-176)
🪛 OSV Scanner (2.1.0)
go.mod
[HIGH] 25-25: github.com/golang-jwt/jwt 3.2.2+incompatible: Excessive memory allocation during header parsing in github.com/golang-jwt/jwt
(GO-2025-3553)
[HIGH] 25-25: github.com/golang-jwt/jwt 3.2.2+incompatible: jwt-go allows excessive memory allocation during header parsing
🪛 Gitleaks (8.27.2)
examples/oauth/oauth2_test_client.go
[high] 32-32: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
[high] 67-67: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🔇 Additional comments (15)
examples/oauth/oauth2_test_client.go (1)
40-49
: Local HTTP is fine for demos; ensure never used in prod without TLS.Please confirm these endpoints are strictly dev-only and documented as such.
web/src/pages/Setting/OAuth2/OAuth2ServerSettings.jsx (1)
218-227
: Nice: clear operational banner.The guidance around HTTPS and key rotation is good.
web/src/components/modals/oauth2/CreateOAuth2ClientModal.jsx (2)
129-163
: Good validations (grants, PKCE, redirect URI rules).
35-36
: Incorrect — do not remove the import.
showInfo is exported at web/src/helpers/utils.jsx:157; the "likely not exported" claim (and the suggested diff) is incorrect.Likely an incorrect or invalid review comment.
web/src/pages/Setting/OAuth2/OAuth2QuickStart.jsx (1)
20-26
: LGTM: recommended defaults are sensible (enabled, issuer, PKCE, RS256).examples/oauth/oauth-demo.html (1)
192-197
: PKCE generation and helpers look solid.Correct S256 generation, base64url encoding, and RFC 7636-compliant verifier charset.
Consider adding a quick “Generate PKCE on first use” if the fields are empty to reduce user error.
Also applies to: 146-157
web/src/pages/Setting/index.jsx (2)
139-148
: New “OAuth2 & SSO” tab: placement and wiring look good.Icon, label, and content render are consistent with existing tabs. The feature is root‑gated.
Ensure i18n has a key for “OAuth2 & SSO” or this literal is acceptable in your locales.
20-36
: Import additions OK — demo file present; ensure build emits it to web/dist or Gin serves it.
web/public/oauth-demo.html (also examples/oauth/oauth-demo.html) — confirm frontend build copies public → dist or configure the server to serve/embed web/public so /oauth-demo.html is available in production.model/main.go (1)
268-269
: Include OAuthClient in AutoMigrate: LGTM.This ensures the table exists during normal migration.
main.go (1)
17-18
: Import path OK — OIDC/OAuth endpoints registered and OAuth initialized before server start.router/api-router.go registers (under /api): /api/.well-known/openid-configuration, /api/.well-known/jwks.json, /api/oauth/authorize, /api/oauth/token, /api/oauth/userinfo. InitResources() calls oauth.InitOAuthServer() before router.SetRouter(...) and server.Run().
web/src/pages/Setting/OAuth2/JWKSManager.jsx (1)
88-99
: Good guard: prevent deleting current key and warn about external JWKS caches.middleware/auth.go (1)
360-365
: Verify token_ context reads; mirror minimal token_ keys on the OAuth path if required**Automated search returned "No files were searched" — confirm whether any handlers read context keys named like "token_group". Run locally: rg -n --hidden -uu 'token_[A-Za-z0-9_]+' || grep -RIn --exclude-dir=vendor 'token_' .
If handlers expect token_* keys, add matching c.Set calls in middleware/auth.go (around lines 360–365), e.g. c.Set("token_group", userCache.Group), to avoid nil reads.
controller/oauth_keys.go (1)
34-36
: logger.LogInfo argument type is wrong (compile error)logger.LogInfo expects context.Context; passing *gin.Context won’t compile. Use request context.
Apply this diff:
- logger.LogInfo(c, "oauth signing key rotated: "+kid) + logger.LogInfo(c.Request.Context(), "oauth signing key rotated: "+kid) @@ - logger.LogInfo(c, "oauth signing key deleted: "+kid) + logger.LogInfo(c.Request.Context(), "oauth signing key deleted: "+kid) @@ - logger.LogInfo(c, "oauth signing key generated to file: "+req.Path+" kid="+kid) + logger.LogInfo(c.Request.Context(), "oauth signing key generated to file: "+req.Path+" kid="+kid) @@ - logger.LogInfo(c, "oauth signing key imported from PEM, kid="+kid) + logger.LogInfo(c.Request.Context(), "oauth signing key imported from PEM, kid="+kid)Also applies to: 55-57, 71-73, 87-89
⛔ Skipped due to learnings
Learnt from: feitianbubu PR: QuantumNous/new-api#1228 File: router/main.go:28-36 Timestamp: 2025-06-15T12:38:11.806Z Learning: gin.Context implements context.Context interface since Gin v1.8.0, providing the methods Deadline(), Done(), Err(), and Value(). When using Gin v1.8.0 or later, gin.Context can be passed directly to functions expecting context.Context without needing to extract c.Request.Context().
Learnt from: feitianbubu PR: QuantumNous/new-api#1228 File: router/main.go:28-36 Timestamp: 2025-06-15T12:38:11.806Z Learning: gin.Context implements context.Context interface since Gin v1.8.0, providing the methods Deadline(), Done(), Err(), and Value(). When using Gin v1.8.0 or later, gin.Context can be passed directly to functions expecting context.Context without needing to extract c.Request.Context().
controller/oauth_client.go (1)
349-356
: Use crypto/rand for client ID/secret generationcontroller/oauth_client.go (lines 349–356) calls randstr.String — confirm randstr’s implementation uses crypto/rand; if it does not, replace with common.GenerateKey()/common.GetUUID() or an explicit crypto/rand-based generator. Verify by locating the randstr package and inspecting its source (e.g. rg -n --hidden -S 'randstr' and open the defining file, or check go.mod/vendor if it’s external).
model/oauth_client.go (1)
32-44
: LGTM: robust JSON unmarshal with safe fallback.Good defensive handling and logging on malformed
RedirectURIs
.
token := c.PostForm("token") | ||
if token == "" { | ||
c.JSON(http.StatusBadRequest, gin.H{ | ||
"error": "invalid_request", | ||
"error_description": "Missing token parameter", | ||
}) | ||
return | ||
} | ||
|
||
token = c.PostForm("token") | ||
if token == "" { | ||
c.JSON(http.StatusBadRequest, gin.H{ | ||
"error": "invalid_request", | ||
"error_description": "Missing token parameter", | ||
}) | ||
return | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove duplicate token extraction code.
Lines 284-300 duplicate the exact same token extraction logic from lines 284-291.
Remove the duplicate code block:
- token := c.PostForm("token")
- if token == "" {
- c.JSON(http.StatusBadRequest, gin.H{
- "error": "invalid_request",
- "error_description": "Missing token parameter",
- })
- return
- }
-
token := c.PostForm("token")
if token == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_request",
"error_description": "Missing token parameter",
})
return
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
token := c.PostForm("token") | |
if token == "" { | |
c.JSON(http.StatusBadRequest, gin.H{ | |
"error": "invalid_request", | |
"error_description": "Missing token parameter", | |
}) | |
return | |
} | |
token = c.PostForm("token") | |
if token == "" { | |
c.JSON(http.StatusBadRequest, gin.H{ | |
"error": "invalid_request", | |
"error_description": "Missing token parameter", | |
}) | |
return | |
} | |
token := c.PostForm("token") | |
if token == "" { | |
c.JSON(http.StatusBadRequest, gin.H{ | |
"error": "invalid_request", | |
"error_description": "Missing token parameter", | |
}) | |
return | |
} |
🤖 Prompt for AI Agents
In controller/oauth.go around lines 284 to 300 there is a duplicated token
extraction and validation block; remove the second duplicate block (the repeated
token := c.PostForm("token") ... check that returns a 400) so only a single
token extraction and missing-token response remains, ensuring no other logic
depends on the removed duplicate.
// 尝试解析JWT,若成功则记录jti到撤销表 | ||
parsed, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { | ||
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { | ||
return nil, jwt.ErrTokenSignatureInvalid | ||
} | ||
pub := oauth.GetRSAPublicKey() | ||
if pub == nil { | ||
return nil, jwt.ErrTokenUnverifiable | ||
} | ||
return pub, nil | ||
}) | ||
if err == nil && parsed != nil && parsed.Valid { | ||
if claims, ok := parsed.Claims.(jwt.MapClaims); ok { | ||
var jti string | ||
var exp int64 | ||
if v, ok := claims["jti"].(string); ok { | ||
jti = v | ||
} | ||
if v, ok := claims["exp"].(float64); ok { | ||
exp = int64(v) | ||
} else if v, ok := claims["exp"].(int64); ok { | ||
exp = v | ||
} | ||
if jti != "" { | ||
// 如果没有exp,默认撤销至当前+TTL 10分钟 | ||
if exp == 0 { | ||
exp = time.Now().Add(10 * time.Minute).Unix() | ||
} | ||
_ = model.RevokeToken(jti, exp) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Key lookup inconsistency in OAuthRevoke.
The OAuthRevoke
function uses GetRSAPublicKey()
(Line 307) which only gets the current signing key, but tokens might have been signed with older keys. This could prevent valid tokens from being revoked.
Use the kid from the token header to look up the correct key:
parsed, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, jwt.ErrTokenSignatureInvalid
}
- pub := oauth.GetRSAPublicKey()
+ kid, _ := t.Header["kid"].(string)
+ pub := oauth.GetPublicKeyByKid(kid)
if pub == nil {
+ // Fallback to current key if kid not found
+ pub = oauth.GetRSAPublicKey()
+ }
+ if pub == nil {
return nil, jwt.ErrTokenUnverifiable
}
return pub, nil
})
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// 尝试解析JWT,若成功则记录jti到撤销表 | |
parsed, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { | |
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { | |
return nil, jwt.ErrTokenSignatureInvalid | |
} | |
pub := oauth.GetRSAPublicKey() | |
if pub == nil { | |
return nil, jwt.ErrTokenUnverifiable | |
} | |
return pub, nil | |
}) | |
if err == nil && parsed != nil && parsed.Valid { | |
if claims, ok := parsed.Claims.(jwt.MapClaims); ok { | |
var jti string | |
var exp int64 | |
if v, ok := claims["jti"].(string); ok { | |
jti = v | |
} | |
if v, ok := claims["exp"].(float64); ok { | |
exp = int64(v) | |
} else if v, ok := claims["exp"].(int64); ok { | |
exp = v | |
} | |
if jti != "" { | |
// 如果没有exp,默认撤销至当前+TTL 10分钟 | |
if exp == 0 { | |
exp = time.Now().Add(10 * time.Minute).Unix() | |
} | |
_ = model.RevokeToken(jti, exp) | |
} | |
} | |
} | |
// 尝试解析JWT,若成功则记录jti到撤销表 | |
parsed, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { | |
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { | |
return nil, jwt.ErrTokenSignatureInvalid | |
} | |
kid, _ := t.Header["kid"].(string) | |
pub := oauth.GetPublicKeyByKid(kid) | |
if pub == nil { | |
// Fallback to current key if kid not found | |
pub = oauth.GetRSAPublicKey() | |
} | |
if pub == nil { | |
return nil, jwt.ErrTokenUnverifiable | |
} | |
return pub, nil | |
}) | |
if err == nil && parsed != nil && parsed.Valid { | |
if claims, ok := parsed.Claims.(jwt.MapClaims); ok { | |
var jti string | |
var exp int64 | |
if v, ok := claims["jti"].(string); ok { | |
jti = v | |
} | |
if v, ok := claims["exp"].(float64); ok { | |
exp = int64(v) | |
} else if v, ok := claims["exp"].(int64); ok { | |
exp = v | |
} | |
if jti != "" { | |
// 如果没有exp,默认撤销至当前+TTL 10分钟 | |
if exp == 0 { | |
exp = time.Now().Add(10 * time.Minute).Unix() | |
} | |
_ = model.RevokeToken(jti, exp) | |
} | |
} | |
} |
async function handleCallback(){ | ||
const code = qs('code'); const err = qs('error'); | ||
const state = qs('state'); | ||
if(err){ setAuthInfo('授权失败:'+err, false); return; } | ||
if(!code){ setAuthInfo('等待授权...', true); return; } | ||
// state check | ||
if(state && load('state') && state !== load('state')){ setAuthInfo('state 不匹配,已拒绝', false); return; } | ||
try{ | ||
const tokenEp = load('token_endpoint'); | ||
const data = await postForm(tokenEp, { | ||
grant_type:'authorization_code', | ||
code, | ||
client_id: load('client_id'), | ||
redirect_uri: load('redirect_uri'), | ||
code_verifier: load('code_verifier') | ||
}); | ||
$('access_token').value = data.access_token || ''; | ||
$('id_token').value = data.id_token || ''; | ||
$('refresh_token').value = data.refresh_token || ''; | ||
$('token_raw').value = JSON.stringify(data, null, 2); | ||
setAuthInfo('授权成功,已获取令牌', true); | ||
}catch(e){ setAuthInfo('交换令牌失败:'+e.message, false); } | ||
} | ||
handleCallback(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enforce state presence and validate ID Token (nonce, iss, aud, exp/iat).
For OIDC, you must reject missing/invalid state and validate the ID Token’s critical claims. Add nonce check and basic claim verification.
- // state check
- if(state && load('state') && state !== load('state')){ setAuthInfo('state 不匹配,已拒绝', false); return; }
+ // state check (must be present and match)
+ const storedState = load('state');
+ if(!state || !storedState || state !== storedState){ setAuthInfo('state 缺失或不匹配,已拒绝', false); return; }
try{
const tokenEp = load('token_endpoint');
const data = await postForm(tokenEp, {
grant_type:'authorization_code',
code,
client_id: load('client_id'),
redirect_uri: load('redirect_uri'),
code_verifier: load('code_verifier')
});
$('access_token').value = data.access_token || '';
$('id_token').value = data.id_token || '';
$('refresh_token').value = data.refresh_token || '';
$('token_raw').value = JSON.stringify(data, null, 2);
- setAuthInfo('授权成功,已获取令牌', true);
+ // OIDC ID Token checks
+ if (data.id_token) {
+ const parseClaims = (t)=>{
+ const p = t.split('.');
+ if (p.length < 2) throw new Error('ID Token 格式错误');
+ const json = atob(p[1].replace(/-/g,'+').replace(/_/g,'/'));
+ return JSON.parse(json);
+ };
+ try {
+ const claims = parseClaims(data.id_token);
+ const now = Math.floor(Date.now()/1000);
+ const expectedNonce = load('nonce');
+ const expectedAud = load('client_id');
+ const expectedIss = load('issuer') || '';
+ const audOk = Array.isArray(claims.aud) ? claims.aud.includes(expectedAud) : claims.aud === expectedAud;
+ if (expectedNonce && claims.nonce !== expectedNonce) throw new Error('ID Token nonce 不匹配');
+ if (expectedIss && claims.iss && claims.iss !== expectedIss) throw new Error('ID Token iss 不匹配');
+ if (!audOk) throw new Error('ID Token aud 不匹配');
+ if (claims.exp && now > claims.exp) throw new Error('ID Token 已过期');
+ if (claims.iat && now + 300 < claims.iat) throw new Error('ID Token iat 超前过多');
+ } catch (e) {
+ setAuthInfo('ID Token 校验失败:' + e.message, false);
+ return;
+ }
+ }
+ setAuthInfo('授权成功,已获取并校验令牌', true);
}catch(e){ setAuthInfo('交换令牌失败:'+e.message, false); }
Also applies to: 296-300
🤖 Prompt for AI Agents
In examples/oauth/oauth-demo.html around lines 270-294 (and also apply same
changes at 296-300), the callback handler currently allows missing state and
does not validate the ID Token; update it to require a non-empty stored state
and reject if the returned state is missing or does not match the stored value,
then when an id_token is returned decode the JWT payload (without relying on
remote signature verification for this demo) and validate the core OIDC claims:
verify nonce matches the stored nonce, iss equals the loaded issuer, aud
contains the loaded client_id, exp is in the future and iat is not unreasonably
in the future; if any check fails reject the callback and surface a clear error
via setAuthInfo; keep existing token exchange flow but perform these checks
before treating the login as successful and include try/catch to surface
parsing/validation errors.
cfg := clientcredentials.Config{ | ||
ClientID: "client_dsFyyoyNZWjhbNa2", // 需要先创建客户端 | ||
ClientSecret: "hLLdn2Ia4UM7hcsJaSuUFDV0Px9BrkNq", | ||
TokenURL: "http://localhost:3000/api/oauth/token", | ||
Scopes: []string{"api:read", "api:write"}, | ||
EndpointParams: map[string][]string{ | ||
"audience": {"api://new-api"}, | ||
}, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hard-coded client credentials committed — rotate immediately and load from env.
ClientID/ClientSecret are in source. Treat them as compromised, rotate now, and use environment variables instead.
Apply this diff:
@@
-import (
+import (
"context"
"fmt"
"io"
"log"
"net/http"
"net/url"
"path"
"strings"
"time"
+ "os"
@@
- cfg := clientcredentials.Config{
- ClientID: "client_dsFyyoyNZWjhbNa2", // 需要先创建客户端
- ClientSecret: "hLLdn2Ia4UM7hcsJaSuUFDV0Px9BrkNq",
+ cfg := clientcredentials.Config{
+ ClientID: os.Getenv("OAUTH_CLIENT_ID"),
+ ClientSecret: os.Getenv("OAUTH_CLIENT_SECRET"),
TokenURL: "http://localhost:3000/api/oauth/token",
Scopes: []string{"api:read", "api:write"},
EndpointParams: map[string][]string{
"audience": {"api://new-api"},
},
}
+ if cfg.ClientID == "" || cfg.ClientSecret == "" {
+ log.Fatal("missing OAUTH_CLIENT_ID or OAUTH_CLIENT_SECRET")
+ }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
cfg := clientcredentials.Config{ | |
ClientID: "client_dsFyyoyNZWjhbNa2", // 需要先创建客户端 | |
ClientSecret: "hLLdn2Ia4UM7hcsJaSuUFDV0Px9BrkNq", | |
TokenURL: "http://localhost:3000/api/oauth/token", | |
Scopes: []string{"api:read", "api:write"}, | |
EndpointParams: map[string][]string{ | |
"audience": {"api://new-api"}, | |
}, | |
} | |
import ( | |
"context" | |
"fmt" | |
"io" | |
"log" | |
"net/http" | |
"net/url" | |
"path" | |
"strings" | |
"time" | |
"os" | |
) | |
cfg := clientcredentials.Config{ | |
ClientID: os.Getenv("OAUTH_CLIENT_ID"), | |
ClientSecret: os.Getenv("OAUTH_CLIENT_SECRET"), | |
TokenURL: "http://localhost:3000/api/oauth/token", | |
Scopes: []string{"api:read", "api:write"}, | |
EndpointParams: map[string][]string{ | |
"audience": {"api://new-api"}, | |
}, | |
} | |
if cfg.ClientID == "" || cfg.ClientSecret == "" { | |
log.Fatal("missing OAUTH_CLIENT_ID or OAUTH_CLIENT_SECRET") | |
} |
🧰 Tools
🪛 Gitleaks (8.27.2)
[high] 32-32: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🤖 Prompt for AI Agents
In examples/oauth/oauth2_test_client.go around lines 30 to 38 the ClientID and
ClientSecret are hard-coded; rotate those compromised credentials immediately
and replace the literals with values loaded from environment variables (e.g.
read CLIENT_ID and CLIENT_SECRET via os.Getenv or a small helper), validate that
both env vars are present and return/log a clear error and exit if they are
missing, and keep TokenURL/Scopes/EndpointParams as-is; do not commit
secrets—ensure .env or secret management is used in deployment instead.
conf := oauth2.Config{ | ||
ClientID: "client_dsFyyoyNZWjhbNa2", // 需要先创建客户端 | ||
ClientSecret: "JHiugKf89OMmTLuZMZyA2sgZnO0Ioae3", | ||
RedirectURL: "http://localhost:9999/callback", | ||
// 包含 openid/profile/email 以便调用 UserInfo | ||
Scopes: []string{"openid", "profile", "email", "api:read"}, | ||
Endpoint: oauth2.Endpoint{ | ||
AuthURL: "http://localhost:3000/api/oauth/authorize", | ||
TokenURL: "http://localhost:3000/api/oauth/token", | ||
}, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Second occurrence of hard-coded secret in auth-code flow.
Same remediation as above. Load from env; do not commit secrets.
Apply this diff:
- conf := oauth2.Config{
- ClientID: "client_dsFyyoyNZWjhbNa2", // 需要先创建客户端
- ClientSecret: "JHiugKf89OMmTLuZMZyA2sgZnO0Ioae3",
+ conf := oauth2.Config{
+ ClientID: os.Getenv("OAUTH_CLIENT_ID"),
+ ClientSecret: os.Getenv("OAUTH_CLIENT_SECRET"),
RedirectURL: "http://localhost:9999/callback",
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
conf := oauth2.Config{ | |
ClientID: "client_dsFyyoyNZWjhbNa2", // 需要先创建客户端 | |
ClientSecret: "JHiugKf89OMmTLuZMZyA2sgZnO0Ioae3", | |
RedirectURL: "http://localhost:9999/callback", | |
// 包含 openid/profile/email 以便调用 UserInfo | |
Scopes: []string{"openid", "profile", "email", "api:read"}, | |
Endpoint: oauth2.Endpoint{ | |
AuthURL: "http://localhost:3000/api/oauth/authorize", | |
TokenURL: "http://localhost:3000/api/oauth/token", | |
}, | |
} | |
conf := oauth2.Config{ | |
ClientID: os.Getenv("OAUTH_CLIENT_ID"), | |
ClientSecret: os.Getenv("OAUTH_CLIENT_SECRET"), | |
RedirectURL: "http://localhost:9999/callback", | |
// 包含 openid/profile/email 以便调用 UserInfo | |
Scopes: []string{"openid", "profile", "email", "api:read"}, | |
Endpoint: oauth2.Endpoint{ | |
AuthURL: "http://localhost:3000/api/oauth/authorize", | |
TokenURL: "http://localhost:3000/api/oauth/token", | |
}, | |
} |
🧰 Tools
🪛 Gitleaks (8.27.2)
[high] 67-67: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🤖 Prompt for AI Agents
In examples/oauth/oauth2_test_client.go around lines 65 to 75, the ClientSecret
is hard-coded; replace this second hard-coded secret with a secret loaded from
the environment (e.g., os.Getenv("OAUTH_CLIENT_SECRET") or from a config loader)
and ensure the code validates non-empty value (log/fatal or return an error)
when missing; keep ClientID/RedirectURL/Scopes/Endpoint as-is or load similarly,
and do not commit any secret values to the repo.
const params = useMemo(() => { | ||
const allowed = [ | ||
'response_type', | ||
'client_id', | ||
'redirect_uri', | ||
'scope', | ||
'state', | ||
'code_challenge', | ||
'code_challenge_method', | ||
'nonce', | ||
]; | ||
const obj = {}; | ||
allowed.forEach((k) => { | ||
const v = query.get(k); | ||
if (v) obj[k] = v; | ||
}); | ||
if (!obj.response_type) obj.response_type = 'code'; | ||
return obj; | ||
}, [query]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t whitelist OAuth params; pass through all original query params.
Whitelisting drops extension params (e.g., response_mode, prompt, max_age, login_hint, acr_values, audience, resource), which can break OIDC/OAuth clients. Also, defaulting response_type to "code" may mask client mistakes. Preserve all query params and let the server validate.
- const params = useMemo(() => {
- const allowed = [
- 'response_type',
- 'client_id',
- 'redirect_uri',
- 'scope',
- 'state',
- 'code_challenge',
- 'code_challenge_method',
- 'nonce',
- ];
- const obj = {};
- allowed.forEach((k) => {
- const v = query.get(k);
- if (v) obj[k] = v;
- });
- if (!obj.response_type) obj.response_type = 'code';
- return obj;
- }, [query]);
+ const params = useMemo(() => {
+ const obj = {};
+ for (const [k, v] of query.entries()) obj[k] = v;
+ return obj; // preserve all; server validates
+ }, [query]);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const params = useMemo(() => { | |
const allowed = [ | |
'response_type', | |
'client_id', | |
'redirect_uri', | |
'scope', | |
'state', | |
'code_challenge', | |
'code_challenge_method', | |
'nonce', | |
]; | |
const obj = {}; | |
allowed.forEach((k) => { | |
const v = query.get(k); | |
if (v) obj[k] = v; | |
}); | |
if (!obj.response_type) obj.response_type = 'code'; | |
return obj; | |
}, [query]); | |
const params = useMemo(() => { | |
const obj = {}; | |
for (const [k, v] of query.entries()) obj[k] = v; | |
return obj; // preserve all; server validates | |
}, [query]); |
🤖 Prompt for AI Agents
In web/src/pages/OAuth/Consent.jsx around lines 20 to 38, the current code
builds a whitelisted subset of query params and injects a default
response_type='code', which drops OIDC/OAuth extension params and can mask
client errors; change the logic to copy through all original query parameters
from the incoming URLSearchParams (preserving every key/value pair, including
repeated keys) into the params object and remove the automatic defaulting of
response_type so the original request's value (or absence) is preserved and
server-side validation can handle it.
web/src/pages/OAuth/Consent.jsx
Outdated
const onApprove = () => { | ||
const u = new URL(window.location.origin + '/api/oauth/authorize'); | ||
Object.entries(params).forEach(([k, v]) => u.searchParams.set(k, v)); | ||
u.searchParams.set('approve', '1'); | ||
window.location.href = u.toString(); | ||
}; | ||
const onDeny = () => { | ||
const u = new URL(window.location.origin + '/api/oauth/authorize'); | ||
Object.entries(params).forEach(([k, v]) => u.searchParams.set(k, v)); | ||
u.searchParams.set('deny', '1'); | ||
window.location.href = u.toString(); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preserve unknown query params on approve/deny redirects.
Rebuilding the query from a filtered object risks losing client-sent parameters. Reuse the current URL’s search params, remove internal keys (e.g., mode), then set approve/deny.
- const onApprove = () => {
- const u = new URL(window.location.origin + '/api/oauth/authorize');
- Object.entries(params).forEach(([k, v]) => u.searchParams.set(k, v));
- u.searchParams.set('approve', '1');
- window.location.href = u.toString();
- };
+ const onApprove = () => {
+ const u = new URL(window.location.origin + '/api/oauth/authorize');
+ const s = new URLSearchParams(window.location.search);
+ s.delete('mode');
+ s.set('approve', '1');
+ u.search = s.toString();
+ window.location.href = u.toString();
+ };
- const onDeny = () => {
- const u = new URL(window.location.origin + '/api/oauth/authorize');
- Object.entries(params).forEach(([k, v]) => u.searchParams.set(k, v));
- u.searchParams.set('deny', '1');
- window.location.href = u.toString();
- };
+ const onDeny = () => {
+ const u = new URL(window.location.origin + '/api/oauth/authorize');
+ const s = new URLSearchParams(window.location.search);
+ s.delete('mode');
+ s.set('deny', '1');
+ u.search = s.toString();
+ window.location.href = u.toString();
+ };
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const onApprove = () => { | |
const u = new URL(window.location.origin + '/api/oauth/authorize'); | |
Object.entries(params).forEach(([k, v]) => u.searchParams.set(k, v)); | |
u.searchParams.set('approve', '1'); | |
window.location.href = u.toString(); | |
}; | |
const onDeny = () => { | |
const u = new URL(window.location.origin + '/api/oauth/authorize'); | |
Object.entries(params).forEach(([k, v]) => u.searchParams.set(k, v)); | |
u.searchParams.set('deny', '1'); | |
window.location.href = u.toString(); | |
}; | |
const onApprove = () => { | |
const u = new URL(window.location.origin + '/api/oauth/authorize'); | |
const s = new URLSearchParams(window.location.search); | |
s.delete('mode'); | |
s.set('approve', '1'); | |
u.search = s.toString(); | |
window.location.href = u.toString(); | |
}; | |
const onDeny = () => { | |
const u = new URL(window.location.origin + '/api/oauth/authorize'); | |
const s = new URLSearchParams(window.location.search); | |
s.delete('mode'); | |
s.set('deny', '1'); | |
u.search = s.toString(); | |
window.location.href = u.toString(); | |
}; |
{/* 密钥显示模态框 */} | ||
<Modal | ||
title="客户端密钥已重新生成" | ||
visible={showSecretModal} | ||
onCancel={() => setShowSecretModal(false)} | ||
onOk={() => setShowSecretModal(false)} | ||
cancelText="" | ||
okText="我已复制保存" | ||
width={600} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Harden the “new secret” modal: hide cancel, prevent accidental close, and wipe secret on close.
This reduces the chance of accidental dismissal and avoids keeping the secret in memory longer than needed.
- <Modal
- title="客户端密钥已重新生成"
- visible={showSecretModal}
- onCancel={() => setShowSecretModal(false)}
- onOk={() => setShowSecretModal(false)}
- cancelText=""
- okText="我已复制保存"
- width={600}
- >
+ <Modal
+ title="客户端密钥已重新生成"
+ visible={showSecretModal}
+ hasCancel={false}
+ maskClosable={false}
+ closeOnEsc={false}
+ onCancel={() => { setShowSecretModal(false); setCurrentSecret(''); }}
+ onOk={() => { setShowSecretModal(false); setCurrentSecret(''); }}
+ okText="我已复制保存"
+ width={600}
+ >
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In web/src/pages/Setting/OAuth2/OAuth2ClientSettings.jsx around lines 411-419,
the "new secret" Modal should be hardened: disable accidental closing by setting
closable={false}, maskClosable={false}, and keyboard={false} (so clicking the X,
outside the modal, or pressing Esc won't dismiss it), remove or omit the cancel
button by providing only the confirmation action (or ensure cancelText/Cancel
button is not shown), and ensure both the onOk and onCancel handlers close the
modal and wipe the secret from state (e.g., call setShowSecretModal(false) and
setClientSecret('') or equivalent) so the secret is not retained in memory after
the modal is dismissed.
const createClient = async () => { | ||
setBusy(true); | ||
try { | ||
const grant_types = client.client_type === 'public' | ||
? ['authorization_code', 'refresh_token'] | ||
: ['authorization_code', 'refresh_token', 'client_credentials']; | ||
const payload = { | ||
name: client.name, | ||
client_type: client.client_type, | ||
grant_types, | ||
redirect_uris: client.redirect_uris.filter(Boolean), | ||
scopes: client.scopes, | ||
require_pkce: true, | ||
}; | ||
const res = await API.post('/api/oauth_clients/', payload); | ||
if (res?.data?.success) { | ||
Toast.success('客户端已创建:' + res.data.client_id); | ||
onChanged && onChanged(); | ||
} else { | ||
showError(res?.data?.message || '创建失败'); | ||
} | ||
} catch (e) { | ||
showError('创建失败'); | ||
} finally { | ||
setBusy(false); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate redirect_uris before POST; mirror Create modal’s rules.
Without validation, server may reject or you’ll persist invalid URIs.
Apply this diff:
const createClient = async () => {
setBusy(true);
try {
+ const isValidRedirectUri = (uri) => {
+ if (!uri || !uri.trim()) return false;
+ try {
+ const u = new URL(uri.trim());
+ if (u.protocol !== 'https:' && u.protocol !== 'http:') return false;
+ if (u.protocol === 'http:') {
+ const host = u.hostname;
+ const isLocal = host === 'localhost' || host === '127.0.0.1' || host.endsWith('.local');
+ if (!isLocal) return false;
+ }
+ return true;
+ } catch { return false; }
+ };
const grant_types = client.client_type === 'public'
? ['authorization_code', 'refresh_token']
: ['authorization_code', 'refresh_token', 'client_credentials'];
const payload = {
name: client.name,
client_type: client.client_type,
grant_types,
- redirect_uris: client.redirect_uris.filter(Boolean),
+ redirect_uris: client.redirect_uris.filter(Boolean),
scopes: client.scopes,
require_pkce: true,
};
+ if (grant_types.includes('authorization_code')) {
+ const uris = payload.redirect_uris;
+ if (!uris.length || !uris.every(isValidRedirectUri)) {
+ showError('重定向URI格式不合法:仅支持https,或本地开发使用http');
+ return;
+ }
+ }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const createClient = async () => { | |
setBusy(true); | |
try { | |
const grant_types = client.client_type === 'public' | |
? ['authorization_code', 'refresh_token'] | |
: ['authorization_code', 'refresh_token', 'client_credentials']; | |
const payload = { | |
name: client.name, | |
client_type: client.client_type, | |
grant_types, | |
redirect_uris: client.redirect_uris.filter(Boolean), | |
scopes: client.scopes, | |
require_pkce: true, | |
}; | |
const res = await API.post('/api/oauth_clients/', payload); | |
if (res?.data?.success) { | |
Toast.success('客户端已创建:' + res.data.client_id); | |
onChanged && onChanged(); | |
} else { | |
showError(res?.data?.message || '创建失败'); | |
} | |
} catch (e) { | |
showError('创建失败'); | |
} finally { | |
setBusy(false); | |
} | |
}; | |
const createClient = async () => { | |
setBusy(true); | |
try { | |
const isValidRedirectUri = (uri) => { | |
if (!uri || !uri.trim()) return false; | |
try { | |
const u = new URL(uri.trim()); | |
if (u.protocol !== 'https:' && u.protocol !== 'http:') return false; | |
if (u.protocol === 'http:') { | |
const host = u.hostname; | |
const isLocal = host === 'localhost' || host === '127.0.0.1' || host.endsWith('.local'); | |
if (!isLocal) return false; | |
} | |
return true; | |
} catch { return false; } | |
}; | |
const grant_types = client.client_type === 'public' | |
? ['authorization_code', 'refresh_token'] | |
: ['authorization_code', 'refresh_token', 'client_credentials']; | |
const payload = { | |
name: client.name, | |
client_type: client.client_type, | |
grant_types, | |
redirect_uris: client.redirect_uris.filter(Boolean), | |
scopes: client.scopes, | |
require_pkce: true, | |
}; | |
if (grant_types.includes('authorization_code')) { | |
const uris = payload.redirect_uris; | |
if (!uris.length || !uris.every(isValidRedirectUri)) { | |
showError('重定向URI格式不合法:仅支持https,或本地开发使用http'); | |
return; | |
} | |
} | |
const res = await API.post('/api/oauth_clients/', payload); | |
if (res?.data?.success) { | |
Toast.success('客户端已创建:' + res.data.client_id); | |
onChanged && onChanged(); | |
} else { | |
showError(res?.data?.message || '创建失败'); | |
} | |
} catch (e) { | |
showError('创建失败'); | |
} finally { | |
setBusy(false); | |
} | |
}; |
}, []); | ||
|
||
const buildAuthorizeURL = () => { | ||
const u = new URL(values.authorization_endpoint || (server.issuer + '/oauth/authorize')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix authorize fallback path; should use /api/oauth/authorize (matches server + modal).
- const u = new URL(values.authorization_endpoint || (server.issuer + '/oauth/authorize'));
+ const u = new URL(values.authorization_endpoint || (server.issuer + '/api/oauth/authorize'));
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const u = new URL(values.authorization_endpoint || (server.issuer + '/oauth/authorize')); | |
const u = new URL(values.authorization_endpoint || (server.issuer + '/api/oauth/authorize')); |
🤖 Prompt for AI Agents
In web/src/pages/Setting/OAuth2/OAuth2Tools.jsx around line 61, the fallback
authorization URL uses '/oauth/authorize' but should use '/api/oauth/authorize'
to match the server and modal; update the fallback to construct the URL using
server.issuer + '/api/oauth/authorize' (or ensure values.authorization_endpoint
defaults to that path) so the authorization_endpoint resolves to the correct API
route.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 25
🧹 Nitpick comments (51)
src/oauth/util.go (2)
23-30
: Validate entropy size in genCode to prevent zero/negative and too‑low entropy.Guard against
nBytes <= 0
(panic risk) and enforce a sensible minimum (e.g., 16 or 32 bytes).func genCode(nBytes int) (string, error) { - b := make([]byte, nBytes) + if nBytes <= 0 { + return "", fmt.Errorf("nBytes must be > 0") + } + if nBytes < 32 { + nBytes = 32 + } + b := make([]byte, nBytes) if _, err := rand.Read(b); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(b), nil }Remember to add:
import ( "crypto/rand" "crypto/sha256" "encoding/base64" + "fmt" "net/http" "net/url" "strings"
38-42
: Optional: strengthen no-store headers.Adding
Expires: 0
helps with legacy intermediaries. Harmless and improves cache-busting.func writeNoStore(c *gin.Context) { c.Header("Cache-Control", "no-store") c.Header("Pragma", "no-cache") + c.Header("Expires", "0") }
web/src/components/modals/oauth2/CreateOAuth2ClientModal.jsx (2)
110-127
: Broaden localhost detection for HTTP redirect URIs.Allow IPv6 loopback (::1), 127.0.0.0/8, and subdomains of .localhost per RFC 6761. Current check blocks valid local dev hosts.
const isValidRedirectUri = (uri) => { if (!uri || !uri.trim()) return false; try { const u = new URL(uri.trim()); if (u.protocol !== 'https:' && u.protocol !== 'http:') return false; if (u.protocol === 'http:') { // 仅允许本地开发时使用 http - const host = u.hostname; - const isLocal = - host === 'localhost' || host === '127.0.0.1' || host.endsWith('.local'); + const host = u.hostname; + const isIPv4Loopback = /^127(?:\.(?:25[0-5]|2[0-4]\d|1?\d{1,2})){3}$/.test(host); + const isLocal = + host === 'localhost' || + host === '::1' || + isIPv4Loopback || + host.endsWith('.localhost'); if (!isLocal) return false; } return true; } catch (e) { return false; } };
259-270
: Avoid double setState in handleGrantTypesChange.Compute once to prevent intermediate states and avoid surprising UI flashes.
const handleGrantTypesChange = (values) => { - setGrantTypes(values); - // 如果包含authorization_code但没有重定向URI,则添加一个 - if (values.includes('authorization_code') && redirectUris.length === 0) { - setRedirectUris(['']); - } - // 公开客户端不允许client_credentials - if (clientType === 'public' && values.includes('client_credentials')) { - setGrantTypes(values.filter((v) => v !== 'client_credentials')); - } + const filtered = values.filter((v) => + allowedGrantTypes.includes(v) && !(clientType === 'public' && v === 'client_credentials') + ); + setGrantTypes(filtered); + if (filtered.includes('authorization_code') && redirectUris.length === 0) { + setRedirectUris(['']); + } };web/src/pages/Setting/OAuth2/OAuth2ServerSettings.jsx (2)
60-97
: Make saves robust: use Promise.allSettled and update the baseline snapshot.
res.includes(undefined)
is unreliable; switch toallSettled
, surface partial failures, and syncinputsRow
after success to avoid false “no changes” or repeated diffs.- function onSubmit() { + function onSubmit() { const updateArray = compareObjects(inputs, inputsRow); if (!updateArray.length) return showWarning(t('你似乎并没有修改什么')); - const requestQueue = updateArray.map((item) => { + const requestQueue = updateArray.map((item) => { let value = ''; if (typeof inputs[item.key] === 'boolean') { value = String(inputs[item.key]); } else if (Array.isArray(inputs[item.key])) { value = JSON.stringify(inputs[item.key]); } else { value = inputs[item.key]; } return API.put('/api/option/', { key: item.key, value, }); }); setLoading(true); - Promise.all(requestQueue) - .then((res) => { - if (requestQueue.length === 1) { - if (res.includes(undefined)) return; - } else if (requestQueue.length > 1) { - if (res.includes(undefined)) - return showError(t('部分保存失败,请重试')); - } - showSuccess(t('保存成功')); - if (props && props.refresh) { - props.refresh(); - } - }) - .catch(() => { - showError(t('保存失败,请重试')); - }) - .finally(() => { - setLoading(false); - }); + Promise.allSettled(requestQueue) + .then((results) => { + const failed = results.filter(r => r.status === 'rejected'); + if (failed.length) { + showError(t('部分保存失败,请重试')); + return; + } + showSuccess(t('保存成功')); + // 更新基准,避免后续比较误报 + setInputsRow(structuredClone(inputs)); + if (props && props.refresh) { + props.refresh(); + } + }) + .catch(() => { + showError(t('保存失败,请重试')); + }) + .finally(() => { + setLoading(false); + }); }
99-112
: Dead code: testOAuth2 is defined but not used.Either wire a “测试连接”按钮或移除函数以减轻维护负担.
examples/oauth/oauth2_test_client.go (1)
91-123
: Avoid DefaultServeMux to prevent handler collisions.Use a dedicated ServeMux for the embedded callback server.
- codeCh := make(chan string, 1) - srv := &http.Server{Addr: ":9999"} - http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + codeCh := make(chan string, 1) + mux := http.NewServeMux() + srv := &http.Server{Addr: ":9999", Handler: mux} + mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { ... }) go func() { - _ = srv.ListenAndServe() + _ = srv.ListenAndServe() }()examples/oauth/oauth-demo.html (1)
296-300
: Pad base64url before atob when decoding ID Token.Without padding, atob may throw for some JWT lengths.
- $('btnDecodeJWT').onclick = () => { - const t = $('id_token').value.trim(); if(!t){ $('jwtClaims').textContent='(空)'; return; } - const parts = t.split('.'); if(parts.length<2){ $('jwtClaims').textContent='格式错误'; return; } - try{ const json = JSON.parse(atob(parts[1].replace(/-/g,'+').replace(/_/g,'/'))); $('jwtClaims').textContent = JSON.stringify(json, null, 2);}catch(e){ $('jwtClaims').textContent='解码失败:'+e; } - }; + $('btnDecodeJWT').onclick = () => { + const t = $('id_token').value.trim(); if(!t){ $('jwtClaims').textContent='(空)'; return; } + const parts = t.split('.'); if(parts.length<2){ $('jwtClaims').textContent='格式错误'; return; } + try{ + let payload = parts[1].replace(/-/g,'+').replace(/_/g,'/'); + payload += '='.repeat((4 - (payload.length % 4)) % 4); + const json = JSON.parse(atob(payload)); + $('jwtClaims').textContent = JSON.stringify(json, null, 2); + } catch(e){ + $('jwtClaims').textContent='解码失败:'+e; + } + };web/src/pages/Setting/OAuth2/OAuth2QuickStart.jsx (4)
17-37
: Redundant API calls in quick succession could be optimizedThe current implementation makes 5 sequential PUT requests to set OAuth2 options. This could be optimized by batching these into a single backend endpoint call to reduce network overhead and improve performance.
Consider creating a backend endpoint that accepts all OAuth2 settings in a single request:
- for (const op of ops) { - await API.put('/api/option/', op); - } + // Consider implementing a batch update endpoint + await API.put('/api/oauth/setup', { + settings: ops + });
61-63
: Grant types should match client type configurationThe grant types array includes
client_credentials
for all clients, but public clients typically shouldn't use this flow according to OAuth2 best practices.The
client_credentials
grant type should only be included for confidential clients, not for public clients.
105-115
: Missing field validation before form submissionThe form doesn't validate that required fields (name, redirect_uris) are properly filled before submission.
Apply input validation:
const applyRecommended = async () => { + if (!issuer?.trim()) { + showError('请填写发行人地址'); + return; + } setBusy(true);
110-114
: Potential array mutation issueDirect array mutation when updating redirect URIs could cause React rendering issues.
Use immutable array updates:
- const arr=[...client.redirect_uris]; arr[0]=v; setClient({...client, redirect_uris: arr}); + setClient({...client, redirect_uris: client.redirect_uris.map((uri, idx) => idx === 0 ? v : uri)});web/src/pages/OAuth/Consent.jsx (2)
118-118
: Potential undefined access when slicing user nameThe code doesn't check if
info?.user?.name
exists before calling.slice(0,1)
, which could cause a runtime error.Apply this fix:
- <Avatar size='small'>{String(info?.user?.name || 'U').slice(0,1).toUpperCase()}</Avatar> + <Avatar size='small'>{(info?.user?.name || 'U')[0].toUpperCase()}</Avatar>
173-173
: Use consistent origin referenceThe issuer display uses
window.location.origin
directly instead of using a consistent reference.+ const origin = useMemo(() => window.location.origin, []); // ... in the component - key: 'Issuer', value: window.location.origin, + key: 'Issuer', value: origin,web/public/oauth-demo.html (2)
149-149
: Path traversal concern in discovery endpointThe issuer URL is directly concatenated with the discovery path, which could potentially be exploited if the issuer contains path traversal sequences.
Validate and normalize the issuer URL:
- const res=await fetch(iss.replace(/\/$/,'')+'/api/.well-known/openid-configuration'); + const url = new URL('/api/.well-known/openid-configuration', iss.replace(/\/$/, '')); + const res = await fetch(url.toString());
158-158
: Complex single-line callback handling reduces maintainabilityThe callback handling logic is crammed into a single line, making it difficult to debug and maintain.
Break down the complex callback handling for better readability:
- async function handleCallback(){ const frag=location.hash&&location.hash.startsWith('#')?new URLSearchParams(location.hash.slice(1)):null; const at=frag?frag.get('access_token'):null; const err=qs('error')|| (frag?frag.get('error'):null); const state=qs('state')||(frag?frag.get('state'):null); if(err){ setAuthInfo('授权失败:'+err,false); return; } if(at){ $('access_token').value=at||''; $('token_raw').value=JSON.stringify({ access_token: at, token_type: frag.get('token_type'), expires_in: frag.get('expires_in'), scope: frag.get('scope'), state }, null, 2); setAuthInfo('隐式模式已获取 Access Token', true); return; } const code=qs('code'); if(!code){ setAuthInfo('等待授权...', true); return; } if(state && load('state') && state!==load('state')){ setAuthInfo('state 不匹配,已拒绝', false); return; } try{ const tokenEp=load('token_endpoint'); const cid=load('client_id'); const csec=$('client_secret').value.trim(); const basic=csec?{id:cid,secret:csec}:null; const data=await postForm(tokenEp,{ grant_type:'authorization_code', code, client_id:cid, redirect_uri:load('redirect_uri'), code_verifier:load('code_verifier') }, basic); $('access_token').value=data.access_token||''; $('id_token').value=data.id_token||''; $('refresh_token').value=data.refresh_token||''; $('token_raw').value=JSON.stringify(data,null,2); setAuthInfo('授权成功,已获取令牌', true);}catch(e){ setAuthInfo('交换令牌失败:'+e.message,false);} } handleCallback(); + async function handleCallback() { + const frag = location.hash && location.hash.startsWith('#') + ? new URLSearchParams(location.hash.slice(1)) + : null; + const at = frag ? frag.get('access_token') : null; + const err = qs('error') || (frag ? frag.get('error') : null); + const state = qs('state') || (frag ? frag.get('state') : null); + + if (err) { + setAuthInfo('授权失败:' + err, false); + return; + } + + // ... rest of the function with proper formatting + }web/src/components/modals/oauth2/OAuth2QuickStartModal.jsx (3)
67-68
: Sequential API calls could benefit from error handling per operationWhen applying multiple settings sequentially, if one fails, it's unclear which specific operation failed, making debugging difficult.
Add granular error handling:
for (const op of ops) { - await API.put('/api/option/', op); + try { + await API.put('/api/option/', op); + } catch (e) { + showError(`Failed to set ${op.key}: ${e.message}`); + throw e; + } }
101-101
: Duplicate grant types logic across componentsThe grant types determination logic is duplicated between this modal and OAuth2QuickStart.jsx.
Extract to a shared utility:
// In helpers/oauth.js export const getGrantTypesForClientType = (clientType) => { return clientType === 'public' ? ['authorization_code', 'refresh_token'] : ['authorization_code', 'refresh_token', 'client_credentials']; };
134-134
: Chinese text inconsistency with other UI elementsThe switch shows '开'/'关' for on/off states, but other parts of the UI use English. Consider maintaining consistency.
Use consistent language or provide internationalization support.
model/main.go (1)
284-301
: Keep migrateDBFast in sync with migrateDB.OAuthClient isn’t in the migrateDBFast() list. Add it to prevent divergence if fast path is used.
migrations := []struct { model interface{} name string }{ {&Channel{}, "Channel"}, {&Token{}, "Token"}, {&User{}, "User"}, {&Option{}, "Option"}, {&Redemption{}, "Redemption"}, {&Ability{}, "Ability"}, {&Log{}, "Log"}, {&Midjourney{}, "Midjourney"}, {&TopUp{}, "TopUp"}, {&QuotaData{}, "QuotaData"}, {&Task{}, "Task"}, {&Model{}, "Model"}, {&Vendor{}, "Vendor"}, {&PrefillGroup{}, "PrefillGroup"}, {&Setup{}, "Setup"}, {&TwoFA{}, "TwoFA"}, {&TwoFABackupCode{}, "TwoFABackupCode"}, + {&OAuthClient{}, "OAuthClient"}, }
web/src/pages/Setting/index.jsx (1)
139-148
: New “OAuth2 & SSO” tab: OK. Consider lazy-loading to trim initial bundle.The tab integration looks correct. You can lazy-load the heavy settings module to reduce the settings page’s initial JS size.
-import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, Suspense } from 'react'; ... -import OAuth2Setting from '../../components/settings/OAuth2Setting'; +const OAuth2Setting = React.lazy(() => import('../../components/settings/OAuth2Setting')); ... - content: <OAuth2Setting />, + content: <Suspense fallback={null}><OAuth2Setting /></Suspense>,web/src/pages/Setting/OAuth2/JWKSManager.jsx (3)
45-60
: Consistent success notifications.Use the same helper for success to keep UX consistent with errors.
- Toast.success('已删除:' + kid); + showSuccess('已删除:' + kid);
101-109
: Add a confirmation for key rotation to prevent accidental rotates.Rotation is sensitive; confirm before proceeding.
- <Button icon={<IconPlay />} type='primary' onClick={rotate} loading={loading}>轮换密钥</Button> + <Popconfirm title='确定轮换密钥?' content='将生成新的 kid 并立即生效' okText='确认' onConfirm={rotate}> + <Button icon={<IconPlay />} type='primary' loading={loading}>轮换密钥</Button> + </Popconfirm>
12-60
: Deduplicate JWKS logic shared with JWKSManagerModal.load/rotate/del and columns are near copies of the modal variant. Extract a shared hook/util (e.g., useJWKSAdmin) to avoid drift.
Also applies to: 66-100, 101-121
web/src/components/settings/OAuth2Setting.jsx (2)
82-83
: Prevent reverse‑tabnabbing on window.open.Add noopener,noreferrer for safety when opening a new tab.
- <Button onClick={()=>window.open('/oauth-demo.html','_blank')}>前端 Demo</Button> + <Button onClick={()=>window.open('/oauth-demo.html','_blank','noopener,noreferrer')}>前端 Demo</Button>
35-54
: Minor: avoid setState after unmount during slow network.Optional: guard getOptions with an abort flag to prevent state updates after unmount.
web/src/components/modals/oauth2/JWKSManagerModal.jsx (1)
112-118
: Confirm before rotating the active signing key.Accidental rotation can immediately invalidate downstream caches; add a Popconfirm.
- <Button icon={<IconPlay />} type='primary' onClick={rotate} loading={loading}>轮换密钥</Button> + <Popconfirm title='确定轮换密钥?' content='将生成新的 kid 并立即生效' okText='确认' onConfirm={rotate}> + <Button icon={<IconPlay />} type='primary' loading={loading}>轮换密钥</Button> + </Popconfirm>middleware/auth.go (2)
183-198
: Prefer early returns in TokenAuth() to improve code readability.The nested error handling can be simplified by extracting OAuth fallback earlier in the flow.
func TokenAuth() func(c *gin.Context) { return func(c *gin.Context) { rawAuth := c.Request.Header.Get("Authorization") + // Try OAuth Bearer fallback first if standard token validation fails + validateStandardToken := func() error { + // existing validation logic here... + token, err := model.ValidateUserToken(key) + // ... rest of standard token validation + return err + } + + if err := validateStandardToken(); err != nil { + if tryOAuthBearer(c, rawAuth) { + c.Next() + return + } + abortWithOpenAiMessage(c, http.StatusUnauthorized, err.Error()) + return + }
334-336
: Validate issuer claim more defensively.The issuer validation should handle the case where the issuer claim exists but is empty.
-if iss, ok2 := claims["iss"].(string); !ok2 || (settings.Issuer != "" && iss != settings.Issuer) { +iss, ok2 := claims["iss"].(string) +if !ok2 || iss == "" || (settings.Issuer != "" && iss != settings.Issuer) { return false }src/oauth/store.go (1)
80-82
: Consider adding error handling to wrapper functions.The store wrapper functions silently swallow errors from the underlying stores, which could mask important issues.
Consider propagating errors to callers or at least logging them for debugging:
-func storeSet(key, val string, ttl time.Duration) error { return getStore().Set(key, val, ttl) } -func storeGet(key string) (string, bool) { return getStore().Get(key) } -func storeDel(key string) error { return getStore().Del(key) } +func storeSet(key, val string, ttl time.Duration) error { + if err := getStore().Set(key, val, ttl); err != nil { + // Consider logging the error + return err + } + return nil +} +func storeGet(key string) (string, bool) { return getStore().Get(key) } +func storeDel(key string) error { + if err := getStore().Del(key); err != nil { + // Consider logging the error + return err + } + return nil +}router/api-router.go (1)
265-275
: OAuth client management routes lack OAuth authentication support.The OAuth client management routes only use
AdminAuth()
withoutOptionalOAuthAuth()
, preventing OAuth-authenticated admins from managing OAuth clients.Consider adding OAuth authentication support:
oauthClientsRoute := apiRouter.Group("/oauth_clients") -oauthClientsRoute.Use(middleware.AdminAuth()) +oauthClientsRoute.Use(middleware.OptionalOAuthAuth(), middleware.RequireOAuthScopeIfPresent("admin"), middleware.AdminAuth())model/oauth_revoked_token.go (2)
48-55
: Fix potential panic with type assertion.The type assertion
exp, _ := v.(int64)
on line 50 silently ignores errors, which could lead to unexpected behavior if the stored value isn't int64.// In-memory check if v, ok := revokedMem.Load(jti); ok { - exp, _ := v.(int64) + exp, ok := v.(int64) + if !ok { + // Log error or handle invalid data + revokedMem.Delete(jti) + return false, nil + } if exp == 0 || time.Now().Unix() <= exp { return true, nil } revokedMem.Delete(jti) }
12-33
: Consider adding periodic cleanup for in-memory revoked tokens.The in-memory store only performs lazy cleanup during
IsTokenRevoked
calls. This could lead to memory growth if tokens are revoked but never checked.Would you like me to implement a periodic cleanup goroutine that removes expired entries from the in-memory store?
Example approach:
func init() { go func() { ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for range ticker.C { cleanupExpiredTokens() } }() } func cleanupExpiredTokens() { now := time.Now().Unix() revokedMem.Range(func(key, value interface{}) bool { if exp, ok := value.(int64); ok && exp > 0 && now > exp { revokedMem.Delete(key) } return true }) }web/src/pages/Setting/OAuth2/OAuth2ClientSettings.jsx (3)
37-41
: Use absolute import paths for consistencyThe import paths mix relative and absolute imports. Consider using consistent import strategy.
-import { API, showError, showSuccess, showInfo } from '../../../helpers'; -import CreateOAuth2ClientModal from '../../../components/modals/oauth2/CreateOAuth2ClientModal'; -import EditOAuth2ClientModal from '../../../components/modals/oauth2/EditOAuth2ClientModal'; +import { API, showError, showSuccess, showInfo } from 'web/src/helpers'; +import CreateOAuth2ClientModal from 'web/src/components/modals/oauth2/CreateOAuth2ClientModal'; +import EditOAuth2ClientModal from 'web/src/components/modals/oauth2/EditOAuth2ClientModal';
148-161
: Prefer extraction for grant type display mappingThe inline conditional mapping makes the render function harder to read.
Extract the grant type display mapping to a constant or helper function:
const GRANT_TYPE_LABELS = { 'client_credentials': '客户端凭证', 'authorization_code': '授权码', 'refresh_token': '刷新令牌' }; // Then in render: {GRANT_TYPE_LABELS[type] || type}
177-177
: Add timezone context to timestamp displayThe date conversion uses the browser's locale, which might differ from server timezone.
Consider showing the timezone or using a consistent UTC display:
-render: (time) => new Date(time * 1000).toLocaleString(), +render: (time) => new Date(time * 1000).toLocaleString() + ' (Local)',controller/oauth_keys.go (2)
26-36
: Missing error validation on JSON bindingThe error from
c.BindJSON
is ignored, which could lead to unexpected behavior.func RotateOAuthSigningKey(c *gin.Context) { var req rotateKeyRequest - _ = c.BindJSON(&req) + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid request body"}) + return + } kid, err := oauth.RotateSigningKey(req.Kid)
75-89
: Consider PEM content size limitsThe PEM import accepts arbitrary size input which could lead to memory issues with very large inputs.
Consider adding a reasonable size limit for the PEM content:
func ImportOAuthSigningKey(c *gin.Context) { var req importPemRequest if err := c.ShouldBindJSON(&req); err != nil || req.Pem == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "pem required"}) return } + // Limit PEM size to 100KB (reasonable for RSA keys) + if len(req.Pem) > 100*1024 { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "pem content too large"}) + return + } kid, err := oauth.ImportPEMKey(req.Pem, req.Kid)web/src/pages/Setting/OAuth2/OAuth2Tools.jsx (1)
60-73
: URL construction needs validationThe authorization endpoint might be malformed or missing, leading to invalid URLs.
const buildAuthorizeURL = () => { + const endpoint = values.authorization_endpoint || (server.issuer + '/oauth/authorize'); + try { + const u = new URL(endpoint); + } catch (e) { + Toast.error('Invalid authorization endpoint URL'); + return ''; + } - const u = new URL(values.authorization_endpoint || (server.issuer + '/oauth/authorize'));web/src/components/modals/oauth2/EditOAuth2ClientModal.jsx (1)
155-169
: Early returns after error reduce nesting but create redundant loading state callsMultiple
setLoading(false)
calls could be consolidated.Consider using a finally block or consolidating error handling:
const handleSubmit = async (values) => { setLoading(true); try { // ... validation logic ... - if (!grantTypes.length) { - showError('请至少选择一种授权类型'); - setLoading(false); - return; - } + if (!grantTypes.length) { + throw new Error('请至少选择一种授权类型'); + } // ... more validation ... } catch (error) { - showError('更新OAuth2客户端失败'); + showError(error.message || '更新OAuth2客户端失败'); } finally { setLoading(false); } };web/src/components/modals/oauth2/OAuth2ToolsModal.jsx (2)
104-105
: Hardcoded API path in discovery URLThe discovery URL construction hardcodes
/api/
which might not work for all deployments.-const url = iss.replace(/\/$/, '') + '/api/.well-known/openid-configuration'; +// Allow issuer to include the full path if needed +const url = iss.includes('/.well-known/') + ? iss + : iss.replace(/\/$/, '') + '/api/.well-known/openid-configuration';
7-15
: Extract duplicated crypto utilities into a shared helpersha256Base64Url and randomString are duplicated in web/src/components/modals/oauth2/OAuth2ToolsModal.jsx and web/src/pages/Setting/OAuth2/OAuth2Tools.jsx — move them to web/src/helpers/crypto.js (export async function sha256Base64Url(input) and export function randomString(len = 43)) and replace the local definitions with:
import { sha256Base64Url, randomString } from '../../../helpers/crypto';middleware/oauth_jwt.go (1)
111-112
: TODO needs implementation for production readinessThe TODO comment indicates missing JWKS caching and refresh mechanism, which is critical for production deployments to avoid performance issues and handle key rotation properly.
Do you want me to generate the JWKS caching and refresh mechanism implementation?
model/oauth_client.go (1)
38-42
: Error handling: JSON unmarshal errors are only loggedWhen JSON unmarshaling fails, the error is logged but an empty array is returned, which could hide configuration issues.
Consider returning an error or storing redirect URIs in a normalized format to avoid runtime parsing:
-func (c *OAuthClient) GetRedirectURIs() []string { +func (c *OAuthClient) GetRedirectURIs() ([]string, error) { if c.RedirectURIs == "" { - return []string{} + return []string{}, nil } var uris []string err := json.Unmarshal([]byte(c.RedirectURIs), &uris) if err != nil { common.SysLog("failed to unmarshal redirect URIs: " + err.Error()) - return []string{} + return []string{}, err } - return uris + return uris, nil }controller/oauth_client.go (7)
40-47
: Add upper bound check for page parameter to prevent integer overflow.The page parameter is unbounded, which could lead to integer overflow when calculating
startIdx
on line 49.page, _ := strconv.Atoi(c.Query("page")) if page < 1 { page = 1 } +// Prevent potential overflow in startIdx calculation +const maxPage = 1000000 +if page > maxPage { + page = maxPage +}
175-175
: Validate that the creator ID exists and handle the zero value case.Getting the ID from context without validation could result in a zero value if the context doesn't contain the ID, potentially causing issues with foreign key constraints.
// 获取创建者ID createdBy := c.GetInt("id") +if createdBy == 0 { + c.JSON(http.StatusUnauthorized, gin.H{ + "success": false, + "message": "用户未认证", + }) + return +}
223-231
: Consider checking ownership before allowing updates.Currently any authenticated admin can update any OAuth client. Consider adding ownership validation to ensure only the creator or super admin can modify a client.
// 获取现有客户端 client, err := model.GetOAuthClientByID(req.ID) if err != nil { c.JSON(http.StatusNotFound, gin.H{ "success": false, "message": "客户端不存在", }) return } + +// Check if user is the creator or has super admin privileges +userID := c.GetInt("id") +userRole := c.GetString("role") // Assuming role is stored in context +if client.CreatedBy != userID && userRole != "super_admin" { + c.JSON(http.StatusForbidden, gin.H{ + "success": false, + "message": "无权修改此客户端", + }) + return +}
349-356
: Consider namespacing client IDs to avoid collisions.The current client ID generation could potentially collide across different environments or installations.
Consider adding environment or timestamp information:
func generateClientID() string { - return "client_" + randstr.String(16) + // Include timestamp for better uniqueness + return "client_" + strconv.FormatInt(time.Now().Unix(), 36) + "_" + randstr.String(12) }
359-364
: Secret masking might be too revealing for short secrets.The current masking shows first and last 3 characters, which could be too revealing for security purposes.
func maskSecret(secret string) string { if len(secret) <= 6 { return strings.Repeat("*", len(secret)) } - return secret[:3] + strings.Repeat("*", len(secret)-6) + secret[len(secret)-3:] + // Show less of the actual secret for better security + return secret[:2] + strings.Repeat("*", len(secret)-4) + secret[len(secret)-2:] }
366-374
: Consider using a map for O(1) lookups instead of O(n) linear search.The
contains
function performs a linear search which could be inefficient for repeated lookups.For better performance with frequent lookups:
+// Define at package level +var validGrantTypesMap = map[string]bool{ + "client_credentials": true, + "authorization_code": true, + "refresh_token": true, +} + // contains 检查字符串切片是否包含指定值 func contains(slice []string, item string) bool { + // For grant types validation, use the map + if slice == nil { + return false + } for _, s := range slice { if s == item { return true } } return false }
117-124
: Clarify 404 message for disabled OAuth clients (GetOAuthClientByID enforces enabled-only)model.GetOAuthClientByID filters by status = common.UserStatusEnabled (model/oauth_client.go:141–145). OAuth paths correctly treat disabled clients as unknown, but controller endpoints should return a clearer message.
Locations: controller/oauth_client.go — callers at ~lines 117, 224, 311.
client, err := model.GetOAuthClientByID(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{ "success": false, - "message": "客户端不存在", + "message": "客户端不存在或已禁用", }) return }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sum
is excluded by!**/*.sum
📒 Files selected for processing (32)
controller/oauth.go
(1 hunks)controller/oauth_client.go
(1 hunks)controller/oauth_keys.go
(1 hunks)examples/oauth/oauth-demo.html
(1 hunks)examples/oauth/oauth2_test_client.go
(1 hunks)go.mod
(5 hunks)main.go
(2 hunks)middleware/auth.go
(4 hunks)middleware/oauth_jwt.go
(1 hunks)model/main.go
(1 hunks)model/oauth_client.go
(1 hunks)model/oauth_revoked_token.go
(1 hunks)router/api-router.go
(8 hunks)setting/system_setting/oauth2.go
(1 hunks)src/oauth/server.go
(1 hunks)src/oauth/store.go
(1 hunks)src/oauth/util.go
(1 hunks)web/public/oauth-demo.html
(1 hunks)web/src/App.jsx
(2 hunks)web/src/components/modals/oauth2/CreateOAuth2ClientModal.jsx
(1 hunks)web/src/components/modals/oauth2/EditOAuth2ClientModal.jsx
(1 hunks)web/src/components/modals/oauth2/JWKSManagerModal.jsx
(1 hunks)web/src/components/modals/oauth2/OAuth2QuickStartModal.jsx
(1 hunks)web/src/components/modals/oauth2/OAuth2ToolsModal.jsx
(1 hunks)web/src/components/settings/OAuth2Setting.jsx
(1 hunks)web/src/pages/OAuth/Consent.jsx
(1 hunks)web/src/pages/Setting/OAuth2/JWKSManager.jsx
(1 hunks)web/src/pages/Setting/OAuth2/OAuth2ClientSettings.jsx
(1 hunks)web/src/pages/Setting/OAuth2/OAuth2QuickStart.jsx
(1 hunks)web/src/pages/Setting/OAuth2/OAuth2ServerSettings.jsx
(1 hunks)web/src/pages/Setting/OAuth2/OAuth2Tools.jsx
(1 hunks)web/src/pages/Setting/index.jsx
(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (26)
model/main.go (1)
model/oauth_client.go (1)
OAuthClient
(13-30)
web/src/pages/Setting/OAuth2/OAuth2QuickStart.jsx (2)
web/src/components/modals/oauth2/OAuth2QuickStartModal.jsx (4)
Typography
(5-5)origin
(8-8)applyRecommended
(57-78)createClient
(98-124)web/src/helpers/utils.jsx (2)
showSuccess
(153-155)showError
(118-147)
web/src/pages/Setting/OAuth2/OAuth2ClientSettings.jsx (3)
web/src/components/modals/oauth2/CreateOAuth2ClientModal.jsx (3)
loading
(42-42)grantTypes
(45-45)CreateOAuth2ClientModal
(40-444)web/src/components/modals/oauth2/EditOAuth2ClientModal.jsx (3)
loading
(41-41)grantTypes
(43-43)EditOAuth2ClientModal
(39-415)web/src/helpers/utils.jsx (3)
showError
(118-147)showSuccess
(153-155)showInfo
(157-159)
web/src/App.jsx (2)
web/src/components/common/ui/Loading.jsx (1)
Loading
(23-29)web/src/pages/OAuth/Consent.jsx (1)
OAuthConsent
(14-199)
main.go (2)
src/oauth/server.go (1)
InitOAuthServer
(37-65)common/sys_log.go (1)
SysLog
(10-13)
src/oauth/store.go (1)
common/redis.go (4)
RedisSet
(64-70)RedisGet
(72-79)RedisDel
(91-97)RedisEnabled
(17-17)
web/src/pages/Setting/OAuth2/JWKSManager.jsx (2)
web/src/components/modals/oauth2/JWKSManagerModal.jsx (6)
Typography
(6-6)loading
(9-9)keys
(10-10)load
(12-19)rotate
(21-28)del
(30-37)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
web/src/pages/Setting/OAuth2/OAuth2Tools.jsx (1)
web/src/components/modals/oauth2/OAuth2ToolsModal.jsx (11)
enc
(8-8)hash
(10-10)i
(13-13)i
(22-22)charset
(18-18)res
(19-19)server
(27-27)values
(39-52)buildAuthorizeURL
(74-88)copy
(90-92)genVerifier
(94-98)
web/src/components/modals/oauth2/OAuth2ToolsModal.jsx (1)
web/src/pages/Setting/OAuth2/OAuth2Tools.jsx (15)
enc
(8-8)data
(9-9)hash
(10-10)bytes
(11-11)binary
(12-12)i
(13-13)i
(22-22)charset
(18-18)res
(19-19)array
(20-20)server
(28-28)values
(29-41)buildAuthorizeURL
(60-73)copy
(75-80)genVerifier
(82-86)
web/src/components/modals/oauth2/EditOAuth2ClientModal.jsx (1)
web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
controller/oauth_client.go (2)
model/oauth_client.go (8)
GetAllOAuthClients
(149-153)CountOAuthClients
(179-183)SearchOAuthClients
(156-161)GetOAuthClientByID
(142-146)CreateOAuthClient
(164-166)OAuthClient
(13-30)UpdateOAuthClient
(169-171)DeleteOAuthClient
(174-176)common/constants.go (1)
UserStatusEnabled
(174-174)
web/src/pages/Setting/index.jsx (1)
web/src/components/settings/OAuth2Setting.jsx (1)
OAuth2Setting
(30-92)
controller/oauth.go (5)
src/oauth/server.go (6)
GetJWKS
(114-116)EnsureInitialized
(68-94)HandleTokenRequest
(321-344)HandleAuthorizeRequest
(748-999)GetPublicKeyByKid
(127-132)GetRSAPublicKey
(119-124)setting/system_setting/oauth2.go (1)
GetOAuth2Settings
(49-51)model/oauth_revoked_token.go (2)
IsTokenRevoked
(35-57)RevokeToken
(12-33)middleware/oauth_jwt.go (1)
GetOAuthClaims
(278-286)model/user.go (1)
GetUserById
(287-299)
web/src/components/modals/oauth2/OAuth2QuickStartModal.jsx (2)
web/src/pages/Setting/OAuth2/OAuth2QuickStart.jsx (3)
origin
(9-9)applyRecommended
(17-37)createClient
(58-84)web/src/helpers/utils.jsx (2)
showSuccess
(153-155)showError
(118-147)
web/src/components/modals/oauth2/JWKSManagerModal.jsx (2)
web/src/pages/Setting/OAuth2/JWKSManager.jsx (7)
Typography
(6-6)loading
(9-9)keys
(10-10)load
(12-26)rotate
(28-43)del
(45-60)columns
(66-99)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
model/oauth_revoked_token.go (1)
common/redis.go (3)
RedisEnabled
(17-17)RedisSet
(64-70)RedisGet
(72-79)
middleware/auth.go (7)
setting/system_setting/oauth2.go (1)
GetOAuth2Settings
(49-51)src/oauth/server.go (1)
GetRSAPublicKey
(119-124)model/oauth_revoked_token.go (1)
IsTokenRevoked
(35-57)model/user_cache.go (1)
GetUserCache
(73-112)common/constants.go (1)
UserStatusEnabled
(174-174)common/gin.go (1)
SetContextKey
(53-55)constant/context_key.go (1)
ContextKeyUsingGroup
(46-46)
web/src/components/settings/OAuth2Setting.jsx (5)
web/src/components/modals/oauth2/JWKSManagerModal.jsx (2)
loading
(9-9)JWKSManagerModal
(8-148)web/src/components/modals/oauth2/OAuth2QuickStartModal.jsx (2)
loading
(10-10)OAuth2QuickStartModal
(7-230)web/src/pages/Setting/OAuth2/OAuth2ClientSettings.jsx (2)
loading
(46-46)OAuth2ClientSettings
(44-437)web/src/pages/Setting/OAuth2/OAuth2ServerSettings.jsx (2)
loading
(33-33)OAuth2ServerSettings
(31-404)web/src/components/modals/oauth2/OAuth2ToolsModal.jsx (3)
res
(19-19)data
(9-9)OAuth2ToolsModal
(26-324)
web/src/components/modals/oauth2/CreateOAuth2ClientModal.jsx (2)
web/src/components/modals/oauth2/EditOAuth2ClientModal.jsx (12)
Typography
(36-36)Select
(37-37)formApi
(40-40)loading
(41-41)redirectUris
(42-42)grantTypes
(43-43)allowedGrantTypes
(44-48)handleSubmit
(146-222)addRedirectUri
(225-227)removeRedirectUri
(230-232)updateRedirectUri
(235-239)handleGrantTypesChange
(242-252)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
controller/oauth_keys.go (2)
src/oauth/server.go (5)
RotateSigningKey
(135-156)ListSigningKeys
(202-210)DeleteSigningKey
(213-227)GenerateAndPersistKey
(159-193)ImportPEMKey
(268-318)logger/logger.go (1)
LogInfo
(55-57)
src/oauth/server.go (6)
setting/system_setting/oauth2.go (2)
GetOAuth2Settings
(49-51)OAuth2Settings
(5-21)common/sys_log.go (1)
SysLog
(10-13)common/utils.go (1)
GetUUID
(155-159)controller/oauth.go (1)
GetJWKS
(19-58)model/oauth_client.go (1)
GetOAuthClientByID
(142-146)model/user.go (1)
GetUserById
(287-299)
setting/system_setting/oauth2.go (1)
setting/config/config.go (1)
GlobalConfig
(18-18)
middleware/oauth_jwt.go (5)
setting/system_setting/oauth2.go (1)
GetOAuth2Settings
(49-51)src/oauth/server.go (1)
GetPublicKeyByKid
(127-132)model/oauth_client.go (1)
GetOAuthClientByID
(142-146)common/constants.go (1)
UserStatusEnabled
(174-174)model/oauth_revoked_token.go (1)
IsTokenRevoked
(35-57)
web/src/pages/Setting/OAuth2/OAuth2ServerSettings.jsx (1)
web/src/helpers/utils.jsx (4)
compareObjects
(294-311)showWarning
(149-151)showError
(118-147)showSuccess
(153-155)
router/api-router.go (7)
controller/oauth.go (8)
GetJWKS
(19-58)OAuthOIDCConfiguration
(156-190)OAuthServerInfo
(117-153)OAuthTokenEndpoint
(61-96)OAuthAuthorizeEndpoint
(99-114)OAuthIntrospect
(193-263)OAuthRevoke
(266-336)OAuthUserInfo
(339-375)src/oauth/server.go (1)
GetJWKS
(114-116)middleware/auth.go (2)
AdminAuth
(165-169)RootAuth
(171-175)middleware/oauth_jwt.go (3)
OAuthJWTAuth
(18-63)OptionalOAuthAuth
(233-247)RequireOAuthScopeIfPresent
(250-275)controller/oauth_keys.go (5)
RotateOAuthSigningKey
(26-36)ListOAuthSigningKeys
(39-42)DeleteOAuthSigningKey
(45-57)GenerateOAuthSigningKeyFile
(60-73)ImportOAuthSigningKey
(76-89)controller/oauth_client.go (7)
GetAllOAuthClients
(39-73)SearchOAuthClients
(76-104)GetOAuthClient
(107-133)CreateOAuthClient
(136-210)UpdateOAuthClient
(213-272)DeleteOAuthClient
(275-298)RegenerateOAuthClientSecret
(301-346)model/oauth_client.go (5)
GetAllOAuthClients
(149-153)SearchOAuthClients
(156-161)CreateOAuthClient
(164-166)UpdateOAuthClient
(169-171)DeleteOAuthClient
(174-176)
model/oauth_client.go (3)
common/sys_log.go (1)
SysLog
(10-13)model/main.go (1)
DB
(63-63)common/constants.go (1)
UserStatusEnabled
(174-174)
🪛 OSV Scanner (2.1.0)
go.mod
[HIGH] 25-25: github.com/golang-jwt/jwt 3.2.2+incompatible: Excessive memory allocation during header parsing in github.com/golang-jwt/jwt
(GO-2025-3553)
[HIGH] 25-25: github.com/golang-jwt/jwt 3.2.2+incompatible: jwt-go allows excessive memory allocation during header parsing
🪛 Gitleaks (8.27.2)
examples/oauth/oauth2_test_client.go
[high] 32-32: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
[high] 67-67: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🔇 Additional comments (26)
web/src/App.jsx (1)
202-209
: LGTM! OAuth consent route properly integratedThe new OAuth consent route is correctly placed alongside other OAuth routes and follows the established pattern with lazy loading and Suspense wrapper.
web/public/oauth-demo.html (2)
143-143
: Security: Cryptographically secure random string generationGood implementation using
crypto.getRandomValues
for generating cryptographically secure random strings for PKCE verifier, state, and nonce values.
157-157
: Good security practice with Basic auth implementationThe implementation correctly uses Basic authentication header when client_secret is provided, following OAuth2 specifications.
web/src/components/modals/oauth2/OAuth2QuickStartModal.jsx (1)
200-203
: Security: Sensitive client secret displayGood practice showing the client secret only once with a clear warning. The copyable text component makes it easy for users to save it securely.
main.go (1)
17-17
: LGTM: wiring OAuth package import.
No issues; import is scoped and clear.web/src/components/modals/oauth2/JWKSManagerModal.jsx (1)
12-37
: Extract shared JWKS API logic into a hook/helperDuplicate load/rotate/delete logic in web/src/components/modals/oauth2/JWKSManagerModal.jsx (lines 12–37, 39–86, 88–101) and the page component — extract into a shared API helper or useJWKS hook to centralize API calls, loading state, and error handling. I ran the provided ripgrep search for /api/oauth/keys* under router/controller — no matches found, so backend endpoints could not be verified. Confirm the server routes exist at /api/oauth/keys, /api/oauth/keys/rotate, /api/oauth/keys/import_pem, /api/oauth/keys/generate_file or provide their locations so I can re-run verification. Suggested refactor target: web/src/hooks/useJWKS.js.
model/main.go (1)
268-269
: No missing OAuth models — AutoMigrate already includes OAuthClientInspected model/oauth*.go: found model/oauth_client.go and model/oauth_revoked_token.go; only the OAuthClient struct is declared and it is present in model/main.go's AutoMigrate, so no additional OAuth model migrations are required.
go.mod (1)
21-22
: No conflict — libraries serve different roles; no action required.
- golang.org/x/oauth2 is only imported in examples/oauth/oauth2_test_client.go (client-side test).
- github.com/go-oauth2/oauth2/v4 and github.com/go-oauth2/gin-server are present in go.mod/go.sum as server-side deps (no direct imports found in repo sources).
- Distinct import paths prevent compile-time conflicts; optional: add a one-line comment in go.mod or README to clarify roles.
web/src/pages/Setting/OAuth2/OAuth2ClientSettings.jsx (3)
1-18
: Copyright header looks goodThe copyright notice is properly formatted with the AGPL license and commercial licensing information.
75-87
: Search filter should be case-insensitive and handle null values safelyThe current implementation has good null safety with optional chaining but could be improved slightly.
423-432
: Security: Ensure client secret visibility window is clearThe UI correctly emphasizes that the secret will only be shown once. Good security practice.
web/src/pages/Setting/OAuth2/OAuth2Tools.jsx (3)
7-15
: Crypto implementation looks secureThe SHA-256 implementation using Web Crypto API with base64url encoding is correct for PKCE.
17-24
: Good CSPRNG usage for random stringsUsing
crypto.getRandomValues()
is the correct approach for cryptographic randomness in the browser.
33-33
: Default redirect URI uses current originGood practice using
window.location.origin
for the default redirect URI, making it work across different deployments.web/src/components/modals/oauth2/EditOAuth2ClientModal.jsx (2)
76-143
: Complex initialization logic handles edge cases wellThe initialization properly handles different data formats (string vs array) and validates grant types against allowed types and client type constraints.
172-187
: Good redirect URI validationThe validation correctly enforces HTTPS for production and allows HTTP only for local development environments.
web/src/components/modals/oauth2/OAuth2ToolsModal.jsx (3)
144-157
: Good Basic Auth implementationThe postForm utility correctly implements both client_credentials in the body and HTTP Basic Auth for confidential clients.
179-190
: JWT decoding handles errors gracefullyThe ID token decoding properly handles malformed tokens and base64url encoding.
283-283
: Demo page present — no change required.
Found oauth-demo.html at web/public/oauth-demo.html (served at /oauth-demo.html) and examples/oauth/oauth-demo.html.middleware/oauth_jwt.go (1)
149-154
: Good security practice: Token revocation checkExcellent implementation of token revocation check using the
jti
claim. This provides a mechanism to invalidate tokens before expiration.src/oauth/server.go (2)
813-826
: Good security practice: PKCE enforcementExcellent implementation of PKCE (Proof Key for Code Exchange) requirement for authorization code flow, following OAuth 2.0 Security Best Practices.
283-285
: Security: PKCS#8 parsing falls through to PKCS#1 parser on errorWhen PKCS#8 parsing fails, the error is ignored and the code attempts PKCS#1 parsing with the same block, which is incorrect.
Apply this diff to fix the error handling:
if block.Type == "RSA PRIVATE KEY" { key, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return "", err + } } else { // try PKCS#8 priv, err2 := x509.ParsePKCS8PrivateKey(block.Bytes) if err2 != nil { return "", err2 } var ok bool key, ok = priv.(*rsa.PrivateKey) if !ok { return "", fmt.Errorf("not an RSA private key") } } -if err != nil { - return "", err -}Likely an incorrect or invalid review comment.
controller/oauth_client.go (4)
159-165
: LGTM! Good validation for authorization_code grant type.The validation correctly enforces that redirect URIs are required when using the authorization_code grant type, which is a critical OAuth2 security requirement.
189-191
: Setters verified — no action required.Found implementations in model/oauth_client.go — SetRedirectURIs (lines 47–54), SetGrantTypes (65–67), SetScopes (78–80).
168-172
: Resolved — randstr uses a cryptographically secure RNGgithub.com/thanhpk/randstr is documented to use crypto/rand; controller/oauth_client.go’s generateClientSecret returns randstr.String(32), so OAuth client secrets are generated securely.
311-318
: Decide whether regenerating a client secret should work for disabled OAuth clientscontroller/oauth_client.go (around lines 311–324) calls model.GetOAuthClientByID(id) and returns 404 if not found — I could not locate GetOAuthClientByID implementation in the provided results. Confirm whether GetOAuthClientByID filters by Status == 1 (enabled). If it does, regenerating secrets for disabled clients will be blocked; choose one of: (a) allow regeneration for disabled clients by adjusting the lookup or adding a dedicated query, or (b) keep the current behavior but return an explicit error explaining disabled clients cannot regenerate secrets.
|
||
// 更新客户端信息 | ||
client.Name = req.Name | ||
client.ClientType = req.ClientType | ||
client.RequirePKCE = req.RequirePKCE | ||
client.Status = req.Status | ||
client.Description = req.Description | ||
client.SetGrantTypes(req.GrantTypes) | ||
client.SetRedirectURIs(req.RedirectURIs) | ||
client.SetScopes(req.Scopes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing validation for authorization_code grant type requiring redirect URIs in update.
The update endpoint doesn't validate that redirect URIs are provided when authorization_code grant type is selected, unlike the create endpoint.
}
}
+// If includes authorization_code, redirect_uris must be provided
+if contains(req.GrantTypes, "authorization_code") && len(req.RedirectURIs) == 0 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "message": "授权码模式需要提供重定向URI",
+ })
+ return
+}
+
// 更新客户端信息
client.Name = req.Name
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In controller/oauth_client.go around lines 244 to 253, the update block assigns
grant types and redirect URIs but lacks the create-endpoint check that ensures
when the authorization_code grant is selected RedirectURIs is non-empty; add a
validation after client.SetGrantTypes(req.GrantTypes) (or immediately before
persisting) that if req.GrantTypes contains "authorization_code" and
req.RedirectURIs is empty/nil, return a 400 error (or the same validation error
response used by create) with a clear message requiring redirect URIs, otherwise
proceed to set redirect URIs and save.
err := model.DeleteOAuthClient(id) | ||
if err != nil { | ||
c.JSON(http.StatusInternalServerError, gin.H{ | ||
"success": false, | ||
"message": "删除客户端失败: " + err.Error(), | ||
}) | ||
return | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add validation to prevent deletion of OAuth clients that have active tokens.
Deleting an OAuth client without checking for active tokens could break existing integrations.
Before deleting, check if there are active tokens:
+// Check if client has active tokens
+client, err := model.GetOAuthClientByID(id)
+if err == nil && client.TokenCount > 0 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "message": "客户端存在活跃令牌,无法删除",
+ })
+ return
+}
+
err := model.DeleteOAuthClient(id)
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In controller/oauth_client.go around lines 285–292, the current code deletes the
OAuth client unconditionally; add a pre-delete validation that queries whether
the client has any active tokens (e.g., call a model function like
HasActiveTokens(clientID) or CountActiveTokens(clientID) that checks
non-expired/unrevoked tokens) and if any exist return a 400/409 JSON response
with success:false and a clear message preventing deletion; only call
model.DeleteOAuthClient(id) if the token check returns zero, and handle any
errors from both the token-check and delete calls with appropriate HTTP statuses
and error messages.
// GenerateOAuthSigningKeyFile generates a private key file and rotates current kid | ||
func GenerateOAuthSigningKeyFile(c *gin.Context) { | ||
var req genKeyFileRequest | ||
if err := c.ShouldBindJSON(&req); err != nil || req.Path == "" { | ||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "path required"}) | ||
return | ||
} | ||
kid, err := oauth.GenerateAndPersistKey(req.Path, req.Kid, req.Overwrite) | ||
if err != nil { | ||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) | ||
return | ||
} | ||
logger.LogInfo(c, "oauth signing key generated to file: "+req.Path+" kid="+kid) | ||
c.JSON(http.StatusOK, gin.H{"success": true, "kid": kid, "path": req.Path}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Path traversal vulnerability in file generation
The req.Path
parameter is used directly without sanitization, allowing potential path traversal attacks.
Add path validation to prevent directory traversal:
func GenerateOAuthSigningKeyFile(c *gin.Context) {
var req genKeyFileRequest
if err := c.ShouldBindJSON(&req); err != nil || req.Path == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "path required"})
return
}
+ // Validate path to prevent traversal attacks
+ if strings.Contains(req.Path, "..") || !filepath.IsAbs(req.Path) {
+ c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid path"})
+ return
+ }
kid, err := oauth.GenerateAndPersistKey(req.Path, req.Kid, req.Overwrite)
You'll also need to import "path/filepath"
and "strings"
.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// GenerateOAuthSigningKeyFile generates a private key file and rotates current kid | |
func GenerateOAuthSigningKeyFile(c *gin.Context) { | |
var req genKeyFileRequest | |
if err := c.ShouldBindJSON(&req); err != nil || req.Path == "" { | |
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "path required"}) | |
return | |
} | |
kid, err := oauth.GenerateAndPersistKey(req.Path, req.Kid, req.Overwrite) | |
if err != nil { | |
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) | |
return | |
} | |
logger.LogInfo(c, "oauth signing key generated to file: "+req.Path+" kid="+kid) | |
c.JSON(http.StatusOK, gin.H{"success": true, "kid": kid, "path": req.Path}) | |
} | |
// GenerateOAuthSigningKeyFile generates a private key file and rotates current kid | |
func GenerateOAuthSigningKeyFile(c *gin.Context) { | |
var req genKeyFileRequest | |
if err := c.ShouldBindJSON(&req); err != nil || req.Path == "" { | |
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "path required"}) | |
return | |
} | |
// Validate path to prevent traversal attacks | |
if strings.Contains(req.Path, "..") || !filepath.IsAbs(req.Path) { | |
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid path"}) | |
return | |
} | |
kid, err := oauth.GenerateAndPersistKey(req.Path, req.Kid, req.Overwrite) | |
if err != nil { | |
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) | |
return | |
} | |
logger.LogInfo(c, "oauth signing key generated to file: "+req.Path+" kid="+kid) | |
c.JSON(http.StatusOK, gin.H{"success": true, "kid": kid, "path": req.Path}) | |
} |
token = c.PostForm("token") | ||
if token == "" { | ||
c.JSON(http.StatusBadRequest, gin.H{ | ||
"error": "invalid_request", | ||
"error_description": "Missing token parameter", | ||
}) | ||
return | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove duplicate token validation code
Lines 293-300 duplicate the exact same validation logic from lines 284-291.
Apply this diff to remove the duplication:
- token = c.PostForm("token")
- if token == "" {
- c.JSON(http.StatusBadRequest, gin.H{
- "error": "invalid_request",
- "error_description": "Missing token parameter",
- })
- return
- }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
token = c.PostForm("token") | |
if token == "" { | |
c.JSON(http.StatusBadRequest, gin.H{ | |
"error": "invalid_request", | |
"error_description": "Missing token parameter", | |
}) | |
return | |
} |
🤖 Prompt for AI Agents
In controller/oauth.go around lines 293 to 300, there is a duplicated token
validation block that repeats the exact check already performed at lines
284–291; remove the duplicated block (lines 293–300) so the token is only
validated once and ensure control flow and returns remain unchanged after
deletion.
parsed, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { | ||
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { | ||
return nil, jwt.ErrTokenSignatureInvalid | ||
} | ||
pub := oauth.GetRSAPublicKey() | ||
if pub == nil { | ||
return nil, jwt.ErrTokenUnverifiable | ||
} | ||
return pub, nil | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: OAuthRevoke uses wrong public key resolution
The function uses GetRSAPublicKey()
which always returns the current key, but should use GetPublicKeyByKid()
to handle tokens signed with older keys.
Apply this diff to fix the key resolution:
parsed, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, jwt.ErrTokenSignatureInvalid
}
- pub := oauth.GetRSAPublicKey()
+ kid, _ := t.Header["kid"].(string)
+ pub := oauth.GetPublicKeyByKid(kid)
if pub == nil {
return nil, jwt.ErrTokenUnverifiable
}
return pub, nil
})
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
parsed, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { | |
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { | |
return nil, jwt.ErrTokenSignatureInvalid | |
} | |
pub := oauth.GetRSAPublicKey() | |
if pub == nil { | |
return nil, jwt.ErrTokenUnverifiable | |
} | |
return pub, nil | |
}) | |
parsed, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { | |
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { | |
return nil, jwt.ErrTokenSignatureInvalid | |
} | |
kid, _ := t.Header["kid"].(string) | |
pub := oauth.GetPublicKeyByKid(kid) | |
if pub == nil { | |
return nil, jwt.ErrTokenUnverifiable | |
} | |
return pub, nil | |
}) |
🤖 Prompt for AI Agents
In controller/oauth.go around lines 303 to 312, the jwt.Parse keyfunc currently
calls GetRSAPublicKey() which only returns the current key; change the key
resolution to extract the token's "kid" from t.Header (ensure it's a string),
call oauth.GetPublicKeyByKid(kid) and return that public key; if the kid is
missing or GetPublicKeyByKid returns nil, return jwt.ErrTokenUnverifiable; keep
the existing signing method check and return jwt.ErrTokenSignatureInvalid for
unsupported methods.
// EnsureInitialized lazily initializes signing keys and JWKS if OAuth2 is enabled but not yet ready | ||
func EnsureInitialized() error { | ||
settings := system_setting.GetOAuth2Settings() | ||
if !settings.Enabled { | ||
return nil | ||
} | ||
if len(signingKeys) > 0 && simpleJWKSSet != nil && currentKeyID != "" { | ||
return nil | ||
} | ||
// generate one key and JWKS on demand | ||
if settings.JWTKeyID == "" { | ||
settings.JWTKeyID = fmt.Sprintf("oauth2-key-%d", time.Now().Unix()) | ||
} | ||
currentKeyID = settings.JWTKeyID | ||
key, err := rsa.GenerateKey(rand.Reader, 2048) | ||
if err != nil { | ||
return err | ||
} | ||
signingKeys[currentKeyID] = key | ||
keyMeta[currentKeyID] = time.Now().Unix() | ||
jwks, err := createSimpleJWKS(key, currentKeyID) | ||
if err != nil { | ||
return err | ||
} | ||
simpleJWKSSet = jwks | ||
common.SysLog("OAuth2 lazy-initialized: signing key and JWKS ready") | ||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thread safety: EnsureInitialized() has race conditions
Multiple goroutines could pass the initial checks and attempt to initialize concurrently, leading to race conditions when modifying global maps.
Apply this diff to add proper synchronization:
+var initMutex sync.Mutex
+
func EnsureInitialized() error {
+ initMutex.Lock()
+ defer initMutex.Unlock()
+
settings := system_setting.GetOAuth2Settings()
if !settings.Enabled {
return nil
}
if len(signingKeys) > 0 && simpleJWKSSet != nil && currentKeyID != "" {
return nil
}
Committable suggestion skipped: line range outside the PR's diff.
func (m *memoryStore) Get(key string) (string, bool) { | ||
v, ok := m.m.Load(key) | ||
if !ok { | ||
return "", false | ||
} | ||
e := v.(memEntry) | ||
if e.exp > 0 && time.Now().Unix() > e.exp { | ||
m.m.Delete(key) | ||
return "", false | ||
} | ||
return e.val, true | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix potential panic with type assertion in memoryStore.Get().
The type assertion e := v.(memEntry)
on line 55 could panic if the value is not of type memEntry
.
Apply this diff to handle the type assertion safely:
func (m *memoryStore) Get(key string) (string, bool) {
v, ok := m.m.Load(key)
if !ok {
return "", false
}
- e := v.(memEntry)
+ e, ok := v.(memEntry)
+ if !ok {
+ return "", false
+ }
if e.exp > 0 && time.Now().Unix() > e.exp {
m.m.Delete(key)
return "", false
}
return e.val, true
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
func (m *memoryStore) Get(key string) (string, bool) { | |
v, ok := m.m.Load(key) | |
if !ok { | |
return "", false | |
} | |
e := v.(memEntry) | |
if e.exp > 0 && time.Now().Unix() > e.exp { | |
m.m.Delete(key) | |
return "", false | |
} | |
return e.val, true | |
} | |
func (m *memoryStore) Get(key string) (string, bool) { | |
v, ok := m.m.Load(key) | |
if !ok { | |
return "", false | |
} | |
e, ok := v.(memEntry) | |
if !ok { | |
return "", false | |
} | |
if e.exp > 0 && time.Now().Unix() > e.exp { | |
m.m.Delete(key) | |
return "", false | |
} | |
return e.val, true | |
} |
🤖 Prompt for AI Agents
In src/oauth/store.go around lines 50 to 61, the direct type assertion e :=
v.(memEntry) can panic if the stored value is not memEntry; change it to a safe
assertion (e, ok := v.(memEntry)) and if ok is false return "", false. After the
safe assertion, keep the existing expiry check and deletion logic, and return
e.val, true only when the assertion succeeds and the entry is not expired.
// writeOAuthRedirectError builds an error redirect to redirect_uri as RFC6749 | ||
func writeOAuthRedirectError(c *gin.Context, redirectURI, errCode, description, state string) { | ||
writeNoStore(c) | ||
q := "error=" + url.QueryEscape(errCode) | ||
if description != "" { | ||
q += "&error_description=" + url.QueryEscape(description) | ||
} | ||
if state != "" { | ||
q += "&state=" + url.QueryEscape(state) | ||
} | ||
sep := "?" | ||
if strings.Contains(redirectURI, "?") { | ||
sep = "&" | ||
} | ||
c.Redirect(http.StatusFound, redirectURI+sep+q) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Fix redirect error building: current logic appends query after URL fragments.
If redirect_uri contains a fragment (#), appending ?
/&
puts OAuth params inside the fragment, and the client won’t receive them. Parse and merge query via url.Parse()
and url.Values
, preserving fragments.
Apply this diff:
func writeOAuthRedirectError(c *gin.Context, redirectURI, errCode, description, state string) {
writeNoStore(c)
- q := "error=" + url.QueryEscape(errCode)
- if description != "" {
- q += "&error_description=" + url.QueryEscape(description)
- }
- if state != "" {
- q += "&state=" + url.QueryEscape(state)
- }
- sep := "?"
- if strings.Contains(redirectURI, "?") {
- sep = "&"
- }
- c.Redirect(http.StatusFound, redirectURI+sep+q)
+ u, err := url.Parse(redirectURI)
+ if err != nil {
+ // fallback: return JSON error when redirect_uri is invalid
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
+ "error": errCode,
+ "error_description": description,
+ "state": state,
+ })
+ return
+ }
+ vals := u.Query()
+ vals.Set("error", errCode)
+ if description != "" {
+ vals.Set("error_description", description)
+ }
+ if state != "" {
+ vals.Set("state", state)
+ }
+ u.RawQuery = vals.Encode()
+ c.Redirect(http.StatusFound, u.String())
}
Also ensure call sites only pass pre-validated redirect URIs to avoid open redirect vectors.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Repository root: $(pwd) ==="
echo "=== Searching for function definition and file contents ==="
rg -n --hidden --no-ignore-vcs -S "writeOAuthRedirectError" || true
echo
echo "=== Print src/oauth/util.go (if present) ==="
if [ -f src/oauth/util.go ]; then
sed -n '1,240p' src/oauth/util.go || true
else
echo "src/oauth/util.go not found"
fi
echo
echo "=== Searching for call sites ==="
rg -n --hidden --no-ignore-vcs -S -C3 "writeOAuthRedirectError\(|writeOAuthRedirect" || true
echo
echo "=== Searching for redirect URI validation helpers or checks ==="
rg -n --hidden --no-ignore-vcs -S -C3 "validate.*redirect|validateRedirect|isValidRedirect|checkRedirect|validateRedirectURI|validate_redirect" || true
echo
echo "=== Searching for common redirect_uri patterns ==="
rg -n --hidden --no-ignore-vcs -S -C3 "redirect_uri|redirectURI|redirectUri|RedirectURI|RedirectUri" || true
echo
echo "=== Listing go files under src/oauth for inspection ==="
ls -la src/oauth || true
Length of output: 71726
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '720,1000p' src/oauth/server.go || true
Length of output: 8341
Fix redirect error building: parse redirect_uri and merge query to preserve fragments.
If redirect_uri contains a fragment (#), concatenating "?"/"&" appends params into the fragment (clients won't receive them). Parse the URL and merge query params via url.Parse() / url.Values so the query is set before any fragment. Callers already validate redirect_uri in HandleAuthorizeRequest (client.ValidateRedirectURI / isLoopbackRedirectAllowed).
Apply this diff:
func writeOAuthRedirectError(c *gin.Context, redirectURI, errCode, description, state string) {
writeNoStore(c)
- q := "error=" + url.QueryEscape(errCode)
- if description != "" {
- q += "&error_description=" + url.QueryEscape(description)
- }
- if state != "" {
- q += "&state=" + url.QueryEscape(state)
- }
- sep := "?"
- if strings.Contains(redirectURI, "?") {
- sep = "&"
- }
- c.Redirect(http.StatusFound, redirectURI+sep+q)
+ u, err := url.Parse(redirectURI)
+ if err != nil {
+ // fallback: return JSON error when redirect_uri is invalid
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
+ "error": errCode,
+ "error_description": description,
+ "state": state,
+ })
+ return
+ }
+ vals := u.Query()
+ vals.Set("error", errCode)
+ if description != "" {
+ vals.Set("error_description", description)
+ }
+ if state != "" {
+ vals.Set("state", state)
+ }
+ u.RawQuery = vals.Encode()
+ c.Redirect(http.StatusFound, u.String())
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// writeOAuthRedirectError builds an error redirect to redirect_uri as RFC6749 | |
func writeOAuthRedirectError(c *gin.Context, redirectURI, errCode, description, state string) { | |
writeNoStore(c) | |
q := "error=" + url.QueryEscape(errCode) | |
if description != "" { | |
q += "&error_description=" + url.QueryEscape(description) | |
} | |
if state != "" { | |
q += "&state=" + url.QueryEscape(state) | |
} | |
sep := "?" | |
if strings.Contains(redirectURI, "?") { | |
sep = "&" | |
} | |
c.Redirect(http.StatusFound, redirectURI+sep+q) | |
} | |
// writeOAuthRedirectError builds an error redirect to redirect_uri as RFC6749 | |
func writeOAuthRedirectError(c *gin.Context, redirectURI, errCode, description, state string) { | |
writeNoStore(c) | |
u, err := url.Parse(redirectURI) | |
if err != nil { | |
// fallback: return JSON error when redirect_uri is invalid | |
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ | |
"error": errCode, | |
"error_description": description, | |
"state": state, | |
}) | |
return | |
} | |
vals := u.Query() | |
vals.Set("error", errCode) | |
if description != "" { | |
vals.Set("error_description", description) | |
} | |
if state != "" { | |
vals.Set("state", state) | |
} | |
u.RawQuery = vals.Encode() | |
c.Redirect(http.StatusFound, u.String()) | |
} |
🤖 Prompt for AI Agents
In src/oauth/util.go around lines 44 to 59, the redirect builder appends query
params by string concat which can place params into a fragment when redirect_uri
contains “#”; parse the redirectURI with url.Parse(), retrieve and merge
existing url.Query() with new error, error_description and state values using
url.Values, set u.RawQuery = merged.Encode(), preserve u.Fragment unchanged,
then use u.String() as the redirect target for c.Redirect; this ensures the
query is set before any fragment and existing query params are preserved.
const res = await API.get('/api/oauth/authorize', { | ||
params: { ...params, mode: 'prepare' }, | ||
// skip error toast, we'll handle gracefully | ||
skipErrorHandler: true, | ||
}); | ||
setInfo(res.data); | ||
setError(''); | ||
} catch (e) { | ||
// 401 login required or other error | ||
setError(e?.response?.data?.error || 'failed'); | ||
} finally { | ||
setLoading(false); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security: Validate OAuth parameters before processing
The consent page should validate critical OAuth parameters like redirect_uri
against a whitelist before making the prepare API call to prevent open redirect vulnerabilities.
Add parameter validation:
useEffect(() => {
(async () => {
setLoading(true);
+ // Validate redirect_uri format
+ if (params.redirect_uri && !isValidRedirectUri(params.redirect_uri)) {
+ setError('invalid_redirect_uri');
+ setLoading(false);
+ return;
+ }
try {
Committable suggestion skipped: line range outside the PR's diff.
web/src/pages/OAuth/Consent.jsx
Outdated
const onApprove = () => { | ||
const u = new URL(window.location.origin + '/api/oauth/authorize'); | ||
Object.entries(params).forEach(([k, v]) => u.searchParams.set(k, v)); | ||
u.searchParams.set('approve', '1'); | ||
window.location.href = u.toString(); | ||
}; | ||
const onDeny = () => { | ||
const u = new URL(window.location.origin + '/api/oauth/authorize'); | ||
Object.entries(params).forEach(([k, v]) => u.searchParams.set(k, v)); | ||
u.searchParams.set('deny', '1'); | ||
window.location.href = u.toString(); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security: Use POST with CSRF protection for approve/deny actions
The approve and deny actions use GET requests with query parameters, which could be vulnerable to CSRF attacks. OAuth2 best practices recommend using POST requests with proper CSRF tokens for state-changing operations.
Use POST requests with CSRF protection:
- const onApprove = () => {
- const u = new URL(window.location.origin + '/api/oauth/authorize');
- Object.entries(params).forEach(([k, v]) => u.searchParams.set(k, v));
- u.searchParams.set('approve', '1');
- window.location.href = u.toString();
- };
+ const onApprove = async () => {
+ const form = document.createElement('form');
+ form.method = 'POST';
+ form.action = '/api/oauth/authorize';
+ Object.entries({...params, approve: '1'}).forEach(([k, v]) => {
+ const input = document.createElement('input');
+ input.type = 'hidden';
+ input.name = k;
+ input.value = v;
+ form.appendChild(input);
+ });
+ document.body.appendChild(form);
+ form.submit();
+ };
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In web/src/pages/OAuth/Consent.jsx around lines 60-71 the approve/deny handlers
perform GET navigations with query params which is a CSRF risk; change them to
send POST requests to /api/oauth/authorize including all original params plus an
approve or deny flag in the request body, and include a CSRF token (read from a
secure cookie or meta tag) in a header or body field. Use fetch with method
'POST', credentials: 'include', appropriate Content-Type (application/json or
application/x-www-form-urlencoded), and on success perform a client-side
redirect to the returned location or handle errors; ensure the server endpoint
accepts POST and validates the CSRF token and original state parameter.
…d UX - Redesign OAuth consent page layout with centered card design - Implement GitHub-style authorization flow presentation - Add application popover with detailed information on hover - Replace generic icons with scope-specific icons (email, profile, admin, etc.) - Integrate i18n support for all hardcoded strings - Optimize permission display with encapsulated ScopeItem component - Improve visual hierarchy with Semi UI Divider components - Unify avatar sizes and implement dynamic color generation - Move action buttons and redirect info to card footer - Add separate meta information card for technical details - Remove redundant color styles to rely on Semi UI theming - Enhance user account section with clearer GitHub-style messaging - Replace dot separators with Lucide icons for better visual consistency - Add site logo with fallback mechanism for branding - Implement responsive design with Tailwind CSS utilities This redesign significantly improves the OAuth consent experience by following modern UI patterns and providing clearer information hierarchy for users.
There was a problem hiding this 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
🧹 Nitpick comments (21)
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (5)
33-33
: Encode KID in path to avoid malformed requestsKIDs can contain non‑URL‑safe chars. Encode before interpolating.
- const res = await API.delete(`/api/oauth/keys/${kid}`); + const res = await API.delete(`/api/oauth/keys/${encodeURIComponent(kid)}`);
2-2
: Remove unused importsInput/TextArea aren’t used (Form.* is). Clean up to reduce bundle size.
-import { Modal, Table, Button, Space, Tag, Typography, Popconfirm, Toast, Form, TextArea, Divider, Input } from '@douyinfe/semi-ui'; +import { Modal, Table, Button, Space, Tag, Typography, Popconfirm, Toast, Form, Divider } from '@douyinfe/semi-ui';
104-111
: Prevent accidental close during key opsThis modal performs destructive/privileged actions. Disable mask click to close.
<Modal visible={visible} title='JWKS 管理' onCancel={onClose} footer={null} width={820} style={{ top: 48 }} + maskClosable={false} >
34-36
: Unify notifications through helpersMixing Toast.* and showSuccess/showError creates inconsistent UX and bypasses Axios 401 handling in showError. Prefer helpers everywhere for success/error; reserve Toast.info/warn for neutral hints.
Also applies to: 60-67, 79-85, 25-27
71-86
: Guard server-side file generation paths (operational)Generating private keys to arbitrary paths is risky. Ensure backend restricts to an allowlist directory and enforces secure file perms (0600) before write. Consider surfacing server-enforced base dir and preventing traversal in UI.
web/src/components/settings/oauth2/modals/OAuth2QuickStartModal.jsx (1)
67-69
: Batch option writes to reduce latency and partial stateUse Promise.all to apply options concurrently; failure still throws and is caught.
- for (const op of ops) { - await API.put('/api/option/', op); - } + await Promise.all(ops.map(op => API.put('/api/option/', op)));web/src/components/settings/oauth2/modals/OAuth2ToolsModal.jsx (4)
185-187
: Base64url decode needs paddingatob may fail without proper padding. Add padding before decode.
- const json = JSON.parse(atob(parts[1].replace(/-/g,'+').replace(/_/g,'/'))); + const b64 = parts[1].replace(/-/g,'+').replace(/_/g,'/'); + const padded = b64 + '==='.slice((b64.length + 3) % 4); + const json = JSON.parse(atob(padded));
161-169
: Avoid sending client_id when using Basic authSome servers reject body client_id when Authorization header is present. Include client_id only for public clients.
- const basic = values.client_secret ? { id: values.client_id, secret: values.client_secret } : undefined; - const data = await postForm(values.token_endpoint, { - grant_type: 'authorization_code', - code: code.trim(), - client_id: values.client_id, - redirect_uri: values.redirect_uri, - code_verifier: values.code_verifier, - }, basic); + const basic = values.client_secret ? { id: values.client_id, secret: values.client_secret } : undefined; + const body = { + grant_type: 'authorization_code', + code: code.trim(), + redirect_uri: values.redirect_uri, + code_verifier: values.code_verifier, + }; + if (!basic) body.client_id = values.client_id; + const data = await postForm(values.token_endpoint, body, basic);
206-211
: Same for refresh_token: omit client_id with BasicAligns with RFC 6749 section 2.3.1.
- const basic = values.client_secret ? { id: values.client_id, secret: values.client_secret } : undefined; - const data = await postForm(values.token_endpoint, { - grant_type: 'refresh_token', - refresh_token: refreshToken, - client_id: values.client_id, - }, basic); + const basic = values.client_secret ? { id: values.client_id, secret: values.client_secret } : undefined; + const body = { + grant_type: 'refresh_token', + refresh_token: refreshToken, + }; + if (!basic) body.client_id = values.client_id; + const data = await postForm(values.token_endpoint, body, basic);
100-119
: Support both /.well-known and /api/.well-known for OIDC discovery
Server exposes discovery at /api/.well-known/openid-configuration (router/api-router.go:37); frontend currently fetches issuer + '/api/.well-known/openid-configuration' (web/src/components/settings/oauth2/modals/OAuth2ToolsModal.jsx lines 100–119) — try issuer+'/.well-known/openid-configuration' first and fall back to issuer+'/api/.well-known/openid-configuration'.web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (2)
103-104
: URL‑encode client ID in API pathsAvoid path issues if IDs contain non‑URL‑safe chars.
- const res = await API.delete(`/api/oauth_clients/${client.id}`); + const res = await API.delete(`/api/oauth_clients/${encodeURIComponent(client.id)}`);
118-119
: URL‑encode client ID in secret regeneration pathSame rationale as above.
- const res = await API.post(`/api/oauth_clients/${client.id}/regenerate_secret`); + const res = await API.post(`/api/oauth_clients/${encodeURIComponent(client.id)}/regenerate_secret`);web/src/components/settings/oauth2/modals/EditOAuth2ClientModal.jsx (4)
74-75
: Re-filter grant types after allowed types load/change.
grantTypes
is initialized/filtered only when the modal opens. IfallowedGrantTypes
arrives later from the API, stale/now‑disallowed types can remain selected. Re-filter whenallowedGrantTypes
orclient.client_type
changes.Apply this diff to add a corrective effect:
}, []); + // 允许的授权类型或客户端类型变化时,重新过滤已选授权类型 + useEffect(() => { + setGrantTypes(prev => { + let next = (prev || []).filter(g => allowedGrantTypes.includes(g)); + if (client?.client_type === 'public') next = next.filter(g => g !== 'client_credentials'); + return next; + }); + }, [allowedGrantTypes, client?.client_type]); + // 初始化表单数据
134-134
: Avoid desync: set form value to the filtered grant types.
formValues.grant_types
usesparsedGrantTypes
while the Select is controlled bygrantTypes
(already filtered). Set both to the same value to prevent confusion.Apply this diff:
- grant_types: parsedGrantTypes, + grant_types: finalGrantTypes,
171-187
: Deduplicate redirect URI validation across Create/Edit.
isValidRedirectUri
is duplicated here and in CreateOAuth2ClientModal. Extract a shared helper (e.g., web/src/components/settings/oauth2/lib/validators.js) and import it in both to keep behavior consistent.If you want, I can prepare the small helper module + imports.
154-170
: i18n consistency: wrap user‑facing strings with t().This modal hardcodes Chinese strings while adjacent components use i18n. Wrap messages (errors/toasts/labels) with
t()
for consistency and localization.I can generate a patch that adds
useTranslation
and replaces the strings you touch here.web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (2)
88-125
: Use Promise.allSettled and handle partial failures correctly.
Promise.all(...).then(... res.includes(undefined) ...)
won't detect partial failures; axios rejects on error. UseallSettled
, surface partial failures, and simplify the flow.Apply this diff:
- function onSubmit() { + async function onSubmit() { const updateArray = compareObjects(inputs, inputsRow); if (!updateArray.length) return showWarning(t('你似乎并没有修改什么')); const requestQueue = updateArray.map((item) => { let value = ''; if (typeof inputs[item.key] === 'boolean') { value = String(inputs[item.key]); } else if (Array.isArray(inputs[item.key])) { value = JSON.stringify(inputs[item.key]); } else { value = inputs[item.key]; } return API.put('/api/option/', { key: item.key, value, }); }); - setLoading(true); - Promise.all(requestQueue) - .then((res) => { - if (requestQueue.length === 1) { - if (res.includes(undefined)) return; - } else if (requestQueue.length > 1) { - if (res.includes(undefined)) - return showError(t('部分保存失败,请重试')); - } - showSuccess(t('保存成功')); - if (props && props.refresh) { - props.refresh(); - } - }) - .catch(() => { - showError(t('保存失败,请重试')); - }) - .finally(() => { - setLoading(false); - }); + setLoading(true); + try { + const results = await Promise.allSettled(requestQueue); + const failures = results.filter(r => r.status === 'rejected'); + if (failures.length) { + return showError(t('部分保存失败,请重试')); + } + showSuccess(t('保存成功')); + if (props?.refresh) props.refresh(); + // 同步基线,避免后续“未修改”的误报 + setInputsRow(structuredClone(inputs)); + } catch { + showError(t('保存失败,请重试')); + } finally { + setLoading(false); + } }
163-166
: structuredClone fallback (compat nit).If you need to support older browsers/environments, prefer a small fallback wrapper.
Example:
const deepClone = (obj) => (typeof structuredClone === 'function' ? structuredClone(obj) : JSON.parse(JSON.stringify(obj)));Then use
deepClone({ ...inputs, ...currentInputs })
anddeepClone(inputs)
.web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx (3)
284-290
: Inline @media in style is ignored.React inline styles don't support
@media
keys. This block is a no‑op. Use CSS, a responsive container, or keepwidth: '90vw', maxWidth: '800px'
.Apply this diff to remove the dead style:
width="90vw" style={{ top: 20, maxWidth: '800px', - '@media (min-width: 768px)': { - width: '600px' - } }}
160-164
: Pinpoint which redirect URI is invalid.Surface the exact index to speed up fixing.
Apply this diff:
- const allValid = validRedirectUris.every(isValidRedirectUri); - if (!allValid) { - showError(t('重定向URI格式不合法:仅支持https,或本地开发使用http')); + const invalidIdx = validRedirectUris.findIndex(u => !isValidRedirectUri(u)); + if (invalidIdx !== -1) { + showError(t('重定向URI格式不合法(第{{n}}项):仅支持https,或本地开发使用http', { n: invalidIdx + 1 })); return; }
112-129
: Deduplicate redirect URI validation with Edit modal.
isValidRedirectUri
is identical to the one in Edit. Extract to a shared helper (e.g.,web/src/components/settings/oauth2/lib/validators.js
) and import here and in Edit to keep logic aligned.I can add the small helper and update both imports if you want.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
web/src/components/settings/OAuth2Setting.jsx
(1 hunks)web/src/components/settings/oauth2/OAuth2ClientSettings.jsx
(1 hunks)web/src/components/settings/oauth2/OAuth2ServerSettings.jsx
(1 hunks)web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx
(1 hunks)web/src/components/settings/oauth2/modals/EditOAuth2ClientModal.jsx
(1 hunks)web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx
(1 hunks)web/src/components/settings/oauth2/modals/OAuth2QuickStartModal.jsx
(1 hunks)web/src/components/settings/oauth2/modals/OAuth2ToolsModal.jsx
(1 hunks)web/src/pages/OAuth/Consent.jsx
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- web/src/pages/OAuth/Consent.jsx
🧰 Additional context used
🧬 Code graph analysis (7)
web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (4)
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (3)
Typography
(6-6)loading
(9-9)JWKSManagerModal
(8-148)web/src/components/settings/oauth2/modals/OAuth2QuickStartModal.jsx (3)
Typography
(5-5)loading
(10-10)OAuth2QuickStartModal
(7-230)web/src/components/settings/oauth2/modals/OAuth2ToolsModal.jsx (3)
Typography
(5-5)res
(19-19)OAuth2ToolsModal
(26-324)web/src/helpers/utils.jsx (4)
compareObjects
(294-311)showWarning
(149-151)showError
(118-147)showSuccess
(153-155)
web/src/components/settings/oauth2/modals/EditOAuth2ClientModal.jsx (2)
web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx (9)
redirectUris
(45-45)grantTypes
(47-47)allowedGrantTypes
(48-52)handleSubmit
(132-226)isValidRedirectUri
(113-129)addRedirectUri
(245-247)removeRedirectUri
(250-252)updateRedirectUri
(255-259)handleGrantTypesChange
(262-272)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (3)
web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx (3)
loading
(44-44)grantTypes
(47-47)CreateOAuth2ClientModal
(41-480)web/src/components/settings/oauth2/modals/EditOAuth2ClientModal.jsx (3)
loading
(41-41)grantTypes
(43-43)EditOAuth2ClientModal
(39-415)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx (1)
web/src/components/settings/oauth2/modals/EditOAuth2ClientModal.jsx (12)
Typography
(36-36)Select
(37-37)formApi
(40-40)loading
(41-41)redirectUris
(42-42)grantTypes
(43-43)allowedGrantTypes
(44-48)handleSubmit
(146-222)addRedirectUri
(225-227)removeRedirectUri
(230-232)updateRedirectUri
(235-239)handleGrantTypesChange
(242-252)
web/src/components/settings/oauth2/modals/OAuth2QuickStartModal.jsx (1)
web/src/helpers/utils.jsx (2)
showSuccess
(153-155)showError
(118-147)
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (1)
web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
web/src/components/settings/OAuth2Setting.jsx (3)
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (2)
loading
(57-57)OAuth2ClientSettings
(55-504)web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (2)
loading
(59-59)OAuth2ServerSettings
(57-536)web/src/helpers/utils.jsx (1)
showError
(118-147)
🔇 Additional comments (4)
web/src/components/settings/OAuth2Setting.jsx (1)
32-51
: LGTM — options loading flow is solidClear state handling, mapping to key/value, and error surfacing via helpers.
web/src/components/settings/oauth2/modals/OAuth2QuickStartModal.jsx (1)
83-90
: Response shape confirmed — use res.data.kidEndpoint returns a top-level "kid" (controller/oauth_keys.go:35 — c.JSON(...{"success": true, "kid": kid})), so res.data.kid is correct; no change needed.
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (1)
88-91
: No action required — backend returns created_at (unix seconds)src/oauth/server.go defines KeyInfo with CreatedAt int64
json:"created_at"
and a comment mapping keyMeta -> created_at (unix); controller/oauth_keys.go returns oauth.ListSigningKeys(), so the frontend's created_at accessor is correct.web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (1)
269-274
: Confirmed timestamp unit — backend uses seconds; keep multiplication by 1000common/utils.go:GetTimestamp() returns time.Now().Unix() and model/oauth_client.go:BeforeCreate sets CreatedTime = time.Now().Unix(), so created_time is seconds since epoch and new Date(time * 1000).toLocaleString() is correct.
if (res?.data?.success) { | ||
setCreatedClient({ id: res.data.client_id, secret: res.data.client_secret }); | ||
showSuccess('客户端已创建'); | ||
setStep(3); | ||
} else { | ||
showError(res?.data?.message || '创建失败'); | ||
} | ||
onDone && onDone(); | ||
} catch (e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onDone fires even on failure
onDone is called unconditionally; it should only run after successful client creation.
- if (res?.data?.success) {
+ if (res?.data?.success) {
setCreatedClient({ id: res.data.client_id, secret: res.data.client_secret });
showSuccess('客户端已创建');
setStep(3);
- } else {
+ onDone && onDone();
+ } else {
showError(res?.data?.message || '创建失败');
}
- onDone && onDone();
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
if (res?.data?.success) { | |
setCreatedClient({ id: res.data.client_id, secret: res.data.client_secret }); | |
showSuccess('客户端已创建'); | |
setStep(3); | |
} else { | |
showError(res?.data?.message || '创建失败'); | |
} | |
onDone && onDone(); | |
} catch (e) { | |
if (res?.data?.success) { | |
setCreatedClient({ id: res.data.client_id, secret: res.data.client_secret }); | |
showSuccess('客户端已创建'); | |
setStep(3); | |
onDone && onDone(); | |
} else { | |
showError(res?.data?.message || '创建失败'); | |
} | |
} catch (e) { |
🤖 Prompt for AI Agents
In web/src/components/settings/oauth2/modals/OAuth2QuickStartModal.jsx around
lines 111 to 119, onDone() is invoked unconditionally after the network
response, causing it to fire even when creation failed; only call onDone after a
successful creation—move the onDone && onDone() call inside the success branch
(after setCreatedClient/setStep) or guard it behind the same condition that
shows success so it is not executed on failure or in the catch block.
- Add responsive layout for user info section with flex-col on mobile - Optimize button layout: vertical stack on mobile, horizontal on desktop - Implement mobile-first approach with sm: breakpoints throughout - Adjust container width: max-w-sm on mobile, max-w-lg on desktop - Enhance touch targets with larger buttons (size='large') on mobile - Improve content hierarchy with primary action button on top for mobile - Add responsive padding and spacing: px-3 sm:px-4, py-6 sm:py-8 - Optimize text sizing: text-sm sm:text-base for better mobile readability - Implement responsive gaps: gap-4 sm:gap-6 for icon spacing - Add break-all class for long URL text wrapping - Adjust meta info card spacing and dot separator sizing - Ensure consistent responsive padding across all content sections This update significantly improves the mobile user experience while maintaining the desktop layout, following mobile-first design principles with Tailwind CSS responsive utilities.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (4)
web/src/pages/OAuth/Consent.jsx (4)
85-103
: [Duplicate] Don't whitelist OAuth params; pass through all original query params.This is still using parameter whitelisting which drops extension params like
response_mode
,prompt
,max_age
, etc., potentially breaking OIDC/OAuth clients. The defaultresponse_type
injection may also mask client configuration errors.
125-130
: [Duplicate] Preserve unknown query params and use POST for approve/deny actions.The
handleAction
function still uses GET requests with query parameters, which creates CSRF vulnerabilities. Additionally, it rebuilds parameters from the filtered object, risking loss of client-sent parameters.
109-123
: [Duplicate] Add parameter validation before API calls.The consent page should validate critical OAuth parameters like
redirect_uri
against expected patterns before making the prepare API call to prevent potential security issues.
270-274
: Insecure account switching implementation.The "Switch account" functionality constructs URLs using
window.location.search
directly without proper validation. This could lead to open redirect vulnerabilities.
Based on the search results, I can see that OAuth2 consent pages require proper validation of redirect URIs and other security measures. Let me continue with the review:Apply this diff to secure the account switching functionality:
- onClick={() => { - const u = new URL(window.location.origin + '/login'); - u.searchParams.set('next', '/oauth/consent' + window.location.search); - window.location.href = u.toString(); - }} + onClick={() => { + const currentParams = new URLSearchParams(window.location.search); + const redirectUri = currentParams.get('redirect_uri'); + // Validate redirect_uri if present + if (redirectUri && !isValidRedirectUri(redirectUri)) { + console.error('Invalid redirect_uri detected'); + return; + } + const u = new URL(window.location.origin + '/login'); + u.searchParams.set('next', '/oauth/consent' + window.location.search); + window.location.href = u.toString(); + }}You'll need to implement
isValidRedirectUri
function to validate against allowed patterns.
🧹 Nitpick comments (2)
web/src/pages/OAuth/Consent.jsx (2)
174-177
: Potential XSS vulnerability in redirect URI display.The redirect URI from server response is displayed directly without sanitization. This could lead to XSS if the server returns malicious content.
Apply this diff to sanitize the redirect URI display:
- <Text type='tertiary' size='small' className='block'> - {info?.redirect_uri?.length > 60 ? info.redirect_uri.slice(0, 60) + '...' : info?.redirect_uri} - </Text> + <Text type='tertiary' size='small' className='block'> + {info?.redirect_uri?.length > 60 + ? `${info.redirect_uri.slice(0, 60)}...` + : info?.redirect_uri || ''} + </Text>Consider using a proper text sanitization library for production.
227-230
: Unsafe error handling for logo loading.The logo error handling directly manipulates DOM styles, which can be error-prone and may not work across all scenarios.
Consider using React state to handle logo loading errors:
+ const [logoError, setLogoError] = useState(false); + <img src={getLogo()} alt='Site Logo' - className='w-full h-full object-cover' - onError={(e) => { - e.target.style.display = 'none'; - e.target.nextSibling.style.display = 'flex'; - }} + className={`w-full h-full object-cover ${logoError ? 'hidden' : ''}`} + onError={() => setLogoError(true)} /> - <div - className='w-full h-full rounded-full flex items-center justify-center' - style={{ - backgroundColor: stringToColor(window.location.hostname || 'S'), - display: 'none' - }} - > + {logoError && ( + <div className='w-full h-full rounded-full flex items-center justify-center' + style={{ backgroundColor: stringToColor(window.location.hostname || 'S') }}> <Text className='font-bold text-lg'> {window.location.hostname.charAt(0).toUpperCase()} </Text> - </div> + </div> + )}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
web/src/i18n/locales/en.json
(1 hunks)web/src/pages/OAuth/Consent.jsx
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
web/src/pages/OAuth/Consent.jsx (2)
web/src/helpers/render.jsx (1)
stringToColor
(561-568)web/src/helpers/utils.jsx (1)
getLogo
(55-59)
🔇 Additional comments (3)
web/src/pages/OAuth/Consent.jsx (2)
35-53
: LGTM! Well-implemented icon mapping.The
getScopeIcon
function provides a clean mapping from OAuth scopes to appropriate lucide-react icons with a sensible fallback.
55-76
: LGTM! Clean scope item component.The
ScopeItem
component is well-structured with proper accessibility considerations and responsive design.web/src/i18n/locales/en.json (1)
522-535
: LGTM! Comprehensive OAuth translation coverage.The translation keys provide comprehensive coverage for the OAuth consent flow, including all necessary UI labels and messages. The English translations are clear and user-friendly.
…t modal components - **UI Restructuring:** - Separate client info into individual table columns (name, ID, description) - Replace icon-only action buttons with text labels for better UX - Adjust table scroll width from 1000px to 1200px for new column layout - Remove unnecessary Tooltip wrappers and Lucide icons (Edit, Key, Trash2) - **Component Architecture:** - Extract all modal dialogs into separate reusable components: * SecretDisplayModal.jsx - for displaying regenerated client secrets * ServerInfoModal.jsx - for OAuth2 server configuration info * JWKSInfoModal.jsx - for JWKS key set information - Simplify main component by removing ~60 lines of inline modal code - Implement proper state management for each modal component - **Code Quality:** - Remove unused imports and clean up component dependencies - Consolidate modal logic into dedicated components with error handling - Improve code maintainability and reusability across the application - **Internationalization:** - Add English translation for '客户端名称': 'Client Name' - Remove duplicate translation keys to fix linter warnings - Ensure all new components support full i18n functionality - **User Experience:** - Enhance table readability with dedicated columns for each data type - Maintain copyable client ID functionality in separate column - Improve action button accessibility with clear text labels - Add loading states and proper error handling in modal components This refactoring improves code organization, enhances user experience, and follows React best practices for component composition and separation of concerns.
There was a problem hiding this 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
🧹 Nitpick comments (2)
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (1)
44-55
: Consider adding explicit success feedback for load operations.Currently, successful key loading doesn't provide user feedback. While this is common for read operations, users might appreciate confirmation when manually refreshing the key list.
const load = async () => { setLoading(true); try { const res = await API.get('/api/oauth/keys'); - if (res?.data?.success) setKeys(res.data.data || []); + if (res?.data?.success) { + setKeys(res.data.data || []); + // Optional: showSuccess(t('密钥列表已更新')); + } else showError(res?.data?.message || t('获取密钥列表失败'));web/src/i18n/locales/en.json (1)
831-831
: Minor translation inconsistency detected.Line 831 has "删除失败-oauth2clients" which appears to be a debug artifact or inconsistent with the surrounding translation patterns.
- "删除失败-oauth2clients": "Delete failed", + "删除失败": "Delete failed",
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (20)
web/public/oauth-demo.html
(1 hunks)web/src/components/common/modals/TwoFactorAuthModal.jsx
(1 hunks)web/src/components/settings/OAuth2Setting.jsx
(1 hunks)web/src/components/settings/oauth2/OAuth2ClientSettings.jsx
(1 hunks)web/src/components/settings/oauth2/OAuth2ServerSettings.jsx
(1 hunks)web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx
(1 hunks)web/src/components/settings/oauth2/modals/EditOAuth2ClientModal.jsx
(1 hunks)web/src/components/settings/oauth2/modals/JWKSInfoModal.jsx
(1 hunks)web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx
(1 hunks)web/src/components/settings/oauth2/modals/SecretDisplayModal.jsx
(1 hunks)web/src/components/settings/oauth2/modals/ServerInfoModal.jsx
(1 hunks)web/src/components/table/channels/modals/EditChannelModal.jsx
(4 hunks)web/src/components/topup/RechargeCard.jsx
(7 hunks)web/src/components/topup/index.jsx
(6 hunks)web/src/components/topup/modals/PaymentConfirmModal.jsx
(1 hunks)web/src/i18n/locales/en.json
(4 hunks)web/src/pages/OAuth/Consent.jsx
(1 hunks)web/src/pages/Setting/Operation/SettingsGeneral.jsx
(1 hunks)web/src/pages/Setting/Operation/SettingsMonitoring.jsx
(1 hunks)web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx
(5 hunks)
✅ Files skipped from review due to trivial changes (8)
- web/src/components/common/modals/TwoFactorAuthModal.jsx
- web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx
- web/src/components/table/channels/modals/EditChannelModal.jsx
- web/src/components/topup/RechargeCard.jsx
- web/src/pages/Setting/Operation/SettingsGeneral.jsx
- web/src/components/topup/index.jsx
- web/src/components/topup/modals/PaymentConfirmModal.jsx
- web/src/pages/Setting/Operation/SettingsMonitoring.jsx
🚧 Files skipped from review as they are similar to previous changes (5)
- web/src/pages/OAuth/Consent.jsx
- web/src/components/settings/oauth2/OAuth2ClientSettings.jsx
- web/src/components/settings/oauth2/modals/EditOAuth2ClientModal.jsx
- web/src/components/settings/OAuth2Setting.jsx
- web/public/oauth-demo.html
🧰 Additional context used
🧬 Code graph analysis (5)
web/src/components/settings/oauth2/modals/JWKSInfoModal.jsx (1)
web/src/helpers/utils.jsx (1)
showError
(118-147)
web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (2)
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (3)
useTranslation
(40-40)loading
(41-41)JWKSManagerModal
(39-313)web/src/helpers/utils.jsx (4)
compareObjects
(294-311)showWarning
(149-151)showError
(118-147)showSuccess
(153-155)
web/src/components/settings/oauth2/modals/ServerInfoModal.jsx (1)
web/src/helpers/utils.jsx (1)
showError
(118-147)
web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx (2)
web/src/components/settings/oauth2/modals/EditOAuth2ClientModal.jsx (13)
Typography
(36-36)Select
(37-37)useTranslation
(40-40)formApi
(41-41)loading
(42-42)redirectUris
(43-43)grantTypes
(44-44)allowedGrantTypes
(45-49)handleSubmit
(152-235)addRedirectUri
(238-240)removeRedirectUri
(243-245)updateRedirectUri
(248-252)handleGrantTypesChange
(255-268)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (1)
web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
🔇 Additional comments (19)
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (3)
39-313
: LGTM! This is a comprehensive JWKS management interface with strong authentication UX.The implementation correctly handles JWT key lifecycle management with proper state management, API error handling, and user-friendly modals. The integration with OAuth2 server endpoints appears well-designed.
105-129
: Strong PEM import validation with good UX patterns.The PEM import flow correctly validates input, provides clear feedback, and resets form state appropriately. The custom KID support is well-implemented.
135-154
: Excellent file generation workflow with proper validation.The file generation feature handles path validation, optional custom KID, and provides clear success feedback with the actual file path used.
web/src/components/settings/oauth2/modals/JWKSInfoModal.jsx (1)
27-85
: Clean and focused JWKS inspection modal.Simple, effective implementation for viewing JWKS data with proper loading states and error handling. The JSON pretty-printing makes the data easily readable for developers.
web/src/components/settings/oauth2/modals/SecretDisplayModal.jsx (1)
26-73
: Excellent one-time secret display UX with security awareness.The implementation correctly emphasizes the one-time nature of the secret display with clear warnings and copy-to-clipboard functionality. The styling and messaging effectively communicate the importance of saving the secret.
web/src/components/settings/oauth2/modals/ServerInfoModal.jsx (1)
27-85
: Consistent modal pattern for server information display.Well-implemented server info modal that follows the same pattern as JWKSInfoModal, providing a unified experience for inspecting OAuth2 server configuration.
web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (5)
45-116
: Robust settings management with batch updates and proper validation.The component handles OAuth2 server configuration with appropriate state management, change detection, and batch API updates. The form validation and error handling patterns are well-implemented.
118-141
: Smart health check implementation with appropriate user feedback.The OAuth2 connection testing is well-designed - it runs silently on configuration changes but allows explicit user-triggered tests with feedback. The health check validates essential endpoints correctly.
143-176
: Comprehensive options parsing with fallback values.The options initialization logic properly handles different data types (boolean, number, JSON arrays) with appropriate fallbacks. The JSON parsing for grant types includes error recovery.
182-206
: Intelligent key readiness detection tied to OAuth2 enablement.The key loading logic appropriately responds to OAuth2 enablement state changes, avoiding unnecessary API calls when disabled and providing clear status indicators.
268-295
: Effective JWKS readiness warning with actionable guidance.The banner warning for missing signing keys provides clear context and direct access to key management, helping administrators understand the importance of JWKS setup.
web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx (7)
79-103
: Smart default grant type computation based on client type.The logic for computing appropriate default grant types based on client type and allowed types is well-designed, properly handling the security constraints for public vs confidential clients.
133-171
: Comprehensive client validation with security-aware checks.The validation logic properly enforces OAuth2 security constraints, including preventing public clients from using client_credentials and requiring redirect URIs for authorization_code flow.
187-228
: Excellent client creation success flow with secure secret handling.The success handling appropriately displays both client_id and client_secret (when applicable) with clear warnings about the one-time nature of secret display. The modal provides good UX for credential management.
273-283
: Smart form interaction with automatic redirect URI management.The grant type change handler intelligently manages redirect URIs, adding one when authorization_code is selected and enforcing client type constraints automatically.
336-395
: Excellent client type selection UI with clear guidance.The visual client type selection blocks provide clear differentiation between confidential and public clients with helpful descriptions. The styling effectively guides user choice.
464-522
: Dynamic redirect URI management with good UX.The conditional rendering and dynamic URI management provide a clean interface for configuring authorization_code flows while keeping the form uncluttered for other grant types.
112-130
: Approve client-side redirect URI validation
Client-side logic enforces HTTPS with local-only HTTP support, and server-side ValidateRedirectURI simply matches against the configured URI list—together they correctly secure redirect URIs.web/src/i18n/locales/en.json (1)
447-447
: Comprehensive OAuth2 localization coverage with consistent terminology.The translation additions provide extensive coverage for OAuth2/OIDC functionality with consistent terminology. The translations appropriately cover technical concepts while remaining user-friendly.
Also applies to: 522-2178
useEffect(() => { | ||
if (visible) load(); | ||
}, [visible]); | ||
useEffect(() => { | ||
if (!visible) return; | ||
(async () => { | ||
try { | ||
const res = await API.get('/api/oauth/server-info'); | ||
const p = res?.data?.default_private_key_path; | ||
if (p) setGenPath(p); | ||
} catch {} | ||
})(); | ||
}, [visible]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
🧩 Analysis chain
Verify server-info endpoint availability and error resilience.
The server-info fetch for default path configuration lacks error handling feedback and could fail silently if the OAuth2 server is not properly configured.
Run this script to verify the server-info endpoint behavior:
🏁 Script executed:
#!/bin/bash
# Verify the server-info endpoint and its expected response structure
rg -n "server-info" --type=js --type=go -A 3 -B 3
Length of output: 2514
Handle /api/oauth/server-info errors in JWKSManagerModal — don’t swallow failures
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (lines 91–97) currently catches API.get('/api/oauth/server-info') errors and does nothing; the endpoint exists (router/api-router.go) and ServerInfoModal surfaces failures — replace the empty catch with explicit error handling (showError/log) or remove the try/catch so the global handler runs; ensure skipErrorHandler is used intentionally.
🤖 Prompt for AI Agents
In web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx around lines
87 to 99, the try/catch around the API.get('/api/oauth/server-info') call
silently swallows failures; replace the empty catch with explicit error handling
by either calling the app error utility (e.g., showError or processLogger.error)
to surface the error, or remove the try/catch so the global error handler can
run; if you intentionally need to suppress global handling, pass
skipErrorHandler explicitly and log the error locally so failures are not
silent.
- **Table Layout Optimization:** - Remove description column from OAuth2 client table to save space - Add tooltip on client name hover to display full description - Adjust table scroll width from 1200px to 1000px for better layout - Improve client name column width to 180px for better readability - **Action Button Simplification:** - Replace icon-only buttons with text labels for better accessibility - Simplify Popconfirm content by removing complex styled layouts - Remove unnecessary Tooltip wrappers around action buttons - Clean up unused Lucide icon imports (Edit, Key, Trash2) - **Code Display Enhancement:** - Replace basic <pre> tags with CodeViewer component in modal dialogs - Add syntax highlighting for JSON content in ServerInfoModal and JWKSInfoModal - Implement copy-to-clipboard functionality for server info and JWKS data - Add performance optimization for large content display - Provide expandable/collapsible interface for better UX - **Component Architecture:** - Import and integrate CodeViewer component in both modal components - Set appropriate props: content, title, and language='json' - Maintain loading states and error handling functionality - **Internationalization:** - Add English translations for new UI elements: * '暂无描述': 'No description' * 'OAuth2 服务器配置': 'OAuth2 Server Configuration' * 'JWKS 密钥集': 'JWKS Key Set' - **User Experience Improvements:** - Enhanced tooltip interaction for description display - Better visual feedback with cursor-help styling - Improved code readability with professional dark theme - Consistent styling across all OAuth2 management interfaces This update focuses on UI/UX improvements while maintaining full functionality and adding modern code viewing capabilities to the OAuth2 management system.
There was a problem hiding this 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
🧹 Nitpick comments (5)
web/src/i18n/locales/en.json (2)
447-447
: Minor translation refinement for consistency with existing patternsThe translation "by" for "由" is good, but for consistency with similar phrases in the codebase, consider "developed by" to maintain the full context when used with phrases like "开发,基于".
831-831
: Inconsistent translation key formatThe translation "删除失败-oauth2clients": "Delete failed" appears to have a formatting issue. The key contains what looks like a concatenated identifier that should either be separated or standardized.
- "删除失败-oauth2clients": "Delete failed", + "删除失败": "Delete failed",Or if this is meant to be OAuth2-specific:
- "删除失败-oauth2clients": "Delete failed", + "删除OAuth2客户端失败": "Failed to delete OAuth2 client",web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (1)
180-211
: Consider improving grant type validation consistency.The grant type parsing and display logic is robust, but there's a potential inconsistency with how grant types are validated in the create/edit modals.
Based on OAuth2 security best practices, the client credentials grant should only be used for confidential clients. While the create/edit modals enforce this rule, this display component should be defensive against any data inconsistencies.
Consider adding a validation check:
const types = typeof grantTypes === 'string' ? grantTypes.split(',') : grantTypes || []; + +// Filter out client_credentials for public clients as a safety measure +const validTypes = types.filter(type => + !(type === 'client_credentials' && record.client_type === 'public') +); + const typeMap = { client_credentials: t('客户端凭证'), authorization_code: t('授权码'), refresh_token: t('刷新令牌'), }; return ( <div className='flex flex-wrap gap-1'> - {types.slice(0, 2).map((type) => ( + {validTypes.slice(0, 2).map((type) => ( <Tag color='white' key={type} size='small' shape='circle'> {typeMap[type] || type} </Tag> ))} - {types.length > 2 && ( + {validTypes.length > 2 && ( <Tooltip - content={types + content={validTypes .slice(2) .map((t) => typeMap[t] || t) .join(', ')} > <Tag color='white' size='small' shape='circle'> - +{types.length - 2} + +{validTypes.length - 2} </Tag> </Tooltip> )} </div> );web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (2)
45-77
: Consider extracting default configuration to a constant.The default OAuth2 configuration object in the
inputs
state could be extracted to a module-level constant for better maintainability and testability.+const DEFAULT_OAUTH2_CONFIG = { + 'oauth2.enabled': false, + 'oauth2.issuer': '', + 'oauth2.access_token_ttl': 10, + 'oauth2.refresh_token_ttl': 720, + 'oauth2.jwt_signing_algorithm': 'RS256', + 'oauth2.jwt_key_id': 'oauth2-key-1', + 'oauth2.allowed_grant_types': [ + 'client_credentials', + 'authorization_code', + 'refresh_token', + ], + 'oauth2.require_pkce': true, + 'oauth2.max_jwks_keys': 3, +}; export default function OAuth2ServerSettings(props) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); - const [inputs, setInputs] = useState({ - 'oauth2.enabled': false, - 'oauth2.issuer': '', - 'oauth2.access_token_ttl': 10, - 'oauth2.refresh_token_ttl': 720, - 'oauth2.jwt_signing_algorithm': 'RS256', - 'oauth2.jwt_key_id': 'oauth2-key-1', - 'oauth2.allowed_grant_types': [ - 'client_credentials', - 'authorization_code', - 'refresh_token', - ], - 'oauth2.require_pkce': true, - 'oauth2.max_jwks_keys': 3, - }); + const [inputs, setInputs] = useState(DEFAULT_OAUTH2_CONFIG);
375-412
: Consider adding validation for JWT key ID format.The JWT key ID field should validate for proper formatting to prevent issues with key management.
<Form.Input field='oauth2.jwt_key_id' label={t('JWT密钥ID')} placeholder='oauth2-key-1' value={inputs['oauth2.jwt_key_id']} onChange={handleFieldChange('oauth2.jwt_key_id')} extraText={t('用于标识JWT签名密钥,支持密钥轮换')} + rules={[ + { + pattern: /^[a-zA-Z0-9_-]+$/, + message: t('密钥ID只能包含字母、数字、下划线和连字符') + } + ]} style={{ width: '100%', opacity: isEnabled ? 1 : 0.5, }} disabled={!isEnabled} />
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
web/src/components/common/ui/CodeViewer.jsx
(1 hunks)web/src/components/playground/DebugPanel.jsx
(1 hunks)web/src/components/settings/oauth2/OAuth2ClientSettings.jsx
(1 hunks)web/src/components/settings/oauth2/OAuth2ServerSettings.jsx
(1 hunks)web/src/components/settings/oauth2/modals/JWKSInfoModal.jsx
(1 hunks)web/src/components/settings/oauth2/modals/SecretDisplayModal.jsx
(1 hunks)web/src/components/settings/oauth2/modals/ServerInfoModal.jsx
(1 hunks)web/src/components/settings/personal/cards/NotificationSettings.jsx
(1 hunks)web/src/i18n/locales/en.json
(4 hunks)
✅ Files skipped from review due to trivial changes (2)
- web/src/components/playground/DebugPanel.jsx
- web/src/components/settings/personal/cards/NotificationSettings.jsx
🚧 Files skipped from review as they are similar to previous changes (2)
- web/src/components/settings/oauth2/modals/SecretDisplayModal.jsx
- web/src/components/settings/oauth2/modals/JWKSInfoModal.jsx
🧰 Additional context used
🧬 Code graph analysis (3)
web/src/components/settings/oauth2/modals/ServerInfoModal.jsx (2)
web/src/components/common/ui/CodeViewer.jsx (2)
useTranslation
(142-142)CodeViewer
(141-355)web/src/helpers/utils.jsx (1)
showError
(118-147)
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (6)
web/src/components/settings/oauth2/modals/SecretDisplayModal.jsx (2)
useTranslation
(27-27)SecretDisplayModal
(26-53)web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx (4)
useTranslation
(41-41)loading
(43-43)grantTypes
(46-46)CreateOAuth2ClientModal
(40-526)web/src/components/settings/oauth2/modals/EditOAuth2ClientModal.jsx (4)
useTranslation
(40-40)loading
(42-42)grantTypes
(44-44)EditOAuth2ClientModal
(39-451)web/src/components/settings/oauth2/modals/JWKSInfoModal.jsx (3)
useTranslation
(27-27)loading
(28-28)JWKSInfoModal
(26-68)web/src/components/settings/oauth2/modals/ServerInfoModal.jsx (3)
useTranslation
(27-27)loading
(28-28)ServerInfoModal
(26-70)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (2)
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (3)
useTranslation
(40-40)loading
(41-41)JWKSManagerModal
(39-313)web/src/helpers/utils.jsx (4)
compareObjects
(294-311)showWarning
(149-151)showError
(118-147)showSuccess
(153-155)
🔇 Additional comments (25)
web/src/i18n/locales/en.json (2)
522-535
: LGTM! Clean OAuth2 authorization flow translationsThe authorization-related translations are well structured and provide clear user guidance for the OAuth2 flow. The translations properly distinguish between different client types and authorization states.
2102-2180
: Comprehensive OAuth2/SSO translation additions look excellentThe OAuth2 and SSO translation additions are comprehensive and well-structured:
- Complete coverage: All OAuth2 client management, server configuration, and JWKS functionality
- Consistent terminology: Proper use of standard OAuth2/OIDC terms throughout
- Clear user guidance: Helpful descriptions for technical concepts like PKCE, JWT signing, and token lifetimes
- Professional tone: Appropriate for enterprise/production use
The translations properly handle technical concepts while remaining accessible to administrators configuring the system.
web/src/components/common/ui/CodeViewer.jsx (1)
24-24
: LGTM! Helper import path correctly updated.The import path for the copy helper has been updated from
../../helpers
to../../../helpers
, which aligns with the centralized helper location mentioned in the AI summary.web/src/components/settings/oauth2/modals/ServerInfoModal.jsx (4)
20-24
: LGTM! Component structure and imports are properly set up.The component properly imports necessary dependencies including React hooks, Semi UI components, and the centralized API and error handling utilities.
31-41
: LGTM! Data fetching logic is implemented correctly.The
loadServerInfo
function properly handles loading states, API calls, and error handling using the centralized error handling pattern consistent with the codebase.
43-47
: LGTM! Effect hook correctly triggers data loading.The
useEffect
properly fetches server info when the modal becomes visible, which is the expected behavior for modal components.
61-67
: LGTM! CodeViewer integration is well implemented.The CodeViewer component is properly configured to display the OAuth2 server information as formatted JSON, with appropriate fallback text during loading.
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (8)
20-42
: LGTM! Component imports and setup are comprehensive.All necessary React hooks, Semi UI components, icons, and modal dependencies are properly imported. The component structure follows React best practices.
45-57
: LGTM! State management is well organized.The component maintains appropriate state for managing the client list, modals, and current operations. State variables are logically grouped and named clearly.
60-75
: LGTM! Data loading with proper error handling.The
loadClients
function correctly implements the API call pattern with loading states and error handling consistent with the codebase patterns.
78-91
: LGTM! Search functionality is comprehensive.The search implementation properly filters clients by name, ID, and description with case-insensitive matching. The logic correctly handles empty search terms.
108-124
: LGTM! Secret regeneration follows security best practices.The regeneration logic properly calls the API, displays the new secret in a modal, and refreshes the client list. This follows OAuth2 security best practices for secret rotation.
232-244
: LGTM! Conditional secret regeneration follows OAuth2 best practices.The regeneration button is correctly only shown for confidential clients, which aligns with OAuth2 security principles that client credentials are only used for confidential, server-side applications.
322-361
: LGTM! Table configuration and empty state are well designed.The table provides comprehensive client management functionality with appropriate pagination and a helpful empty state that guides users to create their first client.
364-406
: LGTM! Modal integrations are properly configured.All modals are correctly wired with appropriate visibility states and callback functions. The modal compositions follow consistent patterns throughout the codebase.
web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (10)
20-41
: LGTM! Clean imports and good organization.The imports are well-organized and all appear to be used appropriately in the component. The use of external libraries (Semi-UI, Lucide React) and internal helpers follows good practices.
118-141
: Good defensive programming with enabledRef check.The use of
enabledRef.current
to prevent unnecessary API calls when OAuth2 is disabled is a good pattern that prevents 404 errors and improves performance.
143-176
: Robust configuration parsing with fallbacks.The configuration parsing logic properly handles different data types (boolean, number, array) with appropriate fallbacks. The
structuredClone
usage for deep copying is correct.
272-295
: Effective use of conditional warning banner.The banner implementation correctly warns users about missing signing keys and provides immediate access to key management. The loading state for the action button is well implemented.
297-316
: Good UX with smart placeholder and auto-detection.The issuer field uses
window.location.origin
as a placeholder and mentions X-Forwarded-Proto support, which shows good consideration for different deployment scenarios.
322-373
: Appropriate input validation and user guidance.The numeric inputs have sensible min/max constraints and helpful extraText descriptions. The conditional styling and disabled states provide clear visual feedback.
417-453
: Well-designed grant type selection with clear labeling.The multi-select for grant types with descriptive Chinese translations provides excellent UX. The PKCE switch placement and description are appropriate.
455-476
: Comprehensive user guidance in help text.The help text provides valuable information about OAuth2 functionality, security considerations, and operational notes. This helps users understand the implications of their configuration choices.
480-486
: Clean modal integration.The JWKS manager modal integration is clean and follows React patterns correctly with proper state management.
182-206
: Verify /api/oauth/keys endpoint exists and harden error handling.Confirm the backend implements GET /api/oauth/keys (and /api/oauth/server-info if referenced); if missing, remove or guard the client call. If present, ensure non-2xx responses set keysReady=false and surface/log an error instead of silently proceeding. Location: web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (useEffect ≈ lines 182–206).
function onSubmit() { | ||
const updateArray = compareObjects(inputs, inputsRow); | ||
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么')); | ||
const requestQueue = updateArray.map((item) => { | ||
let value = ''; | ||
if (typeof inputs[item.key] === 'boolean') { | ||
value = String(inputs[item.key]); | ||
} else if (Array.isArray(inputs[item.key])) { | ||
value = JSON.stringify(inputs[item.key]); | ||
} else { | ||
value = inputs[item.key]; | ||
} | ||
return API.put('/api/option/', { | ||
key: item.key, | ||
value, | ||
}); | ||
}); | ||
setLoading(true); | ||
Promise.all(requestQueue) | ||
.then((res) => { | ||
if (requestQueue.length === 1) { | ||
if (res.includes(undefined)) return; | ||
} else if (requestQueue.length > 1) { | ||
if (res.includes(undefined)) | ||
return showError(t('部分保存失败,请重试')); | ||
} | ||
showSuccess(t('保存成功')); | ||
if (props && props.refresh) { | ||
props.refresh(); | ||
} | ||
}) | ||
.catch(() => { | ||
showError(t('保存失败,请重试')); | ||
}) | ||
.finally(() => { | ||
setLoading(false); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify error handling for partial API failures.
The error handling logic for batch updates has some edge cases that should be verified to ensure proper user feedback.
🏁 Script executed:
#!/bin/bash
# Description: Search for similar batch update patterns in the codebase to verify consistency
# Search for other components using compareObjects and batch API updates
rg -nP --type=js --type=jsx -C3 "Promise\.all.*API\.(put|post)" -g '!node_modules'
# Search for similar error handling patterns with res.includes(undefined)
rg -nP --type=js --type=jsx -C3 "res\.includes\(undefined\)" -g '!node_modules'
Length of output: 55
🏁 Script executed:
#!/bin/bash
# Search for batch API update patterns in .js and .jsx files
rg -nP -C3 "Promise\.all.*API\.(put|post)" --glob '!.git/**' --glob '!node_modules/**' --glob '*.js' --glob '*.jsx'
# Search for error handling patterns with res.includes(undefined)
rg -nP -C3 "res\.includes\(undefined\)" --glob '!.git/**' --glob '!node_modules/**' --glob '*.js' --glob '*.jsx'
Length of output: 12085
Show error on single-request failures
In onSubmit
, the if (res.includes(undefined)) return;
for requestQueue.length === 1
silently swallows failures—replace the bare return with a showError(t('保存失败,请重试'))
so users see an error message.
🤖 Prompt for AI Agents
In web/src/components/settings/oauth2/OAuth2ServerSettings.jsx around lines 79
to 116, the submit handler currently swallows a failed single-request response
by doing a bare `return` when `res.includes(undefined)`; change that branch to
call `showError(t('保存失败,请重试'))` (matching the multi-request failure case) and
then return, so a clear error message is shown to the user when a single API put
fails; keep the existing loading and finally behavior and still call
props.refresh() only on success.
- **Empty Component Enhancement:** - Replace custom User icon with professional IllustrationNoResult from Semi Design - Add dark mode support with IllustrationNoResultDark component - Standardize illustration size to 150x150px for consistency - Add proper padding (30px) to match design system standards - **Style Modernization:** - Convert inline styles to Tailwind CSS classes where appropriate - Replace `style={{ marginBottom: 16 }}` with `className='mb-4'` - Remove redundant `style={{ marginTop: 8 }}` from Table component - Remove custom `style={{ marginTop: 16 }}` from pagination and button - **Pagination Simplification:** - Simplify showTotal configuration from custom function to boolean `true` - Remove unnecessary `size='small'` property from pagination - Clean up pagination styling for better consistency - **Design System Alignment:** - Ensure Empty component matches UsersTable styling patterns - Improve visual consistency across OAuth2 management interfaces - Follow Semi Design illustration guidelines for empty states - **Code Quality:** - Reduce inline style usage in favor of utility classes - Simplify component props where default behavior is sufficient - Maintain functionality while improving maintainability This update enhances visual consistency and follows modern React styling practices while maintaining all existing functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (2)
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (2)
218-221
: Guard against missing/invalid timestampsPrevent “Invalid Date” rendering when
created_time
is absent/null.- render: (time) => new Date(time * 1000).toLocaleString(), + render: (time) => + time ? new Date(time * 1000).toLocaleString() : '—',
268-271
: Preserve filtering after refreshRe-apply current keyword when the list reloads; otherwise refresh resets filters.
useEffect(() => { loadClients(); }, []); + + // Re-apply current filter when client list changes + useEffect(() => { + if (searchKeyword) { + handleSearch(searchKeyword); + } else { + setFilteredClients(clients); + } + }, [clients]); // intentionally not depending on handleSearch
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (6)
web/src/components/settings/oauth2/modals/SecretDisplayModal.jsx (2)
useTranslation
(27-27)SecretDisplayModal
(26-53)web/src/components/settings/oauth2/modals/EditOAuth2ClientModal.jsx (4)
useTranslation
(40-40)loading
(42-42)grantTypes
(44-44)EditOAuth2ClientModal
(39-451)web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx (4)
useTranslation
(41-41)loading
(43-43)grantTypes
(46-46)CreateOAuth2ClientModal
(40-526)web/src/components/settings/oauth2/modals/JWKSInfoModal.jsx (3)
useTranslation
(27-27)loading
(28-28)JWKSInfoModal
(26-68)web/src/components/settings/oauth2/modals/ServerInfoModal.jsx (3)
useTranslation
(27-27)loading
(28-28)ServerInfoModal
(26-70)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
🔇 Additional comments (1)
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (1)
39-39
: Verify helpers barrel exportSandbox search returned no results — confirm that API, showError, showSuccess are re‑exported from ../../../helpers (import at web/src/components/settings/oauth2/OAuth2ClientSettings.jsx:39). If not, update the import to the module that defines them (e.g. web/src/helpers/utils) or add re‑exports.
// 搜索过滤 | ||
const handleSearch = (value) => { | ||
setSearchKeyword(value); | ||
if (!value) { | ||
setFilteredClients(clients); | ||
} else { | ||
const filtered = clients.filter( | ||
(client) => | ||
client.name?.toLowerCase().includes(value.toLowerCase()) || | ||
client.id?.toLowerCase().includes(value.toLowerCase()) || | ||
client.description?.toLowerCase().includes(value.toLowerCase()), | ||
); | ||
setFilteredClients(filtered); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix null-safe search; current logic can throw on undefined fields and numeric IDs
?.
doesn't guard the subsequent .includes(...)
. If name/description
is missing or id
is numeric, this throws. Also include client_id
in search.
Apply:
- const handleSearch = (value) => {
- setSearchKeyword(value);
- if (!value) {
- setFilteredClients(clients);
- } else {
- const filtered = clients.filter(
- (client) =>
- client.name?.toLowerCase().includes(value.toLowerCase()) ||
- client.id?.toLowerCase().includes(value.toLowerCase()) ||
- client.description?.toLowerCase().includes(value.toLowerCase()),
- );
- setFilteredClients(filtered);
- }
- };
+ const handleSearch = (v) => {
+ setSearchKeyword(v);
+ const needle = String(v ?? '').trim().toLowerCase();
+ if (!needle) return setFilteredClients(clients);
+ const norm = (s) => String(s ?? '').toLowerCase();
+ const filtered = clients.filter((client) =>
+ norm(client.name).includes(needle) ||
+ norm(client.client_id).includes(needle) || // OAuth client_id
+ norm(client.id).includes(needle) || // internal id (numeric safe)
+ norm(client.description).includes(needle)
+ );
+ setFilteredClients(filtered);
+ };
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// 搜索过滤 | |
const handleSearch = (value) => { | |
setSearchKeyword(value); | |
if (!value) { | |
setFilteredClients(clients); | |
} else { | |
const filtered = clients.filter( | |
(client) => | |
client.name?.toLowerCase().includes(value.toLowerCase()) || | |
client.id?.toLowerCase().includes(value.toLowerCase()) || | |
client.description?.toLowerCase().includes(value.toLowerCase()), | |
); | |
setFilteredClients(filtered); | |
} | |
}; | |
// 搜索过滤 | |
const handleSearch = (v) => { | |
setSearchKeyword(v); | |
const needle = String(v ?? '').trim().toLowerCase(); | |
if (!needle) return setFilteredClients(clients); | |
const norm = (s) => String(s ?? '').toLowerCase(); | |
const filtered = clients.filter((client) => | |
norm(client.name).includes(needle) || | |
norm(client.client_id).includes(needle) || // OAuth client_id | |
norm(client.id).includes(needle) || // internal id (numeric safe) | |
norm(client.description).includes(needle) | |
); | |
setFilteredClients(filtered); | |
}; |
🤖 Prompt for AI Agents
In web/src/components/settings/oauth2/OAuth2ClientSettings.jsx around lines 81
to 95, the search logic can throw when client fields are undefined or when id is
numeric and client_id is missing; fix by making each field null-safe and
string-coercing before calling toLowerCase/includes, e.g. use
(String(client.name || '')).toLowerCase() and (String(client.id ||
client.client_id || '')).toLowerCase() and (String(client.description ||
'')).toLowerCase(), then compare the lowercased search value to those strings;
ensure you include client.client_id in the search and setFilteredClients with
the resulting filtered list.
title: t('客户端ID'), | ||
dataIndex: 'id', | ||
render: (id) => ( | ||
<Text type='tertiary' size='small' code copyable> | ||
{id} | ||
</Text> | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Show OAuth client_id (external) instead of internal id in “客户端ID” column
The current column likely displays DB id, which confuses users and breaks parity with create/regenerate flows that surface client_id
.
- {
- title: t('客户端ID'),
- dataIndex: 'id',
- render: (id) => (
- <Text type='tertiary' size='small' code copyable>
- {id}
- </Text>
- ),
- },
+ {
+ title: t('客户端ID'),
+ dataIndex: 'client_id',
+ render: (clientId) => (
+ <Text type='tertiary' size='small' code copyable>
+ {clientId}
+ </Text>
+ ),
+ },
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
title: t('客户端ID'), | |
dataIndex: 'id', | |
render: (id) => ( | |
<Text type='tertiary' size='small' code copyable> | |
{id} | |
</Text> | |
), | |
title: t('客户端ID'), | |
dataIndex: 'client_id', | |
render: (clientId) => ( | |
<Text type='tertiary' size='small' code copyable> | |
{clientId} | |
</Text> | |
), |
🤖 Prompt for AI Agents
In web/src/components/settings/oauth2/OAuth2ClientSettings.jsx around lines 155
to 161, the "客户端ID" column currently renders the database internal id; change
the column to display the external OAuth client_id instead by updating the
render to use the row's client_id property (e.g. render: (id, record) =>
record.client_id || id) so it shows client_id when present and falls back to id;
keep the existing Text props (type='tertiary' size='small' code copyable).
{/* 密钥显示模态框 */} | ||
<SecretDisplayModal | ||
visible={showSecretModal} | ||
onClose={() => setShowSecretModal(false)} | ||
secret={currentSecret} | ||
/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clear regenerated secret from state after modal closes
Avoid lingering sensitive data in memory/state.
- <SecretDisplayModal
- visible={showSecretModal}
- onClose={() => setShowSecretModal(false)}
- secret={currentSecret}
- />
+ <SecretDisplayModal
+ visible={showSecretModal}
+ onClose={() => {
+ setShowSecretModal(false);
+ setCurrentSecret('');
+ }}
+ secret={currentSecret}
+ />
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
{/* 密钥显示模态框 */} | |
<SecretDisplayModal | |
visible={showSecretModal} | |
onClose={() => setShowSecretModal(false)} | |
secret={currentSecret} | |
/> | |
{/* 密钥显示模态框 */} | |
<SecretDisplayModal | |
visible={showSecretModal} | |
onClose={() => { | |
setShowSecretModal(false); | |
setCurrentSecret(''); | |
}} | |
secret={currentSecret} | |
/> |
🤖 Prompt for AI Agents
In web/src/components/settings/oauth2/OAuth2ClientSettings.jsx around lines 384
to 389, the SecretDisplayModal's onClose only toggles visibility but leaves
currentSecret in component state; update the onClose handler to both
setShowSecretModal(false) and clear currentSecret (e.g., setCurrentSecret(null)
or empty string) so the regenerated secret is removed from memory/state when the
modal closes; also ensure any code paths that close the modal (cancel,
after-copy, or after-regeneration) call the same clearing logic so no secret
lingers.
…support - Refactor JWKSManagerModal with tab-based navigation using Card components - Add comprehensive i18n support with English translations for all text - Optimize header actions: refresh button only appears in key list tab - Improve responsive design using ResponsiveModal component - Move cautionary text from bottom to Card titles for better visibility - Update button styles: danger type for delete, circle shape for status tags - Standardize code formatting (single quotes, multiline formatting) - Enhance user workflow: separate Import PEM and Generate PEM operations - Remove redundant cancel buttons as modal already has close icon Breaking changes: None Affects: JWKS key management, OAuth2 settings UI
There was a problem hiding this 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
🧹 Nitpick comments (16)
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (4)
63-80
: Preserve active search after reloadReloading resets filteredClients and ignores current searchKeyword.
Apply:
- if (res.data.success) { - setClients(res.data.data || []); - setFilteredClients(res.data.data || []); + if (res.data.success) { + const list = res.data.data || []; + setClients(list); + const needle = String(searchKeyword ?? '').trim().toLowerCase(); + if (needle) { + const norm = (s) => String(s ?? '').toLowerCase(); + setFilteredClients( + list.filter((c) => + norm(c.name).includes(needle) || + norm(c.client_id).includes(needle) || + norm(c.id).includes(needle) || + norm(c.description).includes(needle) + ) + ); + } else { + setFilteredClients(list); + }
327-331
: Prefer client_id as row key to avoid exposing internal DB idsSmall hardening and better stability across environments.
Apply:
- rowKey='id' + rowKey={(r) => r.client_id || r.id}
164-171
: Make status rendering tolerant to boolean/string valuesAPIs often return '1' or true.
Apply:
- render: (status) => ( - <Tag color={status === 1 ? 'green' : 'red'} shape='circle'> - {status === 1 ? t('启用') : t('禁用')} + render: (status) => { + const enabled = status === 1 || status === '1' || status === true; + return ( + <Tag color={enabled ? 'green' : 'red'} shape='circle'> + {enabled ? t('启用') : t('禁用')} + </Tag> + ); ),
218-221
: Guard against missing/invalid created_timePrevents NaN dates when field is absent or malformed.
Apply:
- render: (time) => new Date(time * 1000).toLocaleString(), + render: (time) => { + const ts = Number(time); + if (!Number.isFinite(ts)) return '-'; + return new Date(ts * 1000).toLocaleString(); + },web/src/components/common/ui/ResponsiveModal.jsx (4)
63-66
: Prevent titleProps.className from wiping base styles
{...titleProps}
afterclassName
overwrites your base classes. Merge them instead.- <Title heading={5} className='m-0 min-w-fit' {...titleProps}> + <Title + heading={5} + {...titleProps} + className={`m-0 min-w-fit ${titleProps?.className || ''}`} + >
73-76
: Avoid index keys; preserve caller-provided keysUsing array index can cause unstable reconciliation. Flatten and respect keys via React.Children.
- {headerActions.map((action, index) => ( - <React.Fragment key={index}>{action}</React.Fragment> - ))} + {React.Children.toArray(headerActions)}
83-88
: Harden width selection when an object is partially specifiedIf
width
is an object missingmobile
/desktop
, this returnsundefined
. Add a safe fallback.- const getModalWidth = () => { - if (typeof width === 'object') { - return isMobile ? width.mobile : width.desktop; - } - return width; - }; + const getModalWidth = () => { + if (width && typeof width === 'object' && !Array.isArray(width)) { + const v = isMobile ? width.mobile : width.desktop; + return v ?? (isMobile ? '95%' : 600); + } + return width; + };
97-97
: Verify!top-12
actually affects the correct elementDepending on Semi’s DOM,
className
may not control the positioned node. If it doesn’t shift the dialog, preferstyle={{ top: 48 }}
(or atop
prop if Semi exposes one).web/src/i18n/locales/en.json (3)
522-536
: Fix capitalization and consistency on consent texts.Capitalize “Unknown domain” to match title‑case style elsewhere.
Apply this diff:
- "未知域": "unknown domain", + "未知域": "Unknown domain",
2101-2101
: Disambiguate “Save” vs “Savings”.“节省” in pricing should be “Savings”, not “Save” (which collides with “保存”).
Apply this diff:
- "节省": "Save", + "节省": "Savings",
2108-2180
: Consolidate duplicate i18n keys'授权服务器配置' (web/src/i18n/locales/en.json:2108) duplicates 'OAuth2 服务器配置' (web/src/i18n/locales/en.json:2179); ServerInfoModal.jsx uses 'OAuth2 服务器配置' (web/src/components/settings/oauth2/modals/ServerInfoModal.jsx:65). Remove or merge the unused key and keep naming consistent.
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (2)
132-156
: Unify error handling to leverage global Axios behavior (401 redirect, etc.).Use showError instead of Toast.error in import/generate flows for consistency and auth‑expiry handling.
Apply this diff:
- } else { - Toast.error(res?.data?.message || t('导入失败')); - } - } catch { - Toast.error(t('导入失败')); + } else { + showError(res?.data?.message || t('导入失败')); + } + } catch (err) { + showError(t('导入失败')); } finally { setLoading(false); }- } else { - Toast.error(res?.data?.message || t('生成失败')); - } - } catch { - Toast.error(t('生成失败')); + } else { + showError(res?.data?.message || t('生成失败')); + } + } catch (err) { + showError(t('生成失败')); } finally { setLoading(false); }Also applies to: 158-180
100-102
: Avoid OS‑specific default path.Hardcoding “/etc/new-api/oauth2-private.pem” is Linux‑centric. Prefer empty default and rely on server‑info to populate, or gate by platform.
web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (3)
118-141
: Localize toast messages and reuse existing keys.Use i18n keys instead of literal Chinese; reuse “运行正常”/“获取服务器信息失败”.
Apply this diff:
- if ( + if ( res.status === 200 && (res.data.issuer || res.data.authorization_endpoint) ) { - if (!silent) showSuccess('OAuth2服务器运行正常'); + if (!silent) showSuccess(t('运行正常')); setServerInfo(res.data); } else { if (!enabledRef.current) return; - if (!silent) showError('OAuth2服务器测试失败'); + if (!silent) showError(t('获取服务器信息失败')); } } catch (error) { if (!enabledRef.current) return; - if (!silent) showError('OAuth2服务器连接测试失败'); + if (!silent) showError(t('获取服务器信息失败')); }
375-397
: Security posture: de‑emphasize HS256 for server‑side tokens.HS256 is symmetric and raises key‑sharing risks; keep RS256 as default and consider gating HS256 behind an “expert/legacy” flag or add helper text warning.
79-116
: Confirm diff direction in compareObjects.You compare inputs (current) vs inputsRow (initial). It works, but the helper names imply old→new; consider swapping args or documenting to avoid future confusion.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
web/src/components/common/ui/ResponsiveModal.jsx
(1 hunks)web/src/components/settings/oauth2/OAuth2ClientSettings.jsx
(1 hunks)web/src/components/settings/oauth2/OAuth2ServerSettings.jsx
(1 hunks)web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx
(1 hunks)web/src/components/settings/oauth2/modals/SecretDisplayModal.jsx
(1 hunks)web/src/i18n/locales/en.json
(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- web/src/components/settings/oauth2/modals/SecretDisplayModal.jsx
🧰 Additional context used
🧬 Code graph analysis (3)
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (6)
web/src/components/settings/oauth2/modals/SecretDisplayModal.jsx (2)
useTranslation
(27-27)SecretDisplayModal
(26-55)web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx (4)
useTranslation
(41-41)loading
(43-43)grantTypes
(46-46)CreateOAuth2ClientModal
(40-526)web/src/components/settings/oauth2/modals/EditOAuth2ClientModal.jsx (4)
useTranslation
(40-40)loading
(42-42)grantTypes
(44-44)EditOAuth2ClientModal
(39-451)web/src/components/settings/oauth2/modals/ServerInfoModal.jsx (3)
useTranslation
(27-27)loading
(28-28)ServerInfoModal
(26-70)web/src/components/settings/oauth2/modals/JWKSInfoModal.jsx (3)
useTranslation
(27-27)loading
(28-28)JWKSInfoModal
(26-68)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (2)
web/src/components/common/ui/ResponsiveModal.jsx (1)
ResponsiveModal
(36-103)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (2)
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (3)
useTranslation
(47-47)loading
(48-48)JWKSManagerModal
(46-398)web/src/helpers/utils.jsx (4)
compareObjects
(294-311)showWarning
(149-151)showError
(118-147)showSuccess
(153-155)
🔇 Additional comments (9)
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (3)
155-161
: Show OAuth client_id in “客户端ID” column (not internal id)Users expect the external client_id here.
Apply:
- { - title: t('客户端ID'), - dataIndex: 'id', - render: (id) => ( - <Text type='tertiary' size='small' code copyable> - {id} - </Text> - ), - }, + { + title: t('客户端ID'), + dataIndex: 'client_id', + render: (clientId, record) => ( + <Text type='tertiary' size='small' code copyable> + {clientId || record.id} + </Text> + ), + },
81-95
: Fix null-unsafe search and include client_id in filterSearch can throw when fields are undefined or id is numeric. Also missing client_id.
Apply:
- const handleSearch = (value) => { - setSearchKeyword(value); - if (!value) { - setFilteredClients(clients); - } else { - const filtered = clients.filter( - (client) => - client.name?.toLowerCase().includes(value.toLowerCase()) || - client.id?.toLowerCase().includes(value.toLowerCase()) || - client.description?.toLowerCase().includes(value.toLowerCase()), - ); - setFilteredClients(filtered); - } - }; + const handleSearch = (v) => { + setSearchKeyword(v); + const needle = String(v ?? '').trim().toLowerCase(); + if (!needle) return setFilteredClients(clients); + const norm = (s) => String(s ?? '').toLowerCase(); + const filtered = clients.filter((c) => + norm(c.name).includes(needle) || + norm(c.client_id).includes(needle) || + norm(c.id).includes(needle) || + norm(c.description).includes(needle) + ); + setFilteredClients(filtered); + };
384-388
: Clear regenerated secret from state on modal closeAvoid leaving sensitive secrets in memory/state longer than necessary.
Apply:
- <SecretDisplayModal - visible={showSecretModal} - onClose={() => setShowSecretModal(false)} - secret={currentSecret} - /> + <SecretDisplayModal + visible={showSecretModal} + onClose={() => { + setShowSecretModal(false); + setCurrentSecret(''); + }} + secret={currentSecret} + />web/src/components/common/ui/ResponsiveModal.jsx (2)
50-50
: SSR/hydration safety foruseIsMobile
If this project renders on the server, ensure
useIsMobile
guards againstwindow
access pre‑mount and avoids hydration mismatches.
91-96
: Confirm Semi UI prop name:visible
vsopen
Verification failed: package.json not found at repo root (jq error). Confirm installed @douyinfe/semi-ui version (check package.json or web/package.json) and ensure all usages across the repo consistently use the correct prop (
visible
for older Semi,open
for newer). Run locally:
jq -r '.dependencies["@douyinfe/semi-ui"] // .devDependencies["@douyinfe/semi-ui"] // "NOT FOUND"' package.json || jq -r '.dependencies["@douyinfe/semi-ui"] // .devDependencies["@douyinfe/semi-ui"] // "NOT FOUND"' web/package.json
rg -nP --type=js --type=jsx --type=tsx -C2 '<Modal[^>]\b(visible|open)\s=' .web/src/i18n/locales/en.json (2)
447-447
: LGTM: translation tweak is correct."由" → "by" reads better in English footers.
831-831
: Remove unused translation key (删除失败-oauth2clients
)
No references to this key found in code; remove it fromweb/src/i18n/locales/en.json
.Likely an incorrect or invalid review comment.
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (1)
121-130
: Don’t swallow server‑info errors; surface via showError.Empty catch hides failures and complicates debugging.
Apply this diff:
- } catch {} + } catch (err) { + showError(t('获取服务器信息失败')); + }web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (1)
99-105
: Show an error when a single PUT fails; don’t silently return.Keep UX consistent with the multi‑request case.
Apply this diff:
- if (requestQueue.length === 1) { - if (res.includes(undefined)) return; + if (requestQueue.length === 1) { + if (res.includes(undefined)) { + return showError(t('保存失败,请重试')); + }
{!keysReady && isEnabled && ( | ||
<Banner | ||
type='warning' | ||
description={ | ||
<div> | ||
<div> | ||
⚠️ 尚未准备签名密钥,建议立即初始化或轮换以发布 JWKS。 | ||
</div> | ||
<div>签名密钥用于 JWT 令牌的安全签发。</div> | ||
</div> | ||
} | ||
actions={ | ||
<Button | ||
size='small' | ||
type='primary' | ||
onClick={() => setJwksVisible(true)} | ||
loading={keysLoading} | ||
> | ||
打开密钥管理 | ||
</Button> | ||
} | ||
style={{ marginBottom: 16 }} | ||
/> | ||
)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Externalize hard‑coded Chinese strings to i18n.
The banner text and action should use t() keys (already added in en.json).
Apply this diff:
- <Banner
+ <Banner
type='warning'
description={
<div>
- <div>
- ⚠️ 尚未准备签名密钥,建议立即初始化或轮换以发布 JWKS。
- </div>
- <div>签名密钥用于 JWT 令牌的安全签发。</div>
+ <div>{t('⚠️ 尚未准备签名密钥,建议立即初始化或轮换以发布 JWKS。')}</div>
+ <div>{t('签名密钥用于 JWT 令牌的安全签发。')}</div>
</div>
}
actions={
<Button
size='small'
type='primary'
onClick={() => setJwksVisible(true)}
loading={keysLoading}
>
- 打开密钥管理
+ {t('打开密钥管理')}
</Button>
}
style={{ marginBottom: 16 }}
/>
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
{!keysReady && isEnabled && ( | |
<Banner | |
type='warning' | |
description={ | |
<div> | |
<div> | |
⚠️ 尚未准备签名密钥,建议立即初始化或轮换以发布 JWKS。 | |
</div> | |
<div>签名密钥用于 JWT 令牌的安全签发。</div> | |
</div> | |
} | |
actions={ | |
<Button | |
size='small' | |
type='primary' | |
onClick={() => setJwksVisible(true)} | |
loading={keysLoading} | |
> | |
打开密钥管理 | |
</Button> | |
} | |
style={{ marginBottom: 16 }} | |
/> | |
)} | |
{!keysReady && isEnabled && ( | |
<Banner | |
type='warning' | |
description={ | |
<div> | |
<div>{t('⚠️ 尚未准备签名密钥,建议立即初始化或轮换以发布 JWKS。')}</div> | |
<div>{t('签名密钥用于 JWT 令牌的安全签发。')}</div> | |
</div> | |
} | |
actions={ | |
<Button | |
size='small' | |
type='primary' | |
onClick={() => setJwksVisible(true)} | |
loading={keysLoading} | |
> | |
{t('打开密钥管理')} | |
</Button> | |
} | |
style={{ marginBottom: 16 }} | |
/> | |
)} |
🤖 Prompt for AI Agents
In web/src/components/settings/oauth2/OAuth2ServerSettings.jsx around lines 272
to 295, the Banner and Button contain hard-coded Chinese strings; replace them
with i18n lookups using the t() function (import/use useTranslation from
react-i18next if not already imported). Replace the two description lines and
the Button label with the corresponding t('...') keys that were added to
en.json, keep the JSX structure (e.g. wrap translated strings in <div> as
before) and preserve the onClick, loading and style props; ensure
keysReady/isEnabled logic remains unchanged.
This commit consolidates OAuth2 client management components and enhances the overall user experience with improved UI consistency. ### Major Changes: **Component Consolidation:** - Merge CreateOAuth2ClientModal.jsx and EditOAuth2ClientModal.jsx into OAuth2ClientModal.jsx - Extract inline Modal.info into dedicated ClientInfoModal.jsx component - Adopt consistent SideSheet + Card layout following EditTokenModal.jsx style **UI/UX Improvements:** - Replace custom client type selection with SemiUI RadioGroup component - Use 'card' type RadioGroup with descriptive 'extra' prop for better UX - Remove all Row/Col components in favor of flexbox and margin-based layouts - Refactor redirect URI section to mimic JSONEditor.jsx visual style - Add responsive design support for mobile devices **Form Enhancements:** - Add 'required' attributes to all mandatory form fields - Implement placeholders for grant types, scopes, and redirect URI inputs - Set grant types and scopes to default empty arrays - Add dynamic validation and conditional rendering for client types - Improve redirect URI management with template filling functionality **Bug Fixes:** - Fix SideSheet closing direction consistency between create/edit modes - Resolve client_type submission issue (object vs string) - Prevent "Client Credentials" selection for public clients - Fix grant type filtering when switching between client types - Resolve i18n issues for API scope options (api:read, api:write) **Code Quality:** - Extract RedirectUriCard as reusable sub-component - Add comprehensive internationalization support - Implement proper state management and form validation - Follow single responsibility principle for component separation **Files Modified:** - web/src/components/settings/oauth2/modals/OAuth2ClientModal.jsx - web/src/components/settings/oauth2/modals/ClientInfoModal.jsx (new) - web/src/components/settings/oauth2/OAuth2ClientSettings.jsx - web/src/i18n/locales/en.json **Files Removed:** - web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx - web/src/components/settings/oauth2/modals/EditOAuth2ClientModal.jsx This refactoring significantly improves code maintainability, reduces duplication, and provides a more consistent and intuitive user interface for OAuth2 client management.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (5)
web/src/components/settings/oauth2/modals/ClientInfoModal.jsx (1)
35-37
: Avoid rendering an empty “Cancel” buttonUsing cancelText='' leaves an empty button. Prefer a single affirmative action footer to reduce confusion.
If Semi UI Modal supports customizing the footer, consider:
-import { Modal, Banner, Typography } from '@douyinfe/semi-ui'; +import { Modal, Banner, Typography, Button } from '@douyinfe/semi-ui'; @@ - <Modal + <Modal title={t('客户端创建成功')} visible={visible} - onCancel={onClose} - onOk={onClose} - cancelText='' - okText={t('我已复制保存')} + onCancel={onClose} + footer={ + <Button theme='solid' onClick={onClose}> + {t('我已复制保存')} + </Button> + } width={650} bodyStyle={{ padding: '20px 24px' }} >Confirm against Semi UI docs before changing, or keep current for consistency with other modals.
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (1)
216-219
: Guard timestamp renderingAvoid NaN/Invalid Date if created_time is missing or not a number.
- render: (time) => new Date(time * 1000).toLocaleString(), + render: (t) => { + const ts = Number(t); + return Number.isFinite(ts) ? new Date(ts * 1000).toLocaleString() : '-'; + },web/src/components/settings/oauth2/modals/OAuth2ClientModal.jsx (2)
284-296
: Include client_id in edit form valuesSo it can be displayed read‑only and copied by admins.
const formValues = { id: client.id, + client_id: client.client_id, name: client.name, description: client.description, client_type: client.client_type, grant_types: finalGrantTypes, scopes: parsedScopes, require_pkce: !!client.require_pkce, status: client.status, };
583-596
: Show client_id in edit mode; hide internal idSurface OAuth client_id read‑only; keep numeric id hidden for update payload.
- {isEdit && ( - <> - <Form.Select + {isEdit && ( + <> + <Form.Select field='status' label={t('状态')} rules={[{ required: true, message: t('请选择状态') }]} required > <Option value={1}>{t('启用')}</Option> <Option value={2}>{t('禁用')}</Option> </Form.Select> - <Form.Input field='id' label={t('客户端ID')} disabled /> + <Form.Input field='client_id' label={t('客户端ID')} disabled /> + <Form.Input field='id' style={{ display: 'none' }} /> </> )}web/src/i18n/locales/en.json (1)
530-535
: Correct translation: 授权类型 → Grant Types (not “Response type”)“授权类型” here denotes OAuth grant types, not the OAuth response_type parameter.
- "授权类型": "Response type", + "授权类型": "Grant Types", - "未知域": "unknown domain", + "未知域": "Unknown domain",
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx
(1 hunks)web/src/components/settings/oauth2/modals/ClientInfoModal.jsx
(1 hunks)web/src/components/settings/oauth2/modals/OAuth2ClientModal.jsx
(1 hunks)web/src/i18n/locales/en.json
(5 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (5)
web/src/components/settings/oauth2/modals/OAuth2ClientModal.jsx (4)
useTranslation
(157-157)loading
(160-160)grantTypes
(163-163)OAuth2ClientModal
(156-728)web/src/components/settings/oauth2/modals/SecretDisplayModal.jsx (2)
useTranslation
(27-27)SecretDisplayModal
(26-55)web/src/components/settings/oauth2/modals/JWKSInfoModal.jsx (3)
useTranslation
(27-27)loading
(28-28)JWKSInfoModal
(26-68)web/src/components/settings/oauth2/modals/ServerInfoModal.jsx (3)
useTranslation
(27-27)loading
(28-28)ServerInfoModal
(26-70)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
web/src/components/settings/oauth2/modals/OAuth2ClientModal.jsx (2)
web/src/components/settings/oauth2/modals/ClientInfoModal.jsx (2)
useTranslation
(27-27)ClientInfoModal
(26-76)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
web/src/components/settings/oauth2/modals/ClientInfoModal.jsx (2)
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (2)
Typography
(46-46)useTranslation
(49-49)web/src/components/settings/oauth2/modals/OAuth2ClientModal.jsx (2)
Typography
(49-49)useTranslation
(157-157)
🔇 Additional comments (3)
web/src/components/settings/oauth2/OAuth2ClientSettings.jsx (3)
79-93
: Fix null‑safe search; include client_idCurrent chaining can call .includes on undefined and misses client_id.
- const handleSearch = (value) => { - setSearchKeyword(value); - if (!value) { - setFilteredClients(clients); - } else { - const filtered = clients.filter( - (client) => - client.name?.toLowerCase().includes(value.toLowerCase()) || - client.id?.toLowerCase().includes(value.toLowerCase()) || - client.description?.toLowerCase().includes(value.toLowerCase()), - ); - setFilteredClients(filtered); - } - }; + const handleSearch = (v) => { + setSearchKeyword(v); + const needle = String(v ?? '').trim().toLowerCase(); + if (!needle) return setFilteredClients(clients); + const norm = (s) => String(s ?? '').toLowerCase(); + const filtered = clients.filter((c) => + norm(c.name).includes(needle) || + norm(c.client_id).includes(needle) || + norm(c.id).includes(needle) || + norm(c.description).includes(needle) + ); + setFilteredClients(filtered); + };
153-160
: Show external client_id, not DB idThe “客户端ID” column should display OAuth client_id to match creation/regeneration flows.
- { - title: t('客户端ID'), - dataIndex: 'id', - render: (id) => ( - <Text type='tertiary' size='small' code copyable> - {id} - </Text> - ), - }, + { + title: t('客户端ID'), + dataIndex: 'client_id', + render: (clientId) => ( + <Text type='tertiary' size='small' code copyable> + {clientId} + </Text> + ), + },
381-385
: Clear secret from state on modal closePrevent lingering sensitive data in memory/state.
- <SecretDisplayModal - visible={showSecretModal} - onClose={() => setShowSecretModal(false)} - secret={currentSecret} - /> + <SecretDisplayModal + visible={showSecretModal} + onClose={() => { + setShowSecretModal(false); + setCurrentSecret(''); + }} + secret={currentSecret} + />
- Move OAuth consent component to dedicated OAuth directory as index.jsx - Rename component export structure for better module organization - Update App.jsx import path to reflect new OAuth page structure - Maintain existing OAuth consent functionality while improving
There was a problem hiding this 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
🧹 Nitpick comments (3)
web/src/pages/OAuth/index.jsx (3)
44-62
: Optional: cover more common OIDC scopes.Consider mapping icons for scopes like
offline_access
,address
,phone
to improve UX parity with typical OIDC sets.
377-381
: Internationalize the “State” label.Minor i18n consistency nit.
- <Text type='tertiary' size='small' className='font-mono'> - State: {info.state} - </Text> + <Text type='tertiary' size='small' className='font-mono'> + {t('State')}: {info.state} + </Text>
31-31
: Avoid future confusion withLink
naming.
Link
from lucide-react can be mistaken forreact-router-dom
’sLink
. Alias for clarity.- import { Link, Dot, Key, User, Mail, Eye, Pencil, Shield } from 'lucide-react'; + import { Link as LinkIcon, Dot, Key, User, Mail, Eye, Pencil, Shield } from 'lucide-react'; @@ - <div className='w-10 h-10 rounded-full flex items-center justify-center'> - <Link size={20} /> - </div> + <div className='w-10 h-10 rounded-full flex items-center justify-center'> + <LinkIcon size={20} /> + </div>Also applies to: 247-249
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
web/src/App.jsx
(2 hunks)web/src/pages/OAuth/index.jsx
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- web/src/App.jsx
🧰 Additional context used
🧬 Code graph analysis (1)
web/src/pages/OAuth/index.jsx (2)
web/src/helpers/render.jsx (1)
stringToColor
(561-568)web/src/helpers/utils.jsx (1)
getLogo
(55-59)
🔇 Additional comments (4)
web/src/pages/OAuth/index.jsx (4)
39-42
: Hook looks good.
useQuery
memoizesURLSearchParams
onsearch
changes; clean and idiomatic.
94-112
: Stop whitelisting and defaulting; preserve all params (including duplicates).Whitelisting drops extension params and defaulting
response_type
can mask client bugs. Also, the current object-based approach collapses repeated keys. Use URLSearchParams to snapshot the original query as-is. (Same concern was raised previously.)- const params = useMemo(() => { - const allowed = [ - 'response_type', - 'client_id', - 'redirect_uri', - 'scope', - 'state', - 'code_challenge', - 'code_challenge_method', - 'nonce', - ]; - const obj = {}; - allowed.forEach((k) => { - const v = query.get(k); - if (v) obj[k] = v; - }); - if (!obj.response_type) obj.response_type = 'code'; - return obj; - }, [query]); + const params = useMemo(() => new URLSearchParams(query), [query]);
134-139
: Preserve duplicate query keys when building approve/deny URL.
Object.entries(params)
(pre-change) andURLSearchParams#set
collapse repeats. Useappend
on the cloned URL.- const handleAction = (action) => { - const u = new URL(window.location.origin + '/api/oauth/authorize'); - Object.entries(params).forEach(([k, v]) => u.searchParams.set(k, v)); - u.searchParams.set(action, '1'); - window.location.href = u.toString(); - }; + const handleAction = (action) => { + const u = new URL(window.location.origin + '/api/oauth/authorize'); + params.forEach((v, k) => u.searchParams.append(k, v)); + u.searchParams.set(action, '1'); + window.location.href = u.toString(); + };
301-317
: Ensure post-authnext
redirect is same-origin / allowlisted.Frontend builds
next
from user-controlled query (web/src/pages/OAuth/index.jsx:301–317). Confirm the server-side login/consent handler validatesnext
and only redirects to same-origin relative paths or an explicit allowlist — reject external URLs/schemes to avoid open-redirects. Local repo search produced no evidence of such validation; verify or implement server-side check.
useEffect(() => { | ||
(async () => { | ||
setLoading(true); | ||
try { | ||
const res = await API.get('/api/oauth/authorize', { | ||
params: { ...params, mode: 'prepare' }, | ||
// skip error toast, we'll handle gracefully | ||
skipErrorHandler: true, | ||
}); | ||
setInfo(res.data); | ||
setError(''); | ||
} catch (e) { | ||
// 401 login required or other error | ||
setError(e?.response?.data?.error || 'failed'); | ||
} finally { | ||
setLoading(false); | ||
} | ||
})(); | ||
}, [params]); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Send URLSearchParams to the prepare endpoint; don’t materialize to an object.
Objects lose duplicate keys and ordering. Pass a cloned URLSearchParams and explicitly serialize to preserve all entries.
useEffect(() => {
(async () => {
setLoading(true);
try {
- const res = await API.get('/api/oauth/authorize', {
- params: { ...params, mode: 'prepare' },
- // skip error toast, we'll handle gracefully
- skipErrorHandler: true,
- });
+ const p = new URLSearchParams(params);
+ p.set('mode', 'prepare');
+ const res = await API.get('/api/oauth/authorize', {
+ params: p,
+ // ensure duplicates are preserved
+ paramsSerializer: (ps) => ps.toString(),
+ skipErrorHandler: true,
+ });
setInfo(res.data);
setError('');
} catch (e) {
// 401 login required or other error
setError(e?.response?.data?.error || 'failed');
} finally {
setLoading(false);
}
})();
}, [params]);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
useEffect(() => { | |
(async () => { | |
setLoading(true); | |
try { | |
const res = await API.get('/api/oauth/authorize', { | |
params: { ...params, mode: 'prepare' }, | |
// skip error toast, we'll handle gracefully | |
skipErrorHandler: true, | |
}); | |
setInfo(res.data); | |
setError(''); | |
} catch (e) { | |
// 401 login required or other error | |
setError(e?.response?.data?.error || 'failed'); | |
} finally { | |
setLoading(false); | |
} | |
})(); | |
}, [params]); | |
useEffect(() => { | |
(async () => { | |
setLoading(true); | |
try { | |
const p = new URLSearchParams(params); | |
p.set('mode', 'prepare'); | |
const res = await API.get('/api/oauth/authorize', { | |
params: p, | |
// ensure duplicates are preserved | |
paramsSerializer: (ps) => ps.toString(), | |
// skip error toast, we'll handle gracefully | |
skipErrorHandler: true, | |
}); | |
setInfo(res.data); | |
setError(''); | |
} catch (e) { | |
// 401 login required or other error | |
setError(e?.response?.data?.error || 'failed'); | |
} finally { | |
setLoading(false); | |
} | |
})(); | |
}, [params]); |
🤖 Prompt for AI Agents
In web/src/pages/OAuth/index.jsx around lines 114-133, the code spreads params
into an object which loses duplicate keys and ordering; instead clone the
existing URLSearchParams, add the mode=prepare param on the clone, then
serialize the clone into the query string when calling the API so all entries
are preserved. Concretely: create a new URLSearchParams from params (e.g. new
URLSearchParams(params.toString())), set or append the mode param on that clone,
and pass the serialized string (or build the full URL with ? + clone.toString())
to API.get so you do not materialize params into a plain object.
There was a problem hiding this 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
🧹 Nitpick comments (14)
web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (7)
247-255
: Hide JWKS UI when not RS256; add CTA to open key manager.Avoid confusing UX under HS256 and provide a quick action to fix missing keys.
- {isEnabled && ( + {isEnabled && + inputs['oauth2.jwt_signing_algorithm'] === 'RS256' && ( <Button type='secondary' onClick={() => setJwksVisible(true)} size='small' > {t('密钥管理')} </Button> - )} + )}- {!keysReady && isEnabled && ( + {!keysReady && + isEnabled && + inputs['oauth2.jwt_signing_algorithm'] === 'RS256' && ( <Banner type='warning' className='!rounded-lg' closeIcon={null} description={t( '尚未准备签名密钥,建议立即初始化或轮换以发布 JWKS。签名密钥用于 JWT 令牌的安全签发。', )} + actions={ + <Button + size='small' + type='primary' + onClick={() => setJwksVisible(true)} + loading={keysLoading} + > + {t('打开密钥管理')} + </Button> + } /> )}Also applies to: 272-281
131-140
: Internationalize test messages.Wrap the success/error toasts in t() to be consistent with i18n.
- if (!silent) showSuccess('OAuth2服务器运行正常'); + if (!silent) showSuccess(t('OAuth2服务器运行正常')); ... - if (!silent) showError('OAuth2服务器测试失败'); + if (!silent) showError(t('OAuth2服务器测试失败')); ... - if (!silent) showError('OAuth2服务器连接测试失败'); + if (!silent) showError(t('OAuth2服务器连接测试失败'));
105-109
: Update local baseline after successful save.Prevents repeat “dirty” detection if parent doesn’t immediately refresh.
showSuccess(t('保存成功')); + // 同步基线,避免重复提交相同改动 + setInputsRow(structuredClone(inputs)); + if (refForm.current) { + refForm.current.setValues(inputs); + } if (props && props.refresh) { props.refresh(); }
98-105
: Prefer explicit result validation over res.includes(undefined).Use HTTP status (or backend success flag) to detect failures; current pattern is brittle and hides partial failures where a handler returns a non-undefined error-like object.
- .then((res) => { - if (requestQueue.length === 1) { - if (res.includes(undefined)) return showError(t('保存失败,请重试')); - } else if (requestQueue.length > 1) { - if (res.includes(undefined)) - return showError(t('部分保存失败,请重试')); - } + .then((res) => { + const failed = res.some((r) => !r || r.status >= 400 || r.data?.success === false); + if (failed) { + return showError( + t(requestQueue.length > 1 ? '部分保存失败,请重试' : '保存失败,请重试'), + ); + }
297-301
: Guard window access for SSR safety.If this component ever renders in a non‑browser context, this prevents a ReferenceError.
- placeholder={window.location.origin} + placeholder={ + typeof window !== 'undefined' ? window.location.origin : '' + }
171-174
: structuredClone fallback (compat).Some environments lack structuredClone; add a tiny fallback helper.
+// 安全拷贝(兼容无 structuredClone 的环境) +const clone = (obj) => + typeof structuredClone === 'function' + ? structuredClone(obj) + : JSON.parse(JSON.stringify(obj)); ... - setInputsRow(structuredClone({ ...inputs, ...currentInputs })); + setInputsRow(clone({ ...inputs, ...currentInputs })); ... - refForm.current.setValues({ ...inputs, ...currentInputs }); + refForm.current.setValues({ ...inputs, ...currentInputs });
361-381
: Algorithm‑specific hints.When HS256 is selected, consider hiding JWKS‑related help and optionally exposing a “shared secret” field (masked) to avoid misconfigurations.
web/src/i18n/locales/en.json (7)
522-535
: OAuth consent terminology and capitalization fixes (client type, response type, Unknown domain).Align terms with OAuth2/OIDC vocabulary and title‑case UI labels.
- "公开应用": "Public app", - "机密应用": "Confidential app", - "授权类型": "Response type", - "未知域": "unknown domain", + "公开应用": "Public client", + "机密应用": "Confidential client", + "授权类型": "Response Type", + "未知域": "Unknown domain",
2102-2102
: Disambiguate noun “节省”.Use “Savings” to avoid confusion with the action button “Save”.
- "节省": "Save", + "节省": "Savings",
2106-2111
: Microcopy: improve “填入示例模板”.“Fill Template Example” reads awkward in English; prefer “Use example template”.
- "填入示例模板": "Fill Template Example", + "填入示例模板": "Use example template",
2146-2149
: Title‑case capability scopes.Match UI style: “Read API”, “Write API”.
- "读取API": "read API", - "写入API": "write API", + "读取API": "Read API", + "写入API": "Write API",
2162-2164
: Consistent client‑type labels.Elsewhere you use “Public/Confidential client”; align these two items.
- "机密客户端": "Confidential", - "公开客户端": "Public", + "机密客户端": "Confidential client", + "公开客户端": "Public client",
2113-2116
: Redirect URI rule phrasing.Minor polish: keep HTTPS and localhost notes, but prefer title‑case “Redirect URI(s)”.
- "重定向URI格式不合法:仅支持https,或本地开发使用http": "Invalid Redirect URI: only HTTPS is supported, or HTTP for local development.", + "重定向URI格式不合法:仅支持https,或本地开发使用http": "Invalid Redirect URI: HTTPS only (HTTP allowed for localhost during development).",
831-831
: Remove redundant i18n key "删除失败-oauth2clients"web/src/i18n/locales/en.json:831 defines "删除失败-oauth2clients" but no code references were found; the app uses the generic "删除失败". Delete the unused key or, if you want a client-specific message, rename to "删除OAuth2客户端失败" and update OAuth2 component usages.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
web/src/components/settings/oauth2/OAuth2ServerSettings.jsx
(1 hunks)web/src/i18n/locales/en.json
(5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (2)
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (3)
useTranslation
(47-47)loading
(48-48)JWKSManagerModal
(46-398)web/src/helpers/utils.jsx (4)
compareObjects
(294-311)showWarning
(149-151)showError
(118-147)showSuccess
(153-155)
🔇 Additional comments (5)
web/src/components/settings/oauth2/OAuth2ServerSettings.jsx (2)
99-105
: Don’t swallow single‑request save failures; show an error toast.Replace the bare return so users get feedback on a failed single PUT.
- if (requestQueue.length === 1) { - if (res.includes(undefined)) return; - } else if (requestQueue.length > 1) { + if (requestQueue.length === 1) { + if (res.includes(undefined)) return showError(t('保存失败,请重试')); + } else if (requestQueue.length > 1) { if (res.includes(undefined)) return showError(t('部分保存失败,请重试')); }
239-243
: No change needed — Badge supports "tertiary".
Valid types: primary, secondary, tertiary, danger, warning, success.web/src/i18n/locales/en.json (3)
1015-1015
: Confirm generic “保存” doesn’t clash with noun “节省”.You now have two English “Save” strings: action (“保存”) and noun (“节省”, at Line 2102). Recommend disambiguating the noun where it appears as a label.
2155-2161
: Great security guidance and admin UX.Clear JWKS/rotation copy, HTTPS emphasis, TTL hints, and “secret shown once” UX are all solid.
Please ensure any links or docs referenced by this UI exist in the docs sidebar.
Also applies to: 2189-2194, 2199-2219, 2231-2247
447-447
: No duplicate "by" in footer.Footer renders t('设计与开发由') (web/src/components/layout/Footer.jsx:193). en.json defines "由":"by" (web/src/i18n/locales/en.json:447) and "设计与开发由":"Designed & Developed with love by" (web/src/i18n/locales/en.json:1456), but I found no code concatenating the two keys — no "by by" duplication.
useEffect(() => { | ||
const loadKeys = async () => { | ||
try { | ||
setKeysLoading(true); | ||
const res = await API.get('/api/oauth/keys', { | ||
skipErrorHandler: true, | ||
}); | ||
const list = res?.data?.data || []; | ||
setKeysReady(list.length > 0); | ||
} catch { | ||
setKeysReady(false); | ||
} finally { | ||
setKeysLoading(false); | ||
} | ||
}; | ||
if (inputs['oauth2.enabled']) { | ||
loadKeys(); | ||
testOAuth2(true); | ||
} else { | ||
// 禁用时清理状态,避免残留状态与不必要的请求 | ||
setKeysReady(true); | ||
setServerInfo(null); | ||
setKeysLoading(false); | ||
} | ||
}, [inputs['oauth2.enabled']]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gate JWKS checks and calls by algorithm (RS256 only).
HS256 doesn’t use JWKS; current logic will show a bogus “keys not ready” warning and make unnecessary calls. Guard by alg and expand deps.
- useEffect(() => {
+ useEffect(() => {
const loadKeys = async () => {
try {
setKeysLoading(true);
const res = await API.get('/api/oauth/keys', {
skipErrorHandler: true,
});
const list = res?.data?.data || [];
setKeysReady(list.length > 0);
} catch {
setKeysReady(false);
} finally {
setKeysLoading(false);
}
};
- if (inputs['oauth2.enabled']) {
- loadKeys();
- testOAuth2(true);
- } else {
- // 禁用时清理状态,避免残留状态与不必要的请求
- setKeysReady(true);
- setServerInfo(null);
- setKeysLoading(false);
- }
- }, [inputs['oauth2.enabled']]);
+ if (!inputs['oauth2.enabled']) {
+ // 禁用时清理状态
+ setKeysReady(true);
+ setServerInfo(null);
+ setKeysLoading(false);
+ return;
+ }
+ if (inputs['oauth2.jwt_signing_algorithm'] !== 'RS256') {
+ // HS256 不依赖 JWKS
+ setKeysReady(true);
+ setKeysLoading(false);
+ return;
+ }
+ loadKeys();
+ testOAuth2(true);
+ }, [inputs['oauth2.enabled'], inputs['oauth2.jwt_signing_algorithm']]);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
useEffect(() => { | |
const loadKeys = async () => { | |
try { | |
setKeysLoading(true); | |
const res = await API.get('/api/oauth/keys', { | |
skipErrorHandler: true, | |
}); | |
const list = res?.data?.data || []; | |
setKeysReady(list.length > 0); | |
} catch { | |
setKeysReady(false); | |
} finally { | |
setKeysLoading(false); | |
} | |
}; | |
if (inputs['oauth2.enabled']) { | |
loadKeys(); | |
testOAuth2(true); | |
} else { | |
// 禁用时清理状态,避免残留状态与不必要的请求 | |
setKeysReady(true); | |
setServerInfo(null); | |
setKeysLoading(false); | |
} | |
}, [inputs['oauth2.enabled']]); | |
useEffect(() => { | |
const loadKeys = async () => { | |
try { | |
setKeysLoading(true); | |
const res = await API.get('/api/oauth/keys', { | |
skipErrorHandler: true, | |
}); | |
const list = res?.data?.data || []; | |
setKeysReady(list.length > 0); | |
} catch { | |
setKeysReady(false); | |
} finally { | |
setKeysLoading(false); | |
} | |
}; | |
if (!inputs['oauth2.enabled']) { | |
// 禁用时清理状态 | |
setKeysReady(true); | |
setServerInfo(null); | |
setKeysLoading(false); | |
return; | |
} | |
if (inputs['oauth2.jwt_signing_algorithm'] !== 'RS256') { | |
// HS256 不依赖 JWKS | |
setKeysReady(true); | |
setKeysLoading(false); | |
return; | |
} | |
loadKeys(); | |
testOAuth2(true); | |
}, [inputs['oauth2.enabled'], inputs['oauth2.jwt_signing_algorithm']]); |
🤖 Prompt for AI Agents
In web/src/components/settings/oauth2/OAuth2ServerSettings.jsx around lines 182
to 206, the effect unconditionally loads JWKS and shows key readiness warnings
even when algorithm is HS256; change the effect to only run JWKS-related logic
when oauth2.enabled is true AND oauth2.alg === 'RS256' (HS256 should skip JWKS
checks and calls), update the dependency array to include inputs['oauth2.alg']
so the effect re-runs when algorithm changes, call loadKeys() and
testOAuth2(true) only for RS256, and when skipping (either disabled or
non-RS256) ensure you setKeysReady(true), setServerInfo(null) and
setKeysLoading(false) to clear state and avoid unnecessary network requests.
…ic and set current key Why - First-time “Initialize Keys” caused a nil pointer panic when adding the first JWK to a nil JWKS set. - As a result, the returned kid was missing and the first key appeared as “historical” until a second rotation. - Improve first-time UX: only show Key Management when the server is healthy and guide admins to the correct init flow. Backend (bug fix) - src/oauth/server.go - RotateSigningKey / GenerateAndPersistKey / ImportPEMKey: - If simpleJWKSSet is nil, create a new jwk.NewSet() before AddKey, otherwise AddKey as usual. - Ensure currentKeyID is updated; enforceKeyRetention remains unchanged. - This prevents the nil pointer panic, ensures the first key is added to JWKS, and is immediately the current key. Frontend (UX) - web/src/components/settings/oauth2/OAuth2ServerSettings.jsx - Show “Key Management” only when OAuth2 is enabled AND server is healthy (serverInfo present). - Refine the warning banner text to instruct: enable OAuth2 & SSO → Save configuration → Key Management → Initialize Keys. - web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx - Dynamic primary action in “Key List” tab: - No keys → “Initialize Keys” - Has keys → “Rotate Keys” - Simplify error handling by relying on `message` + localized fallback. Notes - No API surface changes; functional bugfix plus UI/UX improvements. - Linting passed; no new warnings. Test plan 1) Start with OAuth2 enabled and no signing keys. 2) Open “Key Management” → click “Initialize Keys”. 3) Expect: success response with new kid; table shows the new kid as Current; JWKS endpoint returns the key; no server panic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (7)
web/src/i18n/locales/en.json (7)
528-530
: Use “Client” consistently and fix OAuth terminology (“Grant type”).
- “公开应用/机密应用” should align with later usage (“Public/Confidential Client”).
- “授权类型” maps to OAuth2 Grant Type, not Response Type.
Apply:
- "公开应用": "Public app", - "机密应用": "Confidential app", - "授权类型": "Response type", + "公开应用": "Public Client", + "机密应用": "Confidential Client", + "授权类型": "Grant type",
2146-2149
: Scope labels: title case and clearer phrasing.
- Prefer “Read API” / “Write API”.
- Clarify “admin” scope description.
- "读取API": "read API", - "写入API": "write API", - "admin(管理员权限)": "admin (administrator permission)", + "读取API": "Read API", + "写入API": "Write API", + "admin(管理员权限)": "admin (administrator privileges)",
526-526
: Polish phrasing.“Unable to load authorization information at the moment” reads more naturally.
- "暂时无法加载授权信息": "Unable to load authorization information for now", + "暂时无法加载授权信息": "Unable to load authorization information at the moment",
532-532
: Capitalize sentence start.- "未知域": "unknown domain", + "未知域": "Unknown domain",
2104-2104
: Wording tweak.“Insert example template” is clearer.
- "填入示例模板": "Fill Template Example", + "填入示例模板": "Insert example template",
2189-2258
: LGTM: OAuth2 Server/JWKS management copy. Minor optional clarity.Consider mentioning “kid (Key ID)” once to aid users unfamiliar with the term.
- "JWT密钥ID": "JWT key ID", + "JWT密钥ID": "JWT key ID (kid)",
831-833
: Key naming consistency for delete failures.New namespaced key “删除失败-oauth2clients” diverges from existing pattern. If intentional to avoid collisions, OK; otherwise consider a neutral key plus context at call sites.
Would you prefer a generic key (e.g., “删除失败”) with contextual UI composition?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/oauth/server.go
(1 hunks)web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx
(1 hunks)web/src/i18n/locales/en.json
(5 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (2)
web/src/components/common/ui/ResponsiveModal.jsx (1)
ResponsiveModal
(36-103)web/src/helpers/utils.jsx (2)
showError
(118-147)showSuccess
(153-155)
src/oauth/server.go (7)
setting/system_setting/oauth2.go (2)
GetOAuth2Settings
(49-51)OAuth2Settings
(5-21)common/sys_log.go (1)
SysLog
(10-13)common/utils.go (2)
GenerateKey
(190-193)GetUUID
(155-159)controller/oauth.go (1)
GetJWKS
(19-58)model/oauth_client.go (1)
GetOAuthClientByID
(142-146)model/user.go (1)
GetUserById
(287-299)logger/logger.go (1)
LogInfo
(55-57)
🔇 Additional comments (17)
web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (6)
19-36
: Standard React imports and component structure look clean.Clean import structure and proper usage of Semi-UI components for the OAuth2 key management interface.
110-130
: Silent error handling in server-info fetch degrades observability.The past review comment correctly identified this issue. The empty catch block on Line 128 silently swallows network or parsing errors when fetching the default private key path from
/api/oauth/server-info
. This makes troubleshooting difficult since failures are invisible to users and developers.Apply this diff to add proper error handling:
useEffect(() => { if (!visible) return; (async () => { try { const res = await API.get('/api/oauth/server-info'); const p = res?.data?.default_private_key_path; if (p) setGenPath(p); - } catch {} + } catch (error) { + // Log error but don't show user notification since this is optional enhancement + console.warn('Failed to fetch server-info for default path:', error); + } })(); }, [visible]);
326-336
: LGTM! TextArea configuration is correctly implemented.The
rows={8}
prop is properly used to set the initial height of the textarea to 8 visible lines. The PEM private key placeholder is appropriate and the field configuration supports proper multi-line text input.
233-281
: Good dynamic header actions pattern with clear tab-based logic.The
getHeaderActions()
function properly adapts the modal header based on the active tab, providing contextually relevant actions (refresh/rotate for view, import for import tab, generate for generate tab).
371-399
: Excellent responsive modal integration with proper tab structure.The component effectively uses
ResponsiveModal
with appropriate width configuration and integrates well with the tabbed interface for different JWKS management operations.
52-63
: Replace literal Chinese i18n string with a stable translation keyt('获取密钥列表失败') is used in web/src/components/settings/oauth2/modals/JWKSManagerModal.jsx (lines 57, 59); replace with a namespaced key (e.g. "oauth.jwks.fetch_keys_failed"), add the entry to all locale files (zh-CN, en-US, etc.), and run a repo-wide search to ensure no other raw-language keys are used.
src/oauth/server.go (8)
29-34
: Critical: Thread-safety issues with global maps remain unaddressed.The existing review comment correctly identified that global maps
signingKeys
andkeyMeta
are accessed and modified from multiple goroutines without synchronization, causing data races. This is a serious concurrency bug that can lead to corrupted state.Apply this diff to add thread safety:
+import "sync" + var ( signingKeys = map[string]*rsa.PrivateKey{} currentKeyID string simpleJWKSSet jwk.Set keyMeta = map[string]int64{} // kid -> created_at (unix) + keyMutex sync.RWMutex )Then protect all map accesses throughout the file with
keyMutex.Lock()
/Unlock()
for writes andkeyMutex.RLock()
/RUnlock()
for reads.
67-94
: Critical: Race conditions in EnsureInitialized() function.The existing review comment correctly identified that multiple goroutines could pass the initial checks and attempt to initialize concurrently, leading to race conditions when modifying global maps.
Apply this diff to add proper synchronization:
+var initMutex sync.Mutex + func EnsureInitialized() error { + initMutex.Lock() + defer initMutex.Unlock() + settings := system_setting.GetOAuth2Settings() if !settings.Enabled { return nil } if len(signingKeys) > 0 && simpleJWKSSet != nil && currentKeyID != "" { return nil }
135-162
: Key rotation logic is sound but needs thread protection.The
RotateSigningKey
function correctly generates a new RSA key, updates the JWKS, and enforces key retention policies. However, it requires the thread-safety fixes mentioned in previous comments to prevent data races during concurrent access.The approval is contingent on implementing the thread-safety measures identified in the duplicate comments above.
279-336
: Comprehensive PEM import with good format support.The
ImportPEMKey
function properly handles both PKCS#1 and PKCS#8 PEM formats, includes proper error handling for malformed keys, and correctly updates the JWKS set. The logic is robust and handles edge cases well.This approval is contingent on implementing the thread-safety measures for the global maps.
364-423
: Client credentials flow implementation follows OAuth2 spec correctly.The
handleClientCredentials
function properly validates client authentication, checks grant type permissions, validates scopes, and issues JWT tokens with appropriate claims. The implementation correctly handles confidential vs. public client types.
649-706
: JWT token signing implementation is secure and well-structured.The
signAccessToken
function correctly constructs JWT claims with proper issuer derivation, handles subject/audience appropriately, and uses RS256 signing with the current key. The implementation follows JWT best practices.
765-1017
: Authorization code flow handles PKCE and security requirements properly.The
HandleAuthorizeRequest
function implements a comprehensive OAuth2 authorization flow with proper PKCE validation, state management, scope checking, and session handling. The loopback redirect handling follows RFC 8252 guidelines for native apps.
1028-1070
: Excellent loopback redirect validation for native apps.The
isLoopbackRedirectAllowed
function properly implements RFC 8252 guidance for OAuth2 native apps, allowing dynamic ports on loopback addresses while maintaining security. This is a thoughtful implementation of the standard.web/src/i18n/locales/en.json (3)
447-449
: Footer composition check for “由”.Changing “由” → “by” may read awkwardly in combined phrases (e.g., “设计与开发由 …”). Please confirm UI renders natural English in all footer variants.
2109-2115
: LGTM: Redirect URI guidance and validations.HTTPS requirement and localhost exception are accurate and user-friendly.
1015-1015
: Add missing "保存" key to other locales; no duplicate keys detected.web/src/i18n/locales/en.json:1015 contains "保存": "Save". web/src/i18n/locales/zh.json does not contain this key — add matching entries/translations to all locale files.
…`next` and redirect unauthenticated users to consent Problem - Starting OAuth from Discourse hit GET /api/oauth/authorize and 302’d to /login?next=/oauth/consent… - The login page and AuthRedirect always navigated to /console when a session existed, ignoring next, which aborted the OAuth flow and dropped users in the console. Changes - Backend (src/oauth/server.go) - When not logged in, redirect directly to /oauth/consent?<original_query> instead of /login?next=… - Keep no-store headers; preserve the original authorize querystring. - Frontend - web/src/helpers/auth.jsx: AuthRedirect now honors the login page’s next query param and only redirects to safe internal paths (starts with “/”, not “//”); otherwise falls back to /console. - web/src/components/auth/LoginForm.jsx: After successful login and after 2FA success, navigate to next when present and safe; otherwise go to /console. Result - The OAuth authorize flow now reliably reaches the consent screen. - On approval, the server issues an authorization code and 302’s back to the client’s redirect_uri (e.g., Discourse), completing SSO as expected. Security - Sanitize next to avoid open-redirects by allowing only same-origin internal paths. Compatibility - No behavior change for normal username/password sign-ins outside the OAuth flow. - No changes to token/userinfo endpoints. Testing - Manually verified end-to-end with Discourse OAuth2 Basic: - authorize → consent → approve → redirect with code - Lint checks pass for modified files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
web/src/helpers/auth.jsx (1)
39-43
: Harden safe-redirect and dedupe usage across appGood safe-internal check. Consider validating via URL origin to be future-proof and export a small helper so LoginForm reuses the same logic.
Apply this diff here:
- // 优先使用登录页上的 next 参数(仅允许站内相对路径) - const sp = new URLSearchParams(window.location.search); - const next = sp.get('next'); - const isSafeInternalPath = next && next.startsWith('/') && !next.startsWith('//'); - return <Navigate to={isSafeInternalPath ? next : '/console'} replace />; + // 优先使用登录页上的 next 参数(仅允许同源相对路径) + const sp = new URLSearchParams(window.location.search); + const rawNext = sp.get('next'); + let target = '/console'; + if (rawNext && rawNext.startsWith('/') && !rawNext.startsWith('//')) { + try { + const u = new URL(rawNext, window.location.origin); + if (u.origin === window.location.origin) { + target = u.pathname + u.search + u.hash; + } + } catch (e) { + // ignore parse errors and fall back + } + } + return <Navigate to={target} replace />;Optionally, export a helper here to avoid duplication elsewhere:
export const resolveSafeNext = (search) => { const sp = new URLSearchParams(search); const rawNext = sp.get('next'); if (rawNext && rawNext.startsWith('/') && !rawNext.startsWith('//')) { try { const u = new URL(rawNext, window.location.origin); if (u.origin === window.location.origin) { return u.pathname + u.search + u.hash; } } catch {} } return '/console'; };web/src/components/auth/LoginForm.jsx (2)
179-184
: Use useSearchParams and replace history on post-login redirectReuse the existing searchParams, and replace history to avoid leaving the login page in the back stack.
- // 优先跳回 next(仅允许相对路径) - const sp = new URLSearchParams(window.location.search); - const next = sp.get('next'); - const isSafeInternalPath = next && next.startsWith('/') && !next.startsWith('//'); - navigate(isSafeInternalPath ? next : '/console'); + // 优先跳回 next(仅允许站内相对路径) + const next = searchParams.get('next'); + const isSafeInternalPath = next && next.startsWith('/') && !next.startsWith('//'); + navigate(isSafeInternalPath ? next : '/console', { replace: true });Optional: import and use a shared resolver (e.g., resolveSafeNext) from helpers/auth to eliminate duplication.
293-297
: DRY the safe-redirect logic for 2FA success and align with email loginSame suggestion here: use searchParams and replace history.
- const sp = new URLSearchParams(window.location.search); - const next = sp.get('next'); - const isSafeInternalPath = next && next.startsWith('/') && !next.startsWith('//'); - navigate(isSafeInternalPath ? next : '/console'); + const next = searchParams.get('next'); + const isSafeInternalPath = next && next.startsWith('/') && !next.startsWith('//'); + navigate(isSafeInternalPath ? next : '/console', { replace: true });Optional follow-up: consider applying the same safe-redirect to WeChat/Telegram success paths for consistency.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/oauth/server.go
(1 hunks)web/src/components/auth/LoginForm.jsx
(2 hunks)web/src/helpers/auth.jsx
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/oauth/server.go
Summary by CodeRabbit