- Go 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
# Why The bridge already records each user's chat_id at login; this uses it. A Forgejo system webhook posts to /forgejo-hook and the involved users get a DM, so notifications need no changes to Forgejo itself (its mailer is disabled). Routing keys off X-Forgejo-Event-Type rather than X-Forgejo-Event, which collapses review-comment and PR-comment into one ambiguous value. Recipients are owner + author + assignees + reviewers minus the actor. Label, milestone, assign and branch-sync events are dropped as noise. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
| deploy | ||
| .gitignore | ||
| env.example | ||
| go.mod | ||
| main.go | ||
| notify_test.go | ||
| README.md | ||
tg-oidc
Minimal OpenID Connect provider bridging Telegram Login → Forgejo, so
code.pecheny.me can offer "Sign in with Telegram". Pure Go standard library,
single static binary, no external dependencies.
Why deep-link login
Telegram's Login Widget is locked to one domain per bot. This instance is also
reachable via code.pecheny.kz (a censorship-circumvention alias), so a widget
would need a second bot. Instead we use a deep link (t.me/<bot>?start=<sid>),
which is domain-agnostic: one bot serves every front-end domain.
As a bonus, deep-link login requires the user to press Start, which is exactly
what lets a bot DM them later — so each login records the user's chat_id
(OIDC_STORE_PATH), which powers notifications below.
Flow
- Forgejo →
/authorize→ bridge renders a page with at.me/<bot>?start=<sid>link and a match code; the browser polls/poll?sid=<sid>. - User taps the link → Telegram sends
/start <sid>to the bot via webhook. - Bot asks the user to confirm (inline button); the match code lets them verify they're approving their own sign-in (login-CSRF guard).
- Confirm → session
ready;/pollmints the OAuthcodeand returns the Forgejo redirect (on whichever domain the user started). - Forgejo
/token→ RS256id_token.
Telegram gives no email, so the bridge synthesizes <user>@OIDC_EMAIL_DOMAIN.
Multi-domain
ROOT_URL pins Forgejo's OAuth URLs to code.pecheny.me. The .kz relay
rewrites the redirect Location host code.pecheny.me → code.pecheny.kz, so
the whole flow stays on .kz. The bridge accepts any redirect host in
OIDC_ALT_HOST (allowlisted — prevents open redirects) and returns the user to
the host they started on. Forgejo's server-side token call still uses .me; the
bridge compares redirect_uri on the canonical host so both legs match.
Notifications
A Forgejo system webhook (type Forgejo, POST /forgejo-hook) turns repo
activity into DMs. Forgejo itself needs no patching — only that webhook plus
ALLOWED_HOST_LIST = loopback in app.ini.
- Auth: HMAC-SHA256 of the raw body against
FORGEJO_WEBHOOK_SECRET, read fromX-Forgejo-Signature. Unset secret ⇒ the endpoint 503s. - Events: issues and PRs opened/closed/reopened/merged, comments, and
reviews. Routing keys off
X-Forgejo-Event-Type, notX-Forgejo-Event— the latter collapses review kinds into an ambiguouspull_request_comment. Label, milestone, assign and branch-sync events are deliberately dropped. - Recipients: repo owner, thread author, assignees and requested reviewers,
deduped, minus the actor (no self-notifications). Logins with no recorded
chat_idare skipped — a user gets DMs only once they've logged in via the bridge. - Forgejo's login equals the Telegram
@username(it comes frompreferred_username), which is what makes the payload→chat_idjoin work.
Note a merge arrives as action: "closed" with pull_request.merged == true;
there is no merged action.
Endpoints
/.well-known/openid-configuration, /jwks.json, /authorize, /poll,
/tg-webhook (Telegram, secret-header protected), /forgejo-hook
(Forgejo, HMAC-signed), /token, /userinfo, /healthz.
Build & deploy
CGO_ENABLED=0 go build -trimpath -ldflags '-s -w' -o tg-oidc .
install -o root -g tgoidc -m 750 tg-oidc /opt/telegram-oidc/tg-oidc
# config: see env.example -> /opt/telegram-oidc/tg-oidc.env
# service: see deploy/tg-oidc.service
systemctl restart tg-oidc
Mounted behind Caddy at the issuer path (handle_path /-/telegram-oauth/* strips
the prefix so the bridge sees root routes). The webhook is (re)registered with
Telegram on every start.
Config
See env.example.