DEV Community

sealeo
sealeo

Posted on

Stop Copy-Pasting WeCom OAuth Snippets — There's an SDK for That Now

If you've ever had to add WeCom (WeChat Work / 企业微信) login to an application, you know the ritual:

  1. Search for blog posts in Chinese.
  2. Copy a Python or JavaScript snippet from 2019.
  3. Patch it until the redirect stops breaking.
  4. Discover in production that some users get an OpenId instead of a UserId and your code returns null.

I went through that loop enough times to decide the plumbing should be a package, not a paste. So now it is: wecompass, a small TypeScript SDK for WeCom OAuth2 and the most common qyapi endpoints.

npm install wecompass
Enter fullscreen mode Exit fullscreen mode

What you actually need to do for WeCom login

The flow itself is standard OAuth2:

your app → WeCom authorize page → user confirms → redirect back with ?code=…
→ backend exchanges code for identity → you issue your own session
Enter fullscreen mode Exit fullscreen mode

The friction is in the details: percent-encoding the redirect URI, the three different scope values, the two different identity fields, and the access token that expires every two hours and takes every user's session with it if you get the refresh wrong.

Here's the whole thing with the SDK.

1. Build the authorize URL

import { WeComClient } from 'wecompass'

const client = new WeComClient({
  credentials: {
    corpId: process.env.WECOM_CORP_ID!,
    agentId: process.env.WECOM_AGENT_ID!,
    secret: process.env.WECOM_SECRET!,
  },
})

const authorizeUrl = client.buildAuthorizeUrl({
  redirectUri: 'https://app.example.com/api/auth/callback?tenant=acme',
  state: signedRandomState,
  scope: 'snsapi_base', // or snsapi_userinfo / snsapi_privateinfo
})
Enter fullscreen mode Exit fullscreen mode

Every parameter goes through proper URL encoding. A redirect URI with query parameters (?tenant=acme) is the single most common reason hand-rolled authorize URLs silently fail — the & cuts the parameter in half and WeCom drops you on an error page.

2. Exchange the code for identity

app.get('/api/auth/callback', async (req, res) => {
  const identity = await client.getIdentityByCode(req.query.code as string)

  if (identity.userId) {
    // User is inside your app's visible range — full profile is available
    const profile = await client.getUser(identity.userId)
    return issueSession(profile)
  }

  if (identity.openId) {
    // User is OUTSIDE the visible range. Only an OpenId comes back.
    return res.status(403).send('Please ask your admin to add you to the app.')
  }
})
Enter fullscreen mode Exit fullscreen mode

That if / else if is not defensive filler. With snsapi_base, WeCom returns an OpenId instead of a UserId for anyone outside the application's visible range. Code that only reads UserId gets undefined and logs out users at random.

3. Let the token take care of itself

const user = await client.getUser(userId) // that's it
Enter fullscreen mode Exit fullscreen mode

The SDK caches the access_token, refreshes it 5 minutes before expiry, and if WeCom still rejects it with 42001, transparently fetches a new one and retries exactly once. Running multiple instances? Swap the cache for Redis in four lines:

const redisCache: TokenCache = {
  get: (key) => redis.get(key),
  set: (key, value, ttl) => redis.set(key, value, 'EX', ttl),
  delete: (key) => redis.del(key),
}
Enter fullscreen mode Exit fullscreen mode

PC scan-to-login too

The same client builds the desktop QR flow:

const qrUrl = client.buildQrConnectUrl({
  redirectUri: 'https://app.example.com/api/auth/pc-callback',
  state: signedRandomState,
})
Enter fullscreen mode Exit fullscreen mode

Design notes

  • Zero runtime dependencies. It's ~300 lines of TypeScript on top of fetch. Nothing to audit, nothing to break.
  • Typed everything. CodeIdentity, WeComUser, WeComDepartment, TokenCache — your editor knows all of it.
  • Errors are values, not mysteries. Non-zero errcode becomes a WeComApiError with errcode and errmsg attached.
  • Tested. 19 unit tests covering the ugly paths: token refresh mid-request, OpenId-only users, malformed codes.

What you still do yourself

Two things are application concerns by design:

  1. state validation. The SDK requires it and echoes it, but CSRF protection means you compare it against a value you stored before the redirect (a signed cookie works well).
  2. Your session layer. The SDK hands you identity; issuing JWTs, cookies, or sessions is your architecture choice.

Also a production reminder: if you wrap this in JWT-based sessions, keep your signing key fixed across restarts. Randomly generated keys mean every deploy logs out every user.

Get it

npm install wecompass
Enter fullscreen mode Exit fullscreen mode

📦 github.com/sealeos-git/wecompass · MIT licensed

Issues and PRs welcome — especially integrations with other frameworks' callback conventions. If you've hit a WeCom quirk that isn't covered yet, open an issue; that's exactly how the next pitfall gets documented.

Top comments (1)

Collapse
 
sealeosgit profile image
sealeo •

A companion piece on 5 production pitfalls is coming next week