Animated Architecture Patterns

Refresh Token Rotation

access token อายุสั้น refresh token ที่ใช้ได้ครั้งเดียว และถ้าจับได้ว่ามีการใช้ซ้ำก็ revoke ทั้ง family

หรือที่เรียกกันว่า Rotating refresh tokens, One-time-use refresh tokens, Refresh token reuse detection

GET /orders Authorization: Bearer POST /token grant_type=refresh_token no client secret, so the server can't tell who sends a token App public client: SPA or mobile access token, 10 min AT1 AT2 none valid expired refresh token RT1 RT2 none one use, then replaced API calls carry the access token 401: time to refresh both tokens replaced unaware that RT2 is revoked tokens dropped: sign in again signed in: new tokens API resource server accepts access tokens only, never sees a refresh token 200 OK 401 invalid_token Authorization server token endpoint: POST /token Token family revoked new TOKEN (HASH) STATUS RT1 seen twice active used revoked RT2 replaces RT1 active revoked rows share one family ID A refresh token arrives. It is… active mark it used, issue the next used reuse: revoke the family revoked answer invalid_grant Sign-in started this family; RT1 is its first refresh token. The server keeps a family ID, a hash of each token and its status. RT1 arrives and is still active, so the refresh goes through. RT1 becomes used and RT2 joins the family: each refresh token works once. RT1 arrives a second time. It was already used: that is reuse. Thief or app? The server can't tell, so it revokes the whole family. RT2 was revoked with its family, so the app has to sign the user in again. A new family starts. At most, a thief keeps an access token for minutes. Lifetimes (example values) access token 10 min refresh token family 8 h Rotation never extends the 8 h: then the user signs in again. Attacker stole a copy of RT1 earlier its RT1 died with the old family RT1 refused: nothing gained POST /token replays the stolen RT1 AT1 AT2 200 401 RT1 RT2 AT2 RT2 AT1 RT1 invalid_grant RT1 invalid_grant

ขั้นที่ 1 · access token อายุสั้น หลังเข้าสู่ระบบ แอปจะถือ token สองตัว: access token ที่มีอายุแค่หลักนาที (ในตัวอย่างนี้ 10 นาที) กับ refresh token ชื่อ RT1 เป็นตัวแรกของ token family ใหม่บน authorization server เวลาเรียก API แอปแนบไปแค่ access token และไม่เคยส่ง refresh token ไปที่ API พอ access token หมดอายุ API จะตอบ 401 พร้อม error="invalid_token" ตรงนี้คือสัญญาณให้แอป refresh

ขั้นที่ 2 · Refresh แล้ว rotate token แอปส่ง grant_type=refresh_token พร้อม RT1 ไปที่ token endpoint แล้วฝั่ง server เห็นว่า RT1 ยัง active อยู่ ก็เปลี่ยนสถานะเป็น used แล้วคืน access token ใหม่มาพร้อม refresh token ตัวใหม่ คือ RT2 ใน family เดียวกัน: refresh token แต่ละตัวใช้ได้แค่ครั้งเดียวเท่านั้น แอปเปลี่ยน token ทั้งสองตัวเป็นชุดใหม่ แล้วเรียก API อีกรอบ

ขั้นที่ 3 · จับได้ว่ามีการใช้ซ้ำ attacker ที่คัดลอก RT1 ไปก่อนหน้านี้ ตอนนี้เอา RT1 มา replay แล้วฝั่ง server ก็เห็น refresh token ที่ใช้ไปแล้ว: แปลว่ามีสองฝ่ายถือ token ของ family นี้อยู่ และ server ก็แยกไม่ออกว่าฝ่ายไหนเป็นคนขโมย เลย revoke ทั้ง family รวม RT2 ด้วย แล้วตอบ invalid_grant

