-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Feature/7137/gitea sso #7940
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: preview
Are you sure you want to change the base?
Feature/7137/gitea sso #7940
Conversation
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.
Pull Request Overview
This PR adds Gitea SSO (Single Sign-On) authentication support to the application, implementing a new OAuth provider alongside existing providers like Google, GitHub, and GitLab. The implementation includes both frontend components for configuration and backend authentication handling.
Key changes:
- Added Gitea OAuth provider with complete authentication flow
- Extended type definitions and configuration management for Gitea authentication
- Added admin UI components for Gitea SSO configuration
- Integrated Gitea authentication options in the login interface
Reviewed Changes
Copilot reviewed 17 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
File | Description |
---|---|
packages/types/src/instance/base.ts | Added is_gitea_enabled flag to instance configuration interface |
packages/types/src/instance/auth.ts | Extended authentication types to include Gitea configuration keys |
apps/space/core/components/account/auth-forms/auth-root.tsx | Added Gitea login button and OAuth flow integration |
apps/api/plane/license/management/commands/configure_instance.py | Added Gitea configuration setup in instance management command |
apps/api/plane/license/api/views/instance.py | Extended instance API to expose Gitea configuration status |
apps/api/plane/authentication/views/space/gitea.py | Implemented Gitea OAuth endpoints for space authentication |
apps/api/plane/authentication/views/app/gitea.py | Implemented Gitea OAuth endpoints for app authentication |
apps/api/plane/authentication/views/init.py | Added Gitea view imports |
apps/api/plane/authentication/urls.py | Added Gitea OAuth URL patterns |
apps/api/plane/authentication/provider/oauth/gitea.py | Core Gitea OAuth provider implementation |
apps/api/plane/authentication/adapter/oauth.py | Extended OAuth adapter to handle Gitea provider |
apps/api/plane/authentication/adapter/error.py | Added Gitea-specific error codes |
apps/admin/core/components/authentication/gitea-config.tsx | Gitea configuration toggle component |
apps/admin/ce/components/authentication/authentication-modes.tsx | Added Gitea to authentication modes list |
apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx | Gitea authentication configuration page |
apps/admin/app/(all)/(dashboard)/authentication/gitea/layout.tsx | Layout wrapper for Gitea configuration |
apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx | Detailed Gitea configuration form component |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
import GithubDarkLogo from "/public/logos/github-dark.svg"; | ||
import GitlabLogo from "/public/logos/gitlab-logo.svg"; | ||
import GoogleLogo from "/public/logos/google-logo.svg"; | ||
import GiteaLogo from "/public/logos/gitea-logo.svg" |
Copilot
AI
Oct 10, 2025
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 semicolon at the end of import statement.
import GiteaLogo from "/public/logos/gitea-logo.svg" | |
import GiteaLogo from "/public/logos/gitea-logo.svg"; |
Copilot uses AI. Check for mistakes.
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds end-to-end Gitea OAuth support: admin UI to enable/configure Gitea, types/config flags, space app login option, API provider with OAuth flows (initiate/callback) for app and space scopes, error codes, and URL wiring. Includes a dedicated config form, layout/page, and server-side instance configuration handling. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Admin
participant AdminUI as Admin UI
participant API as API (Instance Config)
Admin->>AdminUI: Open Gitea auth page
AdminUI->>API: GET instance configurations
API-->>AdminUI: Config (IS_GITEA_ENABLED, GITEA_* values)
Admin->>AdminUI: Edit + Save Gitea config / Toggle enable
AdminUI->>API: PATCH updateInstanceConfigurations
API-->>AdminUI: Updated config
AdminUI-->>Admin: Toast: success
note over AdminUI,API: IS_GITEA_ENABLED controls visibility in Space App
sequenceDiagram
autonumber
actor User
participant SpaceApp as Space App
participant AuthAPI as API /auth (Gitea)
participant Gitea as Gitea Server
User->>SpaceApp: Click "Continue with Gitea"
SpaceApp->>AuthAPI: GET /auth/gitea (initiate) [+next_path]
AuthAPI->>AuthAPI: Create state, persist session
AuthAPI-->>User: 302 Redirect to Gitea authorize
User->>Gitea: GET /login/oauth/authorize?state=...
Gitea-->>User: 302 Redirect with code&state
User->>AuthAPI: GET /auth/gitea/callback?code&state
AuthAPI->>Gitea: POST /login/oauth/access_token (code)
Gitea-->>AuthAPI: Token JSON
AuthAPI->>Gitea: GET /api/v1/user (+ /emails if needed)
Gitea-->>AuthAPI: User info (+ email)
AuthAPI->>AuthAPI: Create/lookup user, login
AuthAPI-->>User: 302 Redirect to next_path or default
alt Error
AuthAPI-->>User: 302 Redirect with error=GITEA_* code
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (3 warnings)
✨ Finishing touches
🧪 Generate unit tests (beta)
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: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/api/plane/license/management/commands/configure_instance.py (1)
230-351
: Critical: IS_GITEA_ENABLED may never be created on existing instances (global exists() gate).The check
if not InstanceConfiguration.objects.filter(key__in=keys).exists():
short-circuits when any one key exists, so newly introduced flags like IS_GITEA_ENABLED won’t be created on upgraded instances. Use per-key existence checks.Also recommend defaulting GITEA_HOST to https://gitea.com for consistency with the admin UI and documentation.
Apply this diff to switch to per-key creation and set a sane default for GITEA_HOST in the Gitea branch:
- keys = ["IS_GOOGLE_ENABLED", "IS_GITHUB_ENABLED", "IS_GITLAB_ENABLED", "IS_GITEA_ENABLED"] - if not InstanceConfiguration.objects.filter(key__in=keys).exists(): - for key in keys: + keys = ["IS_GOOGLE_ENABLED", "IS_GITHUB_ENABLED", "IS_GITLAB_ENABLED", "IS_GITEA_ENABLED"] + for key in keys: + if InstanceConfiguration.objects.filter(key=key).exists(): + self.stdout.write(self.style.WARNING(f"{key} configuration already exists")) + continue if key == "IS_GOOGLE_ENABLED": GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET = get_configuration_value( [ { "key": "GOOGLE_CLIENT_ID", "default": os.environ.get("GOOGLE_CLIENT_ID", ""), }, { "key": "GOOGLE_CLIENT_SECRET", "default": os.environ.get("GOOGLE_CLIENT_SECRET", "0"), }, ] ) if bool(GOOGLE_CLIENT_ID) and bool(GOOGLE_CLIENT_SECRET): value = "1" else: value = "0" InstanceConfiguration.objects.create( key=key, value=value, category="AUTHENTICATION", is_encrypted=False, ) self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable.")) - if key == "IS_GITHUB_ENABLED": + elif key == "IS_GITHUB_ENABLED": GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET = get_configuration_value( [ { "key": "GITHUB_CLIENT_ID", "default": os.environ.get("GITHUB_CLIENT_ID", ""), }, { "key": "GITHUB_CLIENT_SECRET", "default": os.environ.get("GITHUB_CLIENT_SECRET", "0"), }, ] ) if bool(GITHUB_CLIENT_ID) and bool(GITHUB_CLIENT_SECRET): value = "1" else: value = "0" InstanceConfiguration.objects.create( - key="IS_GITHUB_ENABLED", + key="IS_GITHUB_ENABLED", value=value, category="AUTHENTICATION", is_encrypted=False, ) self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable.")) - if key == "IS_GITLAB_ENABLED": + elif key == "IS_GITLAB_ENABLED": GITLAB_HOST, GITLAB_CLIENT_ID, GITLAB_CLIENT_SECRET = get_configuration_value( [ { "key": "GITLAB_HOST", "default": os.environ.get("GITLAB_HOST", "https://gitlab.com"), }, { "key": "GITLAB_CLIENT_ID", "default": os.environ.get("GITLAB_CLIENT_ID", ""), }, { "key": "GITLAB_CLIENT_SECRET", "default": os.environ.get("GITLAB_CLIENT_SECRET", ""), }, ] ) if bool(GITLAB_HOST) and bool(GITLAB_CLIENT_ID) and bool(GITLAB_CLIENT_SECRET): value = "1" else: value = "0" InstanceConfiguration.objects.create( - key="IS_GITLAB_ENABLED", + key="IS_GITLAB_ENABLED", value=value, category="AUTHENTICATION", is_encrypted=False, ) self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable.")) - if key == "IS_GITEA_ENABLED": + elif key == "IS_GITEA_ENABLED": GITEA_HOST, GITEA_CLIENT_ID, GITEA_CLIENT_SECRET = ( get_configuration_value( [ { "key": "GITEA_HOST", - "default": os.environ.get( - "GITEA_HOST", "" - ), + "default": os.environ.get("GITEA_HOST", "https://gitea.com"), }, { "key": "GITEA_CLIENT_ID", "default": os.environ.get("GITEA_CLIENT_ID", ""), }, { "key": "GITEA_CLIENT_SECRET", "default": os.environ.get( "GITEA_CLIENT_SECRET", "" ), }, ] ) ) if ( bool(GITEA_HOST) and bool(GITEA_CLIENT_ID) and bool(GITEA_CLIENT_SECRET) ): value = "1" else: value = "0" InstanceConfiguration.objects.create( key="IS_GITEA_ENABLED", value=value, category="AUTHENTICATION", is_encrypted=False, ) self.stdout.write( self.style.SUCCESS( f"{key} loaded with value from environment variable." ) ) - else: - for key in keys: - self.stdout.write(self.style.WARNING(f"{key} configuration already exists"))
🧹 Nitpick comments (8)
apps/space/core/components/account/auth-forms/auth-root.tsx (1)
196-204
: Consider adding a trailing comma for consistency.The Gitea OAuth configuration object is correctly structured and follows the pattern of other OAuth providers. However, for consistency with the other OAuth entries (Google, GitHub, GitLab) and to follow modern JavaScript/TypeScript best practices, consider adding a trailing comma after the closing brace.
Apply this diff to add the trailing comma:
{ id: "gitea", text: `${content} with Gitea`, icon: <Image src={GiteaLogo} height={18} width={18} alt="Gitea Logo" />, onClick: () => { window.location.assign(`${API_BASE_URL}/auth/gitea/${next_path ? `?next_path=${next_path}` : ``}`); }, enabled: config?.is_gitea_enabled - } + }, ];apps/api/plane/license/management/commands/configure_instance.py (1)
197-201
: Set a sensible default for GITEA_HOST.Default GITEA_HOST to https://gitea.com to match the admin UI and docs.
- { - "key": "GITEA_HOST", - "value": os.environ.get("GITEA_HOST"), - "category": "GITEA", - "is_encrypted": False, - }, + { + "key": "GITEA_HOST", + "value": os.environ.get("GITEA_HOST", "https://gitea.com"), + "category": "GITEA", + "is_encrypted": False, + },apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx (1)
139-142
: Reset GITEA_HOST to default after save (team convention).Reset host to https://gitea.com post-submit instead of keeping the submitted value.
Based on learnings
- reset({ - GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value, - GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value, - }); + reset({ + GITEA_HOST: "https://gitea.com", + GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value, + GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value, + });apps/admin/core/components/authentication/gitea-config.tsx (1)
35-44
: Simplify toggle logic; avoid parseInt on empty strings.Direct string comparison is clearer and avoids NaN edge cases.
- <ToggleSwitch - value={Boolean(parseInt(GiteaConfig))} - onChange={() => { - Boolean(parseInt(GiteaConfig)) === true - ? updateConfig("IS_GITEA_ENABLED", "0") - : updateConfig("IS_GITEA_ENABLED", "1"); - }} + <ToggleSwitch + value={GiteaConfig === "1"} + onChange={() => updateConfig("IS_GITEA_ENABLED", GiteaConfig === "1" ? "0" : "1")} size="sm" disabled={disabled} />apps/api/plane/authentication/views/app/gitea.py (1)
97-103
: Clear OAuth session artifacts after successful auth.Remove state/next_path/host from session to avoid reuse or leakage.
- if next_path: + if next_path: path = str(validate_next_path(next_path)) else: path = get_redirection_path(user=user) - # redirect to referer path + # Clear transient session data + request.session.pop("state", None) + request.session.pop("next_path", None) + request.session.pop("host", None) + # redirect to referer path url = f"{host}{path}"apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx (2)
52-59
: Use finally to simplify submission state handling.Avoid duplicated setIsSubmitting calls.
- await updateConfigPromise - .then(() => { - setIsSubmitting(false); - }) - .catch((err) => { - console.error(err); - setIsSubmitting(false); - }); + await updateConfigPromise.finally(() => setIsSubmitting(false));
10-10
: Remove unused imports/vars.resolveGeneralTheme and resolvedTheme are unused.
Also applies to: 26-26
apps/api/plane/authentication/provider/oauth/gitea.py (1)
56-56
: Build redirect_uri using Django’s absolute URI helper.request.is_secure may be inaccurate behind proxies; build_absolute_uri respects SECURE_PROXY_SSL_HEADER and forwarded proto.
- redirect_uri = f"{'https' if request.is_secure() else 'http'}://{request.get_host()}/auth/gitea/callback/" + redirect_uri = request.build_absolute_uri("/auth/gitea/callback/")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
apps/admin/public/logos/gitea-logo.svg
is excluded by!**/*.svg
apps/web/public/logos/gitea-logo.svg
is excluded by!**/*.svg
📒 Files selected for processing (17)
apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx
(1 hunks)apps/admin/app/(all)/(dashboard)/authentication/gitea/layout.tsx
(1 hunks)apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx
(1 hunks)apps/admin/ce/components/authentication/authentication-modes.tsx
(3 hunks)apps/admin/core/components/authentication/gitea-config.tsx
(1 hunks)apps/api/plane/authentication/adapter/error.py
(1 hunks)apps/api/plane/authentication/adapter/oauth.py
(1 hunks)apps/api/plane/authentication/provider/oauth/gitea.py
(1 hunks)apps/api/plane/authentication/urls.py
(2 hunks)apps/api/plane/authentication/views/__init__.py
(2 hunks)apps/api/plane/authentication/views/app/gitea.py
(1 hunks)apps/api/plane/authentication/views/space/gitea.py
(1 hunks)apps/api/plane/license/api/views/instance.py
(3 hunks)apps/api/plane/license/management/commands/configure_instance.py
(3 hunks)apps/space/core/components/account/auth-forms/auth-root.tsx
(3 hunks)packages/types/src/instance/auth.ts
(2 hunks)packages/types/src/instance/base.ts
(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-06T09:01:17.915Z
Learnt from: shivam-jainn
PR: makeplane/plane#7545
File: apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx:139-143
Timestamp: 2025-08-06T09:01:17.915Z
Learning: In the Plane project's Gitea authentication form (apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx), the GITEA_HOST field should not be included in the form reset after submission. It should default back to "https://gitea.com" rather than maintaining the submitted value, as this provides a sensible default for most users.
Applied to files:
apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx
🧬 Code graph analysis (11)
apps/admin/core/components/authentication/gitea-config.tsx (4)
packages/types/src/instance/auth.ts (1)
TInstanceAuthenticationMethodKeys
(10-17)apps/space/core/hooks/store/use-instance.ts (1)
useInstance
(7-11)apps/admin/core/store/instance.store.ts (1)
formattedConfig
(83-89)packages/propel/src/button/helper.tsx (1)
getButtonStyling
(109-120)
apps/api/plane/authentication/views/app/gitea.py (6)
apps/api/plane/authentication/provider/oauth/gitea.py (1)
GiteaOAuthProvider
(17-158)apps/api/plane/license/models/instance.py (1)
Instance
(18-46)apps/api/plane/authentication/adapter/error.py (2)
AuthenticationException
(72-87)get_error_dict
(82-87)apps/api/plane/utils/path_validator.py (1)
validate_next_path
(65-94)apps/api/plane/authentication/views/space/gitea.py (2)
get
(22-53)get
(57-98)apps/api/plane/authentication/adapter/oauth.py (2)
get_auth_url
(56-57)authenticate
(65-68)
apps/api/plane/license/management/commands/configure_instance.py (2)
apps/api/plane/license/api/views/instance.py (1)
get
(32-183)apps/api/plane/license/models/instance.py (1)
InstanceConfiguration
(68-79)
apps/api/plane/authentication/urls.py (2)
apps/api/plane/authentication/views/app/gitea.py (2)
GiteaCallbackEndpoint
(60-109)GiteaOauthInitiateEndpoint
(22-57)apps/api/plane/authentication/views/space/gitea.py (2)
GiteaCallbackSpaceEndpoint
(56-98)GiteaOauthInitiateSpaceEndpoint
(21-53)
apps/api/plane/license/api/views/instance.py (2)
apps/api/plane/authentication/views/space/gitea.py (2)
get
(22-53)get
(57-98)apps/api/plane/authentication/views/app/gitea.py (2)
get
(23-57)get
(61-109)
apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx (7)
packages/types/src/instance/base.ts (1)
IFormattedInstanceConfiguration
(97-99)packages/types/src/instance/auth.ts (1)
TInstanceGiteaAuthenticationConfigurationKeys
(31-34)apps/space/core/hooks/store/use-instance.ts (1)
useInstance
(7-11)apps/admin/core/components/common/controller-input.tsx (2)
TControllerInputFormField
(22-30)ControllerInput
(32-83)apps/admin/core/components/common/copy-field.tsx (2)
TCopyField
(15-20)CopyField
(22-46)apps/admin/core/components/common/confirm-discard-modal.tsx (1)
ConfirmDiscardModal
(16-74)packages/propel/src/button/helper.tsx (1)
getButtonStyling
(109-120)
apps/api/plane/authentication/views/space/gitea.py (6)
apps/api/plane/authentication/provider/oauth/gitea.py (1)
GiteaOAuthProvider
(17-158)apps/api/plane/license/models/instance.py (1)
Instance
(18-46)apps/api/plane/authentication/adapter/error.py (2)
AuthenticationException
(72-87)get_error_dict
(82-87)apps/api/plane/utils/path_validator.py (1)
validate_next_path
(65-94)apps/api/plane/authentication/views/app/gitea.py (2)
get
(23-57)get
(61-109)apps/api/plane/authentication/adapter/oauth.py (2)
get_auth_url
(56-57)authenticate
(65-68)
apps/api/plane/authentication/views/__init__.py (2)
apps/api/plane/authentication/views/app/gitea.py (2)
GiteaCallbackEndpoint
(60-109)GiteaOauthInitiateEndpoint
(22-57)apps/api/plane/authentication/views/space/gitea.py (2)
GiteaCallbackSpaceEndpoint
(56-98)GiteaOauthInitiateSpaceEndpoint
(21-53)
apps/api/plane/authentication/provider/oauth/gitea.py (2)
apps/api/plane/authentication/adapter/oauth.py (4)
OauthAdapter
(19-124)get_user_token
(70-78)set_user_data
(90-91)get_user_response
(80-88)apps/api/plane/authentication/adapter/error.py (1)
AuthenticationException
(72-87)
apps/admin/ce/components/authentication/authentication-modes.tsx (1)
apps/admin/core/components/authentication/gitea-config.tsx (1)
GiteaConfiguration
(20-57)
apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx (4)
apps/space/core/hooks/store/use-instance.ts (1)
useInstance
(7-11)apps/admin/core/store/instance.store.ts (1)
formattedConfig
(83-89)apps/admin/core/components/authentication/authentication-method-card.tsx (1)
AuthenticationMethodCard
(17-56)apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx (1)
InstanceGiteaConfigForm
(26-205)
🔇 Additional comments (19)
apps/api/plane/authentication/adapter/oauth.py (1)
51-52
: LGTM!The Gitea error code handling follows the same pattern as other OAuth providers.
packages/types/src/instance/base.ts (1)
45-45
: LGTM!The
is_gitea_enabled
field follows the same pattern as other OAuth provider flags.apps/api/plane/authentication/adapter/error.py (2)
41-41
: LGTM!The error code entries follow the established pattern and numbering scheme for OAuth providers.
45-45
: LGTM!The error code is consistent with other OAuth provider error codes.
apps/admin/ce/components/authentication/authentication-modes.tsx (3)
18-18
: LGTM!The GiteaConfiguration import is correctly added alongside other OAuth provider configurations.
27-27
: LGTM!The Gitea logo import follows the same pattern as other OAuth provider logos.
85-91
: LGTM!The Gitea authentication mode configuration is consistent with other OAuth providers and includes all required fields.
apps/api/plane/license/api/views/instance.py (3)
53-53
: LGTM!The
IS_GITEA_ENABLED
configuration value is correctly added to the tuple following the pattern of other OAuth providers.
90-93
: LGTM!The configuration entry correctly retrieves the Gitea enabled flag with appropriate defaults.
142-142
: LGTM!The boolean conversion follows the same pattern as other OAuth provider flags.
apps/api/plane/authentication/views/__init__.py (2)
8-8
: LGTM!The Gitea endpoint imports follow the established pattern for OAuth providers.
21-22
: LGTM!The space-scoped Gitea endpoint imports are correctly added following the same pattern as other OAuth providers.
apps/space/core/components/account/auth-forms/auth-root.tsx (2)
30-30
: LGTM!The Gitea logo import follows the same pattern as other OAuth provider logos.
99-99
: LGTM!The
isOAuthEnabled
check correctly includes the Gitea configuration flag.apps/admin/app/(all)/(dashboard)/authentication/gitea/layout.tsx (1)
1-10
: LGTM!The layout file follows Next.js App Router conventions and provides appropriate metadata for the Gitea authentication section.
apps/api/plane/authentication/urls.py (1)
136-148
: Gitea routes wired correctly.Endpoints and names mirror existing providers; import list updated accordingly. LGTM.
Also applies to: 39-42
apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx (1)
106-127
: Confirm whether one or two Callback URIs are required.The UI surfaces only /auth/gitea/callback/. We also have /spaces/gitea/callback/ routes. If Gitea requires both to be whitelisted, the form should display both URIs (or document the need to create two OAuth apps).
Would you confirm Gitea’s redirect URI policy? If multiple URIs are supported, I can add a second CopyField for the Space callback; otherwise, we should align flows to a single callback path.
packages/types/src/instance/auth.ts (1)
31-41
: Types updated consistently for Gitea.New method and configuration keys are correctly added and unioned. Looks good.
Also applies to: 10-18
apps/api/plane/authentication/views/app/gitea.py (1)
30-37
: Consider gating by IS_GITEA_ENABLED.Only proceed if Gitea is enabled in instance config, not just setup_done. Otherwise, backend still allows login when disabled in admin UI.
Do we already enforce IS_GITEA_ENABLED server-side elsewhere? If not, add a check here similar to provider-not-configured handling. I can draft it if needed.
setPromiseToast(updateConfigPromise, { | ||
loading: "Saving Configuration...", | ||
success: { | ||
title: "Configuration saved", | ||
message: () => `Gitea authentication is now ${value ? "active" : "disabled"}.`, | ||
}, |
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 toast message: string truthiness bug.
"0" is truthy, so message always says “active”. Compare explicitly.
- message: () => `Gitea authentication is now ${value ? "active" : "disabled"}.`,
+ message: () => `Gitea authentication is now ${value === "1" ? "enabled" : "disabled"}.`,
📝 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.
setPromiseToast(updateConfigPromise, { | |
loading: "Saving Configuration...", | |
success: { | |
title: "Configuration saved", | |
message: () => `Gitea authentication is now ${value ? "active" : "disabled"}.`, | |
}, | |
setPromiseToast(updateConfigPromise, { | |
loading: "Saving Configuration...", | |
success: { | |
title: "Configuration saved", | |
message: () => `Gitea authentication is now ${value === "1" ? "enabled" : "disabled"}.`, | |
}, |
🤖 Prompt for AI Agents
apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx around lines
40-45: the toast message uses the truthiness of value so a string "0" is treated
as true and always yields "active"; change the conditional to an explicit
comparison against the active indicator (for example compare value strictly to
the expected "active" token such as "1" or to boolean true depending on the prop
type), ensuring you normalize the type if needed, and use that explicit result
to choose between "active" and "disabled".
self.scope = "user:email" | ||
|
||
client_id = GITEA_CLIENT_ID | ||
client_secret = GITEA_CLIENT_SECRET | ||
|
||
redirect_uri = f"{'https' if request.is_secure() else 'http'}://{request.get_host()}/auth/gitea/callback/" | ||
url_params = { | ||
"client_id": client_id, | ||
"scope": self.scope, | ||
"redirect_uri": redirect_uri, | ||
"response_type": "code", | ||
"state": state, | ||
} |
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
Confirm Gitea scope value.
Scope is set to "user:email". Please verify it matches Gitea’s OAuth scopes for email access.
🌐 Web query:
What OAuth scopes does Gitea support for retrieving the authenticated user's primary email? Is "user:email" correct?
💡 Result:
Short answer: No — "user:email" is not the Gitea scope. Use OpenID/OIDC scopes.
Details:
- For OIDC sign‑in and to receive the authenticated user's primary email via the UserInfo/jwt, request the OpenID scopes (openid plus email and/or profile/userinfo). The docs state Gitea exposes email/username via the OIDC scopes such as email and userinfo. [1]
- To programmatically list a user's email addresses you can call GET /user/emails; access to that API is governed by Gitea's token scopes (granular scopes mapped to API routes — read vs write for resources). Gitea does not document a "user:email" GitHub‑style scope. Check the OAuth2 provider and API docs for the exact scope mapping on your Gitea version. [1][2]
Sources:
[1] Gitea OAuth2 / OpenID Connect provider documentation (scopes / OIDC notes).
[2] Gitea API reference showing GET /user/emails (list authenticated user's email addresses).
Use correct Gitea OIDC scopes for email retrieval.
Replace self.scope = "user:email"
with the OpenID Connect scopes required by Gitea, for example:
self.scope = "openid email"
🤖 Prompt for AI Agents
In apps/api/plane/authentication/provider/oauth/gitea.py around lines 51 to 63,
the Gitea scope is set to "user:email" which is incorrect for OpenID Connect;
replace self.scope = "user:email" with the OIDC scopes (for example self.scope =
"openid email") ensuring scopes are space-separated and include "openid" and
"email" so Gitea returns an ID token and email claims during OIDC flows.
def __get_email(self, headers): | ||
try: | ||
# Gitea may not provide email in user response, so fetch it separately | ||
emails_url = f"{self.userinfo_url}/emails" | ||
response = requests.get(emails_url, headers=headers) | ||
if not response.ok: | ||
raise AuthenticationException( | ||
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"], | ||
error_message=f"GITEA_OAUTH_PROVIDER_ERROR: Failed to fetch emails (status: {response.status_code}, response: {response.text})", | ||
) | ||
emails_response = response.json() | ||
email = next( | ||
(email["email"] for email in emails_response if email.get("primary")), None | ||
) | ||
if not email and emails_response: | ||
# If no primary email, use the first one | ||
email = emails_response[0].get("email") | ||
return email | ||
except requests.RequestException as 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.
Add timeouts to external HTTP calls to avoid hanging request threads.
requests.get/post without a timeout can block indefinitely on network issues. Prefer adapter-level defaults.
Suggest updating OauthAdapter (apps/api/plane/authentication/adapter/oauth.py) to include timeouts:
# apps/api/plane/authentication/adapter/oauth.py
def get_user_token(self, data, headers=None):
try:
headers = headers or {}
response = requests.post(self.get_token_url(), data=data, headers=headers, timeout=10)
response.raise_for_status()
return response.json()
except requests.RequestException:
...
def get_user_response(self):
try:
headers = {"Authorization": f"Bearer {self.token_data.get('access_token')}"}
response = requests.get(self.get_user_info_url(), headers=headers, timeout=10)
response.raise_for_status()
return response.json()
except requests.RequestException:
...
If changing the adapter isn’t feasible right now, add timeout=10 to the provider’s requests.get calls here as an interim measure.
Also applies to: 135-145
@@ -0,0 +1,109 @@ | |||
import uuid | |||
from urllib.parse import urlencode, urljoin |
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 redirect construction and session host fallback; validate next_path consistently.
- urljoin with a leading "?" can drop the path component from base_host, producing wrong redirects.
- Relying on session["host"] can be None (new session, expiry), causing TypeError in urljoin. Compute a safe fallback from the request.
- Validate next_path before echoing it in error params (defense-in-depth).
- Prefer simple string concat for base + path/query to preserve base_host’s path.
Apply this diff:
-from urllib.parse import urlencode, urljoin
+from urllib.parse import urlencode
@@
- url = urljoin(
- base_host(request=request, is_app=True), "?" + urlencode(params)
- )
+ url = f"{base_host(request=request, is_app=True)}?{urlencode(params)}"
@@
- url = urljoin(
- base_host(request=request, is_app=True), "?" + urlencode(params)
- )
+ url = f"{base_host(request=request, is_app=True)}?{urlencode(params)}"
@@
- base_host = request.session.get("host")
+ host = request.session.get("host") or base_host(request=request, is_app=True)
@@
- if next_path:
- params["next_path"] = str(next_path)
- url = urljoin(base_host, "?" + urlencode(params))
+ if next_path:
+ params["next_path"] = str(validate_next_path(next_path))
+ url = f"{host}?{urlencode(params)}"
@@
- url = urljoin(base_host, "?" + urlencode(params))
+ url = f"{host}?{urlencode(params)}"
@@
- # redirect to referer path
- url = urljoin(base_host, path)
+ # redirect to referer path
+ url = f"{host}{path}"
Also applies to: 40-43, 54-57, 64-66, 72-76, 102-103
🤖 Prompt for AI Agents
In apps/api/plane/authentication/views/app/gitea.py around lines 2 and the other
affected ranges (40-43, 54-57, 64-66, 72-76, 102-103), fixes are needed for
constructing redirect URLs and handling session host/next_path: stop using
urljoin for appending a query or a leading-? (it can drop base path) and instead
build redirects with simple string concatenation (e.g., base_host.rstrip('/') +
path or base_host.rstrip('/') + '?' + urlencode(params)); avoid using
session["host"] directly since it can be None — compute a safe fallback like
host = session.get("host") or request.host_url.rstrip('/') (or request.scheme +
"://" + request.host if host_url is unavailable) and use that host; validate
next_path before echoing it in any error query parameter (e.g., ensure it is a
safe internal path: startswith('/') and contains no scheme/host or suspicious
characters) and only include it when valid; replace each urljoin usage and
direct session["host"] access in the listed line ranges with these safe
concatenation, fallback host computation, and next_path validation steps.
request.session["host"] = base_host(request=request, is_space=True) | ||
next_path = request.GET.get("next_path") | ||
if next_path: | ||
request.session["next_path"] = str(next_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.
Sanitize next_path to prevent open-redirects and unsafe output.
- Store a validated next_path in session.
- Validate next_path before echoing in error params and before final redirect.
- if next_path:
- request.session["next_path"] = str(next_path)
+ if next_path:
+ request.session["next_path"] = str(validate_next_path(next_path))
@@
- if next_path:
- params["next_path"] = str(next_path)
+ if next_path:
+ params["next_path"] = str(validate_next_path(next_path))
@@
- # redirect to referer path
- url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
+ # redirect to referer path
+ safe_next = str(validate_next_path(next_path)) if next_path else ""
+ # Clear transient session data
+ request.session.pop("state", None)
+ request.session.pop("next_path", None)
+ url = f"{base_host(request=request, is_space=True)}{safe_next}"
Also applies to: 49-53, 91-92
🤖 Prompt for AI Agents
In apps/api/plane/authentication/views/space/gitea.py around lines 27 (and also
apply to 49-53 and 91-92), the view stores and later echoes/redirects to
request.session["next_path"] without validation, enabling open-redirects or
unsafe output; validate and sanitize next_path before storing and before any use
by ensuring it is a safe relative path (no scheme, netloc, or leading '//' and
no path traversal like '..'), or if a full URL only allow known internal hosts,
otherwise replace with a safe default (e.g., "/") ; perform escaping when
injecting into error params to avoid unsafe output; update the code to parse the
candidate next_path, enforce these checks, store the sanitized value in session,
and re-validate/normalize it again right before adding to error params or
performing the final redirect, falling back to the safe default when validation
fails.
Description
Added Gitea SSO
Type of Change
Screenshots and Media (if applicable)
Test Scenarios
Did manual testing by creating account on gitea.com . And logged in with their oauth support .
References
#7137
Summary by CodeRabbit
New Features
API
Error Handling