[{"data":1,"prerenderedAt":697},["ShallowReactive",2],{"docs:doc:\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fexpress":3,"docs:pages:docs_en":629},{"id":4,"title":5,"alt":6,"body":7,"description":619,"extension":620,"key":621,"meta":622,"navigation":100,"order":623,"path":624,"placeholder":625,"seo":626,"stem":627,"__hash__":628},"docs_en\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fexpress.md","Express","\u002Fpl\u002Fdocs\u002Fprzewodniki\u002Fframeworki\u002Fexpress",{"type":8,"value":9,"toc":611},"minimark",[10,19,45,48,53,254,268,396,403,422,426,491,508,515,529,533,566,580,584,594,607],[11,12,13,14,18],"p",{},"The other half of every page in this section: the browser has a token, and your\nAPI has to decide what it means. Verification is ",[15,16,17],"strong",{},"offline"," — the token is a\nsigned JWT, your API checks it against your issuer's published key set, and\nnothing is asked of us per request.",[20,21,26],"pre",{"className":22,"code":23,"language":24,"meta":25,"style":25},"language-bash shiki shiki-themes github-light","npm install @kleora-io\u002Fnode\n","bash","",[27,28,29],"code",{"__ignoreMap":25},[30,31,34,38,42],"span",{"class":32,"line":33},"line",1,[30,35,37],{"class":36},"s7eDp","npm",[30,39,41],{"class":40},"sYBdl"," install",[30,43,44],{"class":40}," @kleora-io\u002Fnode\n",[11,46,47],{},"One runtime dependency, a JOSE library. Express and Fastify are optional peers,\nso the package installs with neither.",[49,50,52],"h2",{"id":51},"_1-authenticate-then-authorise","1. Authenticate, then authorise",[20,54,58],{"className":55,"code":56,"language":57,"meta":25,"style":25},"language-ts shiki shiki-themes github-light","import express from 'express'\nimport { express as kleora, requirePermission } from '@kleora-io\u002Fnode'\n\nconst app = express()\n\napp.use(\n  kleora({\n    issuer: process.env.KLEORA_ISSUER!,\n    audience: 'https:\u002F\u002Fapi.example.com',\n  }),\n)\n\napp.get('\u002Finvoices', requirePermission('invoices:read'), (req, res) => {\n  res.json({ tenant: req.kleora.tenant })\n})\n","ts",[27,59,60,76,95,102,121,126,138,147,162,173,179,185,190,236,248],{"__ignoreMap":25},[30,61,62,66,70,73],{"class":32,"line":33},[30,63,65],{"class":64},"sD7c4","import",[30,67,69],{"class":68},"sgsFI"," express ",[30,71,72],{"class":64},"from",[30,74,75],{"class":40}," 'express'\n",[30,77,79,81,84,87,90,92],{"class":32,"line":78},2,[30,80,65],{"class":64},[30,82,83],{"class":68}," { express ",[30,85,86],{"class":64},"as",[30,88,89],{"class":68}," kleora, requirePermission } ",[30,91,72],{"class":64},[30,93,94],{"class":40}," '@kleora-io\u002Fnode'\n",[30,96,98],{"class":32,"line":97},3,[30,99,101],{"emptyLinePlaceholder":100},true,"\n",[30,103,105,108,112,115,118],{"class":32,"line":104},4,[30,106,107],{"class":64},"const",[30,109,111],{"class":110},"sYu0t"," app",[30,113,114],{"class":64}," =",[30,116,117],{"class":36}," express",[30,119,120],{"class":68},"()\n",[30,122,124],{"class":32,"line":123},5,[30,125,101],{"emptyLinePlaceholder":100},[30,127,129,132,135],{"class":32,"line":128},6,[30,130,131],{"class":68},"app.",[30,133,134],{"class":36},"use",[30,136,137],{"class":68},"(\n",[30,139,141,144],{"class":32,"line":140},7,[30,142,143],{"class":36},"  kleora",[30,145,146],{"class":68},"({\n",[30,148,150,153,156,159],{"class":32,"line":149},8,[30,151,152],{"class":68},"    issuer: process.env.",[30,154,155],{"class":110},"KLEORA_ISSUER",[30,157,158],{"class":64},"!",[30,160,161],{"class":68},",\n",[30,163,165,168,171],{"class":32,"line":164},9,[30,166,167],{"class":68},"    audience: ",[30,169,170],{"class":40},"'https:\u002F\u002Fapi.example.com'",[30,172,161],{"class":68},[30,174,176],{"class":32,"line":175},10,[30,177,178],{"class":68},"  }),\n",[30,180,182],{"class":32,"line":181},11,[30,183,184],{"class":68},")\n",[30,186,188],{"class":32,"line":187},12,[30,189,101],{"emptyLinePlaceholder":100},[30,191,193,195,198,201,204,207,210,212,215,218,222,224,227,230,233],{"class":32,"line":192},13,[30,194,131],{"class":68},[30,196,197],{"class":36},"get",[30,199,200],{"class":68},"(",[30,202,203],{"class":40},"'\u002Finvoices'",[30,205,206],{"class":68},", ",[30,208,209],{"class":36},"requirePermission",[30,211,200],{"class":68},[30,213,214],{"class":40},"'invoices:read'",[30,216,217],{"class":68},"), (",[30,219,221],{"class":220},"sqxcx","req",[30,223,206],{"class":68},[30,225,226],{"class":220},"res",[30,228,229],{"class":68},") ",[30,231,232],{"class":64},"=>",[30,234,235],{"class":68}," {\n",[30,237,239,242,245],{"class":32,"line":238},14,[30,240,241],{"class":68},"  res.",[30,243,244],{"class":36},"json",[30,246,247],{"class":68},"({ tenant: req.kleora.tenant })\n",[30,249,251],{"class":32,"line":250},15,[30,252,253],{"class":68},"})\n",[11,255,256,257,260,261,263,264,267],{},"The middleware verifies the token and sets ",[27,258,259],{},"req.kleora"," to its claims.\n",[27,262,209],{}," reads ",[27,265,266],{},"req.kleora.permissions"," — which is in the token\nalready, so the check is a list lookup rather than a call to us.",[269,270,273,286,356,359,382],"callout",{"title":271,"type":272},"That snippet needs one line of TypeScript from you","note",[11,274,275,277,278,281,282,285],{},[27,276,259],{}," is not part of Express's own ",[27,279,280],{},"Request"," type, and this package\ndeliberately declares no global augmentation — it imports no framework, and a\npackage that widened ",[27,283,284],{},"express.Request"," for everyone who installed it would be\nreaching outside itself. Either add the augmentation yourself:",[20,287,289],{"className":55,"code":288,"language":57,"meta":25,"style":25},"declare global {\n  namespace Express {\n    interface Request {\n      kleora?: import('@kleora-io\u002Fnode').Claims\n    }\n  }\n}\n",[27,290,291,299,309,319,341,346,351],{"__ignoreMap":25},[30,292,293,296],{"class":32,"line":33},[30,294,295],{"class":64},"declare",[30,297,298],{"class":68}," global {\n",[30,300,301,304,307],{"class":32,"line":78},[30,302,303],{"class":64},"  namespace",[30,305,306],{"class":36}," Express",[30,308,235],{"class":68},[30,310,311,314,317],{"class":32,"line":97},[30,312,313],{"class":64},"    interface",[30,315,316],{"class":36}," Request",[30,318,235],{"class":68},[30,320,321,324,327,330,332,335,338],{"class":32,"line":104},[30,322,323],{"class":220},"      kleora",[30,325,326],{"class":64},"?:",[30,328,329],{"class":64}," import",[30,331,200],{"class":68},[30,333,334],{"class":40},"'@kleora-io\u002Fnode'",[30,336,337],{"class":68},").",[30,339,340],{"class":36},"Claims\n",[30,342,343],{"class":32,"line":123},[30,344,345],{"class":68},"    }\n",[30,347,348],{"class":32,"line":128},[30,349,350],{"class":68},"  }\n",[30,352,353],{"class":32,"line":140},[30,354,355],{"class":68},"}\n",[11,357,358],{},"or use the framework-free form, which needs none:",[20,360,362],{"className":55,"code":361,"language":57,"meta":25,"style":25},"const claims = await authenticate(req, { issuer, audience })\n",[27,363,364],{"__ignoreMap":25},[30,365,366,368,371,373,376,379],{"class":32,"line":33},[30,367,107],{"class":64},[30,369,370],{"class":110}," claims",[30,372,114],{"class":64},[30,374,375],{"class":64}," await",[30,377,378],{"class":36}," authenticate",[30,380,381],{"class":68},"(req, { issuer, audience })\n",[11,383,384,387,388,391,392,395],{},[27,385,386],{},"authenticate()"," takes anything with a ",[27,389,390],{},"headers.authorization"," property, so it\nworks with any Node framework. ",[27,393,394],{},"KleoraRequest"," is exported for the shape the\nmiddleware reads and writes.",[49,397,399,400],{"id":398},"_2-what-to-pass-as-audience","2. What to pass as ",[27,401,402],{},"audience",[11,404,405,406,409,410,413,414,417,418,421],{},"The ",[27,407,408],{},"aud"," claim of an access token is ",[27,411,412],{},"[client_id]",", or\n",[27,415,416],{},"[client_id, api_audience]"," once you set an API audience on the environment. A\nfresh environment has none, so pass the ",[15,419,420],{},"client id"," until you do, and your own\nAPI identifier afterwards.",[49,423,425],{"id":424},"_3-the-three-answers-a-refusal-can-have","3. The three answers a refusal can have",[427,428,429,442],"table",{},[430,431,432],"thead",{},[433,434,435,439],"tr",{},[436,437,438],"th",{},"Situation",[436,440,441],{},"Answer",[443,444,445,465,478],"tbody",{},[433,446,447,455],{},[448,449,450,451,454],"td",{},"No ",[27,452,453],{},"Authorization"," header, or a token that did not verify",[448,456,457,460,461,464],{},[27,458,459],{},"401"," ",[27,462,463],{},"unauthenticated"," problem document",[433,466,467,470],{},[448,468,469],{},"Verified, but the permission is absent",[448,471,472,460,475],{},[27,473,474],{},"403",[27,476,477],{},"insufficient-permission",[433,479,480,483],{},[448,481,482],{},"The issuer's key set cannot be read at all",[448,484,485,460,488],{},[27,486,487],{},"503",[27,489,490],{},"issuer-unavailable",[11,492,405,493,495,496,500,501,504,505,507],{},[27,494,459],{}," body says only that the request was not authenticated. The ",[497,498,499],"em",{},"reason"," —\nexpired, wrong audience, bad signature — goes to the ",[27,502,503],{},"logger"," option, which is\nwhere it is useful to you and of no use to somebody probing. The ",[27,506,487],{}," matters\nmore than it looks: without it, our own outage would tell every one of your\ncallers that their token is bad and send them back to an issuer that is already\ndown.",[11,509,510,511,514],{},"Anything else is your service being misconfigured, and the package does not\nchoose a status for it: Express gets ",[27,512,513],{},"next(error)"," and your own error handler\nanswers.",[11,516,517,519,520,522,523,525,526,528],{},[27,518,209],{}," answers ",[27,521,459],{}," rather than ",[27,524,474],{}," when ",[27,527,259],{}," is\nabsent entirely — a route that forgot the authentication middleware is not a\npermissions problem.",[49,530,532],{"id":531},"_4-what-the-verifier-actually-checks","4. What the verifier actually checks",[11,534,405,535,538,539,542,543,546,547,550,551,553,554,557,558,561,562,565],{},[27,536,537],{},"typ"," header is ",[27,540,541],{},"at+jwt",", the ",[27,544,545],{},"ES256"," signature against the issuer's key\nset, ",[27,548,549],{},"iss"," exactly, ",[27,552,408],{}," containing your audience, ",[27,555,556],{},"exp"," and ",[27,559,560],{},"iat"," within 60\nseconds of tolerance, and that the token's ",[27,563,564],{},"env"," claim agrees with the issuer\nyou configured — so a sandbox token cannot quietly satisfy a production API.",[11,567,568,569,572,573,576,577,579],{},"The key set is fetched once per issuer and cached for as long as its\n",[27,570,571],{},"Cache-Control"," says. A ",[27,574,575],{},"kid"," the cache has not seen triggers one refetch, rate\nlimited to one a minute; a fetch that fails serves the cached set until twice\nits TTL and then fails closed. Signing keys are published before they are used,\nso a rotation mid-flight finds the ",[27,578,575],{}," already cached.",[49,581,583],{"id":582},"fastify-and-everything-else","Fastify, and everything else",[11,585,586,589,590,593],{},[27,587,588],{},"fastify({ issuer, audience })"," is the same plugin, decorating\n",[27,591,592],{},"request.kleora"," and giving the same three answers. It is registered\nun-encapsulated, because registering an authentication plugin at the root never\nmeans \"only the routes inside this plugin\".",[11,595,596,597,600,601,606],{},"For a Python API, ",[27,598,599],{},"kleora"," does the same verification with the same\nrules —\n",[602,603,605],"a",{"href":604},"\u002Fen\u002Fdocs\u002Fgetting-started\u002Finstallation","Installing an SDK"," has it.",[608,609,610],"style",{},"html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}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 pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}",{"title":25,"searchDepth":97,"depth":97,"links":612},[613,614,616,617,618],{"id":51,"depth":78,"text":52},{"id":398,"depth":78,"text":615},"2. What to pass as audience",{"id":424,"depth":78,"text":425},{"id":531,"depth":78,"text":532},{"id":582,"depth":78,"text":583},"Verifying the access token on your own API — middleware, permissions, and the three answers a refusal can have.","md","guides\u002Fquickstarts\u002Fexpress",{},40,"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fexpress",false,{"title":5,"description":619},"en\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fexpress","oGGJvoXT7V-qg83FrxjbAm7RdSqDPKvTKvcwsVKs4E4",[630,635,639,644,650,655,660,665,666,671,676,681,687,692],{"path":631,"title":632,"description":633,"order":175,"key":634},"\u002Fen\u002Fdocs\u002Fgetting-started","Getting started","From an empty project to a working sign-in, and the packages that get you there.","getting-started\u002Findex",{"path":604,"title":605,"description":636,"order":637,"key":638},"The five packages we publish, what each one is for, and the configuration each one takes.",20,"getting-started\u002Finstallation",{"path":640,"title":641,"description":642,"order":175,"key":643},"\u002Fen\u002Fdocs\u002Fgetting-started\u002Fquickstart","Quickstart","Create an App, wire up the browser SDK, and sign in for the first time — entirely in sandbox.","getting-started\u002Fquickstart",{"path":645,"title":646,"description":647,"order":648,"key":649},"\u002Fen\u002Fdocs\u002Fguides\u002Farchitecture","How Kleora is put together","The shape of the hosted service — one address per environment, two API surfaces, and where state lives. And why self-hosting is not something we support today.",30,"guides\u002Farchitecture",{"path":651,"title":652,"description":653,"order":637,"key":654},"\u002Fen\u002Fdocs\u002Fguides\u002Fconcepts","Workspaces, roles and environments","The five words the product is built from, which of them your tokens carry, and how a change to a role reaches a running application.","guides\u002Fconcepts",{"path":656,"title":657,"description":658,"order":637,"key":659},"\u002Fen\u002Fdocs\u002Fguides","Guides","Wiring Kleora into your stack, the words the product uses, and how the hosted service is put together.","guides\u002Findex",{"path":661,"title":662,"description":663,"order":175,"key":664},"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fbrowser","Plain browser","One client instance, one callback route, and an access token for every request your application makes.","guides\u002Fquickstarts\u002Fbrowser",{"path":624,"title":5,"description":619,"order":623,"key":621},{"path":667,"title":668,"description":669,"order":175,"key":670},"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts","Framework quickstarts","One page per stack — Next.js, Nuxt, Express and the plain browser — and the two redirect URIs all of them share.","guides\u002Fquickstarts\u002Findex",{"path":672,"title":673,"description":674,"order":637,"key":675},"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fnextjs","Next.js","The browser SDK in an App Router application — a lazy client, a callback route, and token verification in a route handler.","guides\u002Fquickstarts\u002Fnextjs",{"path":677,"title":678,"description":679,"order":648,"key":680},"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fnuxt","Nuxt","The Nuxt module — two lines of config, a callback page you do not write, and a route middleware that protects a page from its own meta.","guides\u002Fquickstarts\u002Fnuxt",{"path":682,"title":683,"description":684,"order":685,"key":686},"\u002Fen\u002Fdocs","Kleora documentation","Add hosted sign-in to your application, verify the token on your API, and manage users, workspaces and roles from one console.",0,"index",{"path":688,"title":689,"description":690,"order":175,"key":691},"\u002Fen\u002Fdocs\u002Freference\u002Fcode-highlighting","Code highlighting","One fence per preloaded Shiki grammar — a fixture, not a reference page.","reference\u002Fcode-highlighting",{"path":693,"title":694,"description":695,"order":648,"key":696},"\u002Fen\u002Fdocs\u002Freference","Reference","Reference material for the HTTP APIs and the SDK packages.","reference\u002Findex",1790698197769]