ขั้นที่ 4 · เข้าสู่ระบบใหม่ พอ access token หมดอายุ แอปส่ง RT2 ไปก็ได้ invalid_grant เหมือนกัน แอปเลยทิ้ง token ทั้งหมด แล้วพาผู้ใช้ไปเข้าสู่ระบบใหม่ รอบนี้เริ่ม family ใหม่พร้อม RT1 ตัวใหม่ การโดนขโมยครั้งนี้ทำให้ผู้ใช้ต้องเข้าสู่ระบบเพิ่มแค่หนึ่งครั้ง ความเสียหายก็มีขอบเขต: คนขโมยไม่มี refresh token ที่ใช้ได้เหลืออยู่แล้ว อย่างมากก็ถือ access token ที่อีกไม่กี่นาทีก็หมดอายุ

ปัญหา

API ที่ validate token ในเครื่องดึง access token คืนก่อนมันหมดอายุไม่ได้ ทำให้ access token ควรมีอายุแค่หลักนาที แต่ก็ไม่มีใครอยาก sign in ทุกสิบนาที และวิธีเก่าที่ browser app ใช้ต่ออายุแบบเงียบ ๆ คือ hidden iframe ที่พึ่ง session cookie ของ authorization server ก็พังทุกที่ที่ browser มองว่า cookie นั้นเป็น third-party แล้ว block ทิ้ง refresh token มาอุดช่องนี้: เป็น credential ที่อายุยาวกว่า แอปเอามันไปแลก access token ตัวใหม่ที่ token endpoint ได้ โดยไม่ต้องให้ผู้ใช้ทำอะไร

แบบนี้แค่ย้ายความเสี่ยง ไม่ได้ลบมันทิ้ง refresh token เป็นตัวแทนของทั้ง grant: ใครถือมันก็สร้าง access token สำหรับทุกอย่างที่ผู้ใช้อนุญาตไว้ได้ ซ้ำแล้วซ้ำอีก เป็นวันหรือเป็นเดือน confidential client ต้อง authenticate ตอน refresh ทำให้ refresh token ที่โดนขโมยไปใช้ไม่ได้ถ้าไม่มี credential ของ client ด้วย แต่ public client (single-page app, mobile app หรือ desktop app) ไม่มีอะไรให้ใช้ authenticate ทำให้ refresh token ของมันเป็น bearer credential: ตัวที่ก็อปไปใช้ได้ดีเท่าตัวจริงเป๊ะ และ server ก็ไม่มีวันรู้ว่ามีคนถือสองคน

ทำงานยัง⁠ไง

Rotation ทำให้ refresh token ทุกตัว ใช้ได้ครั้งเดียว และถือว่าการที่ token ที่ใช้ไปแล้วโผล่มาอีกรอบคือสัญญาณเตือน

  1. Rotate ทุกครั้งที่ refresh request เป็นแบบปกติตาม RFC 6749 §6: POST ไปที่ token endpoint พร้อม grant_type=refresh_token และ refresh token แล้ว response ก็มี access token ใหม่ และ refresh token ใหม่ แล้ว token ที่เพิ่งยื่นไปก็ถูกปลดระวาง ส่วนที่ client ต้องทำมีอยู่ใน RFC 6749 อยู่แล้ว: ถ้า response มี refresh token ใหม่มา ก็ทิ้งตัวเก่าแล้วเก็บตัวใหม่ไว้
  2. จำ family ไว้ server เก็บ record ของ token ที่ปลดระวางแล้วและความสัมพันธ์ของมัน refresh token ทุกตัวที่สืบมาจากการ sign in ครั้งเดียวกันต่อกันเป็น chain: เอกสารของ Auth0 เรียกว่า token family ส่วน RFC 9700 พูดถึง grant ที่ token สังกัดอยู่ ณ เวลาใดก็ตาม จะมี token หนึ่งตัวใน family ที่ active และตัวก่อนหน้าทั้งหมดเป็น used
  3. ถือว่าการใช้ซ้ำคือการขโมย token ที่ใช้ไปแล้วแต่โผล่มาอีก เป็นสัญญาณว่ามีสองฝ่ายถือ token จาก family นี้อยู่ server แยกไม่ออกว่า request มาจากคนขโมยหรือจากแอปตัวจริง เลยทำสิ่งเดียวที่ปลอดภัย: revoke ทั้ง family รวม token ที่ active อยู่ด้วย แล้วตอบ invalid_grant (RFC 6749 §5.2, HTTP 400) แอปจะรู้ตอน refresh ครั้งถัดไป แล้วพาผู้ใช้ไป sign in ใหม่ (Authorization Code + PKCE) เท่ากับเริ่ม family ใหม่ไปในตัว

