feat: sync HTTP callback OIDC setup
This commit is contained in:
parent
bf72c246b6
commit
f53579416e
26 changed files with 557 additions and 133 deletions
|
|
@ -164,7 +164,9 @@ TELESRV_PUBLIC_LINK_WEB_ADDR=127.0.0.1:2401
|
|||
# have been generated with `go run ./cmd/telegramloginkeygen -mode init`.
|
||||
TELESRV_TELEGRAM_LOGIN_ENABLE=false
|
||||
TELESRV_TELEGRAM_LOGIN_ISSUER=https://telesrv.net
|
||||
TELESRV_TELEGRAM_LOGIN_ALLOW_LOOPBACK_HTTP=false
|
||||
# Set true to permit an HTTP issuer and HTTP registered origins/redirect URIs
|
||||
# on any hostname or IP address. HTTPS remains the default when false.
|
||||
TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=false
|
||||
TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE=data/telegram-login/signing-keys.json
|
||||
TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE=data/telegram-login/code-keys.json
|
||||
TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE=data/telegram-login/client-secret-pepper
|
||||
|
|
|
|||
|
|
@ -56,10 +56,10 @@ python .\cmd\bots\aiogramecho\echo.py `
|
|||
## Webhook 模式
|
||||
|
||||
telesrv 现在会持久化 webhook 配置,通过跨实例租约投递,并且只在目标返回 2xx
|
||||
后推进 `update_id`。aiogram 可监听本机 HTTP,由 Caddy/Nginx/Tunnel 提供公网 HTTPS:
|
||||
后推进 `update_id`。aiogram 可直接登记 HTTP/HTTPS 域名或 IP,也可以由 Caddy/Nginx/Tunnel 提供公网 HTTPS:
|
||||
|
||||
```powershell
|
||||
$env:TELESRV_BOT_WEBHOOK_URL = "https://bot.example.com/webhook"
|
||||
$env:TELESRV_BOT_WEBHOOK_URL = "http://192.0.2.25:8080/webhook"
|
||||
$env:TELESRV_BOT_WEBHOOK_SECRET = "replace_with_a_random_secret"
|
||||
python .\cmd\bots\aiogramecho\echo.py `
|
||||
--mode webhook `
|
||||
|
|
@ -69,8 +69,8 @@ python .\cmd\bots\aiogramecho\echo.py `
|
|||
--drop-pending
|
||||
```
|
||||
|
||||
公网 URL 必须是 HTTPS,端口限 Telegram 标准的 443/80/88/8443;本机监听地址
|
||||
可以是 HTTP,因为 TLS 通常在反向代理终止。`secret_token` 会由 telesrv 放入
|
||||
Webhook URL 可使用任意合法 HTTP/HTTPS 域名或 IP 及 `1..65535` 端口;本机监听地址
|
||||
也可以直接使用 HTTP。`secret_token` 会由 telesrv 放入
|
||||
`X-Telegram-Bot-Api-Secret-Token`,aiogram 会自动校验。若希望进程退出时删除配置,
|
||||
再加 `--delete-webhook-on-exit`;默认保留配置,以免普通重启造成更新丢窗。
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ def parse_args() -> argparse.Namespace:
|
|||
parser.add_argument(
|
||||
"--webhook-url",
|
||||
default=os.getenv("TELESRV_BOT_WEBHOOK_URL", ""),
|
||||
help="Public HTTPS URL including the webhook path",
|
||||
help="Public HTTP(S) URL including the webhook path",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--webhook-path",
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ enable
|
|||
|
||||
`/setlogin` 首次创建 client 时只展示一次 OIDC Client Secret;不要写进仓库。可用
|
||||
`/logininfo` 查看 Client ID 和登记结果,或用 `/resetloginsecret` 轮换 secret。
|
||||
loopback HTTP 仅应配合 telesrv 的显式开发开关使用;测试部署/生产必须换成精确
|
||||
HTTPS origin。
|
||||
使用 HTTP 域名/IP 时,在 telesrv 配置 `TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=true`;
|
||||
demo 会接受任意合法 HTTP(S) issuer/public origin,不再限制为 loopback。
|
||||
|
||||
把一次性 secret 和 Client ID 放入进程环境,再启动:
|
||||
|
||||
|
|
|
|||
|
|
@ -68,10 +68,6 @@ class PendingFlow:
|
|||
code_verifier: str = ""
|
||||
|
||||
|
||||
def _is_loopback(host: str | None) -> bool:
|
||||
return host in {"127.0.0.1", "::1", "localhost"}
|
||||
|
||||
|
||||
def normalize_web_base(value: str, *, name: str) -> str:
|
||||
raw = value.strip().rstrip("/")
|
||||
parsed = urlsplit(raw)
|
||||
|
|
@ -85,8 +81,6 @@ def normalize_web_base(value: str, *, name: str) -> str:
|
|||
or parsed.path not in {"", "/"}
|
||||
):
|
||||
raise ValueError(f"{name} must be an absolute origin without path, query, or fragment")
|
||||
if parsed.scheme != "https" and not _is_loopback(parsed.hostname):
|
||||
raise ValueError(f"{name} must use HTTPS except on loopback")
|
||||
return f"{parsed.scheme}://{parsed.netloc}"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -49,8 +49,14 @@ class LoginDemoHelpersTest(unittest.TestCase):
|
|||
demo.normalize_web_base("http://127.0.0.1:3000", name="RP"),
|
||||
"http://127.0.0.1:3000",
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
demo.normalize_web_base("http://rp.example", name="RP")
|
||||
self.assertEqual(
|
||||
demo.normalize_web_base("http://192.0.2.25:3000", name="RP"),
|
||||
"http://192.0.2.25:3000",
|
||||
)
|
||||
self.assertEqual(
|
||||
demo.normalize_web_base("http://rp.example:18080", name="RP"),
|
||||
"http://rp.example:18080",
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
demo.normalize_web_base("https://rp.example/callback", name="RP")
|
||||
self.assertEqual(demo.parse_listen("127.0.0.1:3000"), ("127.0.0.1", 3000))
|
||||
|
|
|
|||
|
|
@ -366,7 +366,7 @@ func run(logger *zap.Logger) error {
|
|||
}
|
||||
telegramLoginService, err = telegramloginapp.NewService(postgres.NewTelegramLoginStore(pool), codeSealer, telegramloginapp.Config{
|
||||
Issuer: cfg.TelegramLoginIssuer, AppScheme: cfg.PublicAppScheme,
|
||||
AllowLoopbackHTTP: cfg.TelegramLoginAllowLoopbackHTTP,
|
||||
AllowHTTP: cfg.TelegramLoginAllowHTTP,
|
||||
ClientSecretPepper: clientSecretPepper,
|
||||
SupportedSigningAlgorithms: signingKeys.ActiveAlgorithms(),
|
||||
RequestTTL: cfg.TelegramLoginRequestTTL, CodeTTL: cfg.TelegramLoginCodeTTL,
|
||||
|
|
@ -375,7 +375,7 @@ func run(logger *zap.Logger) error {
|
|||
return fmt.Errorf("initialize telegram login service: %w", err)
|
||||
}
|
||||
telegramLoginIDTokens, err = telegramloginapp.NewIDTokenIssuer(signingKeys, telegramloginapp.IDTokenIssuerConfig{
|
||||
Issuer: cfg.TelegramLoginIssuer, TTL: cfg.TelegramLoginIDTokenTTL,
|
||||
Issuer: cfg.TelegramLoginIssuer, TTL: cfg.TelegramLoginIDTokenTTL, AllowHTTP: cfg.TelegramLoginAllowHTTP,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("initialize telegram login ID-token issuer: %w", err)
|
||||
|
|
@ -393,7 +393,7 @@ func run(logger *zap.Logger) error {
|
|||
Service: telegramLoginService, Tokens: telegramLoginIDTokens,
|
||||
Limiter: redisstore.NewRateLimiter(rdb), AppName: cfg.PublicAppName,
|
||||
Logger: logger.Named("telegram-login-http"), TrustedProxyCIDRs: cfg.TelegramLoginTrustedProxyCIDRs,
|
||||
AllowLoopbackHTTP: cfg.TelegramLoginAllowLoopbackHTTP,
|
||||
AllowHTTP: cfg.TelegramLoginAllowHTTP,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("initialize telegram login HTTP provider: %w", err)
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ This document describes every setting loaded by `internal/config`. Defaults and
|
|||
| Setting | Type / code default | Description and constraints |
|
||||
|---|---|---|
|
||||
| `TELESRV_DEBUG_ADDR` | nullable address / `127.0.0.1:6060` | pprof/debug listener. Empty disables it. Keep loopback-only; use an SSH tunnel for production profiling. |
|
||||
| `TELESRV_BOT_API_ADDR` | nullable address / empty | Minimal HTTP Bot API listener. Empty disables it. It shares MTProto app/store facts. |
|
||||
| `TELESRV_BOT_API_ADDR` | nullable address / empty | Minimal HTTP Bot API listener. Empty disables it. It shares MTProto app/store facts. `setWebhook` accepts any valid `http://` or `https://` host/IP and port in `1..65535`. |
|
||||
| `TELESRV_ADMIN_API_ADDR` | nullable address / empty | In-process Admin write API listener. Empty disables it; production should bind loopback. |
|
||||
| `TELESRV_ADMIN_API_TOKEN` | secret string / empty | Admin API bearer token. Required when the Admin API is enabled and must match the Admin UI token configuration. |
|
||||
| `TELESRV_ADMIN_UI_ADDR` | address / `127.0.0.1:2600` | Standalone `cmd/telesrv-admin` listen address. |
|
||||
|
|
@ -62,7 +62,189 @@ This document describes every setting loaded by `internal/config`. Defaults and
|
|||
| `TELESRV_PUBLIC_APP_SCHEME` | URL scheme / `telesrv` | Automatic app-open scheme on landing pages. Must match patched client registration. `tg`, `http`, and `https` are rejected. |
|
||||
| `TELESRV_PUBLIC_WEB_BASE_URL` | HTTP(S) URL / `https://web.telesrv.net` | Web-client root used by public username pages. Same URL validation as `TELESRV_PUBLIC_BASE_URL`. |
|
||||
| `TELESRV_PUBLIC_APP_NAME` | string / `telesrv` | Public landing-page product name; trimmed, non-empty, no control characters, maximum 64 Unicode characters. |
|
||||
| `TELESRV_PUBLIC_LINK_WEB_ADDR` | nullable address / empty | Read-only username/avatar/sticker/emoji/chatlist landing-page listener. Empty disables it. Production should bind loopback behind exact nginx routes. `.env.example` enables `127.0.0.1:2401` for development. |
|
||||
| `TELESRV_PUBLIC_LINK_WEB_ADDR` | nullable address / empty | Read-only username/avatar/sticker/emoji/chatlist/collectible-gift landing-page listener. Empty disables it. Production should bind loopback behind exact nginx routes. `.env.example` enables `127.0.0.1:2401` for development. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_ENABLE` | bool / `false` | Mount the self-hosted Telegram Login/OIDC provider on `TELESRV_PUBLIC_LINK_WEB_ADDR`. Enabling it requires that listener and all key files below. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_ISSUER` | absolute origin URL / `TELESRV_PUBLIC_BASE_URL` | Exact public issuer used in discovery and tokens. HTTPS is required by default; paths, credentials, query, and fragment are rejected. The next setting permits any HTTP host/IP. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP` | bool / `false` | When enabled, permits any valid HTTP issuer, BotFather Web origin, redirect URI, and native HTTP callback, without loopback, subnet, or port restrictions. When disabled, those Web URLs still require HTTPS. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE` | path / `data/telegram-login/signing-keys.json` | JOSE private-key ring generated by `cmd/telegramloginkeygen`; active plus retiring public keys are published through JWKS. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE` | path / `data/telegram-login/code-keys.json` | AES-256-GCM envelope-key ring for recoverable, one-time authorization codes. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE` | path / `data/telegram-login/client-secret-pepper` | Deployment pepper for HMAC-SHA-256 client-secret hashes. The file must contain a base64 encoding of exactly 32 random bytes. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_REQUEST_TTL` | duration / `5m` | Pending authorization lifetime; bounded to `1m..15m`. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_CODE_TTL` | duration / `2m` | One-time code lifetime; bounded to `30s..10m`. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_ID_TOKEN_TTL` | duration / `1h` | Signed ID-token lifetime; bounded to `1m..24h`. Retiring signing keys must cover this window. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_TRUSTED_PROXY_CIDRS` | comma-separated CIDRs / empty | Only requests whose direct peer is in this list may supply `Forwarded`/`X-Forwarded-*` client metadata. The documented nginx deployment uses `127.0.0.1/32,::1/128`. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_RETENTION` | duration / `168h` | Retention after terminal request/code/revocation state; bounded to `1h..90d`. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_SWEEP_INTERVAL` | duration / `5m` | Retention worker interval; bounded to `10s..1h`. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_SWEEP_BATCH` | int / `500` | Maximum rows per retention pass; bounded to `1..1000`. |
|
||||
|
||||
### 3.1 Complete Telegram Login / OIDC setup
|
||||
|
||||
#### 1. Generate `data/telegram-login` once
|
||||
|
||||
Run this from the `telesrv` repository root:
|
||||
|
||||
```powershell
|
||||
go run ./cmd/telegramloginkeygen -mode init -dir data/telegram-login
|
||||
Get-ChildItem .\data\telegram-login
|
||||
```
|
||||
|
||||
The same command works on Linux; restrict the generated directory afterward:
|
||||
|
||||
```bash
|
||||
go run ./cmd/telegramloginkeygen -mode init -dir data/telegram-login
|
||||
chmod 0700 data/telegram-login
|
||||
chmod 0600 data/telegram-login/*
|
||||
```
|
||||
|
||||
Initialization creates the following private files. It never prints key material and refuses to
|
||||
overwrite an existing `signing-keys.json`, `code-keys.json`, or `client-secret-pepper`:
|
||||
|
||||
- `signing-keys.json` plus three `signing-*.pem` files: the manifest and private keys for RS256,
|
||||
ES256, and EdDSA ID-token signatures;
|
||||
- `code-keys.json`: the AES-256-GCM envelope-key ring for one-time authorization codes;
|
||||
- `client-secret-pepper`: a 32-byte deployment pepper used to store and verify OIDC Client Secret
|
||||
digests.
|
||||
|
||||
The repository ignores `data/*` by default. Never put this directory in Git, release archives,
|
||||
logs, or ordinary backups. All instances must mount the same protected files and restart together
|
||||
after rotation. Losing the pepper invalidates existing Client Secret verification. Losing a signing
|
||||
key that is still in its publication window invalidates otherwise-live ID tokens against JWKS.
|
||||
|
||||
#### 2. Configure and start the Provider
|
||||
|
||||
This example exposes OIDC directly at `http://192.0.2.25:2401`; replace it with the server address
|
||||
that clients can actually reach. Bind `0.0.0.0:2401` for direct LAN/public access, or keep
|
||||
`127.0.0.1:2401` when an on-host reverse proxy is the only caller:
|
||||
|
||||
```env
|
||||
TELESRV_PUBLIC_BASE_URL=http://192.0.2.25:2401
|
||||
TELESRV_PUBLIC_LINK_WEB_ADDR=0.0.0.0:2401
|
||||
TELESRV_PUBLIC_APP_SCHEME=telesrv
|
||||
|
||||
TELESRV_TELEGRAM_LOGIN_ENABLE=true
|
||||
TELESRV_TELEGRAM_LOGIN_ISSUER=http://192.0.2.25:2401
|
||||
TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=true
|
||||
TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE=data/telegram-login/signing-keys.json
|
||||
TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE=data/telegram-login/code-keys.json
|
||||
TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE=data/telegram-login/client-secret-pepper
|
||||
```
|
||||
|
||||
For HTTPS, set the issuer and public base to the exact HTTPS origin and leave
|
||||
`TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=false`. The issuer becomes the token `iss` and the root of all
|
||||
discovery endpoints, so its scheme, host, and port must exactly match the address used by relying
|
||||
parties. Start or restart `telesrv`, then verify the public endpoints:
|
||||
|
||||
```powershell
|
||||
curl.exe http://192.0.2.25:2401/.well-known/openid-configuration
|
||||
curl.exe http://192.0.2.25:2401/.well-known/jwks.json
|
||||
curl.exe -I http://192.0.2.25:2401/js/telegram-login.js
|
||||
```
|
||||
|
||||
The discovery `issuer` must equal the configured value, and its `authorization_endpoint`,
|
||||
`token_endpoint`, and `jwks_uri` must be reachable by the relying party. A reverse proxy must pass
|
||||
through `/.well-known/openid-configuration`, `/.well-known/jwks.json`, `/auth`, `/auth/status`,
|
||||
`/token`, `/crossapp`, `/inapp`, `/telegram-login.js`, and `/js/telegram-login.js` unchanged.
|
||||
|
||||
#### 3. Create an OIDC Client with the local `@BotFather`
|
||||
|
||||
Create a bot with `/newbot` or select an existing bot. In the local `@BotFather`, run `/setlogin`
|
||||
and choose that bot. Initial setup returns:
|
||||
|
||||
- `Client ID`: the bot user ID as a decimal string;
|
||||
- `Client Secret`: shown once, separate from the Bot API token, and meant to be saved immediately
|
||||
in a secret manager.
|
||||
|
||||
Send each configuration command separately. This example runs the relying party at
|
||||
`http://192.0.2.30:3000`:
|
||||
|
||||
```text
|
||||
add origin http://192.0.2.30:3000
|
||||
add redirect http://192.0.2.30:3000/oauth/callback
|
||||
algorithm RS256
|
||||
enable
|
||||
```
|
||||
|
||||
An `origin` is an exact Web origin without a path, query, or fragment; it authorizes the JS SDK,
|
||||
popup CORS, and legacy `login_url`. A `redirect` is the exact full URI that receives an
|
||||
Authorization Code. Wildcards and prefix matching are not supported. Use `/logininfo` to inspect
|
||||
status and registrations; use `/setlogin` to add/remove URLs, change the algorithm, or disable the
|
||||
client; use `/resetloginsecret` to rotate the Client Secret. Available algorithms are RS256,
|
||||
ES256, EdDSA, and ES256K only when its build/key ring is present. EdDSA and ES256K accept only the
|
||||
`openid` scope.
|
||||
|
||||
#### 4. Integrate a relying party with standard OIDC
|
||||
|
||||
Start by loading:
|
||||
|
||||
```text
|
||||
http://192.0.2.25:2401/.well-known/openid-configuration
|
||||
```
|
||||
|
||||
The standard flow is Authorization Code with PKCE S256:
|
||||
|
||||
1. Generate random `state`, `nonce`, and PKCE `code_verifier`; derive the S256 `code_challenge`.
|
||||
2. Open the discovery `authorization_endpoint` with `client_id`, the exact `redirect_uri`,
|
||||
`response_type=code`, a `scope` containing `openid`, `state`, `nonce`, `code_challenge`, and
|
||||
`code_challenge_method=S256`.
|
||||
3. After the user approves in TDesktop/Android, verify `state` at the relying-party callback and
|
||||
read the one-time code.
|
||||
4. Server-side, POST `grant_type=authorization_code`, the code, the same `redirect_uri`, and
|
||||
`code_verifier` to the discovery `token_endpoint`. Confidential clients authenticate with HTTP
|
||||
Basic or `client_secret_post`.
|
||||
5. Verify the ID-token signature with the discovery `jwks_uri`, then strictly validate `iss`,
|
||||
`aud`, `exp`, `nonce`, and a non-empty `sub`. Decoding without signature verification is not
|
||||
sufficient.
|
||||
|
||||
Supported scopes are `openid`, `profile`, `phone`, and `telegram:bot_access`. The provider does not
|
||||
currently expose UserInfo, refresh tokens, or an introspection endpoint. Browser applications may
|
||||
load `<issuer>/js/telegram-login.js` for the local JS SDK. A Client Secret must remain server-side.
|
||||
|
||||
#### 5. Verify the complete path with the Bedolaga demo
|
||||
|
||||
Install the demo dependencies:
|
||||
|
||||
```powershell
|
||||
python -m venv "$env:TEMP\telesrv-bedolaga-demo-venv"
|
||||
& "$env:TEMP\telesrv-bedolaga-demo-venv\Scripts\python.exe" -m pip install `
|
||||
-r .\cmd\bots\bedolagaformat\requirements.txt
|
||||
```
|
||||
|
||||
Put the Client ID/Secret from step 3 and the same bot's Bot API token only in process environment:
|
||||
|
||||
```powershell
|
||||
$env:TELESRV_BOT_TOKEN = "<bot_id>:<bot_api_secret>"
|
||||
$env:TELESRV_BOT_API_SERVER = "http://192.0.2.25:8081"
|
||||
$env:TELESRV_BOT_LOGIN_DEMO = "1"
|
||||
$env:TELESRV_BOT_LOGIN_ISSUER = "http://192.0.2.25:2401"
|
||||
$env:TELESRV_BOT_LOGIN_CLIENT_ID = "<Client ID returned by BotFather>"
|
||||
$env:TELESRV_BOT_LOGIN_CLIENT_SECRET = "<one-time OIDC Client Secret>"
|
||||
$env:TELESRV_BOT_LOGIN_PUBLIC_URL = "http://192.0.2.30:3000"
|
||||
$env:TELESRV_BOT_LOGIN_LISTEN = "0.0.0.0:3000"
|
||||
|
||||
& "$env:TEMP\telesrv-bedolaga-demo-venv\Scripts\python.exe" `
|
||||
.\cmd\bots\bedolagaformat\demo.py --drop-pending --login-demo
|
||||
```
|
||||
|
||||
The BotFather origin must equal `TELESRV_BOT_LOGIN_PUBLIC_URL`, and the redirect must equal
|
||||
`<TELESRV_BOT_LOGIN_PUBLIC_URL>/oauth/callback`. Send `/logindemo` to the bot. The first button
|
||||
tests Bot API `login_url` plus the HMAC callback; the second page tests the local JS SDK popup and
|
||||
Authorization Code + PKCE/JWKS. Omitting the Client Secret leaves JS popup verification available
|
||||
but explicitly disables the server-side code flow.
|
||||
|
||||
#### 6. Rotate keys
|
||||
|
||||
When rotating a signing key, retain the old public key for at least the configured ID-token TTL
|
||||
plus ten minutes. Restart all instances together after the operation:
|
||||
|
||||
```powershell
|
||||
go run ./cmd/telegramloginkeygen -mode rotate-signing -algorithm RS256 `
|
||||
-id-token-ttl 1h -publish-for 2h -dir data/telegram-login
|
||||
go run ./cmd/telegramloginkeygen -mode rotate-code -dir data/telegram-login
|
||||
```
|
||||
|
||||
Run `rotate-signing` separately for RS256, ES256, or EdDSA. `rotate-code` retains old code keys and
|
||||
adds a new active key. Do not edit manifests or PEM files manually, and never generate divergent
|
||||
key rings independently on different instances.
|
||||
|
||||
## 4. PostgreSQL, Redis, files, and seed data
|
||||
|
||||
|
|
@ -95,7 +277,7 @@ The language-pack file manifest is authoritative. To add a language, place `data
|
|||
| `TELESRV_AUTH_CODE_RATE_WINDOW` | duration / `10m` | Shared window for phone and auth-key issuance limits. |
|
||||
| `TELESRV_PHONE_CODE_DELIVERY_PROVIDER` | enum / `development` | `development` uses fixed codes; `webhook` generates random SMS codes for login, registration, and phone changes. Both modes first commit the same code to the durable 777000 dialog for existing accounts; Webhook is additive. |
|
||||
| `TELESRV_EMAIL_CODE_DELIVERY_PROVIDER` | enum / `smtp` | Delivery implementation for login-email and email setup/change codes: `smtp` or `webhook`. Existing-account login-email codes are first mirrored to 777000; setup/change remains provider-only. |
|
||||
| `TELESRV_OTP_WEBHOOK_URL` | absolute URL / empty | Required when any provider selects `webhook`; see [otp-delivery.md](otp-delivery.md) for the fixed v1 contract. Must use `http`/`https` and contain no userinfo. |
|
||||
| `TELESRV_OTP_WEBHOOK_URL` | absolute URL / empty | Required when any provider selects `webhook`; see [otp-delivery.md](otp-delivery.md) for the fixed v1 contract. Any valid `http://` or `https://` host/IP and port is accepted; userinfo is rejected. |
|
||||
| `TELESRV_OTP_WEBHOOK_SECRET` | secret string / empty | Optional HMAC-SHA256 signing secret; enables `X-Telesrv-Signature` when non-empty. |
|
||||
| `TELESRV_OTP_WEBHOOK_TIMEOUT` | duration / `5s` | Webhook HTTP timeout; must be positive when Webhook delivery is enabled. |
|
||||
| `TELESRV_LOGIN_EMAIL_ENABLE` | bool / `false` | Enables login-email verification. SMTP settings are required only when the email provider is `smtp`. |
|
||||
|
|
@ -207,6 +389,19 @@ The following fallback keys are accepted from the **process environment only**.
|
|||
| `TELESRV_STARS_STARTING_GRANT` | int64 / `1000` | Idempotent lazy starting Stars balance for all accounts; `0` disables automatic grant. |
|
||||
| `TELESRV_PREMIUM_SWEEP_INTERVAL` | duration / `1m` | Expired-premium cleanup/push interval. Read paths derive expiry independently. |
|
||||
| `TELESRV_PREMIUM_SWEEP_BATCH` | int / `500` | Maximum expired premium rows processed per sweep. |
|
||||
| `TELESRV_STARGIFT_SWEEP_INTERVAL` | duration / `15s` | Local Star Gift offer/auction lifecycle sweep interval; no blockchain connection is made. |
|
||||
| `TELESRV_STARGIFT_SWEEP_BATCH` | int / `1000` | Maximum offer/auction/outbox work claimed per lifecycle sweep. |
|
||||
| `TELESRV_STARGIFT_TON_STARTING_GRANT` | int64 / `10000000000` | Nanoton granted idempotently on a user's first access to the internal telesrv TON ledger; `0` disables it. This is not an on-chain asset. |
|
||||
| `TELESRV_STARGIFT_TRANSFER_STARS` | int64 / `25` | Stars charged for a collectible transfer; `0` enables the free-transfer RPC. |
|
||||
| `TELESRV_STARGIFT_DROP_DETAILS_STARS` | int64 / `25` | Stars charged to remove a collectible's original sender/message details. |
|
||||
| `TELESRV_STARGIFT_OFFER_MIN_STARS` | int / `1` | Minimum Stars offer snapshotted for user-owned collectibles; `0` disables the offer entry point. |
|
||||
| `TELESRV_STARGIFT_STARS_PROCEEDS_PERMILLE` | int / `1000` | Seller share in Stars sales, in permille; the remainder is recorded as platform commission. |
|
||||
| `TELESRV_STARGIFT_TON_PROCEEDS_PERMILLE` | int / `1000` | Seller share in internal-TON sales, in permille; this affects only the local ledger. |
|
||||
| `TELESRV_STARGIFT_EXPORT_DELAY` | duration / `0s` | Delay snapshotted into `can_export_at` when a collectible is issued. |
|
||||
| `TELESRV_STARGIFT_TRANSFER_DELAY` | duration / `0s` | Delay snapshotted into `can_transfer_at`. |
|
||||
| `TELESRV_STARGIFT_RESELL_DELAY` | duration / `0s` | Delay snapshotted into `can_resell_at`. |
|
||||
| `TELESRV_STARGIFT_CRAFT_DELAY` | duration / `0s` | Delay snapshotted into `can_craft_at`. |
|
||||
| `TELESRV_STARGIFT_CRAFT_CHANCE_PERMILLE` | int / `250` | Per-input local craft success contribution, capped at 1000 permille. |
|
||||
|
||||
## 11. Private calls, group calls, TURN, SFU, and livestream
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
| 参数 | 类型 / 代码默认值 | 说明与约束 |
|
||||
|---|---|---|
|
||||
| `TELESRV_DEBUG_ADDR` | nullable address / `127.0.0.1:6060` | pprof/debug 监听;空值关闭。生产必须保持 loopback,通过 SSH 隧道抓取。 |
|
||||
| `TELESRV_BOT_API_ADDR` | nullable address / 空 | 最小 HTTP Bot API 监听;空值关闭,与 MTProto 共用 app/store 事实。 |
|
||||
| `TELESRV_BOT_API_ADDR` | nullable address / 空 | 最小 HTTP Bot API 监听;空值关闭,与 MTProto 共用 app/store 事实。`setWebhook` 接受任意合法 `http://` 或 `https://` 主机/IP 与 `1..65535` 端口。 |
|
||||
| `TELESRV_ADMIN_API_ADDR` | nullable address / 空 | 进程内 Admin 写 API;空值关闭,生产应只监听 loopback。 |
|
||||
| `TELESRV_ADMIN_API_TOKEN` | secret string / 空 | Admin API bearer token;启用 Admin API 时必须显式配置,并与 Admin UI 使用的 token 一致。 |
|
||||
| `TELESRV_ADMIN_UI_ADDR` | address / `127.0.0.1:2600` | 独立 `cmd/telesrv-admin` 监听地址。 |
|
||||
|
|
@ -62,7 +62,181 @@
|
|||
| `TELESRV_PUBLIC_APP_SCHEME` | URL scheme / `telesrv` | 落地页自动唤起客户端的 scheme,必须与 patched 客户端注册值一致;禁止 `tg`、`http`、`https`。 |
|
||||
| `TELESRV_PUBLIC_WEB_BASE_URL` | HTTP(S) URL / `https://web.telesrv.net` | username 页面 Web 客户端入口,校验规则同 `TELESRV_PUBLIC_BASE_URL`。 |
|
||||
| `TELESRV_PUBLIC_APP_NAME` | string / `telesrv` | 公开落地页产品名;trim 后非空、无控制字符、最多 64 个 Unicode 字符。 |
|
||||
| `TELESRV_PUBLIC_LINK_WEB_ADDR` | nullable address / 空 | 只读 username/avatar/sticker/emoji/chatlist 落地页监听;空值关闭。生产应 loopback + nginx 精确反代;`.env.example` 为开发启用 `127.0.0.1:2401`。 |
|
||||
| `TELESRV_PUBLIC_LINK_WEB_ADDR` | nullable address / 空 | 只读 username/avatar/sticker/emoji/chatlist/collectible gift 落地页监听;空值关闭。生产应 loopback + nginx 精确反代;`.env.example` 为开发启用 `127.0.0.1:2401`。 |
|
||||
| `TELESRV_TELEGRAM_LOGIN_ENABLE` | bool / `false` | 在 `TELESRV_PUBLIC_LINK_WEB_ADDR` 上挂载自建 Telegram Login/OIDC Provider;启用时必须同时配置该 listener 与下列全部密钥文件。 |
|
||||
| `TELESRV_TELEGRAM_LOGIN_ISSUER` | 绝对 origin URL / `TELESRV_PUBLIC_BASE_URL` | discovery 与 token 使用的精确公开 issuer;默认必须 HTTPS,禁止 path、credentials、query、fragment。开启下一项后可直接配置任意 HTTP 域名/IP。 |
|
||||
| `TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP` | bool / `false` | 开启后允许任意合法 HTTP issuer、BotFather Web origin、redirect URI 和 native HTTP callback,不限制为 loopback,也不限制 IP 网段或端口。关闭时这些 Web URL 仍必须 HTTPS。 |
|
||||
| `TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE` | path / `data/telegram-login/signing-keys.json` | 由 `cmd/telegramloginkeygen` 生成的 JOSE 私钥环;JWKS 会发布 active 和仍在退役窗口内的公钥。 |
|
||||
| `TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE` | path / `data/telegram-login/code-keys.json` | 用于可恢复一次性 authorization code 的 AES-256-GCM envelope key ring。 |
|
||||
| `TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE` | path / `data/telegram-login/client-secret-pepper` | HMAC-SHA-256 Client Secret 摘要的部署 pepper 文件,内容必须是恰好 32 个随机字节的 base64 编码。 |
|
||||
| `TELESRV_TELEGRAM_LOGIN_REQUEST_TTL` | duration / `5m` | pending authorization 生命周期,限定 `1m..15m`。 |
|
||||
| `TELESRV_TELEGRAM_LOGIN_CODE_TTL` | duration / `2m` | 一次性 code 生命周期,限定 `30s..10m`。 |
|
||||
| `TELESRV_TELEGRAM_LOGIN_ID_TOKEN_TTL` | duration / `1h` | ID token 生命周期,限定 `1m..24h`;退役签名公钥必须覆盖该窗口。 |
|
||||
| `TELESRV_TELEGRAM_LOGIN_TRUSTED_PROXY_CIDRS` | 逗号分隔 CIDR / 空 | 只有直连 peer 落在该列表时才信任 `Forwarded`/`X-Forwarded-*` 客户端元数据;文档中的单机 nginx 部署使用 `127.0.0.1/32,::1/128`。 |
|
||||
| `TELESRV_TELEGRAM_LOGIN_RETENTION` | duration / `168h` | terminal request/code/revocation 后的保留期,限定 `1h..90d`。 |
|
||||
| `TELESRV_TELEGRAM_LOGIN_SWEEP_INTERVAL` | duration / `5m` | retention worker 周期,限定 `10s..1h`。 |
|
||||
| `TELESRV_TELEGRAM_LOGIN_SWEEP_BATCH` | int / `500` | 每轮最大清理行数,限定 `1..1000`。 |
|
||||
|
||||
### 3.1 Telegram Login / OIDC 完整启用流程
|
||||
|
||||
#### 1. 一次性生成 `data/telegram-login`
|
||||
|
||||
在 `telesrv` 仓库根目录执行:
|
||||
|
||||
```powershell
|
||||
go run ./cmd/telegramloginkeygen -mode init -dir data/telegram-login
|
||||
Get-ChildItem .\data\telegram-login
|
||||
```
|
||||
|
||||
Linux 部署也可使用同一命令;生成后应限制目录权限:
|
||||
|
||||
```bash
|
||||
go run ./cmd/telegramloginkeygen -mode init -dir data/telegram-login
|
||||
chmod 0700 data/telegram-login
|
||||
chmod 0600 data/telegram-login/*
|
||||
```
|
||||
|
||||
初始化会生成以下私密文件,命令不会把密钥内容输出到终端,并会拒绝覆盖已经存在的
|
||||
`signing-keys.json`、`code-keys.json` 或 `client-secret-pepper`:
|
||||
|
||||
- `signing-keys.json` 和三个 `signing-*.pem`:RS256、ES256、EdDSA ID token 签名私钥及清单;
|
||||
- `code-keys.json`:一次性 authorization code 使用的 AES-256-GCM envelope key ring;
|
||||
- `client-secret-pepper`:保存和校验 OIDC Client Secret 摘要时使用的 32 字节部署 pepper。
|
||||
|
||||
`data/*` 默认已被仓库 `.gitignore` 排除。不要把该目录放入 Git、发布压缩包、日志或
|
||||
普通备份;多实例必须挂载同一份受保护的文件,并在轮换后一起重启。丢失 pepper 会让
|
||||
现有 Client Secret 无法验证,丢失仍在发布窗口内的签名私钥会让尚未过期的 ID token
|
||||
无法继续通过 JWKS 验证。
|
||||
|
||||
#### 2. 配置并启动 Provider
|
||||
|
||||
以下示例直接通过 `http://192.0.2.25:2401` 对外提供 OIDC;请替换成客户端实际可达的
|
||||
服务器 IP。直接监听局域网/公网网卡时使用 `0.0.0.0:2401`,仅由同机反向代理转发时
|
||||
应改回 `127.0.0.1:2401`:
|
||||
|
||||
```env
|
||||
TELESRV_PUBLIC_BASE_URL=http://192.0.2.25:2401
|
||||
TELESRV_PUBLIC_LINK_WEB_ADDR=0.0.0.0:2401
|
||||
TELESRV_PUBLIC_APP_SCHEME=telesrv
|
||||
|
||||
TELESRV_TELEGRAM_LOGIN_ENABLE=true
|
||||
TELESRV_TELEGRAM_LOGIN_ISSUER=http://192.0.2.25:2401
|
||||
TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=true
|
||||
TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE=data/telegram-login/signing-keys.json
|
||||
TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE=data/telegram-login/code-keys.json
|
||||
TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE=data/telegram-login/client-secret-pepper
|
||||
```
|
||||
|
||||
使用 HTTPS 时,把 `TELESRV_TELEGRAM_LOGIN_ISSUER` 和公开根地址改成精确 HTTPS
|
||||
origin,并保持 `TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=false`。issuer 是 token 的 `iss`
|
||||
以及 discovery 中所有端点的根地址,scheme、host 和 port 必须与依赖方访问的地址完全
|
||||
一致。启动或重启 `telesrv` 后,先验证公开端点:
|
||||
|
||||
```powershell
|
||||
curl.exe http://192.0.2.25:2401/.well-known/openid-configuration
|
||||
curl.exe http://192.0.2.25:2401/.well-known/jwks.json
|
||||
curl.exe -I http://192.0.2.25:2401/js/telegram-login.js
|
||||
```
|
||||
|
||||
discovery 返回的 `issuer` 必须等于配置值,`authorization_endpoint`、`token_endpoint`
|
||||
和 `jwks_uri` 必须可从依赖方访问。使用反向代理时需原样转发
|
||||
`/.well-known/openid-configuration`、`/.well-known/jwks.json`、`/auth`、`/auth/status`、
|
||||
`/token`、`/crossapp`、`/inapp`、`/telegram-login.js` 和 `/js/telegram-login.js`。
|
||||
|
||||
#### 3. 用本服 `@BotFather` 创建 OIDC Client
|
||||
|
||||
先用 `/newbot` 创建或选择已有 bot,然后在本服 `@BotFather` 中执行 `/setlogin` 并选择
|
||||
该 bot。首次配置会返回:
|
||||
|
||||
- `Client ID`:bot user ID 的十进制字符串;
|
||||
- `Client Secret`:只显示一次,与 Bot API token 不同,必须立即保存到密钥管理系统。
|
||||
|
||||
接着逐条发送配置命令。下面假设依赖方页面运行在 `http://192.0.2.30:3000`:
|
||||
|
||||
```text
|
||||
add origin http://192.0.2.30:3000
|
||||
add redirect http://192.0.2.30:3000/oauth/callback
|
||||
algorithm RS256
|
||||
enable
|
||||
```
|
||||
|
||||
`origin` 只能是无 path/query/fragment 的精确 Web origin,用于 JS SDK、popup CORS 和
|
||||
legacy `login_url`;`redirect` 是 Authorization Code Flow 返回 code 的精确完整 URI。
|
||||
不支持 wildcard 或 prefix 匹配。用 `/logininfo` 检查状态和登记值;用 `/setlogin`
|
||||
增删 URL、切换签名算法或 disable;用 `/resetloginsecret` 轮换 Client Secret。可用的
|
||||
签名算法为 RS256、ES256、EdDSA,以及仅在对应构建和 key ring 已提供时可选的 ES256K;
|
||||
EdDSA/ES256K 只允许 `openid` scope。
|
||||
|
||||
#### 4. 依赖方接入标准 OIDC
|
||||
|
||||
依赖方应首先读取:
|
||||
|
||||
```text
|
||||
http://192.0.2.25:2401/.well-known/openid-configuration
|
||||
```
|
||||
|
||||
标准流程为 Authorization Code + PKCE S256:
|
||||
|
||||
1. 生成随机 `state`、`nonce` 和 PKCE `code_verifier`,计算 S256 `code_challenge`;
|
||||
2. 浏览器打开 discovery 中的 `authorization_endpoint`,携带 `client_id`、精确
|
||||
`redirect_uri`、`response_type=code`、包含 `openid` 的 `scope`、`state`、`nonce`、
|
||||
`code_challenge` 和 `code_challenge_method=S256`;
|
||||
3. 用户在 TDesktop/Android 中确认后,依赖方 callback 校验 `state` 并取得一次性 code;
|
||||
4. 服务端向 discovery 中的 `token_endpoint` POST `grant_type=authorization_code`、code、
|
||||
同一 `redirect_uri` 和 `code_verifier`,机密 client 使用 HTTP Basic 或
|
||||
`client_secret_post` 提交 Client Secret;
|
||||
5. 用 discovery 的 `jwks_uri` 验证 ID token 签名,并严格校验 `iss`、`aud`、`exp`、
|
||||
`nonce` 和非空 `sub`。不要只解码而不验签。
|
||||
|
||||
支持的 scope 为 `openid`、`profile`、`phone`、`telegram:bot_access`。当前不提供
|
||||
UserInfo、refresh token 或 introspection endpoint。浏览器前端可以加载
|
||||
`<issuer>/js/telegram-login.js` 使用本地 JS SDK;Client Secret 只能留在服务端。
|
||||
|
||||
#### 5. 使用 Bedolaga demo 验证完整链路
|
||||
|
||||
安装 demo 依赖:
|
||||
|
||||
```powershell
|
||||
python -m venv "$env:TEMP\telesrv-bedolaga-demo-venv"
|
||||
& "$env:TEMP\telesrv-bedolaga-demo-venv\Scripts\python.exe" -m pip install `
|
||||
-r .\cmd\bots\bedolagaformat\requirements.txt
|
||||
```
|
||||
|
||||
将第 3 步得到的 Client ID/Secret 和同一个 Bot API token 仅放入进程环境:
|
||||
|
||||
```powershell
|
||||
$env:TELESRV_BOT_TOKEN = "<bot_id>:<bot_api_secret>"
|
||||
$env:TELESRV_BOT_API_SERVER = "http://192.0.2.25:8081"
|
||||
$env:TELESRV_BOT_LOGIN_DEMO = "1"
|
||||
$env:TELESRV_BOT_LOGIN_ISSUER = "http://192.0.2.25:2401"
|
||||
$env:TELESRV_BOT_LOGIN_CLIENT_ID = "<BotFather 返回的 Client ID>"
|
||||
$env:TELESRV_BOT_LOGIN_CLIENT_SECRET = "<只显示一次的 OIDC Client Secret>"
|
||||
$env:TELESRV_BOT_LOGIN_PUBLIC_URL = "http://192.0.2.30:3000"
|
||||
$env:TELESRV_BOT_LOGIN_LISTEN = "0.0.0.0:3000"
|
||||
|
||||
& "$env:TEMP\telesrv-bedolaga-demo-venv\Scripts\python.exe" `
|
||||
.\cmd\bots\bedolagaformat\demo.py --drop-pending --login-demo
|
||||
```
|
||||
|
||||
确保 BotFather 登记的 origin 等于 `TELESRV_BOT_LOGIN_PUBLIC_URL`,redirect 等于
|
||||
`<TELESRV_BOT_LOGIN_PUBLIC_URL>/oauth/callback`。在客户端向 bot 发送 `/logindemo`:第一颗
|
||||
按钮验证 Bot API `login_url` 和 HMAC 回调,第二颗按钮页面分别验证本地 JS SDK popup
|
||||
以及 Authorization Code + PKCE/JWKS。省略 Client Secret 时只能验证 JS popup,服务端
|
||||
code flow 会明确禁用。
|
||||
|
||||
#### 6. 密钥轮换
|
||||
|
||||
签名 key 轮换时,旧公钥发布窗口必须至少覆盖配置的 ID token TTL 再加 10 分钟;操作
|
||||
完成后所有实例一起重启:
|
||||
|
||||
```powershell
|
||||
go run ./cmd/telegramloginkeygen -mode rotate-signing -algorithm RS256 `
|
||||
-id-token-ttl 1h -publish-for 2h -dir data/telegram-login
|
||||
go run ./cmd/telegramloginkeygen -mode rotate-code -dir data/telegram-login
|
||||
```
|
||||
|
||||
`rotate-signing` 可分别用于 RS256、ES256、EdDSA;`rotate-code` 保留旧 code key 并新增
|
||||
active key。不要手工编辑 manifest 或 PEM,不要在各实例上分别生成不一致的 key ring。
|
||||
|
||||
## 4. PostgreSQL、Redis、文件与 seed
|
||||
|
||||
|
|
@ -95,7 +269,7 @@
|
|||
| `TELESRV_AUTH_CODE_RATE_WINDOW` | duration / `10m` | 手机号与 auth-key 发码限流共用窗口。 |
|
||||
| `TELESRV_PHONE_CODE_DELIVERY_PROVIDER` | enum / `development` | `development` 使用固定码;`webhook` 为登录、注册、改号生成随机 SMS code 并调用 OTP Webhook。已有账号在两种模式下都先 durable 写入同码 777000 消息,Webhook 只是附加渠道。 |
|
||||
| `TELESRV_EMAIL_CODE_DELIVERY_PROVIDER` | enum / `smtp` | 登录邮箱、邮箱 setup/change 的投递实现:`smtp` 或 `webhook`。已有账号的登录邮箱码会先同码镜像到 777000;邮箱 setup/change 仍只走 provider。 |
|
||||
| `TELESRV_OTP_WEBHOOK_URL` | absolute URL / 空 | 任一 provider 选择 `webhook` 时必填;固定 v1 协议见 [otp-delivery.md](otp-delivery.md)。只允许 `http`/`https` 且不得含 userinfo。 |
|
||||
| `TELESRV_OTP_WEBHOOK_URL` | absolute URL / 空 | 任一 provider 选择 `webhook` 时必填;固定 v1 协议见 [otp-delivery.md](otp-delivery.md)。允许任意合法 `http://` 或 `https://` 主机/IP 与端口,不得含 userinfo。 |
|
||||
| `TELESRV_OTP_WEBHOOK_SECRET` | secret string / 空 | 可选 HMAC-SHA256 签名密钥;非空时发送 `X-Telesrv-Signature`。 |
|
||||
| `TELESRV_OTP_WEBHOOK_TIMEOUT` | duration / `5s` | Webhook HTTP 请求超时,启用 Webhook 时必须为正数。 |
|
||||
| `TELESRV_LOGIN_EMAIL_ENABLE` | bool / `false` | 启用登录邮箱验证码;email provider 为 `smtp` 时要求 SMTP 配置,`webhook` 时不依赖 SMTP。 |
|
||||
|
|
@ -207,6 +381,19 @@
|
|||
| `TELESRV_STARS_STARTING_GRANT` | int64 / `1000` | 对所有账号幂等惰性授予的 Stars 起始余额;`0` 关闭自动赠送。 |
|
||||
| `TELESRV_PREMIUM_SWEEP_INTERVAL` | duration / `1m` | 过期 Premium 清理/推送周期;读取路径独立即时派生到期状态。 |
|
||||
| `TELESRV_PREMIUM_SWEEP_BATCH` | int / `500` | 单次 sweep 最大处理行数。 |
|
||||
| `TELESRV_STARGIFT_SWEEP_INTERVAL` | duration / `15s` | Star Gift 报价/竞拍本地生命周期清扫周期;不会连接区块链。 |
|
||||
| `TELESRV_STARGIFT_SWEEP_BATCH` | int / `1000` | 单次礼物生命周期清扫最多处理的报价、竞拍与 outbox 工作量。 |
|
||||
| `TELESRV_STARGIFT_TON_STARTING_GRANT` | int64 / `10000000000` | 每个用户首次访问 telesrv 内部 TON 账本时幂等授予的 nanoton;`0` 关闭赠送。它不是链上资产。 |
|
||||
| `TELESRV_STARGIFT_TRANSFER_STARS` | int64 / `25` | collectible 转赠费用;设为 `0` 时使用免费转赠 RPC。 |
|
||||
| `TELESRV_STARGIFT_DROP_DETAILS_STARS` | int64 / `25` | 移除 collectible 原始发送者/附言信息所需 Stars。 |
|
||||
| `TELESRV_STARGIFT_OFFER_MIN_STARS` | int / `1` | collectible 签发时固化的用户持有礼物最低 Stars 报价;`0` 不开放报价入口。 |
|
||||
| `TELESRV_STARGIFT_STARS_PROCEEDS_PERMILLE` | int / `1000` | Stars 成交时卖方实收比例(千分比);差额作为平台佣金写入成交记录。 |
|
||||
| `TELESRV_STARGIFT_TON_PROCEEDS_PERMILLE` | int / `1000` | 内部 TON 成交时卖方实收比例(千分比);只影响本地账本。 |
|
||||
| `TELESRV_STARGIFT_EXPORT_DELAY` | duration / `0s` | collectible 签发时固化到 `can_export_at` 的等待期。 |
|
||||
| `TELESRV_STARGIFT_TRANSFER_DELAY` | duration / `0s` | 签发时固化到 `can_transfer_at` 的等待期。 |
|
||||
| `TELESRV_STARGIFT_RESELL_DELAY` | duration / `0s` | 签发时固化到 `can_resell_at` 的等待期。 |
|
||||
| `TELESRV_STARGIFT_CRAFT_DELAY` | duration / `0s` | 签发时固化到 `can_craft_at` 的等待期;可 Craft 礼物即使为 `0s` 也写升级时间这一正数能力边界,0 只表示不具备 Craft 能力或已终结。 |
|
||||
| `TELESRV_STARGIFT_CRAFT_CHANCE_PERMILLE` | int / `250` | 每份输入礼物贡献的本地合成成功概率,累计上限 1000‰。 |
|
||||
|
||||
## 11. 私聊通话、群通话、TURN、SFU 与直播
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ func newBotFatherTelegramLoginService(t *testing.T) *telegramloginapp.Service {
|
|||
pepper := make([]byte, 32)
|
||||
pepper[0] = 2
|
||||
service, err := telegramloginapp.NewService(memory.NewTelegramLoginStore(nil), sealer, telegramloginapp.Config{
|
||||
Issuer: "http://localhost:2404", AppScheme: "telesrv", AllowLoopbackHTTP: true,
|
||||
Issuer: "http://192.0.2.25:2404", AppScheme: "telesrv", AllowHTTP: true,
|
||||
ClientSecretPepper: pepper, Now: func() time.Time { return time.Unix(1_780_000_000, 0).UTC() },
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -52,13 +52,13 @@ func TestBotFatherTelegramLoginConfigurationFlow(t *testing.T) {
|
|||
if len(secret) < 32 {
|
||||
t.Fatalf("client secret is unexpectedly short: %q", secret)
|
||||
}
|
||||
if reply := sendToBotFather(t, svc, messages, owner, "add origin http://localhost:3000"); !strings.Contains(reply, "Success!") {
|
||||
if reply := sendToBotFather(t, svc, messages, owner, "add origin http://rp.example.test:3000"); !strings.Contains(reply, "Success!") {
|
||||
t.Fatalf("add origin reply = %q", reply)
|
||||
}
|
||||
|
||||
sendToBotFather(t, svc, messages, owner, "/setlogin")
|
||||
sendToBotFather(t, svc, messages, owner, "login_demo_bot")
|
||||
if reply := sendToBotFather(t, svc, messages, owner, "add redirect http://localhost:3000/auth/callback"); !strings.Contains(reply, "Success!") {
|
||||
if reply := sendToBotFather(t, svc, messages, owner, "add redirect http://192.0.2.26:3000/auth/callback"); !strings.Contains(reply, "Success!") {
|
||||
t.Fatalf("add redirect reply = %q", reply)
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ func TestBotFatherTelegramLoginConfigurationFlow(t *testing.T) {
|
|||
|
||||
sendToBotFather(t, svc, messages, owner, "/logininfo")
|
||||
info := sendToBotFather(t, svc, messages, owner, "login_demo_bot")
|
||||
for _, want := range []string{"Signing algorithm: ES256", "web_origin http://localhost:3000", "redirect_uri http://localhost:3000/auth/callback", "dev.bedolaga.demo", "Bedolaga iOS Demo", "Bedolaga Android Demo"} {
|
||||
for _, want := range []string{"Signing algorithm: ES256", "web_origin http://rp.example.test:3000", "redirect_uri http://192.0.2.26:3000/auth/callback", "dev.bedolaga.demo", "Bedolaga iOS Demo", "Bedolaga Android Demo"} {
|
||||
if !strings.Contains(info, want) {
|
||||
t.Fatalf("login info = %q, missing %q", info, want)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -296,6 +296,7 @@ type IDTokenIssuerConfig struct {
|
|||
Issuer string
|
||||
TTL time.Duration
|
||||
Now func() time.Time
|
||||
AllowHTTP bool
|
||||
}
|
||||
|
||||
type IDTokenIssuer struct {
|
||||
|
|
@ -337,7 +338,7 @@ func NewIDTokenIssuer(keys *SigningKeyRing, cfg IDTokenIssuerConfig) (*IDTokenIs
|
|||
if keys == nil {
|
||||
return nil, errors.New("telegram login signing key ring is required")
|
||||
}
|
||||
issuer, err := NormalizeWebOrigin(cfg.Issuer, true)
|
||||
issuer, err := NormalizeWebOrigin(cfg.Issuer, cfg.AllowHTTP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("telegram login ID token issuer: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,6 +167,21 @@ func TestIDTokenIssuerScopeProjectionAndVerification(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIDTokenIssuerAcceptsHTTPIPOnlyWhenEnabled(t *testing.T) {
|
||||
now := time.Date(2026, 7, 21, 10, 0, 0, 0, time.UTC)
|
||||
ring := telegramLoginTestSigningKeys(t, &now)
|
||||
if _, err := NewIDTokenIssuer(ring, IDTokenIssuerConfig{Issuer: "http://192.0.2.25:2401"}); err == nil {
|
||||
t.Fatal("HTTP issuer was accepted while AllowHTTP was false")
|
||||
}
|
||||
issuer, err := NewIDTokenIssuer(ring, IDTokenIssuerConfig{Issuer: "http://192.0.2.25:2401", AllowHTTP: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if issuer.Issuer() != "http://192.0.2.25:2401" {
|
||||
t.Fatalf("issuer=%q", issuer.Issuer())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSigningKeyRingRejectsWrongCurveAndDuplicateActiveKey(t *testing.T) {
|
||||
p384, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func normalizeNativeVerificationID(platform domain.TelegramLoginNativePlatform,
|
|||
// non-web custom scheme registered for a native application. Query and
|
||||
// fragment components are forbidden because OAuth response fields are
|
||||
// appended by the provider and must not collide with application input.
|
||||
func NormalizeNativeCallbackURI(raw string, allowLoopbackHTTP bool) (string, error) {
|
||||
func NormalizeNativeCallbackURI(raw string, allowHTTP bool) (string, error) {
|
||||
if raw == "" || len(raw) > maxTelegramLoginURLLength || raw != strings.TrimSpace(raw) || strings.IndexFunc(raw, unicode.IsControl) >= 0 {
|
||||
return "", domain.ErrTelegramLoginURLInvalid
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ func NormalizeNativeCallbackURI(raw string, allowLoopbackHTTP bool) (string, err
|
|||
return "", domain.ErrTelegramLoginURLInvalid
|
||||
}
|
||||
if strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https") {
|
||||
normalized, _, err := NormalizeRedirectURI(raw, allowLoopbackHTTP)
|
||||
normalized, _, err := NormalizeRedirectURI(raw, allowHTTP)
|
||||
return normalized, err
|
||||
}
|
||||
scheme := strings.ToLower(u.Scheme)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ var telegramLoginMatchCodePool = []string{
|
|||
type Config struct {
|
||||
Issuer string
|
||||
AppScheme string
|
||||
AllowLoopbackHTTP bool
|
||||
AllowHTTP bool
|
||||
ClientSecretPepper []byte
|
||||
SupportedSigningAlgorithms []domain.TelegramLoginSigningAlgorithm
|
||||
RequestTTL time.Duration
|
||||
|
|
@ -49,7 +49,7 @@ type Service struct {
|
|||
sealer *CodeSealer
|
||||
issuer string
|
||||
appScheme string
|
||||
allowLoopbackHTTP bool
|
||||
allowHTTP bool
|
||||
clientSecretPepper []byte
|
||||
signingAlgorithms []domain.TelegramLoginSigningAlgorithm
|
||||
signingAlgorithmSet map[domain.TelegramLoginSigningAlgorithm]struct{}
|
||||
|
|
@ -62,7 +62,7 @@ func NewService(loginStore store.TelegramLoginStore, sealer *CodeSealer, cfg Con
|
|||
if loginStore == nil || sealer == nil || len(cfg.ClientSecretPepper) < 32 {
|
||||
return nil, fmt.Errorf("telegram login dependencies are incomplete")
|
||||
}
|
||||
issuer, err := NormalizeWebOrigin(cfg.Issuer, cfg.AllowLoopbackHTTP)
|
||||
issuer, err := NormalizeWebOrigin(cfg.Issuer, cfg.AllowHTTP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("telegram login issuer: %w", err)
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ func NewService(loginStore store.TelegramLoginStore, sealer *CodeSealer, cfg Con
|
|||
}
|
||||
return &Service{
|
||||
store: loginStore, sealer: sealer, issuer: issuer, appScheme: strings.ToLower(cfg.AppScheme),
|
||||
allowLoopbackHTTP: cfg.AllowLoopbackHTTP,
|
||||
allowHTTP: cfg.AllowHTTP,
|
||||
clientSecretPepper: append([]byte(nil), cfg.ClientSecretPepper...),
|
||||
signingAlgorithms: append([]domain.TelegramLoginSigningAlgorithm(nil), cfg.SupportedSigningAlgorithms...),
|
||||
signingAlgorithmSet: signingAlgorithmSet,
|
||||
|
|
@ -248,9 +248,9 @@ func (s *Service) AddAllowedURL(ctx context.Context, botUserID int64, kind domai
|
|||
var err error
|
||||
switch kind {
|
||||
case domain.TelegramLoginAllowedWebOrigin:
|
||||
normalized, err = NormalizeWebOrigin(raw, s.allowLoopbackHTTP)
|
||||
normalized, err = NormalizeWebOrigin(raw, s.allowHTTP)
|
||||
case domain.TelegramLoginAllowedRedirectURI:
|
||||
normalized, _, err = NormalizeRedirectURI(raw, s.allowLoopbackHTTP)
|
||||
normalized, _, err = NormalizeRedirectURI(raw, s.allowHTTP)
|
||||
default:
|
||||
err = domain.ErrTelegramLoginURLInvalid
|
||||
}
|
||||
|
|
@ -267,9 +267,9 @@ func (s *Service) DeleteAllowedURL(ctx context.Context, botUserID int64, kind do
|
|||
var err error
|
||||
switch kind {
|
||||
case domain.TelegramLoginAllowedWebOrigin:
|
||||
normalized, err = NormalizeWebOrigin(raw, s.allowLoopbackHTTP)
|
||||
normalized, err = NormalizeWebOrigin(raw, s.allowHTTP)
|
||||
case domain.TelegramLoginAllowedRedirectURI:
|
||||
normalized, _, err = NormalizeRedirectURI(raw, s.allowLoopbackHTTP)
|
||||
normalized, _, err = NormalizeRedirectURI(raw, s.allowHTTP)
|
||||
default:
|
||||
err = domain.ErrTelegramLoginURLInvalid
|
||||
}
|
||||
|
|
@ -311,7 +311,7 @@ func (s *Service) AddNativeApp(ctx context.Context, botUserID int64, platform do
|
|||
if err != nil {
|
||||
return domain.TelegramLoginNativeApp{}, err
|
||||
}
|
||||
callbackURI, err = NormalizeNativeCallbackURI(callbackURI, s.allowLoopbackHTTP)
|
||||
callbackURI, err = NormalizeNativeCallbackURI(callbackURI, s.allowHTTP)
|
||||
if err != nil {
|
||||
return domain.TelegramLoginNativeApp{}, err
|
||||
}
|
||||
|
|
@ -335,7 +335,7 @@ func (s *Service) DeleteNativeApp(ctx context.Context, botUserID, appID int64) (
|
|||
}
|
||||
|
||||
func (s *Service) matchNativeApp(ctx context.Context, botUserID int64, platform domain.TelegramLoginNativePlatform, rawCallbackURI string) (domain.TelegramLoginNativeApp, string, bool, error) {
|
||||
callbackURI, err := NormalizeNativeCallbackURI(rawCallbackURI, s.allowLoopbackHTTP)
|
||||
callbackURI, err := NormalizeNativeCallbackURI(rawCallbackURI, s.allowHTTP)
|
||||
if err != nil {
|
||||
return domain.TelegramLoginNativeApp{}, "", false, nil
|
||||
}
|
||||
|
|
@ -362,7 +362,7 @@ func (s *Service) ValidateMessageButton(ctx context.Context, botUserID int64, ra
|
|||
if !found || !client.Enabled {
|
||||
return "", "", domain.ErrTelegramLoginClientDisabled
|
||||
}
|
||||
normalizedURL, domainName, err = NormalizeRedirectURI(rawURL, s.allowLoopbackHTTP)
|
||||
normalizedURL, domainName, err = NormalizeRedirectURI(rawURL, s.allowHTTP)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
|
@ -370,7 +370,7 @@ func (s *Service) ValidateMessageButton(ctx context.Context, botUserID int64, ra
|
|||
if err != nil {
|
||||
return "", "", domain.ErrTelegramLoginURLInvalid
|
||||
}
|
||||
origin, err := NormalizeWebOrigin(u.Scheme+"://"+u.Host, s.allowLoopbackHTTP)
|
||||
origin, err := NormalizeWebOrigin(u.Scheme+"://"+u.Host, s.allowHTTP)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
|
@ -415,7 +415,7 @@ func (s *Service) AuthorizeMessageButton(ctx context.Context, params domain.Tele
|
|||
if err != nil {
|
||||
return domain.TelegramLoginMessageButtonResult{}, domain.ErrTelegramLoginURLInvalid
|
||||
}
|
||||
origin, err := NormalizeWebOrigin(u.Scheme+"://"+u.Host, s.allowLoopbackHTTP)
|
||||
origin, err := NormalizeWebOrigin(u.Scheme+"://"+u.Host, s.allowHTTP)
|
||||
if err != nil {
|
||||
return domain.TelegramLoginMessageButtonResult{}, err
|
||||
}
|
||||
|
|
@ -552,7 +552,7 @@ func (s *Service) CreateAuthorization(ctx context.Context, params CreateAuthoriz
|
|||
}
|
||||
var allowed, isApp bool
|
||||
var nativeApp domain.TelegramLoginNativeApp
|
||||
redirectURI, domainName, redirectErr := NormalizeRedirectURI(params.RedirectURI, s.allowLoopbackHTTP)
|
||||
redirectURI, domainName, redirectErr := NormalizeRedirectURI(params.RedirectURI, s.allowHTTP)
|
||||
if params.ResponseType == "code" && redirectErr == nil && !params.NativePlatform.Valid() {
|
||||
allowed, err = s.store.IsTelegramLoginURLAllowed(ctx, client.BotUserID, domain.TelegramLoginAllowedRedirectURI, redirectURI)
|
||||
if err != nil {
|
||||
|
|
@ -603,14 +603,14 @@ func (s *Service) CreateAuthorization(ctx context.Context, params CreateAuthoriz
|
|||
u, _ := url.Parse(redirectURI)
|
||||
origin = u.Scheme + "://" + u.Host
|
||||
}
|
||||
origin, err = NormalizeWebOrigin(origin, s.allowLoopbackHTTP)
|
||||
origin, err = NormalizeWebOrigin(origin, s.allowHTTP)
|
||||
if err != nil {
|
||||
return CreatedAuthorization{}, err
|
||||
}
|
||||
}
|
||||
if params.ResponseType == "post_message" {
|
||||
redirectURL, _ := url.Parse(redirectURI)
|
||||
redirectOrigin, redirectOriginErr := NormalizeWebOrigin(redirectURL.Scheme+"://"+redirectURL.Host, s.allowLoopbackHTTP)
|
||||
redirectOrigin, redirectOriginErr := NormalizeWebOrigin(redirectURL.Scheme+"://"+redirectURL.Host, s.allowHTTP)
|
||||
if redirectOriginErr != nil || redirectOrigin != origin {
|
||||
return CreatedAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed
|
||||
}
|
||||
|
|
@ -627,7 +627,7 @@ func (s *Service) CreateAuthorization(ctx context.Context, params CreateAuthoriz
|
|||
if isApp {
|
||||
return CreatedAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed
|
||||
}
|
||||
inAppOrigin, err = NormalizeWebOrigin(params.InAppOrigin, s.allowLoopbackHTTP)
|
||||
inAppOrigin, err = NormalizeWebOrigin(params.InAppOrigin, s.allowHTTP)
|
||||
if err != nil {
|
||||
return CreatedAuthorization{}, err
|
||||
}
|
||||
|
|
@ -693,7 +693,7 @@ func (s *Service) ResolveAuthorizationErrorTarget(ctx context.Context, clientID,
|
|||
redirectURI, safe, err := s.safeCodeRedirect(ctx, client.BotUserID, rawRedirectURI)
|
||||
return AuthorizationErrorTarget{ResponseType: responseType, RedirectURI: redirectURI}, safe, err
|
||||
case "post_message":
|
||||
redirectURI, _, err := NormalizeRedirectURI(rawRedirectURI, s.allowLoopbackHTTP)
|
||||
redirectURI, _, err := NormalizeRedirectURI(rawRedirectURI, s.allowHTTP)
|
||||
if err != nil {
|
||||
return AuthorizationErrorTarget{}, false, nil
|
||||
}
|
||||
|
|
@ -702,12 +702,12 @@ func (s *Service) ResolveAuthorizationErrorTarget(ctx context.Context, clientID,
|
|||
redirect, _ := url.Parse(redirectURI)
|
||||
origin = redirect.Scheme + "://" + redirect.Host
|
||||
}
|
||||
origin, err = NormalizeWebOrigin(origin, s.allowLoopbackHTTP)
|
||||
origin, err = NormalizeWebOrigin(origin, s.allowHTTP)
|
||||
if err != nil {
|
||||
return AuthorizationErrorTarget{}, false, nil
|
||||
}
|
||||
redirect, _ := url.Parse(redirectURI)
|
||||
redirectOrigin, err := NormalizeWebOrigin(redirect.Scheme+"://"+redirect.Host, s.allowLoopbackHTTP)
|
||||
redirectOrigin, err := NormalizeWebOrigin(redirect.Scheme+"://"+redirect.Host, s.allowHTTP)
|
||||
if err != nil || redirectOrigin != origin {
|
||||
return AuthorizationErrorTarget{}, false, nil
|
||||
}
|
||||
|
|
@ -725,7 +725,7 @@ func (s *Service) ResolveAuthorizationErrorTarget(ctx context.Context, clientID,
|
|||
}
|
||||
|
||||
func (s *Service) safeCodeRedirect(ctx context.Context, botUserID int64, raw string) (string, bool, error) {
|
||||
if redirectURI, _, err := NormalizeRedirectURI(raw, s.allowLoopbackHTTP); err == nil {
|
||||
if redirectURI, _, err := NormalizeRedirectURI(raw, s.allowHTTP); err == nil {
|
||||
allowed, err := s.store.IsTelegramLoginURLAllowed(ctx, botUserID, domain.TelegramLoginAllowedRedirectURI, redirectURI)
|
||||
if err != nil || allowed {
|
||||
return redirectURI, allowed, err
|
||||
|
|
@ -856,7 +856,7 @@ func (s *Service) RequestByDeepLinkForOrigin(ctx context.Context, rawURL, rawOri
|
|||
if rawOrigin == "" || request.InAppOrigin == "" {
|
||||
return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginOriginNotAllowed
|
||||
}
|
||||
origin, err := NormalizeWebOrigin(rawOrigin, s.allowLoopbackHTTP)
|
||||
origin, err := NormalizeWebOrigin(rawOrigin, s.allowHTTP)
|
||||
if err != nil {
|
||||
return domain.TelegramLoginRequest{}, err
|
||||
}
|
||||
|
|
@ -1067,7 +1067,7 @@ func (s *Service) ExchangeInAppTokenAndIssue(ctx context.Context, token, rawOrig
|
|||
if issuer == nil || len(token) < 16 || len(token) > 1024 || strings.IndexFunc(token, func(r rune) bool { return r <= 0x20 || r == 0x7f }) >= 0 {
|
||||
return IssuedAuthorization{}, domain.ErrTelegramLoginCodeInvalid
|
||||
}
|
||||
origin, err := NormalizeWebOrigin(rawOrigin, s.allowLoopbackHTTP)
|
||||
origin, err := NormalizeWebOrigin(rawOrigin, s.allowHTTP)
|
||||
if err != nil {
|
||||
return IssuedAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed
|
||||
}
|
||||
|
|
@ -1294,7 +1294,7 @@ func (s *Service) exchangeAuthorizationCode(ctx context.Context, params Exchange
|
|||
return ExchangedAuthorization{}, "", domain.ErrTelegramLoginCodeInvalid
|
||||
}
|
||||
} else {
|
||||
redirectURI, _, err = NormalizeRedirectURI(params.RedirectURI, s.allowLoopbackHTTP)
|
||||
redirectURI, _, err = NormalizeRedirectURI(params.RedirectURI, s.allowHTTP)
|
||||
if err != nil {
|
||||
return ExchangedAuthorization{}, "", err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ func newTelegramLoginTestServiceWithAlgorithms(t *testing.T, now *time.Time, alg
|
|||
pepper[0] = 9
|
||||
service, err := NewService(loginStore, sealer, Config{
|
||||
Issuer: "https://oauth.telesrv.test", AppScheme: "telesrv",
|
||||
AllowLoopbackHTTP: true, ClientSecretPepper: pepper,
|
||||
AllowHTTP: true, ClientSecretPepper: pepper,
|
||||
SupportedSigningAlgorithms: algorithms,
|
||||
Now: func() time.Time { return *now },
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import (
|
|||
|
||||
const maxTelegramLoginURLLength = 4096
|
||||
|
||||
func NormalizeRedirectURI(raw string, allowLoopbackHTTP bool) (normalized, domainName string, err error) {
|
||||
u, err := parseWebURL(raw, allowLoopbackHTTP)
|
||||
func NormalizeRedirectURI(raw string, allowHTTP bool) (normalized, domainName string, err error) {
|
||||
u, err := parseWebURL(raw, allowHTTP)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
|
@ -34,8 +34,8 @@ func NormalizeRedirectURI(raw string, allowLoopbackHTTP bool) (normalized, domai
|
|||
return u.String(), u.Hostname(), nil
|
||||
}
|
||||
|
||||
func NormalizeWebOrigin(raw string, allowLoopbackHTTP bool) (string, error) {
|
||||
u, err := parseWebURL(raw, allowLoopbackHTTP)
|
||||
func NormalizeWebOrigin(raw string, allowHTTP bool) (string, error) {
|
||||
u, err := parseWebURL(raw, allowHTTP)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ func NormalizeWebOrigin(raw string, allowLoopbackHTTP bool) (string, error) {
|
|||
return u.String(), nil
|
||||
}
|
||||
|
||||
func parseWebURL(raw string, allowLoopbackHTTP bool) (*url.URL, error) {
|
||||
func parseWebURL(raw string, allowHTTP bool) (*url.URL, error) {
|
||||
if raw == "" || len(raw) > maxTelegramLoginURLLength || raw != strings.TrimSpace(raw) || strings.IndexFunc(raw, unicode.IsControl) >= 0 {
|
||||
return nil, domain.ErrTelegramLoginURLInvalid
|
||||
}
|
||||
|
|
@ -78,7 +78,7 @@ func parseWebURL(raw string, allowLoopbackHTTP bool) (*url.URL, error) {
|
|||
port = ""
|
||||
}
|
||||
case "http":
|
||||
if !allowLoopbackHTTP || !isLoopbackHost(host) {
|
||||
if !allowHTTP {
|
||||
return nil, domain.ErrTelegramLoginURLInvalid
|
||||
}
|
||||
if port == "80" {
|
||||
|
|
@ -99,14 +99,6 @@ func parseWebURL(raw string, allowLoopbackHTTP bool) (*url.URL, error) {
|
|||
return u, nil
|
||||
}
|
||||
|
||||
func isLoopbackHost(host string) bool {
|
||||
if host == "localhost" {
|
||||
return true
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
return ip != nil && ip.IsLoopback()
|
||||
}
|
||||
|
||||
func AppendAuthorizationResult(redirectURI, code, state string) (string, error) {
|
||||
u, err := url.Parse(redirectURI)
|
||||
if err != nil || !u.IsAbs() || code == "" {
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ func TestNormalizeRedirectURIIsExactAndRejectsOpenRedirectShapes(t *testing.T) {
|
|||
}{
|
||||
{name: "https canonical", raw: "https://EXAMPLE.com:443/callback?tenant=one", want: "https://example.com/callback?tenant=one", valid: true},
|
||||
{name: "idna", raw: "https://例子.测试/callback", want: "https://xn--fsqu00a.xn--0zwm56d/callback", valid: true},
|
||||
{name: "loopback dev", raw: "http://127.0.0.1:8080/callback", allowHTTP: true, want: "http://127.0.0.1:8080/callback", valid: true},
|
||||
{name: "http production", raw: "http://example.com/callback"},
|
||||
{name: "http hostname enabled", raw: "http://example.com:8080/callback", allowHTTP: true, want: "http://example.com:8080/callback", valid: true},
|
||||
{name: "http ipv4 enabled", raw: "http://192.0.2.25:3000/callback", allowHTTP: true, want: "http://192.0.2.25:3000/callback", valid: true},
|
||||
{name: "http disabled", raw: "http://example.com/callback"},
|
||||
{name: "userinfo", raw: "https://user@example.com/callback"},
|
||||
{name: "fragment", raw: "https://example.com/callback#token"},
|
||||
{name: "reserved code", raw: "https://example.com/callback?code=attacker"},
|
||||
|
|
@ -64,19 +65,19 @@ func TestNormalizeWebOriginRejectsPathAndQuery(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLoopbackIPv6PreservesURLBrackets(t *testing.T) {
|
||||
origin, err := NormalizeWebOrigin("http://[0:0:0:0:0:0:0:1]:80/", true)
|
||||
func TestNormalizeHTTPIPv6PreservesURLBrackets(t *testing.T) {
|
||||
origin, err := NormalizeWebOrigin("http://[2001:db8::25]:80/", true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if origin != "http://[0:0:0:0:0:0:0:1]" {
|
||||
if origin != "http://[2001:db8::25]" {
|
||||
t.Fatalf("origin=%q", origin)
|
||||
}
|
||||
redirect, domainName, err := NormalizeRedirectURI("http://[::1]/callback", true)
|
||||
redirect, domainName, err := NormalizeRedirectURI("http://[2001:db8::26]:3000/callback", true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if redirect != "http://[::1]/callback" || domainName != "::1" {
|
||||
if redirect != "http://[2001:db8::26]:3000/callback" || domainName != "2001:db8::26" {
|
||||
t.Fatalf("redirect=%q domain=%q", redirect, domainName)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1077,11 +1077,18 @@ func validateWebhookURL(raw string) error {
|
|||
return errors.New("WEBHOOK_URL_INVALID")
|
||||
}
|
||||
u, err := neturl.ParseRequestURI(raw)
|
||||
if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.Fragment != "" {
|
||||
if err != nil {
|
||||
return errors.New("WEBHOOK_URL_INVALID")
|
||||
}
|
||||
scheme := strings.ToLower(u.Scheme)
|
||||
if (scheme != "http" && scheme != "https") || u.Hostname() == "" || u.User != nil || u.Fragment != "" {
|
||||
return errors.New("WEBHOOK_URL_INVALID")
|
||||
}
|
||||
if port := u.Port(); port != "" {
|
||||
n, err := strconv.Atoi(port)
|
||||
if err != nil || n < 1 || n > 65535 {
|
||||
return errors.New("WEBHOOK_URL_INVALID")
|
||||
}
|
||||
if port := u.Port(); port != "" && port != "443" && port != "80" && port != "88" && port != "8443" {
|
||||
return errors.New("WEBHOOK_PORT_NOT_ALLOWED")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
|
@ -977,6 +978,23 @@ func TestSetWebhookPersistsConfigReportsInfoAndConflictsWithPolling(t *testing.T
|
|||
}
|
||||
}
|
||||
|
||||
func TestSetWebhookAcceptsHTTPHostIPAndArbitraryPort(t *testing.T) {
|
||||
bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}}
|
||||
for _, rawURL := range []string{
|
||||
"http://bot.example.test:3000/hook",
|
||||
"http://192.0.2.25:18080/hook",
|
||||
"http://[2001:db8::25]:28080/hook",
|
||||
"HTTP://bot.example.test:3100/hook",
|
||||
} {
|
||||
gateway := &fakeBotAPIGateway{}
|
||||
h := (&handler{bots: bots, gateway: gateway}).routes()
|
||||
rec := performBotAPIRequest(t, h, bots.profile, "setWebhook", fmt.Sprintf(`{"url":%q}`, rawURL))
|
||||
if rec.Code != http.StatusOK || !gateway.webhookFound || gateway.webhook.URL != rawURL {
|
||||
t.Fatalf("setWebhook url=%q status=%d body=%s config=%#v", rawURL, rec.Code, rec.Body.String(), gateway.webhook)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetWebhookRejectsUnsafeParameters(t *testing.T) {
|
||||
bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}}
|
||||
h := (&handler{bots: bots, gateway: &fakeBotAPIGateway{}}).routes()
|
||||
|
|
@ -984,8 +1002,9 @@ func TestSetWebhookRejectsUnsafeParameters(t *testing.T) {
|
|||
body string
|
||||
want string
|
||||
}{
|
||||
{`{"url":"http://example.test/hook"}`, "WEBHOOK_URL_INVALID"},
|
||||
{`{"url":"https://example.test:444/hook"}`, "WEBHOOK_PORT_NOT_ALLOWED"},
|
||||
{`{"url":"ftp://example.test/hook"}`, "WEBHOOK_URL_INVALID"},
|
||||
{`{"url":"http://user@example.test/hook"}`, "WEBHOOK_URL_INVALID"},
|
||||
{`{"url":"http://example.test:0/hook"}`, "WEBHOOK_URL_INVALID"},
|
||||
{`{"url":"https://example.test/hook","secret_token":"bad secret"}`, "SECRET_TOKEN_INVALID"},
|
||||
{`{"url":"https://example.test/hook","max_connections":101}`, "MAX_CONNECTIONS_INVALID"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ package config
|
|||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
|
|
@ -95,7 +94,9 @@ type Config struct {
|
|||
// process listings or accidentally copied into tracked .env templates.
|
||||
TelegramLoginEnabled bool
|
||||
TelegramLoginIssuer string
|
||||
TelegramLoginAllowLoopbackHTTP bool
|
||||
// TelegramLoginAllowHTTP permits HTTP issuers and registered Login URLs on
|
||||
// any valid host/IP and port. HTTPS remains mandatory when false.
|
||||
TelegramLoginAllowHTTP bool
|
||||
TelegramLoginSigningKeysFile string
|
||||
TelegramLoginCodeKeysFile string
|
||||
TelegramLoginSecretPepperFile string
|
||||
|
|
@ -491,7 +492,7 @@ func Load() (Config, error) {
|
|||
PublicLinkWebAddr: envAllowEmptyOr("TELESRV_PUBLIC_LINK_WEB_ADDR", ""),
|
||||
TelegramLoginEnabled: envBoolOr("TELESRV_TELEGRAM_LOGIN_ENABLE", false),
|
||||
TelegramLoginIssuer: strings.TrimSuffix(envOr("TELESRV_TELEGRAM_LOGIN_ISSUER", publicBaseURL), "/"),
|
||||
TelegramLoginAllowLoopbackHTTP: envBoolOr("TELESRV_TELEGRAM_LOGIN_ALLOW_LOOPBACK_HTTP", false),
|
||||
TelegramLoginAllowHTTP: envBoolOr("TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP", false),
|
||||
TelegramLoginSigningKeysFile: envOr("TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE", "data/telegram-login/signing-keys.json"),
|
||||
TelegramLoginCodeKeysFile: envOr("TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE", "data/telegram-login/code-keys.json"),
|
||||
TelegramLoginSecretPepperFile: envOr("TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE", "data/telegram-login/client-secret-pepper"),
|
||||
|
|
@ -681,10 +682,8 @@ func validateTelegramLoginConfig(cfg Config) error {
|
|||
switch issuer.Scheme {
|
||||
case "https":
|
||||
case "http":
|
||||
host := issuer.Hostname()
|
||||
ip := net.ParseIP(host)
|
||||
if !cfg.TelegramLoginAllowLoopbackHTTP || (host != "localhost" && (ip == nil || !ip.IsLoopback())) {
|
||||
return fmt.Errorf("TELESRV_TELEGRAM_LOGIN_ISSUER http is allowed only for explicit loopback development")
|
||||
if !cfg.TelegramLoginAllowHTTP {
|
||||
return fmt.Errorf("TELESRV_TELEGRAM_LOGIN_ISSUER http requires TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=true")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("TELESRV_TELEGRAM_LOGIN_ISSUER must use https")
|
||||
|
|
|
|||
|
|
@ -435,8 +435,8 @@ func TestLoadTelegramLoginConfig(t *testing.T) {
|
|||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_PUBLIC_LINK_WEB_ADDR", "127.0.0.1:2401")
|
||||
t.Setenv("TELESRV_TELEGRAM_LOGIN_ENABLE", "true")
|
||||
t.Setenv("TELESRV_TELEGRAM_LOGIN_ISSUER", "http://127.0.0.1:2401/")
|
||||
t.Setenv("TELESRV_TELEGRAM_LOGIN_ALLOW_LOOPBACK_HTTP", "true")
|
||||
t.Setenv("TELESRV_TELEGRAM_LOGIN_ISSUER", "http://192.0.2.25:2401/")
|
||||
t.Setenv("TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP", "true")
|
||||
t.Setenv("TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE", "secrets/signing.json")
|
||||
t.Setenv("TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE", "secrets/codes.json")
|
||||
t.Setenv("TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE", "secrets/pepper")
|
||||
|
|
@ -452,8 +452,8 @@ func TestLoadTelegramLoginConfig(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if !cfg.TelegramLoginEnabled || cfg.TelegramLoginIssuer != "http://127.0.0.1:2401" || !cfg.TelegramLoginAllowLoopbackHTTP {
|
||||
t.Fatalf("telegram login endpoint config = enabled:%v issuer:%q loopback:%v", cfg.TelegramLoginEnabled, cfg.TelegramLoginIssuer, cfg.TelegramLoginAllowLoopbackHTTP)
|
||||
if !cfg.TelegramLoginEnabled || cfg.TelegramLoginIssuer != "http://192.0.2.25:2401" || !cfg.TelegramLoginAllowHTTP {
|
||||
t.Fatalf("telegram login endpoint config = enabled:%v issuer:%q allow_http:%v", cfg.TelegramLoginEnabled, cfg.TelegramLoginIssuer, cfg.TelegramLoginAllowHTTP)
|
||||
}
|
||||
if cfg.TelegramLoginSigningKeysFile != "secrets/signing.json" || cfg.TelegramLoginCodeKeysFile != "secrets/codes.json" || cfg.TelegramLoginSecretPepperFile != "secrets/pepper" {
|
||||
t.Fatalf("telegram login secret files = %q / %q / %q", cfg.TelegramLoginSigningKeysFile, cfg.TelegramLoginCodeKeysFile, cfg.TelegramLoginSecretPepperFile)
|
||||
|
|
@ -484,11 +484,7 @@ func TestValidateTelegramLoginConfigRejectsUnsafeOrUnboundedSettings(t *testing.
|
|||
}{
|
||||
{name: "missing listener", mutate: func(c *Config) { c.PublicLinkWebAddr = "" }},
|
||||
{name: "issuer path", mutate: func(c *Config) { c.TelegramLoginIssuer = "https://login.example.test/oauth" }},
|
||||
{name: "public http", mutate: func(c *Config) {
|
||||
c.TelegramLoginIssuer = "http://login.example.test"
|
||||
c.TelegramLoginAllowLoopbackHTTP = true
|
||||
}},
|
||||
{name: "loopback http disabled", mutate: func(c *Config) { c.TelegramLoginIssuer = "http://127.0.0.1:2401" }},
|
||||
{name: "http disabled", mutate: func(c *Config) { c.TelegramLoginIssuer = "http://192.0.2.25:2401" }},
|
||||
{name: "missing key file", mutate: func(c *Config) { c.TelegramLoginSigningKeysFile = "" }},
|
||||
{name: "request ttl too long", mutate: func(c *Config) { c.TelegramLoginRequestTTL = 16 * time.Minute }},
|
||||
{name: "code ttl too short", mutate: func(c *Config) { c.TelegramLoginCodeTTL = 29 * time.Second }},
|
||||
|
|
@ -508,6 +504,22 @@ func TestValidateTelegramLoginConfigRejectsUnsafeOrUnboundedSettings(t *testing.
|
|||
}
|
||||
}
|
||||
|
||||
func TestValidateTelegramLoginConfigAcceptsHTTPHostAndIPWhenEnabled(t *testing.T) {
|
||||
valid := Config{
|
||||
TelegramLoginEnabled: true, TelegramLoginAllowHTTP: true, PublicLinkWebAddr: "127.0.0.1:2401",
|
||||
TelegramLoginSigningKeysFile: "signing.json", TelegramLoginCodeKeysFile: "codes.json", TelegramLoginSecretPepperFile: "pepper",
|
||||
TelegramLoginRequestTTL: 5 * time.Minute, TelegramLoginCodeTTL: 2 * time.Minute, TelegramLoginIDTokenTTL: time.Hour,
|
||||
TelegramLoginRetention: 7 * 24 * time.Hour, TelegramLoginSweepInterval: 5 * time.Minute, TelegramLoginSweepBatch: 500,
|
||||
}
|
||||
for _, issuer := range []string{"http://login.example.test:3000", "http://192.0.2.25:2401", "http://[2001:db8::25]:2401"} {
|
||||
cfg := valid
|
||||
cfg.TelegramLoginIssuer = issuer
|
||||
if err := validateTelegramLoginConfig(cfg); err != nil {
|
||||
t.Fatalf("issuer %q was rejected: %v", issuer, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidPublicBaseURL(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_PUBLIC_BASE_URL", "https://links.example.test/root?tenant=one")
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package domain
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
|
@ -397,8 +396,9 @@ func validateButtonURL(raw string) error {
|
|||
|
||||
// validateLoginButtonURL performs only the protocol-shape validation shared by
|
||||
// Bot API and MTProto input buttons. The Telegram Login service remains the
|
||||
// authority for the deployment policy: it rejects loopback HTTP unless the
|
||||
// explicit development switch is enabled and the exact origin is registered.
|
||||
// authority for the deployment policy: HTTP is accepted here as a protocol
|
||||
// shape, then allowed only when the Login HTTP switch is enabled and the exact
|
||||
// origin is registered.
|
||||
func validateLoginButtonURL(raw string) error {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" || len(raw) > MaxBotMenuButtonURLLen {
|
||||
|
|
@ -408,15 +408,8 @@ func validateLoginButtonURL(raw string) error {
|
|||
if err != nil || u.Host == "" || u.User != nil {
|
||||
return ErrButtonURLInvalid
|
||||
}
|
||||
if u.Scheme == "https" {
|
||||
return nil
|
||||
}
|
||||
if u.Scheme != "http" {
|
||||
return ErrButtonURLInvalid
|
||||
}
|
||||
host := strings.ToLower(u.Hostname())
|
||||
ip := net.ParseIP(host)
|
||||
if host != "localhost" && (ip == nil || !ip.IsLoopback()) {
|
||||
scheme := strings.ToLower(u.Scheme)
|
||||
if scheme != "http" && scheme != "https" {
|
||||
return ErrButtonURLInvalid
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ func TestValidateReplyMarkup(t *testing.T) {
|
|||
{"url empty bad", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonURL, Text: "go", URL: ""}}}}, ErrButtonURLInvalid},
|
||||
{"login url loopback http ok", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "http://127.0.0.1:8080/login"}}}}, nil},
|
||||
{"login url localhost http ok", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "http://localhost:8080/login"}}}}, nil},
|
||||
{"login url public http bad", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "http://example.com/login"}}}}, ErrButtonURLInvalid},
|
||||
{"login url public http host ok", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "http://example.com:3000/login"}}}}, nil},
|
||||
{"login url public http ip ok", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "http://192.0.2.25:18080/login"}}}}, nil},
|
||||
{"login url credentials bad", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "https://user@example.com/login"}}}}, ErrButtonURLInvalid},
|
||||
{"unknown type bad", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: "rainbow", Text: "x"}}}}, ErrButtonTypeInvalid},
|
||||
{"reply keyboard ok", &MessageReplyMarkup{Type: MessageReplyMarkupKeyboard, Keyboard: [][]MarkupButton{{{Type: MarkupButtonText, Text: "Help"}}}, Resize: true, Persistent: true, Placeholder: "Choose"}, nil},
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ func TestTelegramLoginMessageButtonRereadSignsAndGrantsWriteAccess(t *testing.T)
|
|||
pepper[0] = 8
|
||||
loginStore := memory.NewTelegramLoginStore(telegramLoginBotPermissionAdapter{bots: f.router.deps.Bots})
|
||||
login, err := telegramloginapp.NewService(loginStore, sealer, telegramloginapp.Config{
|
||||
Issuer: "https://oauth.test", AppScheme: "telesrv", ClientSecretPepper: pepper,
|
||||
Issuer: "http://192.0.2.25:2401", AppScheme: "telesrv", AllowHTTP: true, ClientSecretPepper: pepper,
|
||||
Now: func() time.Time { return time.Unix(1_780_000_000, 0).UTC() },
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -270,13 +270,13 @@ func TestTelegramLoginMessageButtonRereadSignsAndGrantsWriteAccess(t *testing.T)
|
|||
if _, err := login.CreateClient(f.ctx, f.bot.ID, domain.TelegramLoginSigningRS256); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := login.AddAllowedURL(f.ctx, f.bot.ID, domain.TelegramLoginAllowedWebOrigin, "https://rp.test"); err != nil {
|
||||
if _, err := login.AddAllowedURL(f.ctx, f.bot.ID, domain.TelegramLoginAllowedWebOrigin, "http://rp.test:3000"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.router.deps.TelegramLogin = login
|
||||
|
||||
markup := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline, Inline: [][]domain.MarkupButton{{{
|
||||
Type: domain.MarkupButtonLoginURL, Text: "Log in", URL: "https://rp.test/login?next=%2Fhome", RequestWriteAccess: true,
|
||||
Type: domain.MarkupButtonLoginURL, Text: "Log in", URL: "http://rp.test:3000/login?next=%2Fhome", RequestWriteAccess: true,
|
||||
}}}}
|
||||
if _, err := f.router.BotAPISendMessage(f.ctx, f.bot.ID, f.owner.ID, "Authorize", nil, markup, false, false, 0); err != nil {
|
||||
t.Fatalf("BotAPISendMessage: %v", err)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ type Config struct {
|
|||
AppName string
|
||||
Logger *zap.Logger
|
||||
TrustedProxyCIDRs []string
|
||||
AllowLoopbackHTTP bool
|
||||
AllowHTTP bool
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
|
|
@ -50,7 +50,7 @@ type Handler struct {
|
|||
logger *zap.Logger
|
||||
limiter RateLimiter
|
||||
trustedProxies []netip.Prefix
|
||||
allowLoopbackHTTP bool
|
||||
allowHTTP bool
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ func NewHandler(cfg Config) (*Handler, error) {
|
|||
}
|
||||
trustedProxies = append(trustedProxies, prefix.Masked())
|
||||
}
|
||||
h := &Handler{service: cfg.Service, tokens: cfg.Tokens, appName: strings.TrimSpace(cfg.AppName), logger: cfg.Logger, limiter: cfg.Limiter, trustedProxies: trustedProxies, allowLoopbackHTTP: cfg.AllowLoopbackHTTP}
|
||||
h := &Handler{service: cfg.Service, tokens: cfg.Tokens, appName: strings.TrimSpace(cfg.AppName), logger: cfg.Logger, limiter: cfg.Limiter, trustedProxies: trustedProxies, allowHTTP: cfg.AllowHTTP}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /.well-known/openid-configuration", h.discovery)
|
||||
mux.HandleFunc("GET /.well-known/jwks.json", h.jwks)
|
||||
|
|
@ -333,7 +333,7 @@ func (h *Handler) inApp(w http.ResponseWriter, r *http.Request) {
|
|||
writeOAuthError(w, http.StatusBadRequest, "unsupported_response_type", "only id_token is supported")
|
||||
return
|
||||
}
|
||||
origin, err := loginapp.NormalizeWebOrigin(values["origin"], h.allowLoopbackHTTP)
|
||||
origin, err := loginapp.NormalizeWebOrigin(values["origin"], h.allowHTTP)
|
||||
if err != nil || r.Header.Get("Origin") != origin {
|
||||
writeOAuthError(w, http.StatusBadRequest, "invalid_request", "in-app origin is invalid")
|
||||
return
|
||||
|
|
@ -511,7 +511,7 @@ func (h *Handler) authorizeStatusOrigin(w http.ResponseWriter, r *http.Request,
|
|||
if origin == "" {
|
||||
return true
|
||||
}
|
||||
issuerOrigin, err := loginapp.NormalizeWebOrigin(h.tokens.Issuer(), h.allowLoopbackHTTP)
|
||||
issuerOrigin, err := loginapp.NormalizeWebOrigin(h.tokens.Issuer(), h.allowHTTP)
|
||||
if err == nil && origin == issuerOrigin {
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ func newTelegramLoginHTTPFixture(t *testing.T) telegramLoginHTTPFixture {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
handler, err := NewHandler(Config{Service: service, Tokens: tokens, AppName: "Telesrv", AllowLoopbackHTTP: true})
|
||||
handler, err := NewHandler(Config{Service: service, Tokens: tokens, AppName: "Telesrv", AllowHTTP: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue