WeCom Scan-Login (protocol deep-dive)
"Log in with WeCom" lets employees log into internal systems (OA, cloud desktop, admin consoles) by scanning a QR code with WeCom. It is essentially a variant of the OAuth2 authorization-code flow: a QR code is shown on a PC web page, the employee scans and confirms in the WeCom app, the browser receives a one-time code, and the backend exchanges that code for the employee's identity.
The consumer-facing WeChat scan-login needs only one step to fetch the user and uses different credentials — don't mix them up. To try it hands-on / see a clickable demo, see Mock WeCom (usage).
The overall flow
Browser (your login page) WeCom Your backend
│ 1. embed QR (JS SDK) │ │
│ ───────────────────────────────► │ │
│ 2. employee scans + confirms │ │
│ 3. redirect_uri?code=&state= │ │
│ ◄─────────────────────────────── │ │
│ 4. hand the code to the backend ──────────────────────────────► │
│ │ ① gettoken(corpid+secret) │
│ │ ② code→userid │
│ │ ③ userid→member detail │
│ 5. establish your own session ◄─────────────────────────────── │
Steps 1–3 are "getting the code in the browser"; steps 4–5 are "the backend exchanges the code for an identity". The JS SDK only handles the front-end part of steps 1–3; the three backend steps have nothing to do with the SDK.
What the JS SDK (wwLogin / @wecom/jssdk) actually does
The WeCom login JS SDK is a very thin browser script. Given corpid (appid), agentid, redirect_uri, state, etc., it does the following inside the <div> container you point it at:
- Builds the official authorize URL from your params (
login_type=CorpApp, …) so you don't have to memorize the format; - Inserts an
<iframe>pointing at WeCom's official QR page (open.work.weixin.qq.com/wwopen/sso/qrConnect, or the newerlogin.work.weixin.qq.com/wwlogin/sso/login) — the QR code itself is a page on WeCom's domain, so rendering, scan-status polling and expiry refresh are all handled by the official page; - Delivers the resulting
codeback to your page: after the employee scans and confirms, the official page redirects to yourredirect_uriwithcode+state(oldwwLogindoes a top-window redirect; the new@wecom/jssdkalso supports anonLoginSuccesscallback that hands you thecodewithout a full-page navigation); - Handles the details: iframe size/style,
redirect_type(redirect inside the iframe vs. top window), Chrome 142+'sallow="local-network-access", etc.
In one line: the SDK = "embed the official QR in your page + deliver the resulting code back". It never touches your secret and takes no part in the backend token exchange.
Why use the JS SDK (instead of hand-rolling a link / iframe)
You can skip the SDK — redirect the whole page to WeCom's scan-login link and let it redirect back. But embedding the QR with the JS SDK has real benefits:
- Users stay on your site: a full-page redirect takes users to WeCom's domain and back; an embedded QR keeps them on your login page — better UX and conversion.
- Avoids cross-origin pitfalls: the QR page is on WeCom's domain, your page on yours. If you build the
<iframe>yourself, you must handle scan-status polling, cross-origin retrieval of thecode, and style adaptation — and the same-origin policy blocks much of it. The SDK polls via the official page and, on success, the official page does the top-level redirect (or callback) to hand back thecode— you never touch cross-origin messaging. - Gets you the "desktop quick login" capability:
@wecom/jssdkautomatically shows a scan-free desktop quick-login panel when conditions are met (see below) — you can't get this by hand-building a URL. - Protocol-aligned, officially maintained: WeCom revisions (new hosts,
login_type, panel logic, quick-login conditions) are tracked by the SDK; your code doesn't change.
Conversely: if you do pure server-side rendering or just want a full-page redirect, you can skip the SDK and build the scan-login link directly. The SDK only wraps the "embed + UX + quick login" layer; the backend flow after you get the
codeis identical.
Old vs. new SDK: wwLogin.js vs. @wecom/jssdk
Old wwLogin.js | New @wecom/jssdk (recommended) | |
|---|---|---|
| Include | <script>, global WwLogin | npm i @wecom/jssdk, import { createWWLoginPanel } |
| Usage | new WwLogin({ id, appid, agentid, redirect_uri, state }) | ww.createWWLoginPanel({ el, params:{ login_type, appid, agentid, redirect_uri, state }, onLoginSuccess }) |
| Desktop quick login | ❌ | ✅ (desktop WeCom > 3.1.23, HTTPS, user in visible scope) |
| Getting the code | top-window redirect | redirect or onLoginSuccess({ code }) callback |
| Maintenance | maintenance mode | actively evolving; new features land here |
The official guidance is to use @wecom/jssdk for new integrations; both end up on the same qrConnect and backend /cgi-bin/* APIs.
After you get the code: why fetching user info takes three steps
This is the biggest difference from WeChat. The WeChat "Website App" fetches the user in one /sns/userinfo call; WeCom takes three:
gettoken: exchangecorpid+ appsecretfor an app-levelaccess_token. It represents the app identity, not a user, and is used to call directory APIs. It has quotas/rate limits and must be cached server-side, refreshed perexpires_in.auth/getuserinfo: exchangeaccess_token+ the scannedcodefor which employee it is (userid, possibly auser_tickettoo).user/get: read that employee's directory detail (name, department, mobile, email, …) withaccess_token+userid.
Why this design? Because WeCom's access_token is a corp/app-level credential (it can read the whole directory), not "a token a user granted to you". So you first prove "I am this app" (gettoken), then use the one-time code to pin down "who scanned this time" (getuserinfo), then use the app's permission to read that person's record (user/get). The code is just a one-time credential linking "the scanner" to "the app".
(For sensitive fields like avatar/gender, use the user_ticket from step ② with auth/getuserdetail.)
Key concepts & parameters
corpid: the corp ID (it's theappidin the SDK).agentid: the self-built app ID; determines visible scope and permissions.secret: the app secret, used only on the backend forgettoken, never in the front end.userid: the member's unique id in the corp directory; the key for reading details and sending messages.- App-level
access_token: as above — always cache it. - Visible scope: an employee outside the app's visible scope gets a no-permission error on scan.
state: generated on start, compared on return — CSRF protection.errcode/errmsg: the uniform error shape of every WeCom API (errcode:0= success).
Security essentials
- Always exchange the
codefor tokens on the backend; thesecretmust never appear in the front end or the repo. - Validate
stateagainst CSRF / forged callbacks. - Enforce HTTPS and configure the app's trusted / callback domains correctly (a mismatch yields a "link cannot be accessed" error).
- The
codeis short-lived and should be treated as single-use; theaccess_tokenhas quotas — mind caching and refresh.
Hands-on / integration
- 🔬 Scan-Login Demo — a clickable, real run of the flow with the Mock's identical SDK (switch to the "WeCom" tab)
- 🧪 Mock WeCom (usage) — endpoints, integration code and the "going live = change only JS + URL" mapping
- 📖 OAuth 2.0 docs · compare with WeChat scan-login