เหตุการณ์จะเรียงแบบไหนก็ทำให้สัญญาณดัง ใน diagram แอป refresh ก่อน แล้วการที่ attacker เอา RT1 มา replay ก็คือสิ่งที่ถูกจับได้ ถ้า attacker refresh ก่อน มันจะได้คู่ที่ใช้ได้จริงคือ AT2 กับ RT2 แล้วตอนนี้แอปกลายเป็นฝ่ายที่ถือ token ที่ใช้ไปแล้ว การ refresh ครั้งถัดไปของแอปด้วย RT1 คือการใช้ซ้ำ: family ถูก revoke และ RT2 ของ attacker ก็โดนไปด้วย แล้วผู้ใช้ก็ sign in ใหม่ ถ้าเรียงแบบนี้คนขโมยก็เข้ามาได้จริงเป็นเวลาเท่ากับอายุของ access token ที่มันถือ เพราะแบบนี้อายุของ access token ถึงเป็นตัวจำกัดความเสียหายที่แท้จริง

กฎนี้มาจากไหน RFC 6749 §10.4 (2012) เสนอ rotation ไว้เป็นตัวอย่างของวิธีจับการใช้ refresh token ในทางที่ผิดเมื่อ authenticate client ไม่ได้ แล้ว RFC 6819 §5.2.2.3 ก็เพิ่มว่าต้องทำอะไรเมื่อเจอการใช้ซ้ำ: revoke token ที่ยัง valid และ authorization ที่อยู่เบื้องหลังมัน RFC 9700 ที่เป็น OAuth security BCP (มกราคม 2025) ทำให้มันเป็นข้อบังคับ: §2.2.2 บอกว่า refresh token ของ public client ต้องเป็นแบบ sender-constrained หรือใช้ rotation และ §4.14.2 นิยามทั้งสองวิธีไว้ RFC 10017 ที่เป็น BCP สำหรับ browser-based application (สิงหาคม 2026) ย้ำข้อบังคับนี้อีกครั้งใน §6.3.2.3 และเพิ่มกฎเรื่องอายุ ส่วน OAuth 2.1 ใส่กฎเดียวกันนี้ไว้ใน core framework (§4.3.1 ของ draft-ietf-oauth-v2-1-16 เดือนกันยายน 2026) แต่ยังเป็น Internet-Draft ของ working group ไม่ใช่ RFC

