Skip to content

Repository files navigation

Authentik WeChat Connect

A Go middleware service that turns WeChat Official Account login into a standard OAuth/OIDC provider for Authentik Generic OAuth Sources. It also handles pushed Official Account messages, managed passive replies, and API-managed custom menus for accounts authorized to publish them.

Login Flow

  1. The user selects WeChat login in Authentik.
  2. Authentik redirects the browser to this service at /oauth/authorize.
  3. For a verified Service Account, this service creates a temporary parameterized QR code and shows the scan page.
  4. The user scans the QR code in WeChat. The Official Account sends a SCAN or subscribe event to /wechat/callback.
  5. This service matches the QR-code scene to the original Authentik authorization request and creates an authorization code.
  6. The scan page polls until the callback arrives, shows a successful binding/login state, and redirects back to the Authentik Source callback.
  7. Authentik calls /oauth/token and /oauth/userinfo to finish login or account binding.

WeChat only authorizes qrcode/create for verified Service Accounts. With the default WECHAT_LOGIN_MODE=auto, error 48001 switches this process to an Official Account message-code flow: the page displays the configured permanent account QR code and an eight-digit one-time code, and the user sends those eight digits directly to the account. The validated message callback supplies the OpenID and completes the same browser session. This extra message is necessary because an ordinary account QR code has no per-login scene.

Endpoints

All paths below belong to this middleware. WeChat only receives and calls the /wechat/callback URL configured in its console; Authentik uses the OIDC paths, the browser uses scan paths, and the management page uses /api/admin/... internally.

  • Discovery: /.well-known/openid-configuration
  • Authorize: /oauth/authorize
  • Token: /oauth/token
  • UserInfo: /oauth/userinfo
  • JWKS: /oauth/jwks
  • WeChat server callback: /wechat/callback
  • WeChat management page: /admin/wechat
  • Management state: GET /api/admin/wechat/state
  • Reply rules: PUT /api/admin/wechat/replies
  • Menu draft: PUT /api/admin/wechat/menu
  • Publish menu: POST /api/admin/wechat/menu/publish
  • Read/delete remote menu: GET|DELETE /api/admin/wechat/menu/remote
  • Import website text menus as keyword replies: POST /api/admin/wechat/menu/remote/import-text-replies
  • Scan page: /scan/{id}
  • Scan status: /api/scan/{id}
  • Health check: /healthz

WeChat Official Account Setup

Configure the server URL in the WeChat Official Account admin console:

${PUBLIC_URL}/wechat/callback

Set the WeChat callback token to the exact same value as WECHAT_CALLBACK_TOKEN.

  • Plaintext mode verifies signature, requires a fresh timestamp, and prevents the same signature tuple from being reused with a different body. WeChat's plaintext signature does not cryptographically cover the XML body, so safe mode should be used for login deployments.
  • Compatibility and safe modes verify msg_signature, decrypt the callback with WECHAT_ENCODING_AES_KEY, validate the embedded AppID, and encrypt passive replies. Safe mode is recommended.
  • The EncodingAESKey is the 43-character value configured in the WeChat console. WECHAT_APP_ID must also be set when AES is enabled.

Enabling message push (formerly server configuration) disables the automatic replies and custom menus configured on the WeChat website. A cached old menu may remain visible in a client, but tapping it does not turn a website text/value action into a CLICK/EventKey callback. Configure replacement replies through this service before enabling push. The website has no API that can write those old rules, so this service stores its own managed rules.

For verified Service Accounts, the service uses temporary parameterized QR codes:

  • If the user already follows the account, WeChat sends Event=SCAN and EventKey=<scene>.
  • If the user follows the account after scanning, WeChat sends Event=subscribe and EventKey=qrscene_<scene>.
  • WeChat does not send a separate start field. The parameter analogous to a start parameter is the QR-code scene.
  • Login scenes use the login:<random-session-id> namespace. A menu click is a separate Event=CLICK with EventKey=<button key> and can never complete a login scan.
  • FromUserName is used as the user's Official Account OpenID. If the user-info API is available, the service also adds nickname, avatar, UnionID, and profile claims.

WECHAT_LOGIN_MODE controls the login method:

  • auto tries the parameterized QR API and falls back to a one-time message code only when WeChat returns 48001 (api unauthorized). The unsupported capability is cached until the process restarts.
  • parameter_qr requires a verified Service Account and never falls back.
  • message_code skips the restricted QR API and always uses the ordinary Official Account message flow.

