From 550cc9dc27c0909bf73618d94b1b5264aa02404c Mon Sep 17 00:00:00 2001 From: AllanZhengYP Date: Fri, 13 Oct 2023 12:00:36 -0700 Subject: [PATCH 1/4] chore(deps): upgrade js-cookie (#12263) --- .../core/__tests__/storage/CookieStorage.test.ts | 13 +++++++++++++ packages/core/package.json | 4 ++-- packages/core/src/storage/CookieStorage.ts | 14 +++++--------- yarn.lock | 16 ++++++++-------- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/core/__tests__/storage/CookieStorage.test.ts b/packages/core/__tests__/storage/CookieStorage.test.ts index b47b8b7d7e1..479160a1be4 100644 --- a/packages/core/__tests__/storage/CookieStorage.test.ts +++ b/packages/core/__tests__/storage/CookieStorage.test.ts @@ -1,4 +1,17 @@ import { CookieStorage } from '../../src/storage/CookieStorage'; +/** + * This mock is a workaround before we upgrade the ts-jest config. + * The current ts-jest config uses only the default tsconfig instead of the tsconfig.json in core package. + * The default tsconfig used by ts-jest does not set the `esModuleInterop` to true. This cause + * `import JsCookie from 'js-cookie'` to fail. + */ +jest.mock('js-cookie', () => ({ + default: { + get: jest.requireActual('js-cookie').get, + set: jest.requireActual('js-cookie').set, + remove: jest.requireActual('js-cookie').remove, + }, +})); const cookieStorageDomain = 'https://testdomain.com'; diff --git a/packages/core/package.json b/packages/core/package.json index cadfab100f0..90986256959 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -58,14 +58,14 @@ "@aws-crypto/sha256-js": "5.0.0", "@aws-sdk/types": "3.398.0", "@smithy/util-hex-encoding": "2.0.0", - "js-cookie": "^2.2.1", + "js-cookie": "^3.0.5", "tslib": "^2.5.0", "uuid": "^9.0.0", "rxjs": "^7.8.1" }, "devDependencies": { "@aws-amplify/react-native": "^1.0.0", - "@types/js-cookie": "^2.2.7", + "@types/js-cookie": "3.0.2", "@types/uuid": "^9.0.0", "find": "^0.2.7", "genversion": "^2.2.0", diff --git a/packages/core/src/storage/CookieStorage.ts b/packages/core/src/storage/CookieStorage.ts index 3e655976bd0..a20ab95b3dd 100644 --- a/packages/core/src/storage/CookieStorage.ts +++ b/packages/core/src/storage/CookieStorage.ts @@ -1,11 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - get as getJsCookie, - remove as removeJsCookie, - set as setJsCookie, -} from 'js-cookie'; +import JsCookie from 'js-cookie'; import { CookieStorageData, KeyValueStorageInterface, @@ -42,20 +38,20 @@ export class CookieStorage implements KeyValueStorageInterface { } async setItem(key: string, value: string) { - setJsCookie(key, value, this.getData()); + JsCookie.set(key, value, this.getData()); } async getItem(key: string) { - const item = getJsCookie(key); + const item = JsCookie.get(key); return item ?? null; } async removeItem(key: string) { - removeJsCookie(key, this.getData()); + JsCookie.remove(key, this.getData()); } async clear() { - const cookie = getJsCookie(); + const cookie = JsCookie.get(); const promises = Object.keys(cookie).map(key => this.removeItem(key)); await Promise.all(promises); } diff --git a/yarn.lock b/yarn.lock index 4c4d7fb08b0..28c453e13be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4132,10 +4132,10 @@ dependencies: jest-diff "^24.3.0" -"@types/js-cookie@^2.2.7": - version "2.2.7" - resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.7.tgz#226a9e31680835a6188e887f3988e60c04d3f6a3" - integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA== +"@types/js-cookie@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.2.tgz#451eaeece64c6bdac8b2dde0caab23b085899e0d" + integrity sha512-6+0ekgfusHftJNYpihfkMu8BWdeHs9EOJuGcSofErjstGPfPGEu9yTu4t460lTzzAMl2cM5zngQJqPMHbbnvYA== "@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8": version "7.0.13" @@ -9178,10 +9178,10 @@ jora@1.0.0-beta.8: dependencies: "@discoveryjs/natural-compare" "^1.0.0" -js-cookie@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" - integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== +js-cookie@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" From d917d539516903046d5bcc3b4ba435af5e8f6227 Mon Sep 17 00:00:00 2001 From: israx <70438514+israx@users.noreply.github.com> Date: Fri, 13 Oct 2023 17:14:09 -0400 Subject: [PATCH 2/4] fix(auth): remove username en/decode logic (#12299) * fix: return username from lastAuthUser in refresh tokens action * fix: remove decode logic from username --- .../providers/cognito/tokenProvider.test.ts | 103 ++++-------------- .../cognito/tokenProvider/TokenStore.ts | 27 +---- .../cognito/utils/refreshAuthTokens.ts | 2 +- 3 files changed, 27 insertions(+), 105 deletions(-) diff --git a/packages/auth/__tests__/providers/cognito/tokenProvider.test.ts b/packages/auth/__tests__/providers/cognito/tokenProvider.test.ts index 2362637a649..4403f38c313 100644 --- a/packages/auth/__tests__/providers/cognito/tokenProvider.test.ts +++ b/packages/auth/__tests__/providers/cognito/tokenProvider.test.ts @@ -84,19 +84,18 @@ describe('Loading tokens', () => { const memoryStorage = new MemoryStorage(); const userPoolClientId = 'userPoolClientId'; const userSub1 = 'user1@email.com'; - const userSub1Encoded = 'user1%40email.com'; const userSub2 = 'user2@email.com'; memoryStorage.setItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${userSub1Encoded}.deviceKey`, + `CognitoIdentityServiceProvider.${userPoolClientId}.${userSub1}.deviceKey`, 'user1-device-key' ); memoryStorage.setItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${userSub1Encoded}.deviceGroupKey`, + `CognitoIdentityServiceProvider.${userPoolClientId}.${userSub1}.deviceGroupKey`, 'user1-device-group-key' ); memoryStorage.setItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${userSub1Encoded}.randomPasswordKey`, + `CognitoIdentityServiceProvider.${userPoolClientId}.${userSub1}.randomPasswordKey`, 'user1-random-password' ); memoryStorage.setItem( @@ -144,7 +143,7 @@ describe('saving tokens', () => { userPoolClientId, }, }); - + const lastAuthUser = 'amplify@user'; await tokenStore.storeTokens({ accessToken: decodeJWT( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzAsInVzZXJuYW1lIjoiYW1wbGlmeUB1c2VyIn0.AAA' @@ -159,22 +158,20 @@ describe('saving tokens', () => { deviceGroupKey: 'device-group-key2', randomPassword: 'random-password2', }, - username: 'amplify@user', + username: lastAuthUser, }); - const usernameDecoded = 'amplify%40user'; - expect( await memoryStorage.getItem( `CognitoIdentityServiceProvider.${userPoolClientId}.LastAuthUser` ) - ).toBe(usernameDecoded); // from decoded JWT + ).toBe(lastAuthUser); // Refreshed tokens expect( await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${usernameDecoded}.accessToken` + `CognitoIdentityServiceProvider.${userPoolClientId}.${lastAuthUser}.accessToken` ) ).toBe( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzAsInVzZXJuYW1lIjoiYW1wbGlmeUB1c2VyIn0.AAA' @@ -182,7 +179,7 @@ describe('saving tokens', () => { expect( await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${usernameDecoded}.idToken` + `CognitoIdentityServiceProvider.${userPoolClientId}.${lastAuthUser}.idToken` ) ).toBe( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzAsInVzZXJuYW1lIjoiYW1wbGlmeUB1c2VyIn0.III' @@ -190,28 +187,28 @@ describe('saving tokens', () => { expect( await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${usernameDecoded}.refreshToken` + `CognitoIdentityServiceProvider.${userPoolClientId}.${lastAuthUser}.refreshToken` ) ).toBe('refresh-token'); expect( await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${usernameDecoded}.clockDrift` + `CognitoIdentityServiceProvider.${userPoolClientId}.${lastAuthUser}.clockDrift` ) ).toBe('150'); expect( await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${usernameDecoded}.deviceKey` + `CognitoIdentityServiceProvider.${userPoolClientId}.${lastAuthUser}.deviceKey` ) ).toBe('device-key2'); expect( await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${usernameDecoded}.deviceGroupKey` + `CognitoIdentityServiceProvider.${userPoolClientId}.${lastAuthUser}.deviceGroupKey` ) ).toBe('device-group-key2'); expect( await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${usernameDecoded}.randomPasswordKey` + `CognitoIdentityServiceProvider.${userPoolClientId}.${lastAuthUser}.randomPasswordKey` ) ).toBe('random-password2'); }); @@ -276,22 +273,20 @@ describe('saving tokens', () => { deviceGroupKey: 'device-group-key2', randomPassword: 'random-password2', }, - username: 'amplify@user', + username: oldUserName, }); - const usernameEncoded = 'amplify%40user'; - expect( await memoryStorage.getItem( `CognitoIdentityServiceProvider.${userPoolClientId}.LastAuthUser` ) - ).toBe(usernameEncoded); // from decoded JWT + ).toBe(oldUserName); // Refreshed tokens expect( await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${usernameEncoded}.accessToken` + `CognitoIdentityServiceProvider.${userPoolClientId}.${oldUserName}.accessToken` ) ).toBe( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzAsInVzZXJuYW1lIjoiYW1wbGlmeUB1c2VyIn0.AAA' @@ -299,92 +294,38 @@ describe('saving tokens', () => { expect( await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${usernameEncoded}.idToken` + `CognitoIdentityServiceProvider.${userPoolClientId}.${oldUserName}.idToken` ) ).toBe( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzAsInVzZXJuYW1lIjoiYW1wbGlmeUB1c2VyIn0.III' ); - expect( - await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${usernameEncoded}.refreshToken` - ) - ).toBe('refresh-token'); - - expect( - await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${usernameEncoded}.clockDrift` - ) - ).toBe('150'); - - expect( - await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${usernameEncoded}.deviceKey` - ) - ).toBe('device-key2'); - expect( - await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${usernameEncoded}.deviceGroupKey` - ) - ).toBe('device-group-key2'); - expect( - await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${usernameEncoded}.randomPasswordKey` - ) - ).toBe('random-password2'); - - // old tokens cleared - expect( - await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${oldUserName}.accessToken` - ) - ).toBeUndefined(); - expect( - await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${oldUserName}.idToken` - ) - ).toBeUndefined(); expect( await memoryStorage.getItem( `CognitoIdentityServiceProvider.${userPoolClientId}.${oldUserName}.refreshToken` ) - ).toBeUndefined(); - expect( - await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${oldUserName}.clockDrift` - ) - ).toBeUndefined(); + ).toBe('refresh-token'); - expect( - await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${oldUserName}.idToken` - ) - ).toBeUndefined(); - expect( - await memoryStorage.getItem( - `CognitoIdentityServiceProvider.${userPoolClientId}.${oldUserName}.refreshToken` - ) - ).toBeUndefined(); expect( await memoryStorage.getItem( `CognitoIdentityServiceProvider.${userPoolClientId}.${oldUserName}.clockDrift` ) - ).toBeUndefined(); + ).toBe('150'); expect( await memoryStorage.getItem( `CognitoIdentityServiceProvider.${userPoolClientId}.${oldUserName}.deviceKey` ) - ).not.toBeUndefined(); + ).toBe('device-key2'); expect( await memoryStorage.getItem( `CognitoIdentityServiceProvider.${userPoolClientId}.${oldUserName}.deviceGroupKey` ) - ).not.toBeUndefined(); + ).toBe('device-group-key2'); expect( await memoryStorage.getItem( `CognitoIdentityServiceProvider.${userPoolClientId}.${oldUserName}.randomPasswordKey` ) - ).not.toBeUndefined(); + ).toBe('random-password2'); }); }); diff --git a/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts b/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts index 082799c9e22..e6a5caadc65 100644 --- a/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts +++ b/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts @@ -71,7 +71,7 @@ export class DefaultTokenStore implements AuthTokenStore { refreshToken, deviceMetadata: (await this.getDeviceMetadata()) ?? undefined, clockDrift, - username: decodeURIComponent(await this.getLastAuthUser()), + username: await this.getLastAuthUser(), }; } catch (err) { return null; @@ -81,8 +81,7 @@ export class DefaultTokenStore implements AuthTokenStore { assert(tokens !== undefined, TokenProviderErrorCode.InvalidAuthTokens); await this.clearTokens(); - const lastAuthUser = - (tokens.username && encodeURIComponent(tokens.username)) ?? 'username'; + const lastAuthUser = tokens.username; await this.getKeyValueStorage().setItem( this.getLastAuthUserKey(), lastAuthUser @@ -146,7 +145,7 @@ export class DefaultTokenStore implements AuthTokenStore { } async getDeviceMetadata(username?: string): Promise { - const authKeys = await this.getDeviceAuthKeys(username); + const authKeys = await this.getAuthKeys(username); const deviceKey = await this.getKeyValueStorage().getItem( authKeys.deviceKey ); @@ -166,7 +165,7 @@ export class DefaultTokenStore implements AuthTokenStore { : null; } async clearDeviceMetadata(username?: string): Promise { - const authKeys = await this.getDeviceAuthKeys(username); + const authKeys = await this.getAuthKeys(username); await Promise.all([ this.getKeyValueStorage().removeItem(authKeys.deviceKey), this.getKeyValueStorage().removeItem(authKeys.deviceGroupKey), @@ -184,24 +183,6 @@ export class DefaultTokenStore implements AuthTokenStore { `${this.authConfig.Cognito.userPoolClientId}.${lastAuthUser}` ); } - private async getDeviceAuthKeys( - username?: string - ): Promise> { - let authKeys: AuthKeys; - if (username) { - const authEncodedKeys = await this.getAuthKeys( - encodeURIComponent(username) - ); - const authNonEncodedKeys = await this.getAuthKeys(username); - const isEncodedKeysPresent = !!(await this.getKeyValueStorage().getItem( - authEncodedKeys.randomPasswordKey - )); - authKeys = isEncodedKeysPresent ? authEncodedKeys : authNonEncodedKeys; - } else { - authKeys = await this.getAuthKeys(); - } - return authKeys; - } private getLastAuthUserKey() { assertTokenProviderConfig(this.authConfig?.Cognito); diff --git a/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts b/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts index 4ab194f2d63..f74c8683fd6 100644 --- a/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts +++ b/packages/auth/src/providers/cognito/utils/refreshAuthTokens.ts @@ -69,6 +69,6 @@ export const refreshAuthTokens: TokenRefresher = async ({ idToken, clockDrift, refreshToken: refreshTokenString, - username: `${accessToken.payload.username}`, + username, }; }; From 40b8f7e8be163b8ccd62c8ccdfe60b396be64cc7 Mon Sep 17 00:00:00 2001 From: Hui Zhao Date: Fri, 13 Oct 2023 13:04:37 -0700 Subject: [PATCH 3/4] fix(adapter-nextjs): update the cookie name encoding function --- ...okieStorageAdapterFromNextServerContext.ts | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/adapter-nextjs/src/utils/createCookieStorageAdapterFromNextServerContext.ts b/packages/adapter-nextjs/src/utils/createCookieStorageAdapterFromNextServerContext.ts index e4303396871..d30b7ba7859 100644 --- a/packages/adapter-nextjs/src/utils/createCookieStorageAdapterFromNextServerContext.ts +++ b/packages/adapter-nextjs/src/utils/createCookieStorageAdapterFromNextServerContext.ts @@ -79,14 +79,14 @@ const createCookieStorageAdapterFromNextRequestAndNextResponse = ( return { get(name) { - return readonlyCookieStore.get(processCookieName(name)); + return readonlyCookieStore.get(ensureEncodedForJSCookie(name)); }, getAll: readonlyCookieStore.getAll.bind(readonlyCookieStore), set(name, value, options) { - mutableCookieStore.set(processCookieName(name), value, options); + mutableCookieStore.set(ensureEncodedForJSCookie(name), value, options); }, delete(name) { - mutableCookieStore.delete(processCookieName(name)); + mutableCookieStore.delete(ensureEncodedForJSCookie(name)); }, }; }; @@ -102,7 +102,7 @@ const createCookieStorageAdapterFromNextRequestAndHttpResponse = ( return { get(name) { - return readonlyCookieStore.get(processCookieName(name)); + return readonlyCookieStore.get(ensureEncodedForJSCookie(name)); }, getAll: readonlyCookieStore.getAll.bind(readonlyCookieStore), ...mutableCookieStore, @@ -121,7 +121,7 @@ const createCookieStorageAdapterFromNextCookies = ( // and safely ignore the error if it is thrown. const setFunc: CookieStorage.Adapter['set'] = (name, value, options) => { try { - cookieStore.set(processCookieName(name), value, options); + cookieStore.set(ensureEncodedForJSCookie(name), value, options); } catch { // no-op } @@ -129,7 +129,7 @@ const createCookieStorageAdapterFromNextCookies = ( const deleteFunc: CookieStorage.Adapter['delete'] = name => { try { - cookieStore.delete(processCookieName(name)); + cookieStore.delete(ensureEncodedForJSCookie(name)); } catch { // no-op } @@ -137,7 +137,7 @@ const createCookieStorageAdapterFromNextCookies = ( return { get(name) { - return cookieStore.get(processCookieName(name)); + return cookieStore.get(ensureEncodedForJSCookie(name)); }, getAll: cookieStore.getAll.bind(cookieStore), set: setFunc, @@ -157,7 +157,7 @@ const createCookieStorageAdapterFromGetServerSidePropsContext = ( return { get(name) { - const value = cookiesMap[processCookieName(name)]; + const value = cookiesMap[ensureEncodedForJSCookie(name)]; return value ? { name, @@ -171,7 +171,7 @@ const createCookieStorageAdapterFromGetServerSidePropsContext = ( set(name, value, options) { response.setHeader( 'Set-Cookie', - `${processCookieName(name)}=${value};${ + `${ensureEncodedForJSCookie(name)}=${value};${ options ? serializeSetCookieOptions(options) : '' }` ); @@ -179,7 +179,9 @@ const createCookieStorageAdapterFromGetServerSidePropsContext = ( delete(name) { response.setHeader( 'Set-Cookie', - `${processCookieName(name)}=;Expires=${DATE_IN_THE_PAST.toUTCString()}` + `${ensureEncodedForJSCookie( + name + )}=;Expires=${DATE_IN_THE_PAST.toUTCString()}` ); }, }; @@ -191,7 +193,7 @@ const createMutableCookieStoreFromHeaders = ( const setFunc: CookieStorage.Adapter['set'] = (name, value, options) => { headers.append( 'Set-Cookie', - `${processCookieName(name)}=${value};${ + `${ensureEncodedForJSCookie(name)}=${value};${ options ? serializeSetCookieOptions(options) : '' }` ); @@ -199,7 +201,9 @@ const createMutableCookieStoreFromHeaders = ( const deleteFunc: CookieStorage.Adapter['delete'] = name => { headers.append( 'Set-Cookie', - `${processCookieName(name)}=;Expires=${DATE_IN_THE_PAST.toUTCString()}` + `${ensureEncodedForJSCookie( + name + )}=;Expires=${DATE_IN_THE_PAST.toUTCString()}` ); }; return { @@ -231,14 +235,11 @@ const serializeSetCookieOptions = ( return serializedOptions.join(';'); }; -const processCookieName = (name: string): string => { - // if the cookie name contains a `%`, it should have been encoded by the - // tokenProvider, to ensure the compatibility of cookie name encoding handled - // by the js-cookie package on the client side (as the cookies is created - // on the client side with sign in), we double encode it. - if (name.includes('%')) { - return encodeURIComponent(name); - } - - return name; -}; +// Ensures the cookie names are encoded in order to look up the cookie store +// that is manipulated by js-cookie on the client side. +// Details of the js-cookie encoding behavior see: +// https://github.com/js-cookie/js-cookie#encoding +// The implementation is borrowed from js-cookie without escaping `[()]` as +// we are not using those chars in the auth keys. +const ensureEncodedForJSCookie = (name: string): string => + encodeURIComponent(name).replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent); From 9eefbfb440ae54f5073b7a1b006af650ca59bf61 Mon Sep 17 00:00:00 2001 From: Hui Zhao Date: Fri, 13 Oct 2023 14:25:18 -0700 Subject: [PATCH 4/4] feat(storage): expose the defaultPrefixResolver from storage/s3/utils subpath --- packages/aws-amplify/package.json | 5 ++ packages/aws-amplify/src/storage/s3/utils.ts | 7 +++ .../aws-amplify/storage/s3/utils/package.json | 7 +++ ...evelAndIdentityAwarePrefixResolver.test.ts | 49 +++++++++++++++++ packages/storage/package.json | 5 ++ packages/storage/s3/utils/package.json | 7 +++ ...cessLevelAndIdentityAwarePrefixResolver.ts | 53 +++++++++++++++++++ .../storage/src/providers/s3/utilityApis.ts | 4 ++ packages/storage/src/types/inputs.ts | 6 +++ packages/storage/src/utils/resolvePrefix.ts | 9 +--- 10 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 packages/aws-amplify/src/storage/s3/utils.ts create mode 100644 packages/aws-amplify/storage/s3/utils/package.json create mode 100644 packages/storage/__tests__/providers/s3/apis/utilityApis/accessLevelAndIdentityAwarePrefixResolver.test.ts create mode 100644 packages/storage/s3/utils/package.json create mode 100644 packages/storage/src/providers/s3/apis/utilityApis/accessLevelAndIdentityAwarePrefixResolver.ts create mode 100644 packages/storage/src/providers/s3/utilityApis.ts diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index a346433a1d7..28885149d8f 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -97,6 +97,11 @@ "import": "./lib-esm/storage/s3/server.js", "require": "./lib/storage/s3/server.js" }, + "./storage/s3/utils": { + "types": "./lib-esm/storage/s3/utils.d.ts", + "import": "./lib-esm/storage/s3/utils.js", + "require": "./lib/storage/s3/utils.js" + }, "./in-app-messaging": { "types": "./lib-esm/in-app-messaging/index.d.ts", "import": "./lib-esm/in-app-messaging/index.js", diff --git a/packages/aws-amplify/src/storage/s3/utils.ts b/packages/aws-amplify/src/storage/s3/utils.ts new file mode 100644 index 00000000000..4063cc5fe9b --- /dev/null +++ b/packages/aws-amplify/src/storage/s3/utils.ts @@ -0,0 +1,7 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* +This file maps exports from `aws-amplify/storage/s3/utils`. It provides access to S3 utility APIs. +*/ +export * from '@aws-amplify/storage/s3/utils'; diff --git a/packages/aws-amplify/storage/s3/utils/package.json b/packages/aws-amplify/storage/s3/utils/package.json new file mode 100644 index 00000000000..1f552c32c89 --- /dev/null +++ b/packages/aws-amplify/storage/s3/utils/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-amplify/storage/s3/utils", + "main": "../../../lib/storage/s3/utils.js", + "browser": "../../../lib-esm/storage/s3/utils.js", + "module": "../../../lib-esm/storage/s3/utils.js", + "typings": "../../../lib-esm/storage/s3/utils.d.ts" +} diff --git a/packages/storage/__tests__/providers/s3/apis/utilityApis/accessLevelAndIdentityAwarePrefixResolver.test.ts b/packages/storage/__tests__/providers/s3/apis/utilityApis/accessLevelAndIdentityAwarePrefixResolver.test.ts new file mode 100644 index 00000000000..f1dd74da62e --- /dev/null +++ b/packages/storage/__tests__/providers/s3/apis/utilityApis/accessLevelAndIdentityAwarePrefixResolver.test.ts @@ -0,0 +1,49 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Amplify } from '@aws-amplify/core'; +import { accessLevelAndIdentityAwarePrefixResolver } from '../../../../../src/providers/s3/apis/utilityApis/accessLevelAndIdentityAwarePrefixResolver'; + +jest.mock('@aws-amplify/core', () => ({ + Amplify: { + getConfig: jest.fn(), + Auth: { + fetchAuthSession: jest.fn(), + }, + }, +})); + +const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; + +describe('accessLevelAndIdentityAwarePrefixResolver', () => { + const mockIdentityId = 'mockIdentityId'; + + beforeAll(() => { + mockFetchAuthSession.mockResolvedValue({ + identityId: mockIdentityId, + }); + }); + + it('returns private prefix when accessLevel is private', async () => { + const prefix = await accessLevelAndIdentityAwarePrefixResolver({ + accessLevel: 'private', + }); + expect(prefix).toEqual(`private/${mockIdentityId}/`); + }); + + it('returns protected prefix when accessLevel is protected', async () => { + const mockTargetIdentityId = 'targetIdentityId'; + const prefix = await accessLevelAndIdentityAwarePrefixResolver({ + accessLevel: 'protected', + targetIdentityId: mockTargetIdentityId, + }); + expect(prefix).toEqual(`protected/${mockTargetIdentityId}/`); + }); + + it('returns public prefix when accessLevel is guest', async () => { + const prefix = await accessLevelAndIdentityAwarePrefixResolver({ + accessLevel: 'guest', + }); + expect(prefix).toEqual('public/'); + }); +}); diff --git a/packages/storage/package.json b/packages/storage/package.json index 76621119831..b1f1d4efc8c 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -97,6 +97,11 @@ "import": "./lib-esm/providers/s3/server.js", "require": "./lib/providers/s3/server.js" }, + "./s3/utils": { + "types": "./lib-esm/providers/s3/utilityApis.d.ts", + "import": "./lib-esm/providers/s3/utilityApis.js", + "require": "./lib/providers/s3/utilityApis.js" + }, "./package.json": "./package.json" }, "peerDependencies": { diff --git a/packages/storage/s3/utils/package.json b/packages/storage/s3/utils/package.json new file mode 100644 index 00000000000..e2e3bc5ebe8 --- /dev/null +++ b/packages/storage/s3/utils/package.json @@ -0,0 +1,7 @@ +{ + "name": "@aws-amplify/storage/s3/utils", + "main": "../../lib/providers/s3/utilityApis.js", + "browser": "../../lib-esm/providers/s3/utilityApis.js", + "module": "../../lib-esm/providers/s3/utilityApis.js", + "typings": "../../lib-esm/providers/s3/utilityApis.d.ts" +} diff --git a/packages/storage/src/providers/s3/apis/utilityApis/accessLevelAndIdentityAwarePrefixResolver.ts b/packages/storage/src/providers/s3/apis/utilityApis/accessLevelAndIdentityAwarePrefixResolver.ts new file mode 100644 index 00000000000..b20d51be5b1 --- /dev/null +++ b/packages/storage/src/providers/s3/apis/utilityApis/accessLevelAndIdentityAwarePrefixResolver.ts @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Amplify } from '@aws-amplify/core'; +import { resolvePrefix as defaultPrefixResolver } from '../../../../utils/resolvePrefix'; +import { ResolvePrefixInput } from '../../../../types/inputs'; + +/** + * Resolves the object prefix based on the {@link accessLevel} and {@link targetIdentityId}. + * + * This is the default prefix resolver used by the storage category and S3 provider. + * You can use this util function to determine the "full path" of an object in your + * interest. + * + * @param input The input that's required to resolve prefix. + * @param input.accessLevel The access level associated with the prefix. + * @param input.targetIdentityId The target identity associated with the prefix, + * if `undefined`, its value will be the identity id of current signed in user. + * @returns resolved prefix. + * + * @example + * import { defaultPrefixResolver } from 'aws-amplify/storage/s3/utils'; + * import { StorageAccessLevel } from '@aws-amplify/core'; + * + * async function getFullPathForKey({ + * key, + * accessLevel = 'guest', + * targetIdentityId, + * }: { + * key: string, + * accessLevel?: StorageAccessLevel, + * targetIdentityId?: string, + * }) { + * const prefix = await defaultPrefixResolver({ + * accessLevel, + * targetIdentityId, + * }); + * + * return `${prefix}${key}`; + * } + * + * const fullPath = await getFullPathForKey({ key: ''my-file.txt, accessLevel: 'private' }); + */ +export const accessLevelAndIdentityAwarePrefixResolver = async ( + input: ResolvePrefixInput +) => { + const { accessLevel, targetIdentityId } = input; + const { identityId } = await Amplify.Auth.fetchAuthSession(); + return defaultPrefixResolver({ + accessLevel, + targetIdentityId: targetIdentityId ?? identityId, + }); +}; diff --git a/packages/storage/src/providers/s3/utilityApis.ts b/packages/storage/src/providers/s3/utilityApis.ts new file mode 100644 index 00000000000..71e8d970769 --- /dev/null +++ b/packages/storage/src/providers/s3/utilityApis.ts @@ -0,0 +1,4 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export { accessLevelAndIdentityAwarePrefixResolver as defaultPrefixResolver } from './apis/utilityApis/accessLevelAndIdentityAwarePrefixResolver'; diff --git a/packages/storage/src/types/inputs.ts b/packages/storage/src/types/inputs.ts index a8602bf5bfd..732edb9a988 100644 --- a/packages/storage/src/types/inputs.ts +++ b/packages/storage/src/types/inputs.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { StorageAccessLevel } from '@aws-amplify/core'; import { StorageOptions, StorageListAllOptions, @@ -54,3 +55,8 @@ export type StorageUploadDataPayload = | ArrayBufferView | ArrayBuffer | string; + +export type ResolvePrefixInput = { + accessLevel: StorageAccessLevel; + targetIdentityId?: string; +}; diff --git a/packages/storage/src/utils/resolvePrefix.ts b/packages/storage/src/utils/resolvePrefix.ts index cba1973179d..10a85cc2ee5 100644 --- a/packages/storage/src/utils/resolvePrefix.ts +++ b/packages/storage/src/utils/resolvePrefix.ts @@ -1,19 +1,14 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { StorageAccessLevel } from '@aws-amplify/core'; import { assertValidationError } from '../errors/utils/assertValidationError'; import { StorageValidationErrorCode } from '../errors/types/validation'; - -type ResolvePrefixOptions = { - accessLevel: StorageAccessLevel; - targetIdentityId?: string; -}; +import { ResolvePrefixInput } from '../types/inputs'; export const resolvePrefix = ({ accessLevel, targetIdentityId, -}: ResolvePrefixOptions) => { +}: ResolvePrefixInput) => { if (accessLevel === 'private') { assertValidationError( !!targetIdentityId,