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