@@ -713,99 +713,252 @@ describe("OAuth Authorization", () => {
713
713
} ) ;
714
714
715
715
describe ( "auth function" , ( ) => {
716
- const mockProvider : OAuthClientProvider = {
717
- get redirectUrl ( ) { return "http://localhost:3000/callback" ; } ,
718
- get clientMetadata ( ) {
719
- return {
720
- redirect_uris : [ "http://localhost:3000/callback" ] ,
721
- client_name : "Test Client" ,
722
- } ;
723
- } ,
724
- clientInformation : jest . fn ( ) ,
725
- tokens : jest . fn ( ) ,
726
- saveTokens : jest . fn ( ) ,
727
- redirectToAuthorization : jest . fn ( ) ,
728
- saveCodeVerifier : jest . fn ( ) ,
729
- codeVerifier : jest . fn ( ) ,
730
- } ;
716
+ describe ( "well-known discovery" , ( ) => {
717
+ const mockProvider : OAuthClientProvider = {
718
+ get redirectUrl ( ) { return "http://localhost:3000/callback" ; } ,
719
+ get clientMetadata ( ) {
720
+ return {
721
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
722
+ client_name : "Test Client" ,
723
+ } ;
724
+ } ,
725
+ clientInformation : jest . fn ( ) ,
726
+ tokens : jest . fn ( ) ,
727
+ saveTokens : jest . fn ( ) ,
728
+ redirectToAuthorization : jest . fn ( ) ,
729
+ saveCodeVerifier : jest . fn ( ) ,
730
+ codeVerifier : jest . fn ( ) ,
731
+ } ;
731
732
732
- beforeEach ( ( ) => {
733
- jest . clearAllMocks ( ) ;
733
+ beforeEach ( ( ) => {
734
+ jest . clearAllMocks ( ) ;
735
+ } ) ;
736
+
737
+ it ( "falls back to /.well-known/oauth-authorization-server when no protected-resource-metadata" , async ( ) => {
738
+ // Setup: First call to protected resource metadata fails (404)
739
+ // Second call to auth server metadata succeeds
740
+ let callCount = 0 ;
741
+ mockFetch . mockImplementation ( ( url ) => {
742
+ callCount ++ ;
743
+
744
+ const urlString = url . toString ( ) ;
745
+
746
+ if ( callCount === 1 && urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
747
+ // First call - protected resource metadata fails with 404
748
+ return Promise . resolve ( {
749
+ ok : false ,
750
+ status : 404 ,
751
+ } ) ;
752
+ } else if ( callCount === 2 && urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
753
+ // Second call - auth server metadata succeeds
754
+ return Promise . resolve ( {
755
+ ok : true ,
756
+ status : 200 ,
757
+ json : async ( ) => ( {
758
+ issuer : "https://auth.example.com" ,
759
+ authorization_endpoint : "https://auth.example.com/authorize" ,
760
+ token_endpoint : "https://auth.example.com/token" ,
761
+ registration_endpoint : "https://auth.example.com/register" ,
762
+ response_types_supported : [ "code" ] ,
763
+ code_challenge_methods_supported : [ "S256" ] ,
764
+ } ) ,
765
+ } ) ;
766
+ } else if ( callCount === 3 && urlString . includes ( "/register" ) ) {
767
+ // Third call - client registration succeeds
768
+ return Promise . resolve ( {
769
+ ok : true ,
770
+ status : 200 ,
771
+ json : async ( ) => ( {
772
+ client_id : "test-client-id" ,
773
+ client_secret : "test-client-secret" ,
774
+ client_id_issued_at : 1612137600 ,
775
+ client_secret_expires_at : 1612224000 ,
776
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
777
+ client_name : "Test Client" ,
778
+ } ) ,
779
+ } ) ;
780
+ }
781
+
782
+ return Promise . reject ( new Error ( `Unexpected fetch call: ${ urlString } ` ) ) ;
783
+ } ) ;
784
+
785
+ // Mock provider methods
786
+ ( mockProvider . clientInformation as jest . Mock ) . mockResolvedValue ( undefined ) ;
787
+ ( mockProvider . tokens as jest . Mock ) . mockResolvedValue ( undefined ) ;
788
+ mockProvider . saveClientInformation = jest . fn ( ) ;
789
+
790
+ // Call the auth function
791
+ const result = await auth ( mockProvider , {
792
+ serverUrl : "https://resource.example.com" ,
793
+ } ) ;
794
+
795
+ // Verify the result
796
+ expect ( result ) . toBe ( "REDIRECT" ) ;
797
+
798
+ // Verify the sequence of calls
799
+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 3 ) ;
800
+
801
+ // First call should be to protected resource metadata
802
+ expect ( mockFetch . mock . calls [ 0 ] [ 0 ] . toString ( ) ) . toBe (
803
+ "https://resource.example.com/.well-known/oauth-protected-resource"
804
+ ) ;
805
+
806
+ // Second call should be to oauth metadata
807
+ expect ( mockFetch . mock . calls [ 1 ] [ 0 ] . toString ( ) ) . toBe (
808
+ "https://resource.example.com/.well-known/oauth-authorization-server"
809
+ ) ;
810
+ } ) ;
734
811
} ) ;
735
812
736
- it ( "falls back to /.well-known/oauth-authorization-server when no protected-resource-metadata" , async ( ) => {
737
- // Setup: First call to protected resource metadata fails (404)
738
- // Second call to auth server metadata succeeds
739
- let callCount = 0 ;
740
- mockFetch . mockImplementation ( ( url ) => {
741
- callCount ++ ;
813
+ describe ( "delegateAuthorization" , ( ) => {
814
+ const validMetadata = {
815
+ issuer : "https://auth.example.com" ,
816
+ authorization_endpoint : "https://auth.example.com/authorize" ,
817
+ token_endpoint : "https://auth.example.com/token" ,
818
+ registration_endpoint : "https://auth.example.com/register" ,
819
+ response_types_supported : [ "code" ] ,
820
+ code_challenge_methods_supported : [ "S256" ] ,
821
+ } ;
742
822
743
- const urlString = url . toString ( ) ;
823
+ const validClientInfo = {
824
+ client_id : "client123" ,
825
+ client_secret : "secret123" ,
826
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
827
+ client_name : "Test Client" ,
828
+ } ;
744
829
745
- if ( callCount === 1 && urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
746
- // First call - protected resource metadata fails with 404
747
- return Promise . resolve ( {
748
- ok : false ,
749
- status : 404 ,
750
- } ) ;
751
- } else if ( callCount === 2 && urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
752
- // Second call - auth server metadata succeeds
753
- return Promise . resolve ( {
754
- ok : true ,
755
- status : 200 ,
756
- json : async ( ) => ( {
757
- issuer : "https://auth.example.com" ,
758
- authorization_endpoint : "https://auth.example.com/authorize" ,
759
- token_endpoint : "https://auth.example.com/token" ,
760
- registration_endpoint : "https://auth.example.com/register" ,
761
- response_types_supported : [ "code" ] ,
762
- code_challenge_methods_supported : [ "S256" ] ,
763
- } ) ,
764
- } ) ;
765
- } else if ( callCount === 3 && urlString . includes ( "/register" ) ) {
766
- // Third call - client registration succeeds
767
- return Promise . resolve ( {
768
- ok : true ,
769
- status : 200 ,
770
- json : async ( ) => ( {
771
- client_id : "test-client-id" ,
772
- client_secret : "test-client-secret" ,
773
- client_id_issued_at : 1612137600 ,
774
- client_secret_expires_at : 1612224000 ,
775
- redirect_uris : [ "http://localhost:3000/callback" ] ,
776
- client_name : "Test Client" ,
777
- } ) ,
778
- } ) ;
779
- }
830
+ const validTokens = {
831
+ access_token : "access123" ,
832
+ token_type : "Bearer" ,
833
+ expires_in : 3600 ,
834
+ refresh_token : "refresh123" ,
835
+ } ;
780
836
781
- return Promise . reject ( new Error ( `Unexpected fetch call: ${ urlString } ` ) ) ;
837
+ // Setup shared mock function for all tests
838
+ beforeEach ( ( ) => {
839
+ // Reset mockFetch implementation
840
+ mockFetch . mockReset ( ) ;
841
+
842
+ // Set up the mockFetch to respond to all necessary API calls
843
+ mockFetch . mockImplementation ( ( url ) => {
844
+ const urlString = url . toString ( ) ;
845
+
846
+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
847
+ return Promise . resolve ( {
848
+ ok : false ,
849
+ status : 404
850
+ } ) ;
851
+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
852
+ return Promise . resolve ( {
853
+ ok : true ,
854
+ status : 200 ,
855
+ json : async ( ) => validMetadata
856
+ } ) ;
857
+ } else if ( urlString . includes ( "/token" ) ) {
858
+ return Promise . resolve ( {
859
+ ok : true ,
860
+ status : 200 ,
861
+ json : async ( ) => validTokens
862
+ } ) ;
863
+ }
864
+
865
+ return Promise . reject ( new Error ( `Unexpected fetch call: ${ urlString } ` ) ) ;
866
+ } ) ;
782
867
} ) ;
783
868
784
- // Mock provider methods
785
- ( mockProvider . clientInformation as jest . Mock ) . mockResolvedValue ( undefined ) ;
786
- ( mockProvider . tokens as jest . Mock ) . mockResolvedValue ( undefined ) ;
787
- mockProvider . saveClientInformation = jest . fn ( ) ;
869
+ it ( "should use delegateAuthorization when implemented and return AUTHORIZED" , async ( ) => {
870
+ const mockProvider : OAuthClientProvider = {
871
+ redirectUrl : "http://localhost:3000/callback" ,
872
+ clientMetadata : {
873
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
874
+ client_name : "Test Client"
875
+ } ,
876
+ clientInformation : ( ) => validClientInfo ,
877
+ tokens : ( ) => validTokens ,
878
+ saveTokens : jest . fn ( ) ,
879
+ redirectToAuthorization : jest . fn ( ) ,
880
+ saveCodeVerifier : jest . fn ( ) ,
881
+ codeVerifier : ( ) => "test_verifier" ,
882
+ delegateAuthorization : jest . fn ( ) . mockResolvedValue ( "AUTHORIZED" )
883
+ } ;
884
+
885
+ const result = await auth ( mockProvider , { serverUrl : "https://auth.example.com" } ) ;
788
886
789
- // Call the auth function
790
- const result = await auth ( mockProvider , {
791
- serverUrl : "https://resource.example.com" ,
887
+ expect ( result ) . toBe ( "AUTHORIZED" ) ;
888
+ expect ( mockProvider . delegateAuthorization ) . toHaveBeenCalledWith (
889
+ "https://auth.example.com" ,
890
+ expect . objectContaining ( validMetadata )
891
+ ) ;
892
+ expect ( mockProvider . redirectToAuthorization ) . not . toHaveBeenCalled ( ) ;
792
893
} ) ;
793
894
794
- // Verify the result
795
- expect ( result ) . toBe ( "REDIRECT" ) ;
895
+ it ( "should fall back to standard flow when delegateAuthorization returns undefined" , async ( ) => {
896
+ const mockProvider : OAuthClientProvider = {
897
+ redirectUrl : "http://localhost:3000/callback" ,
898
+ clientMetadata : {
899
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
900
+ client_name : "Test Client"
901
+ } ,
902
+ clientInformation : ( ) => validClientInfo ,
903
+ tokens : ( ) => validTokens ,
904
+ saveTokens : jest . fn ( ) ,
905
+ redirectToAuthorization : jest . fn ( ) ,
906
+ saveCodeVerifier : jest . fn ( ) ,
907
+ codeVerifier : ( ) => "test_verifier" ,
908
+ delegateAuthorization : jest . fn ( ) . mockResolvedValue ( undefined )
909
+ } ;
796
910
797
- // Verify the sequence of calls
798
- expect ( mockFetch ) . toHaveBeenCalledTimes ( 3 ) ;
911
+ const result = await auth ( mockProvider , { serverUrl : "https://auth.example.com" } ) ;
799
912
800
- // First call should be to protected resource metadata
801
- expect ( mockFetch . mock . calls [ 0 ] [ 0 ] . toString ( ) ) . toBe (
802
- "https://resource.example.com/.well-known/oauth-protected-resource"
803
- ) ;
913
+ expect ( result ) . toBe ( "AUTHORIZED" ) ;
914
+ expect ( mockProvider . delegateAuthorization ) . toHaveBeenCalled ( ) ;
915
+ expect ( mockProvider . saveTokens ) . toHaveBeenCalled ( ) ;
916
+ } ) ;
804
917
805
- // Second call should be to oauth metadata
806
- expect ( mockFetch . mock . calls [ 1 ] [ 0 ] . toString ( ) ) . toBe (
807
- "https://resource.example.com/.well-known/oauth-authorization-server"
808
- ) ;
918
+ it ( "should not call delegateAuthorization when processing authorizationCode" , async ( ) => {
919
+ const mockProvider : OAuthClientProvider = {
920
+ redirectUrl : "http://localhost:3000/callback" ,
921
+ clientMetadata : {
922
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
923
+ client_name : "Test Client"
924
+ } ,
925
+ clientInformation : ( ) => validClientInfo ,
926
+ tokens : jest . fn ( ) ,
927
+ saveTokens : jest . fn ( ) ,
928
+ redirectToAuthorization : jest . fn ( ) ,
929
+ saveCodeVerifier : jest . fn ( ) ,
930
+ codeVerifier : ( ) => "test_verifier" ,
931
+ delegateAuthorization : jest . fn ( )
932
+ } ;
933
+
934
+ await auth ( mockProvider , {
935
+ serverUrl : "https://auth.example.com" ,
936
+ authorizationCode : "code123"
937
+ } ) ;
938
+
939
+ expect ( mockProvider . delegateAuthorization ) . not . toHaveBeenCalled ( ) ;
940
+ expect ( mockProvider . saveTokens ) . toHaveBeenCalled ( ) ;
941
+ } ) ;
942
+
943
+ it ( "should propagate errors from delegateAuthorization" , async ( ) => {
944
+ const mockProvider : OAuthClientProvider = {
945
+ redirectUrl : "http://localhost:3000/callback" ,
946
+ clientMetadata : {
947
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
948
+ client_name : "Test Client"
949
+ } ,
950
+ clientInformation : ( ) => validClientInfo ,
951
+ tokens : jest . fn ( ) ,
952
+ saveTokens : jest . fn ( ) ,
953
+ redirectToAuthorization : jest . fn ( ) ,
954
+ saveCodeVerifier : jest . fn ( ) ,
955
+ codeVerifier : ( ) => "test_verifier" ,
956
+ delegateAuthorization : jest . fn ( ) . mockRejectedValue ( new Error ( "Delegation failed" ) )
957
+ } ;
958
+
959
+ await expect ( auth ( mockProvider , { serverUrl : "https://auth.example.com" } ) )
960
+ . rejects . toThrow ( "Delegation failed" ) ;
961
+ } ) ;
809
962
} ) ;
810
963
} ) ;
811
964
} ) ;
0 commit comments