@@ -4,6 +4,7 @@ import { JSONRPCMessage } from "../types.js";
4
4
import { SSEClientTransport } from "./sse.js" ;
5
5
import { OAuthClientProvider , UnauthorizedError } from "./auth.js" ;
6
6
import { OAuthTokens } from "../shared/auth.js" ;
7
+ import { InvalidClientError , InvalidGrantError , UnauthorizedClientError } from "../server/auth/errors.js" ;
7
8
8
9
describe ( "SSEClientTransport" , ( ) => {
9
10
let server : Server ;
@@ -310,6 +311,7 @@ describe("SSEClientTransport", () => {
310
311
redirectToAuthorization : jest . fn ( ) ,
311
312
saveCodeVerifier : jest . fn ( ) ,
312
313
codeVerifier : jest . fn ( ) ,
314
+ invalidateCredentials : jest . fn ( ) ,
313
315
} ;
314
316
} ) ;
315
317
@@ -721,5 +723,179 @@ describe("SSEClientTransport", () => {
721
723
await expect ( ( ) => transport . start ( ) ) . rejects . toThrow ( UnauthorizedError ) ;
722
724
expect ( mockAuthProvider . redirectToAuthorization ) . toHaveBeenCalled ( ) ;
723
725
} ) ;
726
+
727
+ it ( "invalidates all credentials on InvalidClientError during token refresh" , async ( ) => {
728
+ // Mock tokens() to return token with refresh token
729
+ mockAuthProvider . tokens . mockResolvedValue ( {
730
+ access_token : "expired-token" ,
731
+ token_type : "Bearer" ,
732
+ refresh_token : "refresh-token"
733
+ } ) ;
734
+
735
+ // Create server that returns InvalidClientError on token refresh
736
+ await server . close ( ) ;
737
+
738
+ server = createServer ( ( req , res ) => {
739
+ lastServerRequest = req ;
740
+
741
+ // Handle OAuth metadata discovery
742
+ if ( req . url === "/.well-known/oauth-authorization-server" && req . method === "GET" ) {
743
+ res . writeHead ( 200 , { 'Content-Type' : 'application/json' } ) ;
744
+ res . end ( JSON . stringify ( {
745
+ issuer : baseUrl . href ,
746
+ authorization_endpoint : `${ baseUrl . href } authorize` ,
747
+ token_endpoint : `${ baseUrl . href } token` ,
748
+ response_types_supported : [ "code" ] ,
749
+ code_challenge_methods_supported : [ "S256" ] ,
750
+ } ) ) ;
751
+ return ;
752
+ }
753
+
754
+ if ( req . url === "/token" && req . method === "POST" ) {
755
+ // Handle token refresh request - return InvalidClientError
756
+ const error = new InvalidClientError ( "Client authentication failed" ) ;
757
+ res . writeHead ( 400 , { 'Content-Type' : 'application/json' } )
758
+ . end ( JSON . stringify ( error . toResponseObject ( ) ) ) ;
759
+ return ;
760
+ }
761
+
762
+ if ( req . url !== "/" ) {
763
+ res . writeHead ( 404 ) . end ( ) ;
764
+ return ;
765
+ }
766
+ res . writeHead ( 401 ) . end ( ) ;
767
+ } ) ;
768
+
769
+ await new Promise < void > ( resolve => {
770
+ server . listen ( 0 , "127.0.0.1" , ( ) => {
771
+ const addr = server . address ( ) as AddressInfo ;
772
+ baseUrl = new URL ( `http://127.0.0.1:${ addr . port } ` ) ;
773
+ resolve ( ) ;
774
+ } ) ;
775
+ } ) ;
776
+
777
+ transport = new SSEClientTransport ( baseUrl , {
778
+ authProvider : mockAuthProvider ,
779
+ } ) ;
780
+
781
+ await expect ( ( ) => transport . start ( ) ) . rejects . toThrow ( InvalidClientError ) ;
782
+ expect ( mockAuthProvider . invalidateCredentials ) . toHaveBeenCalledWith ( 'all' ) ;
783
+ } ) ;
784
+
785
+ it ( "invalidates all credentials on UnauthorizedClientError during token refresh" , async ( ) => {
786
+ // Mock tokens() to return token with refresh token
787
+ mockAuthProvider . tokens . mockResolvedValue ( {
788
+ access_token : "expired-token" ,
789
+ token_type : "Bearer" ,
790
+ refresh_token : "refresh-token"
791
+ } ) ;
792
+
793
+ // Create server that returns UnauthorizedClientError on token refresh
794
+ await server . close ( ) ;
795
+
796
+ server = createServer ( ( req , res ) => {
797
+ lastServerRequest = req ;
798
+
799
+ // Handle OAuth metadata discovery
800
+ if ( req . url === "/.well-known/oauth-authorization-server" && req . method === "GET" ) {
801
+ res . writeHead ( 200 , { 'Content-Type' : 'application/json' } ) ;
802
+ res . end ( JSON . stringify ( {
803
+ issuer : baseUrl . href ,
804
+ authorization_endpoint : `${ baseUrl . href } authorize` ,
805
+ token_endpoint : `${ baseUrl . href } token` ,
806
+ response_types_supported : [ "code" ] ,
807
+ code_challenge_methods_supported : [ "S256" ] ,
808
+ } ) ) ;
809
+ return ;
810
+ }
811
+
812
+ if ( req . url === "/token" && req . method === "POST" ) {
813
+ // Handle token refresh request - return UnauthorizedClientError
814
+ const error = new UnauthorizedClientError ( "Client not authorized" ) ;
815
+ res . writeHead ( 400 , { 'Content-Type' : 'application/json' } )
816
+ . end ( JSON . stringify ( error . toResponseObject ( ) ) ) ;
817
+ return ;
818
+ }
819
+
820
+ if ( req . url !== "/" ) {
821
+ res . writeHead ( 404 ) . end ( ) ;
822
+ return ;
823
+ }
824
+ res . writeHead ( 401 ) . end ( ) ;
825
+ } ) ;
826
+
827
+ await new Promise < void > ( resolve => {
828
+ server . listen ( 0 , "127.0.0.1" , ( ) => {
829
+ const addr = server . address ( ) as AddressInfo ;
830
+ baseUrl = new URL ( `http://127.0.0.1:${ addr . port } ` ) ;
831
+ resolve ( ) ;
832
+ } ) ;
833
+ } ) ;
834
+
835
+ transport = new SSEClientTransport ( baseUrl , {
836
+ authProvider : mockAuthProvider ,
837
+ } ) ;
838
+
839
+ await expect ( ( ) => transport . start ( ) ) . rejects . toThrow ( UnauthorizedClientError ) ;
840
+ expect ( mockAuthProvider . invalidateCredentials ) . toHaveBeenCalledWith ( 'all' ) ;
841
+ } ) ;
842
+
843
+ it ( "invalidates tokens on InvalidGrantError during token refresh" , async ( ) => {
844
+ // Mock tokens() to return token with refresh token
845
+ mockAuthProvider . tokens . mockResolvedValue ( {
846
+ access_token : "expired-token" ,
847
+ token_type : "Bearer" ,
848
+ refresh_token : "refresh-token"
849
+ } ) ;
850
+
851
+ // Create server that returns InvalidGrantError on token refresh
852
+ await server . close ( ) ;
853
+
854
+ server = createServer ( ( req , res ) => {
855
+ lastServerRequest = req ;
856
+
857
+ // Handle OAuth metadata discovery
858
+ if ( req . url === "/.well-known/oauth-authorization-server" && req . method === "GET" ) {
859
+ res . writeHead ( 200 , { 'Content-Type' : 'application/json' } ) ;
860
+ res . end ( JSON . stringify ( {
861
+ issuer : baseUrl . href ,
862
+ authorization_endpoint : `${ baseUrl . href } authorize` ,
863
+ token_endpoint : `${ baseUrl . href } token` ,
864
+ response_types_supported : [ "code" ] ,
865
+ code_challenge_methods_supported : [ "S256" ] ,
866
+ } ) ) ;
867
+ return ;
868
+ }
869
+
870
+ if ( req . url === "/token" && req . method === "POST" ) {
871
+ // Handle token refresh request - return InvalidGrantError
872
+ const error = new InvalidGrantError ( "Invalid refresh token" ) ;
873
+ res . writeHead ( 400 , { 'Content-Type' : 'application/json' } )
874
+ . end ( JSON . stringify ( error . toResponseObject ( ) ) ) ;
875
+ return ;
876
+ }
877
+
878
+ if ( req . url !== "/" ) {
879
+ res . writeHead ( 404 ) . end ( ) ;
880
+ return ;
881
+ }
882
+ res . writeHead ( 401 ) . end ( ) ;
883
+ } ) ;
884
+
885
+ await new Promise < void > ( resolve => {
886
+ server . listen ( 0 , "127.0.0.1" , ( ) => {
887
+ const addr = server . address ( ) as AddressInfo ;
888
+ baseUrl = new URL ( `http://127.0.0.1:${ addr . port } ` ) ;
889
+ resolve ( ) ;
890
+ } ) ;
891
+ } ) ;
892
+
893
+ transport = new SSEClientTransport ( baseUrl , {
894
+ authProvider : mockAuthProvider ,
895
+ } ) ;
896
+
897
+ await expect ( ( ) => transport . start ( ) ) . rejects . toThrow ( InvalidGrantError ) ;
898
+ expect ( mockAuthProvider . invalidateCredentials ) . toHaveBeenCalledWith ( 'tokens' ) ;
899
+ } ) ;
724
900
} ) ;
725
901
} ) ;
0 commit comments