@@ -922,4 +922,232 @@ describe("OAuth Authorization", () => {
922
922
) ;
923
923
} ) ;
924
924
} ) ;
925
+
926
+ describe ( "exchangeAuthorization with multiple client authentication methods" , ( ) => {
927
+ const validTokens = {
928
+ access_token : "access123" ,
929
+ token_type : "Bearer" ,
930
+ expires_in : 3600 ,
931
+ refresh_token : "refresh123" ,
932
+ } ;
933
+
934
+ const validClientInfo = {
935
+ client_id : "client123" ,
936
+ client_secret : "secret123" ,
937
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
938
+ client_name : "Test Client" ,
939
+ } ;
940
+
941
+ const metadataWithBasicOnly = {
942
+ issuer : "https://auth.example.com" ,
943
+ authorization_endpoint : "https://auth.example.com/auth" ,
944
+ token_endpoint : "https://auth.example.com/token" ,
945
+ response_types_supported : [ "code" ] ,
946
+ code_challenge_methods_supported : [ "S256" ] ,
947
+ token_endpoint_auth_methods_supported : [ "client_secret_basic" ] ,
948
+ } ;
949
+
950
+ const metadataWithPostOnly = {
951
+ ...metadataWithBasicOnly ,
952
+ token_endpoint_auth_methods_supported : [ "client_secret_post" ] ,
953
+ } ;
954
+
955
+ const metadataWithNoneOnly = {
956
+ ...metadataWithBasicOnly ,
957
+ token_endpoint_auth_methods_supported : [ "none" ] ,
958
+ } ;
959
+
960
+ it ( "uses HTTP Basic authentication when client_secret_basic is supported" , async ( ) => {
961
+ mockFetch . mockResolvedValueOnce ( {
962
+ ok : true ,
963
+ status : 200 ,
964
+ json : async ( ) => validTokens ,
965
+ } ) ;
966
+
967
+ const tokens = await exchangeAuthorization ( "https://auth.example.com" , {
968
+ metadata : metadataWithBasicOnly ,
969
+ clientInformation : validClientInfo ,
970
+ authorizationCode : "code123" ,
971
+ codeVerifier : "verifier123" ,
972
+ redirectUri : "http://localhost:3000/callback" ,
973
+ } ) ;
974
+
975
+ expect ( tokens ) . toEqual ( validTokens ) ;
976
+ const request = mockFetch . mock . calls [ 0 ] [ 1 ] ;
977
+
978
+ // Check Authorization header
979
+ const authHeader = request . headers [ "Authorization" ] ;
980
+ const expected = "Basic " + btoa ( "client123:secret123" ) ;
981
+ expect ( authHeader ) . toBe ( expected ) ;
982
+
983
+ const body = request . body as URLSearchParams ;
984
+ expect ( body . get ( "client_id" ) ) . toBeNull ( ) ; // should not be in body
985
+ expect ( body . get ( "client_secret" ) ) . toBeNull ( ) ; // should not be in body
986
+ } ) ;
987
+
988
+ it ( "includes credentials in request body when client_secret_post is supported" , async ( ) => {
989
+ mockFetch . mockResolvedValueOnce ( {
990
+ ok : true ,
991
+ status : 200 ,
992
+ json : async ( ) => validTokens ,
993
+ } ) ;
994
+
995
+ const tokens = await exchangeAuthorization ( "https://auth.example.com" , {
996
+ metadata : metadataWithPostOnly ,
997
+ clientInformation : validClientInfo ,
998
+ authorizationCode : "code123" ,
999
+ codeVerifier : "verifier123" ,
1000
+ redirectUri : "http://localhost:3000/callback" ,
1001
+ } ) ;
1002
+
1003
+ expect ( tokens ) . toEqual ( validTokens ) ;
1004
+ const request = mockFetch . mock . calls [ 0 ] [ 1 ] ;
1005
+
1006
+ // Check no Authorization header
1007
+ expect ( request . headers [ "Authorization" ] ) . toBeUndefined ( ) ;
1008
+
1009
+ const body = request . body as URLSearchParams ;
1010
+ expect ( body . get ( "client_id" ) ) . toBe ( "client123" ) ;
1011
+ expect ( body . get ( "client_secret" ) ) . toBe ( "secret123" ) ;
1012
+ } ) ;
1013
+
1014
+ it ( "uses public client authentication when none method is specified" , async ( ) => {
1015
+ mockFetch . mockResolvedValueOnce ( {
1016
+ ok : true ,
1017
+ status : 200 ,
1018
+ json : async ( ) => validTokens ,
1019
+ } ) ;
1020
+
1021
+ const clientInfoWithoutSecret = {
1022
+ client_id : "client123" ,
1023
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
1024
+ client_name : "Test Client" ,
1025
+ } ;
1026
+
1027
+ const tokens = await exchangeAuthorization ( "https://auth.example.com" , {
1028
+ metadata : metadataWithNoneOnly ,
1029
+ clientInformation : clientInfoWithoutSecret ,
1030
+ authorizationCode : "code123" ,
1031
+ codeVerifier : "verifier123" ,
1032
+ redirectUri : "http://localhost:3000/callback" ,
1033
+ } ) ;
1034
+
1035
+ expect ( tokens ) . toEqual ( validTokens ) ;
1036
+ const request = mockFetch . mock . calls [ 0 ] [ 1 ] ;
1037
+
1038
+ // Check no Authorization header
1039
+ expect ( request . headers [ "Authorization" ] ) . toBeUndefined ( ) ;
1040
+
1041
+ const body = request . body as URLSearchParams ;
1042
+ expect ( body . get ( "client_id" ) ) . toBe ( "client123" ) ;
1043
+ expect ( body . get ( "client_secret" ) ) . toBeNull ( ) ;
1044
+ } ) ;
1045
+
1046
+ it ( "defaults to client_secret_post when no auth methods specified" , async ( ) => {
1047
+ mockFetch . mockResolvedValueOnce ( {
1048
+ ok : true ,
1049
+ status : 200 ,
1050
+ json : async ( ) => validTokens ,
1051
+ } ) ;
1052
+
1053
+ const tokens = await exchangeAuthorization ( "https://auth.example.com" , {
1054
+ clientInformation : validClientInfo ,
1055
+ authorizationCode : "code123" ,
1056
+ codeVerifier : "verifier123" ,
1057
+ redirectUri : "http://localhost:3000/callback" ,
1058
+ } ) ;
1059
+
1060
+ expect ( tokens ) . toEqual ( validTokens ) ;
1061
+ const request = mockFetch . mock . calls [ 0 ] [ 1 ] ;
1062
+
1063
+ // Check no Authorization header
1064
+ expect ( request . headers [ "Authorization" ] ) . toBeUndefined ( ) ;
1065
+
1066
+ const body = request . body as URLSearchParams ;
1067
+ expect ( body . get ( "client_id" ) ) . toBe ( "client123" ) ;
1068
+ expect ( body . get ( "client_secret" ) ) . toBe ( "secret123" ) ;
1069
+ } ) ;
1070
+ } ) ;
1071
+
1072
+ describe ( "refreshAuthorization with multiple client authentication methods" , ( ) => {
1073
+ const validTokens = {
1074
+ access_token : "newaccess123" ,
1075
+ token_type : "Bearer" ,
1076
+ expires_in : 3600 ,
1077
+ refresh_token : "newrefresh123" ,
1078
+ } ;
1079
+
1080
+ const validClientInfo = {
1081
+ client_id : "client123" ,
1082
+ client_secret : "secret123" ,
1083
+ redirect_uris : [ "http://localhost:3000/callback" ] ,
1084
+ client_name : "Test Client" ,
1085
+ } ;
1086
+
1087
+ const metadataWithBasicOnly = {
1088
+ issuer : "https://auth.example.com" ,
1089
+ authorization_endpoint : "https://auth.example.com/auth" ,
1090
+ token_endpoint : "https://auth.example.com/token" ,
1091
+ response_types_supported : [ "code" ] ,
1092
+ token_endpoint_auth_methods_supported : [ "client_secret_basic" ] ,
1093
+ } ;
1094
+
1095
+ const metadataWithPostOnly = {
1096
+ ...metadataWithBasicOnly ,
1097
+ token_endpoint_auth_methods_supported : [ "client_secret_post" ] ,
1098
+ } ;
1099
+
1100
+ it ( "uses client_secret_basic for refresh token" , async ( ) => {
1101
+ mockFetch . mockResolvedValueOnce ( {
1102
+ ok : true ,
1103
+ status : 200 ,
1104
+ json : async ( ) => validTokens ,
1105
+ } ) ;
1106
+
1107
+ const tokens = await refreshAuthorization ( "https://auth.example.com" , {
1108
+ metadata : metadataWithBasicOnly ,
1109
+ clientInformation : validClientInfo ,
1110
+ refreshToken : "refresh123" ,
1111
+ } ) ;
1112
+
1113
+ expect ( tokens ) . toEqual ( validTokens ) ;
1114
+ const request = mockFetch . mock . calls [ 0 ] [ 1 ] ;
1115
+
1116
+ // Check Authorization header
1117
+ const authHeader = request . headers [ "Authorization" ] ;
1118
+ const expected = "Basic " + btoa ( "client123:secret123" ) ;
1119
+ expect ( authHeader ) . toBe ( expected ) ;
1120
+
1121
+ const body = request . body as URLSearchParams ;
1122
+ expect ( body . get ( "client_id" ) ) . toBeNull ( ) ; // should not be in body
1123
+ expect ( body . get ( "client_secret" ) ) . toBeNull ( ) ; // should not be in body
1124
+ expect ( body . get ( "refresh_token" ) ) . toBe ( "refresh123" ) ;
1125
+ } ) ;
1126
+
1127
+ it ( "uses client_secret_post for refresh token" , async ( ) => {
1128
+ mockFetch . mockResolvedValueOnce ( {
1129
+ ok : true ,
1130
+ status : 200 ,
1131
+ json : async ( ) => validTokens ,
1132
+ } ) ;
1133
+
1134
+ const tokens = await refreshAuthorization ( "https://auth.example.com" , {
1135
+ metadata : metadataWithPostOnly ,
1136
+ clientInformation : validClientInfo ,
1137
+ refreshToken : "refresh123" ,
1138
+ } ) ;
1139
+
1140
+ expect ( tokens ) . toEqual ( validTokens ) ;
1141
+ const request = mockFetch . mock . calls [ 0 ] [ 1 ] ;
1142
+
1143
+ // Check no Authorization header
1144
+ expect ( request . headers [ "Authorization" ] ) . toBeUndefined ( ) ;
1145
+
1146
+ const body = request . body as URLSearchParams ;
1147
+ expect ( body . get ( "client_id" ) ) . toBe ( "client123" ) ;
1148
+ expect ( body . get ( "client_secret" ) ) . toBe ( "secret123" ) ;
1149
+ expect ( body . get ( "refresh_token" ) ) . toBe ( "refresh123" ) ;
1150
+ } ) ;
1151
+ } ) ;
1152
+
925
1153
} ) ;
0 commit comments