@@ -2,7 +2,7 @@ import { createServer, type IncomingMessage, type Server } from "http";
22import { AddressInfo } from "net" ;
33import { JSONRPCMessage } from "../types.js" ;
44import { SSEClientTransport } from "./sse.js" ;
5- import { OAuthClientProvider , UnauthorizedError } from "./auth.js" ;
5+ import { DelegatedAuthClientProvider , OAuthClientProvider , UnauthorizedError } from "./auth.js" ;
66import { OAuthTokens } from "../shared/auth.js" ;
77
88describe ( "SSEClientTransport" , ( ) => {
@@ -935,4 +935,206 @@ describe("SSEClientTransport", () => {
935935 expect ( mockAuthProvider . redirectToAuthorization ) . toHaveBeenCalled ( ) ;
936936 } ) ;
937937 } ) ;
938+
939+ describe ( "delegated authentication" , ( ) => {
940+ let mockDelegatedAuthProvider : jest . Mocked < DelegatedAuthClientProvider > ;
941+
942+ beforeEach ( ( ) => {
943+ mockDelegatedAuthProvider = {
944+ headers : jest . fn ( ) ,
945+ authorize : jest . fn ( ) ,
946+ } ;
947+ } ) ;
948+
949+ it ( "includes delegated auth headers in requests" , async ( ) => {
950+ mockDelegatedAuthProvider . headers . mockResolvedValue ( {
951+ "Authorization" : "Bearer delegated-token" ,
952+ "X-API-Key" : "api-key-123"
953+ } ) ;
954+
955+ transport = new SSEClientTransport ( resourceBaseUrl , {
956+ delegatedAuthProvider : mockDelegatedAuthProvider ,
957+ } ) ;
958+
959+ await transport . start ( ) ;
960+
961+ expect ( lastServerRequest . headers . authorization ) . toBe ( "Bearer delegated-token" ) ;
962+ expect ( lastServerRequest . headers [ "x-api-key" ] ) . toBe ( "api-key-123" ) ;
963+ } ) ;
964+
965+ it ( "takes precedence over OAuth provider" , async ( ) => {
966+ const mockOAuthProvider = {
967+ get redirectUrl ( ) { return "http://localhost/callback" ; } ,
968+ get clientMetadata ( ) { return { redirect_uris : [ "http://localhost/callback" ] } ; } ,
969+ clientInformation : jest . fn ( ( ) => ( { client_id : "oauth-client" , client_secret : "oauth-secret" } ) ) ,
970+ tokens : jest . fn ( ( ) => Promise . resolve ( { access_token : "oauth-token" , token_type : "Bearer" } ) ) ,
971+ saveTokens : jest . fn ( ) ,
972+ redirectToAuthorization : jest . fn ( ) ,
973+ saveCodeVerifier : jest . fn ( ) ,
974+ codeVerifier : jest . fn ( ) ,
975+ } ;
976+
977+ mockDelegatedAuthProvider . headers . mockResolvedValue ( {
978+ "Authorization" : "Bearer delegated-token"
979+ } ) ;
980+
981+ transport = new SSEClientTransport ( resourceBaseUrl , {
982+ authProvider : mockOAuthProvider ,
983+ delegatedAuthProvider : mockDelegatedAuthProvider ,
984+ } ) ;
985+
986+ await transport . start ( ) ;
987+
988+ expect ( lastServerRequest . headers . authorization ) . toBe ( "Bearer delegated-token" ) ;
989+ expect ( mockOAuthProvider . tokens ) . not . toHaveBeenCalled ( ) ;
990+ } ) ;
991+
992+ it ( "handles 401 during SSE connection with successful reauth" , async ( ) => {
993+ mockDelegatedAuthProvider . headers . mockResolvedValueOnce ( undefined ) ;
994+ mockDelegatedAuthProvider . authorize . mockResolvedValue ( true ) ;
995+ mockDelegatedAuthProvider . headers . mockResolvedValueOnce ( {
996+ "Authorization" : "Bearer new-delegated-token"
997+ } ) ;
998+
999+ // Create server that returns 401 on first attempt, 200 on second
1000+ resourceServer . close ( ) ;
1001+
1002+ let attemptCount = 0 ;
1003+ resourceServer = createServer ( ( req , res ) => {
1004+ lastServerRequest = req ;
1005+ attemptCount ++ ;
1006+
1007+ if ( attemptCount === 1 ) {
1008+ res . writeHead ( 401 ) . end ( ) ;
1009+ return ;
1010+ }
1011+
1012+ res . writeHead ( 200 , {
1013+ "Content-Type" : "text/event-stream" ,
1014+ "Cache-Control" : "no-cache, no-transform" ,
1015+ Connection : "keep-alive" ,
1016+ } ) ;
1017+ res . write ( "event: endpoint\n" ) ;
1018+ res . write ( `data: ${ resourceBaseUrl . href } \n\n` ) ;
1019+ } ) ;
1020+
1021+ await new Promise < void > ( ( resolve ) => {
1022+ resourceServer . listen ( 0 , "127.0.0.1" , ( ) => {
1023+ const addr = resourceServer . address ( ) as AddressInfo ;
1024+ resourceBaseUrl = new URL ( `http://127.0.0.1:${ addr . port } ` ) ;
1025+ resolve ( ) ;
1026+ } ) ;
1027+ } ) ;
1028+
1029+ transport = new SSEClientTransport ( resourceBaseUrl , {
1030+ delegatedAuthProvider : mockDelegatedAuthProvider ,
1031+ } ) ;
1032+
1033+ await transport . start ( ) ;
1034+
1035+ expect ( mockDelegatedAuthProvider . authorize ) . toHaveBeenCalledTimes ( 1 ) ;
1036+ expect ( mockDelegatedAuthProvider . authorize ) . toHaveBeenCalledWith ( {
1037+ serverUrl : resourceBaseUrl ,
1038+ resourceMetadataUrl : undefined
1039+ } ) ;
1040+ expect ( attemptCount ) . toBe ( 2 ) ;
1041+ } ) ;
1042+
1043+ it ( "throws UnauthorizedError when reauth fails" , async ( ) => {
1044+ mockDelegatedAuthProvider . headers . mockResolvedValue ( undefined ) ;
1045+ mockDelegatedAuthProvider . authorize . mockResolvedValue ( false ) ;
1046+
1047+ // Create server that always returns 401
1048+ resourceServer . close ( ) ;
1049+
1050+ resourceServer = createServer ( ( req , res ) => {
1051+ res . writeHead ( 401 ) . end ( ) ;
1052+ } ) ;
1053+
1054+ await new Promise < void > ( ( resolve ) => {
1055+ resourceServer . listen ( 0 , "127.0.0.1" , ( ) => {
1056+ const addr = resourceServer . address ( ) as AddressInfo ;
1057+ resourceBaseUrl = new URL ( `http://127.0.0.1:${ addr . port } ` ) ;
1058+ resolve ( ) ;
1059+ } ) ;
1060+ } ) ;
1061+
1062+ transport = new SSEClientTransport ( resourceBaseUrl , {
1063+ delegatedAuthProvider : mockDelegatedAuthProvider ,
1064+ } ) ;
1065+
1066+ await expect ( transport . start ( ) ) . rejects . toThrow ( UnauthorizedError ) ;
1067+ expect ( mockDelegatedAuthProvider . authorize ) . toHaveBeenCalledTimes ( 1 ) ;
1068+ expect ( mockDelegatedAuthProvider . authorize ) . toHaveBeenCalledWith ( {
1069+ serverUrl : resourceBaseUrl ,
1070+ resourceMetadataUrl : undefined
1071+ } ) ;
1072+ } ) ;
1073+
1074+ it ( "handles 401 during POST request with successful reauth" , async ( ) => {
1075+ mockDelegatedAuthProvider . headers . mockResolvedValue ( {
1076+ "Authorization" : "Bearer delegated-token"
1077+ } ) ;
1078+ mockDelegatedAuthProvider . authorize . mockResolvedValue ( true ) ;
1079+
1080+ // Create server that accepts SSE but returns 401 on first POST, 200 on second
1081+ resourceServer . close ( ) ;
1082+
1083+ let postAttempts = 0 ;
1084+ resourceServer = createServer ( ( req , res ) => {
1085+ lastServerRequest = req ;
1086+
1087+ switch ( req . method ) {
1088+ case "GET" :
1089+ res . writeHead ( 200 , {
1090+ "Content-Type" : "text/event-stream" ,
1091+ "Cache-Control" : "no-cache, no-transform" ,
1092+ Connection : "keep-alive" ,
1093+ } ) ;
1094+ res . write ( "event: endpoint\n" ) ;
1095+ res . write ( `data: ${ resourceBaseUrl . href } \n\n` ) ;
1096+ break ;
1097+
1098+ case "POST" :
1099+ postAttempts ++ ;
1100+ if ( postAttempts === 1 ) {
1101+ res . writeHead ( 401 ) . end ( ) ;
1102+ } else {
1103+ res . writeHead ( 200 ) . end ( ) ;
1104+ }
1105+ break ;
1106+ }
1107+ } ) ;
1108+
1109+ await new Promise < void > ( ( resolve ) => {
1110+ resourceServer . listen ( 0 , "127.0.0.1" , ( ) => {
1111+ const addr = resourceServer . address ( ) as AddressInfo ;
1112+ resourceBaseUrl = new URL ( `http://127.0.0.1:${ addr . port } ` ) ;
1113+ resolve ( ) ;
1114+ } ) ;
1115+ } ) ;
1116+
1117+ transport = new SSEClientTransport ( resourceBaseUrl , {
1118+ delegatedAuthProvider : mockDelegatedAuthProvider ,
1119+ } ) ;
1120+
1121+ await transport . start ( ) ;
1122+
1123+ const message : JSONRPCMessage = {
1124+ jsonrpc : "2.0" ,
1125+ id : "1" ,
1126+ method : "test" ,
1127+ params : { } ,
1128+ } ;
1129+
1130+ await transport . send ( message ) ;
1131+
1132+ expect ( mockDelegatedAuthProvider . authorize ) . toHaveBeenCalledTimes ( 1 ) ;
1133+ expect ( mockDelegatedAuthProvider . authorize ) . toHaveBeenCalledWith ( {
1134+ serverUrl : resourceBaseUrl ,
1135+ resourceMetadataUrl : undefined
1136+ } ) ;
1137+ expect ( postAttempts ) . toBe ( 2 ) ;
1138+ } ) ;
1139+ } ) ;
9381140} ) ;
0 commit comments