ใช้ตอนไหนดี

  • Public client ที่ได้ refresh token: single-page app, mobile app และ desktop app สำหรับพวกนี้มันไม่ใช่ทางเลือก: RFC 9700 §2.2.2 บังคับให้ใช้ rotation หรือ sender-constrained refresh token อย่างใดอย่างหนึ่ง
  • Browser app ที่ถือ token เอง พอ third-party cookie ถูก block ตัว refresh token ที่ rotate ได้ก็เป็นสิ่งที่ทำให้ single-page app ยัง sign in ค้างอยู่ได้ RFC 10017 ไม่แนะนำ architecture แบบนี้สำหรับแอปธุรกิจ แอปที่ sensitive หรือแอปที่จัดการข้อมูลส่วนบุคคล: สำหรับพวกนั้นมันแนะนำอย่างหนักแน่นให้ใช้ backend for frontend ที่เก็บ token ไว้บน server
  • Confidential client ในฐานะ defence in depth web backend authenticate ที่ token endpoint อยู่แล้ว refresh token ที่โดนขโมยเลยใช้ไม่ได้ถ้าไม่มี credential ของ client และมาตรฐานก็ไม่ได้บังคับให้ rotate แต่ก็ยังคุ้มที่จะเปิดไว้: ถ้า credential รั่วไปด้วย reuse detection คือสิ่งที่จะจับได้
  • ไม่ใช่ในที่ที่ไม่มี refresh token client credentials grant ไม่มี refresh token (RFC 6749 §4.4.3 บอกว่าไม่ควรออกให้): service แค่ขอ access token ใหม่ก็จบ
  • ไม่ใช่ด่านป้องกันเดียวของแอปที่มีมูลค่าสูง Rotation จับการขโมยได้แบบเดียวและจับได้หลังเกิดเรื่องแล้ว มันไม่ได้กันไม่ให้ token โดนขโมย

ได้อะไร เสียอะไร

  • การจับได้ต้องให้ทั้งสองฝ่ายโผล่มา สัญญาณจะดังก็ต่อเมื่อคนถืออีกคนยื่น token ของตัวเองมา คนขโมยที่ทำให้แน่ใจว่าแอปจะไม่ refresh อีกเลย (ด้วยการลบ token ของแอป หรือรอจนผู้ใช้ปิดแอปหรือ offline) ก็ rotate chain ที่ขโมยมาต่อไปได้โดยไม่มีใครรู้ RFC 10017 §5.1.2 อธิบายกรณีนี้ไว้ตรงตัว idle lifetime กับ absolute lifetime คือสิ่งที่ปิด chain แบบนั้นได้
  • มันไม่ได้หยุดโค้ดที่รันอยู่ในแอป script ที่ถูก inject เข้า single-page app อ่าน token ใหม่แต่ละตัวได้ทันทีที่มันมาถึง หรือจะไม่สน token ของแอปเลย แล้วแอบรัน authorization flow ของตัวเองเพื่อเอา family แยกอีกชุดก็ได้ (RFC 10017 §5.1.3) rotation กับอายุสั้น ๆ ช่วยเรื่องนี้ไม่ได้ ที่ช่วยได้คือการไม่เก็บ token ไว้ใน browser
  • access token อยู่นานกว่า family การ revoke family หยุดการออก access token ใหม่ ส่วนตัวที่ออกไปแล้วยังใช้ได้จนหมดอายุในทุกที่ที่ API validate ในเครื่อง (ดู JWT Validation) "หลักนาที" เป็นสิ่งที่เราต้องตัดสินใจเอง: default ของ vendor มักยาวกว่านั้น เช่น สุ่มระหว่าง 60 ถึง 90 นาทีใน Microsoft Entra ID และ 24 ชั่วโมงสำหรับ API ใน Auth0
  • สัญญาณเตือนผิดพลาดทำให้ผู้ใช้จริงโดน sign out browser สอง tab ที่ refresh พร้อมกันเป๊ะ การ retry หลัง timeout หรือ response ที่ไม่เคยไปถึงเครื่อง ล้วนทำให้ client ที่สุจริตยื่น token ที่ใช้ไปแล้ว ถ้าไม่ระวัง reuse detection ก็จะเปลี่ยน network ที่ไม่เสถียรให้กลายเป็นการ sign out
  • Refresh กลายเป็นการเขียน ทุกการ refresh เปลี่ยน state ที่ทุก node ของ authorization server ต้องเห็นตรงกัน และการ mark token ว่า used ต้องเป็น atomic ไม่อย่างนั้น request สองตัวที่มาพร้อมกันอาจสำเร็จทั้งคู่ RFC 6819 เตือนไว้นานแล้วว่า rotation สร้างปัญหาได้ใน deployment แบบ cluster
  • ผู้ใช้ที่สุจริตต้องจ่ายค่าความไม่แน่นอน เพราะ server แยกคนขโมยออกจากแอปไม่ได้ ผู้ใช้ก็เลยโดน sign out ไปด้วย นี่คือ design ที่ทำงานถูกต้อง และแอปต้องรับมือเรื่องนี้ให้เรียบร้อย