For auto fallback or message_code, set WECHAT_ACCOUNT_QR_CODE_URL to an absolute HTTP(S) URL containing the permanent QR image downloaded from the Official Account console. It must use HTTPS when PUBLIC_URL does. WECHAT_ACCOUNT_NAME is optional display text. Without an image URL, the login page still shows the one-time code and asks the user to enter the configured account manually. Each login uses an eight-digit one-time numeric code (including possible leading zeroes), sent as the entire message without a prefix. It expires with WECHAT_QR_CODE_TTL (capped at 10 minutes for numeric codes) and is consumed once. Invalid guesses are limited per OpenID and globally. Only callbacks accepted by the configured plaintext/AES transport validation can use the code. A bare subscribe event or a menu CLICK never guesses or completes a message-code session.

Replies And Menu Management

Set a strong WECHAT_ADMIN_TOKEN, start the service, and open:

${PUBLIC_URL}/admin/wechat

The token is sent as a Bearer credential and is kept in browser sessionStorage, so it is cleared when the browser session ends. It is independent from OIDC_CLIENT_SECRET.

Reply rules are ordered and use the first match. They can match standard messages (text, image, voice, video, shortvideo, location, and link) or events such as subscribe, parameter QR scan, and menu click. Match modes are any, exact, contains, prefix, and RE2 regular expressions. click matches the button key; menu scan events match the decoded scan result; photo events match selected-image MD5 values; location selection matches its label/place name. An optional default reply applies only to standard messages, never events.

Passive reply types are text, image, voice, video, music, news, official AI, and customer-service transfer. Media replies require a Media ID already uploaded to WeChat. Official AI emits the documented transfer_biz_ai_ivr message type; it only works for standard user messages when the account has WeChat AI reply enabled and its historical articles have finished training. It is distinct from transfer_customer_service.

The management page exposes two deliberately separate paths:

  • For a personal or unverified Subscription Account, Import text buttons as keyword replies reads /cgi-bin/get_current_selfmenu_info and creates exact text rules whose patterns are the button names. Users then send the complete button name in the conversation; this does not restore tapping the disabled website menu. The import enables managed replies, changes replies only, preserves the API menu draft and hand-written rules, and replaces only a previous keyword import. It atomically rejects duplicate names, eight-digit numeric names reserved for login, mixed non-text actions, invalid reply content, and any keyword shadowed by an existing enabled rule.
  • The advanced API menu editor saves a local menu/create draft. Save draft does not change WeChat. Publish explicitly calls /cgi-bin/menu/create; reading the current menu calls /cgi-bin/get_current_selfmenu_info; deleting calls /cgi-bin/menu/delete. Only an active (is_menu_open=1) API-shaped current menu can be imported as a draft. Website-only text, img, voice, video, and news actions are never fabricated into API click/key actions.

Reading and publishing have different account permissions. An account may be allowed to call /cgi-bin/get_current_selfmenu_info but not /cgi-bin/menu/create. The official permission table marks Subscription Account menu creation as 仅认证, defined as enterprise-subject verified accounts only; personal and unverified Subscription Accounts therefore cannot publish API-managed menus. Publishing also requires the AppSecret API-call IP allowlist to include this service. A 48001 publish failure leaves the saved draft, reply rules, and management revision unchanged.

Example menu draft:

{
  "button": [
    {"type": "click", "name": "帮助", "key": "help"},
    {"type": "view", "name": "网站", "url": "https://example.com/"}
  ]
}

Use a click reply rule with an exact pattern of help to reply to the first button. The API uses strong revision ETags: fetch /api/admin/wechat/state, then send its ETag in If-Match when replacing replies, saving the menu draft, or publishing it. This prevents two open management pages from silently overwriting or publishing each other's drafts.

Authentik Setup

Create a Generic OAuth Source in Authentik. The recommended configuration is the discovery URL:

https://wechat-connect.example.com/.well-known/openid-configuration

You can also configure the endpoints manually:

Authorization URL: https://wechat-connect.example.com/oauth/authorize
Token URL:         https://wechat-connect.example.com/oauth/token
User Info URL:     https://wechat-connect.example.com/oauth/userinfo
JWKS URL:          https://wechat-connect.example.com/oauth/jwks
Scopes:            openid profile
Client ID:         same value as OIDC_CLIENT_ID
Client Secret:     same value as OIDC_CLIENT_SECRET

Add the Authentik Source callback URL to OIDC_ALLOWED_REDIRECT_URIS. It usually looks like this:

https://authentik.example.com/source/oauth/callback/wechat-connect/

Do not set PUBLIC_URL, OIDC_ISSUER, or OIDC_ALLOWED_REDIRECT_URIS to an Authentik flow URL such as /if/flow/default-authentication-flow/. OIDC_ALLOWED_REDIRECT_URIS must contain the Source callback URL.

Environment Variables

