[{"data":1,"prerenderedAt":682},["ShallowReactive",2],{"nav-categories":3,"tag-jwt":8},{"pinned":4,"overflow":7},[5,6],"Technology","Security",[],[9],{"id":10,"title":11,"body":12,"category":6,"date":666,"description":667,"extension":668,"image":669,"imageCredit":670,"imageCreditUrl":670,"meta":671,"navigation":189,"path":673,"public":189,"seo":674,"stem":675,"tags":676,"__hash__":681},"posts\u002Fblog\u002Frsa-jwt-nodejs.md","Sign and verify JWT with RSA encryption with NodeJS",{"type":13,"value":14,"toc":654},"minimark",[15,19,22,27,35,42,61,64,68,79,95,100,103,112,121,132,135,146,156,160,166,282,293,302,305,368,468,477,481,484,568,581,585,588,618,629,633,636,639,643,650],[16,17,18],"p",{},"JWT (JSON Web Token) is a compact, self-contained transmission standard for\nsecurely passing claims between parties using JavaScript object notation. It\noperates on the basis of an optional signature (JWS) or optional encryption\n(JWE). This article focuses on JWS, specifically using asymmetric RSA keys with\nthe SHA-256 hashing algorithm to sign and verify tokens.",[16,20,21],{},"A common use case for JWT is stateless client authentication so that servers do\nnot need to rely on stateful sessions. A token is issued once at login, and\nsubsequent requests carry it as proof of identity. Because the token is\ncryptographically signed, the server can verify its authenticity without\nconsulting a database or session store. This only scratches the surface though,\nas JWTs also enable service-to-service interoperability without shared state.",[23,24,26],"h2",{"id":25},"anatomy-of-a-jwt","Anatomy of a JWT",[16,28,29,30,34],{},"A JWT consists of three Base64URL-encoded segments separated by dots:\n",[31,32,33],"code",{},"header.payload.signature",".",[16,36,37,38,41],{},"The header declares the token type and which algorithm was used to produce the\nsignature. For RSA-based signing this will typically be ",[31,39,40],{},"RS256",", meaning RSA\nsignature with SHA-256.",[16,43,44,45,48,49,52,53,56,57,60],{},"The payload carries the claims, which are statements about the entity (usually\nthe user) and any additional metadata. Standard registered claims include ",[31,46,47],{},"iss","\n(issuer), ",[31,50,51],{},"sub"," (subject), ",[31,54,55],{},"aud"," (audience), and ",[31,58,59],{},"exp"," (expiration time as a\nUnix timestamp). You are free to include custom claims alongside these, though\nkeeping payloads lean is good practice since the token travels with every\nrequest.",[16,62,63],{},"The signature is produced by signing the encoded header and payload with the\nprivate key. Anyone holding the corresponding public key can verify the\nsignature without being able to forge a new one. This asymmetry is what makes\nRSA particularly useful for distributed systems where the verifier and the\nissuer are separate services.",[23,65,67],{"id":66},"getting-started","Getting Started",[16,69,70,71,78],{},"We will use the popular\n",[72,73,77],"a",{"href":74,"rel":75},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fjsonwebtoken",[76],"nofollow","jsonwebtoken"," library for signing\nand verification.",[80,81,86],"pre",{"className":82,"code":83,"language":84,"meta":85,"style":85},"language-sh shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","npm install jsonwebtoken\n","sh","",[31,87,88],{"__ignoreMap":85},[89,90,93],"span",{"class":91,"line":92},"line",1,[89,94,83],{},[96,97,99],"h3",{"id":98},"generating-the-key-pair","Generating the Key Pair",[16,101,102],{},"First, generate an RSA private key and derive the public key from it. We use\n3072 bits here as a sensible modern default that balances security and\nperformance.",[80,104,106],{"className":82,"code":105,"language":84,"meta":85,"style":85},"openssl genrsa -out private-key.pem 3072\n",[31,107,108],{"__ignoreMap":85},[89,109,110],{"class":91,"line":92},[89,111,105],{},[80,113,115],{"className":82,"code":114,"language":84,"meta":85,"style":85},"openssl rsa -in private-key.pem -pubout -out public-key.pem\n",[31,116,117],{"__ignoreMap":85},[89,118,119],{"class":91,"line":92},[89,120,114],{},[16,122,123,124,127,128,131],{},"A quick note on security hygiene: even with testing keys, you should never\ncommit private keys to version control. A habit I have developed is to store\ninstance-relative files in a directory called ",[31,125,126],{},"instance\u002F"," at the root of the\npackage and ensure that directory is covered by ",[31,129,130],{},".gitignore",". You do not need to\nfollow this exact pattern, but do ensure your private key is excluded from any\nrepository.",[16,133,134],{},"File permissions matter too. Ensure only the file owner can read the key.",[80,136,140],{"className":137,"code":138,"language":139,"meta":85,"style":85},"language-shell shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","chmod 600 private-key.pem\n","shell",[31,141,142],{"__ignoreMap":85},[89,143,144],{"class":91,"line":92},[89,145,138],{},[147,148,149],"blockquote",{},[16,150,151,152,155],{},"If using hosting options like EC2, permission requirements may vary due to\nprovider constraints on users. In that context ",[31,153,154],{},"chmod 0400"," (read-only for\nowner) may be more appropriate.",[23,157,159],{"id":158},"signing-a-token","Signing a Token",[16,161,162,163,165],{},"With the keys in place, we can sign a token. The ",[31,164,77],{}," library handles\nheader construction internally based on the options you provide, so you only\nneed to supply the payload, the private key, and the signing options.",[80,167,171],{"className":168,"code":169,"language":170,"meta":85,"style":85},"language-javascript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","const fs = require(\"fs\");\nconst jwt = require(\"jsonwebtoken\");\n\nconst privkey = fs.readFileSync(\"instance\u002Fprivate-key.pem\", \"utf-8\");\n\nconst payload = {\n  iss: \"my-auth-service\",\n  sub: \"user-12345\",\n  aud: \"my-application\",\n};\n\nconst options = {\n  algorithm: \"RS256\",\n  expiresIn: \"12h\",\n};\n\nconst token = jwt.sign(payload, privkey, options);\n\nconsole.log(token);\n","javascript",[31,172,173,178,184,191,197,202,208,214,220,226,232,237,243,249,255,260,265,271,276],{"__ignoreMap":85},[89,174,175],{"class":91,"line":92},[89,176,177],{},"const fs = require(\"fs\");\n",[89,179,181],{"class":91,"line":180},2,[89,182,183],{},"const jwt = require(\"jsonwebtoken\");\n",[89,185,187],{"class":91,"line":186},3,[89,188,190],{"emptyLinePlaceholder":189},true,"\n",[89,192,194],{"class":91,"line":193},4,[89,195,196],{},"const privkey = fs.readFileSync(\"instance\u002Fprivate-key.pem\", \"utf-8\");\n",[89,198,200],{"class":91,"line":199},5,[89,201,190],{"emptyLinePlaceholder":189},[89,203,205],{"class":91,"line":204},6,[89,206,207],{},"const payload = {\n",[89,209,211],{"class":91,"line":210},7,[89,212,213],{},"  iss: \"my-auth-service\",\n",[89,215,217],{"class":91,"line":216},8,[89,218,219],{},"  sub: \"user-12345\",\n",[89,221,223],{"class":91,"line":222},9,[89,224,225],{},"  aud: \"my-application\",\n",[89,227,229],{"class":91,"line":228},10,[89,230,231],{},"};\n",[89,233,235],{"class":91,"line":234},11,[89,236,190],{"emptyLinePlaceholder":189},[89,238,240],{"class":91,"line":239},12,[89,241,242],{},"const options = {\n",[89,244,246],{"class":91,"line":245},13,[89,247,248],{},"  algorithm: \"RS256\",\n",[89,250,252],{"class":91,"line":251},14,[89,253,254],{},"  expiresIn: \"12h\",\n",[89,256,258],{"class":91,"line":257},15,[89,259,231],{},[89,261,263],{"class":91,"line":262},16,[89,264,190],{"emptyLinePlaceholder":189},[89,266,268],{"class":91,"line":267},17,[89,269,270],{},"const token = jwt.sign(payload, privkey, options);\n",[89,272,274],{"class":91,"line":273},18,[89,275,190],{"emptyLinePlaceholder":189},[89,277,279],{"class":91,"line":278},19,[89,280,281],{},"console.log(token);\n",[16,283,284,285,288,289,292],{},"The output will be a string beginning with ",[31,286,287],{},"eyJ",", which is the Base64URL\nencoding of ",[31,290,291],{},"{\"alg\"...",". The three dot-separated segments are visible in the raw\ntoken.",[80,294,296],{"className":82,"code":295,"language":84,"meta":85,"style":85},"eyJhbGciOiJSUzI1NiIs***.eyJpc3MiOi***.signature***\n",[31,297,298],{"__ignoreMap":85},[89,299,300],{"class":91,"line":92},[89,301,295],{},[16,303,304],{},"If we decode the first two segments (without verifying) we can see the\nstructure:",[80,306,310],{"className":307,"code":308,"language":309,"meta":85,"style":85},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"alg\": \"RS256\",\n  \"typ\": \"JWT\"\n}\n","json",[31,311,312,318,344,363],{"__ignoreMap":85},[89,313,314],{"class":91,"line":92},[89,315,317],{"class":316},"sMK4o","{\n",[89,319,320,323,327,330,333,336,339,341],{"class":91,"line":180},[89,321,322],{"class":316},"  \"",[89,324,326],{"class":325},"spNyl","alg",[89,328,329],{"class":316},"\"",[89,331,332],{"class":316},":",[89,334,335],{"class":316}," \"",[89,337,40],{"class":338},"sfazB",[89,340,329],{"class":316},[89,342,343],{"class":316},",\n",[89,345,346,348,351,353,355,357,360],{"class":91,"line":186},[89,347,322],{"class":316},[89,349,350],{"class":325},"typ",[89,352,329],{"class":316},[89,354,332],{"class":316},[89,356,335],{"class":316},[89,358,359],{"class":338},"JWT",[89,361,362],{"class":316},"\"\n",[89,364,365],{"class":91,"line":193},[89,366,367],{"class":316},"}\n",[80,369,371],{"className":307,"code":370,"language":309,"meta":85,"style":85},"{\n  \"iss\": \"my-auth-service\",\n  \"sub\": \"user-12345\",\n  \"aud\": \"my-application\",\n  \"iat\": 1674353419,\n  \"exp\": 1674396619\n}\n",[31,372,373,377,396,415,434,451,464],{"__ignoreMap":85},[89,374,375],{"class":91,"line":92},[89,376,317],{"class":316},[89,378,379,381,383,385,387,389,392,394],{"class":91,"line":180},[89,380,322],{"class":316},[89,382,47],{"class":325},[89,384,329],{"class":316},[89,386,332],{"class":316},[89,388,335],{"class":316},[89,390,391],{"class":338},"my-auth-service",[89,393,329],{"class":316},[89,395,343],{"class":316},[89,397,398,400,402,404,406,408,411,413],{"class":91,"line":186},[89,399,322],{"class":316},[89,401,51],{"class":325},[89,403,329],{"class":316},[89,405,332],{"class":316},[89,407,335],{"class":316},[89,409,410],{"class":338},"user-12345",[89,412,329],{"class":316},[89,414,343],{"class":316},[89,416,417,419,421,423,425,427,430,432],{"class":91,"line":193},[89,418,322],{"class":316},[89,420,55],{"class":325},[89,422,329],{"class":316},[89,424,332],{"class":316},[89,426,335],{"class":316},[89,428,429],{"class":338},"my-application",[89,431,329],{"class":316},[89,433,343],{"class":316},[89,435,436,438,441,443,445,449],{"class":91,"line":199},[89,437,322],{"class":316},[89,439,440],{"class":325},"iat",[89,442,329],{"class":316},[89,444,332],{"class":316},[89,446,448],{"class":447},"sbssI"," 1674353419",[89,450,343],{"class":316},[89,452,453,455,457,459,461],{"class":91,"line":204},[89,454,322],{"class":316},[89,456,59],{"class":325},[89,458,329],{"class":316},[89,460,332],{"class":316},[89,462,463],{"class":447}," 1674396619\n",[89,465,466],{"class":91,"line":210},[89,467,367],{"class":316},[16,469,470,471,473,474,476],{},"Notice that ",[31,472,440],{}," (issued at) was added automatically by the library, and ",[31,475,59],{},"\nwas calculated as 12 hours from the issue time. These are both Unix timestamps\nin seconds.",[23,478,480],{"id":479},"verifying-a-token","Verifying a Token",[16,482,483],{},"Verification is where the public key comes in. Any service holding the public\nkey can verify that a token was signed by the corresponding private key without\never needing access to that private key. This separation is the entire point of\nasymmetric cryptography in this context.",[80,485,487],{"className":168,"code":486,"language":170,"meta":85,"style":85},"const fs = require(\"fs\");\nconst jwt = require(\"jsonwebtoken\");\n\nconst pubkey = fs.readFileSync(\"instance\u002Fpublic-key.pem\", \"utf-8\");\n\nconst token = \"eyJhbGciOi...\"; \u002F\u002F token received from client\n\ntry {\n  const decoded = jwt.verify(token, pubkey, {\n    algorithms: [\"RS256\"],\n    issuer: \"my-auth-service\",\n    audience: \"my-application\",\n  });\n  console.log(\"Token is valid:\", decoded);\n} catch (err) {\n  console.error(\"Token verification failed:\", err.message);\n}\n",[31,488,489,493,497,501,506,510,515,519,524,529,534,539,544,549,554,559,564],{"__ignoreMap":85},[89,490,491],{"class":91,"line":92},[89,492,177],{},[89,494,495],{"class":91,"line":180},[89,496,183],{},[89,498,499],{"class":91,"line":186},[89,500,190],{"emptyLinePlaceholder":189},[89,502,503],{"class":91,"line":193},[89,504,505],{},"const pubkey = fs.readFileSync(\"instance\u002Fpublic-key.pem\", \"utf-8\");\n",[89,507,508],{"class":91,"line":199},[89,509,190],{"emptyLinePlaceholder":189},[89,511,512],{"class":91,"line":204},[89,513,514],{},"const token = \"eyJhbGciOi...\"; \u002F\u002F token received from client\n",[89,516,517],{"class":91,"line":210},[89,518,190],{"emptyLinePlaceholder":189},[89,520,521],{"class":91,"line":216},[89,522,523],{},"try {\n",[89,525,526],{"class":91,"line":222},[89,527,528],{},"  const decoded = jwt.verify(token, pubkey, {\n",[89,530,531],{"class":91,"line":228},[89,532,533],{},"    algorithms: [\"RS256\"],\n",[89,535,536],{"class":91,"line":234},[89,537,538],{},"    issuer: \"my-auth-service\",\n",[89,540,541],{"class":91,"line":239},[89,542,543],{},"    audience: \"my-application\",\n",[89,545,546],{"class":91,"line":245},[89,547,548],{},"  });\n",[89,550,551],{"class":91,"line":251},[89,552,553],{},"  console.log(\"Token is valid:\", decoded);\n",[89,555,556],{"class":91,"line":257},[89,557,558],{},"} catch (err) {\n",[89,560,561],{"class":91,"line":262},[89,562,563],{},"  console.error(\"Token verification failed:\", err.message);\n",[89,565,566],{"class":91,"line":267},[89,567,367],{},[16,569,570,571,574,575,577,578,580],{},"The ",[31,572,573],{},"verify"," function does several things simultaneously. It checks that the\nsignature is valid against the public key, that the token has not expired, and\nthat the ",[31,576,47],{}," and ",[31,579,55],{}," claims match what we expect. If any of these checks\nfail, it throws an error rather than returning a decoded payload. Always wrap\nverification in a try\u002Fcatch and treat failures as authentication failures.",[96,582,584],{"id":583},"common-verification-failures","Common Verification Failures",[16,586,587],{},"A few scenarios that will cause verification to throw:",[589,590,591,598,601,611],"ul",{},[592,593,594,595,597],"li",{},"The token has expired (current time is past ",[31,596,59],{},")",[592,599,600],{},"The signature does not match (token was tampered with or signed by a different\nkey)",[592,602,570,603,606,607,610],{},[31,604,605],{},"issuer"," or ",[31,608,609],{},"audience"," options do not match the claims in the token",[592,612,613,614,617],{},"The algorithm in the token header does not match the ",[31,615,616],{},"algorithms"," whitelist",[16,619,620,621,624,625,628],{},"Specifying ",[31,622,623],{},"algorithms: [\"RS256\"]"," is important. Without it, an attacker could\npotentially craft a token with ",[31,626,627],{},"\"alg\": \"none\""," or switch to a symmetric\nalgorithm and trick the verifier into using the public key as an HMAC secret.\nAlways explicitly whitelist the algorithms you expect.",[23,630,632],{"id":631},"putting-it-together","Putting It Together",[16,634,635],{},"In a real application, the signing typically happens in an authentication\nservice at login time, and the verification happens in middleware on every\nsubsequent request. The private key lives only on the auth service, while the\npublic key can be distributed freely to any service that needs to validate\ntokens.",[16,637,638],{},"This pattern scales well because adding a new service that needs to verify\nidentity only requires sharing the public key. No shared secrets, no session\nstores, no database lookups on every request.",[23,640,642],{"id":641},"key-rotation","Key Rotation",[16,644,645,646,649],{},"One consideration worth mentioning is key rotation. Private keys should be\nrotated periodically as a matter of security hygiene. When you rotate, you need\na brief overlap period where both the old and new public keys are accepted for\nverification, since tokens signed with the old key may still be valid (not yet\nexpired). The ",[31,647,648],{},"kid"," (key ID) header parameter exists precisely for this purpose,\nallowing verifiers to look up the correct public key for a given token.",[651,652,653],"style",{},"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 .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":85,"searchDepth":180,"depth":180,"links":655},[656,657,660,661,664,665],{"id":25,"depth":180,"text":26},{"id":66,"depth":180,"text":67,"children":658},[659],{"id":98,"depth":186,"text":99},{"id":158,"depth":180,"text":159},{"id":479,"depth":180,"text":480,"children":662},[663],{"id":583,"depth":186,"text":584},{"id":631,"depth":180,"text":632},{"id":641,"depth":180,"text":642},"2023\u002F06\u002F05","Using RSA asymmetric keys with NodeJS to create a private key signed JWT and verify it with the corresponding public key.","md","\u002Fimages\u002Fblog\u002Fjwt-image.png",null,{"updated":672},"2023\u002F06\u002F08","\u002Fblog\u002Frsa-jwt-nodejs",{"title":11,"description":667},"blog\u002Frsa-jwt-nodejs",[677,678,679,680],"rsa","technology","jwt","encryption","7nA2ft7qBd5Oi4x0xQOl9SrcgQqQFAf5Jtin6WTHPUc",1778958297100]