From d8e706ff9d8fb45667fb5358cb8ec47917ef132d Mon Sep 17 00:00:00 2001 From: Jerome Date: Tue, 8 Jul 2025 12:33:30 +0100 Subject: [PATCH] Create beautiful black and white MCP-branded auth pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **MCP Server Auth Page (src/auth/provider.ts):** - Black background with white card design - Official MCP logo and branding - Clear client information display - Elegant "Continue to Authentication" flow - Professional MCP server identification **Upstream Fake Auth Page (src/handlers/fakeauth.ts):** - Updated branding to distinguish from MCP server - Lock icon and "Upstream Authentication" title - Clear user ID management with localStorage - Generate/edit user ID functionality for testing - Multi-user testing instructions **Technical Updates:** - Permissive CSP headers for both auth pages - Logo serving route at /mcp-logo.png - Responsive design with modern CSS - Clear visual distinction between MCP server auth vs upstream auth - Enhanced userId integration in OAuth authorization flow Both pages now provide professional user experience while maintaining clear distinction between the MCP server's auth and the fake upstream provider auth for testing purposes. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- mcp.png | Bin 0 -> 40652 bytes src/auth/provider.test.ts | 5 + src/auth/provider.ts | 204 ++++++++++++++++++++++++++++++-- src/handlers/fakeauth.ts | 243 ++++++++++++++++++++++++++++++++++++-- src/index.ts | 12 +- src/redis.ts | 2 +- src/services/auth.test.ts | 3 + 7 files changed, 447 insertions(+), 22 deletions(-) create mode 100644 mcp.png diff --git a/mcp.png b/mcp.png new file mode 100644 index 0000000000000000000000000000000000000000..86c52662d890dd8ebf115d6c228f36748c77176e GIT binary patch literal 40652 zcmZs@cU+TK8#kT-iEOP3%2Yv6K~Nd80$P+I0t!S>8L=pcvV^cfLTRN>6;Y-#LRBCV z6i~_v>u#Mz0Zl+qR@4FwTLBpY`CTXWdEWQ)etv)S)Ar7N&biJtzt_0=&k;u(8A(M+ z91bUAXM4aIhZBSU6vM3|!M_qzw3Fap5+Sz7!*DpMwdfx_E+=mT4z~$scVNG3RNh!$ zO$y#N4}7hX7uG&J#*eeqZ(f+7#15@Vy{2Jz zPPt-Lh0N3;{zoh|(SD=l9KM;@gl`~6;iqoU<2CHkH0*NpFhy(v+mHQ)^$FVQFVqm8 z;mHvRHJLoa4D>?Dmxu&1zYp7u%?g5I#LT7h$Z-I@Y7N58<%xA%vVACI)7w1)F=JZb~y6)oM5>Kxxki3xrV%(Xq|>3p_H;E~slhIa;Huaapox~@{cot}eSWHFGGU>;*qD9lI+-qZdysP8v(=T|} znEkz7uyhC=G<4eoPgcfxFefw(&-z8UN$N7&enLbksRrk?7MsQ9eK@1(D=m+(xwzGv(C#jkox(ACwwA+({reUSXh}0S zuEWcP`|;IyN88A7t~sZIx-stEX+qyEsyE5%^q6csFT;@(Q5P#Q@?@8rlO)xZ;7t$G9d0{__9*UVO)b8w? zwOv$eCcUw1mqFxMud1+|I>PC>-VsKuu!^|~7Mo@eg5Ad63uJA$%6(D|1{80k5Q^Jq zTltUae(L_tsw7#0Z_%(UqMb+x5X&o=IIGcG860i=8#>1h&lK-XoA^PawWiWs`3NFX z8DCCRJshc*Cf}WjedLAxirypP$%SU}b}l2n{Oy9egYcfSI^$tMN1La)j0;d&T&{fYh$yVUfM+OS?-N8}(hc&AC7> z8dgKp>5I8b7*qOww*7U%Jw_ECq#3j_sEkTpUvgosQ}Hm`-;O8dzTQ}?FWmOrB{@7I z>?rXbX-T&)(DvNbe;?rQNx&udXc&&F3!+65ClKC{bZsLQ(sJh7X5vJ;VSyjTDHPXB z+9iD&nrRR{QpJxF6j=2Vc1fRiO|{G{i5{jgWD>Qx`9BrzkHK~Cq@*Gma-?WJ{1!TS zv;V6J5;Kq4jxOnd>lUls(Pv_-D6to(R5Wae-b@gW(g_LY_*^q^)&-x_9*k~fie>fn znA+u>W8p5dhE3o-|FQ5^VTwR@KiBpvsp3dXQ#!o%RKCJ6#D$}O%|L#|kTQ(6^O1;> z3^~bBzh+>hO8Vm19uLpB?)V6?Ag4%T+S4`_tXN=zWT?m*YS#I4>-P6fSg4>*^u!J@o!y~GaaU|mx`2_XJ zWF{Foww;X70i&sXF=xvC2fmN@o%^zM6Z??5VSgV>EEb;}Q6PJErok{@A94H|Bb(lO z%ep-ut4UNKzOej38m~kMCOV3%Iw~A+xjp9kPBVWedhrM&yZ%hf-@USN5>cuZ2TXl- z;@{xBbJU>W2V}$h4Wg@4M8RZ>wrY!n1F65V#gj@y-*L*nn0F}&|-VDD!!A&}qKR0x`I;|u;g=0$O zrORH4={jYxJzoZK>}hz$umLS01AdwP(8kg>i#NwLwZQs!&1K02kMmUsZQ}1z-53$PxwHC3q91Gewg9g~Xi%!PRQY{(WR$5CViny;n9OqP%_$Ae8vh_@ zIJ=nnNI7w!khph+e0P(KK>tc+%wm@vJ1E9 zvgoK1^PiVW1=Q-He>R{orC9i#6AgOQ=;p}a&5e& zZE~ozr?d{UtQj3Mz&MyVS?KmA=VO-8uQ2SkX0V`pfJpG`&E-{P!wW-F6&pfaV-DVz zP?5Wr)ApZt;$y2k(q%_B)ZyKYJR{K|7juN_yi$LMe=0uvTbpW~Q>)Mn@indR`FmNA zV-MF&C+XmqG#Y-vgyQ@k0;6Vw_)yQANM zM`AAt7CEw?ZWtST2BS0f1WP0^y-Z3+cF=gRLmYO#K3^lpB9fSTs0&5J9A2()Ew9wq zA*!*PB)D>xj*G?36(&(WSX?iwgllvVb;x-+UOP)i<}gM0eFkSntYHx?{%j%2N8&-n zOH?pPY@;CaA={CZ<>YtZj#Rg7?RJ%!HD4akQ^4aEHV3RYIh9|dQrmuTH_cy@VnGX2 zBOK0s_~_5%C?KGPa14Jj*Gr{z#Lq~WV~1s7tpa}P-I~HtZCJdUbWbub-D`d6hz){` zX9FzBQ->KPk=wzV;$e4#OD(rtu{y!}2kfgPd3(PDAEmnSwc3=`*01^n?;uwAzr>Xi z6Ku+S9JY9gbDaN#T{==CTH>x*D9$3%D%a80~mmIYyb=UHA zLoaNjsq=rGVY^Xnx|d4B2R$VDzt7M%ZdP;Xa)pbluY&ku+W3yzc?zt^y6`XD8$y|b zT+W&cm%HK+ZHL}2?Hxaon;;`IwT|C3!{4|@&!Ni&tt3PdLgHh7E@daZi0lL~Jx%P^ zqcE%=xAUqX6mVDJs&PTK!IX)#M=ia$o9fKkO>N z#Tip-tucLGI69JnGb2P6x;xbN|_0WKNJM#+an#>}#r$%%W5r#H7|gkoo6Udixpy4ZudsEYu#?v%$M7a?>3>}$SWJMAHHp{8w)ZL zwi*fb;97@v1V7{vFJLQ1o|forv8cr4KtsWP{ux1>tv~h-^AzAsjwBb3YN4GotZp;N zTy0lst}q)Yt0K(PVvfjmP_gd?i?DjT^n7ft&^K480CZIqdiaw)vQ+bkRe_v~z-dW> z*Xwxivg%&)*v+=&jxBtu0B?Fk&9=)CetdiI_tu2mR-q~!d!HarYIBn7z?*7$n!&c> z8KZv(tmL`|(V~J()tpBe@!X{F-`?1Z7V59+R*N!jl%%%zVFQKJn`O=_U~0Ui2*wwu zZ@-I0osXF`96K^}VZ zIr7`)yj8}V8b@3^RWL`O%SwF7C1$nUEBSAWb8zHhx7?g5hu#SEBS}Z1_nPWUVx>1H z2riUE8tfmQIpjjE!8siswo(*t3!5G@6Xc*{f5odgmG7a3t5=)>4Y-RtMxL;j8^dom z)098dg)#}3-*m9&ewX>?}LVB^>E~#1r$}95Uqu8OUI|uKz*byb zn=eeQ&>PW-5@$wtPxZ}=9HkBu&ll<6HX&vaRwRslNP8~GO;KnHlimeh*d31N#+-V! z!cpXH>u~bKtB&Q$)Y)fTMxp-g|KLlBYJ?!kGk@m=xiN;xhu89lZ@rUkH58mcvXNyx zNmMIxOJOfkDo&WH=X@-?`H->{@h${k;kG!r=hr1^8uMOS8475c7I|Y@g3jCQwusGr zy0rEQk4y5)8@jhG?^WE2Jr&%ReeNiTfjvw5m(&WpwzEfcXoGa?4zdnW`Ow!1-QfGY zgz}>Vj||Wnwle0|bpOVAUcP(Ra-`}O+9drG>BFRmQbjXJ zP$zlX`>D$~ck}!;;AEWi7uzCpzcdiv98w6{)So3H(H+ofn?pH)8osW=-Bg`zBrG@+ z7q@eHui6&=XZm{*R*D0;`$|?AO|fPqk5FxTmTJg1#Fg9Tqp6F*mIl~v!EG7ry1*0| zo+3_D;r}@^;zS(~lehCbPzgx0>3P(SWJ1~XvOlg}>26)k8|?b_syWS*e|x6IiRw*O zz1j8)xL>yo!8V1XyEHJ%LMbC$7i$!0D3@VFRA9NqvlrQ|39&6f()^Qx5Q7f_L5#Hc z)I9wGiGNDY1XvBV7i%n2u)cuZbfifSSPf^3H|5IOSUK8toumW=#Hw*W(s%FB~|h6OOo zPL4A2c?T64PK6>Kx?^9kK*9QacCS(2CYlMqgKkXvCq?j1=5J~F*0-lb@{swif*2AT zW5{Ah7ERx>phfe4hqp$a)0a<>@7>hp;^KCSZtw>rj}x}#f4k0JMQ7dK$?vhGj+G3JqfqYMRo8m^u& zSLhZ3YWKAmolE8C(f3(D=J93&R6?j6+mx?oIM#?KwCMr*v~Usja_e^|4i^*meD9TA z=0D1?=N>%QD-bAxEDYtkWoq{}hPt#{VJ~`Ww zVs?QZe{$F`JuKWyM4JQQ%Y%hr=eH@JeiU6*&Ld0q`6{z5o*&<2T1_DG>b{Xjjyf|J zU-vFlt-{Y#ZG>neyoyJ7Bk?1Rp)&&r9{IA*Z%oNE>d-%?W}Z;|DAdFS|Gp5+^#l}! zm|+qUZ@S6nZ$m*&jclzkFe6$9N;=<7=?SPe*%M++W?S+iV<$t2i{Gg4WZ%^oe(~2m zZmeQ-;5zY!osC;uGFubwFzz#yc=(N0kRc41@+aum6(i$BY}k$A1)Vq8LE_9jw9z~`sjAKijniXk~ zukuvl%#OqV#`}0EZuY>TK3|LUcU`mEZs?c%%{}-)Nh!gp9CHoVuW&F`A0b9ew4FZE z<||N^)?npSIu(lyAiY)ihoCc?T}EVHL(bJNy5dA{LCaER+xMf4>_RC++@zhzmWy+Q z?S*b$Y>s?XoM<=vgdO=ngLeMUo3|yf=(kAx0v@_W&VPo4l>#d4h&OUTZoLcuXw-(N z!=`dpxFtW1J8|lR=o-Z`6}zSqc-p#DPuYMQ=xj>jS1$f_!Sc5Jx^LJzr@jWSMrtB0N!Dh)H{WDNHs<8{;Fh$fw(z%K^wqTE{z5!9 z*1zE{MT=n1(!XtyD=b#Z)nZNWCe;6-dRW2vXjf#jWin1EJ@${T0mQO>bB(9o{(AR| z^5pw7fdq>T*<{UtmH6pq27|);*)_dIMoS^0V0QyCiqm{FO+&`E&Zen{aQ92b001wrK{bj1-18ulaBG4wgpyArTehO-HG*ErOX;JHOMecos`a zXE`&Okq(`$7lqG$(Xg{JZmo;cD*dATVeTqqlLP3QXB2obf-6_ppJ`9Vh39%bu>`zX z;j_yccCC0-DUbY>s$-qQ0V_Q&Wh}zgL-0O;c%Zw5Ay<4^r!^@Inj=MAADgto+E9jU zh}B}(ajGzAdO($C$JY{soc;6X6{xse`QxU-AkN7DY|yx@EIsm?hQB%fBw=rH!gcEG z1GQsrJx5?I~&M06%M{yn=0!0Y1Ow%TfMM zh%53Hp{D8|ar0iBZtgluW1VD@ZjS0nNVSysfAt+Rt{TQr(9)5~Lz1~jyqD4T+<2Rx zHg1k3rR$LN`S^>`7Yn+A3!vy31XL9ea@ivkQ3RGXHdK+N=1bhW(e}I+XOW{@YtULo zl-*VkTl6d!vbB32$;4jfN;tK%o{M6hdB_Q%-jwLRIsf{FqZ8D*%H1i~N8_QbrD(wS z$5aH~uCp2SzYp91@(+bcMmdZe(bbv@CtKL7!FQHhHvTXfM_u{08 z!$p_%42O(ql{51h8iE9z-V~=;ld@{b2SJHr!xl~$6YeR>P6DW z8RJZi6cbvo2vNj~m?9gp-*gk(^y!c#(Os<8SOr&dA(BoOH+2G_uR8Ui?6qrbu%s7a z2_ro5G9vdiR6DL*DV=zwZD-k;f!rIGS1GDof`VYVYotpX}F|DvA*RPQ;?eI5Qs!D?G#gmy)O z$pL*z)>bH9MOC#XRJIDkM95`#2o(rMDS&)M)86N{E+4W7=QJtB@M2-_)&Wm#R&~ac z=?TO(RP;h3(b}aA67LE;m1ST!L6QyG3lt~fgR8&cArTuycnIYHmU7*ODq{Wz@;xjl z_Nf=_xcl(I4#G75KZg>AMd%9Oi?}O`mp=ruz}rV}D~5K}L>nmt8ZQoBHJOLU>()la zpj%ZFf)0}pPzyqK_mcQL!7f{$56`eWTpzDA100qp@ZRYV^`9 zErRHMX&oE*NdoWf)V?342Q+D9{ulbb*Q!fD{dX3?_006ZYJNIeG$GC3EvSPPjL(7h zE>{*0ECr{d_@i7R_9Jb?4u~kRbZ|G=oa1%J$-fctqbenwv;Lmw|#YMhz=zv;S4yIejZTMdFzrg{h*M z@~fIlyZbkxU|El!D<CnIs6Y%_*&CH1fhOY52$F3iVhCf&aSZ5$$A6 z{2WX4IwTJUW{`GXq8n?pPS@e2iiYLvxE*@|cCBO{?JUB;+1{0l+B~(-bkoT8;B4XU zd^t$w2*&yu``+rmvhi+F1at83dH%L>p zYF@D1vuuBx9hPW_*oX1}&CIY3%mQSK@x1rf42puppo4f41_yBN(#EFCQn%Tbgj9$z z@z{C5P4s`QD`M_(g1<)J@v>%z_TA96#bt?IbrOc&{Z=(Gcz^O`sN%zS=T{1H(awI5 zgb}y+pNRBoG^TU`9XNN%dH+QPD{3?Tm}u{l2mm!=K_U}VtyNc9ZO1D!-!Py@fp(Q< z(2CPxq4Qm~M@rQX2sigNC-IR8c@-ad#0RuZZ(v+A<@t<{398cmVvtm z<>Qt0M>;PI8eKv)q_}MmRQd?!+mT4j=3AQ!n?K*0>4kcWIO-l#zLt(W6ZHoougiA{F;cz2n3&zLuEopOm!Kq(+X&>`?}RJ)<}GK+VbW?R~Y zeQKNW0GB1>t}<^GQ|;Ed8+lbwg#;rw-jfW?IWR>(Vk8S>vAUnhtkm$=z@r9&UoQEpc5d4H+?_8Tq{>=ZRnyL;wv8C$3` zkPZ0sE&eiLzySd(+^C3>$wV?2f8Aiu1fkl#JuuB+8>Wd-1sgr6$!z;HENR)b;+TZqEapvW_z@HzvPh0#@ziG0zh;F@y1FIlnL_dKe!E|ZOvzp* zGm|LMMkM50bmNY~ul3er!&vr< z;UKJ;?q)S1F(wuuV?Ly}a{NJV@(auJD^G+?CKW1E%KRd11^dc5AC`by>d#hL)VBP5 zWFOc_w|Fhs2pfFbW^_g=uGs-%U{NCK$IWK;*n{uzs5>_A=c&I!q79ct7p#B&QL-=4 zMi0LtWBjmQtABLcGhVWvX;Ruc{1~Y>ml@vaBEPBJVc04jJBOKoyJQ>48wpeJ@`M$N zifn7G8E5yx8vX)yB6UMsimq8ivg<{q+AYh;9akFZ3-$CB4gV8(=rb1ic|h6I4g7Lb zXlh(dylY_4dyg%mT4;OWqgxxzj*M$zJ)H4H^~xenPtg$zt@$k>XBOq|`?~~TF^5fq z(+qYp6zBuJB|GH^$h}+80{Kor&PX!$;hW{^s>nK&N^zXbB2-zsSDDLq%Nff?ZSoXz zC3s z$0ITF{d?&JS#9YWJ9qI1S2h`z|1aY8vZyWpzKo54Z$U-YXQ{!RGuhYNt32Gr5%O3~del}*0W0|C>*OuF&! zSj(^@xqIob3cm!~+m~o!JYeT%1Fh+)#fg};RfDQD=+;4TO4=h6;M3I&MZ70A2N%}e z(aGYye-)>#x9TEpjPUZ%u)iyQl&D4yvQzL#|H7sRY4|hgtd4g}HPN2jN&o~uvCO>l zVF!!gFs>@?DE~Qx3vbmiRd!%b+iPQdmY~eOL01LW+L=EXhQUOGzcVC;eh$<=adVMz zUs45mw%nhfb<=NM?ec#? zKs6_%zG>J6NVS+5F@e|Ht`5bd(2hUqW6EBRX{nO|546RO(no1L*M1*^rWasW5_Gmf zI0lH<_C!njWUKTq=lL52y=Olkx+9@HbRXZkU~Q_DW}w7h7Ti3#*23_jv*<|3k-*}} z5E3N@jxZtaX)g1l6M|bels)tzLdvAk>f27&)ey8ZWKzGF5+AQ&3KIvEaW}N)-nCS$Fm{m=iJEIVciEw7lfEK&1>IOcgfWl<yK%{Q1}yqm=u+c2disCXXaX95;+m)|NlhF8 z12QpHb~nmje+H)~kd1YHD#hJ+&4AS6-voml`b@Si*+^8_3PJ%BK{aq|4)cuajW^K@P**5BQ+a8is~^mJ^l)4&hn+tgGjC@-UF?# z>X^IlG>hqw;&!OK+&x=HU)~Bm{5AYs!Er~b0%24Xk$%RAaOSY6Weh8oYN590@Q*#_ zEXcd-fLTU8YQ0jw)?ME-)=r(|-tovQrz!NW`m7$$Rd1JS-YzvG)yX8^>?1lcEKaVFBN%K?qX__}mPTg(Qoo%%KCa^1W0KX^g%uDNuzyZDwv4Y3|B z@E&)Ja4}1E;zqt-b_-V*Yu6MPZ!+UL7 z9-uRxJwFN~CW^+;OYfcuC}t02nMH|^@`?YuuFYq|vRQl7#xn@Odg}LfeE{~Jx3w}OeojxIe;eBFKwWZ7z}>JFW^6cSCX{qfB;&x+6Q^k=jP znlLAx{tfm;&!$pQu~P(DJy9F`mIeO-)`|Rf{oq;*PCLOE2E_Y_LSkl>RTaKB6lxXw z>3*v!0H&%bdUQ)77p88I!xG6?92DX6_ZS(-Y6Y z?Y!&cwILcr>3!yjJUtBK{hPF!{+UBc_awX_+hRK~SAW1_>g_A<@m%#}_|& zG+3PplQM<`fWPrEzRv$I{Ehk^P_lN{(Qv;#cN?GbQ=qpm^j@I9aQdnE$t{?mIftu| z54jj)&HX7-PcNqp^#T2OkZOcuB5wi6!VI{PE8Gji2o6)&RR0WaO=7`cO!{^Bd&X>{d@Kz z38s9kAtgM;zlrv;_-Ww>7`9lz>kyZAQ+~02UMNAhnwtFXe|S1G*ZvK{k%Qw8 zTNBb-SR2|QK;`_XaNJAI*iaTGf4Bw&RkBAq)894Rw(gj&sF5jcyaPzhC*K2ISY)`x z{1BNwLHuG13nj5>j+IUoZDL#f4(@D*fecJ*EAc9?uq{Mwq^*esfGyY$m#2?{*DqJz zF?hSdZgBC5Cyb30e+SSQQ(RMixXYQlj#v!<c#m;;=e-2)FLp4C1?;{dkt)TZ$ z|E^L>^Hj`jBm5-b091Pi{Wyg4%2Q*%NHFs;(tr7y<#u#4qLoNV+Ji z^KF6vrGB}Ifxz%IhB}%`3fi_;yYT!r|Fo9nCQ-#A7zBwB_9UwPJosONtAz}3mib}l zay0YAvy*uF=L2ZjcZ@zpEKmPZj4HuPB;(G+0R*em@RuQEKslxaxI=Asj9n+hp9wI<^Pi6_>oMlW@N7qNS==6FuNF+PH19Z1E!&jbS z9k^T!XA-b}h=(`7?;H{qArah9RDjYC`%dB$ks%2KP>xNC@-S6*zRc+35$RZjU6+%y zc!g8fS@zB|d!KrVq=5Ri;8WL%Qs}Z?37NR<$O1_L&>re|IAOlA)hw8J?F6ym_SYtl zz&g2Z5o}B)={f?c@}BE>4^vp`r~&U zyB>krsu+rsG!w3{QEr39`5ly(i*t+0mKL@XG$qu$Uk zdJaBlIj@znl|Mec%iS<+0vB-@sYweeN)Bo+16Yq)~II6dOmU5 z*w#=#2+ss4vd9Qzc}mL{G*r-R8b9D7}-VSMJ!Io zQ{O)W#%)+Owa{`l5`mFQMm-OIwEYp5E4mXQBnI&%+9Vy#qa6;SY_B&pG-06=!I0kC z$$sB@?>u}3f~J=2BW>I)rx)wlkNKFRUo z^1(^oU)qMI-Dbmsk54$kc5+!(nId|dBoJaoEsBI)qg9@XdQKmym2#2feNA}xt;+-E zx-G{Id9Lk8>8D|*ZA&+ludDvW=7_oLA=>mju$rAYO+yIk ziHyJ$`xzO55(cULYvr1SW|i9QdFjcVN-?9uLI>;PHPhdxSB3&jM{+Cz+dcUm+?(<|}?(jGUFF9?RF0e_=s7)4AjvK}w*Andc22(ji= z5HGlMj&ZN$2tV@Fk5T;=C@#5-y%AC>Rf9n=A&H#b>3t3_Yqv*YJxhC6MNRHYIPVxX z{GkJHaZUH1eM>t$wf(ngJ!f81z0Y1WtZ)_uXB2cNLcjXwGM*9KL^(O*XYk~+M#b&B z0|xU_9k>ZEj^Y9ebcgo!3LYJX`N5*ZwLEny^-O8kuOcScm@4N?*$g~NP!tRJ)|Roj z4%thb$+VUAqZG)an+31J<$mJUSV%uvgn>Z^LY7|7X|B2}Rg3t4ip}3}592I@Ju2Eu3?d60yAGWrRqUARv6EJAZ4iz5K_lQja%q=gs24uXs6mWFiMP#_ zuYNb5Mg8+PA)k08qS3z%ApD_o-`D*rKHuj-y0Ql_Jf!NrZt~ku(faJ4dxbqH$NV3L zcc|5$rayhrTqvA?XAy$I)52f)o-IW56DN(?g?6S>W(O0+Dpxy`-`9k=R*Z-M&FP_qM8 znTR}L)xv-tN~(_!nOrGPUs^}&$JZB0?OmXqoLR9@xkhR*_q==y|HeTfa8or*`5kgttx#kG(}1$(dqf4l zas)^JTiaIV_tW=vOs}5$6&V8h{7cqWz3hv0;ljHjciBHW^TspFG7avud|!7GP?-0Q z6knKK&2LTMeH-MB$D}yH(Vbc3MChj?3cRk#+WPbs?MY>%^I)(AJ}%H`-2l`1Ei-pr zIu#hlp$ZW(=pqfnH~?>*X^qH1j|Ga#Xk4IR*ze}r9eVfV9lHE%8CGaCCc(bTE@utq zQ$Ox{u3gmQmsdfbW*niKG|=y(VV(MaSU2`J-$;J+ZUV&q7!Jzd@f!-{xjF075)>!}3=R8%#y z$ziv6e)bCE-3VRY65@(V+zRfZiIws(*=(1S-;#lN?CHfUlkJ1IK8y%rWW=X_LUTpK zFjolQWPVq>nbP8lz7&9PDWQz)rzO9;RMhr?>N_M_?V>twF?__!kbZtqkRt=Oh9;G_5J_yF*++i?-kDMHZk3`QqQ^gu22bTS|9E659b7uf3QR?O?iz80N|evih)? zP* Wt$UO#QT4xF{!Cg`V=wfbRA8HV?}G@QPh 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 literal 0 HcmV?d00001 diff --git a/src/auth/provider.test.ts b/src/auth/provider.test.ts index 5f7c82b..bfdb9ec 100644 --- a/src/auth/provider.test.ts +++ b/src/auth/provider.test.ts @@ -53,6 +53,7 @@ function getMockAuthValues() { }, clientId: client.client_id, issuedAt: Date.now() / 1000, + userId: "test-user-id", }; return { @@ -279,6 +280,7 @@ describe("EverythingAuthProvider", () => { }, clientId: "different-client-id", issuedAt: Date.now() / 1000, + userId: "test-user-id", }; await authService.saveRefreshToken(refreshToken, accessToken); @@ -314,6 +316,7 @@ describe("EverythingAuthProvider", () => { }, clientId: "client-id", issuedAt: Date.now() / 1000, + userId: "test-user-id", }; await authService.saveMcpInstallation(accessToken, mcpInstallation); @@ -351,6 +354,7 @@ describe("EverythingAuthProvider", () => { }, clientId: "client-id", issuedAt: twoDaysAgoInSeconds, // 2 days ago, with 1-day expiry + userId: "test-user-id", }; await authService.saveMcpInstallation(accessToken, mcpInstallation); @@ -377,6 +381,7 @@ describe("EverythingAuthProvider", () => { }, clientId: client.client_id, issuedAt: Date.now() / 1000, + userId: "test-user-id", }; // Save the installation diff --git a/src/auth/provider.ts b/src/auth/provider.ts index 53cb73e..3d77c61 100644 --- a/src/auth/provider.ts +++ b/src/auth/provider.ts @@ -72,22 +72,196 @@ export class EverythingAuthProvider implements OAuthServerProvider { // You can redirect to another page, or you can send an html response directly // res.redirect(new URL(`fakeupstreamauth/authorize?metadata=${authorizationCode}`, BASE_URI).href); + // Set permissive CSP for styling + res.setHeader('Content-Security-Policy', [ + "default-src 'self'", + "style-src 'self' 'unsafe-inline'", + "script-src 'self' 'unsafe-inline'", + "img-src 'self' data:", + "object-src 'none'", + "frame-ancestors 'none'", + "form-action 'self'", + "base-uri 'self'" + ].join('; ')); + res.send(` - + + - MCP Auth Page + + + MCP Server Authorization + -

