diff --git a/MIGRATION_v4.md b/MIGRATION_v4.md new file mode 100644 index 00000000..00aa8e83 --- /dev/null +++ b/MIGRATION_v4.md @@ -0,0 +1,47 @@ +# Migration from v3.x to v4.x + +When upgrading from v3 to v4, there are a few things to consider. + +## Changed method locations: + +- `cognito.restoreAndLoad()` remains the same; +- `cognito.authenticate()` remains the same; +- `cognito.authenticateUser()` --> `cognito.unauthenticated.verifyUserAuthentication()` +- `cognito.logout()` --> remains the same +- `cognito.invalidateAccessTokens()` --> remains the same +- `cognito.triggerResetPasswordMail()` --> `cognito.unauthenticated.triggerResetPasswordMail()` +- `cognito.updateResetPassword()` --> `cognito.unauthenticated.updateResetPassword()` +- `cognito.setNewPassword()` --> `cognito.unauthenticated.setInitialPassword()` +- `cognito.updatePassword()` --> `cognito.user.updatePassword()` +- `cognito.updateAttributes()` --> `cognito.user.updateAttributes()` +- `cognito.cognitoData.mfa` --> `cognito.user.mfa` +- `cognito.cognitoData.cognitoUser` --> `cognito.user.cognitoUser` +- `cognito.cognitoData.cognitoUserSession` --> `cognito.session.cognitoUserSession` +- `cognito.cognitoData.jwtToken` --> `cognito.session.jwtToken` +- `cognito.cognitoData.userAttributes` --> `cognito.user.userAttributes` +- `cognito.cognitoData.getAccessToken()` --> `cognito.session.getAccessToken()` +- `cognito.cognitoData.getIdToken()` --> `cognito.session.getIdToken()` +- `cognito.refreshAccessToken()` --> `cognito.session.refresh()` + +## `cognitoData` is no more + +As you can see in the above section, `cognito.cognitoData` has been replaced with `cognito.user` and `cognito.session`. + +These two properties will be set when the user is authenticated, else they will be `undefined`. When `isAuthenticated === true` you can assume they are set. + +In contrast, `unauthenticated` is _always_ available. + +## Change token auto-refresh + +In 4.x, JWT tokens will _not_ be automatically refreshed when they expire. +Instead, you can call `cognito.session.enableAutoRefresh()` and `cognito.session.disableAutoRefresh()` to start/stop the auto-refresh background job. + +There are also some new/changed methods to work with token refreshing: + +```js +cognito.session.refresh(); +cognito.session.refreshIfNeeded(); +cognito.session.secondsUntilExpires(); +cognito.session.needsRefresh(); +cognito.session.needsRefreshSoon(); +``` diff --git a/README.md b/README.md index 23114644..b63215a8 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,14 @@ Interact with AWS Cognito from your Ember app. This uses `amazon-cognito-identity-js` under the hood, which is considerably smaller in footprint than the quite enormous AWS Amplify SDK. If all you need is a way to work with the JWT tokens priovded by Cognito, then this addon is perfect for you. +[Upgrading from 3.x to 4.x](MIGRATION_v4.md) + ## Compatibility -* Ember.js v3.24 or above -* Ember CLI v3.24 or above -* Node.js v14 or above -* Native promise support required +- Ember.js v3.24 or above +- Ember CLI v3.24 or above +- Node.js v14 or above +- Native promise support required ## Installation @@ -59,66 +61,79 @@ export default class ApplicationRoute extends Route { After logging in (see below) you can access the JTW token like this: ```js -let token = this.cognito.cognitoData.jwtToken; +let token = this.cognito.session.jwtToken; ``` Here is a summary of the most important available methods - all methods return a promise: ```js +// methods that result in authentication/unauthentication cognito.restoreAndLoad(); cognito.authenticate({ username, password }); -cognito.authenticateUser({ username, password }); +cognito.mfaCompleteAuthentication(code); cognito.logout(); cognito.invalidateAccessTokens(); -cognito.triggerResetPasswordMail({ username }); -cognito.updateResetPassword({ username, code, newPassword }); -cognito.setNewPassword({ username, password, newPassword }); -cognito.updatePassword({ oldPassword, newPassword }); -cognito.updateAttributes(attributeMap); + +// methods for unauthenticated users +cognito.unauthenticated.verifyUserAuthentication({ username, password }); +cognito.unauthenticated.triggerResetPasswordMail({ username }); +cognito.unauthenticated.updateResetPassword({ username, code, newPassword }); +cognito.unauthenticated.setInitialPassword({ username, password, newPassword }); + +// the following are only available if authenticated +cognito.user.updatePassword(); +cognito.user.updateAttributes(); + +cognito.session.refresh(); +cognito.session.refreshIfNeeded(); +cognito.session.secondsUntilExpires(); + +cognito.user.mfa.enable(); +cognito.user.mfa.disable(); +cognito.user.mfa.isEnabled(); +cognito.user.mfa.setupDevice(); +cognito.user.mfa.verifyDevice(token); ``` ## Cognito service The `cognito` service provides promise-ified methods to work with AWS Cognito. -### isAuthenticated +```js +let { cognito } = this; -Will be true if a user is currently logged in. -This means that you can safely access `cognitoData` and work with it. +// A boolean to indicate the current status +let { isAuthenticated } = cognito; -### cognitoData +// Methods for dealing with unauthenticated users +let { unauthenticated } = cognito; -This property will contain an object with your main Cognito-related information, if the user is logged in. -If the user is not logged in, this will be `null`. +// Methods for dealing with the current user/session - only available if authenticated! +let { user, session } = cognito; -This is an object that looks like this: +// Methods to authenticate a user +await cognito.restoreAndLoad(); +await cognito.authenticate({ username, password }); +await cognito.mfaCompleteAuthentication(code); -```js -let cognitoData = { - cognitoUser: CognitoUser, - cognitoUserSession: CognitoUserSession, - jwtToken: 'xxxxx', - userAttributes: { Email: '...' }, - getAccessToken: () => CognitoAccessToken, - getIdToken: () => CognitoIdToken, - mfa: { - enable: () => {}, - disable: () => {}, - isEnabled: () => {}, - setupDevice: () => {}, - verifyDevice: (code) => {}, - }, -}; +// Methods to unauthenticate a user +cognito.logout(); +await cognito.invalidateAccessTokens(); ``` -### restoreAndLoad() +### `isAuthenticated` + +Will be true if a user is currently logged in. +This means that you can safely access `session` and `user` and work with it. + +### `restoreAndLoad()` Will try to lookup a prior session in local storage and authenticate the user. If this resolves, you can assume that the user is logged in. It will reject if the user is not logged in. Call this (and wait for it to complete) in your application route! -### authenticate({ username, password }) +### `authenticate({ username, password })` Try to login with the given username & password. Will reject with an Error, or resolve if successfull. @@ -133,53 +148,149 @@ cognito: { } ``` -### authenticateUser({ username, password }) +### `mfaCompleteAuthentication(code)` -Verify only the given username & password. -This will _not_ sign the user in, but can be used to e.g. guard special places in your app behind a repeated password check. -Will reject with an Error, or resolve if successfull. +This has to be called when `authenticate()` rejects with `MfaCodeRequiredError`. -### logout() +Returns a promise & signs the user in (if successful). + +### `logout()` Log out the user from the current device. -### invalidateAccessTokens() +### `invalidateAccessTokens()` -Logout & invalidate all issues access tokens (also on other devices). +Logout & invalidate all issued access tokens (also on other devices). In contrast, `logout()` does not revoke access tokens, it only removes them locally. Returns a promise. -### triggerResetPasswordMail({ username }) +## session -Trigger an email to get a verification code to reset the password. +The `session` object is available when the user is signed in. -Returns a promise. +Example usage: -### updateResetPassword({ username, code, newPassword }) +```js +cognito.session.jwtToken; +``` -Set a new password for a user. +It provides the following functionality: + +### `jwtToken` + +The current JWT access token. +This is a tracked property and may be updated in the background. + +### `getAccessToken()` + +Get the AWS access token object. +This includes metadata. + +### `getIdToken()` + +Get the AWS id token object. +This includes metadata. + +### `needsRefresh()` + +Returns true if the session is expired. + +### `needsRefreshSoon()` + +Returns true if the session will expire soon (in the next 15 minutes). + +### `secondsUntilExpires()` + +Returns the number of seconds until the session will expire. + +### `refresh()` + +Refresh the session, generating new JWT tokens. + +This will debounce when being run multiple times simultaneously. Returns a promise. -### setNewPassword({ username, password, newPassword }) +### `refreshIfNeeded()` -Set a new password, if a user requires a new password to be set (e.g. after an admin created the user). +Refresh the session only if it will expire soon. + +This will debounce when being run multiple times simultaneously. Returns a promise. -### updatePassword({ oldPassword, newPassword }) +## user + +The `user` object is available when the user is signed in. + +Example usage: + +```js +cognito.user.updatePassword({ oldPassword, newPassword }); +``` + +It provides the following functionality: + +### `userAttributes` + +This tracked property provides access to a key-value pair object of user attributes. + +### `updatePassword({ oldPassword, newPassword })` Update the password of the currently logged in user. Returns a promise. -## Token expiration +### `updateAttributes(attributes)` + +Update the attributes of the currently logged in user. + +This expects an object with key-value pairs, e.g.: + +```js +user.updateAttributes({ Email: 'my@email.com', Name: 'John Doe' }); +``` + +Returns a promise. + +### unauthenticated + +The `unauthenticated` object is always available. + +Example usage: + +```js +cognito.unauthenticated.triggerResetPasswordMail({ username }); +``` + +It provides the following functionality: + +### `verifyUserAuthentication({ username, password })` + +Verify only the given username & password. +This will _not_ sign the user in, but can be used to e.g. guard special places in your app behind a repeated password check. +Will reject with an Error, or resolve if successfull. + +### `triggerResetPasswordMail({ username })` -This addon will automatically refresh the JWT Token every 15 minutes before it expires. -The tokens have a lifespan of 60 minutes, so this should ensure that the local token never experies in the middle of a session. +Trigger an email to get a verification code to reset the password. + +Returns a promise. + +### `updateResetPassword({ username, code, newPassword })` + +Set a new password for a user. + +Returns a promise. -## Multi-Factor Authentication (MFA) +### `setInitialPassword({ username, password, newPassword })` + +Set a new password, if a user requires a new password to be set (e.g. after an admin created the user). + +Returns a promise. + +## mfa (Multi-Factor Authentication) This addon allows you to work with optional TOTP (Temporary One Time Password) MFA. SMS-based MFA is not supported for now (to reduce complexity, and since it is less secure than TOTP). @@ -190,10 +301,10 @@ Using MFA in your app requires changes in two places: The sign in process, and a ### Setting up MFA -A user can opt into MFA for their own account. For this, you can use the `mfa` object on the `cognitoData` object: +A user can opt into MFA for their own account. For this, you can use the `mfa` object on the `user` object: ```js -let { mfa } = this.cognito.cognitoData; +let { mfa } = this.cognito.user; // Available methods await mfa.enable(); @@ -277,6 +388,16 @@ await this.cognito.mfaCompleteAuthentication(mfaCode); After that, the user will be signed in (or it will throw an error if the MFA code is incorrect). +## Token expiration + +The generated JWT tokens have a lifespan of 60 minutes. After that, you need to refresh the session. + +You can call `cognito.session.enableAutoRefresh()` to start an automated background job, +which will refresh the token every 45 minutes. + +Alternatively, you can also manually handle this with the provided `session.needsRefresh()`, +`session.needsRefreshSoon()`, `session.refresh()` and `session.refreshIfNeeded()` methods. + ## Example You can find example components in the dummy app to see how a concrete implementation could look like. @@ -371,7 +492,7 @@ test('it works with new password required', function (assert) { ### Generating mocked data -You can generate mocked cognitoData with the provided utils: +You can generate a mocked user/session with the provided utils: ```js import ApplicationInstance from '@ember/application/instance'; @@ -383,7 +504,7 @@ export function initialize(appInstance: ApplicationInstance): void { let cognitoData = mockCognitoData(); if (cognitoData) { let cognito = appInstance.lookup('service:cognito'); - cognito.cognitoData = cognitoData; + cognito.setupSession(cognitoData); } } @@ -453,7 +574,7 @@ test('test helper correctly mocks a cognito session', async function (assert) { assert, }); - cognito.cognitoData = cognitoData; + cognito.setupSession(cognitoData); }); ``` diff --git a/addon-test-support/helpers/mock-cognito.ts b/addon-test-support/helpers/mock-cognito.ts index 1a27c33a..d34d6e2e 100644 --- a/addon-test-support/helpers/mock-cognito.ts +++ b/addon-test-support/helpers/mock-cognito.ts @@ -32,14 +32,16 @@ export function mockCognitoAuthenticated( hooks.beforeEach(function (this: any, assert: any) { let cognito = this.owner.lookup('service:cognito') as CognitoService; - cognito.cognitoData = mockCognitoData({ + let data = mockCognitoData({ jwtToken, username, assert: includeAssertSteps ? assert : undefined, })!; + cognito.setupSession(data); + // @ts-ignore - cognito.userPool.setCurrentUser(cognito.cognitoData.cognitoUser!); + cognito.userPool.setCurrentUser(data.user.cognitoUser); if (includeAssertSteps) { cognito._assert = assert; @@ -50,7 +52,7 @@ export function mockCognitoAuthenticated( } export function mockCognitoLogoutCurrentUser(cognito: CognitoService): void { - cognito.cognitoData = null; + cognito.setupSession(undefined); // @ts-ignore cognito.userPool.setCurrentUser(undefined); diff --git a/addon/services/cognito.ts b/addon/services/cognito.ts index 48e15d7b..6142936e 100644 --- a/addon/services/cognito.ts +++ b/addon/services/cognito.ts @@ -1,151 +1,50 @@ +import 'ember-cached-decorator-polyfill'; import { getOwner } from '@ember/application'; +import type ApplicationInstance from '@ember/application/instance'; import { assert } from '@ember/debug'; -import RouterService from '@ember/routing/router-service'; -import Service, { inject as service } from '@ember/service'; -import { getOwnConfig, isTesting, macroCondition } from '@embroider/macros'; -import { tracked } from '@glimmer/tracking'; -import { - CognitoAccessToken, - CognitoIdToken, - CognitoUser, - CognitoUserPool, - CognitoUserSession, - ICognitoStorage, - ICognitoUserAttributeData, -} from 'amazon-cognito-identity-js'; -import { - MfaCodeRequiredError, - NewPasswordRequiredError, -} from 'ember-cognito-identity/errors/cognito'; -import { CognitoUserMfa } from 'ember-cognito-identity/utils/cognito-mfa'; -import { authenticateUser } from 'ember-cognito-identity/utils/cognito/authenticate-user'; -import { globalSignOut } from 'ember-cognito-identity/utils/cognito/global-sign-out'; -import { refreshAccessToken } from 'ember-cognito-identity/utils/cognito/refresh-access-token'; -import { setNewPassword } from 'ember-cognito-identity/utils/cognito/set-new-password'; -import { triggerResetPasswordMail } from 'ember-cognito-identity/utils/cognito/trigger-reset-password-mail'; -import { updatePassword } from 'ember-cognito-identity/utils/cognito/update-password'; -import { updateResetPassword } from 'ember-cognito-identity/utils/cognito/update-reset-password'; -import { updateUserAttributes } from 'ember-cognito-identity/utils/cognito/update-user-attributes'; -import { getTokenRefreshWait } from 'ember-cognito-identity/utils/get-token-refresh-wait'; -import { getUserAttributes } from 'ember-cognito-identity/utils/get-user-attributes'; -import { loadUserDataAndAccessToken } from 'ember-cognito-identity/utils/load-user-data-and-access-token'; -import { mockCognitoUser } from 'ember-cognito-identity/utils/mock/cognito-user'; +import Service from '@ember/service'; +import { getOwnConfig, macroCondition } from '@embroider/macros'; +import { cached, tracked } from '@glimmer/tracking'; +import { CognitoUserPool, ICognitoStorage } from 'amazon-cognito-identity-js'; +import 'ember-cached-decorator-polyfill'; +import { CognitoAuthenticatedUser } from 'ember-cognito-identity/utils/cognito-authenticated-user'; +import { CognitoSession } from 'ember-cognito-identity/utils/cognito-session'; +import { CognitoUnauthenticatedUser } from 'ember-cognito-identity/utils/cognito-unauthenticated-user'; import { mockCognitoUserPool } from 'ember-cognito-identity/utils/mock/cognito-user-pool'; -import { restartableTask, timeout } from 'ember-concurrency'; -import { taskFor } from 'ember-concurrency-ts'; -import type ApplicationInstance from '@ember/application/instance'; - -export interface CognitoData { - cognitoUser: CognitoUser; - userAttributes: UserAttributes; - cognitoUserSession: CognitoUserSession; - jwtToken: string; - getAccessToken: () => CognitoAccessToken; - getIdToken: () => CognitoIdToken; - mfa: CognitoUserMfa; -} +// @ts-ignore +import { associateDestroyableChild, destroy } from '@ember/destroyable'; export type UserAttributes = { [key: string]: any }; export default class CognitoService extends Service { - @service declare router: RouterService; + @tracked session?: CognitoSession; + @tracked user?: CognitoAuthenticatedUser; // Overwrite for testing _cognitoStorage: undefined | ICognitoStorage; - @tracked cognitoData: null | CognitoData = null; - - // When calling `authenticate()` throws a `MfaCodeRequiredError`, we cache the user here - // We need it when then calling `mfaCompleteAuthentication()` later, at which point it will be reset - _tempMfaCognitoUser?: CognitoUser; - // Can be set in tests to generate assert.step() logs _assert?: any; get isAuthenticated(): boolean { - return Boolean(this.cognitoData); - } - - get config(): { - userPoolId: string; - clientId: string; - endpoint?: string; - authenticationFlowType?: 'USER_SRP_AUTH' | 'USER_PASSWORD_AUTH'; - } { - let config = (getOwner(this) as ApplicationInstance).resolveRegistration( - 'config:environment' - ) as any; - return config.cognito; + return !!this.session; } - shouldAutoRefresh = !isTesting(); - - _userPool: CognitoUserPool | undefined; - get userPool(): CognitoUserPool { - if (this._userPool) { - return this._userPool; - } - - if (macroCondition(getOwnConfig().enableMocks)) { - // eslint-disable-next-line ember/no-side-effects - this._userPool = mockCognitoUserPool() as unknown as CognitoUserPool; - - return this._userPool; - } - - assert( - 'A `cognito` configuration object needs to be defined in config/environment.js', - typeof this.config === 'object' - ); - let { userPoolId, clientId, endpoint } = this.config; - - assert( - '`userPoolId` must be specified in the `cognito` configuration in config/environment.js', - userPoolId - ); - assert( - '`clientId` must be specified in the `cognito` configuration in config/environment.js', - clientId - ); - - let poolData = { - UserPoolId: userPoolId, - ClientId: clientId, - Storage: this._cognitoStorage, - endpoint, - }; - - // eslint-disable-next-line ember/no-side-effects - this._userPool = new CognitoUserPool(poolData); - - return this._userPool; - } - - // This should be called in application route, before everything else - async restoreAndLoad(): Promise { - if (this.cognitoData) { - return this.cognitoData; - } - - this.cognitoData = await loadUserDataAndAccessToken(this.userPool); - - if (this.shouldAutoRefresh) { - taskFor(this._debouncedRefreshAccessToken).perform(); + async restoreAndLoad(): Promise { + if (this.session && this.user) { + return; } - return this.cognitoData; - } + let data = await this.unauthenticated.restoreAndLoad(); - async refreshAccessToken(): Promise { - assert( - 'cognitoData is not setup, user is probably not logged in', - !!this.cognitoData + let user = new CognitoAuthenticatedUser( + data.cognitoUser, + data.userAttributes ); - let { cognitoUser, cognitoUserSession } = this.cognitoData!; + let session = new CognitoSession(data.cognitoUser, data.cognitoUserSession); - await refreshAccessToken(cognitoUserSession, cognitoUser); - this.cognitoData = await loadUserDataAndAccessToken(this.userPool); + this.setupSession({ user, session }); } async authenticate({ @@ -154,243 +53,92 @@ export default class CognitoService extends Service { }: { username: string; password: string; - }): Promise { - assert('cognitoData is already setup', !this.cognitoData); - - await this._authenticate({ username, password }); + }): Promise { + await this.unauthenticated.authenticate({ username, password }); await this.restoreAndLoad(); + } - return this.cognitoData!; + async mfaCompleteAuthentication(code: string) { + await this.unauthenticated.mfaCompleteAuthentication(code); + await this.restoreAndLoad(); } - logout(): void { - if (this.cognitoData) { - this.cognitoData.cognitoUser.signOut(); - this.cognitoData = null; + setupSession(data?: { + user: CognitoAuthenticatedUser; + session: CognitoSession; + }) { + this.user = data?.user; - taskFor(this._debouncedRefreshAccessToken).cancelAll(); + if (this.session) { + destroy(this.session); } - } - - // Only call this if you are in an inconsistent state - logoutForce(): void { - this.userPool.getCurrentUser()?.signOut(); - this.cognitoData = null; - taskFor(this._debouncedRefreshAccessToken).cancelAll(); + this.session = data + ? associateDestroyableChild(this, data.session) + : undefined; } - invalidateAccessTokens(): Promise { - assert( - 'cognitoData is not set, make sure to be authenticated before calling `invalidateAccessTokens()`', - !!this.cognitoData - ); - - let { cognitoUser } = this.cognitoData!; - - return globalSignOut(cognitoUser); + logout(): void { + this.session?.logout(); + this.setupSession(undefined); } - triggerResetPasswordMail({ username }: { username: string }): Promise { - let cognitoUser = this._createCognitoUser({ username }); - - return triggerResetPasswordMail(cognitoUser); + async invalidateAccessTokens(): Promise { + await this.session?.invalidateAccessTokens(); + this.setupSession(undefined); } - async mfaCompleteAuthentication(code: string): Promise { - assert( - 'mfaCompleteAuthentication: You may only call this method after calling `authenticate()` before, leading to a `MfaCodeRequiredError` error.', - !!this._tempMfaCognitoUser - ); - - let cognitoMfa = new CognitoUserMfa(this._tempMfaCognitoUser!); - - await cognitoMfa.completeAuthentication(code); - this._tempMfaCognitoUser = undefined; - - await this.restoreAndLoad(); + @cached + get unauthenticated(): CognitoUnauthenticatedUser { + return new CognitoUnauthenticatedUser(this.userPool, { + authenticationFlowType: this.config.authenticationFlowType, + _cognitoStorage: this._cognitoStorage, + _assert: this._assert, + }); } - /* - Might reject with: - * InvalidPasswordError (e.g. password too short) - * VerificationCodeMismatchError (wrong code) - */ - updateResetPassword({ - username, - code, - newPassword, - }: { - username: string; - code: string; - newPassword: string; - }): Promise { - let cognitoUser = this._createCognitoUser({ username }); - - return updateResetPassword(cognitoUser, { code, newPassword }); + get config(): { + userPoolId: string; + clientId: string; + endpoint?: string; + authenticationFlowType?: 'USER_SRP_AUTH' | 'USER_PASSWORD_AUTH'; + } { + let config = (getOwner(this) as ApplicationInstance).resolveRegistration( + 'config:environment' + ) as any; + return config.cognito; } - /* - Might reject with: - * InvalidPasswordError (e.g. password too short) - */ - async setNewPassword( - { - username, - password, - newPassword, - }: { username: string; password: string; newPassword: string }, - newAttributes = {} - ): Promise { - let cognitoUser = this._createCognitoUser({ username }); - - try { - await this._authenticate({ username, password, cognitoUser }); - } catch (error) { - // This _should_ error, otherwise the user does not need to set a new password - if (!(error instanceof NewPasswordRequiredError)) { - throw error; - } - - await setNewPassword(cognitoUser, { newPassword }, newAttributes); - return; + @cached + get userPool(): CognitoUserPool { + if (macroCondition(getOwnConfig().enableMocks)) { + // eslint-disable-next-line ember/no-side-effects + return mockCognitoUserPool() as unknown as CognitoUserPool; } assert( - 'You seem to have called `setNewPassword` without it being required.', - false + 'A `cognito` configuration object needs to be defined in config/environment.js', + typeof this.config === 'object' ); - } + let { userPoolId, clientId, endpoint } = this.config; - updatePassword({ - oldPassword, - newPassword, - }: { - oldPassword: string; - newPassword: string; - }): Promise { assert( - 'cognitoData is not set, make sure to be authenticated before calling `updatePassword()`', - !!this.cognitoData + '`userPoolId` must be specified in the `cognito` configuration in config/environment.js', + userPoolId ); - - let { cognitoUser } = this.cognitoData!; - - return updatePassword(cognitoUser, { oldPassword, newPassword }); - } - - async updateAttributes(attributes: { - [index: string]: string; - }): Promise<{ [index: string]: string }> { assert( - 'cognitoData is not set, make sure to be authenticated before calling `updateAttributes()`', - !!this.cognitoData - ); - - let { cognitoUser } = this.cognitoData!; - - let attributeList: ICognitoUserAttributeData[] = Object.keys( - attributes - ).map((attributeName) => { - return { - Name: attributeName, - Value: attributes[attributeName]!, - }; - }); - - await updateUserAttributes(cognitoUser, attributeList); - let userAttributes = await getUserAttributes(cognitoUser); - - this.cognitoData!.userAttributes = userAttributes; - - return userAttributes; - } - - authenticateUser( - cognitoUser: CognitoUser, - { username, password }: { username: string; password: string } - ): Promise { - return authenticateUser(cognitoUser, { username, password }); - } - - @restartableTask - *_debouncedRefreshAccessToken(): any { - assert( - 'cognitoData is not set, make sure to be authenticated before calling `_debouncedRefreshAccessToken.perform()`', - !!this.cognitoData - ); - - let autoRefreshWait = getTokenRefreshWait( - this.cognitoData.cognitoUserSession + '`clientId` must be specified in the `cognito` configuration in config/environment.js', + clientId ); - yield timeout(autoRefreshWait); - - try { - yield this.refreshAccessToken(); - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - // We want to continue and try again - } - - taskFor(this._debouncedRefreshAccessToken).perform(); - } - - async _authenticate({ - username, - password, - cognitoUser, - }: { - username: string; - password: string; - cognitoUser?: CognitoUser; - }): Promise { - assert('cognitoData is already setup', !this.cognitoData); - - let actualCognitoUser = - typeof cognitoUser === 'undefined' - ? this._createCognitoUser({ username }) - : cognitoUser; - - try { - return await this.authenticateUser(actualCognitoUser, { - username, - password, - }); - } catch (error) { - if (error instanceof MfaCodeRequiredError) { - this._tempMfaCognitoUser = error.cognitoUser; - } - - throw error; - } - } - - _createCognitoUser({ username }: { username: string }): CognitoUser { - let { userPool, _cognitoStorage: storage } = this; - - let userData = { - Username: username, - Pool: userPool, - Storage: storage, + let poolData = { + UserPoolId: userPoolId, + ClientId: clientId, + Storage: this._cognitoStorage, + endpoint, }; - if (macroCondition(getOwnConfig().enableMocks)) { - return mockCognitoUser({ - username, - userPool, - assert: this._assert, - }) as unknown as CognitoUser; - } - - let cognitoUser = new CognitoUser(userData); - let { config } = this; - if (config.authenticationFlowType) { - cognitoUser.setAuthenticationFlowType(config.authenticationFlowType); - } - - return cognitoUser; + return new CognitoUserPool(poolData); } } diff --git a/addon/utils/cognito-authenticated-user.ts b/addon/utils/cognito-authenticated-user.ts new file mode 100644 index 00000000..a1ae9582 --- /dev/null +++ b/addon/utils/cognito-authenticated-user.ts @@ -0,0 +1,57 @@ +import { + CognitoUser, + ICognitoUserAttributeData, +} from 'amazon-cognito-identity-js'; +import { CognitoUserMfa } from './cognito-mfa'; +import { updatePassword } from './cognito/update-password'; +import { updateUserAttributes } from './cognito/update-user-attributes'; +import { getUserAttributes } from './get-user-attributes'; + +export type UserAttributes = { [key: string]: any }; + +export class CognitoAuthenticatedUser { + cognitoUser: CognitoUser; + userAttributes: UserAttributes; + mfa: CognitoUserMfa; + + constructor(cognitoUser: CognitoUser, userAttributes: UserAttributes) { + this.cognitoUser = cognitoUser; + this.userAttributes = userAttributes; + this.mfa = new CognitoUserMfa(cognitoUser); + } + + updatePassword({ + oldPassword, + newPassword, + }: { + oldPassword: string; + newPassword: string; + }): Promise { + let { cognitoUser } = this; + + return updatePassword(cognitoUser, { oldPassword, newPassword }); + } + + async updateAttributes(attributes: { + [index: string]: string; + }): Promise { + let { cognitoUser } = this; + + let attributeList: ICognitoUserAttributeData[] = Object.keys( + attributes + ).map((attributeName) => { + return { + Name: attributeName, + Value: attributes[attributeName]!, + }; + }); + + await updateUserAttributes(cognitoUser, attributeList); + + let userAttributes = await getUserAttributes(cognitoUser); + + this.userAttributes = userAttributes; + + return userAttributes; + } +} diff --git a/addon/utils/cognito-session.ts b/addon/utils/cognito-session.ts new file mode 100644 index 00000000..d1c7a8aa --- /dev/null +++ b/addon/utils/cognito-session.ts @@ -0,0 +1,122 @@ +import { tracked } from '@glimmer/tracking'; +import { CognitoUser, CognitoUserSession } from 'amazon-cognito-identity-js'; +import { globalSignOut } from './cognito/global-sign-out'; +import { refreshAccessToken } from './cognito/refresh-access-token'; +import { + getSecondsUntilTokenExpires, + getTokenRefreshWait, +} from './get-token-refresh-wait'; +import { reloadUserSession } from './load-user-data-and-access-token'; +// @ts-ignore +import { registerDestructor } from '@ember/destroyable'; +import { cancel, later } from '@ember/runloop'; + +export class CognitoSession { + cognitoUser: CognitoUser; + + @tracked cognitoUserSession: CognitoUserSession; + @tracked jwtToken!: string; + + constructor( + cognitoUser: CognitoUser, + cognitoUserSession: CognitoUserSession + ) { + this.cognitoUser = cognitoUser; + this.cognitoUserSession = cognitoUserSession; + + this._setJwtToken(); + + registerDestructor(this, () => this.disableAutoRefresh()); + } + + logout(): void { + this.cognitoUser.signOut(); + } + + async invalidateAccessTokens(): Promise { + await globalSignOut(this.cognitoUser); + } + + getAccessToken() { + return this.cognitoUserSession.getAccessToken(); + } + + getIdToken() { + return this.cognitoUserSession.getIdToken(); + } + + needsRefresh() { + return !this.cognitoUserSession.isValid(); + } + + // This will return true if the token will expire in the next 15 minutes + needsRefreshSoon() { + return this.secondsUntilExpires() <= 15 * 60; + } + + secondsUntilExpires() { + return getSecondsUntilTokenExpires(this.cognitoUserSession); + } + + refresh(): Promise { + if (this._pendingRefresh) { + return this._pendingRefresh; + } + + this._pendingRefresh = this._refresh(); + return this._pendingRefresh; + } + + _timeout?: any; + enableAutoRefresh() { + if (this._timeout) { + this.disableAutoRefresh(); + } + + let timeout = later( + this, + async () => { + try { + await this.refreshIfNeeded(); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + // We want to continue and try again + } + + this.enableAutoRefresh(); + }, + getTokenRefreshWait(this.cognitoUserSession) + ); + + this._timeout = timeout; + } + + disableAutoRefresh() { + if (this._timeout) { + cancel(this._timeout); + this._timeout = undefined; + } + } + + async refreshIfNeeded(): Promise { + if (this.needsRefreshSoon()) { + await this.refresh(); + } + } + + _pendingRefresh?: Promise; + async _refresh(): Promise { + await refreshAccessToken(this.cognitoUserSession, this.cognitoUser); + await this.reloadData(); + } + + async reloadData() { + this.cognitoUserSession = await reloadUserSession(this.cognitoUser); + this._setJwtToken(); + } + + _setJwtToken() { + this.jwtToken = this.getAccessToken().getJwtToken(); + } +} diff --git a/addon/utils/cognito-unauthenticated-user.ts b/addon/utils/cognito-unauthenticated-user.ts new file mode 100644 index 00000000..22c668ce --- /dev/null +++ b/addon/utils/cognito-unauthenticated-user.ts @@ -0,0 +1,193 @@ +import { assert } from '@ember/debug'; +import { getOwnConfig, macroCondition } from '@embroider/macros'; +import { + CognitoUser, + CognitoUserPool, + ICognitoStorage, +} from 'amazon-cognito-identity-js'; +import { + MfaCodeRequiredError, + NewPasswordRequiredError, +} from 'ember-cognito-identity/errors/cognito'; +import { CognitoUserMfa } from './cognito-mfa'; +import { authenticateUser } from './cognito/authenticate-user'; +import { setNewPassword } from './cognito/set-new-password'; +import { triggerResetPasswordMail } from './cognito/trigger-reset-password-mail'; +import { updateResetPassword } from './cognito/update-reset-password'; +import { + CognitoData, + loadUserFromUserPool, +} from './load-user-data-and-access-token'; +import { mockCognitoUser } from './mock/cognito-user'; + +type AuthenticationFlowType = 'USER_SRP_AUTH' | 'USER_PASSWORD_AUTH'; + +export class CognitoUnauthenticatedUser { + userPool: CognitoUserPool; + authenticationFlowType?: AuthenticationFlowType; + + // Overwrite for testing + _cognitoStorage: undefined | ICognitoStorage; + + // Can be set in tests to generate assert.step() logs + _assert?: any; + + constructor( + userPool: CognitoUserPool, + options?: { + authenticationFlowType?: AuthenticationFlowType; + _cognitoStorage?: ICognitoStorage; + _assert?: any; + } + ) { + this.userPool = userPool; + this._cognitoStorage = options?._cognitoStorage; + this._assert = options?._assert; + } + + // When calling `authenticate()` throws a `MfaCodeRequiredError`, we cache the user here + // We need it when then calling `mfaCompleteAuthentication()` later, at which point it will be reset + _tempMfaCognitoUser?: CognitoUser; + + async restoreAndLoad(): Promise { + let cognitoData = await loadUserFromUserPool(this.userPool); + + return cognitoData; + } + + verifyUserAuthentication({ + username, + password, + cognitoUser, + }: { + username: string; + password: string; + cognitoUser?: CognitoUser; + }): Promise { + let actualCognitoUser = + typeof cognitoUser === 'undefined' + ? this._createCognitoUser({ username }) + : cognitoUser; + + return authenticateUser(actualCognitoUser, { username, password }); + } + + async mfaCompleteAuthentication(code: string): Promise { + assert( + 'mfaCompleteAuthentication: You may only call this method after calling `authenticate()` before, leading to a `MfaCodeRequiredError` error.', + !!this._tempMfaCognitoUser + ); + + let cognitoMfa = new CognitoUserMfa(this._tempMfaCognitoUser!); + + await cognitoMfa.completeAuthentication(code); + this._tempMfaCognitoUser = undefined; + } + + triggerResetPasswordMail({ username }: { username: string }): Promise { + let cognitoUser = this._createCognitoUser({ username }); + + return triggerResetPasswordMail(cognitoUser); + } + + /* + Might reject with: + * InvalidPasswordError (e.g. password too short) + * VerificationCodeMismatchError (wrong code) + */ + updateResetPassword({ + username, + code, + newPassword, + }: { + username: string; + code: string; + newPassword: string; + }): Promise { + let cognitoUser = this._createCognitoUser({ username }); + + return updateResetPassword(cognitoUser, { code, newPassword }); + } + + /* + Might reject with: + * InvalidPasswordError (e.g. password too short) + */ + async setInitialPassword( + { + username, + password, + newPassword, + }: { username: string; password: string; newPassword: string }, + newAttributes = {} + ): Promise { + let cognitoUser = this._createCognitoUser({ username }); + + try { + await this.authenticate({ username, password, cognitoUser }); + } catch (error) { + // This _should_ error, otherwise the user does not need to set a new password + if (!(error instanceof NewPasswordRequiredError)) { + throw error; + } + + await setNewPassword(cognitoUser, { newPassword }, newAttributes); + return; + } + + assert( + 'You seem to have called `setNewPassword` without it being required.', + false + ); + } + + async authenticate({ + username, + password, + cognitoUser, + }: { + username: string; + password: string; + cognitoUser?: CognitoUser; + }): Promise { + try { + return await this.verifyUserAuthentication({ + username, + password, + cognitoUser, + }); + } catch (error) { + if (error instanceof MfaCodeRequiredError) { + this._tempMfaCognitoUser = error.cognitoUser; + } + + throw error; + } + } + + _createCognitoUser({ username }: { username: string }): CognitoUser { + let { userPool, _cognitoStorage: storage } = this; + + let userData = { + Username: username, + Pool: userPool, + Storage: storage, + }; + + if (macroCondition(getOwnConfig().enableMocks)) { + return mockCognitoUser({ + username, + userPool, + assert: this._assert, + }) as unknown as CognitoUser; + } + + let cognitoUser = new CognitoUser(userData); + let { authenticationFlowType } = this; + if (authenticationFlowType) { + cognitoUser.setAuthenticationFlowType(authenticationFlowType); + } + + return cognitoUser; + } +} diff --git a/addon/utils/get-token-refresh-wait.ts b/addon/utils/get-token-refresh-wait.ts index b0792cf6..0f166643 100644 --- a/addon/utils/get-token-refresh-wait.ts +++ b/addon/utils/get-token-refresh-wait.ts @@ -1,16 +1,33 @@ import { CognitoUserSession } from 'amazon-cognito-identity-js'; -export function getTokenRefreshWait( +// Taken from: +// https://github.com/aws-amplify/amplify-js/blob/08e01b1c09cfab73f2eb1b1b18fe1a696e2a028f/packages/amazon-cognito-identity-js/src/CognitoUserSession.js#L73 +export function getSecondsUntilTokenExpires( cognitoUserSession: CognitoUserSession ): number { - let expirationTimestamp = cognitoUserSession.getAccessToken().getExpiration(); let now = Math.floor(+new Date() / 1000); + // @ts-ignore + let drift = cognitoUserSession.getClockDrift(); + let adjusted = now - drift; + + // Whichever is next + let remaining = Math.min( + cognitoUserSession.getAccessToken().getExpiration() - adjusted, + cognitoUserSession.getIdToken().getExpiration() - adjusted + ); + + // Cannot be below 0 + return Math.max(remaining, 0); +} + +// In ms +export function getTokenRefreshWait( + cognitoUserSession: CognitoUserSession +): number { + let remaining = getSecondsUntilTokenExpires(cognitoUserSession); // We want to refresh 15 minutes before the actual expiration date, to leave some wiggle room let offset = 15 * 60; - // Fall back to default of 45 mins, if timestamp is not available for whatever reason. - return expirationTimestamp - ? Math.max(0, expirationTimestamp - now - offset) * 1000 - : 60 * 45 * 1000; + return Math.max(0, remaining - offset) * 1000; } diff --git a/addon/utils/load-user-data-and-access-token.ts b/addon/utils/load-user-data-and-access-token.ts index 92b9567a..2f4d82bd 100644 --- a/addon/utils/load-user-data-and-access-token.ts +++ b/addon/utils/load-user-data-and-access-token.ts @@ -1,17 +1,35 @@ import { + CognitoUser, CognitoUserPool, CognitoUserSession, } from 'amazon-cognito-identity-js'; import { CognitoNotAuthenticatedError } from 'ember-cognito-identity/errors/cognito'; -import { - CognitoData, - UserAttributes, -} from 'ember-cognito-identity/services/cognito'; -import { CognitoUserMfa } from './cognito-mfa'; +import { UserAttributes } from 'ember-cognito-identity/services/cognito'; import { getUserSession } from './cognito/get-user-session'; import { getUserAttributes } from './get-user-attributes'; -export async function loadUserDataAndAccessToken( +export interface CognitoData { + cognitoUser: CognitoUser; + cognitoUserSession: CognitoUserSession; + userAttributes: { [key: string]: any }; +} + +export async function reloadUserSession( + cognitoUser: CognitoUser +): Promise { + if (!cognitoUser) { + throw new CognitoNotAuthenticatedError(); + } + + try { + return await getUserSession(cognitoUser); + } catch (error) { + cognitoUser.signOut(); + throw error; + } +} + +export async function loadUserFromUserPool( userPool: CognitoUserPool ): Promise { let cognitoUser = userPool.getCurrentUser(); @@ -29,17 +47,9 @@ export async function loadUserDataAndAccessToken( throw error; } - let jwtToken = cognitoUserSession.getAccessToken().getJwtToken(); - - let cognitoData: CognitoData = { + return { cognitoUser, userAttributes, cognitoUserSession, - jwtToken, - getAccessToken: () => cognitoUserSession.getAccessToken(), - getIdToken: () => cognitoUserSession.getIdToken(), - mfa: new CognitoUserMfa(cognitoUser), }; - - return cognitoData; } diff --git a/addon/utils/mock/cognito-data.ts b/addon/utils/mock/cognito-data.ts index 8b599558..303288a0 100644 --- a/addon/utils/mock/cognito-data.ts +++ b/addon/utils/mock/cognito-data.ts @@ -1,7 +1,7 @@ import { getOwnConfig, macroCondition } from '@embroider/macros'; -import { CognitoUser } from 'amazon-cognito-identity-js'; -import { CognitoData } from 'ember-cognito-identity/services/cognito'; -import { CognitoUserMfa } from '../cognito-mfa'; +import { CognitoUser, CognitoUserSession } from 'amazon-cognito-identity-js'; +import { CognitoAuthenticatedUser } from '../cognito-authenticated-user'; +import { CognitoSession } from '../cognito-session'; import { mockCognitoUser } from './cognito-user'; import { mockCognitoUserSession } from './cognito-user-session'; @@ -15,20 +15,23 @@ export function mockCognitoData({ username?: string; mfaEnabled?: boolean; assert?: any; -} = {}): CognitoData | undefined { +} = {}): + | { session: CognitoSession; user: CognitoAuthenticatedUser } + | undefined { if (macroCondition(!getOwnConfig()?.enableMocks)) { return undefined; } - let cognitoUserSession = mockCognitoUserSession({ jwtToken })!; + let cognitoUserSession = mockCognitoUserSession({ + jwtToken, + }) as unknown as CognitoUserSession; + let cognitoUser = mockCognitoUser({ username, cognitoUserSession, mfaEnabled, assert, - })!; - - let mfa = new CognitoUserMfa(cognitoUser as unknown as CognitoUser); + }) as unknown as CognitoUser; /* eslint-disable camelcase */ let userAttributes = { @@ -37,15 +40,8 @@ export function mockCognitoData({ }; /* eslint-enable camelcase */ - let cognitoData: unknown = { - cognitoUser, - cognitoUserSession, - userAttributes, - jwtToken, - getAccessToken: () => cognitoUserSession.getAccessToken(), - getIdToken: () => cognitoUserSession.getIdToken(), - mfa, - }; + let session = new CognitoSession(cognitoUser, cognitoUserSession); + let user = new CognitoAuthenticatedUser(cognitoUser, userAttributes); - return cognitoData as CognitoData; + return { session, user }; } diff --git a/addon/utils/mock/cognito-user-session.ts b/addon/utils/mock/cognito-user-session.ts index e2d4736b..ee3cf210 100644 --- a/addon/utils/mock/cognito-user-session.ts +++ b/addon/utils/mock/cognito-user-session.ts @@ -32,6 +32,16 @@ class MockCognitoUserSession { return this.getAccessToken(); } + isValid() { + let now = +new Date(); + + return now < +this.#expirationDate; + } + + getClockDrift() { + return 0; + } + get refreshToken() { return { getToken: () => { diff --git a/package.json b/package.json index 5798633b..8d58d834 100644 --- a/package.json +++ b/package.json @@ -35,14 +35,13 @@ "@embroider/macros": "^1.0.0", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", - "@types/amazon-cognito-auth-js": "^1.2.2", - "amazon-cognito-identity-js": "^5.2.4", + "@types/amazon-cognito-auth-js": "^1.3.0", + "amazon-cognito-identity-js": "^5.2.10", "ember-auto-import": "^2.4.2", + "ember-cached-decorator-polyfill": "^0.1.4", "ember-cli-babel": "^7.26.11", "ember-cli-htmlbars": "^6.1.0", - "ember-cli-typescript": "^5.0.0", - "ember-concurrency": "^2.0.1", - "ember-concurrency-ts": "^0.3.1" + "ember-cli-typescript": "^5.0.0" }, "devDependencies": { "@babel/core": "~7.18.2", @@ -51,8 +50,9 @@ "@ember/optional-features": "~2.0.0", "@ember/test-helpers": "~2.8.1", "@embroider/test-setup": "~1.8.3", + "@release-it-plugins/lerna-changelog": "~5.0.0", "@simple-dom/interface": "~1.4.0", - "@types/ember": "~4.0.0", + "@types/ember": "~4.0.1", "@types/ember-qunit": "~5.0.0", "@types/ember__test-helpers": "~2.8.0", "@typescript-eslint/parser": "~5.32.0", @@ -64,6 +64,8 @@ "ember-cli-inject-live-reload": "~2.1.0", "ember-cli-terser": "~4.0.2", "ember-cli-typescript-blueprints": "~3.0.0", + "ember-concurrency": "~2.2.1", + "ember-concurrency-ts": "~0.3.1", "ember-disable-prototype-extensions": "~1.1.3", "ember-load-initializers": "~2.1.2", "ember-modifier": "~3.2.2", @@ -83,7 +85,6 @@ "qunit": "~2.19.1", "qunit-dom": "~2.0.0", "release-it": "~15.2.0", - "@release-it-plugins/lerna-changelog": "~5.0.0", "typescript": "~4.7.2", "webpack": "~5.74.0" }, diff --git a/tests/acceptance/login-test.ts b/tests/acceptance/login-test.ts index 1cc240b9..10c1bf5c 100644 --- a/tests/acceptance/login-test.ts +++ b/tests/acceptance/login-test.ts @@ -1,8 +1,8 @@ import { click, fillIn, - visit, TestContext as Context, + visit, } from '@ember/test-helpers'; import CognitoService from 'ember-cognito-identity/services/cognito'; import { MOCK_COGNITO_CONFIG } from 'ember-cognito-identity/utils/mock/cognito-user'; @@ -43,7 +43,7 @@ module('Acceptance | login', function (hooks) { assert.ok(cognito.isAuthenticated, 'user is authenticated now'); assert.strictEqual( - cognito.cognitoData && cognito.cognitoData.jwtToken, + cognito.session?.jwtToken, JWT_TOKEN, 'correct jwtToken is set on service' ); @@ -134,7 +134,7 @@ module('Acceptance | login', function (hooks) { assert.ok(cognito.isAuthenticated, 'user is authenticated now'); assert.strictEqual( - cognito.cognitoData && cognito.cognitoData.jwtToken, + cognito.session?.jwtToken, JWT_TOKEN, 'correct jwtToken is set on service' ); @@ -230,7 +230,7 @@ module('Acceptance | login', function (hooks) { assert.ok(cognito.isAuthenticated, 'user is authenticated now'); assert.strictEqual( - cognito.cognitoData && cognito.cognitoData.jwtToken, + cognito.session?.jwtToken, JWT_TOKEN, 'correct jwtToken is set on service' ); diff --git a/tests/acceptance/logout-test.ts b/tests/acceptance/logout-test.ts new file mode 100644 index 00000000..6be4e8b7 --- /dev/null +++ b/tests/acceptance/logout-test.ts @@ -0,0 +1,52 @@ +import { + click, + currentRouteName, + TestContext as Context, + visit, +} from '@ember/test-helpers'; +import CognitoService from 'ember-cognito-identity/services/cognito'; +import { mockCognitoAuthenticated } from 'ember-cognito-identity/test-support/helpers/mock-cognito'; +import { setupApplicationTest } from 'ember-qunit'; +import { module, test } from 'qunit'; +import { getMockConfig } from '../helpers/get-mock-config'; + +type TestContext = Context & { + cognito: CognitoService; +}; + +module('Acceptance | logout', function (hooks) { + setupApplicationTest(hooks); + + mockCognitoAuthenticated(hooks, { includeAssertSteps: true }); + + hooks.beforeEach(function (this: TestContext) { + let cognito = this.owner.lookup('service:cognito') as CognitoService; + this.cognito = cognito; + }); + + test('it allows to logout', async function (this: TestContext, assert) { + let { cognito } = this; + + await visit('/'); + + assert.strictEqual(currentRouteName(), 'index', 'user is on index page'); + assert.ok(cognito.isAuthenticated, 'user is authenticated'); + assert.strictEqual( + cognito.session?.jwtToken, + getMockConfig().mockJwtToken, + 'correct jwtToken is set on service' + ); + + await click('[data-test-logout]'); + + assert.notOk(cognito.isAuthenticated, 'user is not authenticated'); + assert.strictEqual(cognito.session, undefined, 'session is reset'); + assert.strictEqual(cognito.user, undefined, 'user is reset'); + assert.true(!!cognito.unauthenticated, 'unauthenticated is available'); + + assert.verifySteps([ + 'cognitoUser.getUserData({"bypassCache":false})', + 'cognitoUser.signOut()', + ]); + }); +}); diff --git a/tests/acceptance/remember-authentication-test.ts b/tests/acceptance/remember-authentication-test.ts index 6e58e326..749a0a0a 100644 --- a/tests/acceptance/remember-authentication-test.ts +++ b/tests/acceptance/remember-authentication-test.ts @@ -13,7 +13,6 @@ import CognitoService from 'ember-cognito-identity/services/cognito'; import { mockCognitoAuthenticated } from 'ember-cognito-identity/test-support/helpers/mock-cognito'; import { mockCognitoData } from 'ember-cognito-identity/utils/mock/cognito-data'; import { timeout } from 'ember-concurrency'; -import { taskFor } from 'ember-concurrency-ts'; import { setupApplicationTest } from 'ember-qunit'; import { module, test } from 'qunit'; import { getMockConfig } from '../helpers/get-mock-config'; @@ -50,7 +49,7 @@ module('Acceptance | remember-authentication', function (hooks) { assert.strictEqual(currentRouteName(), 'index', 'user is on index page'); assert.ok(cognito.isAuthenticated, 'user is authenticated'); assert.strictEqual( - cognito.cognitoData?.jwtToken, + cognito.session?.jwtToken, getMockConfig().mockJwtToken, 'correct jwtToken is set on service' ); @@ -66,17 +65,17 @@ module('Acceptance | remember-authentication', function (hooks) { assert.strictEqual(currentRouteName(), 'index', 'user is on index page'); assert.ok(cognito.isAuthenticated, 'user is authenticated'); assert.strictEqual( - cognito.cognitoData?.jwtToken, + cognito.session?.jwtToken, getMockConfig().mockJwtToken, 'correct jwtToken is set on service' ); // setExpireIn is only available on MockCognitoUserSession // @ts-ignore - cognito.cognitoData?.cognitoUserSession.setExpireIn(2 + 15 * 60); + cognito.session?.cognitoUserSession.setExpireIn(2 + 15 * 60); assert.step('now run auto-refresh'); - taskFor(cognito._debouncedRefreshAccessToken).perform(); + cognito.session?.enableAutoRefresh(); await timeout(1400); @@ -85,7 +84,7 @@ module('Acceptance | remember-authentication', function (hooks) { await timeout(1000); assert.strictEqual( - cognito.cognitoData?.jwtToken, + cognito.session?.jwtToken, `${getMockConfig().mockJwtToken}-REFRESHED`, 'correct jwtToken is updated on service' ); @@ -96,7 +95,6 @@ module('Acceptance | remember-authentication', function (hooks) { 'still not refreshed', 'cognitoUser.refreshSession()', 'cognitoUser.getSession()', - 'cognitoUser.getUserData({"bypassCache":false})', ]); }); }); @@ -120,11 +118,13 @@ module('Acceptance | remember-authentication', function (hooks) { test('it handles an API error when trying to refresh token', async function (this: TestContext, assert) { let cognito = this.owner.lookup('service:cognito') as CognitoService; - cognito.cognitoData = mockCognitoData({ + let data = mockCognitoData({ assert, })!; - cognito.cognitoData.cognitoUser.refreshSession = ( + cognito.setupSession(data); + + data.session.cognitoUser.refreshSession = ( _: any, callback: (error: null | AmazonCognitoIdentityJsError) => void ) => { @@ -132,7 +132,7 @@ module('Acceptance | remember-authentication', function (hooks) { // setExpireIn is only available on MockCognitoUserSession // @ts-ignore - cognito.cognitoData!.cognitoUserSession.setExpireIn(20 * 60); + data.session!.cognitoUserSession.setExpireIn(20 * 60); callback({ code: 'UserNotFoundException', @@ -146,24 +146,24 @@ module('Acceptance | remember-authentication', function (hooks) { assert.strictEqual(currentRouteName(), 'index', 'user is on index page'); assert.ok(cognito.isAuthenticated, 'user is authenticated'); assert.strictEqual( - cognito.cognitoData?.jwtToken, + cognito.session?.jwtToken, getMockConfig().mockJwtToken, 'correct jwtToken is set on service' ); // setExpireIn is only available on MockCognitoUserSession // @ts-ignore - cognito.cognitoData.cognitoUserSession.setExpireIn(1 + 15 * 60); + cognito.session.cognitoUserSession.setExpireIn(1 + 15 * 60); assert.step('now run auto-refresh'); - taskFor(cognito._debouncedRefreshAccessToken).perform(); + cognito.session?.enableAutoRefresh(); await timeout(1400); assert.strictEqual(currentRouteName(), 'index', 'user is on index page'); assert.ok(cognito.isAuthenticated, 'user is authenticated'); assert.strictEqual( - cognito.cognitoData?.jwtToken, + cognito.session?.jwtToken, getMockConfig().mockJwtToken, 'correct jwtToken is set on service' ); diff --git a/tests/acceptance/reset-password-test.ts b/tests/acceptance/reset-password-test.ts index 247c7e58..2644b8b1 100644 --- a/tests/acceptance/reset-password-test.ts +++ b/tests/acceptance/reset-password-test.ts @@ -55,7 +55,7 @@ module('Acceptance | reset-password', function (hooks) { assert.ok(cognito.isAuthenticated, 'user is authenticated now'); assert.strictEqual( - cognito.cognitoData && cognito.cognitoData.jwtToken, + cognito.session?.jwtToken, JWT_TOKEN, 'correct jwtToken is set on service' ); @@ -63,6 +63,8 @@ module('Acceptance | reset-password', function (hooks) { assert.verifySteps([ 'cognitoUser.forgotPassword()', 'cognitoUser.confirmPassword(123456, test1234)', + 'cognitoUser.authenticateUser(jane@example.com, test1234)', + 'cognitoUser.getUserData({"bypassCache":false})', ]); }); @@ -92,12 +94,16 @@ module('Acceptance | reset-password', function (hooks) { assert.ok(cognito.isAuthenticated, 'user is authenticated now'); assert.strictEqual( - cognito.cognitoData && cognito.cognitoData.jwtToken, + cognito.session?.jwtToken, JWT_TOKEN, 'correct jwtToken is set on service' ); - assert.verifySteps(['cognitoUser.confirmPassword(123456, test1234)']); + assert.verifySteps([ + 'cognitoUser.confirmPassword(123456, test1234)', + 'cognitoUser.authenticateUser(jane@example.com, test1234)', + 'cognitoUser.getUserData({"bypassCache":false})', + ]); }); test('it allows to resend a code & reset the password', async function (this: TestContext, assert) { @@ -131,7 +137,7 @@ module('Acceptance | reset-password', function (hooks) { assert.ok(cognito.isAuthenticated, 'user is authenticated now'); assert.strictEqual( - cognito.cognitoData && cognito.cognitoData.jwtToken, + cognito.session?.jwtToken, JWT_TOKEN, 'correct jwtToken is set on service' ); @@ -139,6 +145,8 @@ module('Acceptance | reset-password', function (hooks) { assert.verifySteps([ 'cognitoUser.forgotPassword()', 'cognitoUser.confirmPassword(123456, test1234)', + 'cognitoUser.authenticateUser(jane@example.com, test1234)', + 'cognitoUser.getUserData({"bypassCache":false})', ]); }); }); diff --git a/tests/dummy/app/components/cognito-login-form.ts b/tests/dummy/app/components/cognito-login-form.ts index 15f928cf..fd75316f 100644 --- a/tests/dummy/app/components/cognito-login-form.ts +++ b/tests/dummy/app/components/cognito-login-form.ts @@ -64,7 +64,7 @@ export default class CognitoLoginForm extends Component { try { let newAttributes = this._getNewPasswordAttributes({ username }); - yield cognito.setNewPassword( + yield cognito.unauthenticated.setInitialPassword( { username, password, newPassword }, newAttributes ); diff --git a/tests/dummy/app/components/cognito-reset-password-form.ts b/tests/dummy/app/components/cognito-reset-password-form.ts index 37aa2c30..2812abdc 100644 --- a/tests/dummy/app/components/cognito-reset-password-form.ts +++ b/tests/dummy/app/components/cognito-reset-password-form.ts @@ -48,7 +48,7 @@ export default class CognitoResetPasswordForm extends Component { this.error = null; try { - yield cognito.triggerResetPasswordMail({ username }); + yield cognito.unauthenticated.triggerResetPasswordMail({ username }); } catch (error) { this.error = error; return; @@ -77,7 +77,7 @@ export default class CognitoResetPasswordForm extends Component { } try { - yield cognito.updateResetPassword({ + yield cognito.unauthenticated.updateResetPassword({ username, code: verificationCode, newPassword: password, diff --git a/tests/dummy/app/components/mfa-setup.ts b/tests/dummy/app/components/mfa-setup.ts index 9e57d2fb..9294a78d 100644 --- a/tests/dummy/app/components/mfa-setup.ts +++ b/tests/dummy/app/components/mfa-setup.ts @@ -16,22 +16,22 @@ export default class MfaSetup extends Component { @tracked error?: string; get mfa(): CognitoUserMfa { - return this.cognito.cognitoData!.mfa; + return this.cognito.user!.mfa; } get qrCodeData(): | { secret: string; user: string; label: string } | undefined { - let { cognitoData } = this.cognito; + let { user } = this.cognito; let { secret } = this; - if (!secret || !cognitoData) { + if (!secret || !user) { return undefined; } return { secret, - user: cognitoData.cognitoUser.getUsername(), + user: user.cognitoUser.getUsername(), label: 'Fabscale', }; } diff --git a/tests/dummy/app/index/controller.ts b/tests/dummy/app/index/controller.ts index 398011be..aaabc66a 100644 --- a/tests/dummy/app/index/controller.ts +++ b/tests/dummy/app/index/controller.ts @@ -8,8 +8,8 @@ export default class IndexController extends Controller { @service declare cognito: CognitoService; @service declare router: RouterService; - get jwtToken(): string { - return this.cognito.cognitoData!.jwtToken; + get jwtToken(): string | undefined { + return this.cognito.session?.jwtToken; } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types diff --git a/tests/dummy/app/index/template.hbs b/tests/dummy/app/index/template.hbs index e6beeee6..4365bd7d 100644 --- a/tests/dummy/app/index/template.hbs +++ b/tests/dummy/app/index/template.hbs @@ -1,11 +1,12 @@
- Token: {{this.jwtToken}} + Token: + {{this.jwtToken}}
- \ No newline at end of file diff --git a/tests/dummy/app/instance-initializers/mock-cognito.ts b/tests/dummy/app/instance-initializers/mock-cognito.ts index fe528577..800c4509 100644 --- a/tests/dummy/app/instance-initializers/mock-cognito.ts +++ b/tests/dummy/app/instance-initializers/mock-cognito.ts @@ -4,10 +4,11 @@ import CognitoService from 'ember-cognito-identity/services/cognito'; import { mockCognitoData } from 'ember-cognito-identity/utils/mock/cognito-data'; export function initialize(appInstance: ApplicationInstance): void { - let cognitoData = mockCognitoData(); - if (cognitoData && !isTesting()) { + let data = mockCognitoData(); + if (data && !isTesting()) { let cognito = appInstance.lookup('service:cognito') as CognitoService; - cognito.cognitoData = cognitoData; + + cognito.setupSession(data); } } diff --git a/tests/unit/services/cognito-test.ts b/tests/unit/services/cognito-test.ts index dbbfb3d9..ac2104d4 100644 --- a/tests/unit/services/cognito-test.ts +++ b/tests/unit/services/cognito-test.ts @@ -36,142 +36,147 @@ module('Unit | Service | cognito', function (hooks) { assert.verifySteps(['restoreAndLoad rejects']); }); - module('updateAttributes', function (hooks) { - mockCognitoAuthenticated(hooks, { includeAssertSteps: true }); - - hooks.beforeEach(async function () { - setupOnerror((error) => { - // ignore cognito errors, as they are handled in the UI - if (error instanceof CognitoError) { - return; - } - - throw error; - }); - }); - - hooks.afterEach(function () { - resetOnerror(); - }); - - test('it works', async function (this: TestContext, assert) { - let { cognito } = this; - - let response = await cognito.updateAttributes({ - name: 'John W.', - age: '52', + module('user', function () { + module('updateAttributes', function (hooks) { + mockCognitoAuthenticated(hooks, { includeAssertSteps: true }); + + hooks.beforeEach(async function () { + setupOnerror((error) => { + // ignore cognito errors, as they are handled in the UI + if (error instanceof CognitoError) { + return; + } + + throw error; + }); }); - /* eslint-disable camelcase */ - assert.deepEqual(response, { - email: 'jane@example.com', - email_verified: 'true', - sub: 'aaa-bbb-ccc', + hooks.afterEach(function () { + resetOnerror(); }); - /* eslint-enable camelcase */ - assert.deepEqual(cognito.cognitoData!.userAttributes, response); - - assert.verifySteps([ - 'cognitoUser.updateAttributes()', - 'cognitoUser.getUserData({"bypassCache":false})', - ]); - }); - - test('it handles a server error when updating the attributes', async function (this: TestContext, assert) { - let { cognito } = this; - cognito.cognitoData!.cognitoUser.updateAttributes = ( - _: any, - callback: (error: AmazonCognitoIdentityJsError) => void - ) => { - assert.step(`cognitoUser.updateAttributes()`); + test('it works', async function (this: TestContext, assert) { + let { cognito } = this; - callback({ - code: 'NotAuthorizedException', - name: 'NotAuthorizedException', - message: 'A client attempted to write unauthorized attribute', - }); - }; - - try { - await cognito.updateAttributes({ + let response = await cognito.user!.updateAttributes({ name: 'John W.', age: '52', }); - } catch (error) { - assert.step('error occurred'); - assert.strictEqual( - (error as any).message, - 'A client attempted to write unauthorized attribute' - ); - } - - assert.verifySteps(['cognitoUser.updateAttributes()', 'error occurred']); - }); - }); - module('updatePassword', function (hooks) { - mockCognitoAuthenticated(hooks, { includeAssertSteps: true }); + /* eslint-disable camelcase */ + assert.deepEqual(response, { + email: 'jane@example.com', + email_verified: 'true', + sub: 'aaa-bbb-ccc', + }); + /* eslint-enable camelcase */ + assert.deepEqual(cognito.user!.userAttributes, response); + + assert.verifySteps([ + 'cognitoUser.updateAttributes()', + 'cognitoUser.getUserData({"bypassCache":false})', + ]); + }); - hooks.beforeEach(async function () { - setupOnerror((error) => { - // ignore cognito errors, as they are handled in the UI - if (error instanceof CognitoError) { - return; + test('it handles a server error when updating the attributes', async function (this: TestContext, assert) { + let { cognito } = this; + + cognito.user!.cognitoUser.updateAttributes = ( + _: any, + callback: (error: AmazonCognitoIdentityJsError) => void + ) => { + assert.step(`cognitoUser.updateAttributes()`); + + callback({ + code: 'NotAuthorizedException', + name: 'NotAuthorizedException', + message: 'A client attempted to write unauthorized attribute', + }); + }; + + try { + await cognito.user!.updateAttributes({ + name: 'John W.', + age: '52', + }); + } catch (error) { + assert.step('error occurred'); + assert.strictEqual( + (error as any).message, + 'A client attempted to write unauthorized attribute' + ); } - throw error; + assert.verifySteps([ + 'cognitoUser.updateAttributes()', + 'error occurred', + ]); }); }); - hooks.afterEach(function () { - resetOnerror(); - }); + module('updatePassword', function (hooks) { + mockCognitoAuthenticated(hooks, { includeAssertSteps: true }); - test('it works', async function (this: TestContext, assert) { - let { cognito } = this; + hooks.beforeEach(async function () { + setupOnerror((error) => { + // ignore cognito errors, as they are handled in the UI + if (error instanceof CognitoError) { + return; + } - await cognito.updatePassword({ - oldPassword: 'test1234', - newPassword: 'new-test1234', + throw error; + }); }); - assert.verifySteps([ - 'cognitoUser.changePassword(test1234, new-test1234)', - ]); - }); - - test('it handles a server error when updating the password', async function (this: TestContext, assert) { - let { cognito } = this; - - cognito.cognitoData!.cognitoUser.changePassword = ( - _: any, - _2: any, - callback: (error: AmazonCognitoIdentityJsError) => void - ) => { - assert.step(`cognitoUser.changePassword()`); + hooks.afterEach(function () { + resetOnerror(); + }); - callback({ - code: 'NotAuthorizedException', - name: 'NotAuthorizedException', - message: 'Incorrect username or password.', - }); - }; + test('it works', async function (this: TestContext, assert) { + let { cognito } = this; - try { - await cognito.updatePassword({ + await cognito.user!.updatePassword({ oldPassword: 'test1234', newPassword: 'new-test1234', }); - } catch (error) { - assert.strictEqual( - (error as any).message, - 'The password you provided is incorrect.' - ); - assert.step('error occurred'); - } - - assert.verifySteps(['cognitoUser.changePassword()', 'error occurred']); + + assert.verifySteps([ + 'cognitoUser.changePassword(test1234, new-test1234)', + ]); + }); + + test('it handles a server error when updating the password', async function (this: TestContext, assert) { + let { cognito } = this; + + cognito.user!.cognitoUser.changePassword = ( + _: any, + _2: any, + callback: (error: AmazonCognitoIdentityJsError) => void + ) => { + assert.step(`cognitoUser.changePassword()`); + + callback({ + code: 'NotAuthorizedException', + name: 'NotAuthorizedException', + message: 'Incorrect username or password.', + }); + }; + + try { + await cognito.user!.updatePassword({ + oldPassword: 'test1234', + newPassword: 'new-test1234', + }); + } catch (error) { + assert.strictEqual( + (error as any).message, + 'The password you provided is incorrect.' + ); + assert.step('error occurred'); + } + + assert.verifySteps(['cognitoUser.changePassword()', 'error occurred']); + }); }); }); }); diff --git a/tests/unit/utils/get-token-refresh-wait-test.ts b/tests/unit/utils/get-token-refresh-wait-test.ts index 5ba0fabb..89288bbe 100644 --- a/tests/unit/utils/get-token-refresh-wait-test.ts +++ b/tests/unit/utils/get-token-refresh-wait-test.ts @@ -1,39 +1,29 @@ import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { getTokenRefreshWait } from 'ember-cognito-identity/utils/get-token-refresh-wait'; +import { mockCognitoUserSession } from 'ember-cognito-identity/utils/mock/cognito-user-session'; import { module, test } from 'qunit'; module('Unit | Utility | get-token-refresh-wait', function () { - test('it works with missing/empty expiration', function (assert) { - let mockSession = { - getAccessToken() { - return { - getExpiration() { - return undefined; - }, - }; - }, - } as unknown as CognitoUserSession; - - let result = getTokenRefreshWait(mockSession); - - // 45 minutes - assert.strictEqual(result, 2700000); - }); - test('it works with expiration in the future', function (assert) { let now = new Date(); - let mockSession = { - getAccessToken() { - return { - getExpiration() { - return Math.floor(+now / 1000 + 20 * 60); - }, - }; - }, - } as unknown as CognitoUserSession; + let mockSession = mockCognitoUserSession()!; + + mockSession.getAccessToken = () => { + return { + getJwtToken: () => { + return 'XXX'; + }, - let result = getTokenRefreshWait(mockSession); + getExpiration() { + return Math.floor(+now / 1000 + 20 * 60); + }, + }; + }; + + let result = getTokenRefreshWait( + mockSession as unknown as CognitoUserSession + ); // 5 minutes assert.strictEqual(result, 300000); @@ -42,17 +32,23 @@ module('Unit | Utility | get-token-refresh-wait', function () { test('it works with expiration in the past', function (assert) { let now = new Date(); - let mockSession = { - getAccessToken() { - return { - getExpiration() { - return Math.floor(+now / 1000 - 5 * 60); - }, - }; - }, - } as unknown as CognitoUserSession; + let mockSession = mockCognitoUserSession()!; + + mockSession.getAccessToken = () => { + return { + getJwtToken: () => { + return 'XXX'; + }, - let result = getTokenRefreshWait(mockSession); + getExpiration() { + return Math.floor(+now / 1000 - 5 * 60); + }, + }; + }; + + let result = getTokenRefreshWait( + mockSession as unknown as CognitoUserSession + ); // 5 minutes assert.strictEqual(result, 0); @@ -61,17 +57,23 @@ module('Unit | Utility | get-token-refresh-wait', function () { test('it works with expiration in the future, but below margin/threshold', function (assert) { let now = new Date(); - let mockSession = { - getAccessToken() { - return { - getExpiration() { - return Math.floor(+now / 1000 + 5 * 60); - }, - }; - }, - } as unknown as CognitoUserSession; - - let result = getTokenRefreshWait(mockSession); + let mockSession = mockCognitoUserSession()!; + + mockSession.getAccessToken = () => { + return { + getJwtToken: () => { + return 'XXX'; + }, + + getExpiration() { + return Math.floor(+now / 1000 + 5 * 60); + }, + }; + }; + + let result = getTokenRefreshWait( + mockSession as unknown as CognitoUserSession + ); // 5 minutes assert.strictEqual(result, 0); diff --git a/yarn.lock b/yarn.lock index 92e3e10f..38e6ed02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1691,7 +1691,7 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== -"@glimmer/component@^1.1.2": +"@glimmer/component@^1.1.0", "@glimmer/component@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@glimmer/component/-/component-1.1.2.tgz#892ec0c9f0b6b3e41c112be502fde073cf24d17c" integrity sha512-XyAsEEa4kWOPy+gIdMjJ8XlzA3qrGH55ZDv6nA16ibalCR17k74BI0CztxuRds+Rm6CtbUVgheCVlcCULuqD7A== @@ -2226,7 +2226,7 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@types/amazon-cognito-auth-js@^1.2.2": +"@types/amazon-cognito-auth-js@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@types/amazon-cognito-auth-js/-/amazon-cognito-auth-js-1.3.0.tgz#362fbd9dfc2b516c22098b90e0c7cd382eac4b70" integrity sha512-LC3khKkazRjyX0qaGUA3SyNHOxe1qXdBc86hmFCW+iZuavz5uWpFQE4RcLfsffPBibG1brlbC45n7MPl/bHPpA== @@ -2307,10 +2307,10 @@ dependencies: "@types/ember__object" "*" -"@types/ember@*", "@types/ember@~4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/ember/-/ember-4.0.0.tgz#0c29294fa0e5aa07ba6090f60243707dde8fc411" - integrity sha512-IR4o8OkFgoiRKVLRI8URvyNhEBSkjO5DXp2900/TptxOl0Retu8/tKtFaRTwkqteg2a0/6zXAA1rpFb3BbxNpA== +"@types/ember@*", "@types/ember@~4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/ember/-/ember-4.0.1.tgz#5fe3dc8219b7e0391b2c97d8db945a9e0881badc" + integrity sha512-fYCZtkGq1UqD1rWjvdY4bVQpdZvxkHqVx/B6p2M0cUdyheLNjQgwXfx3o1UU0Bk99mls5Or4ZCnW7s1yliABiw== dependencies: "@types/ember__application" "*" "@types/ember__array" "*" @@ -2332,15 +2332,17 @@ "@types/rsvp" "*" "@types/ember__application@*": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/ember__application/-/ember__application-4.0.0.tgz#a4d2fead37845550dad83bb1fd8afd52052563a7" - integrity sha512-1Atwevfyu1/vjiezPPdP4s96BxWGelEQlCJRU5ZQV9WlzVuMTuCDPumZ1lQdS4/EYycFZeod030FjE3CT9mZFA== + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/ember__application/-/ember__application-4.0.1.tgz#1bdbd4bf4a995558c5e6297856ad705f181899cc" + integrity sha512-dKyiEHpZRENCRNyS9PQOnYjrcO4QsmeTU+csmiH8B3McYBCp1AKwlnjOd6eTs3O69WDcUq2mFk6BAQM49bAF7Q== dependencies: + "@glimmer/component" "^1.1.0" "@types/ember" "*" "@types/ember-resolver" "*" "@types/ember__application" "*" "@types/ember__engine" "*" "@types/ember__object" "*" + "@types/ember__owner" "*" "@types/ember__routing" "*" "@types/ember__array@*": @@ -2353,14 +2355,13 @@ "@types/ember__object" "*" "@types/ember__component@*": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/ember__component/-/ember__component-4.0.2.tgz#4a332ab2cfe42d3de5bc7a808d9306c6a0fe5c1b" - integrity sha512-etdp17FYAy0fQAD00XvAv7RZbgnRdGHln3XKUWqpPePGm8JvCZCKEcAo1fwhEW2GnmFJAHmYsjNuU00SJsfpPQ== + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/ember__component/-/ember__component-4.0.8.tgz#09a5f954f734fcbe6c988a173f4de4fa09084470" + integrity sha512-YVGn/kpWtpZAu6I2XtS9fsZV+78/sON5NyKzK5EOUyMiCwwpbUr5XL8dTSdkHehYrsfzJikcYvqpmwbNZSJxGQ== dependencies: "@types/ember" "*" "@types/ember__component" "*" "@types/ember__object" "*" - "@types/jquery" "*" "@types/ember__controller@*": version "4.0.0" @@ -2370,22 +2371,23 @@ "@types/ember__object" "*" "@types/ember__debug@*": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/ember__debug/-/ember__debug-4.0.0.tgz#02d3d710988988e5cff094ddcd45e5364727b161" - integrity sha512-yY+U7veF8jYFJdm+IU+7Y/DoxVsyV/73UvFUdR0jCasMVL9vuGkVx0rXlqe6XAM2cWI1LwV7zKQzgqDC8kQD2g== + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/ember__debug/-/ember__debug-4.0.1.tgz#1e4a8a1045484295dddc7bd4356d0b3014b0d509" + integrity sha512-qrKk6Ujh6oev7TSB0eB7AEmQWKCt5t84k/K3hDvJXUiLU3YueN0kyt7aPoIAkVjC111A9FqDugl9n60+N5yeEw== dependencies: "@types/ember-resolver" "*" "@types/ember__debug" "*" "@types/ember__object" "*" "@types/ember__engine@*": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/ember__engine/-/ember__engine-4.0.0.tgz#e39c06d98c7a085912508e8257c48a70196c1a87" - integrity sha512-AfJHIWaBeZ+TZWJbSoUz7LK+z8uNPjMqmucz8C5u+EV2NDiaq02oGPTB4SeKInLNBMga8c5xvz0gVefZJnTBnQ== + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/ember__engine/-/ember__engine-4.0.1.tgz#ec44f405f199b80df888f4676626ba0977557d16" + integrity sha512-j6T/7raHSZoF1rUjNK3QpDAtKMOOqvQog+VhSDJn0Z5BKeIPHdNmEId25aVuRm9/2Ch2qY1Fe4an3ZorwHvLpQ== dependencies: "@types/ember-resolver" "*" "@types/ember__engine" "*" "@types/ember__object" "*" + "@types/ember__owner" "*" "@types/ember__error@*": version "4.0.0" @@ -2393,33 +2395,39 @@ integrity sha512-1WVMR65/QTqPzMWafK2vKEwGafILxRxItbWJng6eEJyKDHRvvHFCl3XzJ4dQjdFcfOlozsn0mmEYCpjKoyzMqA== "@types/ember__object@*": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/ember__object/-/ember__object-4.0.0.tgz#18ffa0747ebaceed8ea3dbc9793dd033dc47adf2" - integrity sha512-mOd53VoaHTKZiRDnZlY1aouCb6P62OK2IrBLEY4pmnpmQPIFM8Q1COceMw8bh6Xjg8xOnhae5jsi3Ux2Ucnd2A== + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/ember__object/-/ember__object-4.0.3.tgz#c8410863e5950a0bd84708ce5a7224335002870e" + integrity sha512-25vYl0SBgZ7wGem16ELJ+rtA/sNMNnbFgNzDomA33qtc7tXgbiwTlGQh1HWoN5y7yiqdfMGZcFCLVWD+t5gjfw== dependencies: "@types/ember" "*" "@types/ember__object" "*" "@types/rsvp" "*" +"@types/ember__owner@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__owner/-/ember__owner-4.0.0.tgz#2058a8fbf9636774dc79430abd355b07538c51d7" + integrity sha512-7ZotJNCkZUvJpcGHYswQlQsHyRITQ3aNOoFPi86NFxmOXEIVAGVKPHB87w8ZlMmhssG2vitCuNzuQCeDwPaokQ== + "@types/ember__polyfills@*": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/ember__polyfills/-/ember__polyfills-4.0.0.tgz#d83ae94ff2890ad47798315426d9916f39ff4ae6" integrity sha512-Yk85J18y1Ys6agoIBLdJWu6ZkWe68oaC9JPyW7BhOINVNKm89PXrR/yxdOJ1/vN1Hj7ZZQKq+4X6fz3sxebavA== "@types/ember__routing@*": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/ember__routing/-/ember__routing-4.0.2.tgz#5af1b2dd1a5491fdaccc908f8e69d39c11ed3536" - integrity sha512-ozZgAx6uRWasJmTeMYVT0/ckLmh4Yg3eSowJz78ipMTRjIkcp5wDcn/iQvPMZlAbDLAwQHE/9LOyI4JzVSSeTA== + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/ember__routing/-/ember__routing-4.0.9.tgz#2b60a7e43dd2b3976dea24fd5f5e7ee725e247a2" + integrity sha512-JoxNUp6hYvzMf9eJeXxQxkJDWPX76jUoatFEm5Us36omhUuk00H5zyehCClRg3JkeyGTmVb3UCHE2UpQp3SfhQ== dependencies: + "@types/ember" "*" "@types/ember__controller" "*" "@types/ember__object" "*" "@types/ember__routing" "*" "@types/ember__service" "*" "@types/ember__runloop@*": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/ember__runloop/-/ember__runloop-4.0.0.tgz#74079b2423240f7b6a7c8f0d6485455082e21d4b" - integrity sha512-2BArjaywglzZ0xN0GwHZpkKJQJmjXohiY8i+cHTWLv9U34uiPjGpbIm4v5ev8zSobOcA4wHY8svJ/b/MhCpf0g== + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/ember__runloop/-/ember__runloop-4.0.1.tgz#7f6e45af7dbf1158655ef3ad852852b0bf87065f" + integrity sha512-3HrsavVrdgxUkYptQUv/e9RwJG02cV9WbnJxKSvwl9ZYpeX4JbuDVucjTWk5BAvJUVtbiQLPGzLEHZ6daoCbbg== dependencies: "@types/ember" "*" "@types/ember__runloop" "*" @@ -2442,13 +2450,14 @@ integrity sha512-51bAEQecMKpDYRXMmVVfU7excrtxDJixRU7huUsAm4acBCqL2+TmMgTqZEkOQSNy6qnKUc2ktSzX28a9//C6pA== "@types/ember__test-helpers@*", "@types/ember__test-helpers@~2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@types/ember__test-helpers/-/ember__test-helpers-2.8.0.tgz#9e8fb470935b1f94ba60b2c673b720addaf36940" - integrity sha512-lAnpTPmGNuocrJiIxFP6pkMWhEeTmrqsa4yBKjaBBxDXWF1qh3CCWEr/Tj7oe7HVUyL7w53mxyPIjskvCyiEBw== + version "2.8.1" + resolved "https://registry.yarnpkg.com/@types/ember__test-helpers/-/ember__test-helpers-2.8.1.tgz#06414a77bea02bd77a5f849bc1bfd2a9cbe4d86f" + integrity sha512-UlzmA3yAynqsj5tklBEnPt3WXCj9vLtLTSY9PJDt/NWkcFsDIssYnTQUzzNUflkO+ngHVHRX3tiYuLzcwpTEmA== dependencies: "@types/ember-resolver" "*" "@types/ember__application" "*" "@types/ember__error" "*" + "@types/ember__owner" "*" "@types/ember__test-helpers" "*" "@types/htmlbars-inline-precompile" "*" @@ -2546,13 +2555,6 @@ resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== -"@types/jquery@*": - version "3.5.5" - resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.5.tgz#2c63f47c9c8d96693d272f5453602afd8338c903" - integrity sha512-6RXU9Xzpc6vxNrS6FPPapN1SxSHgQ336WC6Jj/N8q30OiaBZ00l1GBgeP7usjVZPivSkGUfL1z/WW6TX989M+w== - dependencies: - "@types/sizzle" "*" - "@types/json-schema@*": version "7.0.7" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" @@ -2665,11 +2667,6 @@ "@types/mime" "*" "@types/node" "*" -"@types/sizzle@*": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" - integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== - "@types/symlink-or-copy@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/symlink-or-copy/-/symlink-or-copy-1.2.0.tgz#4151a81b4052c80bc2becbae09f3a9ec010a9c7a" @@ -3222,10 +3219,10 @@ ajv@^8.0.0, ajv@^8.8.0: require-from-string "^2.0.2" uri-js "^4.2.2" -amazon-cognito-identity-js@^5.2.4: - version "5.2.4" - resolved "https://registry.yarnpkg.com/amazon-cognito-identity-js/-/amazon-cognito-identity-js-5.2.4.tgz#9b0168a59bd1b933e89b560840f1c50902b87269" - integrity sha512-CTRG5LKBfMYmNrqAU0l7X0SA4OOXsPFdHmkW0Ky3NOZZo767W5GZecm5uPz3YyAVde1OHaUyLXYzhXEmnn+VcQ== +amazon-cognito-identity-js@^5.2.10: + version "5.2.10" + resolved "https://registry.yarnpkg.com/amazon-cognito-identity-js/-/amazon-cognito-identity-js-5.2.10.tgz#c70dc355c22215a9b7898b78105b5748d4a52649" + integrity sha512-40ZFQeMjNRymyE7X7XwW78VHftPMFt3di0GsL2/lFfWOXJJWrJGuBFWBYvz/aFMpl/omZHuwwqfwR4bZ+xF5Wg== dependencies: buffer "4.9.2" crypto-js "^4.1.1" @@ -6017,12 +6014,32 @@ ember-auto-import@^2.4.1, ember-auto-import@^2.4.2: typescript-memoize "^1.0.0-alpha.3" walk-sync "^3.0.0" +ember-cache-primitive-polyfill@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ember-cache-primitive-polyfill/-/ember-cache-primitive-polyfill-1.0.1.tgz#a27075443bd87e5af286c1cd8a7df24e3b9f6715" + integrity sha512-hSPcvIKarA8wad2/b6jDd/eU+OtKmi6uP+iYQbzi5TQpjsqV6b4QdRqrLk7ClSRRKBAtdTuutx+m+X+WlEd2lw== + dependencies: + ember-cli-babel "^7.22.1" + ember-cli-version-checker "^5.1.1" + ember-compatibility-helpers "^1.2.1" + silent-error "^1.1.1" + +ember-cached-decorator-polyfill@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/ember-cached-decorator-polyfill/-/ember-cached-decorator-polyfill-0.1.4.tgz#f1e2c65cc78d0d9c4ac0e047e643af477eb85ace" + integrity sha512-JOK7kBCWsTVCzmCefK4nr9BACDJk0owt9oIUaVt6Q0UtQ4XeAHmoK5kQ/YtDcxQF1ZevHQFdGhsTR3JLaHNJgA== + dependencies: + "@glimmer/tracking" "^1.0.4" + ember-cache-primitive-polyfill "^1.0.1" + ember-cli-babel "^7.21.0" + ember-cli-babel-plugin-helpers "^1.1.1" + ember-cli-babel-plugin-helpers@^1.0.0, ember-cli-babel-plugin-helpers@^1.1.0, ember-cli-babel-plugin-helpers@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ember-cli-babel-plugin-helpers/-/ember-cli-babel-plugin-helpers-1.1.1.tgz#5016b80cdef37036c4282eef2d863e1d73576879" integrity sha512-sKvOiPNHr5F/60NLd7SFzMpYPte/nnGkq/tMIfXejfKHIhaiIkYFqX8Z9UFTKWLLn+V7NOaby6niNPZUdvKCRw== -ember-cli-babel@^7.0.0, ember-cli-babel@^7.1.2, ember-cli-babel@^7.13.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.19.0, ember-cli-babel@^7.22.1, ember-cli-babel@^7.23.0, ember-cli-babel@^7.23.1, ember-cli-babel@^7.26.11, ember-cli-babel@^7.26.6, ember-cli-babel@^7.7.3: +ember-cli-babel@^7.0.0, ember-cli-babel@^7.1.2, ember-cli-babel@^7.13.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.19.0, ember-cli-babel@^7.21.0, ember-cli-babel@^7.22.1, ember-cli-babel@^7.23.0, ember-cli-babel@^7.23.1, ember-cli-babel@^7.26.11, ember-cli-babel@^7.26.6, ember-cli-babel@^7.7.3: version "7.26.11" resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.26.11.tgz#50da0fe4dcd99aada499843940fec75076249a9f" integrity sha512-JJYeYjiz/JTn34q7F5DSOjkkZqy8qwFOOxXfE6pe9yEJqWGu4qErKxlz8I22JoVEQ/aBUO+OcKTpmctvykM9YA== @@ -6099,9 +6116,9 @@ ember-cli-get-component-path-option@^1.0.0: integrity sha1-DXtZVVni+QUKvtgE8djv8bCLx3E= ember-cli-htmlbars@^4.3.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-4.4.1.tgz#838ca42071bb38aead41204c87340c2fba18ebb5" - integrity sha512-ESXk6lJDK4ua71b8/d8FFK9qKd4Bfdi0hNyNSJ/Z6g8ZgO2SwZ7NMHfzgMRgqD+E2n2TGXA6xk0qLTDxbqq51w== + version "4.5.0" + resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-4.5.0.tgz#d299e4f7eba6f30dc723ee086906cc550beb252e" + integrity sha512-bYJpK1pqFu9AadDAGTw05g2LMNzY8xTCIqQm7dMJmKEoUpLRFbPf4SfHXrktzDh7Q5iggl6Skzf1M0bPlIxARw== dependencies: "@ember/edition-utils" "^1.2.0" babel-plugin-htmlbars-inline-precompile "^3.2.0" @@ -6437,7 +6454,7 @@ ember-cli@~4.6.0: workerpool "^6.2.1" yam "^1.0.0" -ember-compatibility-helpers@^1.1.2, ember-compatibility-helpers@^1.2.0, ember-compatibility-helpers@^1.2.1, ember-compatibility-helpers@^1.2.5: +ember-compatibility-helpers@^1.1.2, ember-compatibility-helpers@^1.2.1, ember-compatibility-helpers@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/ember-compatibility-helpers/-/ember-compatibility-helpers-1.2.5.tgz#b8363b1d5b8725afa9a4fe2b2986ac28626c6f23" integrity sha512-7cddkQQp8Rs2Mqrj0xqZ0uO7eC9tBCKyZNcP2iE1RxQqOGPv8fiPkj1TUeidUB/Qe80lstoVXWMEuqqhW7Yy9A== @@ -6447,7 +6464,18 @@ ember-compatibility-helpers@^1.1.2, ember-compatibility-helpers@^1.2.0, ember-co fs-extra "^9.1.0" semver "^5.4.1" -ember-concurrency-ts@^0.3.1: +ember-compatibility-helpers@^1.2.0: + version "1.2.6" + resolved "https://registry.yarnpkg.com/ember-compatibility-helpers/-/ember-compatibility-helpers-1.2.6.tgz#603579ab2fb14be567ef944da3fc2d355f779cd8" + integrity sha512-2UBUa5SAuPg8/kRVaiOfTwlXdeVweal1zdNPibwItrhR0IvPrXpaqwJDlEZnWKEoB+h33V0JIfiWleSG6hGkkA== + dependencies: + babel-plugin-debug-macros "^0.2.0" + ember-cli-version-checker "^5.1.1" + find-up "^5.0.0" + fs-extra "^9.1.0" + semver "^5.4.1" + +ember-concurrency-ts@~0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/ember-concurrency-ts/-/ember-concurrency-ts-0.3.1.tgz#0b3b524a6a96c8ab749b20e1d4da00569773567a" integrity sha512-lE9uqPgK1Y9PN/0BJ5zE2a+h95izRCn6FCyt7qVV3012TlblTynsBaoUuAbN1T3KfzFsrJaXwsxzRbDjEde2Sw== @@ -6455,10 +6483,10 @@ ember-concurrency-ts@^0.3.1: ember-cli-babel "^7.19.0" ember-cli-htmlbars "^4.3.1" -ember-concurrency@^2.0.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/ember-concurrency/-/ember-concurrency-2.2.0.tgz#0acfb8ca855e0fdfa4c543be150028299a89ba32" - integrity sha512-Ns1MH6t08oJqfeWQ4EMxyf6bLsXM87SbUPwGNUsg7idpikvkGr1PWqtvU6qxDTv1mA4Vrwdhv0I9yxN0ShH9Bg== +ember-concurrency@~2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ember-concurrency/-/ember-concurrency-2.2.1.tgz#4ed2e167036d00f7142312bc93c16f13ed9a259c" + integrity sha512-a4283Yq+jimxqoD5YaxQu7cXePHKqkNQfsT4fs0nYTz5PYbUd6wzUtelp6k8R1JTNPwDdxyVvUgu7yYoC8Sk5A== dependencies: "@glimmer/tracking" "^1.0.4" ember-cli-babel "^7.26.6" @@ -7912,19 +7940,7 @@ fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-merger@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/fs-merger/-/fs-merger-3.1.0.tgz#f30f74f6c70b2ff7333ec074f3d2f22298152f3b" - integrity sha512-RZ9JtqugaE8Rkt7idO5NSwcxEGSDZpLmVFjtVQUm3f+bWun7JAU6fKyU6ZJUeUnKdJwGx8uaro+K4QQfOR7vpA== - dependencies: - broccoli-node-api "^1.7.0" - broccoli-node-info "^2.1.0" - fs-extra "^8.0.1" - fs-tree-diff "^2.0.1" - rimraf "^2.6.3" - walk-sync "^2.0.2" - -fs-merger@^3.2.1: +fs-merger@^3.0.1, fs-merger@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/fs-merger/-/fs-merger-3.2.1.tgz#a225b11ae530426138294b8fbb19e82e3d4e0b3b" integrity sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==