Variable Description Default
PUBLIC_URL Public base URL for this middleware service http://localhost:8080
LISTEN_ADDR HTTP listen address :8080
WECHAT_APP_ID WeChat Official Account AppID empty
WECHAT_APP_SECRET WeChat Official Account AppSecret empty
WECHAT_CALLBACK_TOKEN WeChat server callback token empty
WECHAT_ENCODING_AES_KEY 43-character callback EncodingAESKey; enables compatibility/safe mode empty
WECHAT_LOGIN_MODE auto, parameter_qr, or message_code auto
WECHAT_ACCOUNT_NAME Account name shown by the message-code login page empty
WECHAT_ACCOUNT_QR_CODE_URL Absolute HTTP(S) URL of the permanent account QR image used by message-code login empty
WECHAT_QR_CODE_TTL Temporary parameter-QR lifetime (maximum 30 days); numeric message codes are capped at 10 minutes 5m
WECHAT_USER_INFO_LANG WeChat user-info language zh_CN
WECHAT_CALLBACK_TIMEOUT Maximum user-profile lookup time inside the 5-second callback window; maximum 4 seconds 3s
WECHAT_ADMIN_TOKEN Independent Bearer token for the WeChat management API; 32+ bytes in production empty (management API disabled)
WECHAT_MANAGEMENT_DATA_FILE Atomic JSON state file for replies and menu draft data/wechat-management.json
OIDC_ISSUER OIDC issuer ${PUBLIC_URL}
OIDC_CLIENT_ID Client ID used by Authentik to call this service authentik
OIDC_CLIENT_SECRET Client secret used by Authentik to call this service change-me
OIDC_ALLOWED_REDIRECT_URIS Allowed Authentik callback URLs, comma-separated empty
OIDC_INSECURE_ALLOW_ALL_REDIRECTS Allow any redirect_uri; development only false
OIDC_RSA_PRIVATE_KEY_FILE Optional persistent OIDC RS256 private key file empty
OIDC_RSA_PRIVATE_KEY_PEM Optional persistent OIDC RS256 private key content empty
SESSION_SECRET Encryption key for web sessions, authorization codes, and access tokens; required in production generated at startup
SESSION_COOKIE_NAME Local web-login cookie name wechat_connect_session
AUTH_CODE_TTL OIDC authorization-code lifetime 10m
ACCESS_TOKEN_TTL OIDC access-token lifetime 1h
SESSION_TTL Local web-login session lifetime 24h

Docker Compose

cp .env.example .env
docker compose up --build

Then open http://localhost:8080. Compose mounts the named wechat-data volume at /app/data, so reply rules and menu drafts survive container replacement.

For local Authentik testing, you can temporarily set:

OIDC_INSECURE_ALLOW_ALL_REDIRECTS=true

GHCR Image

The CI workflow publishes multi-architecture images to GitHub Container Registry on pushes to main and on v* tags:

ghcr.io/as214933/authentik-wechat-connect:main
ghcr.io/as214933/authentik-wechat-connect:sha-<commit>
ghcr.io/as214933/authentik-wechat-connect:<version>

Example:

docker run --rm -p 8080:8080 --env-file .env \
  -v wechat-data:/app/data \
  ghcr.io/as214933/authentik-wechat-connect:main

Production Notes

  • Set a stable, random SESSION_SECRET in production. At least 32 bytes is recommended.
  • Without OIDC_RSA_PRIVATE_KEY_FILE or OIDC_RSA_PRIVATE_KEY_PEM, the service generates an ephemeral signing key at startup. This is supported, but a persistent key avoids JWKS changes across restarts and replicas.
  • Non-localhost PUBLIC_URL rejects the default OIDC_CLIENT_SECRET=change-me.
  • Non-localhost deployments reject a configured WECHAT_ADMIN_TOKEN shorter than 32 bytes. Generate an independent random value, for example with openssl rand -base64 32.
  • Back up WECHAT_MANAGEMENT_DATA_FILE. The included Docker Compose volume persists it; a container without a volume loses it when removed.
  • WeChat scan sessions are stored in this service's memory. For multiple replicas, route /oauth/authorize, /wechat/callback, and /api/scan/{id} for the same login to the same replica, or extend the service with shared external state.
  • The JSON management store is a single-replica store. Do not let multiple replicas write separate copies; use one management/callback replica or replace it with shared storage.
  • WeChat retries callbacks that do not receive a valid response within five seconds. The service bounds profile lookup, falls back to the signed OpenID, and replays a cached identical response for duplicate MsgId/event deliveries.
  • The WeChat admin server URL must be ${PUBLIC_URL}/wechat/callback, not the Authentik Source callback or an Authentik flow URL.

References

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages