@@ -713,99 +713,252 @@ describe("OAuth Authorization", () => {
713713 } ) ;
714714
715715 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+ } ;
731732
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+ } ) ;
734811 } ) ;
735812
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+ } ;
742822
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+ } ;
744829
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+ } ;
780836
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+ } ) ;
782867 } ) ;
783868
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" } ) ;
788886
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 ( ) ;
792893 } ) ;
793894
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+ } ;
796910
797- // Verify the sequence of calls
798- expect ( mockFetch ) . toHaveBeenCalledTimes ( 3 ) ;
911+ const result = await auth ( mockProvider , { serverUrl : "https://auth.example.com" } ) ;
799912
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+ } ) ;
804917
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+ } ) ;
809962 } ) ;
810963 } ) ;
811964} ) ;
0 commit comments