* [WEB-7778] fix(security): reject unverified OAuth provider emails to prevent ATO An attacker controlling a self-hosted OAuth provider (Gitea, GitLab) could assert any email address in the OAuth response and be matched to an existing Plane account, bypassing authentication entirely. - Add OAUTH_PROVIDER_UNVERIFIED_EMAIL (5124) error code - GitHub: require both primary=True AND verified=True on email (was primary-only) - Google: check verified_email=False field in userinfo response - GitLab: check confirmed_at is non-null before accepting email - Gitea __get_email: remove unverified fallbacks (primary-unverified, any-unverified) - Gitea set_user_data: remove fast-path using .email from user object (no verification flag); always go through __get_email() which enforces verified Fixes GHSA-7j95-vh8g-f365 (critical ATO). Note: GHSA-cv9p-325g-wmv5 and GHSA-hx79-5pj5-qh42 (avatar SSRF) were already fixed in PR #9163. Co-authored-by: Plane AI <noreply@plane.so> * fix(security): add read:user scope to Gitea; fail-closed on absent Google verified_email Gitea's /api/v1/user/emails endpoint requires the read:user granular scope — openid+email+profile alone is insufficient and __get_email() would return a 401/403. Add read:user to the scope string. Google: change default from True to fail-closed (is not True) so a userinfo response that omits verified_email is rejected rather than trusted. The service-account justification was incorrect — service accounts do not go through the interactive OAuth2 callback flow. Co-authored-by: Plane AI <noreply@plane.so> --------- Co-authored-by: Plane AI <noreply@plane.so>
94 lines
3 KiB
Python
94 lines
3 KiB
Python
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# See the LICENSE file for details.
|
|
|
|
AUTHENTICATION_ERROR_CODES = {
|
|
# Global
|
|
"INSTANCE_NOT_CONFIGURED": 5000,
|
|
"INVALID_EMAIL": 5005,
|
|
"EMAIL_REQUIRED": 5010,
|
|
"SIGNUP_DISABLED": 5015,
|
|
"MAGIC_LINK_LOGIN_DISABLED": 5016,
|
|
"BOT_USER_LOGIN_FORBIDDEN": 5017,
|
|
"PASSWORD_LOGIN_DISABLED": 5018,
|
|
"USER_ACCOUNT_DEACTIVATED": 5019,
|
|
# Password strength
|
|
"INVALID_PASSWORD": 5020,
|
|
"PASSWORD_TOO_WEAK": 5021,
|
|
"SMTP_NOT_CONFIGURED": 5025,
|
|
# Sign Up
|
|
"USER_ALREADY_EXIST": 5030,
|
|
"AUTHENTICATION_FAILED_SIGN_UP": 5035,
|
|
"REQUIRED_EMAIL_PASSWORD_SIGN_UP": 5040,
|
|
"INVALID_EMAIL_SIGN_UP": 5045,
|
|
"INVALID_EMAIL_MAGIC_SIGN_UP": 5050,
|
|
"MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED": 5055,
|
|
"EMAIL_PASSWORD_AUTHENTICATION_DISABLED": 5056,
|
|
# Sign In
|
|
"USER_DOES_NOT_EXIST": 5060,
|
|
"AUTHENTICATION_FAILED_SIGN_IN": 5065,
|
|
"REQUIRED_EMAIL_PASSWORD_SIGN_IN": 5070,
|
|
"INVALID_EMAIL_SIGN_IN": 5075,
|
|
"INVALID_EMAIL_MAGIC_SIGN_IN": 5080,
|
|
"MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED": 5085,
|
|
# Both Sign in and Sign up for magic
|
|
"INVALID_MAGIC_CODE_SIGN_IN": 5090,
|
|
"INVALID_MAGIC_CODE_SIGN_UP": 5092,
|
|
"EXPIRED_MAGIC_CODE_SIGN_IN": 5095,
|
|
"EXPIRED_MAGIC_CODE_SIGN_UP": 5097,
|
|
"EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN": 5100,
|
|
"EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP": 5102,
|
|
# Oauth
|
|
"OAUTH_NOT_CONFIGURED": 5104,
|
|
"GOOGLE_NOT_CONFIGURED": 5105,
|
|
"GITHUB_NOT_CONFIGURED": 5110,
|
|
"GITHUB_USER_NOT_IN_ORG": 5122,
|
|
"GITLAB_NOT_CONFIGURED": 5111,
|
|
"GITEA_NOT_CONFIGURED": 5112,
|
|
"GOOGLE_OAUTH_PROVIDER_ERROR": 5115,
|
|
"GITHUB_OAUTH_PROVIDER_ERROR": 5120,
|
|
"GITLAB_OAUTH_PROVIDER_ERROR": 5121,
|
|
"GITEA_OAUTH_PROVIDER_ERROR": 5123,
|
|
"OAUTH_PROVIDER_UNVERIFIED_EMAIL": 5124,
|
|
# Reset Password
|
|
"INVALID_PASSWORD_TOKEN": 5125,
|
|
"EXPIRED_PASSWORD_TOKEN": 5130,
|
|
# Change password
|
|
"INCORRECT_OLD_PASSWORD": 5135,
|
|
"MISSING_PASSWORD": 5138,
|
|
"INVALID_NEW_PASSWORD": 5140,
|
|
# set password
|
|
"PASSWORD_ALREADY_SET": 5145,
|
|
# Admin
|
|
"ADMIN_ALREADY_EXIST": 5150,
|
|
"REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME": 5155,
|
|
"INVALID_ADMIN_EMAIL": 5160,
|
|
"INVALID_ADMIN_PASSWORD": 5165,
|
|
"REQUIRED_ADMIN_EMAIL_PASSWORD": 5170,
|
|
"ADMIN_AUTHENTICATION_FAILED": 5175,
|
|
"ADMIN_USER_ALREADY_EXIST": 5180,
|
|
"ADMIN_USER_DOES_NOT_EXIST": 5185,
|
|
"ADMIN_USER_DEACTIVATED": 5190,
|
|
# Rate limit
|
|
"RATE_LIMIT_EXCEEDED": 5900,
|
|
# Unknown
|
|
"AUTHENTICATION_FAILED": 5999,
|
|
}
|
|
|
|
|
|
class AuthenticationException(Exception):
|
|
error_code = None
|
|
error_message = None
|
|
payload = {}
|
|
|
|
def __init__(self, error_code, error_message, payload={}):
|
|
self.error_code = error_code
|
|
self.error_message = error_message
|
|
self.payload = payload
|
|
|
|
def get_error_dict(self):
|
|
error = {"error_code": self.error_code, "error_message": self.error_message}
|
|
for key in self.payload:
|
|
error[key] = self.payload[key]
|
|
|
|
return error
|