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