@@ -2,7 +2,7 @@ import { createServer, type IncomingMessage, type Server } from "http";
2
2
import { AddressInfo } from "net" ;
3
3
import { JSONRPCMessage } from "../types.js" ;
4
4
import { SSEClientTransport } from "./sse.js" ;
5
- import { OAuthClientProvider , UnauthorizedError } from "./auth.js" ;
5
+ import { DelegatedAuthClientProvider , OAuthClientProvider , UnauthorizedError } from "./auth.js" ;
6
6
import { OAuthTokens } from "../shared/auth.js" ;
7
7
8
8
describe ( "SSEClientTransport" , ( ) => {
@@ -935,4 +935,206 @@ describe("SSEClientTransport", () => {
935
935
expect ( mockAuthProvider . redirectToAuthorization ) . toHaveBeenCalled ( ) ;
936
936
} ) ;
937
937
} ) ;
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
+ } ) ;
938
1140
} ) ;
0 commit comments