diff --git a/CHANGELOG.md b/CHANGELOG.md index cb281f3d..25a22d44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add WeChat Open Platform QR-code authentication for web login and registration, including OAuth callback handling, user binding/creation, i18n error messaging, and environment variable configuration. +### Changed +- Respect `ALLOW_PUBLIC_REGISTRATION` when auto-creating first-time WeChat users. + ## [1.8.4] - 2026-04-09 ### Security diff --git a/api/services/wechat_auth_service.py b/api/services/wechat_auth_service.py index 7bc443b5..cf41c330 100644 --- a/api/services/wechat_auth_service.py +++ b/api/services/wechat_auth_service.py @@ -52,6 +52,15 @@ def _wechat_redirect_uri() -> Optional[str]: return os.getenv("WECHAT_OPEN_REDIRECT_URI") or os.getenv("WECHAT_REDIRECT_URI") +def _allow_wechat_user_creation() -> bool: + return os.getenv("ALLOW_PUBLIC_REGISTRATION", "false").lower() in { + "true", + "1", + "yes", + "on", + } + + async def build_wechat_authorize_url(state: str) -> WeChatAuthorizeUrlResponse: app_id = _wechat_app_id() redirect_uri = _wechat_redirect_uri() @@ -167,6 +176,9 @@ async def handle_wechat_callback(request: WeChatCallbackRequest) -> LoginRespons updated = await UserRepository.update_user(user_id, updates) user = updated[0] if updated else {**user, **updates} else: + if not _allow_wechat_user_creation(): + raise HTTPException(status_code=403, detail="Public registration is disabled") + user = await UserRepository.create_wechat_user( { "username": _username_from_wechat(user_info), diff --git a/deploy/non-docker/env.online-dev.example b/deploy/non-docker/env.online-dev.example index e68acfa1..c1562b76 100644 --- a/deploy/non-docker/env.online-dev.example +++ b/deploy/non-docker/env.online-dev.example @@ -10,6 +10,13 @@ OPEN_NOTEBOOK_CORS_ORIGINS=https://lumina.yinhour.com OPEN_NOTEBOOK_AUTH_MODE=jwt ALLOW_PUBLIC_REGISTRATION=false +# Optional WeChat Open Platform web login. +# The redirect URI must be registered in WeChat exactly as shown here. +# If ALLOW_PUBLIC_REGISTRATION=false, WeChat can only sign in existing bound users. +WECHAT_OPEN_APP_ID= +WECHAT_OPEN_APP_SECRET= +WECHAT_OPEN_REDIRECT_URI=https://lumina.yinhour.com/login/wechat/callback + # Required for saving AI provider credentials. OPEN_NOTEBOOK_ENCRYPTION_KEY=CHANGE_ME_generate_with_openssl_rand_base64_48 diff --git a/docs/1-INSTALLATION/non-docker-online-dev.md b/docs/1-INSTALLATION/non-docker-online-dev.md index 88a8ee3f..8c9a2714 100644 --- a/docs/1-INSTALLATION/non-docker-online-dev.md +++ b/docs/1-INSTALLATION/non-docker-online-dev.md @@ -88,6 +88,16 @@ openssl rand -base64 32 Keep `OPEN_NOTEBOOK_AUTH_MODE=jwt` for any Internet-facing site. Keep `ALLOW_PUBLIC_REGISTRATION=false` unless you intentionally want public signups. +Optional WeChat QR-code login uses a WeChat Open Platform website application: + +```bash +WECHAT_OPEN_APP_ID=... +WECHAT_OPEN_APP_SECRET=... +WECHAT_OPEN_REDIRECT_URI=https://lumina.yinhour.com/login/wechat/callback +``` + +Register the same redirect URI in WeChat Open Platform. With `ALLOW_PUBLIC_REGISTRATION=false`, WeChat sign-in is limited to existing bound users; set it to `true` only if first-time WeChat users should be able to create accounts. + ## 6. Install App Dependencies and Build Frontend ```bash diff --git a/docs/5-CONFIGURATION/environment-reference.md b/docs/5-CONFIGURATION/environment-reference.md index c6186a7a..dbfd9498 100644 --- a/docs/5-CONFIGURATION/environment-reference.md +++ b/docs/5-CONFIGURATION/environment-reference.md @@ -22,6 +22,20 @@ Comprehensive list of all environment variables available in Open Notebook. --- +## WeChat Web Login + +WeChat web login uses a WeChat Open Platform website application and the frontend callback page at `/login/wechat/callback`. + +| Variable | Required? | Default | Description | +|----------|-----------|---------|-------------| +| `WECHAT_OPEN_APP_ID` | Required for WeChat login | None | WeChat Open Platform website application AppID. Legacy alias: `WECHAT_APP_ID` | +| `WECHAT_OPEN_APP_SECRET` | Required for callback token exchange | None | WeChat Open Platform website application AppSecret. Supports `_FILE` secret loading via `WECHAT_OPEN_APP_SECRET_FILE`; legacy alias: `WECHAT_APP_SECRET` | +| `WECHAT_OPEN_REDIRECT_URI` | Required for WeChat login | None | Full frontend callback URL registered in WeChat, for example `https://lumina.yinhour.com/login/wechat/callback`. Legacy alias: `WECHAT_REDIRECT_URI` | + +If `ALLOW_PUBLIC_REGISTRATION=false`, WeChat can sign in existing bound users but will not auto-create accounts for new WeChat identities. + +--- + ## Email Verification Open Notebook uses email verification for public registration and password reset. diff --git a/tests/test_wechat_auth_service.py b/tests/test_wechat_auth_service.py index 458a0644..3da761b5 100644 --- a/tests/test_wechat_auth_service.py +++ b/tests/test_wechat_auth_service.py @@ -83,6 +83,7 @@ async def test_handle_wechat_callback_logs_in_existing_bound_user(monkeypatch): @pytest.mark.asyncio async def test_handle_wechat_callback_creates_user_for_new_wechat_identity(monkeypatch): + monkeypatch.setenv("ALLOW_PUBLIC_REGISTRATION", "true") created_user = { "id": "app_user:wx_openid_2", "username": "wx_openid_2", @@ -138,3 +139,48 @@ async def test_handle_wechat_callback_creates_user_for_new_wechat_identity(monke assert create_payload["display_name"] == "New WeChat User" assert create_payload["wechat_openid"] == "openid-2" assert create_payload["avatar_url"] == "https://example.com/new-avatar.jpg" + + +@pytest.mark.asyncio +async def test_handle_wechat_callback_blocks_new_user_when_public_registration_disabled(monkeypatch): + monkeypatch.setenv("ALLOW_PUBLIC_REGISTRATION", "false") + monkeypatch.setattr( + wechat_auth_service, + "_exchange_code_for_tokens", + AsyncMock( + return_value=WeChatOAuthTokens( + access_token="wx-token", + openid="openid-3", + unionid=None, + ) + ), + ) + monkeypatch.setattr( + wechat_auth_service, + "_fetch_user_info", + AsyncMock( + return_value=WeChatUserInfo( + openid="openid-3", + unionid=None, + nickname="Blocked WeChat User", + headimgurl=None, + ) + ), + ) + monkeypatch.setattr( + wechat_auth_service.UserRepository, + "get_user_by_wechat_identity", + AsyncMock(return_value=None), + ) + monkeypatch.setattr( + wechat_auth_service.UserRepository, + "create_wechat_user", + AsyncMock(), + ) + + with pytest.raises(wechat_auth_service.HTTPException) as exc_info: + await handle_wechat_callback(WeChatCallbackRequest(code="auth-code", state=None)) + + assert exc_info.value.status_code == 403 + assert exc_info.value.detail == "Public registration is disabled" + wechat_auth_service.UserRepository.create_wechat_user.assert_not_awaited()