[{"data":1,"prerenderedAt":394},["ShallowReactive",2],{"blog-de-der-rfc-verlangt-ein-token-das-niemand-haelt":3,"header-blog-translations-/de/blog/der-rfc-verlangt-ein-token-das-niemand-haelt":391},{"id":4,"title":5,"author":6,"body":7,"date":373,"description":374,"draft":375,"extension":376,"image":377,"meta":378,"navigation":379,"path":380,"seo":381,"stem":382,"tags":383,"translationKey":389,"__hash__":390},"blog_de/blog/de/der-rfc-verlangt-ein-token-das-niemand-haelt.md","Der RFC verlangt ein Token, das niemand hält","Patrick Hofmann",{"type":8,"value":9,"toc":366},"minimark",[10,27,30,35,67,74,77,87,93,97,107,117,123,127,136,287,293,299,306,310,316,329,333,336,339,342,345,362],[11,12,13,14,18,19,22,23,26],"p",{},"Der Nest ist ein Control-Plane-Daemon, der lokal als eigener Unix-User läuft — ",[15,16,17],"code",{},"_openape_nest",", ",[15,20,21],{},"HOME=/var/openape/nest",", gestartet von mir, nicht vom Agent. Er soll für einen Agent Token besorgen, mit denen der Agent gegen einen Service-Provider arbeitet. Auf meine Identität, aber ohne dass mein Token jemals in seine Reichweite kommt. Mein DDISA-Token liegt in ",[15,24,25],{},"~/.config/apes/auth.json"," in meinem Home. Der Nest läuft als anderer User und kann es nicht lesen. Das ist kein Implementierungs-Detail, das ist der ganze Zweck dieser Trennung.",[11,28,29],{},"Für Token-Delegation gibt es einen Standard: RFC 8693, OAuth 2.0 Token Exchange. Ich habe mich hingesetzt, um das nach Spec zu bauen. An einer Stelle ging das nicht. Hier ist warum.",[31,32,34],"h2",{"id":33},"was-der-rfc-verlangt","Was der RFC verlangt",[11,36,37,38,41,42,45,46,48,49,53,54,56,57,60,61,63,64,66],{},"RFC 8693 kennt zwei Token im Request: ",[15,39,40],{},"subject_token"," und ",[15,43,44],{},"actor_token",". Das ",[15,47,40],{}," repräsentiert die Partei, ",[50,51,52],"em",{},"für"," die das neue Token ausgestellt wird. Das ",[15,55,44],{}," repräsentiert die Partei, die ",[50,58,59],{},"handelt",". Im Spec-Text ist ",[15,62,40],{}," required, ",[15,65,44],{}," optional.",[11,68,69,70,73],{},"Der kanonische Use-Case dahinter: ein Confidential Client hält bereits eine signierte User-Assertion — ein vorab ausgestelltes Token, das den Nutzer repräsentiert — und will daraus ein heruntergeskaltes Token für einen Downstream-Service tauschen. Der Aufrufer hat in diesem Modell ",[50,71,72],{},"beide"," Token. Er hält das Subject in der Hand und gibt es weiter.",[11,75,76],{},"Ein typischer Request sieht ungefähr so aus:",[78,79,84],"pre",{"className":80,"code":82,"language":83},[81],"language-text","grant_type=urn:ietf:params:oauth:grant-type:token-exchange\n&subject_token=\u003Cdas Token, das den Nutzer repräsentiert>\n&subject_token_type=urn:ietf:params:oauth:token-type:jwt\n&audience=https://timetrack.openape.ai\n","text",[15,85,82],{"__ignoreMap":86},"",[11,88,89,90,92],{},"Sauber, solange der, der den Request stellt, das ",[15,91,40],{}," tatsächlich besitzt.",[31,94,96],{"id":95},"mein-aufrufer-besitzt-es-nicht","Mein Aufrufer besitzt es nicht",[11,98,99,100,102,103,106],{},"Der Nest hält sein eigenes Token — er hat eine eigene DDISA-Agent-Identität. Was er nicht hält, ist meins. Nicht \"es ist umständlich zu beschaffen\", sondern: es liegt hinter einer Filesystem-Grenze, die ich absichtlich gezogen habe. Der ",[15,101,40],{},"-Slot, den der RFC verlangt, ist auf der Nest-Seite nicht leer, weil ich faul war. Er ist leer, weil dort nichts sein ",[50,104,105],{},"soll",".",[11,108,109,110,112,113,116],{},"Die Information, die das ",[15,111,40],{}," transportieren würde — ",[50,114,115],{},"dieses Token handelt im Auftrag von Patrick"," — existiert trotzdem. Sie liegt nur woanders: in einem Delegation-Grant. Ein Standing Grant, in dem ich diesem Agent vorab erlaubt habe, in meinem Namen zu handeln. Der Grant ist server-seitig, vom IdP signiert, und enthält explizit, wer der Delegator ist. Der Agent kann ihn referenzieren, aber nicht fälschen und nicht ableiten.",[11,118,119,120,122],{},"Damit war die Wahl konkret: entweder ich lege mein Token dorthin, wo der Nest es lesen kann — und kippe die Isolation, die der ganze Aufbau trägt — oder ich akzeptiere, dass der RFC-vorgeschriebene ",[15,121,40],{}," in diesem Fall durch einen Pointer ersetzt wird.",[31,124,126],{"id":125},"wie-es-jetzt-aussieht","Wie es jetzt aussieht",[11,128,129,131,132,135],{},[15,130,40],{}," ist optional, wenn eine ",[15,133,134],{},"delegation_grant_id"," da ist. Der Delegator wird aus dem Grant abgeleitet, nicht aus einem mitgereichten Token.",[78,137,141],{"className":138,"code":139,"language":140,"meta":86,"style":86},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","if (!subjectToken) {\n  if (!delegationGrantId) {\n    throw badRequest(\"subject_token required\");\n  }\n  const grant = await grants.get(delegationGrantId);\n  // Der \"Subject\" ist hier kein Token, das durch den Agent reist,\n  // sondern ein server-seitiger Pointer auf den Grant-Owner.\n  delegator = grant.delegator;\n}\n","ts",[15,142,143,166,185,213,219,251,258,264,281],{"__ignoreMap":86},[144,145,148,152,156,160,163],"span",{"class":146,"line":147},"line",1,[144,149,151],{"class":150},"s7zQu","if",[144,153,155],{"class":154},"sTEyZ"," (",[144,157,159],{"class":158},"sMK4o","!",[144,161,162],{"class":154},"subjectToken) ",[144,164,165],{"class":158},"{\n",[144,167,169,172,175,177,180,183],{"class":146,"line":168},2,[144,170,171],{"class":150},"  if",[144,173,155],{"class":174},"swJcz",[144,176,159],{"class":158},[144,178,179],{"class":154},"delegationGrantId",[144,181,182],{"class":174},") ",[144,184,165],{"class":158},[144,186,188,191,195,198,201,205,207,210],{"class":146,"line":187},3,[144,189,190],{"class":150},"    throw",[144,192,194],{"class":193},"s2Zo4"," badRequest",[144,196,197],{"class":174},"(",[144,199,200],{"class":158},"\"",[144,202,204],{"class":203},"sfazB","subject_token required",[144,206,200],{"class":158},[144,208,209],{"class":174},")",[144,211,212],{"class":158},";\n",[144,214,216],{"class":146,"line":215},4,[144,217,218],{"class":158},"  }\n",[144,220,222,226,229,232,235,238,240,243,245,247,249],{"class":146,"line":221},5,[144,223,225],{"class":224},"spNyl","  const",[144,227,228],{"class":154}," grant",[144,230,231],{"class":158}," =",[144,233,234],{"class":150}," await",[144,236,237],{"class":154}," grants",[144,239,106],{"class":158},[144,241,242],{"class":193},"get",[144,244,197],{"class":174},[144,246,179],{"class":154},[144,248,209],{"class":174},[144,250,212],{"class":158},[144,252,254],{"class":146,"line":253},6,[144,255,257],{"class":256},"sHwdD","  // Der \"Subject\" ist hier kein Token, das durch den Agent reist,\n",[144,259,261],{"class":146,"line":260},7,[144,262,263],{"class":256},"  // sondern ein server-seitiger Pointer auf den Grant-Owner.\n",[144,265,267,270,272,274,276,279],{"class":146,"line":266},8,[144,268,269],{"class":154},"  delegator",[144,271,231],{"class":158},[144,273,228],{"class":154},[144,275,106],{"class":158},[144,277,278],{"class":154},"delegator",[144,280,212],{"class":158},[144,282,284],{"class":146,"line":283},9,[144,285,286],{"class":158},"}\n",[11,288,289,290,292],{},"Der Request, den der Nest schickt, trägt sein eigenes Token als ",[15,291,44],{}," und verweist auf den Grant statt auf ein Subject:",[78,294,297],{"className":295,"code":296,"language":83},[81],"grant_type=urn:ietf:params:oauth:grant-type:token-exchange\n&actor_token=\u003Cdas eigene Token des Nest>\n&actor_token_type=urn:ietf:params:oauth:token-type:jwt\n&delegation_grant_id=grant_\u003Cid-des-delegation-grants>\n&audience=https://timetrack.openape.ai\n",[15,298,296],{"__ignoreMap":86},[11,300,301,302,305],{},"Der Effekt ist identisch zu dem, was der RFC erreichen will: ein Token, ausgestellt auf mich, zum Handeln durch den Agent, geskoped auf einen Ziel-SP. Was sich ändert, ist ",[50,303,304],{},"wo"," der Beweis \"im Auftrag von Patrick\" herkommt. Nicht aus einem Token, das der Agent durchreicht. Aus einem Grant, den der IdP hält.",[31,307,309],{"id":308},"was-weggefallen-ist","Was weggefallen ist",[11,311,312,313,315],{},"Der ",[15,314,40],{},". Und mit ihm die Annahme, dass der Agent jemals etwas in der Hand hat, das mich repräsentiert.",[11,317,318,319,321,322,324,325,328],{},"Das ist nicht strikt RFC mehr. Ich nenne es auch nicht so. Es ist eine bewusste Abweichung an genau einer Achse — ",[15,320,40],{}," optional bei vorhandener ",[15,323,134],{},", Delegator aus ",[15,326,327],{},"grant.delegator"," — und alles andere bleibt RFC-konform. Wer beide Token hat, fährt weiter den Standard-Pfad. Der delegation-grant-only-Pfad ist eine Erweiterung für einen Fall, den der RFC nicht im Blick hatte.",[31,330,332],{"id":331},"die-pointe","Die Pointe",[11,334,335],{},"Der RFC ist nicht falsch. Er ist für Confidential Clients geschrieben, die eine vorab signierte User-Assertion halten — Aufrufer hat beide Token. Das ist eine vernünftige Welt. Sie ist nur nicht meine.",[11,337,338],{},"Agent-zu-Agent-on-behalf-of-human ist eine andere Form. Strikte RFC-Konformität hätte mich gezwungen, mein Token dorthin zu legen, wo der Agent es erreichen kann — exakt das, was die Architektur verhindern soll. Die Abweichung ist keine Abkürzung. Sie ist dieselbe Sicherheits-Eigenschaft, nur anders ausgedrückt: nicht \"der Agent trägt das Subject vorsichtig durch\", sondern \"der Agent trägt das Subject gar nicht\".",[11,340,341],{},"Eine Spec, die verlangt, dass das Token des Nutzers durch den Agent reist, ist eine Spec, die annimmt, dass du dem Agent vertraust. Ich vertraue ihm nicht. Der Grant trägt die Delegation. Der Agent trägt nichts.",[343,344],"hr",{},[11,346,347],{},[50,348,349,350,357,358,361],{},"Code: ",[351,352,356],"a",{"href":353,"rel":354},"https://github.com/openape-ai/openape",[355],"nofollow","github.com/openape-ai/openape",", MIT-lizenziert. Der Token-Exchange-Endpoint lebt im IdP, ",[15,359,360],{},"exchangeWithDelegation"," im CLI-Auth-Layer, der Nest nutzt beides über seine eigene DDISA-Identität.",[363,364,365],"style",{},"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 .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 .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}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 .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);}",{"title":86,"searchDepth":168,"depth":168,"links":367},[368,369,370,371,372],{"id":33,"depth":168,"text":34},{"id":95,"depth":168,"text":96},{"id":125,"depth":168,"text":126},{"id":308,"depth":168,"text":309},{"id":331,"depth":168,"text":332},"2026-05-09","Ich wollte RFC 8693 Token-Exchange sauber implementieren. An genau einer Stelle ging das nicht — nicht aus Faulheit, sondern weil der RFC einen Fall annimmt, den meine Architektur bewusst nicht hat: dass der Aufrufer das Token des Nutzers in der Hand hält. Im Agent-zu-Agent-on-behalf-of-human-Fall hat das niemand.",false,"md",null,{},true,"/blog/de/der-rfc-verlangt-ein-token-das-niemand-haelt",{"title":5,"description":374},"blog/de/der-rfc-verlangt-ein-token-das-niemand-haelt",[384,385,386,387,388],"OpenApe","AI Agents","Security","Infrastructure","Building in Public","the-rfc-wants-a-token-nobody-holds","TjURoM8-8buTvfZErEKzUZgB3er-FeT4AKZ26fUNIww",{"de":392,"en":393},"/de/blog/der-rfc-verlangt-ein-token-das-niemand-haelt","/en/blog/the-rfc-wants-a-token-nobody-holds",1779001885471]