MCP Server Auth Page

-

- This page is the authorization page presented by the MCP server, routing the user upstream. This is only - needed on 2025-03-26 Auth spec, where the MCP server acts as it's own authoriztion server. This page should - be present to avoid confused deputy attacks. -

-

- Click here - to continue to the upstream auth -

+
+
+ +
MCP
+
+ +

Authorization Required

+

This client wants to connect to your MCP server

+ +
+

Client Application

+
${client.client_id}
+
+ +
+

What happens next?

+

You'll be redirected to authenticate with the upstream provider. Once verified, you'll be granted access to this MCP server's resources.

+
+ + + Continue to Authentication + + +
+ Model Context Protocol (MCP) Server +
+
`); @@ -156,6 +330,7 @@ export class EverythingAuthProvider implements OAuthServerProvider { ...mcpInstallation, mcpTokens: newTokens, issuedAt: Date.now() / 1000, + userId: mcpInstallation.userId, // Preserve the user ID }); return newTokens; @@ -183,7 +358,10 @@ export class EverythingAuthProvider implements OAuthServerProvider { token, clientId: installation.clientId, scopes: ['mcp'], - expiresAt + expiresAt, + extra: { + userId: installation.userId + } }; } diff --git a/src/handlers/fakeauth.ts b/src/handlers/fakeauth.ts index d19e786..49f1f70 100644 --- a/src/handlers/fakeauth.ts +++ b/src/handlers/fakeauth.ts @@ -12,18 +12,245 @@ export async function handleFakeAuthorize(req: Request, res: Response) { // get the redirect_uri and state from the query params const { redirect_uri, state } = req.query; - // TODO, mint actual codes? + // Set a more permissive CSP for auth pages to allow inline styles and scripts + res.setHeader('Content-Security-Policy', [ + "default-src 'self'", + "style-src 'self' 'unsafe-inline'", // Allow inline styles for auth page styling + "script-src 'self' 'unsafe-inline'", // Allow inline scripts for auth page functionality + "object-src 'none'", + "frame-ancestors 'none'", + "form-action 'self'", + "base-uri 'self'" + ].join('; ')); res.send(` - + + - Fake Upstream Auth Provider! + + + Upstream Provider Authentication + -