ข้อควรรู้ตอนลงมือทำ

  • server เก็บอะไร record หนึ่งตัวต่อ refresh token หนึ่งตัว: family ID (หรือ ID ของ grant), hash ของ token, status ของมัน (active, used หรือ revoked), client, user และ scope ที่มันถูกออกให้ และเวลาหมดอายุ เก็บ hash แทนตัว token ถ้า table รั่วจะได้เอาไป replay ไม่ได้ (RFC 6819 §5.1.4.1.3) ใช้ SHA-256 ธรรมดาก็พอ: การใส่ salt และ hash แบบช้าที่ password ต้องใช้มีไว้กัน dictionary attack แต่ refresh token เป็น string สุ่มยาว ๆ (RFC 6749 §10.10 บังคับให้โอกาสเดาถูกไม่เกิน 2⁻¹²⁸) เก็บ record ของ token ที่ใช้ไปแล้วไว้จนกว่า family จะหมดอายุ เพราะมันคือสิ่งที่ทำให้จับการใช้ซ้ำได้ RFC 9700 ยังบอกด้วยว่า grant จะ encode ไว้ในตัว refresh token เองก็ได้ โดยป้องกันด้วย signature เพื่อให้ server หา family ได้จาก token ทุกตัวที่เคยออก
  • ทำการสลับให้เป็น atomic หา token ด้วย hash แล้วเปลี่ยนจาก active เป็น used ใน conditional update หรือ transaction เดียว แล้วค่อยออกตัวถัดไป request สองตัวที่ถือ token ตัวเดียวกันต้องไม่มีทางเจอว่ามัน active ทั้งคู่
  • เมื่อเจอการใช้ซ้ำ ให้ revoke token ทุกตัวใน family และ grant ที่อยู่เบื้องหลัง (Auth0 ทำทั้งสองอย่าง และ draft ของ OAuth 2.1 ก็กำหนดทั้งสองอย่าง) ตอบ invalid_grant และบันทึก security event ตัวอย่างเช่น Okta เขียน reuse event ลงใน System Log (app.oauth2.as.token.detect_reuse สำหรับ custom authorization server และ app.oauth2.token.detect_reuse สำหรับ org authorization server)
  • อายุสองแบบ absolute lifetime ครอบทั้ง family: token ที่ตามมาทุกตัวรับเวลาหมดอายุของ token ตัวแรกไป การ rotate เลยไม่เคยยืดมันออกไป ส่วน idle lifetime ทำให้ token ที่ไม่ได้ใช้มาสักพักหมดอายุ RFC 9700 §4.14.2 บอกว่า refresh token ควรหมดอายุหลังไม่ได้ใช้งาน แล้ว RFC 10017 §6.3.2.3 ก็บังคับให้ refresh token ของ browser app มี maximum lifetime หรือ idle timeout และห้าม token ที่ rotate แล้วอยู่นานกว่าตัวแรกถ้าตัวแรกมีเวลาหมดอายุตายตัว ตัวอย่างในนั้นจับคู่ access token อายุ 10 นาทีกับ refresh token อายุ 8 ชั่วโมง เป็นค่าเดียวกับที่ใช้ใน diagram ส่วน section เดียวกันนั้นก็บอกว่าอายุของ refresh token ควรผูกกับ session ของผู้ใช้ที่ authorization server การ sign out ที่นั่นจะได้ปิด family ไปด้วย
  • Client: refresh ทีละครั้ง ให้การ refresh ทั้งหมดผ่าน request ที่กำลังวิ่งอยู่ตัวเดียวต่อ instance ของแอป ข้าม browser tab ให้ใช้ lock (Web Locks API) หรือให้ tab เดียว refresh แล้วแชร์ผลให้ตัวอื่น เซฟ refresh token ใหม่ก่อนจะใช้ access token ใหม่ แล้วให้ refresh เมื่อ API ตอบ 401 พร้อม error="invalid_token" (RFC 6750 §3.1) หรือก่อน expires_in จะหมดเล็กน้อย ถือว่า invalid_grant คือ "sign out แล้ว": ล้าง token แล้วเริ่ม sign in และอย่า retry ไม่มี field มาตรฐานตัวไหนบอก client ว่า refresh token ของมันจะหมดอายุเมื่อไร มันเลยต้องพร้อมรับคำตอบนั้นได้ทุกเมื่อ
  • Grace period สำหรับการ retry ที่สุจริต vendor ยอมให้ token ตัวก่อนหน้าใช้ได้อีกสักพัก การ retry จะได้ไม่ถูกมองว่าเป็นการขโมย grace period ของ Okta มี default เป็น 30 วินาที และตั้งได้ตั้งแต่ 0 ถึง 60 ส่วน rotation overlap period ของ Auth0 ปิดไว้เป็น default และถ้าตั้งไว้ก็รับแค่ token ตัวก่อนหน้าตัวเดียว Amazon Cognito ให้ได้ถึง 60 วินาที สิ่งที่ต้องแลกนั้นชัดเจน: ภายในช่วงนั้น token ที่ถูก replay จะได้ token ใหม่กลับไปแทนที่จะได้สัญญาณเตือน คนขโมยที่เร็วพอเลยรอดไปได้ แก้ race ในฝั่ง client ก่อน แล้วค่อยตั้งช่วงนี้ให้สั้นที่สุดเท่าที่ network ของเราจะยอม
  • Sender-constrained refresh token คือทางเลือกที่แข็งแรงกว่า RFC 9700 รับได้ทั้งสองวิธี และการผูก token ไว้กับ key ดีกว่าการมาจับการใช้ผิดทีหลัง: ถ้าไม่มี private key ตัวที่ก็อปไปก็ใช้ไม่ได้ ไม่ต้องมีใครโดน sign out และไม่มี race ให้ต้องจัดการ ถ้าใช้ DPoP (RFC 9449 §5) ตัว public client จะส่ง proof ที่ sign ด้วย key pair ของตัวเองไปกับทุก token request และ server ต้องผูก refresh token ไว้กับ key นั้น ใน browser จะ generate key แบบ non-extractable ด้วย Web Crypto API ก็ได้ ส่วนถ้าใช้ mutual TLS (RFC 8705 §4) ฝั่ง server ควรผูก refresh token ของ public client ไว้กับ certificate ของมัน ข้อจำกัดมีสองข้อ: script ที่รันอยู่ในแอปยังใช้ key ได้ตราบที่หน้ายังเปิดอยู่ (RFC 9449 §11.4) และ authorization server ของเราต้องรองรับด้วย สองวิธีนี้ยังใช้ร่วมกันได้: refresh token ที่ผูกกับ key ก็ rotate ได้เหมือนกัน
  • Confidential client RFC 6749 §6 บังคับให้มัน authenticate ทุกครั้งที่ refresh และ RFC 9449 §5 บอกว่าข้อนี้ทำให้ refresh token ของมันเป็นแบบ sender-constrained อยู่แล้ว แถมยืดหยุ่นกว่าการผูกกับ key อีก ส่วน rotation เป็นทางเลือกสำหรับพวกนี้
  • token อยู่ที่ไหน browser ไม่มีที่เก็บไหนที่ script ที่ถูก inject เข้าไม่ถึง: localStorage, sessionStorage, IndexedDB และตัวแปรใน memory ล้วนแพ้โค้ดที่รันอยู่ในหน้า (RFC 10017 §8) ส่วน architecture ที่ RFC 10017 จัดว่าปลอดภัยที่สุดคือ backend for frontend: มันเป็น confidential client เก็บ access token และ refresh token ไว้บน server และ browser ถือแค่ session cookie แบบ HttpOnly ส่วน native app ควรเก็บ refresh token ไว้ใน secure storage ของ platform (Keychain บน iOS และ Keystore บน Android)
  • การขอ refresh token ถ้าใช้ OpenID Connect แอปจะขอด้วยการเพิ่ม scope offline_access (Core §11) โดย Auth0 กับ Okta บังคับให้ทำแบบนี้ ภายใต้ RFC 9700 §4.14.2 ตัว authorization server เป็นคนตัดสินจากการประเมินความเสี่ยงว่า client ตัวไหนจะได้ refresh token หรือไม่
  • Sign-out POST refresh token ไปที่ revocation endpoint (RFC 7009) แล้วทิ้ง copy ที่อยู่ในเครื่อง ส่วน authorization server ที่ implement RFC 7009 ต้องรองรับการ revoke refresh token และถ้ามัน revoke access token ได้ด้วย ก็ควร invalidate access token ที่ออกภายใต้ grant เดียวกันไปด้วย และจะตอบ 200 ถึง token นั้นจะ invalid อยู่แล้วก็ตาม ฝั่ง server ตัว RFC 9700 §4.14.2 อนุญาตให้ revoke refresh token อัตโนมัติเมื่อผู้ใช้เปลี่ยน password หรือ sign out ที่ authorization server
  • ผลิตภัณฑ์ ตามเอกสาร ณ ตุลาคม 2026:
    • Auth0: rotation เป็นการตั้งค่าราย application ส่วน reuse detection ทำงานอัตโนมัติและ revoke ทั้ง family พร้อม grant แล้วอายุ (default 30 วันเมื่อเปิด rotation และได้สูงสุดหนึ่งปี) ก็ไม่ยืดออกเมื่อ token ถูก rotate
    • Okta: rotation เป็น default สำหรับ single-page app ส่วน web app กับ mobile app จะได้ persistent refresh token ยกเว้นจะเลือก "Rotate token after every use" ส่วนตอนเจอการใช้ซ้ำ มันจะ invalidate refresh token ตัวล่าสุดและ access token ทุกตัวที่ออกไปตั้งแต่ผู้ใช้ authenticate ส่วน token ที่ rotate แล้วยังใช้เวลาหมดอายุของ token ตัวแรก
    • Microsoft Entra ID: ทุกการ refresh คืน refresh token ตัวใหม่ แต่ตัวเก่าไม่ถูก revoke ตอนที่ใช้ (เอกสารบอกให้แอปลบมันเอง) ทำให้ token ไม่ได้ใช้ได้ครั้งเดียว สิ่งที่จำกัด token ที่โดนขโมยในกรณีนี้คืออายุของมัน: 24 ชั่วโมงสำหรับ single-page app ที่ส่งต่อไปถึง token ใหม่ทุกตัว และ 90 วันในสถานการณ์อื่นเกือบทั้งหมด
    • Amazon Cognito: rotation เป็นทางเลือกราย app client (RefreshTokenRotation) มี retry grace period ตั้งแต่ 0 ถึง 60 วินาที และใช้ร่วมกับ flow REFRESH_TOKEN_AUTH ไม่ได้ ส่วน token ที่ rotate แล้วยังใช้เวลาหมดอายุเดิม (default 30 วัน ตั้งได้ตั้งแต่ 60 นาทีถึง 10 ปี) เอกสารอธิบายว่า token เก่าถูก invalidate แต่ไม่ได้บอกว่าทั้ง chain จะถูก revoke เมื่อมีตัวหนึ่งถูก replay