Outlook-plugin: token-refresh debuggen

Aan het einde van deze gids weet je waar in de stack een specifieke gebruikersklacht (“plugin werkt niet meer”, “moet steeds opnieuw inloggen”, “krijgt ‘koppel opnieuw’ melding”) vandaan komt, en kun je gericht actie ondernemen.

Voor de architecturele context (waarom de flow zo is), zie Explanation: Outlook-plugin. Voor de volledige event-tabel, zie Reference: log-events.

Klacht-tot-event matrix

Klacht Verwachte logs Wat te checken
“Moet elke maand opnieuw inloggen” session/refresh 401 in API-logs of geen refresh-aanroep Slide-refresh aan client-zijde gebroken; check of outlook-plugin-auth.js correct geladen wordt en pendingSlideRefresh niet vastloopt
“Krijgt ‘koppel opnieuw’ (EXACT_REAUTH_REQUIRED)” exact.outlook.refresh.invalid_grant of .threshold_revoked voor deze exactUserId Check of refresh_token verlopen is (kan na lange inactiviteit) of dat een serie transient fouten de threshold heeft geraakt
“Probeer opnieuw-knop doet niets” Geen netwerk-call zichtbaar in plugin-DevTools Zie de fix-historie van #2204; controleer of outlook-plugin.js retryLoadCurrentItem niet stil bestaande UI hidet
“Random 401’s op willekeurige acties” Verspreide exact.outlook.refresh.transient events Mogelijk Exact-side rate-limit of korte downtime; check Exact-status pagina
“Werkt elke ochtend even niet” exact.outlook.refresh.batch met hoge rateLimited count tussen 02:00-03:00 UTC Exact rate-limits ‘s nachts; geen actie nodig, cron-tick herstelt vanzelf

Stappen voor een specifieke gebruiker

1. Vind de exactUserId

Vraag de gebruiker zijn Outlook-emailadres. Query in mongo (outlookexacttokens-collectie):

db.outlookexacttokens.findOne({ outlookEmail: 'user@example.com' })

Resultaat geeft exactUserId, tenantUuid, revokedAt, reauthRequiredAt, refreshFailureCount, lastUsedAt, lastRefreshedAt. Eerste check:

  • revokedAt: null + reauthRequiredAt: null + refreshFailureCount: 0 → gezonde sessie. Klacht zit waarschijnlijk client-side of in een ander deel van de flow.
  • revokedAt: <datum> → token is server-side ingetrokken. Zoek terug in logs waarom (zie volgende stap).
  • reauthRequiredAt: <datum> → Exact meldde “Old refresh token used” (verweesd refresh_token). De doc is NIET gerevoked maar valt uit de refresh-cron; de gebruiker moet opnieuw koppelen. Re-OAuth (upsert) wist het veld. Zet dit veld nooit handmatig op null zonder verse tokens, anders herleeft de cron-loop.
  • refreshFailureCount >= 30 → klop tegen REVOKE_THRESHOLD; sessie wordt bij de eerstvolgende cron of API-call gerevoked.
  • lastUsedAt ouder dan 60 dagen → cleanup-outlook-exact-tokens revoked dit doc; gebruiker moet opnieuw koppelen.

2. Volg de refresh-events in Loki

{app="tapster-api"} |= "exact.outlook.refresh" | json | exactUserId="<id>"

Tijdslijn van events laat zien wat er gebeurde rond het moment van klacht. De verwachte gezonde flow voor een actieve gebruiker:

.success  every ~10 min (cron of on-demand)
.stale    occasional (race-loss; geen probleem)

Verdachte patronen:

.transient × N, .threshold_revoked    → auto-revoke
.invalid_grant                         → refresh_token kapot (gebruiker moet opnieuw koppelen)
.reauth_required (eenmalig)            → verweesd refresh_token; doc krijgt `reauthRequiredAt` en valt uit de cron. Gebruiker moet opnieuw koppelen
.reauth_required herhaaldelijk (~1x/min) → cron-loop: `reauthRequiredAt` wordt niet gezet/gerespecteerd. Dit was de bug vóór de reauthRequiredAt-fix; bij terugkeer check of findExpiringSoon nog op `reauthRequiredAt: null` filtert
.rate_limited × veel                   → Exact-rate-limit, vanzelf weg
.race_lost herhaaldelijk               → 2 servers proberen elk tegelijk te refreshen (acceptabel). Check `attempts`: 1 = direct gedetecteerd, 2-4 = via retry-window opgevangen (zie `RACE_DETECTION_BACKOFF_MS` in `exactService.ts`)

3. Check client-side state

Vraag de gebruiker DevTools te openen op de plugin (Outlook → klik in taakvenster → rechtsklik → Inspect). In de Console:

// Toont de huidige JWT-sessie (exp = ms-epoch)
JSON.parse(localStorage.getItem('tapsterOutlookSession'))

// Office-context aanwezig?
typeof Office !== 'undefined' && Office.context && Office.context.mailbox && Office.context.mailbox.item

// Auth-module geladen?
window.TapsterOutlookAuth

Veelvoorkomende bevindingen:

  • localStorage.tapsterOutlookSession is null na een refresh-loop → tryServerSideSessionRecovery heeft het opnieuw moeten opbouwen; check Network-tab op POST /exact/outlook/session/recover.
  • Office.context.mailbox.item is null → Outlook-cache-issue; gebruiker moet de mail opnieuw openen of plugin herstarten.

4. Forceer een refresh in DB

Voor handmatige acties (alleen via een dev-tool of geverifieerde admin-shell; niet in productie zonder reden):

// Forceer near-expiry zodat de volgende API-call een refresh triggert
db.outlookexacttokens.updateOne(
  { exactUserId: '<id>' },
  { $set: { expiresAt: new Date(Date.now() + 10_000) } }
)

Daarna een endpoint vanuit de plugin aanroepen (of de cron-tick afwachten); binnen 10 seconden zie je een exact.outlook.refresh.success of een faal-event.

Logs filteren voor een tenant

Niet altijd is een specifieke gebruiker bekend. Voor tenant-wide patronen:

{app="tapster-api"} |= "exact.outlook.refresh" | json | tenantUuid="<uuid>"

Rare clustering — bv. 50 invalid_grant events binnen een paar minuten — suggereert een Exact-API-incident of een tenant-brede revoke (admin-actie).

Wanneer escaleren

  • invalid_grant voor één gebruiker: gebruiker moet opnieuw koppelen. Geen actie nodig dan een vriendelijk bericht.
  • invalid_grant voor veel gebruikers tegelijk: contacteer Exact, mogelijk hebben ze refresh_tokens client-side ingetrokken via App Center.
  • threshold_revoked voor één gebruiker: kijk waar de transient-flood vandaan kwam (.transient-events). Vaak een Exact-incident waarvan we de cleanup pas later zien. Reset desnoods via $set: { refreshFailureCount: 0, revokedAt: null } als Exact-side al weer werkt.
  • Geen outlookexacttokens-doc voor een gebruiker die zegt ‘gekoppeld’ te zijn: ofwel outlookEmail is anders dan verwacht, ofwel doc is via deleteOldRevoked (>90d) weggegooid. Gebruiker moet opnieuw koppelen.

Gerelateerd