@@ -53,13 +53,63 @@ const parameters = {
5353const url =
5454 'https://localhost:3000/callback?usid=048adcfb-aa93-4978-be9e-09cb569fdcb9&code=J2lHm0cgXmnXpwDhjhLoyLJBoUAlBfxDY-AhjqGMC-o' ;
5555
56+ const createMockResponse = (
57+ body : unknown ,
58+ setCookieHeaders ?: string [ ]
59+ ) : Response =>
60+ ( {
61+ ok : true ,
62+ status : 200 ,
63+ text : jest . fn ( ) . mockResolvedValue ( JSON . stringify ( body ) ) ,
64+ headers : {
65+ get : jest . fn ( ( name : string ) =>
66+ name === 'set-cookie' && setCookieHeaders
67+ ? setCookieHeaders . join ( ', ' )
68+ : null
69+ ) ,
70+ getSetCookie : setCookieHeaders
71+ ? jest . fn ( ( ) => setCookieHeaders )
72+ : undefined ,
73+ } ,
74+ } as unknown as Response ) ;
75+
76+ // Simulates the middleware stripping tokens from the body and setting them as httpOnly cookies
77+ const strippedTokenResponse = {
78+ id_token : 'id_token' ,
79+ expires_in : 0 ,
80+ refresh_token_expires_in : 0 ,
81+ token_type : 'Bearer' ,
82+ usid : 'usid' ,
83+ customer_id : 'customer_id' ,
84+ enc_user_id : 'enc_user_id' ,
85+ } ;
86+
87+ const mockSetCookieHeaders = [
88+ 'cc-at_site_id=access_token; Path=/; HttpOnly; Secure; SameSite=lax' ,
89+ 'cc-nx-g_site_id=refresh_token; Path=/; HttpOnly; Secure; SameSite=lax' ,
90+ 'idp_access_token_site_id=idp; Path=/; HttpOnly; Secure; SameSite=lax' ,
91+ ] ;
92+
5693const authenticateCustomerMock = jest . fn ( ( ) => ( { url} ) ) ;
5794
95+ // Returns parsed TokenResponse directly (enableHttpOnlySessionCookies=false)
5896const getAccessTokenMock = jest . fn ( ( ) => expectedTokenResponse ) ;
97+
98+ // Simulates httpOnly mode: returns raw Response with tokens stripped
99+ // from body and placed in Set-Cookie headers (enableHttpOnlySessionCookies=true)
100+ const getAccessTokenMockHttpOnlySessionCookies = jest . fn ( ( ) =>
101+ createMockResponse ( strippedTokenResponse , mockSetCookieHeaders )
102+ ) ;
103+
59104const logoutCustomerMock = jest . fn ( ( ) => expectedTokenResponse ) ;
60105const generateCodeChallengeMock = jest . fn ( ( ) => 'code_challenge' ) ;
61106const authorizePasswordlessCustomerMock = jest . fn ( ) ;
62- const getPasswordLessAccessTokenMock = jest . fn ( ) ;
107+
108+ const getPasswordLessAccessTokenMock = jest . fn ( ( ) => expectedTokenResponse ) ;
109+
110+ const getPasswordLessAccessTokenMockHttpOnlySessionCookies = jest . fn ( ( ) =>
111+ createMockResponse ( strippedTokenResponse , mockSetCookieHeaders )
112+ ) ;
63113
64114const createMockSlasClient = ( ) =>
65115 ( {
@@ -84,6 +134,32 @@ const createMockSlasClient = () =>
84134 siteId : string ;
85135 } > ) ;
86136
137+ // Mock client for httpOnly session cookies tests.
138+ // Uses mocks that return raw Response with tokens in Set-Cookie headers.
139+ const createMockSlasClientHttpOnlySessionCookies = ( ) =>
140+ ( {
141+ clientConfig : {
142+ parameters : {
143+ shortCode : 'short_code' ,
144+ organizationId : 'organization_id' ,
145+ clientId : 'client_id' ,
146+ siteId : 'site_id' ,
147+ } ,
148+ } ,
149+ authenticateCustomer : authenticateCustomerMock ,
150+ getAccessToken : getAccessTokenMockHttpOnlySessionCookies ,
151+ logoutCustomer : logoutCustomerMock ,
152+ generateCodeChallenge : generateCodeChallengeMock ,
153+ authorizePasswordlessCustomer : authorizePasswordlessCustomerMock ,
154+ getPasswordLessAccessToken :
155+ getPasswordLessAccessTokenMockHttpOnlySessionCookies ,
156+ } as unknown as ShopperLogin < {
157+ shortCode : string ;
158+ organizationId : string ;
159+ clientId : string ;
160+ siteId : string ;
161+ } > ) ;
162+
87163const mockIsBrowserTrue = {
88164 isBrowser : true ,
89165} ;
@@ -724,7 +800,7 @@ describe('Registered B2C user flow', () => {
724800 credentials,
725801 parameters : registeredUserFlowParams ,
726802 } ) ;
727- expect ( accessToken ) . toStrictEqual ( expectedTokenResponse ) ;
803+ expect ( accessToken ) . toBe ( expectedTokenResponse ) ;
728804 } ) ;
729805} ) ;
730806
@@ -968,7 +1044,7 @@ describe('getPasswordLessAccessToken is working', () => {
9681044} ) ;
9691045
9701046describe ( 'Refresh Token' , ( ) => {
971- test ( 'refreshes the token with slas public client' , ( ) => {
1047+ test ( 'refreshes the token with slas public client' , async ( ) => {
9721048 const expectedBody = {
9731049 body : {
9741050 client_id : 'client_id' ,
@@ -978,15 +1054,15 @@ describe('Refresh Token', () => {
9781054 dnt : 'false' ,
9791055 } ,
9801056 } ;
981- const token = slasHelper . refreshAccessToken ( {
1057+ const token = await slasHelper . refreshAccessToken ( {
9821058 slasClient : createMockSlasClient ( ) ,
9831059 parameters,
9841060 } ) ;
9851061 expect ( getAccessTokenMock ) . toBeCalledWith ( expectedBody ) ;
9861062 expect ( token ) . toStrictEqual ( expectedTokenResponse ) ;
9871063 } ) ;
9881064
989- test ( 'refreshes the token with slas private client' , ( ) => {
1065+ test ( 'refreshes the token with slas private client' , async ( ) => {
9901066 const expectedReqOpts = {
9911067 headers : {
9921068 Authorization : `Basic ${ stringToBase64 (
@@ -1001,7 +1077,7 @@ describe('Refresh Token', () => {
10011077 dnt : 'false' ,
10021078 } ,
10031079 } ;
1004- const token = slasHelper . refreshAccessToken ( {
1080+ const token = await slasHelper . refreshAccessToken ( {
10051081 slasClient : createMockSlasClient ( ) ,
10061082 parameters,
10071083 credentials : {
@@ -1034,3 +1110,238 @@ describe('Logout', () => {
10341110 expect ( token ) . toStrictEqual ( expectedTokenResponse ) ;
10351111 } ) ;
10361112} ) ;
1113+
1114+ describe ( 'httpOnly session cookies' , ( ) => {
1115+ test ( 'loginGuestUser extracts tokens from Set-Cookie headers' , async ( ) => {
1116+ const mockSlasClient = createMockSlasClientHttpOnlySessionCookies ( ) ;
1117+ const { shortCode, organizationId} = mockSlasClient . clientConfig . parameters ;
1118+
1119+ nock ( `https://${ shortCode } .api.commercecloud.salesforce.com` )
1120+ . get ( `/shopper/auth/v1/organizations/${ organizationId } /oauth2/authorize` )
1121+ . query ( true )
1122+ . reply ( 303 , { response_body : 'response_body' } , { location : url } ) ;
1123+
1124+ const accessToken = await slasHelper . loginGuestUser ( {
1125+ slasClient : mockSlasClient ,
1126+ parameters : {
1127+ redirectURI : 'redirect_uri' ,
1128+ } ,
1129+ enableHttpOnlySessionCookies : true ,
1130+ } ) ;
1131+ expect ( getAccessTokenMockHttpOnlySessionCookies ) . toBeCalledWith (
1132+ expect . any ( Object ) ,
1133+ true
1134+ ) ;
1135+ expect ( accessToken . access_token ) . toBe ( 'access_token' ) ;
1136+ expect ( accessToken . refresh_token ) . toBe ( 'refresh_token' ) ;
1137+ expect ( accessToken . idp_access_token ) . toBe ( 'idp' ) ;
1138+ } ) ;
1139+
1140+ test ( 'loginGuestUserPrivate extracts tokens from Set-Cookie headers' , async ( ) => {
1141+ const accessToken = await slasHelper . loginGuestUserPrivate ( {
1142+ slasClient : createMockSlasClientHttpOnlySessionCookies ( ) ,
1143+ parameters : { } ,
1144+ credentials : { clientSecret : 'slas_private_secret' } ,
1145+ enableHttpOnlySessionCookies : true ,
1146+ } ) ;
1147+ expect ( getAccessTokenMockHttpOnlySessionCookies ) . toBeCalledWith (
1148+ expect . any ( Object ) ,
1149+ true
1150+ ) ;
1151+ expect ( accessToken . access_token ) . toBe ( 'access_token' ) ;
1152+ expect ( accessToken . refresh_token ) . toBe ( 'refresh_token' ) ;
1153+ } ) ;
1154+
1155+ test ( 'refreshAccessToken extracts tokens from Set-Cookie headers' , async ( ) => {
1156+ const token = await slasHelper . refreshAccessToken ( {
1157+ slasClient : createMockSlasClientHttpOnlySessionCookies ( ) ,
1158+ parameters : {
1159+ refreshToken : 'refresh_token' ,
1160+ dnt : false ,
1161+ } ,
1162+ enableHttpOnlySessionCookies : true ,
1163+ } ) ;
1164+ expect ( getAccessTokenMockHttpOnlySessionCookies ) . toBeCalledWith (
1165+ expect . any ( Object ) ,
1166+ true
1167+ ) ;
1168+ expect ( token . access_token ) . toBe ( 'access_token' ) ;
1169+ expect ( token . refresh_token ) . toBe ( 'refresh_token' ) ;
1170+ } ) ;
1171+
1172+ test ( 'refreshAccessToken with private client extracts tokens from Set-Cookie headers' , async ( ) => {
1173+ const token = await slasHelper . refreshAccessToken ( {
1174+ slasClient : createMockSlasClientHttpOnlySessionCookies ( ) ,
1175+ parameters : {
1176+ refreshToken : 'refresh_token' ,
1177+ dnt : false ,
1178+ } ,
1179+ credentials : { clientSecret : 'slas_private_secret' } ,
1180+ enableHttpOnlySessionCookies : true ,
1181+ } ) ;
1182+ expect ( getAccessTokenMockHttpOnlySessionCookies ) . toBeCalledWith (
1183+ expect . any ( Object ) ,
1184+ true
1185+ ) ;
1186+ expect ( token . access_token ) . toBe ( 'access_token' ) ;
1187+ expect ( token . refresh_token ) . toBe ( 'refresh_token' ) ;
1188+ } ) ;
1189+
1190+ test ( 'loginIDPUser with private client extracts tokens from Set-Cookie headers' , async ( ) => {
1191+ const accessToken = await slasHelper . loginIDPUser ( {
1192+ slasClient : createMockSlasClientHttpOnlySessionCookies ( ) ,
1193+ credentials : { clientSecret : credentialsPrivate . clientSecret } ,
1194+ parameters : {
1195+ redirectURI : 'redirect_uri' ,
1196+ code : 'J2lHm0cgXmnXpwDhjhLoyLJBoUAlBfxDY-AhjqGMC-o' ,
1197+ usid : '048adcfb-aa93-4978-be9e-09cb569fdcb9' ,
1198+ dnt : false ,
1199+ } ,
1200+ enableHttpOnlySessionCookies : true ,
1201+ } ) ;
1202+ expect ( getAccessTokenMockHttpOnlySessionCookies ) . toBeCalledWith (
1203+ expect . any ( Object ) ,
1204+ true
1205+ ) ;
1206+ expect ( accessToken . access_token ) . toBe ( 'access_token' ) ;
1207+ expect ( accessToken . refresh_token ) . toBe ( 'refresh_token' ) ;
1208+ expect ( accessToken . idp_access_token ) . toBe ( 'idp' ) ;
1209+ } ) ;
1210+
1211+ test ( 'loginIDPUser with public client extracts tokens from Set-Cookie headers' , async ( ) => {
1212+ const accessToken = await slasHelper . loginIDPUser ( {
1213+ slasClient : createMockSlasClientHttpOnlySessionCookies ( ) ,
1214+ credentials : { codeVerifier : 'code_verifier' } ,
1215+ parameters : {
1216+ redirectURI : 'redirect_uri' ,
1217+ code : 'J2lHm0cgXmnXpwDhjhLoyLJBoUAlBfxDY-AhjqGMC-o' ,
1218+ usid : '048adcfb-aa93-4978-be9e-09cb569fdcb9' ,
1219+ dnt : false ,
1220+ } ,
1221+ enableHttpOnlySessionCookies : true ,
1222+ } ) ;
1223+ expect ( getAccessTokenMockHttpOnlySessionCookies ) . toBeCalledWith (
1224+ expect . any ( Object ) ,
1225+ true
1226+ ) ;
1227+ expect ( accessToken . access_token ) . toBe ( 'access_token' ) ;
1228+ expect ( accessToken . refresh_token ) . toBe ( 'refresh_token' ) ;
1229+ } ) ;
1230+
1231+ test ( 'loginRegisteredUserB2C with public client extracts tokens from Set-Cookie headers' , async ( ) => {
1232+ const mockSlasClient = createMockSlasClientHttpOnlySessionCookies ( ) ;
1233+ const { shortCode, organizationId} = mockSlasClient . clientConfig . parameters ;
1234+
1235+ nock ( `https://${ shortCode } .api.commercecloud.salesforce.com` )
1236+ . post ( `/shopper/auth/v1/organizations/${ organizationId } /oauth2/login` )
1237+ . reply ( 303 , { response_body : 'response_body' } , { location : url } ) ;
1238+
1239+ const accessToken = await slasHelper . loginRegisteredUserB2C ( {
1240+ slasClient : mockSlasClient ,
1241+ credentials,
1242+ parameters : {
1243+ redirectURI : 'redirect_uri' ,
1244+ dnt : false ,
1245+ } ,
1246+ enableHttpOnlySessionCookies : true ,
1247+ } ) ;
1248+ expect ( getAccessTokenMockHttpOnlySessionCookies ) . toBeCalledWith (
1249+ expect . any ( Object ) ,
1250+ true
1251+ ) ;
1252+ expect ( accessToken . access_token ) . toBe ( 'access_token' ) ;
1253+ expect ( accessToken . refresh_token ) . toBe ( 'refresh_token' ) ;
1254+ } ) ;
1255+
1256+ test ( 'loginRegisteredUserB2C with private client extracts tokens from Set-Cookie headers' , async ( ) => {
1257+ const mockSlasClient = createMockSlasClientHttpOnlySessionCookies ( ) ;
1258+ const { shortCode, organizationId} = mockSlasClient . clientConfig . parameters ;
1259+
1260+ nock ( `https://${ shortCode } .api.commercecloud.salesforce.com` )
1261+ . post ( `/shopper/auth/v1/organizations/${ organizationId } /oauth2/login` )
1262+ . reply ( 303 , { response_body : 'response_body' } , { location : url } ) ;
1263+
1264+ const accessToken = await slasHelper . loginRegisteredUserB2C ( {
1265+ slasClient : mockSlasClient ,
1266+ credentials : credentialsPrivate ,
1267+ parameters : {
1268+ redirectURI : 'redirect_uri' ,
1269+ dnt : false ,
1270+ } ,
1271+ enableHttpOnlySessionCookies : true ,
1272+ } ) ;
1273+ expect ( getAccessTokenMockHttpOnlySessionCookies ) . toBeCalledWith (
1274+ expect . any ( Object ) ,
1275+ true
1276+ ) ;
1277+ expect ( accessToken . access_token ) . toBe ( 'access_token' ) ;
1278+ expect ( accessToken . refresh_token ) . toBe ( 'refresh_token' ) ;
1279+ } ) ;
1280+
1281+ test ( 'getPasswordLessAccessToken extracts tokens from Set-Cookie headers' , async ( ) => {
1282+ const token = await slasHelper . getPasswordLessAccessToken ( {
1283+ slasClient : createMockSlasClientHttpOnlySessionCookies ( ) ,
1284+ credentials : { clientSecret : 'slas_private_secret' } ,
1285+ parameters : {
1286+ pwdlessLoginToken : 'pwdless_token' ,
1287+ } ,
1288+ enableHttpOnlySessionCookies : true ,
1289+ } ) ;
1290+ expect ( getPasswordLessAccessTokenMockHttpOnlySessionCookies ) . toBeCalledWith (
1291+ expect . any ( Object ) ,
1292+ true
1293+ ) ;
1294+ expect ( token . access_token ) . toBe ( 'access_token' ) ;
1295+ expect ( token . refresh_token ) . toBe ( 'refresh_token' ) ;
1296+ } ) ;
1297+
1298+ test ( 'does not extract tokens from Set-Cookie headers on browser (isBrowser=true)' , async ( ) => {
1299+ // Re-import slasHelper with isBrowser mocked to true
1300+ jest . resetModules ( ) ;
1301+ // Provide btoa for the browser mock since stringToBase64 uses it when isBrowser=true
1302+ global . btoa = ( str : string ) => Buffer . from ( str ) . toString ( 'base64' ) ;
1303+ jest . doMock ( './environment' , ( ) => ( { isBrowser : true } ) ) ;
1304+
1305+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
1306+ const browserSlasHelper = require ( './slasHelper' ) as typeof slasHelper ;
1307+
1308+ // Mock client that returns stripped body + Set-Cookie headers
1309+ const mockGetAccessToken = jest . fn ( ( ) =>
1310+ createMockResponse ( strippedTokenResponse , mockSetCookieHeaders )
1311+ ) ;
1312+ const mockSlasClient = {
1313+ clientConfig : {
1314+ parameters : {
1315+ shortCode : 'short_code' ,
1316+ organizationId : 'organization_id' ,
1317+ clientId : 'client_id' ,
1318+ siteId : 'site_id' ,
1319+ } ,
1320+ } ,
1321+ getAccessToken : mockGetAccessToken ,
1322+ } as unknown as ShopperLogin < {
1323+ shortCode : string ;
1324+ organizationId : string ;
1325+ clientId : string ;
1326+ siteId : string ;
1327+ } > ;
1328+
1329+ const accessToken = await browserSlasHelper . loginGuestUserPrivate ( {
1330+ slasClient : mockSlasClient ,
1331+ parameters : { } ,
1332+ credentials : { clientSecret : 'slas_private_secret' } ,
1333+ enableHttpOnlySessionCookies : true ,
1334+ } ) ;
1335+
1336+ // On browser: tokens should NOT be extracted from Set-Cookie headers
1337+ // The stripped body is returned as-is (browser relies on httpOnly cookies)
1338+ expect ( accessToken . access_token ) . toBeUndefined ( ) ;
1339+ expect ( accessToken . refresh_token ) . toBeUndefined ( ) ;
1340+ expect ( accessToken . customer_id ) . toBe ( 'customer_id' ) ;
1341+
1342+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1343+ // @ts -ignore
1344+ delete global . btoa ;
1345+ jest . restoreAllMocks ( ) ;
1346+ } ) ;
1347+ } ) ;
0 commit comments