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.
- Ember.js v3.24 or above
- Ember CLI v3.24 or above
- Node.js v14 or above
- Native promise support required
ember install ember-cognito-identity
And add this to your config/environment.js
:
cognito: {
userPoolId: process.env.COGNITO_USER_POOL_ID,
clientId: process.env.COGNITO_CLIENT_ID
}
You can configure these e.g. via an .env file, for example via ember-cli-dotenv.
This addon provides a cognito
service with some methods to be used to work with AWS Cognito.
Generally, your should call cognito.restoreAndLoad()
in your application route.
This will try to fetch an active user session from local storage and refresh the token, if necessary.
Once this is done, you will know if a user is logged in or not.
This is how this could look in the application route:
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ApplicationRoute extends Route {
@service cognito;
async beforeModel() {
try {
await this.cognito.restoreAndLoad();
} catch (error) {
// go to login...
}
}
}
After logging in (see below) you can access the JTW token like this:
let token = this.cognito.cognitoData.jwtToken;
Here is a summary of the most important available methods - all methods return a promise:
cognito.restoreAndLoad();
cognito.authenticate({ username, password });
cognito.authenticateUser({ username, password });
cognito.logout();
cognito.invalidateAccessTokens();
cognito.triggerResetPasswordMail({ username });
cognito.updateResetPassword({ username, code, newPassword });
cognito.setNewPassword({ username, password, newPassword });
cognito.updatePassword({ oldPassword, newPassword });
cognito.updateAttributes(attributeMap);
The cognito
service provides promise-ified methods to work with AWS Cognito.
Will be true if a user is currently logged in.
This means that you can safely access cognitoData
and work with it.
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
.
This is an object that looks like this:
let cognitoData = {
cognitoUser: CognitoUser,
cognitoUserSession: CognitoUserSession,
jwtToken: 'xxxxx',
userAttributes: { Email: '...' },
getAccessToken: () => CognitoAccessToken,
getIdToken: () => CognitoIdToken,
mfa: {
enable: () => {},
disable: () => {},
isEnabled: () => {},
setupDevice: () => {},
verifyDevice: (code) => {},
},
};
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!
Try to login with the given username & password. Will reject with an Error, or resolve if successfull.
If you want to use a specific authentication flow type, you can specify it in your config/environment.js
like this:
cognito: {
userPoolId: 'XXX',
clientId: 'YYY',
authenticationFlowType: 'USER_PASSWORD_AUTH'
}
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.
Log out the user from the current device.
Logout & invalidate all issues access tokens (also on other devices).
In contrast, logout()
does not revoke access tokens, it only removes them locally.
Returns a promise.
Trigger an email to get a verification code to reset the password.
Returns a promise.
Set a new password for a user.
Returns a promise.
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.
Update the password of the currently logged in user.
Returns a promise.
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.
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).
You will need to enable TOTP-based MFA in your Cognito user pool first.
Using MFA in your app requires changes in two places: The # process, and allowing users to opt into MFA.
A user can opt into MFA for their own account. For this, you can use the mfa
object on the cognitoData
object:
let { mfa } = this.cognito.cognitoData;
// Available methods
await mfa.enable();
await mfa.disable();
await mfa.isEnabled();
await mfa.setupDevice();
await mfa.verifyDevice(token);
To do so requires the following steps:
let secret = await mfa.setupDevice();
The secret is the code needed to set up your TOTP app, e.g. Google or Microsoft Authenticator. Users can either enter this code manually (which is pretty tedious as it is a very long code), or most apps also allow to scan a QR code.
You can use the included util to generate a fitting URL for your QR code like this:
import { generateMfaQrCodeUrl } from 'ember-cognito-identity/utils/mfa-qr-code';
let url = generateMfaQrCodeUrl({
user: 'my-email@test.com',
label: 'My App Name',
secret,
});
And generate a QR code with any QR code library, e.g. using qrcode & ember-auto-import:
import QRCode from 'qrcode';
QRCode.toCanvas(canvasElement, url);
After users scan this QR code, they will have your app setup in their Authenticator app. Finally, users have to enter a generated code from the Authenticator back into your app:
// token is the 6-digit code from the Authenticator
await mfa.verifyDevice(token);
// if that was successfull, enable MFA for this user
await mfa.enable();
After it is setup, you can always disable MFA again:
await mfa.disable();
Note that Cognito does not provide a MFA recovery process. If a user looses access to their MFA device, an administrator will have to reset MFA for them.
When a user signs in with MFA, authenticate()
will throw a MfaCodeRequiredError
error that you have to handle, e.g. like this:
import { MfaCodeRequiredError } from 'ember-cognito-identity/errors/cognito';
try {
cognito.authenticate({ username, password });
} catch (error) {
if (error instanceof MfaCodeRequiredError) {
this.mustEnterMfaCode = true;
return;
}
throw error;
}
You'll need to prompt the user to enter the 6-digit code, and then call mfaCompleteAuthentication
:
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).
You can find example components in the dummy app to see how a concrete implementation could look like.
You might want to mock Cognito either for testing or for development/demoing. For these cases, this addon provides helpful utilities.
In order for the Cognito mocking to work, you will need to enable mocks.
You can do that in your ember-cli-build.js
:
let app = new EmberApp(defaults, {
'@embroider/macros': {
setConfig: {
'ember-cognito-identity': {
// If this is set to true, mock classes will be included in the build & used for all processes
enableMocks: true,
// Optional overwrite these defaults to your liking
mockUsername: 'jane@example.com',
mockPassword: 'test1234',
mockCode: 123456,
mockJwtToken: 'TEST-ACCESS-TOKEN-AUTO',
},
},
},
});
For example, if you want to use mocks only when testing, you could configure it like this:
let env = process.env.EMBER_ENV || 'development';
let app = new EmberApp(defaults, {
'@embroider/macros': {
setConfig: {
'ember-cognito-identity': {
enableMocks: env === 'test',
},
},
},
});
When mocks are enabled, the cognito service will not hit any API but will handle all processes locally by mocked classes.
You will be able to normally do all processes, like signing in, resetting your password, etc.
The mocks will validate all input based on the configuration. The defaults are:
- mockUsername:
'jane@example.com'
, - mockPassword:
'test1234'
, - mockCode:
123456
, - mockJwtToken:
'TEST-ACCESS-TOKEN-AUTO'
,
This means, if you try to # with a different username or password you will get an error.
This can be used to test various states.
The mockCode
is used for both MFA authorization as well as for password resetting.
There are two special cases that you can manually opt-in to, e.g. for testing purposes: Requiring an MFA code or a new password.
To trigger these processes, you can do the following:
import { MOCK_COGNITO_CONFIG } from 'ember-cognito-identity/utils/mock/cognito-user';
test('it works with MFA code required', function (assert) {
MOCK_COGNITO_CONFIG.mustEnterMfaCode = true;
// test it
// make sure to reset it afterwards
MOCK_COGNITO_CONFIG.mustEnterMfaCode = false;
});
test('it works with new password required', function (assert) {
MOCK_COGNITO_CONFIG.mustEnterNewPassword = true;
// test it
// make sure to reset it afterwards
MOCK_COGNITO_CONFIG.mustEnterNewPassword = false;
});
You can generate mocked cognitoData with the provided utils:
import ApplicationInstance from '@ember/application/instance';
import { isTesting } from '@embroider/macros';
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) {
let cognito = appInstance.lookup('service:cognito');
cognito.cognitoData = cognitoData;
}
}
export default {
initialize,
};
Note that mockCognitoData()
(and all other utils it uses under the hood) will return undefined
if mocks are not enabled.
In that case, all the mocking code is stripped out and the utils remain as empty wrappers only.
The above example would automatically sign a user in with a simulated user when running the app with mocks enabled.
You can also provide some configuration to mockCognitoData
:
let cognitoData = mockCognitoData({
username = 'my-custom-user@test.com',
// To test states with enabled or disabled MFA
mfaEnabled = false,
});
You can use the provided test helpers for testing.
Note that all async methods on the Cognito service will automatically register test waiters,
so calling settled()
will wait for them to complete.
import { mockCognitoAuthenticated } from 'ember-cognito-identity/test-support/helpers/mock-cognito';
module('my-module', function (hooks) {
mockCognitoAuthenticated(hooks);
});
You can optionally also provide some additional configuration:
mockCognitoAuthenticated(hooks, {
// If this is true, it will include assert.step() invocations for all cognito steps
// This can be useful to test that the correct cognito stuff is happening behind the scenes
includeAssertSteps = true,
// If you need your user to be signed in with a special username
username = 'my-custom-user@test.com',
});
This uses mockCognitoData
under the hood, and sets everything up correctly.
Alternatively, you can also use mockCognitoData
to build your own state, like this:
import { mockCognitoData } from 'ember-cognito-identity/utils/mock/cognito-data';
test('test helper correctly mocks a cognito session', async function (assert) {
let cognito = this.owner.lookup('service:cognito');
let cognitoData = mockCognitoData({
username = 'my-custom-user@test.com',
mfaEnabled = false,
// If you pass in `assert`, it will include assert.step() invocations for all cognito steps
assert,
});
cognito.cognitoData = cognitoData;
});
See the Contributing guide for details.
This project is licensed under the MIT License.