[{"data":1,"prerenderedAt":7206},["ShallowReactive",2],{"/knowledge-base/jwt":3,"/knowledge-base/jwt-surround":7197},{"id":4,"title":5,"body":6,"category":7179,"contentType":7180,"date":7181,"description":7182,"extension":7183,"head":7184,"image":7185,"meta":7186,"navigation":835,"ogImage":7184,"path":7187,"readingTime":7188,"robots":7184,"schemaOrg":7184,"seo":7189,"sitemap":7190,"stem":7191,"tags":7192,"__hash__":7196},"knowledgeBase/knowledge-base/jwt.md","JWT Implementation: Refresh Tokens vs. Private Key",{"type":7,"value":8,"toc":7155},"minimark",[9,14,18,21,24,29,34,47,51,72,82,86,90,118,122,139,146,151,155,161,167,170,178,182,196,200,636,642,645,648,650,654,659,667,672,842,847,854,856,860,1116,1121,1124,1127,1131,1149,1153,1156,1183,1186,1191,1270,1275,1323,1328,1332,1335,1346,1354,4171,4174,4179,4228,4238,4244,4248,4251,4337,4356,4360,4536,4543,4548,4553,4709,4713,4719,4742,5155,5175,5179,5183,5186,5192,5195,5199,5202,5205,5209,5218,5221,5227,5231,5234,5237,5241,5275,5279,5282,5286,5391,5395,5398,5406,5410,5424,5428,5431,5435,5810,5824,5828,6574,6591,6595,6599,6607,6611,6619,6623,6628,6632,6697,6702,6708,6715,6718,6722,6725,6730,6733,6737,6765,6773,6777,6812,6837,6841,6844,6848,6889,6897,6901,6912,6916,6937,6941,6978,6989,6993,7006,7011,7015,7040,7044,7051,7070,7074,7078,7092,7096,7107,7111,7119,7123,7135,7143,7148,7151],[10,11,13],"h2",{"id":12},"part-1-jwt-implementation-refresh-tokens-vs-private-key","Part 1: JWT Implementation: Refresh Tokens vs. Private Key",[15,16,17],"p",{},"This guide provides a comprehensive breakdown of how refresh tokens and access tokens work within a JWT authentication flow. It explains why a refresh token should never be treated as a “private key,” and how to architect signing and verification securely.",[15,19,20],{},"A refresh token is not a substitute for your cryptographic signing key. Instead, the signing key (HMAC secret or RSA/EC private key) must remain strictly on the server, while the refresh token serves as a long‐lived credential that is stored and validated server‐side.",[22,23],"hr",{},[25,26,28],"h3",{"id":27},"_1-jwt-signing-verification-the-real-keys","1. JWT Signing & Verification: The Real “Keys”",[30,31,33],"h4",{"id":32},"signing-key-secret-or-private-key","Signing Key (Secret or Private Key)",[35,36,37,41,44],"ul",{},[38,39,40],"li",{},"This key lives only on your server (or in a secured KMS/HSM).",[38,42,43],{},"You use it to sign every access‐token JWT you issue.",[38,45,46],{},"It must never be shared with clients. If it leaks, anyone can forge valid JWTs.",[30,48,50],{"id":49},"verification-key-same-secret-or-a-public-key","Verification Key (Same Secret or a Public Key)",[35,52,53,61],{},[38,54,55,56,60],{},"If you use HMAC (e.g. HS256), then signing and verification use the same secret. Clients never see that secret; your server just calls ",[57,58,59],"code",{},"jwt.verify(...)",".",[38,62,63,64],{},"If you use asymmetric signing (e.g. RS256/ES256), then you sign with your private key and verify with the public key.",[35,65,66,69],{},[38,67,68],{},"You can distribute the public key to downstream services (or embed it in your resource server) so they can independently check the JWT’s signature.",[38,70,71],{},"But the private key stays on your auth server and is never exposed to clients.",[73,74,75],"blockquote",{},[15,76,77,81],{},[78,79,80],"strong",{},"Key point:"," The JWT signing key (whether a shared HMAC secret or an RSA/EC private key) is the true >“private key” in the system. Clients only ever get a signed token (the access JWT), not the signing key itself.",[25,83,85],{"id":84},"_2-access-token-vs-refresh-token-what-each-really-is","2. Access Token vs. Refresh Token: What Each Really Is",[30,87,89],{"id":88},"access-token-jwt","Access Token (JWT)",[35,91,92,95,98,101,108,115],{},[38,93,94],{},"A short‐lived, self‐contained credential.",[38,96,97],{},"Contains claims (e.g. user ID, roles, expiration) signed by your server.",[38,99,100],{},"Bearer‐style: whoever holds it can “act as” that user for the duration of its lifetime (for example, 10–15 minutes).",[38,102,103,104,107],{},"Sent on every API request in the ",[57,105,106],{},"Authorization: Bearer \u003Caccess_token>"," header.",[38,109,110,111,114],{},"Resource servers verify its signature (with the HMAC secret or with your public key) and check the ",[57,112,113],{},"exp"," claim.",[38,116,117],{},"Once it expires, clients must use a valid refresh token to get a new access token.",[30,119,121],{"id":120},"refresh-token","Refresh Token",[35,123,124,127],{},[38,125,126],{},"A long‐lived credential whose sole purpose is to obtain new access tokens.",[38,128,129,130],{},"It is typically:",[131,132,133,136],"ol",{},[38,134,135],{},"A random, high‐entropy opaque string (e.g. a UUID or a securely generated random byte sequence), or",[38,137,138],{},"A JWT with a very long expiration (less common—because if a long‐lived JWT leaks, it’s riskier).",[73,140,141],{},[15,142,143,145],{},[78,144,80],{}," The refresh token is not a signing key. It is just a bearer credential that your server checks against a database (or an in-memory store, or Redis) to see “Is this refresh token still valid/issued?”",[35,147,148],{},[38,149,150],{},"Stored by the client in an HTTP-only, secure cookie (or in secure storage if you’re building an SPA with strict XSS protections). The client never “sees” the raw refresh‐token string if you use cookies with the HttpOnly flag.",[25,152,154],{"id":153},"_3-why-you-should-not-treat-a-refresh-token-as-a-private-key","3. Why You Should Not Treat a Refresh Token as a “Private Key”",[15,156,157,160],{},[78,158,159],{},"Private‐Key Role:"," A private key in a JWT setup is what actually signs the access token. It must remain secret, and is used mathematically (RSA/EC) or as an HMAC secret.",[15,162,163,166],{},[78,164,165],{},"Refresh Token Role:"," A refresh token is just a long‐lived opaque value (or a long‐lived JWT). It’s stored server-side (e.g. in a database table or Redis) so that when the client presents it, you can look up its status (still valid? user not disabled? device not blocked?).",[15,168,169],{},"If you were to give a client something that it could use to sign future access tokens, they could mint any access token they want. That completely breaks security.",[73,171,172],{},[15,173,174,177],{},[78,175,176],{},"In other words:"," If you hand out your “refresh token” as if it were your signing private key, the client could just create a fresh, valid access‐token JWT at will—bypassing every check. That is why your signing key can never leave your backend environment.",[25,179,181],{"id":180},"_4-a-proper-jwt-flow-in-nodejs-hs256-or-rs256","4. A Proper JWT Flow in Node.js (HS256 or RS256)",[15,183,184,185,188,189,188,192,195],{},"The following is a canonical way to implement JWT + refresh token logic in Node.js/Express. The exact library calls will depend on whichever JWT library you choose (e.g. ",[78,186,187],{},"jsonwebtoken",", ",[78,190,191],{},"jose",[78,193,194],{},"@auth0/node-jsonwebtoken",", etc.), but the pattern remains:",[30,197,199],{"id":198},"_41-at-login-issue-both-tokens","4.1 At Login (Issue Both Tokens)",[131,201,202,208,419,504],{},[38,203,204,207],{},[78,205,206],{},"Authenticate User Credentials"," (e.g. email/password)",[38,209,210,213,216,217,306,308,311,327],{},[78,211,212],{},"Create an Access Token (JWT)",[214,215],"br",{},"Payload might include:",[218,219,224],"pre",{"className":220,"code":221,"language":222,"meta":223,"style":223},"language-js shiki shiki-themes github-light github-dark github-dark","{\n  sub: user.id,\n  name: user.name,\n  roles: user.roles,\n  iat: \u003Cnow>,\n  exp: \u003Cnow + 15 minutes>\n}\n","js","",[57,225,226,235,245,254,263,279,300],{"__ignoreMap":223},[227,228,231],"span",{"class":229,"line":230},"line",1,[227,232,234],{"class":233},"slsVL","{\n",[227,236,238,242],{"class":229,"line":237},2,[227,239,241],{"class":240},"shcOC","  sub",[227,243,244],{"class":233},": user.id,\n",[227,246,248,251],{"class":229,"line":247},3,[227,249,250],{"class":240},"  name",[227,252,253],{"class":233},": user.name,\n",[227,255,257,260],{"class":229,"line":256},4,[227,258,259],{"class":240},"  roles",[227,261,262],{"class":233},": user.roles,\n",[227,264,266,269,272,276],{"class":229,"line":265},5,[227,267,268],{"class":240},"  iat",[227,270,271],{"class":233},": \u003C",[227,273,275],{"class":274},"sByVh","now",[227,277,278],{"class":233},">,\n",[227,280,282,285,287,291,294,297],{"class":229,"line":281},6,[227,283,284],{"class":233},"  exp: \u003C",[227,286,275],{"class":274},[227,288,290],{"class":289},"sVAnh"," +",[227,292,293],{"class":289}," 15",[227,295,296],{"class":240}," minutes",[227,298,299],{"class":233},">\n",[227,301,303],{"class":229,"line":302},7,[227,304,305],{"class":233},"}\n",[214,307],{},[78,309,310],{},"Sign it with:",[35,312,313,320],{},[38,314,315,316,319],{},"For HS256: a server‐only secret (e.g. ",[57,317,318],{},"process.env.JWT_SECRET",").",[38,321,322,323,326],{},"For RS256: your RSA/EC private key (loaded via ",[57,324,325],{},"fs.readFileSync + crypto.createPrivateKey(...)"," or a JWK).",[218,328,330],{"className":220,"code":329,"language":222,"meta":223,"style":223},"import jwt from 'jsonwebtoken';\n// HS256 example:\nconst accessToken = jwt.sign(\n  { sub: user.id, name: user.name, roles: user.roles },\n  process.env.JWT_SECRET,               // keep this secret on server only\n  { algorithm: 'HS256', expiresIn: '15m' }\n);\n",[57,331,332,351,357,378,383,397,414],{"__ignoreMap":223},[227,333,334,338,341,344,348],{"class":229,"line":230},[227,335,337],{"class":336},"so5gQ","import",[227,339,340],{"class":233}," jwt ",[227,342,343],{"class":336},"from",[227,345,347],{"class":346},"sfrk1"," 'jsonwebtoken'",[227,349,350],{"class":233},";\n",[227,352,353],{"class":229,"line":237},[227,354,356],{"class":355},"sCsY4","// HS256 example:\n",[227,358,359,362,366,369,372,375],{"class":229,"line":247},[227,360,361],{"class":336},"const",[227,363,365],{"class":364},"suiK_"," accessToken",[227,367,368],{"class":336}," =",[227,370,371],{"class":233}," jwt.",[227,373,374],{"class":240},"sign",[227,376,377],{"class":233},"(\n",[227,379,380],{"class":229,"line":256},[227,381,382],{"class":233},"  { sub: user.id, name: user.name, roles: user.roles },\n",[227,384,385,388,391,394],{"class":229,"line":265},[227,386,387],{"class":233},"  process.env.",[227,389,390],{"class":364},"JWT_SECRET",[227,392,393],{"class":233},",               ",[227,395,396],{"class":355},"// keep this secret on server only\n",[227,398,399,402,405,408,411],{"class":229,"line":281},[227,400,401],{"class":233},"  { algorithm: ",[227,403,404],{"class":346},"'HS256'",[227,406,407],{"class":233},", expiresIn: ",[227,409,410],{"class":346},"'15m'",[227,412,413],{"class":233}," }\n",[227,415,416],{"class":229,"line":302},[227,417,418],{"class":233},");\n",[38,420,421,424,426,427,480,482,483,486,487,490,491,319,494,496,497,500,501,60],{},[78,422,423],{},"Create a Refresh Token",[214,425],{},"Best practice: generate a totally random string, e.g.:",[218,428,430],{"className":220,"code":429,"language":222,"meta":223,"style":223},"import { randomBytes } from 'crypto';\nconst refreshToken = randomBytes(32).toString('hex'); // 256-bit of randomness\n",[57,431,432,446],{"__ignoreMap":223},[227,433,434,436,439,441,444],{"class":229,"line":230},[227,435,337],{"class":336},[227,437,438],{"class":233}," { randomBytes } ",[227,440,343],{"class":336},[227,442,443],{"class":346}," 'crypto'",[227,445,350],{"class":233},[227,447,448,450,453,455,458,461,464,466,469,471,474,477],{"class":229,"line":237},[227,449,361],{"class":336},[227,451,452],{"class":364}," refreshToken",[227,454,368],{"class":336},[227,456,457],{"class":240}," randomBytes",[227,459,460],{"class":233},"(",[227,462,463],{"class":364},"32",[227,465,319],{"class":233},[227,467,468],{"class":240},"toString",[227,470,460],{"class":233},[227,472,473],{"class":346},"'hex'",[227,475,476],{"class":233},"); ",[227,478,479],{"class":355},"// 256-bit of randomness\n",[214,481],{},"Store ",[57,484,485],{},"refreshToken"," in your database or Redis, associated with ",[57,488,489],{},"user.id"," and a rotation/expiry timestamp (e.g. ",[57,492,493],{},"expiresAt = now + 30 days",[214,495],{},"Optionally hash the refresh token in the database (so your DB doesn’t store the raw token if it ever leaks). E.g. ",[57,498,499],{},"hashed = sha256(refreshToken)"," and store ",[57,502,503],{},"hashed + userId",[38,505,506,509,523],{},[78,507,508],{},"Send Both Tokens to the Client",[35,510,511,517],{},[38,512,513,516],{},[78,514,515],{},"Access Token:"," Usually returned in the JSON response body.",[38,518,519,522],{},[78,520,521],{},"Refresh Token:"," Send as an HTTP-only, Secure cookie:",[218,524,526],{"className":220,"code":525,"language":222,"meta":223,"style":223},"// Example Express route handler\nres\n  .cookie('refreshToken', refreshToken, {\n    httpOnly: true,\n    secure: true,       // in production, only over HTTPS\n    path: '/auth/refresh',\n    maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days\n  })\n  .json({ accessToken });\n",[57,527,528,533,538,554,565,578,588,619,625],{"__ignoreMap":223},[227,529,530],{"class":229,"line":230},[227,531,532],{"class":355},"// Example Express route handler\n",[227,534,535],{"class":229,"line":237},[227,536,537],{"class":233},"res\n",[227,539,540,543,546,548,551],{"class":229,"line":247},[227,541,542],{"class":233},"  .",[227,544,545],{"class":240},"cookie",[227,547,460],{"class":233},[227,549,550],{"class":346},"'refreshToken'",[227,552,553],{"class":233},", refreshToken, {\n",[227,555,556,559,562],{"class":229,"line":256},[227,557,558],{"class":233},"    httpOnly: ",[227,560,561],{"class":364},"true",[227,563,564],{"class":233},",\n",[227,566,567,570,572,575],{"class":229,"line":265},[227,568,569],{"class":233},"    secure: ",[227,571,561],{"class":364},[227,573,574],{"class":233},",       ",[227,576,577],{"class":355},"// in production, only over HTTPS\n",[227,579,580,583,586],{"class":229,"line":281},[227,581,582],{"class":233},"    path: ",[227,584,585],{"class":346},"'/auth/refresh'",[227,587,564],{"class":233},[227,589,590,593,596,599,602,604,607,609,611,613,616],{"class":229,"line":302},[227,591,592],{"class":233},"    maxAge: ",[227,594,595],{"class":364},"30",[227,597,598],{"class":336}," *",[227,600,601],{"class":364}," 24",[227,603,598],{"class":336},[227,605,606],{"class":364}," 60",[227,608,598],{"class":336},[227,610,606],{"class":364},[227,612,598],{"class":336},[227,614,615],{"class":364}," 1000",[227,617,618],{"class":355}," // 30 days\n",[227,620,622],{"class":229,"line":621},8,[227,623,624],{"class":233},"  })\n",[227,626,628,630,633],{"class":229,"line":627},9,[227,629,542],{"class":233},[227,631,632],{"class":240},"json",[227,634,635],{"class":233},"({ accessToken });\n",[15,637,638,639],{},"🔒 ",[78,640,641],{},"Why not return the refresh token in JSON?",[15,643,644],{},"If you return it in JSON, front-end code could read it (localStorage), making it vulnerable to XSS.",[15,646,647],{},"By using an HTTP-only cookie, you force the browser to send it automatically, but JS can’t inspect it.",[22,649],{},[30,651,653],{"id":652},"_42-on-every-api-call-protecting-routes","4.2 On Every API Call (Protecting Routes)",[15,655,656],{},[78,657,658],{},"Client Sends Access Token in Header",[218,660,665],{"className":661,"code":663,"language":664},[662],"language-text","GET /api/protected\nAuthorization: Bearer \u003CaccessToken>\n","text",[57,666,663],{"__ignoreMap":223},[15,668,669],{},[78,670,671],{},"Server Middleware Verifies Access Token",[35,673,674,766],{},[38,675,676,677],{},"With HS256:",[218,678,680],{"className":220,"code":679,"language":222,"meta":223,"style":223},"try {\n  const payload = jwt.verify(token, process.env.JWT_SECRET);\n  req.user = { id: payload.sub, roles: payload.roles };\n  next();\n} catch (err) {\n  // token expired or invalid → return 401\n  res.sendStatus(401);\n}\n",[57,681,682,690,712,723,731,742,747,762],{"__ignoreMap":223},[227,683,684,687],{"class":229,"line":230},[227,685,686],{"class":336},"try",[227,688,689],{"class":233}," {\n",[227,691,692,695,698,700,702,705,708,710],{"class":229,"line":237},[227,693,694],{"class":336},"  const",[227,696,697],{"class":364}," payload",[227,699,368],{"class":336},[227,701,371],{"class":233},[227,703,704],{"class":240},"verify",[227,706,707],{"class":233},"(token, process.env.",[227,709,390],{"class":364},[227,711,418],{"class":233},[227,713,714,717,720],{"class":229,"line":247},[227,715,716],{"class":233},"  req.user ",[227,718,719],{"class":336},"=",[227,721,722],{"class":233}," { id: payload.sub, roles: payload.roles };\n",[227,724,725,728],{"class":229,"line":256},[227,726,727],{"class":240},"  next",[227,729,730],{"class":233},"();\n",[227,732,733,736,739],{"class":229,"line":265},[227,734,735],{"class":233},"} ",[227,737,738],{"class":336},"catch",[227,740,741],{"class":233}," (err) {\n",[227,743,744],{"class":229,"line":281},[227,745,746],{"class":355},"  // token expired or invalid → return 401\n",[227,748,749,752,755,757,760],{"class":229,"line":302},[227,750,751],{"class":233},"  res.",[227,753,754],{"class":240},"sendStatus",[227,756,460],{"class":233},[227,758,759],{"class":364},"401",[227,761,418],{"class":233},[227,763,764],{"class":229,"line":621},[227,765,305],{"class":233},[38,767,768,769],{},"With RS256:",[218,770,772],{"className":220,"code":771,"language":222,"meta":223,"style":223},"import fs from 'fs';\nconst publicKey = fs.readFileSync('/path/to/public.pem');\nconst payload = jwt.verify(token, publicKey, { algorithms: ['RS256'] });\n\n// If valid → Proceed → req.user holds the user’s identity\n",[57,773,774,788,810,831,837],{"__ignoreMap":223},[227,775,776,778,781,783,786],{"class":229,"line":230},[227,777,337],{"class":336},[227,779,780],{"class":233}," fs ",[227,782,343],{"class":336},[227,784,785],{"class":346}," 'fs'",[227,787,350],{"class":233},[227,789,790,792,795,797,800,803,805,808],{"class":229,"line":237},[227,791,361],{"class":336},[227,793,794],{"class":364}," publicKey",[227,796,368],{"class":336},[227,798,799],{"class":233}," fs.",[227,801,802],{"class":240},"readFileSync",[227,804,460],{"class":233},[227,806,807],{"class":346},"'/path/to/public.pem'",[227,809,418],{"class":233},[227,811,812,814,816,818,820,822,825,828],{"class":229,"line":247},[227,813,361],{"class":336},[227,815,697],{"class":364},[227,817,368],{"class":336},[227,819,371],{"class":233},[227,821,704],{"class":240},[227,823,824],{"class":233},"(token, publicKey, { algorithms: [",[227,826,827],{"class":346},"'RS256'",[227,829,830],{"class":233},"] });\n",[227,832,833],{"class":229,"line":256},[227,834,836],{"emptyLinePlaceholder":835},true,"\n",[227,838,839],{"class":229,"line":265},[227,840,841],{"class":355},"// If valid → Proceed → req.user holds the user’s identity\n",[15,843,844],{},[78,845,846],{},"If Invalid/Expired → Return 401 Unauthorized",[73,848,849],{},[15,850,851,853],{},[78,852,80],{}," Access tokens are verified using your server‐only HMAC secret or your server’s public key (if you used RS256). The client never sees the HMAC secret or RSA private key.",[22,855],{},[30,857,859],{"id":858},"_43-when-access-token-expires-using-the-refresh-token","4.3 When Access Token Expires (Using the Refresh Token)",[131,861,862,868,892,1110],{},[38,863,864,867],{},[78,865,866],{},"Client Detects 401"," from Protected Route (Expired Access Token)",[38,869,870,873,874,877,878,881,882,884,885,887,888,891],{},[78,871,872],{},"Front-end code makes a request to"," ",[57,875,876],{},"/auth/refresh"," (e.g. ",[57,879,880],{},"POST /auth/refresh",") with no body.",[214,883],{},"Because you set ",[57,886,485],{}," as an HTTP-only cookie on ",[57,889,890],{},"Path=/auth/refresh",", the browser automatically includes that cookie.",[38,893,894,900,902,903,919,921,922,924,925],{},[78,895,896,897,899],{},"Server’s ",[57,898,876],{}," Handler",[214,901],{},"Extract the cookie:",[218,904,906],{"className":220,"code":905,"language":222,"meta":223,"style":223},"const refreshToken = req.cookies.refreshToken;\n",[57,907,908],{"__ignoreMap":223},[227,909,910,912,914,916],{"class":229,"line":230},[227,911,361],{"class":336},[227,913,452],{"class":364},[227,915,368],{"class":336},[227,917,918],{"class":233}," req.cookies.refreshToken;\n",[214,920],{},"Look up ",[57,923,485],{}," (or its hash) in your database/Redis:",[218,926,928],{"className":220,"code":927,"language":222,"meta":223,"style":223},"// 2a. Assume you stored `hashedToken = sha256(refreshToken)` when you first issued it.\nconst hashed = sha256(refreshTokenFromCookie);\nconst record = await db.findRefreshToken(hashed);\nif (!record) throw new Error('Invalid refresh token');\n\n// 2b. If valid, generate a new access token:\nconst newAccessToken = jwt.sign(\n  { sub: record.userId, roles: record.userRoles },\n  process.env.JWT_SECRET,\n  { algorithm: 'HS256', expiresIn: '15m' }\n);\n\n// 2c. (Optional) implement “rotate on use”:\n//      i. Remove the old refresh token record from DB.\n//     ii. Generate a new random refreshToken (randomBytes(32).toString('hex'))\n//    iii. Store new hashedRefreshToken in DB with new expiry.\n//     iv. Set new HTTP-only cookie: res.cookie('refreshToken', newRefreshToken, …)\n\nreturn res.json({ accessToken: newAccessToken });\n",[57,929,930,935,950,971,1001,1005,1010,1025,1030,1038,1051,1056,1061,1067,1073,1079,1085,1091,1096],{"__ignoreMap":223},[227,931,932],{"class":229,"line":230},[227,933,934],{"class":355},"// 2a. Assume you stored `hashedToken = sha256(refreshToken)` when you first issued it.\n",[227,936,937,939,942,944,947],{"class":229,"line":237},[227,938,361],{"class":336},[227,940,941],{"class":364}," hashed",[227,943,368],{"class":336},[227,945,946],{"class":240}," sha256",[227,948,949],{"class":233},"(refreshTokenFromCookie);\n",[227,951,952,954,957,959,962,965,968],{"class":229,"line":247},[227,953,361],{"class":336},[227,955,956],{"class":364}," record",[227,958,368],{"class":336},[227,960,961],{"class":336}," await",[227,963,964],{"class":233}," db.",[227,966,967],{"class":240},"findRefreshToken",[227,969,970],{"class":233},"(hashed);\n",[227,972,973,976,979,982,985,988,991,994,996,999],{"class":229,"line":256},[227,974,975],{"class":336},"if",[227,977,978],{"class":233}," (",[227,980,981],{"class":336},"!",[227,983,984],{"class":233},"record) ",[227,986,987],{"class":336},"throw",[227,989,990],{"class":336}," new",[227,992,993],{"class":240}," Error",[227,995,460],{"class":233},[227,997,998],{"class":346},"'Invalid refresh token'",[227,1000,418],{"class":233},[227,1002,1003],{"class":229,"line":265},[227,1004,836],{"emptyLinePlaceholder":835},[227,1006,1007],{"class":229,"line":281},[227,1008,1009],{"class":355},"// 2b. If valid, generate a new access token:\n",[227,1011,1012,1014,1017,1019,1021,1023],{"class":229,"line":302},[227,1013,361],{"class":336},[227,1015,1016],{"class":364}," newAccessToken",[227,1018,368],{"class":336},[227,1020,371],{"class":233},[227,1022,374],{"class":240},[227,1024,377],{"class":233},[227,1026,1027],{"class":229,"line":621},[227,1028,1029],{"class":233},"  { sub: record.userId, roles: record.userRoles },\n",[227,1031,1032,1034,1036],{"class":229,"line":627},[227,1033,387],{"class":233},[227,1035,390],{"class":364},[227,1037,564],{"class":233},[227,1039,1041,1043,1045,1047,1049],{"class":229,"line":1040},10,[227,1042,401],{"class":233},[227,1044,404],{"class":346},[227,1046,407],{"class":233},[227,1048,410],{"class":346},[227,1050,413],{"class":233},[227,1052,1054],{"class":229,"line":1053},11,[227,1055,418],{"class":233},[227,1057,1059],{"class":229,"line":1058},12,[227,1060,836],{"emptyLinePlaceholder":835},[227,1062,1064],{"class":229,"line":1063},13,[227,1065,1066],{"class":355},"// 2c. (Optional) implement “rotate on use”:\n",[227,1068,1070],{"class":229,"line":1069},14,[227,1071,1072],{"class":355},"//      i. Remove the old refresh token record from DB.\n",[227,1074,1076],{"class":229,"line":1075},15,[227,1077,1078],{"class":355},"//     ii. Generate a new random refreshToken (randomBytes(32).toString('hex'))\n",[227,1080,1082],{"class":229,"line":1081},16,[227,1083,1084],{"class":355},"//    iii. Store new hashedRefreshToken in DB with new expiry.\n",[227,1086,1088],{"class":229,"line":1087},17,[227,1089,1090],{"class":355},"//     iv. Set new HTTP-only cookie: res.cookie('refreshToken', newRefreshToken, …)\n",[227,1092,1094],{"class":229,"line":1093},18,[227,1095,836],{"emptyLinePlaceholder":835},[227,1097,1099,1102,1105,1107],{"class":229,"line":1098},19,[227,1100,1101],{"class":336},"return",[227,1103,1104],{"class":233}," res.",[227,1106,632],{"class":240},[227,1108,1109],{"class":233},"({ accessToken: newAccessToken });\n",[38,1111,1112,1115],{},[78,1113,1114],{},"Client Retries Original API Call"," with New Access Token.",[15,1117,638,1118],{},[78,1119,1120],{},"Security note:",[15,1122,1123],{},"If an attacker somehow steals a refresh token, they can keep minting new access tokens until that refresh token is revoked/expired.",[15,1125,1126],{},"That’s why you want to keep refresh tokens in an HttpOnly, Secure cookie, check IP/client fingerprints if needed, and rotate them on every use (so you can blacklist a stolen one).",[25,1128,1130],{"id":1129},"_5-summary-why-refresh-token-as-private-key-is-incorrect","5. Summary: Why “Refresh Token as Private Key” Is Incorrect",[35,1132,1133,1136,1143,1146],{},[38,1134,1135],{},"You never hand out your signing key—that is your “private key.” It stays on the server (or KMS).",[38,1137,1138,1139,1142],{},"A refresh token is just a long‐lived credential you validate against a store. It is not used to ",[1140,1141,374],"em",{}," anything.",[38,1144,1145],{},"Access tokens (JWTs) already contain the client-visible “proof” (the signed payload). Resource servers verify that proof with your HMAC secret (HS256) or with your public key (RS256).",[38,1147,1148],{},"Treat the refresh token like a password or API key—only your back end can verify it (by looking it up in a database or by decrypting/verifying a signed JWT). Don’t let the client “sign” tokens or think of the refresh token as a signing key.",[25,1150,1152],{"id":1151},"_6-if-you-really-want-asymmetric-jwt-rs256-across-services","6. If You Really Want Asymmetric JWT (RS256) Across Services",[15,1154,1155],{},"Sometimes you issue an access token on Service A and want Service B (a separate microservice) to trust it without hitting a central database. In that scenario:",[131,1157,1158,1168,1171,1177],{},[38,1159,1160,1161,1164,1165,319],{},"Generate an RSA/EC key pair on your auth server (e.g. with ",[57,1162,1163],{},"openssl"," or ",[57,1166,1167],{},"crypto.generateKeyPairSync('rsa', …)",[38,1169,1170],{},"Keep the private key on your auth server only; all JWTs are signed with that.",[38,1172,1173,1174,60],{},"Publish the public key (or JWK Set) to any downstream service that needs to verify access tokens—e.g. via a well‐known endpoint like ",[57,1175,1176],{},"https://auth.myapp.com/.well-known/jwks.json",[38,1178,1179,1180,60],{},"Downstream services load that public key (or periodically rotate and cache it), then call ",[57,1181,1182],{},"jwt.verify(token, publicKey, { algorithms: ['RS256'] })",[15,1184,1185],{},"Refresh token logic remains exactly the same: an opaque, server‐validated credential that never leaves your auth cluster.",[15,1187,1188],{},[78,1189,1190],{},"Auth Server:",[35,1192,1193,1200,1261,1264],{},[38,1194,1195,1196,1199],{},"Holds ",[57,1197,1198],{},"private.pem"," (never exposed).",[38,1201,1202,1203],{},"Signs JWT access tokens:",[218,1204,1206],{"className":220,"code":1205,"language":222,"meta":223,"style":223},"const accessToken = jwt.sign(\n  { sub: user.id, … },\n  fs.readFileSync('./private.pem'), // private key\n  { algorithm: 'RS256', expiresIn: '15m' }\n);\n",[57,1207,1208,1222,1227,1245,1257],{"__ignoreMap":223},[227,1209,1210,1212,1214,1216,1218,1220],{"class":229,"line":230},[227,1211,361],{"class":336},[227,1213,365],{"class":364},[227,1215,368],{"class":336},[227,1217,371],{"class":233},[227,1219,374],{"class":240},[227,1221,377],{"class":233},[227,1223,1224],{"class":229,"line":237},[227,1225,1226],{"class":233},"  { sub: user.id, … },\n",[227,1228,1229,1232,1234,1236,1239,1242],{"class":229,"line":247},[227,1230,1231],{"class":233},"  fs.",[227,1233,802],{"class":240},[227,1235,460],{"class":233},[227,1237,1238],{"class":346},"'./private.pem'",[227,1240,1241],{"class":233},"), ",[227,1243,1244],{"class":355},"// private key\n",[227,1246,1247,1249,1251,1253,1255],{"class":229,"line":256},[227,1248,401],{"class":233},[227,1250,827],{"class":346},[227,1252,407],{"class":233},[227,1254,410],{"class":346},[227,1256,413],{"class":233},[227,1258,1259],{"class":229,"line":265},[227,1260,418],{"class":233},[38,1262,1263],{},"Creates/stores refresh tokens in DB.",[38,1265,1266,1267,1269],{},"Exposes ",[57,1268,876],{}," that verifies refresh tokens and issues new JWTs.",[15,1271,1272],{},[78,1273,1274],{},"API Gateway / Microservice:",[35,1276,1277,1283,1320],{},[38,1278,1195,1279,1282],{},[57,1280,1281],{},"public.pem"," (only the public half).",[38,1284,1285,1286],{},"Verifies incoming JWTs:",[218,1287,1289],{"className":220,"code":1288,"language":222,"meta":223,"style":223},"const payload = jwt.verify(token, fs.readFileSync('./public.pem'), { algorithms: ['RS256'] });\n",[57,1290,1291],{"__ignoreMap":223},[227,1292,1293,1295,1297,1299,1301,1303,1306,1308,1310,1313,1316,1318],{"class":229,"line":230},[227,1294,361],{"class":336},[227,1296,697],{"class":364},[227,1298,368],{"class":336},[227,1300,371],{"class":233},[227,1302,704],{"class":240},[227,1304,1305],{"class":233},"(token, fs.",[227,1307,802],{"class":240},[227,1309,460],{"class":233},[227,1311,1312],{"class":346},"'./public.pem'",[227,1314,1315],{"class":233},"), { algorithms: [",[227,1317,827],{"class":346},[227,1319,830],{"class":233},[38,1321,1322],{},"Never sees the private key, never touches refresh tokens.",[73,1324,1325],{},[15,1326,1327],{},"Again: neither the access token nor the refresh token “is” the private key. The private key is solely used to sign the access token; the access token itself is just data + signature. The refresh token is just a server‐validated string (or long‐lived JWT) whose only job is to let you issue a new access token.",[25,1329,1331],{"id":1330},"_7-putting-it-into-code-nodejs-example-hs256","7. Putting It Into Code: Node.js Example (HS256)",[15,1333,1334],{},"The following is a minimal Express snippet showing:",[35,1336,1337,1340,1343],{},[38,1338,1339],{},"How to issue JWTs (HS256).",[38,1341,1342],{},"How to store/validate refresh tokens in an in‐memory map (for demo only—use a real DB in production).",[38,1344,1345],{},"How to rotate refresh tokens on use.",[73,1347,1348],{},[15,1349,1350,1353],{},[78,1351,1352],{},"IMPORTANT:"," Never use an in‐memory store for refresh tokens in production. Use a persistent store (Redis, SQL, etc.), so you can revoke tokens if your server restarts or if you need to blacklist a compromised token.",[218,1355,1357],{"className":220,"code":1356,"language":222,"meta":223,"style":223},"// File: authServer.js\nimport express from 'express';\nimport cookieParser from 'cookie-parser';\nimport jwt from 'jsonwebtoken';\nimport { randomBytes, createHash } from 'crypto';\n\nconst app = express();\napp.use(express.json());\napp.use(cookieParser());\n\nconst JWT_SECRET = process.env.JWT_SECRET || 'super-secure-secret-key'; // keep this safe\nconst ACCESS_TOKEN_EXPIRY = '15m';\nconst REFRESH_TOKEN_EXPIRY_SEC = 30 * 24 * 60 * 60; // 30 days in seconds\n\n// Simulated in‐memory store for refresh tokens:\n// key = hashedRefreshToken, value = { userId, expiresAt: unixTimestampSec }\nconst refreshTokenStore = new Map();\n\n// Utility to hash a token before storing in DB:\nfunction hashToken(token) {\n  return createHash('sha256').update(token).digest('hex');\n}\n\n// 1. LOGIN: Validate credentials, then issue both tokens\napp.post('/auth/login', async (req, res) => {\n  const { email, password } = req.body;\n  // (In production) verify email+password from your user database:\n  const user = await findUserByEmail(email);\n  if (!user || !checkPassword(user, password)) {\n    return res.status(401).json({ error: 'Invalid credentials' });\n  }\n\n  // 1a. Issue access token (short‐lived)\n  const accessToken = jwt.sign(\n    { sub: user.id, name: user.name, roles: user.roles },\n    JWT_SECRET,\n    { algorithm: 'HS256', expiresIn: ACCESS_TOKEN_EXPIRY }\n  );\n\n  // 1b. Issue a refresh token (random string), store its hash in DB\n  const rawRefreshToken = randomBytes(32).toString('hex'); // 256-bit\n  const hashed = hashToken(rawRefreshToken);\n  const expiresAt = Math.floor(Date.now() / 1000) + REFRESH_TOKEN_EXPIRY_SEC;\n\n  refreshTokenStore.set(hashed, { userId: user.id, expiresAt });\n\n  // 1c. Send both to the client (access in JSON; refresh as HttpOnly cookie)\n  res\n    .cookie('refreshToken', rawRefreshToken, {\n      httpOnly: true,\n      secure: process.env.NODE_ENV === 'production',\n      path: '/auth/refresh',\n      maxAge: REFRESH_TOKEN_EXPIRY_SEC * 1000\n    })\n    .json({ accessToken });\n});\n\n// 2. PROTECTED ROUTE: Verify Access Token\napp.get('/api/protected', (req, res) => {\n  const authHeader = req.headers['authorization'] || '';\n  const token = authHeader.split(' ')[1]; // “Bearer \u003Ctoken>”\n  if (!token) return res.sendStatus(401);\n\n  try {\n    const payload = jwt.verify(token, JWT_SECRET);\n    // payload = { sub: user.id, name: user.name, roles: [ … ], iat: \u003Cn>, exp: \u003Cn> }\n    req.user = { id: payload.sub, roles: payload.roles };\n    return res.json({ data: 'This is protected data', user: req.user });\n  } catch (err) {\n    return res.sendStatus(401);\n  }\n});\n\n// 3. REFRESH: Validate refresh token, rotate on use, issue new access token\napp.post('/auth/refresh', (req, res) => {\n  const rawRefreshToken = req.cookies.refreshToken;\n  if (!rawRefreshToken) {\n    return res.sendStatus(401);\n  }\n\n  const hashed = hashToken(rawRefreshToken);\n  const record = refreshTokenStore.get(hashed);\n\n  if (!record) {\n    return res.sendStatus(401); // no such token → unauthorized\n  }\n\n  const nowSec = Math.floor(Date.now() / 1000);\n  if (record.expiresAt \u003C nowSec) {\n    // expired refresh token\n    refreshTokenStore.delete(hashed);\n    return res.sendStatus(401);\n  }\n\n  // 3a. Rotate the refresh token:\n  refreshTokenStore.delete(hashed); // revoke old\n  const newRawRefresh = randomBytes(32).toString('hex');\n  const newHashed = hashToken(newRawRefresh);\n  const newExpiresAt = nowSec + REFRESH_TOKEN_EXPIRY_SEC;\n  refreshTokenStore.set(newHashed, { userId: record.userId, expiresAt: newExpiresAt });\n\n  // 3b. Issue a new access token:\n  const newAccessToken = jwt.sign(\n    { sub: record.userId /*, other claims… */ },\n    JWT_SECRET,\n    { algorithm: 'HS256', expiresIn: ACCESS_TOKEN_EXPIRY }\n  );\n\n  // 3c. Set new refresh token cookie, send new access token in JSON\n  res\n    .cookie('refreshToken', newRawRefresh, {\n      httpOnly: true,\n      secure: process.env.NODE_ENV === 'production',\n      path: '/auth/refresh',\n      maxAge: REFRESH_TOKEN_EXPIRY_SEC * 1000\n    })\n    .json({ accessToken: newAccessToken });\n});\n\n// 4. LOGOUT: Invalidate current refresh token\napp.post('/auth/logout', (req, res) => {\n  const rawRefreshToken = req.cookies.refreshToken;\n  if (rawRefreshToken) {\n    const hashed = hashToken(rawRefreshToken);\n    refreshTokenStore.delete(hashed);\n  }\n  // Clear cookie on client\n  res\n    .clearCookie('refreshToken', { path: '/auth/refresh' })\n    .sendStatus(204);\n});\n\napp.listen(4000, () => {\n  console.log('Auth server listening on port 4000');\n});\n\n/**\n * Dummy user‐lookup/password‐check functions for illustration only:\n */\nasync function findUserByEmail(email) {\n  // In reality, query your users table\n  if (email === 'alice@example.com') {\n    return { id: 'user123', name: 'Alice', roles: ['user'], passwordHash: '…' };\n  }\n  return null;\n}\nfunction checkPassword(user, password) {\n  // In reality, bcrypt.compare(password, user.passwordHash)\n  return password === 'correct‐horse‐battery‐staple';\n}\n\n### 8. Key Takeaways\n\n- Never confuse the refresh token with your signing key.\n- The signing key (HMAC secret or RSA/EC private key) stays on your server, never goes to a client, and is used purely to sign (and verify) JWTs.\n- A refresh token is just a long‐lived random credential that your server stores and checks; it has nothing to do with cryptographic signing of access tokens.\n- Access tokens are publicly visible to the client, but they are only valid because your server signed them with its private key/secret. The client does not hold any secret—only the signed JWT itself.\n- Refresh tokens are also “bearer” tokens, but they are never meant to be parsed or “verified” by the client. They simply live in the client’s HttpOnly cookie, and your server treats them as a key in a server‐side store (database/Redis) that says, “Yes, that client is allowed to get a new access token now.”\n- If you need asymmetric validation (e.g., microservices), use an RSA/EC key pair: sign with the private key on Auth Server; distribute only the public key to the resource servers to let them verify incoming JWTs. The refresh token stays opaque and is validated only by the Auth Server.\n- Do not send any “private key” to the browser. You do not want client code (or malicious third‐parties) to ever have the ability to sign their own tokens.\n\nBy following the above pattern, you ensure that:\n- No sensitive signing material ever leaves your backend.\n- Clients only hold a short‐lived access token and an opaque refresh token in an HttpOnly cookie.\n- Your refresh token never “becomes” a private key—it simply lets you decide when to issue a brand‐new signed access token.\n\n**TL;DR**\n\nIn a proper JWT flow, you must keep a single signing key (HMAC secret or RSA/EC private key) strictly on the server. You sign access tokens with that key and verify them with its counterpart (the same secret in HS256 or the public key in RS256). The refresh token is merely a long‐lived credential you store server‐side and never share except in an HttpOnly cookie. It should never act as a \"private key\" and is not used to cryptographically sign or verify access tokens.\n\n---\n\n## Part 2: Stateless Refresh-Token Flow\n\nThis section explains how and why you might build a truly stateless refresh‐token flow—along with the inherent trade‐offs. In short, avoiding server‐side state means giving up per‐token revocation and forced expiry. However, if those limitations are acceptable, a self‐contained (JWT) refresh token can live entirely in the client and be validated solely via signature and expiration time.\n\n### 1. What “Stateless Refresh Token” Actually Means\n\n#### Stateless (no server‐side store at all)\n\n* The server keeps zero records of which refresh tokens were issued.\n* Each refresh token must be fully “self‐contained”: it carries in its own payload everything the server needs to decide “OK, is this valid?”\n\nIn practice, that means the refresh token itself is a signed JWT (or JWS) that:\n\n* Encodes a unique user/session identifier (e.g. a user ID or “session ID”).\n* Carries an `exp` (expiration) claim.\n* (Optionally) carries any other claims you need (e.g. user roles, token version, etc.).\n\nWhen the client presents that refresh‐JWT, the server just:\n\n1. Verifies its signature against a server‐only signing key.\n2. Checks `exp` (and maybe `nbf`).\n3. If valid, issues a new access token (and optionally a new refresh token).\n\nNo database check—no “lookup” is done. Everything is in the JWT.\n\nBecause there’s no lookup (no hash‐table or DB), the server cannot:\n\n* Immediately revoke a single user’s existing refresh token (unless you switch to a new signing key or embed some revocation‐list version, which reintroduces state).\n* Force‐expire one client’s refresh token early; once it’s signed and unexpired, it’s valid until its JWT `exp`.\n* Detect “replay” of an old token (a stolen refresh JWT can be used until it expires).\n\n#### Why consider stateless at all?\n\n* You avoid any server‐side overhead for storing/reading refresh‐token records (e.g. no Redis or no SQL table).\n* Under very high load or simple microservices, you may want to keep your auth servers purely “sign/verify” without hitting a database on every refresh.\n* Fewer moving parts can mean easier horizontal scaling: any instance with the same signing key can handle a refresh request in one step.\n\n### 2. How to Build a Self-Contained (Stateless) Refresh Token in Node.js\n\nThe following is a minimal outline of a refresh flow that maintains zero state on the server. It uses JWTs (signed with HS256 or RS256), but any JWS-compatible algorithm can be substituted.\n\n#### 2.1 Decide on Your Signing Key\n\n**HS256 (HMAC):**\n\n```js\n// Example: put this in your environment (never check into Git)\nprocess.env.JWT_REFRESH_SECRET = 'a‐very‐long‐random‐string‐only‐on‐server';\n",[57,1358,1359,1364,1378,1392,1404,1417,1421,1435,1451,1464,1468,1494,1508,1537,1541,1546,1551,1567,1571,1576,1594,1625,1630,1635,1641,1677,1701,1707,1725,1750,1778,1784,1789,1795,1810,1816,1824,1839,1845,1850,1856,1885,1899,1937,1942,1954,1959,1965,1971,1986,1996,2013,2023,2037,2043,2052,2058,2063,2069,2097,2123,2156,2180,2185,2193,2214,2220,2230,2248,2258,2273,2278,2283,2288,2294,2319,2330,2342,2357,2362,2367,2380,2396,2401,2413,2431,2436,2441,2467,2481,2487,2498,2513,2518,2523,2529,2542,2568,2583,2602,2612,2617,2623,2638,2650,2657,2670,2675,2680,2686,2691,2705,2714,2727,2736,2747,2752,2761,2766,2771,2777,2803,2814,2822,2835,2844,2849,2855,2860,2880,2894,2899,2904,2924,2940,2945,2950,2956,2962,2968,2984,2990,3006,3038,3043,3053,3058,3077,3083,3098,3103,3108,3120,3125,3140,3175,3201,3225,3292,3332,3350,3355,3366,3374,3387,3400,3405,3423,3428,3536,3541,3547,3552,3569,3574,3605,3610,3621,3626,3638,3643,3657,3688,3693,3712,3717,3747,3761,3775,3780,3796,3801,3809,3829,3853,3858,3873,3878,3901,3906,3924,3939,3962,3967,3976,3981,4006,4019,4043,4048,4071,4076,4111,4116,4127,4132,4148,4153,4159,4165],{"__ignoreMap":223},[227,1360,1361],{"class":229,"line":230},[227,1362,1363],{"class":355},"// File: authServer.js\n",[227,1365,1366,1368,1371,1373,1376],{"class":229,"line":237},[227,1367,337],{"class":336},[227,1369,1370],{"class":233}," express ",[227,1372,343],{"class":336},[227,1374,1375],{"class":346}," 'express'",[227,1377,350],{"class":233},[227,1379,1380,1382,1385,1387,1390],{"class":229,"line":247},[227,1381,337],{"class":336},[227,1383,1384],{"class":233}," cookieParser ",[227,1386,343],{"class":336},[227,1388,1389],{"class":346}," 'cookie-parser'",[227,1391,350],{"class":233},[227,1393,1394,1396,1398,1400,1402],{"class":229,"line":256},[227,1395,337],{"class":336},[227,1397,340],{"class":233},[227,1399,343],{"class":336},[227,1401,347],{"class":346},[227,1403,350],{"class":233},[227,1405,1406,1408,1411,1413,1415],{"class":229,"line":265},[227,1407,337],{"class":336},[227,1409,1410],{"class":233}," { randomBytes, createHash } ",[227,1412,343],{"class":336},[227,1414,443],{"class":346},[227,1416,350],{"class":233},[227,1418,1419],{"class":229,"line":281},[227,1420,836],{"emptyLinePlaceholder":835},[227,1422,1423,1425,1428,1430,1433],{"class":229,"line":302},[227,1424,361],{"class":336},[227,1426,1427],{"class":364}," app",[227,1429,368],{"class":336},[227,1431,1432],{"class":240}," express",[227,1434,730],{"class":233},[227,1436,1437,1440,1443,1446,1448],{"class":229,"line":621},[227,1438,1439],{"class":233},"app.",[227,1441,1442],{"class":240},"use",[227,1444,1445],{"class":233},"(express.",[227,1447,632],{"class":240},[227,1449,1450],{"class":233},"());\n",[227,1452,1453,1455,1457,1459,1462],{"class":229,"line":627},[227,1454,1439],{"class":233},[227,1456,1442],{"class":240},[227,1458,460],{"class":233},[227,1460,1461],{"class":240},"cookieParser",[227,1463,1450],{"class":233},[227,1465,1466],{"class":229,"line":1040},[227,1467,836],{"emptyLinePlaceholder":835},[227,1469,1470,1472,1475,1477,1480,1482,1485,1488,1491],{"class":229,"line":1053},[227,1471,361],{"class":336},[227,1473,1474],{"class":364}," JWT_SECRET",[227,1476,368],{"class":336},[227,1478,1479],{"class":233}," process.env.",[227,1481,390],{"class":364},[227,1483,1484],{"class":336}," ||",[227,1486,1487],{"class":346}," 'super-secure-secret-key'",[227,1489,1490],{"class":233},"; ",[227,1492,1493],{"class":355},"// keep this safe\n",[227,1495,1496,1498,1501,1503,1506],{"class":229,"line":1058},[227,1497,361],{"class":336},[227,1499,1500],{"class":364}," ACCESS_TOKEN_EXPIRY",[227,1502,368],{"class":336},[227,1504,1505],{"class":346}," '15m'",[227,1507,350],{"class":233},[227,1509,1510,1512,1515,1517,1520,1522,1524,1526,1528,1530,1532,1534],{"class":229,"line":1063},[227,1511,361],{"class":336},[227,1513,1514],{"class":364}," REFRESH_TOKEN_EXPIRY_SEC",[227,1516,368],{"class":336},[227,1518,1519],{"class":364}," 30",[227,1521,598],{"class":336},[227,1523,601],{"class":364},[227,1525,598],{"class":336},[227,1527,606],{"class":364},[227,1529,598],{"class":336},[227,1531,606],{"class":364},[227,1533,1490],{"class":233},[227,1535,1536],{"class":355},"// 30 days in seconds\n",[227,1538,1539],{"class":229,"line":1069},[227,1540,836],{"emptyLinePlaceholder":835},[227,1542,1543],{"class":229,"line":1075},[227,1544,1545],{"class":355},"// Simulated in‐memory store for refresh tokens:\n",[227,1547,1548],{"class":229,"line":1081},[227,1549,1550],{"class":355},"// key = hashedRefreshToken, value = { userId, expiresAt: unixTimestampSec }\n",[227,1552,1553,1555,1558,1560,1562,1565],{"class":229,"line":1087},[227,1554,361],{"class":336},[227,1556,1557],{"class":364}," refreshTokenStore",[227,1559,368],{"class":336},[227,1561,990],{"class":336},[227,1563,1564],{"class":240}," Map",[227,1566,730],{"class":233},[227,1568,1569],{"class":229,"line":1093},[227,1570,836],{"emptyLinePlaceholder":835},[227,1572,1573],{"class":229,"line":1098},[227,1574,1575],{"class":355},"// Utility to hash a token before storing in DB:\n",[227,1577,1579,1582,1585,1587,1591],{"class":229,"line":1578},20,[227,1580,1581],{"class":336},"function",[227,1583,1584],{"class":240}," hashToken",[227,1586,460],{"class":233},[227,1588,1590],{"class":1589},"sQHwn","token",[227,1592,1593],{"class":233},") {\n",[227,1595,1597,1600,1603,1605,1608,1610,1613,1616,1619,1621,1623],{"class":229,"line":1596},21,[227,1598,1599],{"class":336},"  return",[227,1601,1602],{"class":240}," createHash",[227,1604,460],{"class":233},[227,1606,1607],{"class":346},"'sha256'",[227,1609,319],{"class":233},[227,1611,1612],{"class":240},"update",[227,1614,1615],{"class":233},"(token).",[227,1617,1618],{"class":240},"digest",[227,1620,460],{"class":233},[227,1622,473],{"class":346},[227,1624,418],{"class":233},[227,1626,1628],{"class":229,"line":1627},22,[227,1629,305],{"class":233},[227,1631,1633],{"class":229,"line":1632},23,[227,1634,836],{"emptyLinePlaceholder":835},[227,1636,1638],{"class":229,"line":1637},24,[227,1639,1640],{"class":355},"// 1. LOGIN: Validate credentials, then issue both tokens\n",[227,1642,1644,1646,1649,1651,1654,1656,1659,1661,1664,1666,1669,1672,1675],{"class":229,"line":1643},25,[227,1645,1439],{"class":233},[227,1647,1648],{"class":240},"post",[227,1650,460],{"class":233},[227,1652,1653],{"class":346},"'/auth/login'",[227,1655,188],{"class":233},[227,1657,1658],{"class":336},"async",[227,1660,978],{"class":233},[227,1662,1663],{"class":1589},"req",[227,1665,188],{"class":233},[227,1667,1668],{"class":1589},"res",[227,1670,1671],{"class":233},") ",[227,1673,1674],{"class":336},"=>",[227,1676,689],{"class":233},[227,1678,1680,1682,1685,1688,1690,1693,1696,1698],{"class":229,"line":1679},26,[227,1681,694],{"class":336},[227,1683,1684],{"class":233}," { ",[227,1686,1687],{"class":364},"email",[227,1689,188],{"class":233},[227,1691,1692],{"class":364},"password",[227,1694,1695],{"class":233}," } ",[227,1697,719],{"class":336},[227,1699,1700],{"class":233}," req.body;\n",[227,1702,1704],{"class":229,"line":1703},27,[227,1705,1706],{"class":355},"  // (In production) verify email+password from your user database:\n",[227,1708,1710,1712,1715,1717,1719,1722],{"class":229,"line":1709},28,[227,1711,694],{"class":336},[227,1713,1714],{"class":364}," user",[227,1716,368],{"class":336},[227,1718,961],{"class":336},[227,1720,1721],{"class":240}," findUserByEmail",[227,1723,1724],{"class":233},"(email);\n",[227,1726,1728,1731,1733,1735,1738,1741,1744,1747],{"class":229,"line":1727},29,[227,1729,1730],{"class":336},"  if",[227,1732,978],{"class":233},[227,1734,981],{"class":336},[227,1736,1737],{"class":233},"user ",[227,1739,1740],{"class":336},"||",[227,1742,1743],{"class":336}," !",[227,1745,1746],{"class":240},"checkPassword",[227,1748,1749],{"class":233},"(user, password)) {\n",[227,1751,1753,1756,1758,1761,1763,1765,1767,1769,1772,1775],{"class":229,"line":1752},30,[227,1754,1755],{"class":336},"    return",[227,1757,1104],{"class":233},[227,1759,1760],{"class":240},"status",[227,1762,460],{"class":233},[227,1764,759],{"class":364},[227,1766,319],{"class":233},[227,1768,632],{"class":240},[227,1770,1771],{"class":233},"({ error: ",[227,1773,1774],{"class":346},"'Invalid credentials'",[227,1776,1777],{"class":233}," });\n",[227,1779,1781],{"class":229,"line":1780},31,[227,1782,1783],{"class":233},"  }\n",[227,1785,1787],{"class":229,"line":1786},32,[227,1788,836],{"emptyLinePlaceholder":835},[227,1790,1792],{"class":229,"line":1791},33,[227,1793,1794],{"class":355},"  // 1a. Issue access token (short‐lived)\n",[227,1796,1798,1800,1802,1804,1806,1808],{"class":229,"line":1797},34,[227,1799,694],{"class":336},[227,1801,365],{"class":364},[227,1803,368],{"class":336},[227,1805,371],{"class":233},[227,1807,374],{"class":240},[227,1809,377],{"class":233},[227,1811,1813],{"class":229,"line":1812},35,[227,1814,1815],{"class":233},"    { sub: user.id, name: user.name, roles: user.roles },\n",[227,1817,1819,1822],{"class":229,"line":1818},36,[227,1820,1821],{"class":364},"    JWT_SECRET",[227,1823,564],{"class":233},[227,1825,1827,1830,1832,1834,1837],{"class":229,"line":1826},37,[227,1828,1829],{"class":233},"    { algorithm: ",[227,1831,404],{"class":346},[227,1833,407],{"class":233},[227,1835,1836],{"class":364},"ACCESS_TOKEN_EXPIRY",[227,1838,413],{"class":233},[227,1840,1842],{"class":229,"line":1841},38,[227,1843,1844],{"class":233},"  );\n",[227,1846,1848],{"class":229,"line":1847},39,[227,1849,836],{"emptyLinePlaceholder":835},[227,1851,1853],{"class":229,"line":1852},40,[227,1854,1855],{"class":355},"  // 1b. Issue a refresh token (random string), store its hash in DB\n",[227,1857,1859,1861,1864,1866,1868,1870,1872,1874,1876,1878,1880,1882],{"class":229,"line":1858},41,[227,1860,694],{"class":336},[227,1862,1863],{"class":364}," rawRefreshToken",[227,1865,368],{"class":336},[227,1867,457],{"class":240},[227,1869,460],{"class":233},[227,1871,463],{"class":364},[227,1873,319],{"class":233},[227,1875,468],{"class":240},[227,1877,460],{"class":233},[227,1879,473],{"class":346},[227,1881,476],{"class":233},[227,1883,1884],{"class":355},"// 256-bit\n",[227,1886,1888,1890,1892,1894,1896],{"class":229,"line":1887},42,[227,1889,694],{"class":336},[227,1891,941],{"class":364},[227,1893,368],{"class":336},[227,1895,1584],{"class":240},[227,1897,1898],{"class":233},"(rawRefreshToken);\n",[227,1900,1902,1904,1907,1909,1912,1915,1918,1920,1923,1926,1928,1930,1933,1935],{"class":229,"line":1901},43,[227,1903,694],{"class":336},[227,1905,1906],{"class":364}," expiresAt",[227,1908,368],{"class":336},[227,1910,1911],{"class":233}," Math.",[227,1913,1914],{"class":240},"floor",[227,1916,1917],{"class":233},"(Date.",[227,1919,275],{"class":240},[227,1921,1922],{"class":233},"() ",[227,1924,1925],{"class":336},"/",[227,1927,615],{"class":364},[227,1929,1671],{"class":233},[227,1931,1932],{"class":336},"+",[227,1934,1514],{"class":364},[227,1936,350],{"class":233},[227,1938,1940],{"class":229,"line":1939},44,[227,1941,836],{"emptyLinePlaceholder":835},[227,1943,1945,1948,1951],{"class":229,"line":1944},45,[227,1946,1947],{"class":233},"  refreshTokenStore.",[227,1949,1950],{"class":240},"set",[227,1952,1953],{"class":233},"(hashed, { userId: user.id, expiresAt });\n",[227,1955,1957],{"class":229,"line":1956},46,[227,1958,836],{"emptyLinePlaceholder":835},[227,1960,1962],{"class":229,"line":1961},47,[227,1963,1964],{"class":355},"  // 1c. Send both to the client (access in JSON; refresh as HttpOnly cookie)\n",[227,1966,1968],{"class":229,"line":1967},48,[227,1969,1970],{"class":233},"  res\n",[227,1972,1974,1977,1979,1981,1983],{"class":229,"line":1973},49,[227,1975,1976],{"class":233},"    .",[227,1978,545],{"class":240},[227,1980,460],{"class":233},[227,1982,550],{"class":346},[227,1984,1985],{"class":233},", rawRefreshToken, {\n",[227,1987,1989,1992,1994],{"class":229,"line":1988},50,[227,1990,1991],{"class":233},"      httpOnly: ",[227,1993,561],{"class":364},[227,1995,564],{"class":233},[227,1997,1999,2002,2005,2008,2011],{"class":229,"line":1998},51,[227,2000,2001],{"class":233},"      secure: process.env.",[227,2003,2004],{"class":364},"NODE_ENV",[227,2006,2007],{"class":336}," ===",[227,2009,2010],{"class":346}," 'production'",[227,2012,564],{"class":233},[227,2014,2016,2019,2021],{"class":229,"line":2015},52,[227,2017,2018],{"class":233},"      path: ",[227,2020,585],{"class":346},[227,2022,564],{"class":233},[227,2024,2026,2029,2032,2034],{"class":229,"line":2025},53,[227,2027,2028],{"class":233},"      maxAge: ",[227,2030,2031],{"class":364},"REFRESH_TOKEN_EXPIRY_SEC",[227,2033,598],{"class":336},[227,2035,2036],{"class":364}," 1000\n",[227,2038,2040],{"class":229,"line":2039},54,[227,2041,2042],{"class":233},"    })\n",[227,2044,2046,2048,2050],{"class":229,"line":2045},55,[227,2047,1976],{"class":233},[227,2049,632],{"class":240},[227,2051,635],{"class":233},[227,2053,2055],{"class":229,"line":2054},56,[227,2056,2057],{"class":233},"});\n",[227,2059,2061],{"class":229,"line":2060},57,[227,2062,836],{"emptyLinePlaceholder":835},[227,2064,2066],{"class":229,"line":2065},58,[227,2067,2068],{"class":355},"// 2. PROTECTED ROUTE: Verify Access Token\n",[227,2070,2072,2074,2077,2079,2082,2085,2087,2089,2091,2093,2095],{"class":229,"line":2071},59,[227,2073,1439],{"class":233},[227,2075,2076],{"class":240},"get",[227,2078,460],{"class":233},[227,2080,2081],{"class":346},"'/api/protected'",[227,2083,2084],{"class":233},", (",[227,2086,1663],{"class":1589},[227,2088,188],{"class":233},[227,2090,1668],{"class":1589},[227,2092,1671],{"class":233},[227,2094,1674],{"class":336},[227,2096,689],{"class":233},[227,2098,2100,2102,2105,2107,2110,2113,2116,2118,2121],{"class":229,"line":2099},60,[227,2101,694],{"class":336},[227,2103,2104],{"class":364}," authHeader",[227,2106,368],{"class":336},[227,2108,2109],{"class":233}," req.headers[",[227,2111,2112],{"class":346},"'authorization'",[227,2114,2115],{"class":233},"] ",[227,2117,1740],{"class":336},[227,2119,2120],{"class":346}," ''",[227,2122,350],{"class":233},[227,2124,2126,2128,2131,2133,2136,2139,2141,2144,2147,2150,2153],{"class":229,"line":2125},61,[227,2127,694],{"class":336},[227,2129,2130],{"class":364}," token",[227,2132,368],{"class":336},[227,2134,2135],{"class":233}," authHeader.",[227,2137,2138],{"class":240},"split",[227,2140,460],{"class":233},[227,2142,2143],{"class":346},"' '",[227,2145,2146],{"class":233},")[",[227,2148,2149],{"class":364},"1",[227,2151,2152],{"class":233},"]; ",[227,2154,2155],{"class":355},"// “Bearer \u003Ctoken>”\n",[227,2157,2159,2161,2163,2165,2168,2170,2172,2174,2176,2178],{"class":229,"line":2158},62,[227,2160,1730],{"class":336},[227,2162,978],{"class":233},[227,2164,981],{"class":336},[227,2166,2167],{"class":233},"token) ",[227,2169,1101],{"class":336},[227,2171,1104],{"class":233},[227,2173,754],{"class":240},[227,2175,460],{"class":233},[227,2177,759],{"class":364},[227,2179,418],{"class":233},[227,2181,2183],{"class":229,"line":2182},63,[227,2184,836],{"emptyLinePlaceholder":835},[227,2186,2188,2191],{"class":229,"line":2187},64,[227,2189,2190],{"class":336},"  try",[227,2192,689],{"class":233},[227,2194,2196,2199,2201,2203,2205,2207,2210,2212],{"class":229,"line":2195},65,[227,2197,2198],{"class":336},"    const",[227,2200,697],{"class":364},[227,2202,368],{"class":336},[227,2204,371],{"class":233},[227,2206,704],{"class":240},[227,2208,2209],{"class":233},"(token, ",[227,2211,390],{"class":364},[227,2213,418],{"class":233},[227,2215,2217],{"class":229,"line":2216},66,[227,2218,2219],{"class":355},"    // payload = { sub: user.id, name: user.name, roles: [ … ], iat: \u003Cn>, exp: \u003Cn> }\n",[227,2221,2223,2226,2228],{"class":229,"line":2222},67,[227,2224,2225],{"class":233},"    req.user ",[227,2227,719],{"class":336},[227,2229,722],{"class":233},[227,2231,2233,2235,2237,2239,2242,2245],{"class":229,"line":2232},68,[227,2234,1755],{"class":336},[227,2236,1104],{"class":233},[227,2238,632],{"class":240},[227,2240,2241],{"class":233},"({ data: ",[227,2243,2244],{"class":346},"'This is protected data'",[227,2246,2247],{"class":233},", user: req.user });\n",[227,2249,2251,2254,2256],{"class":229,"line":2250},69,[227,2252,2253],{"class":233},"  } ",[227,2255,738],{"class":336},[227,2257,741],{"class":233},[227,2259,2261,2263,2265,2267,2269,2271],{"class":229,"line":2260},70,[227,2262,1755],{"class":336},[227,2264,1104],{"class":233},[227,2266,754],{"class":240},[227,2268,460],{"class":233},[227,2270,759],{"class":364},[227,2272,418],{"class":233},[227,2274,2276],{"class":229,"line":2275},71,[227,2277,1783],{"class":233},[227,2279,2281],{"class":229,"line":2280},72,[227,2282,2057],{"class":233},[227,2284,2286],{"class":229,"line":2285},73,[227,2287,836],{"emptyLinePlaceholder":835},[227,2289,2291],{"class":229,"line":2290},74,[227,2292,2293],{"class":355},"// 3. REFRESH: Validate refresh token, rotate on use, issue new access token\n",[227,2295,2297,2299,2301,2303,2305,2307,2309,2311,2313,2315,2317],{"class":229,"line":2296},75,[227,2298,1439],{"class":233},[227,2300,1648],{"class":240},[227,2302,460],{"class":233},[227,2304,585],{"class":346},[227,2306,2084],{"class":233},[227,2308,1663],{"class":1589},[227,2310,188],{"class":233},[227,2312,1668],{"class":1589},[227,2314,1671],{"class":233},[227,2316,1674],{"class":336},[227,2318,689],{"class":233},[227,2320,2322,2324,2326,2328],{"class":229,"line":2321},76,[227,2323,694],{"class":336},[227,2325,1863],{"class":364},[227,2327,368],{"class":336},[227,2329,918],{"class":233},[227,2331,2333,2335,2337,2339],{"class":229,"line":2332},77,[227,2334,1730],{"class":336},[227,2336,978],{"class":233},[227,2338,981],{"class":336},[227,2340,2341],{"class":233},"rawRefreshToken) {\n",[227,2343,2345,2347,2349,2351,2353,2355],{"class":229,"line":2344},78,[227,2346,1755],{"class":336},[227,2348,1104],{"class":233},[227,2350,754],{"class":240},[227,2352,460],{"class":233},[227,2354,759],{"class":364},[227,2356,418],{"class":233},[227,2358,2360],{"class":229,"line":2359},79,[227,2361,1783],{"class":233},[227,2363,2365],{"class":229,"line":2364},80,[227,2366,836],{"emptyLinePlaceholder":835},[227,2368,2370,2372,2374,2376,2378],{"class":229,"line":2369},81,[227,2371,694],{"class":336},[227,2373,941],{"class":364},[227,2375,368],{"class":336},[227,2377,1584],{"class":240},[227,2379,1898],{"class":233},[227,2381,2383,2385,2387,2389,2392,2394],{"class":229,"line":2382},82,[227,2384,694],{"class":336},[227,2386,956],{"class":364},[227,2388,368],{"class":336},[227,2390,2391],{"class":233}," refreshTokenStore.",[227,2393,2076],{"class":240},[227,2395,970],{"class":233},[227,2397,2399],{"class":229,"line":2398},83,[227,2400,836],{"emptyLinePlaceholder":835},[227,2402,2404,2406,2408,2410],{"class":229,"line":2403},84,[227,2405,1730],{"class":336},[227,2407,978],{"class":233},[227,2409,981],{"class":336},[227,2411,2412],{"class":233},"record) {\n",[227,2414,2416,2418,2420,2422,2424,2426,2428],{"class":229,"line":2415},85,[227,2417,1755],{"class":336},[227,2419,1104],{"class":233},[227,2421,754],{"class":240},[227,2423,460],{"class":233},[227,2425,759],{"class":364},[227,2427,476],{"class":233},[227,2429,2430],{"class":355},"// no such token → unauthorized\n",[227,2432,2434],{"class":229,"line":2433},86,[227,2435,1783],{"class":233},[227,2437,2439],{"class":229,"line":2438},87,[227,2440,836],{"emptyLinePlaceholder":835},[227,2442,2444,2446,2449,2451,2453,2455,2457,2459,2461,2463,2465],{"class":229,"line":2443},88,[227,2445,694],{"class":336},[227,2447,2448],{"class":364}," nowSec",[227,2450,368],{"class":336},[227,2452,1911],{"class":233},[227,2454,1914],{"class":240},[227,2456,1917],{"class":233},[227,2458,275],{"class":240},[227,2460,1922],{"class":233},[227,2462,1925],{"class":336},[227,2464,615],{"class":364},[227,2466,418],{"class":233},[227,2468,2470,2472,2475,2478],{"class":229,"line":2469},89,[227,2471,1730],{"class":336},[227,2473,2474],{"class":233}," (record.expiresAt ",[227,2476,2477],{"class":336},"\u003C",[227,2479,2480],{"class":233}," nowSec) {\n",[227,2482,2484],{"class":229,"line":2483},90,[227,2485,2486],{"class":355},"    // expired refresh token\n",[227,2488,2490,2493,2496],{"class":229,"line":2489},91,[227,2491,2492],{"class":233},"    refreshTokenStore.",[227,2494,2495],{"class":240},"delete",[227,2497,970],{"class":233},[227,2499,2501,2503,2505,2507,2509,2511],{"class":229,"line":2500},92,[227,2502,1755],{"class":336},[227,2504,1104],{"class":233},[227,2506,754],{"class":240},[227,2508,460],{"class":233},[227,2510,759],{"class":364},[227,2512,418],{"class":233},[227,2514,2516],{"class":229,"line":2515},93,[227,2517,1783],{"class":233},[227,2519,2521],{"class":229,"line":2520},94,[227,2522,836],{"emptyLinePlaceholder":835},[227,2524,2526],{"class":229,"line":2525},95,[227,2527,2528],{"class":355},"  // 3a. Rotate the refresh token:\n",[227,2530,2532,2534,2536,2539],{"class":229,"line":2531},96,[227,2533,1947],{"class":233},[227,2535,2495],{"class":240},[227,2537,2538],{"class":233},"(hashed); ",[227,2540,2541],{"class":355},"// revoke old\n",[227,2543,2545,2547,2550,2552,2554,2556,2558,2560,2562,2564,2566],{"class":229,"line":2544},97,[227,2546,694],{"class":336},[227,2548,2549],{"class":364}," newRawRefresh",[227,2551,368],{"class":336},[227,2553,457],{"class":240},[227,2555,460],{"class":233},[227,2557,463],{"class":364},[227,2559,319],{"class":233},[227,2561,468],{"class":240},[227,2563,460],{"class":233},[227,2565,473],{"class":346},[227,2567,418],{"class":233},[227,2569,2571,2573,2576,2578,2580],{"class":229,"line":2570},98,[227,2572,694],{"class":336},[227,2574,2575],{"class":364}," newHashed",[227,2577,368],{"class":336},[227,2579,1584],{"class":240},[227,2581,2582],{"class":233},"(newRawRefresh);\n",[227,2584,2586,2588,2591,2593,2596,2598,2600],{"class":229,"line":2585},99,[227,2587,694],{"class":336},[227,2589,2590],{"class":364}," newExpiresAt",[227,2592,368],{"class":336},[227,2594,2595],{"class":233}," nowSec ",[227,2597,1932],{"class":336},[227,2599,1514],{"class":364},[227,2601,350],{"class":233},[227,2603,2605,2607,2609],{"class":229,"line":2604},100,[227,2606,1947],{"class":233},[227,2608,1950],{"class":240},[227,2610,2611],{"class":233},"(newHashed, { userId: record.userId, expiresAt: newExpiresAt });\n",[227,2613,2615],{"class":229,"line":2614},101,[227,2616,836],{"emptyLinePlaceholder":835},[227,2618,2620],{"class":229,"line":2619},102,[227,2621,2622],{"class":355},"  // 3b. Issue a new access token:\n",[227,2624,2626,2628,2630,2632,2634,2636],{"class":229,"line":2625},103,[227,2627,694],{"class":336},[227,2629,1016],{"class":364},[227,2631,368],{"class":336},[227,2633,371],{"class":233},[227,2635,374],{"class":240},[227,2637,377],{"class":233},[227,2639,2641,2644,2647],{"class":229,"line":2640},104,[227,2642,2643],{"class":233},"    { sub: record.userId ",[227,2645,2646],{"class":355},"/*, other claims… */",[227,2648,2649],{"class":233}," },\n",[227,2651,2653,2655],{"class":229,"line":2652},105,[227,2654,1821],{"class":364},[227,2656,564],{"class":233},[227,2658,2660,2662,2664,2666,2668],{"class":229,"line":2659},106,[227,2661,1829],{"class":233},[227,2663,404],{"class":346},[227,2665,407],{"class":233},[227,2667,1836],{"class":364},[227,2669,413],{"class":233},[227,2671,2673],{"class":229,"line":2672},107,[227,2674,1844],{"class":233},[227,2676,2678],{"class":229,"line":2677},108,[227,2679,836],{"emptyLinePlaceholder":835},[227,2681,2683],{"class":229,"line":2682},109,[227,2684,2685],{"class":355},"  // 3c. Set new refresh token cookie, send new access token in JSON\n",[227,2687,2689],{"class":229,"line":2688},110,[227,2690,1970],{"class":233},[227,2692,2694,2696,2698,2700,2702],{"class":229,"line":2693},111,[227,2695,1976],{"class":233},[227,2697,545],{"class":240},[227,2699,460],{"class":233},[227,2701,550],{"class":346},[227,2703,2704],{"class":233},", newRawRefresh, {\n",[227,2706,2708,2710,2712],{"class":229,"line":2707},112,[227,2709,1991],{"class":233},[227,2711,561],{"class":364},[227,2713,564],{"class":233},[227,2715,2717,2719,2721,2723,2725],{"class":229,"line":2716},113,[227,2718,2001],{"class":233},[227,2720,2004],{"class":364},[227,2722,2007],{"class":336},[227,2724,2010],{"class":346},[227,2726,564],{"class":233},[227,2728,2730,2732,2734],{"class":229,"line":2729},114,[227,2731,2018],{"class":233},[227,2733,585],{"class":346},[227,2735,564],{"class":233},[227,2737,2739,2741,2743,2745],{"class":229,"line":2738},115,[227,2740,2028],{"class":233},[227,2742,2031],{"class":364},[227,2744,598],{"class":336},[227,2746,2036],{"class":364},[227,2748,2750],{"class":229,"line":2749},116,[227,2751,2042],{"class":233},[227,2753,2755,2757,2759],{"class":229,"line":2754},117,[227,2756,1976],{"class":233},[227,2758,632],{"class":240},[227,2760,1109],{"class":233},[227,2762,2764],{"class":229,"line":2763},118,[227,2765,2057],{"class":233},[227,2767,2769],{"class":229,"line":2768},119,[227,2770,836],{"emptyLinePlaceholder":835},[227,2772,2774],{"class":229,"line":2773},120,[227,2775,2776],{"class":355},"// 4. LOGOUT: Invalidate current refresh token\n",[227,2778,2780,2782,2784,2786,2789,2791,2793,2795,2797,2799,2801],{"class":229,"line":2779},121,[227,2781,1439],{"class":233},[227,2783,1648],{"class":240},[227,2785,460],{"class":233},[227,2787,2788],{"class":346},"'/auth/logout'",[227,2790,2084],{"class":233},[227,2792,1663],{"class":1589},[227,2794,188],{"class":233},[227,2796,1668],{"class":1589},[227,2798,1671],{"class":233},[227,2800,1674],{"class":336},[227,2802,689],{"class":233},[227,2804,2806,2808,2810,2812],{"class":229,"line":2805},122,[227,2807,694],{"class":336},[227,2809,1863],{"class":364},[227,2811,368],{"class":336},[227,2813,918],{"class":233},[227,2815,2817,2819],{"class":229,"line":2816},123,[227,2818,1730],{"class":336},[227,2820,2821],{"class":233}," (rawRefreshToken) {\n",[227,2823,2825,2827,2829,2831,2833],{"class":229,"line":2824},124,[227,2826,2198],{"class":336},[227,2828,941],{"class":364},[227,2830,368],{"class":336},[227,2832,1584],{"class":240},[227,2834,1898],{"class":233},[227,2836,2838,2840,2842],{"class":229,"line":2837},125,[227,2839,2492],{"class":233},[227,2841,2495],{"class":240},[227,2843,970],{"class":233},[227,2845,2847],{"class":229,"line":2846},126,[227,2848,1783],{"class":233},[227,2850,2852],{"class":229,"line":2851},127,[227,2853,2854],{"class":355},"  // Clear cookie on client\n",[227,2856,2858],{"class":229,"line":2857},128,[227,2859,1970],{"class":233},[227,2861,2863,2865,2868,2870,2872,2875,2877],{"class":229,"line":2862},129,[227,2864,1976],{"class":233},[227,2866,2867],{"class":240},"clearCookie",[227,2869,460],{"class":233},[227,2871,550],{"class":346},[227,2873,2874],{"class":233},", { path: ",[227,2876,585],{"class":346},[227,2878,2879],{"class":233}," })\n",[227,2881,2883,2885,2887,2889,2892],{"class":229,"line":2882},130,[227,2884,1976],{"class":233},[227,2886,754],{"class":240},[227,2888,460],{"class":233},[227,2890,2891],{"class":364},"204",[227,2893,418],{"class":233},[227,2895,2897],{"class":229,"line":2896},131,[227,2898,2057],{"class":233},[227,2900,2902],{"class":229,"line":2901},132,[227,2903,836],{"emptyLinePlaceholder":835},[227,2905,2907,2909,2912,2914,2917,2920,2922],{"class":229,"line":2906},133,[227,2908,1439],{"class":233},[227,2910,2911],{"class":240},"listen",[227,2913,460],{"class":233},[227,2915,2916],{"class":364},"4000",[227,2918,2919],{"class":233},", () ",[227,2921,1674],{"class":336},[227,2923,689],{"class":233},[227,2925,2927,2930,2933,2935,2938],{"class":229,"line":2926},134,[227,2928,2929],{"class":233},"  console.",[227,2931,2932],{"class":240},"log",[227,2934,460],{"class":233},[227,2936,2937],{"class":346},"'Auth server listening on port 4000'",[227,2939,418],{"class":233},[227,2941,2943],{"class":229,"line":2942},135,[227,2944,2057],{"class":233},[227,2946,2948],{"class":229,"line":2947},136,[227,2949,836],{"emptyLinePlaceholder":835},[227,2951,2953],{"class":229,"line":2952},137,[227,2954,2955],{"class":355},"/**\n",[227,2957,2959],{"class":229,"line":2958},138,[227,2960,2961],{"class":355}," * Dummy user‐lookup/password‐check functions for illustration only:\n",[227,2963,2965],{"class":229,"line":2964},139,[227,2966,2967],{"class":355}," */\n",[227,2969,2971,2973,2976,2978,2980,2982],{"class":229,"line":2970},140,[227,2972,1658],{"class":336},[227,2974,2975],{"class":336}," function",[227,2977,1721],{"class":240},[227,2979,460],{"class":233},[227,2981,1687],{"class":1589},[227,2983,1593],{"class":233},[227,2985,2987],{"class":229,"line":2986},141,[227,2988,2989],{"class":355},"  // In reality, query your users table\n",[227,2991,2993,2995,2998,3001,3004],{"class":229,"line":2992},142,[227,2994,1730],{"class":336},[227,2996,2997],{"class":233}," (email ",[227,2999,3000],{"class":336},"===",[227,3002,3003],{"class":346}," 'alice@example.com'",[227,3005,1593],{"class":233},[227,3007,3009,3011,3014,3017,3020,3023,3026,3029,3032,3035],{"class":229,"line":3008},143,[227,3010,1755],{"class":336},[227,3012,3013],{"class":233}," { id: ",[227,3015,3016],{"class":346},"'user123'",[227,3018,3019],{"class":233},", name: ",[227,3021,3022],{"class":346},"'Alice'",[227,3024,3025],{"class":233},", roles: [",[227,3027,3028],{"class":346},"'user'",[227,3030,3031],{"class":233},"], passwordHash: ",[227,3033,3034],{"class":346},"'…'",[227,3036,3037],{"class":233}," };\n",[227,3039,3041],{"class":229,"line":3040},144,[227,3042,1783],{"class":233},[227,3044,3046,3048,3051],{"class":229,"line":3045},145,[227,3047,1599],{"class":336},[227,3049,3050],{"class":364}," null",[227,3052,350],{"class":233},[227,3054,3056],{"class":229,"line":3055},146,[227,3057,305],{"class":233},[227,3059,3061,3063,3066,3068,3071,3073,3075],{"class":229,"line":3060},147,[227,3062,1581],{"class":336},[227,3064,3065],{"class":240}," checkPassword",[227,3067,460],{"class":233},[227,3069,3070],{"class":1589},"user",[227,3072,188],{"class":233},[227,3074,1692],{"class":1589},[227,3076,1593],{"class":233},[227,3078,3080],{"class":229,"line":3079},148,[227,3081,3082],{"class":355},"  // In reality, bcrypt.compare(password, user.passwordHash)\n",[227,3084,3086,3088,3091,3093,3096],{"class":229,"line":3085},149,[227,3087,1599],{"class":336},[227,3089,3090],{"class":233}," password ",[227,3092,3000],{"class":336},[227,3094,3095],{"class":346}," 'correct‐horse‐battery‐staple'",[227,3097,350],{"class":233},[227,3099,3101],{"class":229,"line":3100},150,[227,3102,305],{"class":233},[227,3104,3106],{"class":229,"line":3105},151,[227,3107,836],{"emptyLinePlaceholder":835},[227,3109,3111,3114,3117],{"class":229,"line":3110},152,[227,3112,3113],{"class":233},"### ",[227,3115,3116],{"class":364},"8.",[227,3118,3119],{"class":233}," Key Takeaways\n",[227,3121,3123],{"class":229,"line":3122},153,[227,3124,836],{"emptyLinePlaceholder":835},[227,3126,3128,3131,3134,3137],{"class":229,"line":3127},154,[227,3129,3130],{"class":336},"-",[227,3132,3133],{"class":233}," Never confuse the refresh token ",[227,3135,3136],{"class":336},"with",[227,3138,3139],{"class":233}," your signing key.\n",[227,3141,3143,3145,3148,3151,3153,3156,3159,3162,3164,3167,3170,3172],{"class":229,"line":3142},155,[227,3144,3130],{"class":336},[227,3146,3147],{"class":233}," The signing ",[227,3149,3150],{"class":240},"key",[227,3152,978],{"class":233},[227,3154,3155],{"class":364},"HMAC",[227,3157,3158],{"class":233}," secret or ",[227,3160,3161],{"class":364},"RSA",[227,3163,1925],{"class":336},[227,3165,3166],{"class":364},"EC",[227,3168,3169],{"class":233}," private key) stays on your server, never goes to a client, and is used purely to ",[227,3171,374],{"class":240},[227,3173,3174],{"class":233}," (and verify) JWTs.\n",[227,3176,3178,3180,3183,3186,3189,3192,3195,3198],{"class":229,"line":3177},156,[227,3179,3130],{"class":336},[227,3181,3182],{"class":364}," A",[227,3184,3185],{"class":233}," refresh token is just a long‐lived random credential that your server stores and checks; it has nothing to ",[227,3187,3188],{"class":336},"do",[227,3190,3191],{"class":336}," with",[227,3193,3194],{"class":233}," cryptographic signing ",[227,3196,3197],{"class":336},"of",[227,3199,3200],{"class":233}," access tokens.\n",[227,3202,3204,3206,3209,3211,3214,3216,3219,3222],{"class":229,"line":3203},157,[227,3205,3130],{"class":336},[227,3207,3208],{"class":233}," Access tokens are publicly visible to the client, but they are only valid because your server signed them ",[227,3210,3136],{"class":336},[227,3212,3213],{"class":233}," its private key",[227,3215,1925],{"class":336},[227,3217,3218],{"class":233},"secret. The client does not hold any secret—only the signed ",[227,3220,3221],{"class":364},"JWT",[227,3223,3224],{"class":233}," itself.\n",[227,3226,3228,3230,3233,3236,3239,3242,3245,3248,3251,3253,3256,3259,3262,3265,3267,3270,3272,3275,3277,3280,3283,3286,3289],{"class":229,"line":3227},158,[227,3229,3130],{"class":336},[227,3231,3232],{"class":233}," Refresh tokens are also “bearer” tokens, but they are never meant to be parsed or “verified” by the client. They simply live ",[227,3234,3235],{"class":336},"in",[227,3237,3238],{"class":233}," the client’s HttpOnly cookie, and your server treats them ",[227,3240,3241],{"class":336},"as",[227,3243,3244],{"class":240}," a",[227,3246,3247],{"class":240}," key",[227,3249,3250],{"class":240}," in",[227,3252,3244],{"class":240},[227,3254,3255],{"class":240}," server",[227,3257,3258],{"class":233},"‐",[227,3260,3261],{"class":240},"side",[227,3263,3264],{"class":240}," store",[227,3266,978],{"class":233},[227,3268,3269],{"class":240},"database",[227,3271,1925],{"class":233},[227,3273,3274],{"class":240},"Redis",[227,3276,1671],{"class":233},[227,3278,3279],{"class":240},"that",[227,3281,3282],{"class":240}," says",[227,3284,3285],{"class":233},", “Yes, that client is allowed to get a ",[227,3287,3288],{"class":336},"new",[227,3290,3291],{"class":233}," access token now.”\n",[227,3293,3295,3297,3300,3303,3306,3308,3310,3312,3315,3318,3321,3323,3326,3329],{"class":229,"line":3294},159,[227,3296,3130],{"class":336},[227,3298,3299],{"class":233}," If you need asymmetric ",[227,3301,3302],{"class":240},"validation",[227,3304,3305],{"class":233}," (e.g., microservices), use an ",[227,3307,3161],{"class":364},[227,3309,1925],{"class":336},[227,3311,3166],{"class":364},[227,3313,3314],{"class":233}," key ",[227,3316,3317],{"class":240},"pair",[227,3319,3320],{"class":233},": sign ",[227,3322,3136],{"class":336},[227,3324,3325],{"class":233}," the private key on Auth Server; distribute only the public key to the resource servers to ",[227,3327,3328],{"class":336},"let",[227,3330,3331],{"class":233}," them verify incoming JWTs. The refresh token stays opaque and is validated only by the Auth Server.\n",[227,3333,3335,3337,3340,3342,3345,3347],{"class":229,"line":3334},160,[227,3336,3130],{"class":336},[227,3338,3339],{"class":233}," Do not send any “private key” to the browser. You ",[227,3341,3188],{"class":336},[227,3343,3344],{"class":233}," not want client ",[227,3346,57],{"class":240},[227,3348,3349],{"class":233}," (or malicious third‐parties) to ever have the ability to sign their own tokens.\n",[227,3351,3353],{"class":229,"line":3352},161,[227,3354,836],{"emptyLinePlaceholder":835},[227,3356,3358,3361,3363],{"class":229,"line":3357},162,[227,3359,3360],{"class":233},"By following the above pattern, you ensure ",[227,3362,3279],{"class":240},[227,3364,3365],{"class":233},":\n",[227,3367,3369,3371],{"class":229,"line":3368},163,[227,3370,3130],{"class":336},[227,3372,3373],{"class":233}," No sensitive signing material ever leaves your backend.\n",[227,3375,3377,3379,3382,3384],{"class":229,"line":3376},164,[227,3378,3130],{"class":336},[227,3380,3381],{"class":233}," Clients only hold a short‐lived access token and an opaque refresh token ",[227,3383,3235],{"class":336},[227,3385,3386],{"class":233}," an HttpOnly cookie.\n",[227,3388,3390,3392,3395,3397],{"class":229,"line":3389},165,[227,3391,3130],{"class":336},[227,3393,3394],{"class":233}," Your refresh token never “becomes” a private key—it simply lets you decide when to issue a brand‐",[227,3396,3288],{"class":336},[227,3398,3399],{"class":233}," signed access token.\n",[227,3401,3403],{"class":229,"line":3402},166,[227,3404,836],{"emptyLinePlaceholder":835},[227,3406,3408,3411,3414,3417,3420],{"class":229,"line":3407},167,[227,3409,3410],{"class":336},"**",[227,3412,3413],{"class":364},"TL",[227,3415,3416],{"class":233},";",[227,3418,3419],{"class":364},"DR",[227,3421,3422],{"class":336},"**\n",[227,3424,3426],{"class":229,"line":3425},168,[227,3427,836],{"emptyLinePlaceholder":835},[227,3429,3431,3434,3436,3439,3441,3443,3445,3447,3449,3451,3453,3456,3458,3461,3463,3466,3469,3472,3474,3477,3480,3482,3485,3488,3490,3493,3495,3497,3500,3503,3506,3509,3512,3515,3518,3521,3524,3527,3530,3533],{"class":229,"line":3430},169,[227,3432,3433],{"class":233},"In a proper ",[227,3435,3221],{"class":364},[227,3437,3438],{"class":233}," flow, you must keep a single signing ",[227,3440,3150],{"class":240},[227,3442,978],{"class":233},[227,3444,3155],{"class":364},[227,3446,3158],{"class":233},[227,3448,3161],{"class":364},[227,3450,1925],{"class":336},[227,3452,3166],{"class":364},[227,3454,3455],{"class":233}," private key) strictly on the server. You sign access tokens ",[227,3457,3136],{"class":336},[227,3459,3460],{"class":233}," that key and verify them ",[227,3462,3136],{"class":336},[227,3464,3465],{"class":233}," its ",[227,3467,3468],{"class":240},"counterpart",[227,3470,3471],{"class":233}," (the same secret ",[227,3473,3235],{"class":336},[227,3475,3476],{"class":364}," HS256",[227,3478,3479],{"class":233}," or the public key ",[227,3481,3235],{"class":336},[227,3483,3484],{"class":364}," RS256",[227,3486,3487],{"class":233},"). The refresh token is merely a long‐lived credential you store server‐side and never share except ",[227,3489,3235],{"class":336},[227,3491,3492],{"class":233}," an HttpOnly cookie. It should never act ",[227,3494,3241],{"class":336},[227,3496,3244],{"class":240},[227,3498,3499],{"class":346}," \"private key\"",[227,3501,3502],{"class":240}," and",[227,3504,3505],{"class":240}," is",[227,3507,3508],{"class":240}," not",[227,3510,3511],{"class":240}," used",[227,3513,3514],{"class":240}," to",[227,3516,3517],{"class":240}," cryptographically",[227,3519,3520],{"class":240}," sign",[227,3522,3523],{"class":240}," or",[227,3525,3526],{"class":240}," verify",[227,3528,3529],{"class":240}," access",[227,3531,3532],{"class":240}," tokens",[227,3534,3535],{"class":233},".\n",[227,3537,3539],{"class":229,"line":3538},170,[227,3540,836],{"emptyLinePlaceholder":835},[227,3542,3544],{"class":229,"line":3543},171,[227,3545,3546],{"class":336},"---\n",[227,3548,3550],{"class":229,"line":3549},172,[227,3551,836],{"emptyLinePlaceholder":835},[227,3553,3555,3558,3561,3564,3566],{"class":229,"line":3554},173,[227,3556,3557],{"class":233},"## Part ",[227,3559,3560],{"class":364},"2",[227,3562,3563],{"class":233},": Stateless Refresh",[227,3565,3130],{"class":336},[227,3567,3568],{"class":233},"Token Flow\n",[227,3570,3572],{"class":229,"line":3571},174,[227,3573,836],{"emptyLinePlaceholder":835},[227,3575,3577,3580,3582,3585,3587,3590,3593,3595,3597,3600,3602],{"class":229,"line":3576},175,[227,3578,3579],{"class":233},"This section explains how and why you might build a truly stateless refresh‐token flow—along ",[227,3581,3136],{"class":336},[227,3583,3584],{"class":233}," the inherent trade‐offs. In short, avoiding server‐side state means giving up per‐token revocation and forced expiry. However, ",[227,3586,975],{"class":336},[227,3588,3589],{"class":233}," those limitations are acceptable, a self‐",[227,3591,3592],{"class":240},"contained",[227,3594,978],{"class":233},[227,3596,3221],{"class":364},[227,3598,3599],{"class":233},") refresh token can live entirely ",[227,3601,3235],{"class":336},[227,3603,3604],{"class":233}," the client and be validated solely via signature and expiration time.\n",[227,3606,3608],{"class":229,"line":3607},176,[227,3609,836],{"emptyLinePlaceholder":835},[227,3611,3613,3615,3618],{"class":229,"line":3612},177,[227,3614,3113],{"class":233},[227,3616,3617],{"class":364},"1.",[227,3619,3620],{"class":233}," What “Stateless Refresh Token” Actually Means\n",[227,3622,3624],{"class":229,"line":3623},178,[227,3625,836],{"emptyLinePlaceholder":835},[227,3627,3629,3632,3635],{"class":229,"line":3628},179,[227,3630,3631],{"class":233},"#### ",[227,3633,3634],{"class":240},"Stateless",[227,3636,3637],{"class":233}," (no server‐side store at all)\n",[227,3639,3641],{"class":229,"line":3640},180,[227,3642,836],{"emptyLinePlaceholder":835},[227,3644,3646,3649,3652,3654],{"class":229,"line":3645},181,[227,3647,3648],{"class":336},"*",[227,3650,3651],{"class":233}," The server keeps zero records ",[227,3653,3197],{"class":336},[227,3655,3656],{"class":233}," which refresh tokens were issued.\n",[227,3658,3660,3662,3665,3667,3670,3673,3676,3679,3682,3685],{"class":229,"line":3659},182,[227,3661,3648],{"class":336},[227,3663,3664],{"class":233}," Each refresh token must be fully “self‐contained”: it carries ",[227,3666,3235],{"class":336},[227,3668,3669],{"class":233}," its own payload everything the server needs to decide “",[227,3671,3672],{"class":364},"OK",[227,3674,3675],{"class":233},", is ",[227,3677,3678],{"class":364},"this",[227,3680,3681],{"class":233}," valid",[227,3683,3684],{"class":336},"?",[227,3686,3687],{"class":233},"”\n",[227,3689,3691],{"class":229,"line":3690},183,[227,3692,836],{"emptyLinePlaceholder":835},[227,3694,3696,3699,3701,3704,3707,3710],{"class":229,"line":3695},184,[227,3697,3698],{"class":233},"In practice, that means the refresh token itself is a signed ",[227,3700,3221],{"class":240},[227,3702,3703],{"class":233}," (or ",[227,3705,3706],{"class":364},"JWS",[227,3708,3709],{"class":233},") that",[227,3711,3365],{"class":336},[227,3713,3715],{"class":229,"line":3714},185,[227,3716,836],{"emptyLinePlaceholder":835},[227,3718,3720,3722,3725,3727,3730,3733,3736,3739,3742,3744],{"class":229,"line":3719},186,[227,3721,3648],{"class":336},[227,3723,3724],{"class":233}," Encodes a unique user",[227,3726,1925],{"class":336},[227,3728,3729],{"class":233},"session ",[227,3731,3732],{"class":240},"identifier",[227,3734,3735],{"class":233}," (e.g. a user ",[227,3737,3738],{"class":364},"ID",[227,3740,3741],{"class":233}," or “session ",[227,3743,3738],{"class":364},[227,3745,3746],{"class":233},"”).\n",[227,3748,3750,3752,3755,3758],{"class":229,"line":3749},187,[227,3751,3648],{"class":336},[227,3753,3754],{"class":233}," Carries an ",[227,3756,3757],{"class":346},"`exp`",[227,3759,3760],{"class":233}," (expiration) claim.\n",[227,3762,3764,3766,3769,3772],{"class":229,"line":3763},188,[227,3765,3648],{"class":336},[227,3767,3768],{"class":233}," (Optionally) carries any other claims you ",[227,3770,3771],{"class":240},"need",[227,3773,3774],{"class":233}," (e.g. user roles, token version, etc.).\n",[227,3776,3778],{"class":229,"line":3777},189,[227,3779,836],{"emptyLinePlaceholder":835},[227,3781,3783,3786,3788,3791,3794],{"class":229,"line":3782},190,[227,3784,3785],{"class":233},"When the client presents that refresh‐",[227,3787,3221],{"class":364},[227,3789,3790],{"class":233},", the server ",[227,3792,3793],{"class":240},"just",[227,3795,3365],{"class":233},[227,3797,3799],{"class":229,"line":3798},191,[227,3800,836],{"emptyLinePlaceholder":835},[227,3802,3804,3806],{"class":229,"line":3803},192,[227,3805,3617],{"class":364},[227,3807,3808],{"class":233}," Verifies its signature against a server‐only signing key.\n",[227,3810,3812,3815,3818,3820,3823,3826],{"class":229,"line":3811},193,[227,3813,3814],{"class":364},"2.",[227,3816,3817],{"class":233}," Checks ",[227,3819,3757],{"class":346},[227,3821,3822],{"class":233}," (and maybe ",[227,3824,3825],{"class":346},"`nbf`",[227,3827,3828],{"class":233},").\n",[227,3830,3832,3835,3838,3840,3843,3845,3848,3850],{"class":229,"line":3831},194,[227,3833,3834],{"class":364},"3.",[227,3836,3837],{"class":233}," If valid, issues a ",[227,3839,3288],{"class":336},[227,3841,3842],{"class":233}," access ",[227,3844,1590],{"class":240},[227,3846,3847],{"class":233}," (and optionally a ",[227,3849,3288],{"class":336},[227,3851,3852],{"class":233}," refresh token).\n",[227,3854,3856],{"class":229,"line":3855},195,[227,3857,836],{"emptyLinePlaceholder":835},[227,3859,3861,3864,3866,3869,3871],{"class":229,"line":3860},196,[227,3862,3863],{"class":233},"No database check—no “lookup” is done. Everything is ",[227,3865,3235],{"class":336},[227,3867,3868],{"class":233}," the ",[227,3870,3221],{"class":364},[227,3872,3535],{"class":233},[227,3874,3876],{"class":229,"line":3875},197,[227,3877,836],{"emptyLinePlaceholder":835},[227,3879,3881,3884,3887,3890,3893,3896,3899],{"class":229,"line":3880},198,[227,3882,3883],{"class":233},"Because there’s no ",[227,3885,3886],{"class":240},"lookup",[227,3888,3889],{"class":233}," (no hash‐table or ",[227,3891,3892],{"class":364},"DB",[227,3894,3895],{"class":233},"), the server ",[227,3897,3898],{"class":240},"cannot",[227,3900,3365],{"class":233},[227,3902,3904],{"class":229,"line":3903},199,[227,3905,836],{"emptyLinePlaceholder":835},[227,3907,3909,3911,3914,3916,3919,3921],{"class":229,"line":3908},200,[227,3910,3648],{"class":336},[227,3912,3913],{"class":233}," Immediately revoke a single user’s existing refresh ",[227,3915,1590],{"class":240},[227,3917,3918],{"class":233}," (unless you switch to a ",[227,3920,3288],{"class":336},[227,3922,3923],{"class":233}," signing key or embed some revocation‐list version, which reintroduces state).\n",[227,3925,3927,3929,3932,3934,3937],{"class":229,"line":3926},201,[227,3928,3648],{"class":336},[227,3930,3931],{"class":233}," Force‐expire one client’s refresh token early; once it’s signed and unexpired, it’s valid until its ",[227,3933,3221],{"class":364},[227,3935,3936],{"class":346}," `exp`",[227,3938,3535],{"class":233},[227,3940,3942,3944,3947,3949,3952,3954,3957,3959],{"class":229,"line":3941},202,[227,3943,3648],{"class":336},[227,3945,3946],{"class":233}," Detect “replay” ",[227,3948,3197],{"class":336},[227,3950,3951],{"class":233}," an old ",[227,3953,1590],{"class":240},[227,3955,3956],{"class":233}," (a stolen refresh ",[227,3958,3221],{"class":364},[227,3960,3961],{"class":233}," can be used until it expires).\n",[227,3963,3965],{"class":229,"line":3964},203,[227,3966,836],{"emptyLinePlaceholder":835},[227,3968,3970,3973],{"class":229,"line":3969},204,[227,3971,3972],{"class":233},"#### Why consider stateless at all",[227,3974,3975],{"class":336},"?\n",[227,3977,3979],{"class":229,"line":3978},205,[227,3980,836],{"emptyLinePlaceholder":835},[227,3982,3984,3986,3989,3991,3994,3997,4000,4003],{"class":229,"line":3983},206,[227,3985,3648],{"class":336},[227,3987,3988],{"class":233}," You avoid any server‐side overhead for storing",[227,3990,1925],{"class":336},[227,3992,3993],{"class":233},"reading refresh‐token ",[227,3995,3996],{"class":240},"records",[227,3998,3999],{"class":233}," (e.g. no Redis or no ",[227,4001,4002],{"class":364},"SQL",[227,4004,4005],{"class":233}," table).\n",[227,4007,4009,4011,4014,4016],{"class":229,"line":4008},207,[227,4010,3648],{"class":336},[227,4012,4013],{"class":233}," Under very high load or simple microservices, you may want to keep your auth servers purely “sign",[227,4015,1925],{"class":336},[227,4017,4018],{"class":233},"verify” without hitting a database on every refresh.\n",[227,4020,4022,4024,4027,4030,4033,4035,4038,4040],{"class":229,"line":4021},208,[227,4023,3648],{"class":336},[227,4025,4026],{"class":233}," Fewer moving parts can mean easier horizontal scaling",[227,4028,4029],{"class":336},":",[227,4031,4032],{"class":233}," any instance ",[227,4034,3136],{"class":336},[227,4036,4037],{"class":233}," the same signing key can handle a refresh request ",[227,4039,3235],{"class":336},[227,4041,4042],{"class":233}," one step.\n",[227,4044,4046],{"class":229,"line":4045},209,[227,4047,836],{"emptyLinePlaceholder":835},[227,4049,4051,4053,4055,4058,4060,4063,4066,4068],{"class":229,"line":4050},210,[227,4052,3113],{"class":233},[227,4054,3814],{"class":364},[227,4056,4057],{"class":233}," How to Build a Self",[227,4059,3130],{"class":336},[227,4061,4062],{"class":240},"Contained",[227,4064,4065],{"class":233}," (Stateless) Refresh Token ",[227,4067,3235],{"class":336},[227,4069,4070],{"class":233}," Node.js\n",[227,4072,4074],{"class":229,"line":4073},211,[227,4075,836],{"emptyLinePlaceholder":835},[227,4077,4079,4082,4084,4087,4090,4093,4096,4098,4101,4104,4106,4108],{"class":229,"line":4078},212,[227,4080,4081],{"class":233},"The following is a minimal outline ",[227,4083,3197],{"class":336},[227,4085,4086],{"class":233}," a refresh flow that maintains zero state on the server. It uses ",[227,4088,4089],{"class":240},"JWTs",[227,4091,4092],{"class":233}," (signed with ",[227,4094,4095],{"class":364},"HS256",[227,4097,1164],{"class":233},[227,4099,4100],{"class":364},"RS256",[227,4102,4103],{"class":233},"), but any ",[227,4105,3706],{"class":364},[227,4107,3130],{"class":336},[227,4109,4110],{"class":233},"compatible algorithm can be substituted.\n",[227,4112,4114],{"class":229,"line":4113},213,[227,4115,836],{"emptyLinePlaceholder":835},[227,4117,4119,4121,4124],{"class":229,"line":4118},214,[227,4120,3631],{"class":233},[227,4122,4123],{"class":364},"2.1",[227,4125,4126],{"class":233}," Decide on Your Signing Key\n",[227,4128,4130],{"class":229,"line":4129},215,[227,4131,836],{"emptyLinePlaceholder":835},[227,4133,4135,4137,4139,4141,4143,4146],{"class":229,"line":4134},216,[227,4136,3410],{"class":336},[227,4138,4095],{"class":240},[227,4140,978],{"class":233},[227,4142,3155],{"class":364},[227,4144,4145],{"class":233},"):",[227,4147,3422],{"class":336},[227,4149,4151],{"class":229,"line":4150},217,[227,4152,836],{"emptyLinePlaceholder":835},[227,4154,4156],{"class":229,"line":4155},218,[227,4157,4158],{"class":346},"```js\n",[227,4160,4162],{"class":229,"line":4161},219,[227,4163,4164],{"class":346},"// Example: put this in your environment (never check into Git)\n",[227,4166,4168],{"class":229,"line":4167},220,[227,4169,4170],{"class":346},"process.env.JWT_REFRESH_SECRET = 'a‐very‐long‐random‐string‐only‐on‐server';\n",[15,4172,4173],{},"All instances of your auth service use that same secret to sign/verify.",[15,4175,4176],{},[78,4177,4178],{},"RS256 (RSA):",[218,4180,4184],{"className":4181,"code":4182,"language":4183,"meta":223,"style":223},"language-bash shiki shiki-themes github-light github-dark github-dark","# Generate once (do not share the private key)\nopenssl genrsa -out refresh‐private.pem 2048\nopenssl rsa ‐in refresh‐private.pem ‐pubout ‐out refresh‐public.pem\n","bash",[57,4185,4186,4191,4207],{"__ignoreMap":223},[227,4187,4188],{"class":229,"line":230},[227,4189,4190],{"class":355},"# Generate once (do not share the private key)\n",[227,4192,4193,4195,4198,4201,4204],{"class":229,"line":237},[227,4194,1163],{"class":240},[227,4196,4197],{"class":346}," genrsa",[227,4199,4200],{"class":364}," -out",[227,4202,4203],{"class":346}," refresh‐private.pem",[227,4205,4206],{"class":364}," 2048\n",[227,4208,4209,4211,4214,4217,4219,4222,4225],{"class":229,"line":247},[227,4210,1163],{"class":240},[227,4212,4213],{"class":346}," rsa",[227,4215,4216],{"class":346}," ‐in",[227,4218,4203],{"class":346},[227,4220,4221],{"class":346}," ‐pubout",[227,4223,4224],{"class":346}," ‐out",[227,4226,4227],{"class":346}," refresh‐public.pem\n",[15,4229,4230,4231,4234,4235,4237],{},"Your server loads ",[57,4232,4233],{},"refresh‐private.pem"," in memory (via ",[57,4236,325],{},") and uses it to sign every refresh JWT.",[15,4239,4240,4241,60],{},"On verify, you use ",[57,4242,4243],{},"refresh‐public.pem",[30,4245,4247],{"id":4246},"_22-define-the-refresh-token-payload","2.2 Define the Refresh-Token Payload",[15,4249,4250],{},"At a minimum, your refresh JWT should contain:",[218,4252,4255],{"className":4253,"code":4254,"language":632,"meta":223,"style":223},"language-json shiki shiki-themes github-light github-dark github-dark","{\n  \"sub\": \"\u003CuserId>\",            // subject = the user’s unique ID\n  \"typ\": \"refresh\",             // optional: mark it as a refresh token\n  \"iat\": \u003Cissued-at>,             // auto‐included by most libraries \n  \"exp\": \u003Cexpiry timestamp>,      // e.g. now + 30 days\n  // (optionally) \"jti\": \"\u003Csome‐unique‐identifier>\", if you want to blacklist entire “batches” by rotating keys\n}\n",[57,4256,4257,4261,4278,4294,4309,4328,4333],{"__ignoreMap":223},[227,4258,4259],{"class":229,"line":230},[227,4260,234],{"class":233},[227,4262,4263,4266,4269,4272,4275],{"class":229,"line":237},[227,4264,4265],{"class":364},"  \"sub\"",[227,4267,4268],{"class":233},": ",[227,4270,4271],{"class":346},"\"\u003CuserId>\"",[227,4273,4274],{"class":233},",            ",[227,4276,4277],{"class":355},"// subject = the user’s unique ID\n",[227,4279,4280,4283,4285,4288,4291],{"class":229,"line":247},[227,4281,4282],{"class":364},"  \"typ\"",[227,4284,4268],{"class":233},[227,4286,4287],{"class":346},"\"refresh\"",[227,4289,4290],{"class":233},",             ",[227,4292,4293],{"class":355},"// optional: mark it as a refresh token\n",[227,4295,4296,4299,4301,4304,4306],{"class":229,"line":256},[227,4297,4298],{"class":364},"  \"iat\"",[227,4300,4268],{"class":233},[227,4302,4303],{"class":289},"\u003Cissued-at>",[227,4305,4290],{"class":233},[227,4307,4308],{"class":355},"// auto‐included by most libraries \n",[227,4310,4311,4314,4316,4319,4322,4325],{"class":229,"line":265},[227,4312,4313],{"class":364},"  \"exp\"",[227,4315,4268],{"class":233},[227,4317,4318],{"class":289},"\u003Cexpiry",[227,4320,4321],{"class":289}," timestamp>",[227,4323,4324],{"class":233},",      ",[227,4326,4327],{"class":355},"// e.g. now + 30 days\n",[227,4329,4330],{"class":229,"line":281},[227,4331,4332],{"class":355},"  // (optionally) \"jti\": \"\u003Csome‐unique‐identifier>\", if you want to blacklist entire “batches” by rotating keys\n",[227,4334,4335],{"class":229,"line":302},[227,4336,305],{"class":233},[35,4338,4339,4345,4350],{},[38,4340,4341,4344],{},[57,4342,4343],{},"sub",": your user’s unique identifier.",[38,4346,4347,4349],{},[57,4348,113],{},": e.g. now + 30 days if you want a month‐long refresh window.",[38,4351,4352,4355],{},[57,4353,4354],{},"jti",": JWT ID (optional). If you ever need to revoke “all old tokens,” you can add a rotated key or a “current‐token‐version” in the user’s profile (but that reintroduces some state).",[30,4357,4359],{"id":4358},"_23-issue-a-refresh-jwt-at-login","2.3 Issue a Refresh JWT at Login",[218,4361,4363],{"className":220,"code":4362,"language":222,"meta":223,"style":223},"import jwt from 'jsonwebtoken';\n\n// after user has successfully authenticated:\nfunction issueTokens(userId) {\n  // 1) Access Token (short‐lived)\n  const accessToken = jwt.sign(\n    { sub: userId, typ: 'access' },\n    process.env.JWT_ACCESS_SECRET,   // e.g. HS256 secret or RS256 private key\n    { algorithm: 'HS256', expiresIn: '15m' }\n  );\n\n  // 2) Refresh Token (long‐lived, self‐contained)\n  const refreshToken = jwt.sign(\n    { sub: userId, typ: 'refresh' },\n    process.env.JWT_REFRESH_SECRET,  // only the auth server knows this\n    { algorithm: 'HS256', expiresIn: '30d' }\n  );\n\n  return { accessToken, refreshToken };\n}\n",[57,4364,4365,4377,4381,4386,4400,4405,4419,4429,4443,4455,4459,4463,4468,4482,4491,4504,4517,4521,4525,4532],{"__ignoreMap":223},[227,4366,4367,4369,4371,4373,4375],{"class":229,"line":230},[227,4368,337],{"class":336},[227,4370,340],{"class":233},[227,4372,343],{"class":336},[227,4374,347],{"class":346},[227,4376,350],{"class":233},[227,4378,4379],{"class":229,"line":237},[227,4380,836],{"emptyLinePlaceholder":835},[227,4382,4383],{"class":229,"line":247},[227,4384,4385],{"class":355},"// after user has successfully authenticated:\n",[227,4387,4388,4390,4393,4395,4398],{"class":229,"line":256},[227,4389,1581],{"class":336},[227,4391,4392],{"class":240}," issueTokens",[227,4394,460],{"class":233},[227,4396,4397],{"class":1589},"userId",[227,4399,1593],{"class":233},[227,4401,4402],{"class":229,"line":265},[227,4403,4404],{"class":355},"  // 1) Access Token (short‐lived)\n",[227,4406,4407,4409,4411,4413,4415,4417],{"class":229,"line":281},[227,4408,694],{"class":336},[227,4410,365],{"class":364},[227,4412,368],{"class":336},[227,4414,371],{"class":233},[227,4416,374],{"class":240},[227,4418,377],{"class":233},[227,4420,4421,4424,4427],{"class":229,"line":302},[227,4422,4423],{"class":233},"    { sub: userId, typ: ",[227,4425,4426],{"class":346},"'access'",[227,4428,2649],{"class":233},[227,4430,4431,4434,4437,4440],{"class":229,"line":621},[227,4432,4433],{"class":233},"    process.env.",[227,4435,4436],{"class":364},"JWT_ACCESS_SECRET",[227,4438,4439],{"class":233},",   ",[227,4441,4442],{"class":355},"// e.g. HS256 secret or RS256 private key\n",[227,4444,4445,4447,4449,4451,4453],{"class":229,"line":627},[227,4446,1829],{"class":233},[227,4448,404],{"class":346},[227,4450,407],{"class":233},[227,4452,410],{"class":346},[227,4454,413],{"class":233},[227,4456,4457],{"class":229,"line":1040},[227,4458,1844],{"class":233},[227,4460,4461],{"class":229,"line":1053},[227,4462,836],{"emptyLinePlaceholder":835},[227,4464,4465],{"class":229,"line":1058},[227,4466,4467],{"class":355},"  // 2) Refresh Token (long‐lived, self‐contained)\n",[227,4469,4470,4472,4474,4476,4478,4480],{"class":229,"line":1063},[227,4471,694],{"class":336},[227,4473,452],{"class":364},[227,4475,368],{"class":336},[227,4477,371],{"class":233},[227,4479,374],{"class":240},[227,4481,377],{"class":233},[227,4483,4484,4486,4489],{"class":229,"line":1069},[227,4485,4423],{"class":233},[227,4487,4488],{"class":346},"'refresh'",[227,4490,2649],{"class":233},[227,4492,4493,4495,4498,4501],{"class":229,"line":1075},[227,4494,4433],{"class":233},[227,4496,4497],{"class":364},"JWT_REFRESH_SECRET",[227,4499,4500],{"class":233},",  ",[227,4502,4503],{"class":355},"// only the auth server knows this\n",[227,4505,4506,4508,4510,4512,4515],{"class":229,"line":1081},[227,4507,1829],{"class":233},[227,4509,404],{"class":346},[227,4511,407],{"class":233},[227,4513,4514],{"class":346},"'30d'",[227,4516,413],{"class":233},[227,4518,4519],{"class":229,"line":1087},[227,4520,1844],{"class":233},[227,4522,4523],{"class":229,"line":1093},[227,4524,836],{"emptyLinePlaceholder":835},[227,4526,4527,4529],{"class":229,"line":1098},[227,4528,1599],{"class":336},[227,4530,4531],{"class":233}," { accessToken, refreshToken };\n",[227,4533,4534],{"class":229,"line":1578},[227,4535,305],{"class":233},[15,4537,4538,4539,4542],{},"Send the ",[57,4540,4541],{},"accessToken"," in JSON response.",[15,4544,4538,4545,4547],{},[57,4546,485],{}," in an HTTP‐only cookie (or let the client store it in secure storage if you trust your front‐end environment).",[15,4549,4550],{},[78,4551,4552],{},"Example Express handler:",[218,4554,4556],{"className":220,"code":4555,"language":222,"meta":223,"style":223},"app.post('/auth/login', async (req, res) => {\n  // 1. validate credentials (e.g. DB lookup, bcrypt.compare, etc.)\n  // assume user.id == 'alice123'\n  const { accessToken, refreshToken } = issueTokens('alice123');\n\n  res\n    .cookie('refreshToken', refreshToken, {\n      httpOnly: true,\n      secure: process.env.NODE_ENV === 'production',\n      path: '/auth/refresh',\n      maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days\n    })\n    .json({ accessToken });\n});\n",[57,4557,4558,4586,4591,4596,4621,4625,4629,4641,4649,4661,4669,4693,4697,4705],{"__ignoreMap":223},[227,4559,4560,4562,4564,4566,4568,4570,4572,4574,4576,4578,4580,4582,4584],{"class":229,"line":230},[227,4561,1439],{"class":233},[227,4563,1648],{"class":240},[227,4565,460],{"class":233},[227,4567,1653],{"class":346},[227,4569,188],{"class":233},[227,4571,1658],{"class":336},[227,4573,978],{"class":233},[227,4575,1663],{"class":1589},[227,4577,188],{"class":233},[227,4579,1668],{"class":1589},[227,4581,1671],{"class":233},[227,4583,1674],{"class":336},[227,4585,689],{"class":233},[227,4587,4588],{"class":229,"line":237},[227,4589,4590],{"class":355},"  // 1. validate credentials (e.g. DB lookup, bcrypt.compare, etc.)\n",[227,4592,4593],{"class":229,"line":247},[227,4594,4595],{"class":355},"  // assume user.id == 'alice123'\n",[227,4597,4598,4600,4602,4604,4606,4608,4610,4612,4614,4616,4619],{"class":229,"line":256},[227,4599,694],{"class":336},[227,4601,1684],{"class":233},[227,4603,4541],{"class":364},[227,4605,188],{"class":233},[227,4607,485],{"class":364},[227,4609,1695],{"class":233},[227,4611,719],{"class":336},[227,4613,4392],{"class":240},[227,4615,460],{"class":233},[227,4617,4618],{"class":346},"'alice123'",[227,4620,418],{"class":233},[227,4622,4623],{"class":229,"line":265},[227,4624,836],{"emptyLinePlaceholder":835},[227,4626,4627],{"class":229,"line":281},[227,4628,1970],{"class":233},[227,4630,4631,4633,4635,4637,4639],{"class":229,"line":302},[227,4632,1976],{"class":233},[227,4634,545],{"class":240},[227,4636,460],{"class":233},[227,4638,550],{"class":346},[227,4640,553],{"class":233},[227,4642,4643,4645,4647],{"class":229,"line":621},[227,4644,1991],{"class":233},[227,4646,561],{"class":364},[227,4648,564],{"class":233},[227,4650,4651,4653,4655,4657,4659],{"class":229,"line":627},[227,4652,2001],{"class":233},[227,4654,2004],{"class":364},[227,4656,2007],{"class":336},[227,4658,2010],{"class":346},[227,4660,564],{"class":233},[227,4662,4663,4665,4667],{"class":229,"line":1040},[227,4664,2018],{"class":233},[227,4666,585],{"class":346},[227,4668,564],{"class":233},[227,4670,4671,4673,4675,4677,4679,4681,4683,4685,4687,4689,4691],{"class":229,"line":1053},[227,4672,2028],{"class":233},[227,4674,595],{"class":364},[227,4676,598],{"class":336},[227,4678,601],{"class":364},[227,4680,598],{"class":336},[227,4682,606],{"class":364},[227,4684,598],{"class":336},[227,4686,606],{"class":364},[227,4688,598],{"class":336},[227,4690,615],{"class":364},[227,4692,618],{"class":355},[227,4694,4695],{"class":229,"line":1058},[227,4696,2042],{"class":233},[227,4698,4699,4701,4703],{"class":229,"line":1063},[227,4700,1976],{"class":233},[227,4702,632],{"class":240},[227,4704,635],{"class":233},[227,4706,4707],{"class":229,"line":1069},[227,4708,2057],{"class":233},[30,4710,4712],{"id":4711},"_24-verify-the-stateless-refresh-jwt-no-db-hit","2.4 Verify the Stateless Refresh JWT (No DB Hit)",[15,4714,4715,4716,4718],{},"When the client calls ",[57,4717,876],{},", you simply:",[131,4720,4721,4731,4734,4739],{},[38,4722,4723,4724,4726,4727,4730],{},"Pull ",[57,4725,485],{}," from the cookie (or from ",[57,4728,4729],{},"Authorization: Bearer",", if you choose).",[38,4732,4733],{},"Verify its signature and expiry with your single shared secret (or RSA public key).",[38,4735,4736,4737,319],{},"If valid → issue a new access token (and optionally a brand‐new refresh JWT with a fresh ",[57,4738,113],{},[38,4740,4741],{},"If invalid (expired, malformed, wrong signature) → force a full login.",[218,4743,4745],{"className":220,"code":4744,"language":222,"meta":223,"style":223},"app.post('/auth/refresh', (req, res) => {\n  const rawRefresh = req.cookies.refreshToken;\n  if (!rawRefresh) {\n    return res.sendStatus(401);\n  }\n\n  try {\n    // 1) verify signature + expiry\n    const payload = jwt.verify(\n      rawRefresh,\n      process.env.JWT_REFRESH_SECRET, // or the public key if RS256\n      { algorithms: ['HS256'] }\n    );\n    // 2) ensure it’s actually a “refresh” token\n    if (payload.typ !== 'refresh') {\n      throw new Error('Not a refresh token');\n    }\n\n    // 3) payload.sub is the user ID\n    const newAccessToken = jwt.sign(\n      { sub: payload.sub, typ: 'access' },\n      process.env.JWT_ACCESS_SECRET,\n      { algorithm: 'HS256', expiresIn: '15m' }\n    );\n\n    // 4) (OPTIONAL) If you want to rotate your refresh token’s expiry:\n    const newRefreshToken = jwt.sign(\n      { sub: payload.sub, typ: 'refresh' },\n      process.env.JWT_REFRESH_SECRET,\n      { algorithm: 'HS256', expiresIn: '30d' }\n    );\n    // send the new refresh JWT in the cookie\n    res.cookie('refreshToken', newRefreshToken, {\n      httpOnly: true,\n      secure: process.env.NODE_ENV === 'production',\n      path: '/auth/refresh',\n      maxAge: 30 * 24 * 60 * 60 * 1000\n    });\n\n    // 5) send new accessToken in JSON\n    return res.json({ accessToken: newAccessToken });\n  } catch (err) {\n    // invalid signature or expired → force re‐login\n    return res.sendStatus(401);\n  }\n});\n",[57,4746,4747,4771,4782,4793,4807,4811,4815,4821,4826,4840,4845,4857,4867,4872,4877,4893,4909,4914,4918,4923,4937,4946,4954,4967,4971,4975,4980,4995,5003,5011,5023,5027,5032,5046,5054,5066,5074,5096,5101,5105,5110,5120,5128,5133,5147,5151],{"__ignoreMap":223},[227,4748,4749,4751,4753,4755,4757,4759,4761,4763,4765,4767,4769],{"class":229,"line":230},[227,4750,1439],{"class":233},[227,4752,1648],{"class":240},[227,4754,460],{"class":233},[227,4756,585],{"class":346},[227,4758,2084],{"class":233},[227,4760,1663],{"class":1589},[227,4762,188],{"class":233},[227,4764,1668],{"class":1589},[227,4766,1671],{"class":233},[227,4768,1674],{"class":336},[227,4770,689],{"class":233},[227,4772,4773,4775,4778,4780],{"class":229,"line":237},[227,4774,694],{"class":336},[227,4776,4777],{"class":364}," rawRefresh",[227,4779,368],{"class":336},[227,4781,918],{"class":233},[227,4783,4784,4786,4788,4790],{"class":229,"line":247},[227,4785,1730],{"class":336},[227,4787,978],{"class":233},[227,4789,981],{"class":336},[227,4791,4792],{"class":233},"rawRefresh) {\n",[227,4794,4795,4797,4799,4801,4803,4805],{"class":229,"line":256},[227,4796,1755],{"class":336},[227,4798,1104],{"class":233},[227,4800,754],{"class":240},[227,4802,460],{"class":233},[227,4804,759],{"class":364},[227,4806,418],{"class":233},[227,4808,4809],{"class":229,"line":265},[227,4810,1783],{"class":233},[227,4812,4813],{"class":229,"line":281},[227,4814,836],{"emptyLinePlaceholder":835},[227,4816,4817,4819],{"class":229,"line":302},[227,4818,2190],{"class":336},[227,4820,689],{"class":233},[227,4822,4823],{"class":229,"line":621},[227,4824,4825],{"class":355},"    // 1) verify signature + expiry\n",[227,4827,4828,4830,4832,4834,4836,4838],{"class":229,"line":627},[227,4829,2198],{"class":336},[227,4831,697],{"class":364},[227,4833,368],{"class":336},[227,4835,371],{"class":233},[227,4837,704],{"class":240},[227,4839,377],{"class":233},[227,4841,4842],{"class":229,"line":1040},[227,4843,4844],{"class":233},"      rawRefresh,\n",[227,4846,4847,4850,4852,4854],{"class":229,"line":1053},[227,4848,4849],{"class":233},"      process.env.",[227,4851,4497],{"class":364},[227,4853,188],{"class":233},[227,4855,4856],{"class":355},"// or the public key if RS256\n",[227,4858,4859,4862,4864],{"class":229,"line":1058},[227,4860,4861],{"class":233},"      { algorithms: [",[227,4863,404],{"class":346},[227,4865,4866],{"class":233},"] }\n",[227,4868,4869],{"class":229,"line":1063},[227,4870,4871],{"class":233},"    );\n",[227,4873,4874],{"class":229,"line":1069},[227,4875,4876],{"class":355},"    // 2) ensure it’s actually a “refresh” token\n",[227,4878,4879,4882,4885,4888,4891],{"class":229,"line":1075},[227,4880,4881],{"class":336},"    if",[227,4883,4884],{"class":233}," (payload.typ ",[227,4886,4887],{"class":336},"!==",[227,4889,4890],{"class":346}," 'refresh'",[227,4892,1593],{"class":233},[227,4894,4895,4898,4900,4902,4904,4907],{"class":229,"line":1081},[227,4896,4897],{"class":336},"      throw",[227,4899,990],{"class":336},[227,4901,993],{"class":240},[227,4903,460],{"class":233},[227,4905,4906],{"class":346},"'Not a refresh token'",[227,4908,418],{"class":233},[227,4910,4911],{"class":229,"line":1087},[227,4912,4913],{"class":233},"    }\n",[227,4915,4916],{"class":229,"line":1093},[227,4917,836],{"emptyLinePlaceholder":835},[227,4919,4920],{"class":229,"line":1098},[227,4921,4922],{"class":355},"    // 3) payload.sub is the user ID\n",[227,4924,4925,4927,4929,4931,4933,4935],{"class":229,"line":1578},[227,4926,2198],{"class":336},[227,4928,1016],{"class":364},[227,4930,368],{"class":336},[227,4932,371],{"class":233},[227,4934,374],{"class":240},[227,4936,377],{"class":233},[227,4938,4939,4942,4944],{"class":229,"line":1596},[227,4940,4941],{"class":233},"      { sub: payload.sub, typ: ",[227,4943,4426],{"class":346},[227,4945,2649],{"class":233},[227,4947,4948,4950,4952],{"class":229,"line":1627},[227,4949,4849],{"class":233},[227,4951,4436],{"class":364},[227,4953,564],{"class":233},[227,4955,4956,4959,4961,4963,4965],{"class":229,"line":1632},[227,4957,4958],{"class":233},"      { algorithm: ",[227,4960,404],{"class":346},[227,4962,407],{"class":233},[227,4964,410],{"class":346},[227,4966,413],{"class":233},[227,4968,4969],{"class":229,"line":1637},[227,4970,4871],{"class":233},[227,4972,4973],{"class":229,"line":1643},[227,4974,836],{"emptyLinePlaceholder":835},[227,4976,4977],{"class":229,"line":1679},[227,4978,4979],{"class":355},"    // 4) (OPTIONAL) If you want to rotate your refresh token’s expiry:\n",[227,4981,4982,4984,4987,4989,4991,4993],{"class":229,"line":1703},[227,4983,2198],{"class":336},[227,4985,4986],{"class":364}," newRefreshToken",[227,4988,368],{"class":336},[227,4990,371],{"class":233},[227,4992,374],{"class":240},[227,4994,377],{"class":233},[227,4996,4997,4999,5001],{"class":229,"line":1709},[227,4998,4941],{"class":233},[227,5000,4488],{"class":346},[227,5002,2649],{"class":233},[227,5004,5005,5007,5009],{"class":229,"line":1727},[227,5006,4849],{"class":233},[227,5008,4497],{"class":364},[227,5010,564],{"class":233},[227,5012,5013,5015,5017,5019,5021],{"class":229,"line":1752},[227,5014,4958],{"class":233},[227,5016,404],{"class":346},[227,5018,407],{"class":233},[227,5020,4514],{"class":346},[227,5022,413],{"class":233},[227,5024,5025],{"class":229,"line":1780},[227,5026,4871],{"class":233},[227,5028,5029],{"class":229,"line":1786},[227,5030,5031],{"class":355},"    // send the new refresh JWT in the cookie\n",[227,5033,5034,5037,5039,5041,5043],{"class":229,"line":1791},[227,5035,5036],{"class":233},"    res.",[227,5038,545],{"class":240},[227,5040,460],{"class":233},[227,5042,550],{"class":346},[227,5044,5045],{"class":233},", newRefreshToken, {\n",[227,5047,5048,5050,5052],{"class":229,"line":1797},[227,5049,1991],{"class":233},[227,5051,561],{"class":364},[227,5053,564],{"class":233},[227,5055,5056,5058,5060,5062,5064],{"class":229,"line":1812},[227,5057,2001],{"class":233},[227,5059,2004],{"class":364},[227,5061,2007],{"class":336},[227,5063,2010],{"class":346},[227,5065,564],{"class":233},[227,5067,5068,5070,5072],{"class":229,"line":1818},[227,5069,2018],{"class":233},[227,5071,585],{"class":346},[227,5073,564],{"class":233},[227,5075,5076,5078,5080,5082,5084,5086,5088,5090,5092,5094],{"class":229,"line":1826},[227,5077,2028],{"class":233},[227,5079,595],{"class":364},[227,5081,598],{"class":336},[227,5083,601],{"class":364},[227,5085,598],{"class":336},[227,5087,606],{"class":364},[227,5089,598],{"class":336},[227,5091,606],{"class":364},[227,5093,598],{"class":336},[227,5095,2036],{"class":364},[227,5097,5098],{"class":229,"line":1841},[227,5099,5100],{"class":233},"    });\n",[227,5102,5103],{"class":229,"line":1847},[227,5104,836],{"emptyLinePlaceholder":835},[227,5106,5107],{"class":229,"line":1852},[227,5108,5109],{"class":355},"    // 5) send new accessToken in JSON\n",[227,5111,5112,5114,5116,5118],{"class":229,"line":1858},[227,5113,1755],{"class":336},[227,5115,1104],{"class":233},[227,5117,632],{"class":240},[227,5119,1109],{"class":233},[227,5121,5122,5124,5126],{"class":229,"line":1887},[227,5123,2253],{"class":233},[227,5125,738],{"class":336},[227,5127,741],{"class":233},[227,5129,5130],{"class":229,"line":1901},[227,5131,5132],{"class":355},"    // invalid signature or expired → force re‐login\n",[227,5134,5135,5137,5139,5141,5143,5145],{"class":229,"line":1939},[227,5136,1755],{"class":336},[227,5138,1104],{"class":233},[227,5140,754],{"class":240},[227,5142,460],{"class":233},[227,5144,759],{"class":364},[227,5146,418],{"class":233},[227,5148,5149],{"class":229,"line":1944},[227,5150,1783],{"class":233},[227,5152,5153],{"class":229,"line":1956},[227,5154,2057],{"class":233},[35,5156,5157,5160,5169],{},[38,5158,5159],{},"No database or in‐memory lookup is ever performed.",[38,5161,5162,5163,5165,5166,5168],{},"The ",[57,5164,59],{}," call alone both checks the signature and confirms ",[57,5167,113],{}," hasn’t passed.",[38,5170,5171,5172,5174],{},"You could optionally rotate the refresh JWT on each use (as shown), but that still remains stateless—every old refresh token (signed earlier) is still valid until its original ",[57,5173,113],{}," passes.",[25,5176,5178],{"id":5177},"_3-trade-offs-security-implications","3. Trade-Offs & Security Implications",[30,5180,5182],{"id":5181},"_31-you-lose-per-token-revocation","3.1 You Lose Per-Token Revocation",[15,5184,5185],{},"If you issue a refresh‐JWT that’s valid for 30 days, and the user’s device is stolen on day 5, you cannot immediately revoke it.",[15,5187,5188,5189,5191],{},"The only way to “revoke” would be to rotate your entire ",[57,5190,4497],{}," (or RSA private key), which instantly invalidates all outstanding refresh tokens for all users. That’s almost never acceptable in a large system.",[15,5193,5194],{},"In other words, stateless refresh tokens force you to accept that once signed, each token lives until its built-in expiry (unless you rotate the global signing key).",[30,5196,5198],{"id":5197},"_32-you-cannot-track-or-invalidate-individual-sessions","3.2 You Cannot Track or Invalidate Individual Sessions",[15,5200,5201],{},"You have no way (server‐side) to see which device/session is using which refresh token.",[15,5203,5204],{},"You cannot “log out” a single device without “log out everyone” (key rotation) or embedding some “revokedTokensList” reference that reintroduces state.",[30,5206,5208],{"id":5207},"_33-replay-risk-is-permanent-until-expiration","3.3 Replay Risk Is Permanent Until Expiration",[15,5210,5211,5212,5214,5215,5217],{},"If someone exfiltrates a valid refresh JWT, they can keep calling ",[57,5213,876],{}," until that JWT’s ",[57,5216,113],{}," timestamp passes.",[15,5219,5220],{},"You have no way to detect that replay happens because you never store “this token was already used.”",[15,5222,5223,5224,5226],{},"In a more stateful design, you might store a ",[57,5225,4354],{}," (JWT ID) in a DB and flag it as used or revoked; stateless means you don’t.",[30,5228,5230],{"id":5229},"_34-clock-skew-expiry-boundaries","3.4 Clock-Skew & Expiry Boundaries",[15,5232,5233],{},"You must ensure all your servers’ clocks are tightly synchronized (e.g. via NTP).",[15,5235,5236],{},"If a client’s clock is off by a few minutes, it may try to refresh a token that your server already considers expired (or vice versa).",[25,5238,5240],{"id":5239},"_4-when-is-stateless-refresh-reasonable","4. When Is Stateless Refresh Reasonable?",[35,5242,5243,5251,5259,5267],{},[38,5244,5245,5248,5250],{},[78,5246,5247],{},"Small/Short-Lived Deployments or Internal Tools",[214,5249],{},"If you don’t care about revocation or compromised refresh tokens—e.g. internal tools that rotate keys every day.",[38,5252,5253,5256,5258],{},[78,5254,5255],{},"Low-Risk Data",[214,5257],{},"If the access tokens only grant access to non-critical resources (e.g. a public RSS feed).",[38,5260,5261,5264,5266],{},[78,5262,5263],{},"Very Low Friction Environments",[214,5265],{},"If you can tolerate “force-expire everyone if the key leaks.”",[38,5268,5269,5272,5274],{},[78,5270,5271],{},"Microservices That Rely on Downstream Validation Only",[214,5273],{},"If your gateways or downstream services need to verify refresh tokens without hitting a central DB, you save that hop—but accept the lack of revocation.",[25,5276,5278],{"id":5277},"_5-alternatives-that-keep-some-state-but-minimize-it","5. Alternatives That Keep Some “State” But Minimize It",[15,5280,5281],{},"If you want most of the scaling benefits of statelessness but still occasionally need to revoke or blacklist:",[30,5283,5285],{"id":5284},"_51-store-a-token-version-or-token-counter-in-the-users-record","5.1 Store a “Token Version” or “Token Counter” in the User’s Record",[35,5287,5288,5294,5300],{},[38,5289,5290,5291,60],{},"In each refresh‐JWT’s payload, embed a custom claim, e.g. ",[57,5292,5293],{},"ver: 3",[38,5295,5296,5297,60],{},"In your user table, store ",[57,5298,5299],{},"currentRefreshVersion = 3",[38,5301,5302,5303,5305,5306,5366],{},"On ",[57,5304,876],{},", do:",[218,5307,5309],{"className":220,"code":5308,"language":222,"meta":223,"style":223},"const payload = jwt.verify(...);\nif (payload.ver !== user.currentRefreshVersion) throw 401;\n// otherwise, issue new tokens with ver++\nupdate user.currentRefreshVersion in DB;\n",[57,5310,5311,5330,5349,5354],{"__ignoreMap":223},[227,5312,5313,5315,5317,5319,5321,5323,5325,5328],{"class":229,"line":230},[227,5314,361],{"class":336},[227,5316,697],{"class":364},[227,5318,368],{"class":336},[227,5320,371],{"class":233},[227,5322,704],{"class":240},[227,5324,460],{"class":233},[227,5326,5327],{"class":336},"...",[227,5329,418],{"class":233},[227,5331,5332,5334,5337,5339,5342,5344,5347],{"class":229,"line":237},[227,5333,975],{"class":336},[227,5335,5336],{"class":233}," (payload.ver ",[227,5338,4887],{"class":336},[227,5340,5341],{"class":233}," user.currentRefreshVersion) ",[227,5343,987],{"class":336},[227,5345,5346],{"class":364}," 401",[227,5348,350],{"class":233},[227,5350,5351],{"class":229,"line":247},[227,5352,5353],{"class":355},"// otherwise, issue new tokens with ver++\n",[227,5355,5356,5359,5361,5364],{"class":229,"line":256},[227,5357,5358],{"class":233},"update user.currentRefreshVersion ",[227,5360,3235],{"class":336},[227,5362,5363],{"class":364}," DB",[227,5365,350],{"class":233},[35,5367,5368,5377],{},[38,5369,5370,5373,5374,60],{},[78,5371,5372],{},"Pros:"," You only store a single integer per user. You can revoke all refresh tokens for user “alice” by bumping ",[57,5375,5376],{},"currentRefreshVersion",[38,5378,5379,5382,5383,5386,5387,5390],{},[78,5380,5381],{},"Cons:"," You still need exactly one DB lookup per refresh (to check ",[57,5384,5385],{},"payload.ver"," against ",[57,5388,5389],{},"user.currentRefreshVersion","). It’s not completely stateless, but it’s minimal state.",[30,5392,5394],{"id":5393},"_52-shorten-the-refresh-token-expiry","5.2 Shorten the Refresh Token Expiry",[15,5396,5397],{},"If you’re worried about stolen refresh tokens, reduce the TTL from 30 days to say 7 days or even 24 hours.",[35,5399,5400,5403],{},[38,5401,5402],{},"The shorter its window, the less time an attacker has to abuse stolen credentials.",[38,5404,5405],{},"But now your clients must refresh more frequently.",[30,5407,5409],{"id":5408},"_53-use-a-sliding-window-in-a-very-thin-cache","5.3 Use a “Sliding Window” in a Very Thin Cache",[35,5411,5412,5418,5421],{},[38,5413,5414,5415,5417],{},"Keep a small in‐memory LRU cache (or Redis) of recently issued ",[57,5416,4354],{}," values.",[38,5419,5420],{},"Whenever a refresh‐JWT is used, you check “have I seen this jti already? If yes, possibly replay attack. If no, mark it seen for next 10 minutes.”",[38,5422,5423],{},"This only catches immediate replay, not longer‐term theft. Also, it’s still a stateful store, just extremely short‐lived.",[25,5425,5427],{"id":5426},"_6-code-example-stateless-vs-minimal-state","6. Code Example: Stateless vs. Minimal State",[15,5429,5430],{},"The following is a side-by-side sketch:",[30,5432,5434],{"id":5433},"_61-fully-stateless-no-db-at-all","6.1 Fully Stateless (No DB at All)",[218,5436,5438],{"className":220,"code":5437,"language":222,"meta":223,"style":223},"// REFRESH_SECRET is an HS256 secret only on server\nconst REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;\n\n// Issue at login:\nfunction issueStatelessRefreshToken(userId) {\n  // no “jti” or DB check—just a long‐lived exp\n  return jwt.sign(\n    { sub: userId, typ: 'refresh' },\n    REFRESH_SECRET,\n    { algorithm: 'HS256', expiresIn: '30d' }\n  );\n}\n\n// /auth/refresh route:\napp.post('/auth/refresh', (req, res) => {\n  const token = req.cookies.refreshToken;\n  if (!token) return res.sendStatus(401);\n\n  try {\n    const payload = jwt.verify(token, REFRESH_SECRET, { algorithms: ['HS256'] });\n    if (payload.typ !== 'refresh') throw new Error();\n    // OK: issue new access & (optionally) new refresh\n    const accessToken = jwt.sign({ sub: payload.sub, typ: 'access' }, ACCESS_SECRET, { expiresIn: '15m' });\n    // If rotating, generate a brand‐new refresh here:\n    const newRefresh = jwt.sign({ sub: payload.sub, typ: 'refresh' }, REFRESH_SECRET, { expiresIn: '30d' });\n    res.cookie('refreshToken', newRefresh, { httpOnly: true, secure: true, path: '/auth/refresh', maxAge: 30*24*60*60*1000 });\n    return res.json({ accessToken });\n  } catch (e) {\n    return res.sendStatus(401);\n  }\n});\n",[57,5439,5440,5445,5460,5464,5469,5482,5487,5497,5505,5512,5524,5528,5532,5536,5541,5565,5575,5597,5601,5607,5631,5651,5656,5686,5691,5718,5769,5779,5788,5802,5806],{"__ignoreMap":223},[227,5441,5442],{"class":229,"line":230},[227,5443,5444],{"class":355},"// REFRESH_SECRET is an HS256 secret only on server\n",[227,5446,5447,5449,5452,5454,5456,5458],{"class":229,"line":237},[227,5448,361],{"class":336},[227,5450,5451],{"class":364}," REFRESH_SECRET",[227,5453,368],{"class":336},[227,5455,1479],{"class":233},[227,5457,4497],{"class":364},[227,5459,350],{"class":233},[227,5461,5462],{"class":229,"line":247},[227,5463,836],{"emptyLinePlaceholder":835},[227,5465,5466],{"class":229,"line":256},[227,5467,5468],{"class":355},"// Issue at login:\n",[227,5470,5471,5473,5476,5478,5480],{"class":229,"line":265},[227,5472,1581],{"class":336},[227,5474,5475],{"class":240}," issueStatelessRefreshToken",[227,5477,460],{"class":233},[227,5479,4397],{"class":1589},[227,5481,1593],{"class":233},[227,5483,5484],{"class":229,"line":281},[227,5485,5486],{"class":355},"  // no “jti” or DB check—just a long‐lived exp\n",[227,5488,5489,5491,5493,5495],{"class":229,"line":302},[227,5490,1599],{"class":336},[227,5492,371],{"class":233},[227,5494,374],{"class":240},[227,5496,377],{"class":233},[227,5498,5499,5501,5503],{"class":229,"line":621},[227,5500,4423],{"class":233},[227,5502,4488],{"class":346},[227,5504,2649],{"class":233},[227,5506,5507,5510],{"class":229,"line":627},[227,5508,5509],{"class":364},"    REFRESH_SECRET",[227,5511,564],{"class":233},[227,5513,5514,5516,5518,5520,5522],{"class":229,"line":1040},[227,5515,1829],{"class":233},[227,5517,404],{"class":346},[227,5519,407],{"class":233},[227,5521,4514],{"class":346},[227,5523,413],{"class":233},[227,5525,5526],{"class":229,"line":1053},[227,5527,1844],{"class":233},[227,5529,5530],{"class":229,"line":1058},[227,5531,305],{"class":233},[227,5533,5534],{"class":229,"line":1063},[227,5535,836],{"emptyLinePlaceholder":835},[227,5537,5538],{"class":229,"line":1069},[227,5539,5540],{"class":355},"// /auth/refresh route:\n",[227,5542,5543,5545,5547,5549,5551,5553,5555,5557,5559,5561,5563],{"class":229,"line":1075},[227,5544,1439],{"class":233},[227,5546,1648],{"class":240},[227,5548,460],{"class":233},[227,5550,585],{"class":346},[227,5552,2084],{"class":233},[227,5554,1663],{"class":1589},[227,5556,188],{"class":233},[227,5558,1668],{"class":1589},[227,5560,1671],{"class":233},[227,5562,1674],{"class":336},[227,5564,689],{"class":233},[227,5566,5567,5569,5571,5573],{"class":229,"line":1081},[227,5568,694],{"class":336},[227,5570,2130],{"class":364},[227,5572,368],{"class":336},[227,5574,918],{"class":233},[227,5576,5577,5579,5581,5583,5585,5587,5589,5591,5593,5595],{"class":229,"line":1087},[227,5578,1730],{"class":336},[227,5580,978],{"class":233},[227,5582,981],{"class":336},[227,5584,2167],{"class":233},[227,5586,1101],{"class":336},[227,5588,1104],{"class":233},[227,5590,754],{"class":240},[227,5592,460],{"class":233},[227,5594,759],{"class":364},[227,5596,418],{"class":233},[227,5598,5599],{"class":229,"line":1093},[227,5600,836],{"emptyLinePlaceholder":835},[227,5602,5603,5605],{"class":229,"line":1098},[227,5604,2190],{"class":336},[227,5606,689],{"class":233},[227,5608,5609,5611,5613,5615,5617,5619,5621,5624,5627,5629],{"class":229,"line":1578},[227,5610,2198],{"class":336},[227,5612,697],{"class":364},[227,5614,368],{"class":336},[227,5616,371],{"class":233},[227,5618,704],{"class":240},[227,5620,2209],{"class":233},[227,5622,5623],{"class":364},"REFRESH_SECRET",[227,5625,5626],{"class":233},", { algorithms: [",[227,5628,404],{"class":346},[227,5630,830],{"class":233},[227,5632,5633,5635,5637,5639,5641,5643,5645,5647,5649],{"class":229,"line":1596},[227,5634,4881],{"class":336},[227,5636,4884],{"class":233},[227,5638,4887],{"class":336},[227,5640,4890],{"class":346},[227,5642,1671],{"class":233},[227,5644,987],{"class":336},[227,5646,990],{"class":336},[227,5648,993],{"class":240},[227,5650,730],{"class":233},[227,5652,5653],{"class":229,"line":1627},[227,5654,5655],{"class":355},"    // OK: issue new access & (optionally) new refresh\n",[227,5657,5658,5660,5662,5664,5666,5668,5671,5673,5676,5679,5682,5684],{"class":229,"line":1632},[227,5659,2198],{"class":336},[227,5661,365],{"class":364},[227,5663,368],{"class":336},[227,5665,371],{"class":233},[227,5667,374],{"class":240},[227,5669,5670],{"class":233},"({ sub: payload.sub, typ: ",[227,5672,4426],{"class":346},[227,5674,5675],{"class":233}," }, ",[227,5677,5678],{"class":364},"ACCESS_SECRET",[227,5680,5681],{"class":233},", { expiresIn: ",[227,5683,410],{"class":346},[227,5685,1777],{"class":233},[227,5687,5688],{"class":229,"line":1637},[227,5689,5690],{"class":355},"    // If rotating, generate a brand‐new refresh here:\n",[227,5692,5693,5695,5698,5700,5702,5704,5706,5708,5710,5712,5714,5716],{"class":229,"line":1643},[227,5694,2198],{"class":336},[227,5696,5697],{"class":364}," newRefresh",[227,5699,368],{"class":336},[227,5701,371],{"class":233},[227,5703,374],{"class":240},[227,5705,5670],{"class":233},[227,5707,4488],{"class":346},[227,5709,5675],{"class":233},[227,5711,5623],{"class":364},[227,5713,5681],{"class":233},[227,5715,4514],{"class":346},[227,5717,1777],{"class":233},[227,5719,5720,5722,5724,5726,5728,5731,5733,5736,5738,5741,5743,5746,5748,5750,5753,5755,5758,5760,5762,5764,5767],{"class":229,"line":1679},[227,5721,5036],{"class":233},[227,5723,545],{"class":240},[227,5725,460],{"class":233},[227,5727,550],{"class":346},[227,5729,5730],{"class":233},", newRefresh, { httpOnly: ",[227,5732,561],{"class":364},[227,5734,5735],{"class":233},", secure: ",[227,5737,561],{"class":364},[227,5739,5740],{"class":233},", path: ",[227,5742,585],{"class":346},[227,5744,5745],{"class":233},", maxAge: ",[227,5747,595],{"class":364},[227,5749,3648],{"class":336},[227,5751,5752],{"class":364},"24",[227,5754,3648],{"class":336},[227,5756,5757],{"class":364},"60",[227,5759,3648],{"class":336},[227,5761,5757],{"class":364},[227,5763,3648],{"class":336},[227,5765,5766],{"class":364},"1000",[227,5768,1777],{"class":233},[227,5770,5771,5773,5775,5777],{"class":229,"line":1703},[227,5772,1755],{"class":336},[227,5774,1104],{"class":233},[227,5776,632],{"class":240},[227,5778,635],{"class":233},[227,5780,5781,5783,5785],{"class":229,"line":1709},[227,5782,2253],{"class":233},[227,5784,738],{"class":336},[227,5786,5787],{"class":233}," (e) {\n",[227,5789,5790,5792,5794,5796,5798,5800],{"class":229,"line":1727},[227,5791,1755],{"class":336},[227,5793,1104],{"class":233},[227,5795,754],{"class":240},[227,5797,460],{"class":233},[227,5799,759],{"class":364},[227,5801,418],{"class":233},[227,5803,5804],{"class":229,"line":1752},[227,5805,1783],{"class":233},[227,5807,5808],{"class":229,"line":1780},[227,5809,2057],{"class":233},[35,5811,5812,5815],{},[38,5813,5814],{},"No DB calls anywhere.",[38,5816,5817,5818,5820,5821,5823],{},"Once a refresh JWT is issued, it lives until its own ",[57,5819,113],{}," passes (or until you rotate ",[57,5822,5623],{}," globally).",[30,5825,5827],{"id":5826},"_62-minimal-state-single-token-version-field","6.2 Minimal State: Single “Token Version” Field",[218,5829,5831],{"className":220,"code":5830,"language":222,"meta":223,"style":223},"// Assume a user table schema: { id, email, passwordHash, refreshVersion }\nimport User from './models/User.js'; // (ORM or raw query, whatever)\n\n// At login:\nasync function loginHandler(req, res) {\n  const { email, password } = req.body;\n  const user = await User.findOne({ where: { email } });\n  if (!user || !compareHash(password, user.passwordHash)) {\n    return res.sendStatus(401);\n  }\n\n  // Issue access\n  const accessToken = jwt.sign(\n    { sub: user.id, typ: 'access' },\n    ACCESS_SECRET,\n    { algorithm: 'HS256', expiresIn: '15m' }\n  );\n\n  // Issue refresh with “ver” equal to user.refreshVersion\n  const refreshToken = jwt.sign(\n    { sub: user.id, typ: 'refresh', ver: user.refreshVersion },\n    REFRESH_SECRET,\n    { algorithm: 'HS256', expiresIn: '30d' }\n  );\n\n  res\n    .cookie('refreshToken', refreshToken, { httpOnly: true, secure: true, path: '/auth/refresh', maxAge: 30*24*60*60*1000 })\n    .json({ accessToken });\n}\n\n// /auth/refresh route:\napp.post('/auth/refresh', async (req, res) => {\n  const token = req.cookies.refreshToken;\n  if (!token) return res.sendStatus(401);\n\n  let payload;\n  try {\n    payload = jwt.verify(token, REFRESH_SECRET, { algorithms: ['HS256'] });\n  } catch {\n    return res.sendStatus(401);\n  }\n  if (payload.typ !== 'refresh') return res.sendStatus(401);\n\n  // 1) Look up user row to check version\n  const user = await User.findByPk(payload.sub);\n  if (!user) return res.sendStatus(401);\n\n  // 2) If token’s ver ≠ user.refreshVersion → it’s been revoked/stale\n  if (payload.ver !== user.refreshVersion) {\n    return res.sendStatus(401);\n  }\n\n  // 3) Everything lines up → issue new tokens, bump version\n  const newAccess = jwt.sign(\n    { sub: user.id, typ: 'access' },\n    ACCESS_SECRET,\n    { algorithm: 'HS256', expiresIn: '15m' }\n  );\n  // bump refreshVersion\n  await user.update({ refreshVersion: user.refreshVersion + 1 });\n\n  // issue a brand‐new refresh with the new version\n  const newRefresh = jwt.sign(\n    { sub: user.id, typ: 'refresh', ver: user.refreshVersion },\n    REFRESH_SECRET,\n    { algorithm: 'HS256', expiresIn: '30d' }\n  );\n\n  res\n    .cookie('refreshToken', newRefresh, { httpOnly: true, secure: true, path: '/auth/refresh', maxAge: 30*24*60*60*1000 })\n    .json({ accessToken: newAccess });\n});\n",[57,5832,5833,5838,5855,5859,5864,5883,5901,5920,5940,5954,5958,5962,5967,5981,5990,5997,6009,6013,6017,6022,6036,6045,6051,6063,6067,6071,6075,6120,6128,6132,6136,6140,6168,6178,6200,6204,6212,6218,6239,6247,6261,6265,6289,6293,6298,6316,6339,6343,6348,6359,6373,6377,6381,6386,6401,6409,6415,6427,6431,6436,6456,6460,6465,6479,6487,6493,6505,6509,6513,6517,6561,6570],{"__ignoreMap":223},[227,5834,5835],{"class":229,"line":230},[227,5836,5837],{"class":355},"// Assume a user table schema: { id, email, passwordHash, refreshVersion }\n",[227,5839,5840,5842,5845,5847,5850,5852],{"class":229,"line":237},[227,5841,337],{"class":336},[227,5843,5844],{"class":233}," User ",[227,5846,343],{"class":336},[227,5848,5849],{"class":346}," './models/User.js'",[227,5851,1490],{"class":233},[227,5853,5854],{"class":355},"// (ORM or raw query, whatever)\n",[227,5856,5857],{"class":229,"line":247},[227,5858,836],{"emptyLinePlaceholder":835},[227,5860,5861],{"class":229,"line":256},[227,5862,5863],{"class":355},"// At login:\n",[227,5865,5866,5868,5870,5873,5875,5877,5879,5881],{"class":229,"line":265},[227,5867,1658],{"class":336},[227,5869,2975],{"class":336},[227,5871,5872],{"class":240}," loginHandler",[227,5874,460],{"class":233},[227,5876,1663],{"class":1589},[227,5878,188],{"class":233},[227,5880,1668],{"class":1589},[227,5882,1593],{"class":233},[227,5884,5885,5887,5889,5891,5893,5895,5897,5899],{"class":229,"line":281},[227,5886,694],{"class":336},[227,5888,1684],{"class":233},[227,5890,1687],{"class":364},[227,5892,188],{"class":233},[227,5894,1692],{"class":364},[227,5896,1695],{"class":233},[227,5898,719],{"class":336},[227,5900,1700],{"class":233},[227,5902,5903,5905,5907,5909,5911,5914,5917],{"class":229,"line":302},[227,5904,694],{"class":336},[227,5906,1714],{"class":364},[227,5908,368],{"class":336},[227,5910,961],{"class":336},[227,5912,5913],{"class":233}," User.",[227,5915,5916],{"class":240},"findOne",[227,5918,5919],{"class":233},"({ where: { email } });\n",[227,5921,5922,5924,5926,5928,5930,5932,5934,5937],{"class":229,"line":621},[227,5923,1730],{"class":336},[227,5925,978],{"class":233},[227,5927,981],{"class":336},[227,5929,1737],{"class":233},[227,5931,1740],{"class":336},[227,5933,1743],{"class":336},[227,5935,5936],{"class":240},"compareHash",[227,5938,5939],{"class":233},"(password, user.passwordHash)) {\n",[227,5941,5942,5944,5946,5948,5950,5952],{"class":229,"line":627},[227,5943,1755],{"class":336},[227,5945,1104],{"class":233},[227,5947,754],{"class":240},[227,5949,460],{"class":233},[227,5951,759],{"class":364},[227,5953,418],{"class":233},[227,5955,5956],{"class":229,"line":1040},[227,5957,1783],{"class":233},[227,5959,5960],{"class":229,"line":1053},[227,5961,836],{"emptyLinePlaceholder":835},[227,5963,5964],{"class":229,"line":1058},[227,5965,5966],{"class":355},"  // Issue access\n",[227,5968,5969,5971,5973,5975,5977,5979],{"class":229,"line":1063},[227,5970,694],{"class":336},[227,5972,365],{"class":364},[227,5974,368],{"class":336},[227,5976,371],{"class":233},[227,5978,374],{"class":240},[227,5980,377],{"class":233},[227,5982,5983,5986,5988],{"class":229,"line":1069},[227,5984,5985],{"class":233},"    { sub: user.id, typ: ",[227,5987,4426],{"class":346},[227,5989,2649],{"class":233},[227,5991,5992,5995],{"class":229,"line":1075},[227,5993,5994],{"class":364},"    ACCESS_SECRET",[227,5996,564],{"class":233},[227,5998,5999,6001,6003,6005,6007],{"class":229,"line":1081},[227,6000,1829],{"class":233},[227,6002,404],{"class":346},[227,6004,407],{"class":233},[227,6006,410],{"class":346},[227,6008,413],{"class":233},[227,6010,6011],{"class":229,"line":1087},[227,6012,1844],{"class":233},[227,6014,6015],{"class":229,"line":1093},[227,6016,836],{"emptyLinePlaceholder":835},[227,6018,6019],{"class":229,"line":1098},[227,6020,6021],{"class":355},"  // Issue refresh with “ver” equal to user.refreshVersion\n",[227,6023,6024,6026,6028,6030,6032,6034],{"class":229,"line":1578},[227,6025,694],{"class":336},[227,6027,452],{"class":364},[227,6029,368],{"class":336},[227,6031,371],{"class":233},[227,6033,374],{"class":240},[227,6035,377],{"class":233},[227,6037,6038,6040,6042],{"class":229,"line":1596},[227,6039,5985],{"class":233},[227,6041,4488],{"class":346},[227,6043,6044],{"class":233},", ver: user.refreshVersion },\n",[227,6046,6047,6049],{"class":229,"line":1627},[227,6048,5509],{"class":364},[227,6050,564],{"class":233},[227,6052,6053,6055,6057,6059,6061],{"class":229,"line":1632},[227,6054,1829],{"class":233},[227,6056,404],{"class":346},[227,6058,407],{"class":233},[227,6060,4514],{"class":346},[227,6062,413],{"class":233},[227,6064,6065],{"class":229,"line":1637},[227,6066,1844],{"class":233},[227,6068,6069],{"class":229,"line":1643},[227,6070,836],{"emptyLinePlaceholder":835},[227,6072,6073],{"class":229,"line":1679},[227,6074,1970],{"class":233},[227,6076,6077,6079,6081,6083,6085,6088,6090,6092,6094,6096,6098,6100,6102,6104,6106,6108,6110,6112,6114,6116,6118],{"class":229,"line":1703},[227,6078,1976],{"class":233},[227,6080,545],{"class":240},[227,6082,460],{"class":233},[227,6084,550],{"class":346},[227,6086,6087],{"class":233},", refreshToken, { httpOnly: ",[227,6089,561],{"class":364},[227,6091,5735],{"class":233},[227,6093,561],{"class":364},[227,6095,5740],{"class":233},[227,6097,585],{"class":346},[227,6099,5745],{"class":233},[227,6101,595],{"class":364},[227,6103,3648],{"class":336},[227,6105,5752],{"class":364},[227,6107,3648],{"class":336},[227,6109,5757],{"class":364},[227,6111,3648],{"class":336},[227,6113,5757],{"class":364},[227,6115,3648],{"class":336},[227,6117,5766],{"class":364},[227,6119,2879],{"class":233},[227,6121,6122,6124,6126],{"class":229,"line":1709},[227,6123,1976],{"class":233},[227,6125,632],{"class":240},[227,6127,635],{"class":233},[227,6129,6130],{"class":229,"line":1727},[227,6131,305],{"class":233},[227,6133,6134],{"class":229,"line":1752},[227,6135,836],{"emptyLinePlaceholder":835},[227,6137,6138],{"class":229,"line":1780},[227,6139,5540],{"class":355},[227,6141,6142,6144,6146,6148,6150,6152,6154,6156,6158,6160,6162,6164,6166],{"class":229,"line":1786},[227,6143,1439],{"class":233},[227,6145,1648],{"class":240},[227,6147,460],{"class":233},[227,6149,585],{"class":346},[227,6151,188],{"class":233},[227,6153,1658],{"class":336},[227,6155,978],{"class":233},[227,6157,1663],{"class":1589},[227,6159,188],{"class":233},[227,6161,1668],{"class":1589},[227,6163,1671],{"class":233},[227,6165,1674],{"class":336},[227,6167,689],{"class":233},[227,6169,6170,6172,6174,6176],{"class":229,"line":1791},[227,6171,694],{"class":336},[227,6173,2130],{"class":364},[227,6175,368],{"class":336},[227,6177,918],{"class":233},[227,6179,6180,6182,6184,6186,6188,6190,6192,6194,6196,6198],{"class":229,"line":1797},[227,6181,1730],{"class":336},[227,6183,978],{"class":233},[227,6185,981],{"class":336},[227,6187,2167],{"class":233},[227,6189,1101],{"class":336},[227,6191,1104],{"class":233},[227,6193,754],{"class":240},[227,6195,460],{"class":233},[227,6197,759],{"class":364},[227,6199,418],{"class":233},[227,6201,6202],{"class":229,"line":1812},[227,6203,836],{"emptyLinePlaceholder":835},[227,6205,6206,6209],{"class":229,"line":1818},[227,6207,6208],{"class":336},"  let",[227,6210,6211],{"class":233}," payload;\n",[227,6213,6214,6216],{"class":229,"line":1826},[227,6215,2190],{"class":336},[227,6217,689],{"class":233},[227,6219,6220,6223,6225,6227,6229,6231,6233,6235,6237],{"class":229,"line":1841},[227,6221,6222],{"class":233},"    payload ",[227,6224,719],{"class":336},[227,6226,371],{"class":233},[227,6228,704],{"class":240},[227,6230,2209],{"class":233},[227,6232,5623],{"class":364},[227,6234,5626],{"class":233},[227,6236,404],{"class":346},[227,6238,830],{"class":233},[227,6240,6241,6243,6245],{"class":229,"line":1847},[227,6242,2253],{"class":233},[227,6244,738],{"class":336},[227,6246,689],{"class":233},[227,6248,6249,6251,6253,6255,6257,6259],{"class":229,"line":1852},[227,6250,1755],{"class":336},[227,6252,1104],{"class":233},[227,6254,754],{"class":240},[227,6256,460],{"class":233},[227,6258,759],{"class":364},[227,6260,418],{"class":233},[227,6262,6263],{"class":229,"line":1858},[227,6264,1783],{"class":233},[227,6266,6267,6269,6271,6273,6275,6277,6279,6281,6283,6285,6287],{"class":229,"line":1887},[227,6268,1730],{"class":336},[227,6270,4884],{"class":233},[227,6272,4887],{"class":336},[227,6274,4890],{"class":346},[227,6276,1671],{"class":233},[227,6278,1101],{"class":336},[227,6280,1104],{"class":233},[227,6282,754],{"class":240},[227,6284,460],{"class":233},[227,6286,759],{"class":364},[227,6288,418],{"class":233},[227,6290,6291],{"class":229,"line":1901},[227,6292,836],{"emptyLinePlaceholder":835},[227,6294,6295],{"class":229,"line":1939},[227,6296,6297],{"class":355},"  // 1) Look up user row to check version\n",[227,6299,6300,6302,6304,6306,6308,6310,6313],{"class":229,"line":1944},[227,6301,694],{"class":336},[227,6303,1714],{"class":364},[227,6305,368],{"class":336},[227,6307,961],{"class":336},[227,6309,5913],{"class":233},[227,6311,6312],{"class":240},"findByPk",[227,6314,6315],{"class":233},"(payload.sub);\n",[227,6317,6318,6320,6322,6324,6327,6329,6331,6333,6335,6337],{"class":229,"line":1956},[227,6319,1730],{"class":336},[227,6321,978],{"class":233},[227,6323,981],{"class":336},[227,6325,6326],{"class":233},"user) ",[227,6328,1101],{"class":336},[227,6330,1104],{"class":233},[227,6332,754],{"class":240},[227,6334,460],{"class":233},[227,6336,759],{"class":364},[227,6338,418],{"class":233},[227,6340,6341],{"class":229,"line":1961},[227,6342,836],{"emptyLinePlaceholder":835},[227,6344,6345],{"class":229,"line":1967},[227,6346,6347],{"class":355},"  // 2) If token’s ver ≠ user.refreshVersion → it’s been revoked/stale\n",[227,6349,6350,6352,6354,6356],{"class":229,"line":1973},[227,6351,1730],{"class":336},[227,6353,5336],{"class":233},[227,6355,4887],{"class":336},[227,6357,6358],{"class":233}," user.refreshVersion) {\n",[227,6360,6361,6363,6365,6367,6369,6371],{"class":229,"line":1988},[227,6362,1755],{"class":336},[227,6364,1104],{"class":233},[227,6366,754],{"class":240},[227,6368,460],{"class":233},[227,6370,759],{"class":364},[227,6372,418],{"class":233},[227,6374,6375],{"class":229,"line":1998},[227,6376,1783],{"class":233},[227,6378,6379],{"class":229,"line":2015},[227,6380,836],{"emptyLinePlaceholder":835},[227,6382,6383],{"class":229,"line":2025},[227,6384,6385],{"class":355},"  // 3) Everything lines up → issue new tokens, bump version\n",[227,6387,6388,6390,6393,6395,6397,6399],{"class":229,"line":2039},[227,6389,694],{"class":336},[227,6391,6392],{"class":364}," newAccess",[227,6394,368],{"class":336},[227,6396,371],{"class":233},[227,6398,374],{"class":240},[227,6400,377],{"class":233},[227,6402,6403,6405,6407],{"class":229,"line":2045},[227,6404,5985],{"class":233},[227,6406,4426],{"class":346},[227,6408,2649],{"class":233},[227,6410,6411,6413],{"class":229,"line":2054},[227,6412,5994],{"class":364},[227,6414,564],{"class":233},[227,6416,6417,6419,6421,6423,6425],{"class":229,"line":2060},[227,6418,1829],{"class":233},[227,6420,404],{"class":346},[227,6422,407],{"class":233},[227,6424,410],{"class":346},[227,6426,413],{"class":233},[227,6428,6429],{"class":229,"line":2065},[227,6430,1844],{"class":233},[227,6432,6433],{"class":229,"line":2071},[227,6434,6435],{"class":355},"  // bump refreshVersion\n",[227,6437,6438,6441,6444,6446,6449,6451,6454],{"class":229,"line":2099},[227,6439,6440],{"class":336},"  await",[227,6442,6443],{"class":233}," user.",[227,6445,1612],{"class":240},[227,6447,6448],{"class":233},"({ refreshVersion: user.refreshVersion ",[227,6450,1932],{"class":336},[227,6452,6453],{"class":364}," 1",[227,6455,1777],{"class":233},[227,6457,6458],{"class":229,"line":2125},[227,6459,836],{"emptyLinePlaceholder":835},[227,6461,6462],{"class":229,"line":2158},[227,6463,6464],{"class":355},"  // issue a brand‐new refresh with the new version\n",[227,6466,6467,6469,6471,6473,6475,6477],{"class":229,"line":2182},[227,6468,694],{"class":336},[227,6470,5697],{"class":364},[227,6472,368],{"class":336},[227,6474,371],{"class":233},[227,6476,374],{"class":240},[227,6478,377],{"class":233},[227,6480,6481,6483,6485],{"class":229,"line":2187},[227,6482,5985],{"class":233},[227,6484,4488],{"class":346},[227,6486,6044],{"class":233},[227,6488,6489,6491],{"class":229,"line":2195},[227,6490,5509],{"class":364},[227,6492,564],{"class":233},[227,6494,6495,6497,6499,6501,6503],{"class":229,"line":2216},[227,6496,1829],{"class":233},[227,6498,404],{"class":346},[227,6500,407],{"class":233},[227,6502,4514],{"class":346},[227,6504,413],{"class":233},[227,6506,6507],{"class":229,"line":2222},[227,6508,1844],{"class":233},[227,6510,6511],{"class":229,"line":2232},[227,6512,836],{"emptyLinePlaceholder":835},[227,6514,6515],{"class":229,"line":2250},[227,6516,1970],{"class":233},[227,6518,6519,6521,6523,6525,6527,6529,6531,6533,6535,6537,6539,6541,6543,6545,6547,6549,6551,6553,6555,6557,6559],{"class":229,"line":2260},[227,6520,1976],{"class":233},[227,6522,545],{"class":240},[227,6524,460],{"class":233},[227,6526,550],{"class":346},[227,6528,5730],{"class":233},[227,6530,561],{"class":364},[227,6532,5735],{"class":233},[227,6534,561],{"class":364},[227,6536,5740],{"class":233},[227,6538,585],{"class":346},[227,6540,5745],{"class":233},[227,6542,595],{"class":364},[227,6544,3648],{"class":336},[227,6546,5752],{"class":364},[227,6548,3648],{"class":336},[227,6550,5757],{"class":364},[227,6552,3648],{"class":336},[227,6554,5757],{"class":364},[227,6556,3648],{"class":336},[227,6558,5766],{"class":364},[227,6560,2879],{"class":233},[227,6562,6563,6565,6567],{"class":229,"line":2275},[227,6564,1976],{"class":233},[227,6566,632],{"class":240},[227,6568,6569],{"class":233},"({ accessToken: newAccess });\n",[227,6571,6572],{"class":229,"line":2280},[227,6573,2057],{"class":233},[35,6575,6576,6582,6588],{},[38,6577,6578,6579,60],{},"One extra read and one extra write (per refresh) to check/increment ",[57,6580,6581],{},"refreshVersion",[38,6583,6584,6585,6587],{},"You get immediate revocation (by incrementing ",[57,6586,6581],{}," if you suspect compromise).",[38,6589,6590],{},"You still do not store every single token string—just a single integer per user. This is often a good middle ground.",[25,6592,6594],{"id":6593},"_7-when-and-why-to-avoid-fully-stateless-refresh","7. When (and Why) to Avoid Fully Stateless Refresh",[30,6596,6598],{"id":6597},"regulatory-or-security-requirements","Regulatory or Security Requirements",[35,6600,6601,6604],{},[38,6602,6603],{},"If regulators require the ability to revoke tokens on demand (e.g. when a user requests “log me out of all devices”), you need some state.",[38,6605,6606],{},"If your app handles highly sensitive data (banking, healthcare), you cannot afford to let a stolen refresh JWT live until expiry.",[30,6608,6610],{"id":6609},"user-experience","User Experience",[35,6612,6613,6616],{},[38,6614,6615],{},"If you want to allow users to “log out — immediately,” you must change something server‐side. A stateless JWT can’t detect a logout until it expires.",[38,6617,6618],{},"If you want to show “Active sessions in other browsers/devices” and let the user revoke them, you need to record each issuance server‐side.",[30,6620,6622],{"id":6621},"granular-token-control","Granular Token Control",[35,6624,6625],{},[38,6626,6627],{},"If you want to revoke a single device’s refresh token (but leave others active), you must track each one separately in a DB or store a per‐token entry somewhere.",[25,6629,6631],{"id":6630},"_8-summary-of-recommendations","8. Summary of Recommendations",[35,6633,6634,6653,6678],{},[38,6635,6636,6639],{},[78,6637,6638],{},"If you truly want zero state (fully stateless):",[35,6640,6641,6644,6647,6650],{},[38,6642,6643],{},"Issue refresh tokens as self-contained JWTs signed with a server‐only key.",[38,6645,6646],{},"Verify them purely by signature+exp.",[38,6648,6649],{},"Accept that you cannot revoke them individually—only rotate your global signing key to kill them all.",[38,6651,6652],{},"Be prepared for replay risk until each token’s exp runs out.",[38,6654,6655,6658],{},[78,6656,6657],{},"If you need minimal state but still low overhead:",[35,6659,6660,6663,6675],{},[38,6661,6662],{},"Store a single integer (“token version”) or a single timestamp (“issuedAt cut‐off”) per user.",[38,6664,6665,6666,1164,6669,6672,6673,60],{},"Embed that version/timestamp in each refresh JWT (",[57,6667,6668],{},"ver",[57,6670,6671],{},"iat",") and check it on ",[57,6674,876],{},[38,6676,6677],{},"This lets you revoke or “expire all previously issued refresh tokens” for that user in one DB write, but still avoids storing every token string.",[38,6679,6680,6683],{},[78,6681,6682],{},"If you need full revocation or per-device tracking:",[35,6684,6685,6691,6694],{},[38,6686,6687,6688,6690],{},"Store each refresh token’s hash (or ",[57,6689,4354],{},") in Redis or SQL with an expiry.",[38,6692,6693],{},"On each refresh, check/rotate/delete that single token record.",[38,6695,6696],{},"This is the most secure, but it requires a stateful lookup on every refresh (and some storage cleanup process).",[15,6698,6699],{},[78,6700,6701],{},"Bottom Line",[15,6703,6704,6705,6707],{},"Stateless (in-memory-free) means your refresh token is a signed JWT with an ",[57,6706,113],{},". The server only checks signature+expiry, never a database. That does achieve full “statelessness,” but you forfeit on-demand revocation or forced logout for individual sessions.",[15,6709,6710,6711,6714],{},"If you can live with “refresh tokens are valid until their built-in expiry, unless you rotate the global signing key,” then a self-contained JWT is fine. Otherwise, introduce some minimal state (e.g. ",[57,6712,6713],{},"token‐version"," per user) to regain revocation capability without a full in‐memory table of tokens.",[15,6716,6717],{},"Choose the approach whose security guarantees and operational overhead best match your application’s risk profile.",[10,6719,6721],{"id":6720},"part-3-layered-defense-against-refresh-token-theft","Part 3: Layered Defense Against Refresh Token Theft",[15,6723,6724],{},"Even with strong standard security measures in place (HttpOnly + Secure + SameSite cookies, one‐time refresh tokens, CSRF tokens on the refresh endpoint, short TTLs, and anomaly detection), there is still a fundamental problem anytime an attacker extracts the raw token from a browser:",[73,6726,6727],{},[15,6728,6729],{},"Once an attacker has the exact string that was issued to a user, nothing in server‐side “require both token and userId” logic will stop them from replaying it.",[15,6731,6732],{},"This section covers where attackers typically succeed, how standard mitigations help, and what additional defenses can be layered on top to make real‐world compromise significantly harder.",[25,6734,6736],{"id":6735},"_1-why-someone-literally-steals-the-cookie-is-the-worstcase-scenario","1. Why “someone literally steals the cookie” is the worst‐case scenario",[35,6738,6739,6742,6752],{},[38,6740,6741],{},"If an attacker exfiltrates the refresh‐token cookie, they already know exactly which user it belongs to (their browser holds that token, and it was set under that user’s session). Asking for an extra user‐ID or CSRF value at refresh time won’t block a malicious script that already controls the victim’s browser context.",[38,6743,6744,6747,6748,6751],{},[78,6745,6746],{},"XSS"," is the most common way cookies get stolen—even with HttpOnly set, there are clever workarounds (e.g. if an attacker can modify the page to submit a hidden form to your refresh endpoint, they can force the user’s browser to send the HttpOnly cookie for them). ",[57,6749,6750],{},"SameSite=Strict"," helps mitigate that, but only if the attacker’s payload can’t run from the same top‐level origin.",[38,6753,6754,6757,6758,6761,6762,6764],{},[78,6755,6756],{},"Phishing"," or “push a malicious browser extension” can simply read the cookie jar or do a ",[57,6759,6760],{},"fetch('/auth/refresh')"," on behalf of the user (sending the HttpOnly cookie automatically). Again, ",[57,6763,6750],{}," only stops cross‐site GET/POST if the browser honors it, but it won’t help if malicious code is already running inside your origin.",[73,6766,6767],{},[15,6768,6769,6772],{},[78,6770,6771],{},"In short:"," once a refresh token is exfiltrated at the client, it is effectively “in the wild” and can be replayed—no server‐side check of userId or visitor_id will stop that.",[25,6774,6776],{"id":6775},"_2-standard-baseline-mitigations","2. Standard Baseline Mitigations",[35,6778,6779,6785,6791,6797,6806],{},[38,6780,6781,6784],{},[78,6782,6783],{},"HttpOnly + Secure + SameSite=Strict cookies","\nThis is the single most important control for protecting tokens from “simple” XSS or cross‐site request forgery.",[38,6786,6787,6790],{},[78,6788,6789],{},"Very short TTL (3–5 days)","\nLimits the window during which a stolen token works.",[38,6792,6793,6796],{},[78,6794,6795],{},"One‐time‐use / revocation flag","\nBy marking each token as invalid once it’s used (and storing that in the DB), you prevent “long‐lived, unrevokable tokens.” If your server sees the same refresh token used twice, you know something’s up.",[38,6798,6799,6802,6803,6805],{},[78,6800,6801],{},"CSRF token on the refresh endpoint","\nIf an attacker tries to trick the user’s browser into POSTing to ",[57,6804,876],{},", they will need to know your dynamically generated CSRF token. (Assuming you’ve stored that token in a secure, same‐site cookie or in a hidden form input.)",[38,6807,6808,6811],{},[78,6809,6810],{},"Additional re‐auth checks for very sensitive routes","\nEmail confirmation or requiring the old password again for “change password” or “change email” actions means that even if someone hijacks a session, they still can’t immediately alter highly‐sensitive settings without that second factor.",[73,6813,6814,6820,6834],{},[15,6815,6816,6819],{},[78,6817,6818],{},"All of these measures collectively shrink the “attack surface.”"," A would‐be attacker now needs to:",[131,6821,6822,6825,6828,6831],{},[38,6823,6824],{},"Run code inside your origin (to bypass SameSite/HttpOnly),",[38,6826,6827],{},"Extract the raw refresh cookie,",[38,6829,6830],{},"Observe the CSRF token or trick the page into including it,",[38,6832,6833],{},"Present the token before it expires or is revoked.",[15,6835,6836],{},"That is orders of magnitude harder than simply “guessing a userId,” so you’re on the right track.",[25,6838,6840],{"id":6839},"_3-where-realworld-attackers-still-succeedand-how-to-make-that-harder","3. Where real‐world attackers still succeed—and how to make that harder",[15,6842,6843],{},"The following additional layers can be implemented. While none are silver bullets, each significantly raises the bar:",[30,6845,6847],{"id":6846},"a-refreshtoken-rotation-untrusted-device-flag","A. Refresh‐token rotation + “untrusted device” flag",[35,6849,6850,6853,6874],{},[38,6851,6852],{},"Rotate on every use (you’re already invalidating each token once it’s used). That means if an attacker steals a valid token, the moment the real user’s app does a “silent” refresh, it will invalidate the stolen token.",[38,6854,6855,6856],{},"In parallel, record fingerprint or “device ID” whenever you issue a token. That could be a hash of:",[35,6857,6858,6861,6868,6871],{},[38,6859,6860],{},"User‐agent string",[38,6862,6863,6864,6867],{},"A random GUID your frontend generated (and stored in ",[57,6865,6866],{},"localStorage",")",[38,6869,6870],{},"IP address (with care—mobile users often change IPs)",[38,6872,6873],{},"Any other non‐spoofable client attribute (e.g. a TLS client certificate or WebAuthn key).",[38,6875,6876,6877,6880,6881],{},"When you generate the original refresh token, save ",[57,6878,6879],{},"device_id = ABC123"," alongside it. Then, on subsequent refresh calls, re‐compute that same “device ID” from the request. If it doesn’t match the value stored in the DB, treat this request as suspicious:",[131,6882,6883,6886],{},[38,6884,6885],{},"Force a full re‐login (throw away the stolen token), or",[38,6887,6888],{},"Send an email “We detected a login from a new device/IP. Was this you?”",[73,6890,6891],{},[15,6892,6893,6894,6896],{},"This way, even if someone copies the raw token, they cannot present the correct “device ID.” They could try to imitate it, but if you choose a fingerprint that’s hard to spoof (e.g. a random GUID stored in ",[57,6895,6866],{},"), you will catch the thief.",[30,6898,6900],{"id":6899},"b-anomaly-detection-riskbased-throttling","B. Anomaly detection & risk‐based throttling",[35,6902,6903,6906,6909],{},[38,6904,6905],{},"Log every refresh attempt (timestamp, IP address, userAgent, device fingerprint).",[38,6907,6908],{},"If your system sees “User 42’s refresh token was used by IP=1.2.3.4 at 3 pm, and then by IP=5.6.7.8 five minutes later,” you can immediately revoke all outstanding refresh tokens for that user.",[38,6910,6911],{},"You can also temporarily lock down the account or force an MFA challenge if there’s unusual behavior (e.g. thousands of refresh attempts in a short time, or a geo-location jump from USA → Europe within minutes).",[30,6913,6915],{"id":6914},"c-offline-revocation-list-forced-re-authentication-after-inactivity","C. Offline revocation list + forced re-authentication after inactivity",[35,6917,6918],{},[38,6919,6920,6921],{},"Even though you mark each refresh token invalid on use, you might also:",[35,6922,6923,6934],{},[38,6924,6925,6926,6929,6930,6933],{},"Keep a global ",[57,6927,6928],{},"userIsLockedOut"," flag in your users table. If you detect any suspicious refresh behavior, set ",[57,6931,6932],{},"users.isLocked = true",". Then on every refresh or API call, you immediately reject until the user explicitly re-logs in (and perhaps changes their password).",[38,6935,6936],{},"Have a short inactivity timer: if the user hasn’t used the app in, say, 24 hours, force a full login—even if their refresh token hasn’t technically expired. This reduces the window for replay.",[30,6938,6940],{"id":6939},"d-device-browser-isolation-with-subdomains","D. Device / browser isolation with subdomains",[35,6942,6943,6954,6961],{},[38,6944,6945,6946,6949,6950,6953],{},"If you serve your front end on ",[57,6947,6948],{},"app.example.com"," but your refresh endpoint runs on ",[57,6951,6952],{},"auth.example.com",", you can limit cookie scope even further.",[38,6955,6956,6957,6960],{},"Issue the refresh cookie with ",[57,6958,6959],{},"Domain=auth.example.com; Path=/; Secure; HttpOnly; SameSite=None"," (or whatever is appropriate).",[38,6962,6963,6964,6966,6967,6970,6971,6974,6975,60],{},"Your JavaScript on ",[57,6965,6948],{}," never sees that cookie at all. To invoke a refresh, you do a hidden ",[57,6968,6969],{},"\u003Ciframe src=\"https://auth.example.com/refresh\">"," or a ",[57,6972,6973],{},"fetch"," with ",[57,6976,6977],{},"credentials: 'include'",[73,6979,6980],{},[15,6981,6982,6983,6985,6986,6988],{},"Even if an XSS vulnerability exists on ",[57,6984,6948],{},", it cannot read or exfiltrate cookies set under ",[57,6987,6952],{},". It can only force the browser to send them, which at least denies the attacker the ability to steal the raw token.",[30,6990,6992],{"id":6991},"e-enforce-mfa-step-up-authentication-for-sensitive-actions","E. Enforce MFA / step-up authentication for sensitive actions",[35,6994,6995],{},[38,6996,6997,6998],{},"You mentioned that password changes require the old password and email verification. You can extend this principle:",[35,6999,7000,7003],{},[38,7001,7002],{},"Every N days, force an MFA re-prompt. E.g. once a week, for any action—even a simple refresh—you require a one-time code.",[38,7004,7005],{},"For any refresh from a new device or new IP, step up to 2FA.",[73,7007,7008],{},[15,7009,7010],{},"That way, even if someone has the raw token, they cannot refresh it from an unfamiliar context without that second factor.",[30,7012,7014],{"id":7013},"f-content-security-policy-csp-xss-hardening","F. Content Security Policy (CSP) / XSS hardening",[35,7016,7017,7034,7037],{},[38,7018,7019,7020,7023,7024,3703,7027,7030,7031,60],{},"Lock down your CSP so that even if an attacker can inject a small ",[57,7021,7022],{},"\u003Cscript>",", they cannot load external JS or steal cookies easily. Use ",[57,7025,7026],{},"script-src 'self'",[57,7028,7029],{},"'nonce-…'",") and ",[57,7032,7033],{},"style-src 'self'",[38,7035,7036],{},"Audit any third-party libs/plugins that might open an XSS vector. A single malicious NPM dependency can bypass HttpOnly by inserting inline scripts that forward cookies as form posts.",[38,7038,7039],{},"Use strict input sanitization / encoding to prevent stored XSS in user profiles or comments. The fewer XSS vectors you have, the harder it is for an attacker to exfiltrate cookies.",[25,7041,7043],{"id":7042},"_4-if-you-absolutely-must-bind-tokens-to-a-specific-user-id","4. If you absolutely must bind tokens to a specific user ID",[15,7045,7046,7047,7050],{},"Requiring the user to send both ",[57,7048,7049],{},"userId + token"," can make automated attacks slightly more cumbersome in theory, but in practice:",[35,7052,7053,7060,7063],{},[38,7054,7055,7056,7059],{},"User ID is often public—it can appear in your JSON API responses, in profile pages, in client-side code, etc. An attacker who already controls a malicious script in the user’s browser can simply read the DOM or make a quick ",[57,7057,7058],{},"GET /api/me"," call to learn the numeric ID.",[38,7061,7062],{},"If an attacker steals token Y from user 42, they already have Y and already know this was issued for user 42. Trying Y with any other ID will fail. Trying Y with “42” will succeed. There’s no need to protect that numerical “42” value; it’s not a secret.",[38,7064,7065,7066,7069],{},"A “",[57,7067,7068],{},"JOIN on token_hash","” query alone is sufficient to prove “token Y belongs to user 42.” You don’t gain anything meaningful by also requiring “AND user_id = 42” in SQL—because the only time SQL will return a row is when both are true. It only increases your code complexity slightly, without significantly enhancing security.",[25,7071,7073],{"id":7072},"_5-summary-a-layered-defense-in-depth-approach","5. Summary: a layered, defense-in-depth approach",[30,7075,7077],{"id":7076},"prevent-theft-in-the-first-place","Prevent theft in the first place",[35,7079,7080,7086,7089],{},[38,7081,7082,7085],{},[57,7083,7084],{},"HttpOnly + Secure + SameSite=Strict"," on your refresh cookie.",[38,7087,7088],{},"Strong CSP, input sanitization, minimal third-party JS.",[38,7090,7091],{},"Frequent security audits of any frontend code that handles tokens.",[30,7093,7095],{"id":7094},"detect-respond-quickly-if-theft-happens","Detect & respond quickly if theft happens",[35,7097,7098,7101,7104],{},[38,7099,7100],{},"Rotate refresh tokens on every use (one-time use) and immediately revoke the old one.",[38,7102,7103],{},"Log “device fingerprint” or IP whenever a refresh occurs; revoke all tokens on anomalies.",[38,7105,7106],{},"Maintain a “user locked” flag if you see suspicious refresh patterns.",[30,7108,7110],{"id":7109},"add-step-up-checks-for-sensitive-contexts","Add step-up checks for sensitive contexts",[35,7112,7113,7116],{},[38,7114,7115],{},"CSRF token on the refresh endpoint (you already have).",[38,7117,7118],{},"Require a fresh MFA or password‐reentry for any “account settings” call.",[30,7120,7122],{"id":7121},"choose-a-single-clear-lookup-mechanism","Choose a single, clear lookup mechanism",[35,7124,7125],{},[38,7126,7127,7128,7131,7132,7134],{},"Either “lookup by token only” (with a unique index), or “lookup by ",[57,7129,7130],{},"token_hash","” alone. In both cases, the attacker who has the raw token can always discover the ",[57,7133,4397],{},". What really matters is that the database will return exactly one row for that token, so you know “this belongs to user N.”",[73,7136,7137],{},[15,7138,7139,7140,7142],{},"Personally, I’d do “lookup by token only” with a UNIQUE constraint on ",[57,7141,7130],{},", because it’s simpler and requires fewer round trips.",[15,7144,7145],{},[78,7146,7147],{},"Bottom‐line",[15,7149,7150],{},"Even if you bind “token + userId” together in your SQL, any attacker who already has the raw token string can trivially learn (or guess) the correct userId. The only way to truly break that chain is to prevent token theft in the first place or to implement strong step-up checks (MFA, email confirmations, device fingerprinting) that invalidate a stolen token the moment it’s used from an unfamiliar environment.",[7152,7153,7154],"style",{},"html pre.shiki code .slsVL, html code.shiki .slsVL{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .shcOC, html code.shiki .shcOC{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .sByVh, html code.shiki .sByVh{--shiki-light:#22863A;--shiki-default:#85E89D;--shiki-dark:#85E89D}html pre.shiki code .sVAnh, html code.shiki .sVAnh{--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-default:#FDAEB7;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .so5gQ, html code.shiki .so5gQ{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sfrk1, html code.shiki .sfrk1{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .sCsY4, html code.shiki .sCsY4{--shiki-light:#6A737D;--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .suiK_, html code.shiki .suiK_{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sQHwn, html code.shiki .sQHwn{--shiki-light:#E36209;--shiki-default:#FFAB70;--shiki-dark:#FFAB70}",{"title":223,"searchDepth":237,"depth":237,"links":7156},[7157,7172],{"id":12,"depth":237,"text":13,"children":7158},[7159,7160,7161,7162,7163,7164,7165,7166,7167,7168,7169,7170,7171],{"id":27,"depth":247,"text":28},{"id":84,"depth":247,"text":85},{"id":153,"depth":247,"text":154},{"id":180,"depth":247,"text":181},{"id":1129,"depth":247,"text":1130},{"id":1151,"depth":247,"text":1152},{"id":1330,"depth":247,"text":1331},{"id":5177,"depth":247,"text":5178},{"id":5239,"depth":247,"text":5240},{"id":5277,"depth":247,"text":5278},{"id":5426,"depth":247,"text":5427},{"id":6593,"depth":247,"text":6594},{"id":6630,"depth":247,"text":6631},{"id":6720,"depth":237,"text":6721,"children":7173},[7174,7175,7176,7177,7178],{"id":6735,"depth":247,"text":6736},{"id":6775,"depth":247,"text":6776},{"id":6839,"depth":247,"text":6840},{"id":7042,"depth":247,"text":7043},{"id":7072,"depth":247,"text":7073},"backend","guide","2026-07-15","An in-depth guide on implementing JSON Web Tokens (JWT) using refresh tokens and private keys.","md",null,"/knowledge-base/auth.jpg",{},"/knowledge-base/jwt","20",{"title":5,"description":7182},{"loc":7187},"knowledge-base/jwt",[3221,7193,7194,7195],"Authentication","Security","Node.js","QglOTZjnVTDEqWWgogimMWP2zN9R2NX1U6d4yeqqeBU",[7198,7202],{"title":7199,"path":7200,"stem":7201,"children":-1},"Jq logs Cheat sheet","/knowledge-base/jq-cheatsheet","knowledge-base/jq-cheatsheet",{"title":7203,"path":7204,"stem":7205,"children":-1},"mTLS CookBook With Caddy","/knowledge-base/mtls","knowledge-base/mtls",1784306722466]