Telekot is a minimal Telegram-based OpenID Connect provider. Lets your OIDC-compatible apps and websites accept "Sign in with Telegram", restricted to members of a specific Telegram chat/group/channel.
⚠️ What? TL;DR: Only your telegram friends (members of a group) can access your apps and websites.
Built on zitadel/oidc — a certified Go OpenID Provider library.
- Your app redirects the user to
/authorize(standard OIDC authorization code flow, PKCE supported). - Telekot redirects to a Telegram Login Widget page.
- The user approves via Telegram; Telegram redirects back to Telekot with signed user data (verified via HMAC-SHA256, per the Login Widget spec).
- If the OAuth client's mode is
group_membership, Telekot checks (via the Bot APIgetChatMember) that the user is a member of the configuredaccess_chat_id. Non-members are rejected. Clients inpublicmode skip this check and let any Telegram user through. - Telekot completes the OIDC authorization request and redirects back to your app with a code (or errors out).
- Your app exchanges the code for tokens at
/oauth/tokenas usual. The ID token and userinfo includesub,name,given_name,family_name,picture,preferred_username, a synthesized stableemail(telegram.<id>@$EMAIL_DOMAIN), and a customgroupclaim.
Note
The synthesized user email address is just for protocol compliance. It is not functional.
example .env file:
TELEGRAM_TOKEN=123456:your-bot-token
TELEGRAM_BOT=your_bot_username
URL=https://telekot.example.com
EMAIL_DOMAIN=tg.example.com
DATA_PATH=./data
LISTEN_ADDR=:8075
ENABLE_LOGIN_LOGGING=true
LOG_RETENTION_DAYS=90
LOG_IP_ADDRESSES=true
DATA_PATH holds the SQLite database and the persisted RSA signing key (generated on first run).
In @BotFather, create your bot and set your domain for the login widget.
go build -o telekot ./cmd/telekot
./telekot serveOr with Docker:
docker compose up -d --buildManage OAuth clients and inspect login logs:
# Register a client restricted to members of a Telegram chat/group/channel
./telekot client add my-app \
--redirect-uris=https://my-app.example.com/callback \
--secret=some-strong-secret \
--mode=group_membership \
--access-chat-id=@my_group_or_chat_id
# Register a client open to any Telegram user
./telekot client add my-public-app \
--redirect-uris=https://my-public-app.example.com/callback \
--mode=public
# Update existing clients
./telekot client update my-app --access-chat-id=-100123456789
./telekot client update my-public-app --mode=group_membership --access-chat-id=-100123456789
# List, inspect, or remove clients
./telekot client list
./telekot client show my-app
./telekot client rm my-app
# Login audit log
./telekot logs list --limit=50 --client=my-app
./telekot logs prune # deletes logs older than LOG_RETENTION_DAYSStandard OIDC discovery is served at /.well-known/openid-configuration, exposing /authorize, /oauth/token, /userinfo, /keys (JWKS), etc. — all implemented by zitadel/oidc. Telekot adds the Telegram-specific login UI at /telegram/initiate and /telegram/callback.
Auth plugins for reverse proxies allow you to protect to create a login page for any website. Below are instructions for Caddy but others (Nginx, Traefik, etc.) have similar plugins too.
Caddy
Example Caddyfile for server:
telekot.example.com {
reverse_proxy localhost:8075
}
Example Caddyfile for your protected apps (uses caddy security plugin):
{
order authenticate before respond
order authorize before basicauth
security {
oauth identity provider generic {
realm generic
driver generic
client_id my-app
client_secret your-client-secret
scopes openid email profile
base_auth_url https://telekot.example.com/
metadata_url https://telekot.example.com/.well-known/openid-configuration
}
authentication portal my-portal {
# 3600 = 1 hour
crypto default token lifetime 10800
enable identity provider generic
cookie domain example.com
transform user {
match realm generic
action add role authp/user
}
}
authorization-code-callback
authorization policy plausiblepolicy {
bypass uri prefix /api/
bypass uri prefix /js/
set auth url https://auth.example.com/oauth2/generic
allow roles authp/admin authp/user
validate bearer header
inject headers with claims
}
}
}
auth.example.com {
authenticate with my-portal
}
homepage.example.com {
authorize with plausiblepolicy
reverse_proxy localhost:3311
}
immich.example.com {
authorize with plausiblepolicy
reverse_proxy localhost:2283
}
plausible.example.com {
authorize with plausiblepolicy
reverse_proxy localhost:8181
}
Add the callback url https://auth.example.com/oauth2/generic/ to redirect-uris in your client settings.
Optional bypass uri prefix /api/ and bypass uri prefix /js/ will allow you to use use your apps' API etc. without auth. Just be careful about what prefixes you allow. You can also have multiple authorization policies for different apps.
You can use a different domain for server and apps.
Tip: You can download caddy binary with security plugin at caddyserver.com/download
Some apps natively support OpenID/OAuth login: Outline, Gitea, and many more.
Tip: Immich does have OAuth support, but I still use Caddy-security for it to protect the "public" links for sharing albums.




