[{"data":1,"prerenderedAt":1359},["ShallowReactive",2],{"navigation":3,"\u002Fecosystem\u002Fmulti-tenant-idp":105,"\u002Fecosystem\u002Fmulti-tenant-idp-surround":1354},[4,44,79,92],{"title":5,"path":6,"stem":7,"children":8,"icon":43},"Getting Started","\u002Fgetting-started","1.getting-started\u002F1.index",[9,11,15,19,23,27,31,35,39],{"title":10,"path":6,"stem":7},"Introduction",{"title":12,"path":13,"stem":14},"Quick Start: Service Provider","\u002Fgetting-started\u002Fquickstart-sp","1.getting-started\u002F2.quickstart-sp",{"title":16,"path":17,"stem":18},"Quick Start: Identity Provider","\u002Fgetting-started\u002Fquickstart-idp","1.getting-started\u002F3.quickstart-idp",{"title":20,"path":21,"stem":22},"Quick Start: Agent","\u002Fgetting-started\u002Fquickstart-agent","1.getting-started\u002F4.quickstart-agent",{"title":24,"path":25,"stem":26},"Quick Start","\u002Fgetting-started\u002Finstallation","1.getting-started\u002F5.installation",{"title":28,"path":29,"stem":30},"How It Works","\u002Fgetting-started\u002Fhow-it-works","1.getting-started\u002F6.how-it-works",{"title":32,"path":33,"stem":34},"For Service Providers","\u002Fgetting-started\u002Ffor-service-providers","1.getting-started\u002F7.for-service-providers",{"title":36,"path":37,"stem":38},"CLI (apes & ape-shell)","\u002Fgetting-started\u002Fcli","1.getting-started\u002F8.cli",{"title":40,"path":41,"stem":42},"Free-IdP Hosting Guide","\u002Fgetting-started\u002Ffree-idp-hosting","1.getting-started\u002F9.free-idp-hosting",false,{"title":45,"path":46,"stem":47,"children":48,"icon":43},"Ecosystem","\u002Fecosystem","2.ecosystem\u002F1.index",[49,51,55,59,63,67,71,75],{"title":50,"path":46,"stem":47},"Overview",{"title":52,"path":53,"stem":54},"OpenApe Auth","\u002Fecosystem\u002Fauth","2.ecosystem\u002F2.auth",{"title":56,"path":57,"stem":58},"OpenApe Grants","\u002Fecosystem\u002Fgrants","2.ecosystem\u002F3.grants",{"title":60,"path":61,"stem":62},"nuxt-auth-sp","\u002Fecosystem\u002Fnuxt-auth-sp","2.ecosystem\u002F4.nuxt-auth-sp",{"title":64,"path":65,"stem":66},"escapes","\u002Fecosystem\u002Fescapes","2.ecosystem\u002F5.escapes",{"title":68,"path":69,"stem":70},"nuxt-auth-idp","\u002Fecosystem\u002Fnuxt-auth-idp","2.ecosystem\u002F6.nuxt-auth-idp",{"title":72,"path":73,"stem":74},"Multi-Tenant IdP","\u002Fecosystem\u002Fmulti-tenant-idp","2.ecosystem\u002F7.multi-tenant-idp",{"title":76,"path":77,"stem":78},"Agent Recipe","\u002Fecosystem\u002Fagent-recipe","2.ecosystem\u002F8.agent-recipe",{"title":80,"icon":43,"path":81,"stem":82,"children":83,"page":43},"Security","\u002Fsecurity","3.security",[84,88],{"title":85,"path":86,"stem":87},"Compliance","\u002Fsecurity\u002Fcompliance","3.security\u002F1.compliance",{"title":89,"path":90,"stem":91},"Threat Model","\u002Fsecurity\u002Fthreat-model","3.security\u002F2.threat-model",{"title":93,"icon":43,"path":94,"stem":95,"children":96,"page":43},"Guides","\u002Fguides","4.guides",[97,101],{"title":98,"path":99,"stem":100},"Capabilities & Grants","\u002Fguides\u002Fcapabilities","4.guides\u002F1.capabilities",{"title":102,"path":103,"stem":104},"Delegation","\u002Fguides\u002Fdelegation","4.guides\u002F2.delegation",{"id":106,"title":72,"body":107,"description":1348,"extension":1349,"links":1350,"meta":1351,"navigation":499,"path":73,"seo":1352,"stem":74,"__hash__":1353},"docs\u002F2.ecosystem\u002F7.multi-tenant-idp.md",{"type":108,"value":109,"toc":1333},"minimark",[110,114,134,145,150,170,201,205,208,255,268,272,277,283,410,413,431,435,446,1004,1019,1026,1032,1054,1069,1080,1084,1090,1169,1176,1180,1201,1204,1222,1232,1236,1239,1259,1263,1266,1309,1312,1316,1329],[111,112,72],"h1",{"id":113},"multi-tenant-idp",[115,116,117,121,122,125,126,129,130,133],"p",{},[118,119,120],"code",{},"@openape\u002Fnuxt-auth-idp"," can host more than one identity origin from a single deployment. A request to ",[118,123,124],{},"id.acme.com"," and a request to ",[118,127,128],{},"id.acme.at"," hit the same Nitro process, share the same database, and see the same users — but WebAuthn passkeys, OAuth issuers, and origin checks are scoped per request according to the incoming ",[118,131,132],{},"Host"," header.",[115,135,136,137,140,141,144],{},"This is the pattern ",[118,138,139],{},"id.openape.ai"," and ",[118,142,143],{},"id.openape.at"," use today (one instance, both hostnames). It also lets you bring up a new tenant with an nginx vhost and a DNS record, no redeploy.",[146,147,149],"h2",{"id":148},"why","Why",[115,151,152,153,156,157,160,161,164,165,169],{},"WebAuthn passkeys are intrinsically bound to the Relying Party ID (",[118,154,155],{},"rp.id",") they were created against. A passkey registered at ",[118,158,159],{},"https:\u002F\u002Fid.acme.com\u002F"," cannot be used at ",[118,162,163],{},"https:\u002F\u002Fid.acme.at\u002F"," — browsers enforce that at the credential level. So \"multi-tenant\" here does ",[166,167,168],"strong",{},"not"," mean one passkey crosses hosts; it means:",[171,172,173,177,191,198],"ul",{},[174,175,176],"li",{},"One deployment, one DB, one user table.",[174,178,179,180,182,183,185,186,182,188,190],{},"The same email can own credentials on both ",[118,181,124],{}," (rp_id = ",[118,184,124],{},") and ",[118,187,128],{},[118,189,128],{},"), each usable only on its own origin.",[174,192,193,194,197],{},"Issued JWTs carry the ",[118,195,196],{},"iss"," claim of the hostname the user logged in through.",[174,199,200],{},"Adding a new hostname is a nginx\u002FDNS task, not a code change.",[146,202,204],{"id":203},"how-it-works","How it works",[115,206,207],{},"The module resolves Relying-Party config through a precedence chain:",[209,210,211,221,244],"ol",{},[174,212,213,216,217,220],{},[166,214,215],{},"Per-request tenant config"," — ",[118,218,219],{},"event.context.openapeRpConfig"," (populated by your middleware).",[174,222,223,216,226,229,230,229,233,229,236,239,240,243],{},[166,224,225],{},"Static module config",[118,227,228],{},"openapeIdp.rpID",", ",[118,231,232],{},"rpOrigin",[118,234,235],{},"rpName",[118,237,238],{},"issuer"," from ",[118,241,242],{},"nuxt.config.ts",".",[174,245,246,216,249,229,252,243],{},[166,247,248],{},"Dev fallback",[118,250,251],{},"rpID = 'localhost'",[118,253,254],{},"origin = http:\u002F\u002Flocalhost:3000",[115,256,257,258,260,261,264,265,267],{},"Your middleware inspects each request, matches the ",[118,259,132],{}," header against an allow-list, and writes a typed tenant config on ",[118,262,263],{},"event.context",". All WebAuthn endpoints and the OAuth ",[118,266,196],{}," minter read from that seam first.",[146,269,271],{"id":270},"configuration","Configuration",[273,274,276],"h3",{"id":275},"_1-declare-your-allow-list","1. Declare your allow-list",[115,278,279,280,282],{},"In ",[118,281,242],{},":",[284,285,290],"pre",{"className":286,"code":287,"language":288,"meta":289,"style":289},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","export default defineNuxtConfig({\n  modules: ['@openape\u002Fnuxt-auth-idp'],\n  openapeIdp: {\n    rpHostAllowList: 'id.acme.com,id.acme.at',\n    rpName: 'Acme Identity',\n    \u002F\u002F rpID\u002FrpOrigin\u002Fissuer left empty — resolved per request\n  },\n})\n","ts","",[118,291,292,316,342,353,371,388,395,401],{"__ignoreMap":289},[293,294,297,301,304,308,312],"span",{"class":295,"line":296},"line",1,[293,298,300],{"class":299},"s7zQu","export",[293,302,303],{"class":299}," default",[293,305,307],{"class":306},"s2Zo4"," defineNuxtConfig",[293,309,311],{"class":310},"sTEyZ","(",[293,313,315],{"class":314},"sMK4o","{\n",[293,317,319,323,325,328,331,334,336,339],{"class":295,"line":318},2,[293,320,322],{"class":321},"swJcz","  modules",[293,324,282],{"class":314},[293,326,327],{"class":310}," [",[293,329,330],{"class":314},"'",[293,332,120],{"class":333},"sfazB",[293,335,330],{"class":314},[293,337,338],{"class":310},"]",[293,340,341],{"class":314},",\n",[293,343,345,348,350],{"class":295,"line":344},3,[293,346,347],{"class":321},"  openapeIdp",[293,349,282],{"class":314},[293,351,352],{"class":314}," {\n",[293,354,356,359,361,364,367,369],{"class":295,"line":355},4,[293,357,358],{"class":321},"    rpHostAllowList",[293,360,282],{"class":314},[293,362,363],{"class":314}," '",[293,365,366],{"class":333},"id.acme.com,id.acme.at",[293,368,330],{"class":314},[293,370,341],{"class":314},[293,372,374,377,379,381,384,386],{"class":295,"line":373},5,[293,375,376],{"class":321},"    rpName",[293,378,282],{"class":314},[293,380,363],{"class":314},[293,382,383],{"class":333},"Acme Identity",[293,385,330],{"class":314},[293,387,341],{"class":314},[293,389,391],{"class":295,"line":390},6,[293,392,394],{"class":393},"sHwdD","    \u002F\u002F rpID\u002FrpOrigin\u002Fissuer left empty — resolved per request\n",[293,396,398],{"class":295,"line":397},7,[293,399,400],{"class":314},"  },\n",[293,402,404,407],{"class":295,"line":403},8,[293,405,406],{"class":314},"}",[293,408,409],{"class":310},")\n",[115,411,412],{},"Or via env:",[284,414,418],{"className":415,"code":416,"language":417,"meta":289,"style":289},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","NUXT_OPENAPE_IDP_RP_HOST_ALLOW_LIST=id.acme.com,id.acme.at\n","bash",[118,419,420],{"__ignoreMap":289},[293,421,422,425,428],{"class":295,"line":296},[293,423,424],{"class":310},"NUXT_OPENAPE_IDP_RP_HOST_ALLOW_LIST",[293,426,427],{"class":314},"=",[293,429,430],{"class":333},"id.acme.com,id.acme.at\n",[273,432,434],{"id":433},"_2-add-a-tenant-middleware","2. Add a tenant middleware",[115,436,437,438,441,442,445],{},"Create ",[118,439,440],{},"server\u002Fmiddleware\u002F00.rp-tenant.ts"," in your IdP app (the leading ",[118,443,444],{},"00."," ensures it runs before any handler):",[284,447,449],{"className":286,"code":448,"language":288,"meta":289,"style":289},"import { getRequestHost } from 'h3'\nimport { useRuntimeConfig } from '#imports'\n\nexport default defineEventHandler((event) => {\n  const config = useRuntimeConfig()\n  const idpCfg = (config.openapeIdp || {}) as Record\u003Cstring, unknown>\n  const raw = (idpCfg.rpHostAllowList as string | undefined) || ''\n  const allow = raw.split(',').map(s => s.trim()).filter(Boolean)\n\n  const host = getRequestHost(event, { xForwardedHost: true })?.split(':')[0]\n  if (!host || !allow.includes(host)) return\n\n  const origin = `https:\u002F\u002F${host}`\n  event.context.openapeRpConfig = {\n    rpName: (idpCfg.rpName as string | undefined) || 'OpenApe Identity Server',\n    rpID: host,\n    origin,\n    requireUserVerification: idpCfg.requireUserVerification ?? false,\n    residentKey: idpCfg.residentKey ?? 'preferred',\n    attestationType: idpCfg.attestationType ?? 'none',\n  }\n  event.context.openapeIssuer = origin\n})\n",[118,450,451,475,495,501,527,543,594,633,694,699,755,792,797,821,841,877,889,897,920,946,972,978,997],{"__ignoreMap":289},[293,452,453,456,459,462,465,468,470,472],{"class":295,"line":296},[293,454,455],{"class":299},"import",[293,457,458],{"class":314}," {",[293,460,461],{"class":310}," getRequestHost",[293,463,464],{"class":314}," }",[293,466,467],{"class":299}," from",[293,469,363],{"class":314},[293,471,273],{"class":333},[293,473,474],{"class":314},"'\n",[293,476,477,479,481,484,486,488,490,493],{"class":295,"line":318},[293,478,455],{"class":299},[293,480,458],{"class":314},[293,482,483],{"class":310}," useRuntimeConfig",[293,485,464],{"class":314},[293,487,467],{"class":299},[293,489,363],{"class":314},[293,491,492],{"class":333},"#imports",[293,494,474],{"class":314},[293,496,497],{"class":295,"line":344},[293,498,500],{"emptyLinePlaceholder":499},true,"\n",[293,502,503,505,507,510,512,514,518,521,525],{"class":295,"line":355},[293,504,300],{"class":299},[293,506,303],{"class":299},[293,508,509],{"class":306}," defineEventHandler",[293,511,311],{"class":310},[293,513,311],{"class":314},[293,515,517],{"class":516},"sHdIc","event",[293,519,520],{"class":314},")",[293,522,524],{"class":523},"spNyl"," =>",[293,526,352],{"class":314},[293,528,529,532,535,538,540],{"class":295,"line":373},[293,530,531],{"class":523},"  const",[293,533,534],{"class":310}," config",[293,536,537],{"class":314}," =",[293,539,483],{"class":306},[293,541,542],{"class":321},"()\n",[293,544,545,547,550,552,555,558,560,563,566,569,572,575,579,582,585,588,591],{"class":295,"line":390},[293,546,531],{"class":523},[293,548,549],{"class":310}," idpCfg",[293,551,537],{"class":314},[293,553,554],{"class":321}," (",[293,556,557],{"class":310},"config",[293,559,243],{"class":314},[293,561,562],{"class":310},"openapeIdp",[293,564,565],{"class":314}," ||",[293,567,568],{"class":314}," {}",[293,570,571],{"class":321},") ",[293,573,574],{"class":299},"as",[293,576,578],{"class":577},"sBMFI"," Record",[293,580,581],{"class":314},"\u003C",[293,583,584],{"class":577},"string",[293,586,587],{"class":314},",",[293,589,590],{"class":577}," unknown",[293,592,593],{"class":314},">\n",[293,595,596,598,601,603,605,608,610,613,616,619,622,625,627,630],{"class":295,"line":397},[293,597,531],{"class":523},[293,599,600],{"class":310}," raw",[293,602,537],{"class":314},[293,604,554],{"class":321},[293,606,607],{"class":310},"idpCfg",[293,609,243],{"class":314},[293,611,612],{"class":310},"rpHostAllowList",[293,614,615],{"class":299}," as",[293,617,618],{"class":577}," string",[293,620,621],{"class":314}," |",[293,623,624],{"class":577}," undefined",[293,626,571],{"class":321},[293,628,629],{"class":314},"||",[293,631,632],{"class":314}," ''\n",[293,634,635,637,640,642,644,646,649,651,653,655,657,659,661,664,666,669,671,674,676,679,682,684,687,689,692],{"class":295,"line":403},[293,636,531],{"class":523},[293,638,639],{"class":310}," allow",[293,641,537],{"class":314},[293,643,600],{"class":310},[293,645,243],{"class":314},[293,647,648],{"class":306},"split",[293,650,311],{"class":321},[293,652,330],{"class":314},[293,654,587],{"class":333},[293,656,330],{"class":314},[293,658,520],{"class":321},[293,660,243],{"class":314},[293,662,663],{"class":306},"map",[293,665,311],{"class":321},[293,667,668],{"class":516},"s",[293,670,524],{"class":523},[293,672,673],{"class":310}," s",[293,675,243],{"class":314},[293,677,678],{"class":306},"trim",[293,680,681],{"class":321},"())",[293,683,243],{"class":314},[293,685,686],{"class":306},"filter",[293,688,311],{"class":321},[293,690,691],{"class":310},"Boolean",[293,693,409],{"class":321},[293,695,697],{"class":295,"line":696},9,[293,698,500],{"emptyLinePlaceholder":499},[293,700,702,704,707,709,711,713,715,717,719,722,724,728,730,732,735,737,739,741,743,745,748,752],{"class":295,"line":701},10,[293,703,531],{"class":523},[293,705,706],{"class":310}," host",[293,708,537],{"class":314},[293,710,461],{"class":306},[293,712,311],{"class":321},[293,714,517],{"class":310},[293,716,587],{"class":314},[293,718,458],{"class":314},[293,720,721],{"class":321}," xForwardedHost",[293,723,282],{"class":314},[293,725,727],{"class":726},"sfNiH"," true",[293,729,464],{"class":314},[293,731,520],{"class":321},[293,733,734],{"class":314},"?.",[293,736,648],{"class":306},[293,738,311],{"class":321},[293,740,330],{"class":314},[293,742,282],{"class":333},[293,744,330],{"class":314},[293,746,747],{"class":321},")[",[293,749,751],{"class":750},"sbssI","0",[293,753,754],{"class":321},"]\n",[293,756,758,761,763,766,769,771,774,777,779,782,784,786,789],{"class":295,"line":757},11,[293,759,760],{"class":299},"  if",[293,762,554],{"class":321},[293,764,765],{"class":314},"!",[293,767,768],{"class":310},"host",[293,770,565],{"class":314},[293,772,773],{"class":314}," !",[293,775,776],{"class":310},"allow",[293,778,243],{"class":314},[293,780,781],{"class":306},"includes",[293,783,311],{"class":321},[293,785,768],{"class":310},[293,787,788],{"class":321},")) ",[293,790,791],{"class":299},"return\n",[293,793,795],{"class":295,"line":794},12,[293,796,500],{"emptyLinePlaceholder":499},[293,798,800,802,805,807,810,813,816,818],{"class":295,"line":799},13,[293,801,531],{"class":523},[293,803,804],{"class":310}," origin",[293,806,537],{"class":314},[293,808,809],{"class":314}," `",[293,811,812],{"class":333},"https:\u002F\u002F",[293,814,815],{"class":314},"${",[293,817,768],{"class":310},[293,819,820],{"class":314},"}`\n",[293,822,824,827,829,832,834,837,839],{"class":295,"line":823},14,[293,825,826],{"class":310},"  event",[293,828,243],{"class":314},[293,830,831],{"class":310},"context",[293,833,243],{"class":314},[293,835,836],{"class":310},"openapeRpConfig",[293,838,537],{"class":314},[293,840,352],{"class":314},[293,842,844,846,848,850,852,854,856,858,860,862,864,866,868,870,873,875],{"class":295,"line":843},15,[293,845,376],{"class":321},[293,847,282],{"class":314},[293,849,554],{"class":321},[293,851,607],{"class":310},[293,853,243],{"class":314},[293,855,235],{"class":310},[293,857,615],{"class":299},[293,859,618],{"class":577},[293,861,621],{"class":314},[293,863,624],{"class":577},[293,865,571],{"class":321},[293,867,629],{"class":314},[293,869,363],{"class":314},[293,871,872],{"class":333},"OpenApe Identity Server",[293,874,330],{"class":314},[293,876,341],{"class":314},[293,878,880,883,885,887],{"class":295,"line":879},16,[293,881,882],{"class":321},"    rpID",[293,884,282],{"class":314},[293,886,706],{"class":310},[293,888,341],{"class":314},[293,890,892,895],{"class":295,"line":891},17,[293,893,894],{"class":310},"    origin",[293,896,341],{"class":314},[293,898,900,903,905,907,909,912,915,918],{"class":295,"line":899},18,[293,901,902],{"class":321},"    requireUserVerification",[293,904,282],{"class":314},[293,906,549],{"class":310},[293,908,243],{"class":314},[293,910,911],{"class":310},"requireUserVerification",[293,913,914],{"class":314}," ??",[293,916,917],{"class":726}," false",[293,919,341],{"class":314},[293,921,923,926,928,930,932,935,937,939,942,944],{"class":295,"line":922},19,[293,924,925],{"class":321},"    residentKey",[293,927,282],{"class":314},[293,929,549],{"class":310},[293,931,243],{"class":314},[293,933,934],{"class":310},"residentKey",[293,936,914],{"class":314},[293,938,363],{"class":314},[293,940,941],{"class":333},"preferred",[293,943,330],{"class":314},[293,945,341],{"class":314},[293,947,949,952,954,956,958,961,963,965,968,970],{"class":295,"line":948},20,[293,950,951],{"class":321},"    attestationType",[293,953,282],{"class":314},[293,955,549],{"class":310},[293,957,243],{"class":314},[293,959,960],{"class":310},"attestationType",[293,962,914],{"class":314},[293,964,363],{"class":314},[293,966,967],{"class":333},"none",[293,969,330],{"class":314},[293,971,341],{"class":314},[293,973,975],{"class":295,"line":974},21,[293,976,977],{"class":314},"  }\n",[293,979,981,983,985,987,989,992,994],{"class":295,"line":980},22,[293,982,826],{"class":310},[293,984,243],{"class":314},[293,986,831],{"class":310},[293,988,243],{"class":314},[293,990,991],{"class":310},"openapeIssuer",[293,993,537],{"class":314},[293,995,996],{"class":310}," origin\n",[293,998,1000,1002],{"class":295,"line":999},23,[293,1001,406],{"class":314},[293,1003,409],{"class":310},[1005,1006,1008],"callout",{"type":1007},"warning",[115,1009,1010,1011,1014,1015,1018],{},"The allow-list is a ",[166,1012,1013],{},"security boundary",". Without it, a malicious ",[118,1016,1017],{},"Host: attacker.test"," header could bind a freshly-created credential to an attacker-chosen RP. Only hostnames present in the allow-list are promoted; everything else falls through to the static module config.",[273,1020,1022,1023],{"id":1021},"_3-schema-scope-credentials-by-rp_id","3. Schema: scope credentials by ",[118,1024,1025],{},"rp_id",[115,1027,1028,1029,1031],{},"The WebAuthn tables gain an ",[118,1030,1025],{}," column so the IdP can present only the credentials that match the current request's RP:",[284,1033,1037],{"className":1034,"code":1035,"language":1036,"meta":289,"style":289},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","ALTER TABLE credentials            ADD COLUMN rp_id TEXT;\nALTER TABLE webauthn_challenges    ADD COLUMN rp_id TEXT;\nCREATE INDEX idx_credentials_rp_id ON credentials(rp_id);\n","sql",[118,1038,1039,1044,1049],{"__ignoreMap":289},[293,1040,1041],{"class":295,"line":296},[293,1042,1043],{},"ALTER TABLE credentials            ADD COLUMN rp_id TEXT;\n",[293,1045,1046],{"class":295,"line":318},[293,1047,1048],{},"ALTER TABLE webauthn_challenges    ADD COLUMN rp_id TEXT;\n",[293,1050,1051],{"class":295,"line":344},[293,1052,1053],{},"CREATE INDEX idx_credentials_rp_id ON credentials(rp_id);\n",[115,1055,1056,1057,1064,1065,1068],{},"In the reference ",[1058,1059,1063],"a",{"href":1060,"rel":1061},"https:\u002F\u002Fgithub.com\u002Fopenape-ai\u002Fopenape\u002Ftree\u002Fmain\u002Fapps\u002Fopenape-free-idp",[1062],"nofollow","openape-free-idp",", this is added idempotently at startup in ",[118,1066,1067],{},"server\u002Fplugins\u002F02.database.ts",". Existing rows are backfilled to a default RP (e.g. your canonical hostname) so pre-multi-tenant credentials keep working.",[115,1070,1071,1072,1075,1076,1079],{},"The ",[118,1073,1074],{},"CredentialStore"," exposes an optional ",[118,1077,1078],{},"findByUserAndRp(email, rpId)"," that every WebAuthn handler calls before registration options, login options, and verification — making cross-RP credential leakage impossible at the data layer.",[273,1081,1083],{"id":1082},"_4-nginx-vhost-per-tenant","4. nginx vhost per tenant",[115,1085,1086,1087,1089],{},"Each tenant is a plain vhost that forwards ",[118,1088,132],{}," to the shared Nitro port:",[284,1091,1095],{"className":1092,"code":1093,"language":1094,"meta":289,"style":289},"language-nginx shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","server {\n  listen 443 ssl http2;\n  server_name id.acme.at;\n\n  ssl_certificate     \u002Fetc\u002Fletsencrypt\u002Flive\u002Fid.acme.com\u002Ffullchain.pem;\n  ssl_certificate_key \u002Fetc\u002Fletsencrypt\u002Flive\u002Fid.acme.com\u002Fprivkey.pem;\n\n  location \u002F {\n    proxy_pass http:\u002F\u002F127.0.0.1:3003;\n    proxy_set_header Host $host;\n    proxy_set_header X-Forwarded-Host $host;\n    proxy_set_header X-Forwarded-Proto $scheme;\n    # …plus standard proxy headers\n  }\n}\n","nginx",[118,1096,1097,1102,1107,1112,1116,1121,1126,1130,1135,1140,1145,1150,1155,1160,1164],{"__ignoreMap":289},[293,1098,1099],{"class":295,"line":296},[293,1100,1101],{},"server {\n",[293,1103,1104],{"class":295,"line":318},[293,1105,1106],{},"  listen 443 ssl http2;\n",[293,1108,1109],{"class":295,"line":344},[293,1110,1111],{},"  server_name id.acme.at;\n",[293,1113,1114],{"class":295,"line":355},[293,1115,500],{"emptyLinePlaceholder":499},[293,1117,1118],{"class":295,"line":373},[293,1119,1120],{},"  ssl_certificate     \u002Fetc\u002Fletsencrypt\u002Flive\u002Fid.acme.com\u002Ffullchain.pem;\n",[293,1122,1123],{"class":295,"line":390},[293,1124,1125],{},"  ssl_certificate_key \u002Fetc\u002Fletsencrypt\u002Flive\u002Fid.acme.com\u002Fprivkey.pem;\n",[293,1127,1128],{"class":295,"line":397},[293,1129,500],{"emptyLinePlaceholder":499},[293,1131,1132],{"class":295,"line":403},[293,1133,1134],{},"  location \u002F {\n",[293,1136,1137],{"class":295,"line":696},[293,1138,1139],{},"    proxy_pass http:\u002F\u002F127.0.0.1:3003;\n",[293,1141,1142],{"class":295,"line":701},[293,1143,1144],{},"    proxy_set_header Host $host;\n",[293,1146,1147],{"class":295,"line":757},[293,1148,1149],{},"    proxy_set_header X-Forwarded-Host $host;\n",[293,1151,1152],{"class":295,"line":794},[293,1153,1154],{},"    proxy_set_header X-Forwarded-Proto $scheme;\n",[293,1156,1157],{"class":295,"line":799},[293,1158,1159],{},"    # …plus standard proxy headers\n",[293,1161,1162],{"class":295,"line":823},[293,1163,977],{},[293,1165,1166],{"class":295,"line":843},[293,1167,1168],{},"}\n",[115,1170,1171,1172,1175],{},"Use a single expanded Let's Encrypt cert for every tenant hostname — ",[118,1173,1174],{},"certbot --nginx --expand -d id.acme.com -d id.acme.at"," — or issue one per tenant.",[146,1177,1179],{"id":1178},"issuing-tokens","Issuing tokens",[115,1181,1182,1183,1186,1187,1189,1190,1193,1194,1196,1197,1200],{},"When ",[118,1184,1185],{},"event.context.openapeIssuer"," is set, the OAuth token minter uses that value as the ",[118,1188,196],{}," claim instead of the static ",[118,1191,1192],{},"openapeIdp.issuer",". A user who logs in at ",[118,1195,163],{}," therefore receives a JWT with ",[118,1198,1199],{},"iss = \"https:\u002F\u002Fid.acme.at\""," even though the process itself knows both hostnames.",[115,1202,1203],{},"Consumers (service providers, grant verifiers) should either:",[171,1205,1206,1212],{},[174,1207,1208,1209,1211],{},"Accept a known allow-list of ",[118,1210,196],{}," values, or",[174,1213,1214,1215,1218,1219,1221],{},"Use ",[1058,1216,1217],{"href":29},"DDISA discovery"," to validate ",[118,1220,196],{}," against the token subject's domain.",[115,1223,1224,1227,1228,1231],{},[118,1225,1226],{},"jose.jwtVerify()"," accepts ",[118,1229,1230],{},"issuer: string | string[]",", so adding multi-iss acceptance to a service provider is a one-line change.",[146,1233,1235],{"id":1234},"adding-a-new-tenant","Adding a new tenant",[115,1237,1238],{},"Once the pattern is in place, onboarding a new hostname is:",[209,1240,1241,1244,1253,1256],{},[174,1242,1243],{},"Add DNS record pointing to the deploy host.",[174,1245,1246,1247,1249,1250,1252],{},"Add hostname to ",[118,1248,612],{}," (env or ",[118,1251,242],{},") and redeploy.",[174,1254,1255],{},"Add nginx vhost + expand the LE cert to cover the new SAN.",[174,1257,1258],{},"Done — users can now create passkeys scoped to the new RP.",[146,1260,1262],{"id":1261},"migrating-data-from-an-existing-single-tenant-idp","Migrating data from an existing single-tenant IdP",[115,1264,1265],{},"If you already run a separate IdP for a second hostname and want to collapse it into a multi-tenant deployment:",[209,1267,1268,1271,1303,1306],{},[174,1269,1270],{},"Export the old DB.",[174,1272,1273,1274,229,1277,229,1280,229,1283,229,1286,229,1289,1292,1293,1296,1297,140,1299,1302],{},"Import ",[118,1275,1276],{},"users",[118,1278,1279],{},"credentials",[118,1281,1282],{},"ssh_keys",[118,1284,1285],{},"registration_urls",[118,1287,1288],{},"signing_keys",[118,1290,1291],{},"grants"," into the multi-tenant DB, stamping ",[118,1294,1295],{},"rp_id = \u003Cold-hostname>"," on every ",[118,1298,1279],{},[118,1300,1301],{},"webauthn_challenges"," row.",[174,1304,1305],{},"On email collisions, keep the canonical row and attach the imported credentials to it (they're keyed by email in the FKs).",[174,1307,1308],{},"Flip DNS for the old hostname at the new deploy host and expand the TLS cert.",[115,1310,1311],{},"Users whose passkeys were registered against the old hostname keep using them — the rp_id scoping presents only the credentials valid for the request's RP. SSH keys work against both hostnames because they're email-keyed and have no RP binding.",[146,1313,1315],{"id":1314},"related","Related",[171,1317,1318,1323],{},[174,1319,1320,1322],{},[1058,1321,68],{"href":69}," — Module options and store interfaces",[174,1324,1325,1328],{},[1058,1326,1327],{"href":53},"Auth Overview"," — Authentication modes (WebAuthn, SSH, federated)",[1330,1331,1332],"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 .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":289,"searchDepth":296,"depth":318,"links":1334},[1335,1336,1337,1344,1345,1346,1347],{"id":148,"depth":318,"text":149},{"id":203,"depth":318,"text":204},{"id":270,"depth":318,"text":271,"children":1338},[1339,1340,1341,1343],{"id":275,"depth":344,"text":276},{"id":433,"depth":344,"text":434},{"id":1021,"depth":344,"text":1342},"3. Schema: scope credentials by rp_id",{"id":1082,"depth":344,"text":1083},{"id":1178,"depth":318,"text":1179},{"id":1234,"depth":318,"text":1235},{"id":1261,"depth":318,"text":1262},{"id":1314,"depth":318,"text":1315},"Serve multiple OpenApe IdP domains from a single Nuxt process.","md",null,{},{"title":72,"description":1348},"O6ZUIVuXQSxtYBKwUcjmI3r2_VJXzpxnXm6Mt7Z1Zis",[1355,1357],{"title":68,"path":69,"stem":70,"description":1356,"children":-1},"Build your own OpenApe Identity Provider as a Nuxt app.",{"title":76,"path":77,"stem":78,"description":1358,"children":-1},"One-step deployable, scheduled, capability-using agents from a pinned repo.",1781287647231]