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