Skip to content

feat: support configurable custom OAuth PKCE - #6620

Open
cosohuang wants to merge 1 commit into
QuantumNous:mainfrom
cosohuang:feat/custom-oauth-pkce
Open

feat: support configurable custom OAuth PKCE#6620
cosohuang wants to merge 1 commit into
QuantumNous:mainfrom
cosohuang:feat/custom-oauth-pkce

Conversation

@cosohuang

@cosohuang cosohuang commented Aug 3, 2026

Copy link
Copy Markdown

Summary

  • 为 Custom OAuth Provider 增加数据库驱动的 pkce_enabled 配置,统一使用 OAuth Authorization Code + PKCE S256。
  • OAuth flow API 返回服务端生成的 flow token、code challenge 和 method;Token Exchange 从请求上下文读取 verifier,不把 verifier 写入数据库。
  • 前端授权 URL、绑定弹窗和账号绑定 UI 支持 PKCE 与可解绑的自定义 Provider;保留已配置 Provider 的非 PKCE 兼容路径。
  • 增加配置校验、Token Exchange、前端授权 URL 回归测试及多语言文案。

Verification

  • go test ./controller ./oauth ./model
  • bun test src/features/auth/lib/__tests__/custom-oauth-url.test.ts
  • bun run typecheck (in web/)
  • bun run build:check (in web/)

该 PR 从 cosohuang/new-api fork 提交,因为当前账号没有上游仓库直接写权限。

Summary by CodeRabbit

  • New Features

    • Added optional PKCE (S256) protection for custom OAuth providers.
    • Added a PKCE toggle to the custom OAuth provider settings form.
    • OAuth login and account binding now support PKCE-enabled authorization flows.
    • Added localized PKCE labels and explanatory text.
  • Bug Fixes

    • Improved OAuth security by validating PKCE data and preventing sensitive authorization details from appearing in logs.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Custom OAuth providers now support persisted PKCE configuration. OAuth login and account binding flows exchange structured S256 challenge data, store verifiers server-side, and submit them during token exchange. The frontend validates flow responses and builds provider URLs with conditional PKCE parameters.

Changes

Custom OAuth PKCE

