@@ -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
import { InvalidClientError , InvalidGrantError , UnauthorizedClientError } from "../server/auth/errors.js" ;
8
8
@@ -1108,4 +1108,206 @@ describe("SSEClientTransport", () => {
1108
1108
expect ( mockAuthProvider . invalidateCredentials ) . toHaveBeenCalledWith ( 'tokens' ) ;
1109
1109
} ) ;
1110
1110
} ) ;
1111
+
1112
+ describe ( "delegated authentication" , ( ) => {
1113
+ let mockDelegatedAuthProvider : jest . Mocked < DelegatedAuthClientProvider > ;
1114
+
1115
+ beforeEach ( ( ) => {
1116
+ mockDelegatedAuthProvider = {
1117
+ headers : jest . fn ( ) ,
1118
+ authorize : jest . fn ( ) ,
1119
+ } ;
1120
+ } ) ;
1121
+
1122
+ it ( "includes delegated auth headers in requests" , async ( ) => {
1123
+ mockDelegatedAuthProvider . headers . mockResolvedValue ( {
1124
+ "Authorization" : "Bearer delegated-token" ,
1125
+ "X-API-Key" : "api-key-123"
1126
+ } ) ;
1127
+
1128
+ transport = new SSEClientTransport ( resourceBaseUrl , {
1129
+ delegatedAuthProvider : mockDelegatedAuthProvider ,
1130
+ } ) ;
1131
+
1132
+ await transport . start ( ) ;
1133
+
1134
+ expect ( lastServerRequest . headers . authorization ) . toBe ( "Bearer delegated-token" ) ;
1135
+ expect ( lastServerRequest . headers [ "x-api-key" ] ) . toBe ( "api-key-123" ) ;
1136
+ } ) ;
1137
+
1138
+ it ( "takes precedence over OAuth provider" , async ( ) => {
1139
+ const mockOAuthProvider = {
1140
+ get redirectUrl ( ) { return "http://localhost/callback" ; } ,
1141
+ get clientMetadata ( ) { return { redirect_uris : [ "http://localhost/callback" ] } ; } ,
1142
+ clientInformation : jest . fn ( ( ) => ( { client_id : "oauth-client" , client_secret : "oauth-secret" } ) ) ,
1143
+ tokens : jest . fn ( ( ) => Promise . resolve ( { access_token : "oauth-token" , token_type : "Bearer" } ) ) ,
1144
+ saveTokens : jest . fn ( ) ,
1145
+ redirectToAuthorization : jest . fn ( ) ,
1146
+ saveCodeVerifier : jest . fn ( ) ,
1147
+ codeVerifier : jest . fn ( ) ,
1148
+ } ;
1149
+
1150
+ mockDelegatedAuthProvider . headers . mockResolvedValue ( {
1151
+ "Authorization" : "Bearer delegated-token"
1152
+ } ) ;
1153
+
1154
+ transport = new SSEClientTransport ( resourceBaseUrl , {
1155
+ authProvider : mockOAuthProvider ,
1156
+ delegatedAuthProvider : mockDelegatedAuthProvider ,
1157
+ } ) ;
1158
+
1159
+ await transport . start ( ) ;
1160
+
1161
+ expect ( lastServerRequest . headers . authorization ) . toBe ( "Bearer delegated-token" ) ;
1162
+ expect ( mockOAuthProvider . tokens ) . not . toHaveBeenCalled ( ) ;
1163
+ } ) ;
1164
+
1165
+ it ( "handles 401 during SSE connection with successful reauth" , async ( ) => {
1166
+ mockDelegatedAuthProvider . headers . mockResolvedValueOnce ( undefined ) ;
1167
+ mockDelegatedAuthProvider . authorize . mockResolvedValue ( true ) ;
1168
+ mockDelegatedAuthProvider . headers . mockResolvedValueOnce ( {
1169
+ "Authorization" : "Bearer new-delegated-token"
1170
+ } ) ;
1171
+
1172
+ // Create server that returns 401 on first attempt, 200 on second
1173
+ resourceServer . close ( ) ;
1174
+
1175
+ let attemptCount = 0 ;
1176
+ resourceServer = createServer ( ( req , res ) => {
1177
+ lastServerRequest = req ;
1178
+ attemptCount ++ ;
1179
+
1180
+ if ( attemptCount === 1 ) {
1181
+ res . writeHead ( 401 ) . end ( ) ;
1182
+ return ;
1183
+ }
1184
+
1185
+ res . writeHead ( 200 , {
1186
+ "Content-Type" : "text/event-stream" ,
1187
+ "Cache-Control" : "no-cache, no-transform" ,
1188
+ Connection : "keep-alive" ,
1189
+ } ) ;
1190
+ res . write ( "event: endpoint\n" ) ;
1191
+ res . write ( `data: ${ resourceBaseUrl . href } \n\n` ) ;
1192
+ } ) ;
1193
+
1194
+ await new Promise < void > ( ( resolve ) => {
1195
+ resourceServer . listen ( 0 , "127.0.0.1" , ( ) => {
1196
+ const addr = resourceServer . address ( ) as AddressInfo ;
1197
+ resourceBaseUrl = new URL ( `http://127.0.0.1:${ addr . port } ` ) ;
1198
+ resolve ( ) ;
1199
+ } ) ;
1200
+ } ) ;
1201
+
1202
+ transport = new SSEClientTransport ( resourceBaseUrl , {
1203
+ delegatedAuthProvider : mockDelegatedAuthProvider ,
1204
+ } ) ;
1205
+
1206
+ await transport . start ( ) ;
1207
+
1208
+ expect ( mockDelegatedAuthProvider . authorize ) . toHaveBeenCalledTimes ( 1 ) ;
1209
+ expect ( mockDelegatedAuthProvider . authorize ) . toHaveBeenCalledWith ( {
1210
+ serverUrl : resourceBaseUrl ,
1211
+ resourceMetadataUrl : undefined
1212
+ } ) ;
1213
+ expect ( attemptCount ) . toBe ( 2 ) ;
1214
+ } ) ;
1215
+
1216
+ it ( "throws UnauthorizedError when reauth fails" , async ( ) => {
1217
+ mockDelegatedAuthProvider . headers . mockResolvedValue ( undefined ) ;
1218
+ mockDelegatedAuthProvider . authorize . mockResolvedValue ( false ) ;
1219
+
1220
+ // Create server that always returns 401
1221
+ resourceServer . close ( ) ;
1222
+
1223
+ resourceServer = createServer ( ( req , res ) => {
1224
+ res . writeHead ( 401 ) . end ( ) ;
1225
+ } ) ;
1226
+
1227
+ await new Promise < void > ( ( resolve ) => {
1228
+ resourceServer . listen ( 0 , "127.0.0.1" , ( ) => {
1229
+ const addr = resourceServer . address ( ) as AddressInfo ;
1230
+ resourceBaseUrl = new URL ( `http://127.0.0.1:${ addr . port } ` ) ;
1231
+ resolve ( ) ;
1232
+ } ) ;
1233
+ } ) ;
1234
+
1235
+ transport = new SSEClientTransport ( resourceBaseUrl , {
1236
+ delegatedAuthProvider : mockDelegatedAuthProvider ,
1237
+ } ) ;
1238
+
1239
+ await expect ( transport . start ( ) ) . rejects . toThrow ( UnauthorizedError ) ;
1240
+ expect ( mockDelegatedAuthProvider . authorize ) . toHaveBeenCalledTimes ( 1 ) ;
1241
+ expect ( mockDelegatedAuthProvider . authorize ) . toHaveBeenCalledWith ( {
1242
+ serverUrl : resourceBaseUrl ,
1243
+ resourceMetadataUrl : undefined
1244
+ } ) ;
1245
+ } ) ;
1246
+
1247
+ it ( "handles 401 during POST request with successful reauth" , async ( ) => {
1248
+ mockDelegatedAuthProvider . headers . mockResolvedValue ( {
1249
+ "Authorization" : "Bearer delegated-token"
1250
+ } ) ;
1251
+ mockDelegatedAuthProvider . authorize . mockResolvedValue ( true ) ;
1252
+
1253
+ // Create server that accepts SSE but returns 401 on first POST, 200 on second
1254
+ resourceServer . close ( ) ;
1255
+
1256
+ let postAttempts = 0 ;
1257
+ resourceServer = createServer ( ( req , res ) => {
1258
+ lastServerRequest = req ;
1259
+
1260
+ switch ( req . method ) {
1261
+ case "GET" :
1262
+ res . writeHead ( 200 , {
1263
+ "Content-Type" : "text/event-stream" ,
1264
+ "Cache-Control" : "no-cache, no-transform" ,
1265
+ Connection : "keep-alive" ,
1266
+ } ) ;
1267
+ res . write ( "event: endpoint\n" ) ;
1268
+ res . write ( `data: ${ resourceBaseUrl . href } \n\n` ) ;
1269
+ break ;
1270
+
1271
+ case "POST" :
1272
+ postAttempts ++ ;
1273
+ if ( postAttempts === 1 ) {
1274
+ res . writeHead ( 401 ) . end ( ) ;
1275
+ } else {
1276
+ res . writeHead ( 200 ) . end ( ) ;
1277
+ }
1278
+ break ;
1279
+ }
1280
+ } ) ;
1281
+
1282
+ await new Promise < void > ( ( resolve ) => {
1283
+ resourceServer . listen ( 0 , "127.0.0.1" , ( ) => {
1284
+ const addr = resourceServer . address ( ) as AddressInfo ;
1285
+ resourceBaseUrl = new URL ( `http://127.0.0.1:${ addr . port } ` ) ;
1286
+ resolve ( ) ;
1287
+ } ) ;
1288
+ } ) ;
1289
+
1290
+ transport = new SSEClientTransport ( resourceBaseUrl , {
1291
+ delegatedAuthProvider : mockDelegatedAuthProvider ,
1292
+ } ) ;
1293
+
1294
+ await transport . start ( ) ;
1295
+
1296
+ const message : JSONRPCMessage = {
1297
+ jsonrpc : "2.0" ,
1298
+ id : "1" ,
1299
+ method : "test" ,
1300
+ params : { } ,
1301
+ } ;
1302
+
1303
+ await transport . send ( message ) ;
1304
+
1305
+ expect ( mockDelegatedAuthProvider . authorize ) . toHaveBeenCalledTimes ( 1 ) ;
1306
+ expect ( mockDelegatedAuthProvider . authorize ) . toHaveBeenCalledWith ( {
1307
+ serverUrl : resourceBaseUrl ,
1308
+ resourceMetadataUrl : undefined
1309
+ } ) ;
1310
+ expect ( postAttempts ) . toBe ( 2 ) ;
1311
+ } ) ;
1312
+ } ) ;
1111
1313
} ) ;
0 commit comments