Fake Auth

-

Fake auth page

-

Click here to authorize

-

Click here to fail authorization

+
+ +

Upstream Authentication

+

Please verify your identity with the upstream provider

+ +
+

Your User Identity

+
Loading...
+ +
+ + + +
+ Testing Multiple Users: Open this page in different browser windows or incognito tabs to simulate different users. Each will have their own unique User ID and separate MCP sessions. +
+
+ + `); @@ -36,6 +263,7 @@ export async function handleFakeAuthorizeRedirect(req: Request, res: Response) { // The state returned from the upstream auth server is actually the authorization code state: mcpAuthorizationCode, code: upstreamAuthorizationCode, + userId, // User ID from the authorization flow } = req.query; // This is where you'd exchange the upstreamAuthorizationCode for access/refresh tokens @@ -63,6 +291,7 @@ export async function handleFakeAuthorizeRedirect(req: Request, res: Response) { mcpTokens, clientId: pendingAuth.clientId, issuedAt: Date.now() / 1000, + userId: (userId as string) || 'anonymous-user', // Include user ID from auth flow } // Store the upstream authorization data diff --git a/src/index.ts b/src/index.ts index 5f7aa32..37e94e9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,6 +36,11 @@ const baseSecurityHeaders = (req: express.Request, res: express.Response, next: // simple logging middleware const logger = (req: express.Request, res: express.Response, next: express.NextFunction) => { + console.log(`${new Date().toISOString()} ${req.method} ${req.url}`); + if (req.url.includes('/mcp')) { + console.log(' Headers:', JSON.stringify(req.headers, null, 2)); + console.log(' Body:', JSON.stringify(req.body, null, 2)); + } next(); }; @@ -106,7 +111,12 @@ app.get("/mcp", cors(corsOptions), bearerAuth, authContext, handleStreamableHTTP app.post("/mcp", cors(corsOptions), bearerAuth, authContext, handleStreamableHTTP); app.delete("/mcp", cors(corsOptions), bearerAuth, authContext, handleStreamableHTTP); -// Upstream auth routes +// Static assets +app.get("/mcp-logo.png", (req, res) => { + res.sendFile("mcp.png", { root: "." }); +}); + +// Upstream auth routes app.get("/fakeupstreamauth/authorize", cors(corsOptions), handleFakeAuthorize); app.get("/fakeupstreamauth/callback", cors(corsOptions), handleFakeAuthorizeRedirect); diff --git a/src/redis.ts b/src/redis.ts index cc57f00..1316592 100644 --- a/src/redis.ts +++ b/src/redis.ts @@ -142,7 +142,7 @@ export class MockRedisClient implements RedisClient { options = { url: "redis://localhost:6379" }; private store = new Map(); private lists = new Map(); - private subscribers = new Map void)[]>(); + public subscribers = new Map void)[]>(); // Public for testing access private errorCallbacks = new Map void)[]>(); async get(key: string): Promise { diff --git a/src/services/auth.test.ts b/src/services/auth.test.ts index e79dd10..6a761bd 100644 --- a/src/services/auth.test.ts +++ b/src/services/auth.test.ts @@ -193,6 +193,7 @@ describe("auth utils", () => { }, clientId: "client-id", issuedAt: Date.now() / 1000, + userId: "test-user-id", } await saveMcpInstallation(accessToken, mcpInstallation); @@ -239,6 +240,7 @@ describe("auth utils", () => { }, clientId: "client-id", issuedAt: Date.now() / 1000, + userId: "test-user-id", }); const getDel = jest.spyOn(mockRedis, 'getDel').mockImplementationOnce(() => { @@ -252,6 +254,7 @@ describe("auth utils", () => { }, clientId: "client-id", issuedAt: Date.now() / 1000, + userId: "test-user-id", }; const value = JSON.stringify(mcpInstallation); const iv = crypto.randomBytes(16);