@faststore/core 3.99.6 → 3.100.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +62 -55
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/prerender-manifest.js +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/react-loadable-manifest.json +6 -6
- package/.next/routes-manifest.json +1 -1
- package/.next/server/chunks/3779.js +1 -0
- package/.next/server/chunks/7098.js +2 -2
- package/.next/server/chunks/83.js +1 -1
- package/.next/server/chunks/948.js +3 -3
- package/.next/server/chunks/{9984.js → UINavbarSlider.js} +1 -1
- package/.next/server/edge-runtime-webpack.js +24 -0
- package/.next/server/edge-runtime-webpack.js.map +1 -1
- package/.next/server/instrumentation.js.nft.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-manifest.json +27 -2
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.js +1 -1
- package/.next/server/pages/404.js.nft.json +1 -1
- package/.next/server/pages/500.js +1 -1
- package/.next/server/pages/500.js.nft.json +1 -1
- package/.next/server/pages/[...slug].js +1 -1
- package/.next/server/pages/[...slug].js.nft.json +1 -1
- package/.next/server/pages/[slug]/p.js +1 -1
- package/.next/server/pages/[slug]/p.js.nft.json +1 -1
- package/.next/server/pages/_app.js.nft.json +1 -1
- package/.next/server/pages/_document.js.nft.json +1 -1
- package/.next/server/pages/_error.js.nft.json +1 -1
- package/.next/server/pages/api/fs/auth/login.js +1 -0
- package/.next/server/pages/api/fs/auth/login.js.nft.json +1 -0
- package/.next/server/pages/api/fs/logout.js.nft.json +1 -1
- package/.next/server/pages/api/graphql.js.nft.json +1 -1
- package/.next/server/pages/api/health/live.js.nft.json +1 -1
- package/.next/server/pages/api/health/ready.js.nft.json +1 -1
- package/.next/server/pages/api/preview.js.nft.json +1 -1
- package/.next/server/pages/checkout.js +1 -1
- package/.next/server/pages/checkout.js.nft.json +1 -1
- package/.next/server/pages/en-US/404.html +2 -2
- package/.next/server/pages/en-US/500.html +2 -2
- package/.next/server/pages/en-US/checkout.html +2 -2
- package/.next/server/pages/en-US/fs-auth-login.html +42 -0
- package/.next/server/pages/en-US/login.html +2 -2
- package/.next/server/pages/en-US/s.html +2 -2
- package/.next/server/pages/en-US.html +2 -2
- package/.next/server/pages/fs-auth-login.js.nft.json +1 -0
- package/.next/server/pages/index.js +1 -1
- package/.next/server/pages/index.js.nft.json +1 -1
- package/.next/server/pages/login.js +1 -1
- package/.next/server/pages/login.js.nft.json +1 -1
- package/.next/server/pages/pvt/account/403.js +1 -1
- package/.next/server/pages/pvt/account/403.js.nft.json +1 -1
- package/.next/server/pages/pvt/account/404.js +1 -1
- package/.next/server/pages/pvt/account/404.js.nft.json +1 -1
- package/.next/server/pages/pvt/account/[...unknown].js.nft.json +1 -1
- package/.next/server/pages/pvt/account/orders/[id].js +1 -1
- package/.next/server/pages/pvt/account/orders/[id].js.nft.json +1 -1
- package/.next/server/pages/pvt/account/orders.js +1 -1
- package/.next/server/pages/pvt/account/orders.js.nft.json +1 -1
- package/.next/server/pages/pvt/account/profile.js +1 -1
- package/.next/server/pages/pvt/account/profile.js.nft.json +1 -1
- package/.next/server/pages/pvt/account/security.js +1 -1
- package/.next/server/pages/pvt/account/security.js.nft.json +1 -1
- package/.next/server/pages/pvt/account/user-details.js +1 -1
- package/.next/server/pages/pvt/account/user-details.js.nft.json +1 -1
- package/.next/server/pages/pvt/account.js.nft.json +1 -1
- package/.next/server/pages/s.js +1 -1
- package/.next/server/pages/s.js.nft.json +1 -1
- package/.next/server/pages-manifest.json +1 -1
- package/.next/server/src/middleware.js +5417 -0
- package/.next/server/src/middleware.js.map +1 -0
- package/.next/server/webpack-runtime.js +1 -1
- package/.next/static/Lk9fl1r-H5eeVzeqYkA-Y/_buildManifest.js +1 -0
- package/.next/static/chunks/{2927.5a79877943a6bf7c.js → 2927.23bae2c79f0ac0f3.js} +1 -1
- package/.next/static/chunks/{9173-515913f3f10caf5e.js → 9173-1670c2bce097f620.js} +1 -1
- package/.next/static/chunks/{UIToast.19a8664c01a00d3a.js → UIToast.de15325248043ce5.js} +1 -1
- package/.next/static/chunks/main-083a618181bb5fbb.js +1 -0
- package/.next/static/chunks/pages/fs-auth-login-ba2d1b3ef0ef0415.js +1 -0
- package/.next/static/chunks/webpack-ff85816fb58111fa.js +1 -0
- package/.next/static/css/3bd4edd425711792.css +1 -0
- package/.next/trace +156 -143
- package/.turbo/turbo-build.log +44 -19
- package/.turbo/turbo-test.log +11 -10
- package/CHANGELOG.md +12 -0
- package/package.json +3 -2
- package/src/components/ui/Image/loader.ts +2 -2
- package/src/middleware.ts +87 -0
- package/src/pages/api/fs/auth/login.ts +123 -0
- package/src/pages/fs-auth-login/fs-auth-login.module.scss +41 -0
- package/src/pages/fs-auth-login/index.tsx +90 -0
- package/src/server/authentication-service.ts +270 -0
- package/src/server/password-protection/webops-api.ts +38 -0
- package/src/utils/loginResponse.ts +20 -0
- package/test/server/authentication-service.test.ts +460 -0
- package/.next/static/UmIFhJcdMOfy3xrHNNZS2/_buildManifest.js +0 -1
- package/.next/static/chunks/main-ec03882c4375091d.js +0 -1
- package/.next/static/chunks/webpack-65b1de5f075b8367.js +0 -1
- package/src/middleware__DISABLED.ts +0 -65
- /package/.next/static/{UmIFhJcdMOfy3xrHNNZS2 → Lk9fl1r-H5eeVzeqYkA-Y}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { decodeJwt, errors, importSPKI, jwtVerify } from 'jose'
|
|
2
|
+
import { NextRequest } from 'next/server'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AuthenticationService,
|
|
6
|
+
resetPasswordProtectionPublicKeyCacheForTests,
|
|
7
|
+
} from '../../src/server/authentication-service'
|
|
8
|
+
|
|
9
|
+
function setNodeEnv(value: string | undefined): void {
|
|
10
|
+
const env = process.env as Record<string, string | undefined>
|
|
11
|
+
env.NODE_ENV = value
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
jest.mock('../../discovery.config', () => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: {
|
|
17
|
+
api: { storeId: 'test-store' },
|
|
18
|
+
},
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
jest.mock('jose', () => {
|
|
22
|
+
class MockJWTExpired extends Error {
|
|
23
|
+
payload: Record<string, unknown>
|
|
24
|
+
claim: string
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
message: string,
|
|
28
|
+
payload: Record<string, unknown>,
|
|
29
|
+
claim = 'exp'
|
|
30
|
+
) {
|
|
31
|
+
super(message)
|
|
32
|
+
this.name = 'JWTExpired'
|
|
33
|
+
this.payload = payload
|
|
34
|
+
this.claim = claim
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
jwtVerify: jest.fn(),
|
|
40
|
+
importSPKI: jest.fn(),
|
|
41
|
+
decodeJwt: jest.fn(),
|
|
42
|
+
errors: {
|
|
43
|
+
JWTExpired: MockJWTExpired,
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const jwtVerifyMock = jwtVerify as jest.MockedFunction<typeof jwtVerify>
|
|
49
|
+
const importSPKIMock = importSPKI as jest.MockedFunction<typeof importSPKI>
|
|
50
|
+
const decodeJwtMock = decodeJwt as jest.MockedFunction<typeof decodeJwt>
|
|
51
|
+
|
|
52
|
+
function previewRequest(
|
|
53
|
+
path: string,
|
|
54
|
+
init?: ConstructorParameters<typeof NextRequest>[1]
|
|
55
|
+
): NextRequest {
|
|
56
|
+
return new NextRequest(`https://preview.vtex.app${path}`, {
|
|
57
|
+
...init,
|
|
58
|
+
headers: {
|
|
59
|
+
host: 'preview.vtex.app',
|
|
60
|
+
...init?.headers,
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('AuthenticationService', () => {
|
|
66
|
+
const originalNodeEnv = process.env.NODE_ENV
|
|
67
|
+
const originalCustomDomains = process.env.CUSTOM_DOMAINS_PROTECTION_ENABLED
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
setNodeEnv('production')
|
|
71
|
+
delete process.env.CUSTOM_DOMAINS_PROTECTION_ENABLED
|
|
72
|
+
resetPasswordProtectionPublicKeyCacheForTests()
|
|
73
|
+
jest.clearAllMocks()
|
|
74
|
+
importSPKIMock.mockResolvedValue({} as CryptoKey)
|
|
75
|
+
global.fetch = jest.fn()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
afterAll(() => {
|
|
79
|
+
setNodeEnv(originalNodeEnv)
|
|
80
|
+
process.env.CUSTOM_DOMAINS_PROTECTION_ENABLED = originalCustomDomains
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('skips protection in development', async () => {
|
|
84
|
+
setNodeEnv('development')
|
|
85
|
+
|
|
86
|
+
const service = new AuthenticationService()
|
|
87
|
+
const { response } = await service.authenticateRequest(previewRequest('/p'))
|
|
88
|
+
|
|
89
|
+
expect(response.status).toBe(200)
|
|
90
|
+
expect(global.fetch).not.toHaveBeenCalled()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('allows traffic when WebOps reports not protected and sets negative-cache cookie', async () => {
|
|
94
|
+
;(global.fetch as jest.Mock).mockResolvedValue({
|
|
95
|
+
ok: true,
|
|
96
|
+
json: async () => ({
|
|
97
|
+
protected: false,
|
|
98
|
+
token: 'neg-cache-token',
|
|
99
|
+
}),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const service = new AuthenticationService()
|
|
103
|
+
const { response } = await service.authenticateRequest(
|
|
104
|
+
previewRequest('/products')
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
expect(response.status).toBe(200)
|
|
108
|
+
expect(response.cookies.get('__fs_auth_token')?.value).toBe(
|
|
109
|
+
'neg-cache-token'
|
|
110
|
+
)
|
|
111
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
href: expect.stringMatching(
|
|
114
|
+
/\/api\/v1\/password-protection\/status.*storeId=test-store/
|
|
115
|
+
),
|
|
116
|
+
}),
|
|
117
|
+
expect.any(Object)
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('redirects to login when protected and host matches DEFAULT_DOMAINS scope', async () => {
|
|
122
|
+
;(global.fetch as jest.Mock).mockResolvedValue({
|
|
123
|
+
ok: true,
|
|
124
|
+
json: async () => ({
|
|
125
|
+
protected: true,
|
|
126
|
+
passwordVersion: 'v1',
|
|
127
|
+
scope: 'DEFAULT_DOMAINS',
|
|
128
|
+
}),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const service = new AuthenticationService()
|
|
132
|
+
const { response } = await service.authenticateRequest(
|
|
133
|
+
previewRequest('/secret')
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
expect(response.status).toBe(307)
|
|
137
|
+
const location = response.headers.get('location') ?? ''
|
|
138
|
+
expect(location).toContain('/fs-auth-login')
|
|
139
|
+
expect(location).toContain('returnTo=%2Fsecret')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('allows traffic when protected but scope does not apply to this host', async () => {
|
|
143
|
+
;(global.fetch as jest.Mock).mockResolvedValue({
|
|
144
|
+
ok: true,
|
|
145
|
+
json: async () => ({
|
|
146
|
+
protected: true,
|
|
147
|
+
scope: 'CUSTOM_DOMAINS',
|
|
148
|
+
}),
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const service = new AuthenticationService()
|
|
152
|
+
const { response } = await service.authenticateRequest(previewRequest('/p'))
|
|
153
|
+
|
|
154
|
+
expect(response.status).toBe(200)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('redirects to login when protected for ALL_DOMAINS on custom host', async () => {
|
|
158
|
+
process.env.CUSTOM_DOMAINS_PROTECTION_ENABLED = 'true'
|
|
159
|
+
;(global.fetch as jest.Mock).mockResolvedValue({
|
|
160
|
+
ok: true,
|
|
161
|
+
json: async () => ({
|
|
162
|
+
protected: true,
|
|
163
|
+
scope: 'ALL_DOMAINS',
|
|
164
|
+
}),
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const service = new AuthenticationService()
|
|
168
|
+
const { response } = await service.authenticateRequest(
|
|
169
|
+
new NextRequest('https://shop.example.com/checkout', {
|
|
170
|
+
headers: { host: 'shop.example.com' },
|
|
171
|
+
})
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
expect(response.status).toBe(307)
|
|
175
|
+
expect(response.headers.get('location')).toContain('returnTo=%2Fcheckout')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('allows traffic when JWT is valid and store is password-protected', async () => {
|
|
179
|
+
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
180
|
+
ok: true,
|
|
181
|
+
json: async () => ({ publicKey: 'test-pem' }),
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
jwtVerifyMock.mockResolvedValueOnce({
|
|
185
|
+
payload: {
|
|
186
|
+
storeId: 'test-store',
|
|
187
|
+
protected: true,
|
|
188
|
+
scope: 'DEFAULT_DOMAINS',
|
|
189
|
+
},
|
|
190
|
+
protectedHeader: { alg: 'RS256' },
|
|
191
|
+
} as unknown as Awaited<ReturnType<typeof jwtVerify>>)
|
|
192
|
+
|
|
193
|
+
const service = new AuthenticationService()
|
|
194
|
+
const { response } = await service.authenticateRequest(
|
|
195
|
+
previewRequest('/account', {
|
|
196
|
+
headers: { cookie: '__fs_auth_token=valid' },
|
|
197
|
+
})
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
expect(response.status).toBe(200)
|
|
201
|
+
expect(global.fetch).toHaveBeenCalledTimes(1)
|
|
202
|
+
expect((global.fetch as jest.Mock).mock.calls[0][0]).toEqual(
|
|
203
|
+
expect.objectContaining({
|
|
204
|
+
pathname: '/api/v1/password-protection/public-key',
|
|
205
|
+
})
|
|
206
|
+
)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('verifies JWT locally when cookie present (not protected payload)', async () => {
|
|
210
|
+
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
211
|
+
ok: true,
|
|
212
|
+
json: async () => ({ publicKey: 'test-pem' }),
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
jwtVerifyMock.mockResolvedValueOnce({
|
|
216
|
+
payload: {
|
|
217
|
+
storeId: 'test-store',
|
|
218
|
+
protected: false,
|
|
219
|
+
},
|
|
220
|
+
protectedHeader: { alg: 'RS256' },
|
|
221
|
+
} as unknown as Awaited<ReturnType<typeof jwtVerify>>)
|
|
222
|
+
|
|
223
|
+
const service = new AuthenticationService()
|
|
224
|
+
const { response } = await service.authenticateRequest(
|
|
225
|
+
previewRequest('/p', {
|
|
226
|
+
headers: {
|
|
227
|
+
host: 'preview.vtex.app',
|
|
228
|
+
cookie: '__fs_auth_token=abc',
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
expect(response.status).toBe(200)
|
|
234
|
+
expect(jwtVerifyMock).toHaveBeenCalled()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('treats wrong storeId in JWT as unauthenticated and calls status', async () => {
|
|
238
|
+
jwtVerifyMock.mockResolvedValueOnce({
|
|
239
|
+
payload: {
|
|
240
|
+
storeId: 'other-store',
|
|
241
|
+
protected: false,
|
|
242
|
+
},
|
|
243
|
+
protectedHeader: { alg: 'RS256' },
|
|
244
|
+
} as unknown as Awaited<ReturnType<typeof jwtVerify>>)
|
|
245
|
+
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
246
|
+
ok: true,
|
|
247
|
+
json: async () => ({ publicKey: 'test-pem' }),
|
|
248
|
+
})
|
|
249
|
+
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
250
|
+
ok: true,
|
|
251
|
+
json: async () => ({
|
|
252
|
+
protected: true,
|
|
253
|
+
scope: 'DEFAULT_DOMAINS',
|
|
254
|
+
}),
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const service = new AuthenticationService()
|
|
258
|
+
const { response } = await service.authenticateRequest(
|
|
259
|
+
previewRequest('/x', {
|
|
260
|
+
headers: {
|
|
261
|
+
cookie: '__fs_auth_token=bad',
|
|
262
|
+
},
|
|
263
|
+
})
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
expect(response.status).toBe(307)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('falls back to status when cookie JWT fails verification for non-expiry reasons', async () => {
|
|
270
|
+
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
271
|
+
ok: true,
|
|
272
|
+
json: async () => ({ publicKey: 'test-pem' }),
|
|
273
|
+
})
|
|
274
|
+
jwtVerifyMock.mockRejectedValueOnce(new Error('invalid signature'))
|
|
275
|
+
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
276
|
+
ok: true,
|
|
277
|
+
json: async () => ({
|
|
278
|
+
protected: false,
|
|
279
|
+
token: 'after-bad-jwt',
|
|
280
|
+
}),
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
const service = new AuthenticationService()
|
|
284
|
+
const { response } = await service.authenticateRequest(
|
|
285
|
+
previewRequest('/p', {
|
|
286
|
+
headers: { cookie: '__fs_auth_token=garbage' },
|
|
287
|
+
})
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
expect(response.status).toBe(200)
|
|
291
|
+
expect(response.cookies.get('__fs_auth_token')?.value).toBe('after-bad-jwt')
|
|
292
|
+
expect(jwtVerifyMock).toHaveBeenCalled()
|
|
293
|
+
expect(global.fetch).toHaveBeenCalledTimes(2)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('renews session when JWT is expired and WebOps renew succeeds', async () => {
|
|
297
|
+
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
298
|
+
ok: true,
|
|
299
|
+
json: async () => ({ publicKey: 'test-pem' }),
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
jwtVerifyMock.mockRejectedValueOnce(
|
|
303
|
+
new errors.JWTExpired('jwt expired', { storeId: 'test-store' })
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
decodeJwtMock.mockReturnValueOnce({
|
|
307
|
+
storeId: 'test-store',
|
|
308
|
+
protected: true,
|
|
309
|
+
scope: 'DEFAULT_DOMAINS',
|
|
310
|
+
} as ReturnType<typeof decodeJwt>)
|
|
311
|
+
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
312
|
+
ok: true,
|
|
313
|
+
json: async () => ({
|
|
314
|
+
valid: true,
|
|
315
|
+
token: 'renewed-token',
|
|
316
|
+
}),
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
const service = new AuthenticationService()
|
|
320
|
+
const { response } = await service.authenticateRequest(
|
|
321
|
+
previewRequest('/p', {
|
|
322
|
+
headers: { cookie: '__fs_auth_token=old' },
|
|
323
|
+
})
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
expect(response.status).toBe(200)
|
|
327
|
+
expect(response.cookies.get('__fs_auth_token')?.value).toBe('renewed-token')
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('redirects to login when JWT is expired and renew response is not ok', async () => {
|
|
331
|
+
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
332
|
+
ok: true,
|
|
333
|
+
json: async () => ({ publicKey: 'test-pem' }),
|
|
334
|
+
})
|
|
335
|
+
jwtVerifyMock.mockRejectedValueOnce(
|
|
336
|
+
new errors.JWTExpired('jwt expired', { storeId: 'test-store' })
|
|
337
|
+
)
|
|
338
|
+
decodeJwtMock.mockReturnValueOnce({
|
|
339
|
+
storeId: 'test-store',
|
|
340
|
+
protected: true,
|
|
341
|
+
scope: 'DEFAULT_DOMAINS',
|
|
342
|
+
} as ReturnType<typeof decodeJwt>)
|
|
343
|
+
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
344
|
+
ok: false,
|
|
345
|
+
status: 502,
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const service = new AuthenticationService()
|
|
349
|
+
const { response } = await service.authenticateRequest(
|
|
350
|
+
previewRequest('/p', {
|
|
351
|
+
headers: { cookie: '__fs_auth_token=expired' },
|
|
352
|
+
})
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
expect(response.status).toBe(307)
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('redirects to login when JWT is expired and renew body is not valid', async () => {
|
|
359
|
+
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
360
|
+
ok: true,
|
|
361
|
+
json: async () => ({ publicKey: 'test-pem' }),
|
|
362
|
+
})
|
|
363
|
+
jwtVerifyMock.mockRejectedValueOnce(
|
|
364
|
+
new errors.JWTExpired('jwt expired', { storeId: 'test-store' })
|
|
365
|
+
)
|
|
366
|
+
decodeJwtMock.mockReturnValueOnce({
|
|
367
|
+
storeId: 'test-store',
|
|
368
|
+
protected: true,
|
|
369
|
+
scope: 'DEFAULT_DOMAINS',
|
|
370
|
+
} as ReturnType<typeof decodeJwt>)
|
|
371
|
+
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
372
|
+
ok: true,
|
|
373
|
+
json: async () => ({ valid: false }),
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
const service = new AuthenticationService()
|
|
377
|
+
const { response } = await service.authenticateRequest(
|
|
378
|
+
previewRequest('/p', {
|
|
379
|
+
headers: { cookie: '__fs_auth_token=expired' },
|
|
380
|
+
})
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
expect(response.status).toBe(307)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('allows traffic when renew fails but expired JWT scope does not apply to this host', async () => {
|
|
387
|
+
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
388
|
+
ok: true,
|
|
389
|
+
json: async () => ({ publicKey: 'test-pem' }),
|
|
390
|
+
})
|
|
391
|
+
jwtVerifyMock.mockRejectedValueOnce(
|
|
392
|
+
new errors.JWTExpired('jwt expired', { storeId: 'test-store' })
|
|
393
|
+
)
|
|
394
|
+
decodeJwtMock.mockReturnValueOnce({
|
|
395
|
+
storeId: 'test-store',
|
|
396
|
+
protected: true,
|
|
397
|
+
scope: 'CUSTOM_DOMAINS',
|
|
398
|
+
} as ReturnType<typeof decodeJwt>)
|
|
399
|
+
;(global.fetch as jest.Mock).mockRejectedValueOnce(new Error('renew down'))
|
|
400
|
+
|
|
401
|
+
const service = new AuthenticationService()
|
|
402
|
+
const { response } = await service.authenticateRequest(
|
|
403
|
+
previewRequest('/p', {
|
|
404
|
+
headers: { cookie: '__fs_auth_token=old' },
|
|
405
|
+
})
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
expect(response.status).toBe(200)
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('fail-closes to login on default domain when status request fails', async () => {
|
|
412
|
+
;(global.fetch as jest.Mock).mockRejectedValue(new Error('network'))
|
|
413
|
+
|
|
414
|
+
const service = new AuthenticationService()
|
|
415
|
+
const { response } = await service.authenticateRequest(previewRequest('/p'))
|
|
416
|
+
|
|
417
|
+
expect(response.status).toBe(307)
|
|
418
|
+
expect(response.headers.get('location')).toContain('/fs-auth-login')
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('fail-closes to login when status endpoint returns a non-ok HTTP status', async () => {
|
|
422
|
+
;(global.fetch as jest.Mock).mockResolvedValue({
|
|
423
|
+
ok: false,
|
|
424
|
+
status: 503,
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
const service = new AuthenticationService()
|
|
428
|
+
const { response } = await service.authenticateRequest(previewRequest('/p'))
|
|
429
|
+
|
|
430
|
+
expect(response.status).toBe(307)
|
|
431
|
+
expect(response.headers.get('location')).toContain('/fs-auth-login')
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('fail-closes to login on custom domain when status request fails', async () => {
|
|
435
|
+
process.env.CUSTOM_DOMAINS_PROTECTION_ENABLED = 'true'
|
|
436
|
+
;(global.fetch as jest.Mock).mockRejectedValue(new Error('network'))
|
|
437
|
+
|
|
438
|
+
const service = new AuthenticationService()
|
|
439
|
+
const request = new NextRequest('https://shop.example.com/page', {
|
|
440
|
+
headers: { host: 'shop.example.com' },
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
const { response } = await service.authenticateRequest(request)
|
|
444
|
+
|
|
445
|
+
expect(response.status).toBe(307)
|
|
446
|
+
expect(response.headers.get('location')).toContain('/fs-auth-login')
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it('does not enforce protection on custom domain when env gate is off', async () => {
|
|
450
|
+
const service = new AuthenticationService()
|
|
451
|
+
const request = new NextRequest('https://shop.example.com/page', {
|
|
452
|
+
headers: { host: 'shop.example.com' },
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
const { response } = await service.authenticateRequest(request)
|
|
456
|
+
|
|
457
|
+
expect(response.status).toBe(200)
|
|
458
|
+
expect(global.fetch).not.toHaveBeenCalled()
|
|
459
|
+
})
|
|
460
|
+
})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
self.__BUILD_MANIFEST=function(c,s,t,a,e,u,n){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[c,s,t,e,"static/css/02259c549b2179f2.css","static/chunks/pages/index-8774d5b4a8744eb2.js"],"/404":[c,s,t,a,"static/chunks/pages/404-e3dfbbac003b02a2.js"],"/500":[c,s,t,a,"static/chunks/pages/500-c7814e085081d7b8.js"],"/_error":["static/chunks/pages/_error-2b0148be56a716e9.js"],"/checkout":[c,s,t,"static/chunks/pages/checkout-2071467ad42dfb99.js"],"/login":[c,s,t,a,"static/chunks/pages/login-e74747c9f9cb5b49.js"],"/pvt/account":["static/chunks/pages/pvt/account-65fefcc699344bdb.js"],"/pvt/account/403":[c,s,t,"static/css/6819dddf2b1b5ea7.css","static/chunks/pages/pvt/account/403-ccddb4364ef27179.js"],"/pvt/account/404":[c,s,t,"static/css/ea11d74a2848b093.css","static/chunks/pages/pvt/account/404-5ad472d9f2ee1980.js"],"/pvt/account/orders":[c,s,t,"static/css/14176a4704ff97b2.css","static/chunks/pages/pvt/account/orders-faab39f2aff05fae.js"],"/pvt/account/orders/[id]":[c,s,t,"static/css/82f3cd529b672812.css","static/chunks/pages/pvt/account/orders/[id]-64e42157c0974c48.js"],"/pvt/account/profile":[c,s,t,"static/css/a852afa9be31e946.css","static/chunks/pages/pvt/account/profile-9672177ee2665bc0.js"],"/pvt/account/security":[c,s,t,"static/css/24d91007fc96be10.css","static/chunks/pages/pvt/account/security-03eb1ccab9a30563.js"],"/pvt/account/user-details":[c,s,t,"static/css/16d65422bfa80f51.css","static/chunks/pages/pvt/account/user-details-7838e3f79781596c.js"],"/pvt/account/[...unknown]":["static/chunks/pages/pvt/account/[...unknown]-f80f645594d2740c.js"],"/s":[c,s,t,u,n,a,"static/chunks/pages/s-162ba789e6611175.js"],"/[slug]/p":[c,"static/chunks/227-f109b24190a008ab.js",s,t,e,"static/css/1c4983f11b861ea0.css","static/chunks/3166-0b30a3887c0c346d.js","static/css/54cedf4b65e5863f.css","static/chunks/pages/[slug]/p-644f1132f5a0f736.js"],"/[...slug]":[c,s,t,u,n,"static/chunks/pages/[...slug]-dc8a395a43b081f1.js"],sortedPages:["/","/404","/500","/_app","/_error","/checkout","/login","/pvt/account","/pvt/account/403","/pvt/account/404","/pvt/account/orders","/pvt/account/orders/[id]","/pvt/account/profile","/pvt/account/security","/pvt/account/user-details","/pvt/account/[...unknown]","/s","/[slug]/p","/[...slug]"]}}("static/chunks/9298-1d1ed0a54133cb59.js","static/css/2c19433e38fd7533.css","static/chunks/9173-515913f3f10caf5e.js","static/css/2980acad3f8e1028.css","static/css/837662922091162f.css","static/css/fdc1868dcfd42aa4.css","static/chunks/7191-d221f01388571ca9.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
|