Layer / File(s) Summary
Provider configuration and API wiring
model/custom_oauth_provider.go, controller/custom_oauth.go, controller/misc.go, web/src/features/system-settings/auth/custom-oauth/*, web/src/i18n/locales/*
Providers persist pkce_enabled, expose it through APIs, and provide a localized S256 toggle in the settings form. Validation tests cover defaults, preservation, and invalid access-policy operators.
Backend PKCE generation and exchange
controller/oauth.go, oauth/provider.go, oauth/generic.go, controller/auth_flow_test.go, oauth/generic_test.go
The backend generates S256 challenges, stores flow verifiers in request context, and sends them during token exchange. Tests cover generated challenges, verifier persistence boundaries, successful exchange, and missing-verifier errors. OAuth logs no longer include authorization codes or response bodies.
Frontend flow contract and URL construction
web/src/features/auth/api.ts, web/src/features/auth/types.ts, web/src/features/auth/lib/oauth.ts, web/src/features/auth/hooks/use-oauth-login.ts, web/src/features/auth/lib/__tests__/custom-oauth-url.test.ts
The frontend validates structured OAuth flow responses and builds custom-provider URLs with PKCE parameters only when enabled.
Account binding integration
web/src/features/profile/components/tabs/account-bindings-tab.tsx
Account binding uses the flow token for popup and pending state, then adds the server-provided S256 parameters to custom OAuth authorization URLs.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant OAuthFlowAPI
  participant OAuthFlowController
  participant OAuthProvider
  participant TokenEndpoint
  Browser->>OAuthFlowAPI: Request OAuth flow details
  OAuthFlowAPI->>OAuthFlowController: Create flow and PKCE challenge
  OAuthFlowController-->>OAuthFlowAPI: Return flow token and S256 challenge
  Browser->>OAuthProvider: Authorize with state and challenge
  OAuthProvider-->>OAuthFlowAPI: Return authorization code
  OAuthFlowAPI->>OAuthProvider: Exchange code with stored verifier
  OAuthProvider->>TokenEndpoint: Submit code and PKCE verifier
  TokenEndpoint-->>OAuthProvider: Return access token
Loading

Possibly related PRs

Poem

A rabbit hops through OAuth’s gate,
With S256 keys to validate.
The challenge flies, the code returns,
The verifier waits where safety learns.
“PKCE!” cries Bun, “the flow is bright!”
And bindings thump into the night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: configurable PKCE support for custom OAuth providers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
web/src/features/profile/components/tabs/account-bindings-tab.tsx (1)

157-229: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add account-binding OAuth coverage for custom providers.

The PKCE coverage in web/src/features/auth/lib/__tests__/custom-oauth-url.test.ts only covers buildCustomOAuthUrl. It does not cover account-bindings-tab.tsx bindings or handleBindCustomOAuth calling createOAuthFlowDetails(..., 'bind') and building URLs with PKCE and non-PKCE providers. Add component/module tests for the binding callback, including PKCE and pkce_enabled: false cases.

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

In `@web/src/features/profile/components/tabs/account-bindings-tab.tsx` around
lines 157 - 229, Add component or module tests covering handleBindCustomOAuth
and startOAuthBinding for custom providers. Verify createOAuthFlowDetails is
called with the provider slug and 'bind', and that both PKCE-enabled and
pkce_enabled: false providers produce the expected OAuth URL parameters and
popup navigation behavior.

Source: Coding guidelines

🧹 Nitpick comments (2)
model/custom_oauth_provider_test.go (1)

24-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use assert for independent default checks.

Keep require.NoError on Line 24 because it is a prerequisite. The comparisons on Lines 25-31 are independent output checks. Use testify/assert for them instead of require so one mismatch does not stop the remaining checks.

Proposed assertion update
 import (
 	"testing"

+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@
-	require.Equal(t, "example-sso", provider.Slug)
-	require.Equal(t, "openid profile email", provider.Scopes)
-	require.Equal(t, "sub", provider.UserIdField)
-	require.Equal(t, "preferred_username", provider.UsernameField)
-	require.Equal(t, "name", provider.DisplayNameField)
-	require.Equal(t, "email", provider.EmailField)
-	require.True(t, provider.PKCEEnabled)
+	assert.Equal(t, "example-sso", provider.Slug)
+	assert.Equal(t, "openid profile email", provider.Scopes)
+	assert.Equal(t, "sub", provider.UserIdField)
+	assert.Equal(t, "preferred_username", provider.UsernameField)
+	assert.Equal(t, "name", provider.DisplayNameField)
+	assert.Equal(t, "email", provider.EmailField)
+	assert.True(t, provider.PKCEEnabled)

As per coding guidelines: “New or substantially rewritten tests must use testify/require for setup and fatal assertions and testify/assert for non-fatal checks.”

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

In `@model/custom_oauth_provider_test.go` around lines 24 - 31, Keep
require.NoError for the validateCustomOAuthProvider prerequisite, and change the
independent provider field checks for Slug, Scopes, UserIdField, UsernameField,
DisplayNameField, EmailField, and PKCEEnabled to use testify/assert so all
default-value mismatches are reported.

Source: Coding guidelines

oauth/generic_test.go (1)

41-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use assert for non-fatal result checks.

Line 42 uses require.NoError correctly as a fatal setup check. Lines 43-46 check independent response fields (token.AccessToken, received.Get("code_verifier"), received.Get("code"), received.Get("redirect_uri")). Use assert.Equal for these, so a mismatch in one field does not hide mismatches in the others.

♻️ Proposed fix
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )

 ...
 	token, err := provider.ExchangeToken(context.Background(), "authorization-code", c)
 	require.NoError(t, err)
-	require.Equal(t, "access-token", token.AccessToken)
-	require.Equal(t, "verifier-value", received.Get("code_verifier"))
-	require.Equal(t, "authorization-code", received.Get("code"))
-	require.Equal(t, "https://dashboard.example.test/oauth/example-sso", received.Get("redirect_uri"))
+	assert.Equal(t, "access-token", token.AccessToken)
+	assert.Equal(t, "verifier-value", received.Get("code_verifier"))
+	assert.Equal(t, "authorization-code", received.Get("code"))
+	assert.Equal(t, "https://dashboard.example.test/oauth/example-sso", received.Get("redirect_uri"))

As per coding guidelines: "New or substantially rewritten tests must use testify/require for setup and fatal assertions and testify/assert for non-fatal checks."

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

In `@oauth/generic_test.go` around lines 41 - 47, In the ExchangeToken test, keep
require.NoError for the fatal error check, but change the independent field
validations for token.AccessToken and the received code_verifier, code, and
redirect_uri values to assert.Equal so all mismatches are reported.

Source: Coding guidelines

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

Inline comments:
In `@model/custom_oauth_provider_test.go`:
- Around line 12-17: Update the validCustomOAuthProvider defaults test fixture
to use an empty Slug so default slug generation is exercised, and add a separate
provider case with PKCEEnabled set to false. Assert both behaviors, preserving
the existing enabled-PKCE and normalized-slug coverage while protecting the
non-PKCE compatibility path.

In `@web/src/i18n/locales/ja.json`:
- Around line 3249-3250: Update the Japanese translation for “Require a proof
key for authorization code exchanges” in the locale resource to use a complete
setting label that explicitly states the proof key is required during
authorization code exchanges, such as the wording requested in the review.

---

Outside diff comments:
In `@web/src/features/profile/components/tabs/account-bindings-tab.tsx`:
- Around line 157-229: Add component or module tests covering
handleBindCustomOAuth and startOAuthBinding for custom providers. Verify
createOAuthFlowDetails is called with the provider slug and 'bind', and that
both PKCE-enabled and pkce_enabled: false providers produce the expected OAuth
URL parameters and popup navigation behavior.

---

Nitpick comments:
In `@model/custom_oauth_provider_test.go`:
- Around line 24-31: Keep require.NoError for the validateCustomOAuthProvider
prerequisite, and change the independent provider field checks for Slug, Scopes,
UserIdField, UsernameField, DisplayNameField, EmailField, and PKCEEnabled to use
testify/assert so all default-value mismatches are reported.

In `@oauth/generic_test.go`:
- Around line 41-47: In the ExchangeToken test, keep require.NoError for the
fatal error check, but change the independent field validations for
token.AccessToken and the received code_verifier, code, and redirect_uri values
to assert.Equal so all mismatches are reported.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f709c800-5932-4144-88b4-c4b0ebca6f3d

📥 Commits

Reviewing files that changed from the base of the PR and between 0ab0202 and 40db006.

📒 Files selected for processing (24)
  • controller/auth_flow_test.go
  • controller/custom_oauth.go
  • controller/misc.go
  • controller/oauth.go
  • model/custom_oauth_provider.go
  • model/custom_oauth_provider_test.go
  • oauth/generic.go
  • oauth/generic_test.go
  • oauth/provider.go
  • web/src/features/auth/api.ts
  • web/src/features/auth/hooks/use-oauth-login.ts
  • web/src/features/auth/lib/__tests__/custom-oauth-url.test.ts
  • web/src/features/auth/lib/oauth.ts
  • web/src/features/auth/types.ts
  • web/src/features/profile/components/tabs/account-bindings-tab.tsx
  • web/src/features/system-settings/auth/custom-oauth/components/provider-form-dialog.tsx
  • web/src/features/system-settings/auth/custom-oauth/types.ts
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/i18n/locales/zh.json

Comment on lines +12 to +17
Slug: "Example-SSO",
ClientId: "client-id",
AuthorizationEndpoint: "https://sso.example.test/authorize",
TokenEndpoint: "https://sso.example.test/token",
UserInfoEndpoint: "https://sso.example.test/userinfo",
PKCEEnabled: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise the empty-slug and disabled-PKCE paths.

validCustomOAuthProvider sets Slug to "Example-SSO" at Line 12 and PKCEEnabled to true at Line 17. The defaults test therefore checks slug normalization and enabled-PKCE preservation. It does not fail if empty-slug defaulting breaks or disabled-PKCE providers are changed to PKCE. Set Slug to an empty value for the defaults case and add a case with PKCEEnabled == false.

The PR objective requires preserving the non-PKCE compatibility path.

Proposed test coverage
 func TestValidateCustomOAuthProviderAppliesDefaults(t *testing.T) {
 	provider := validCustomOAuthProvider()
+	provider.Slug = ""
 
 	require.NoError(t, validateCustomOAuthProvider(provider))
 	require.Equal(t, "example-sso", provider.Slug)
 }
+
+func TestValidateCustomOAuthProviderPreservesDisabledPKCE(t *testing.T) {
+	provider := validCustomOAuthProvider()
+	provider.PKCEEnabled = false
+
+	require.NoError(t, validateCustomOAuthProvider(provider))
+	require.False(t, provider.PKCEEnabled)
+}

As per coding guidelines: “Backend tests must protect real behavior, API contracts, billing/accounting invariants, compatibility, or regression paths.”

Also applies to: 21-31

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

In `@model/custom_oauth_provider_test.go` around lines 12 - 17, Update the
validCustomOAuthProvider defaults test fixture to use an empty Slug so default
slug generation is exercised, and add a separate provider case with PKCEEnabled
set to false. Assert both behaviors, preserving the existing enabled-PKCE and
normalized-slug coverage while protecting the non-PKCE compatibility path.

Source: Coding guidelines

Comment on lines +3249 to +3250
"PKCE (S256)": "PKCE(S256)",
"Require a proof key for authorization code exchanges": "認可コード交換に証明鍵を要求",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a complete Japanese setting label.

Line 3250 reads as a fragment and is less clear for Japanese administrators. Use wording that states the requirement explicitly, such as 認可コード交換時に証明鍵を必須にする.

Proposed fix
-    "Require a proof key for authorization code exchanges": "認可コード交換に証明鍵を要求",
+    "Require a proof key for authorization code exchanges": "認可コード交換時に証明鍵を必須にする",
📝 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.

Suggested change
"PKCE (S256)": "PKCE(S256)",
"Require a proof key for authorization code exchanges": "認可コード交換に証明鍵を要求",
"PKCE (S256)": "PKCE(S256)",
"Require a proof key for authorization code exchanges": "認可コード交換時に証明鍵を必須にする",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/i18n/locales/ja.json` around lines 3249 - 3250, Update the Japanese
translation for “Require a proof key for authorization code exchanges” in the
locale resource to use a complete setting label that explicitly states the proof
key is required during authorization code exchanges, such as the wording
requested in the review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants