├── gifs ├── demo.png ├── backup-min.gif └── restore-min.gif ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── tsconfig.json ├── LICENSE ├── package.json ├── src ├── cli │ ├── cli.ts │ ├── args.ts │ └── options.ts └── index.ts ├── .all-contributorsrc └── README.md /gifs/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahulpsd18/cognito-backup-restore/HEAD/gifs/demo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # open_collective: rahulpsd18 2 | custom: ['https://www.paypal.me/rahulpsd18'] 3 | -------------------------------------------------------------------------------- /gifs/backup-min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahulpsd18/cognito-backup-restore/HEAD/gifs/backup-min.gif -------------------------------------------------------------------------------- /gifs/restore-min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahulpsd18/cognito-backup-restore/HEAD/gifs/restore-min.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules 3 | .vscode 4 | .DS_Store 5 | 6 | npm-shrinkwrap.json 7 | package-lock.json 8 | yarn.lock -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | gifs/ 3 | build/cli/*.d.ts 4 | 5 | .vscode 6 | .DS_Store 7 | 8 | npm-shrinkwrap.json 9 | package-lock.json 10 | yarn.lock 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "lib": ["es6", "dom", "esnext.asynciterable", "es7"], 6 | "moduleResolution": "node", 7 | "rootDir": "./src/", 8 | "outDir": "build", 9 | "noImplicitAny": true, 10 | "declaration": true, 11 | // "noUnusedLocals": true, 12 | "noImplicitThis": true, 13 | "strictNullChecks": true, 14 | "noImplicitReturns": true, 15 | "preserveConstEnums": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "suppressImplicitAnyIndexErrors": true 18 | }, 19 | "exclude": ["node_modules", "build"] 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018-present Risabh Kumar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cognito-backup-restore", 3 | "version": "1.3.2", 4 | "description": "AIO Tool for backing up and restoring AWS Cognito User Pools", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "clean": "rm -rf ./node_modules && rm package-lock.json && echo Cleanup done.", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "start": "ts-node src/cli/cli.ts" 11 | }, 12 | "keywords": [ 13 | "cognito", 14 | "cognito-user-pool", 15 | "backup", 16 | "restore", 17 | "aws", 18 | "aws-cognito", 19 | "backup-cli", 20 | "typescript", 21 | "amazon-cognito" 22 | ], 23 | "bugs": { 24 | "url": "https://github.com/rahulpsd18/cognito-backup-restore/issues" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/rahulpsd18/cognito-backup-restore.git" 29 | }, 30 | "author": { 31 | "name": "Risabh Kumar", 32 | "email": "rahulpsd18@gmail.com" 33 | }, 34 | "license": "MIT", 35 | "bin": { 36 | "cbr": "./build/cli/cli.js", 37 | "cognito-backup-restore": "./build/cli/cli.js" 38 | }, 39 | "files": [ 40 | "build" 41 | ], 42 | "devDependencies": { 43 | "@types/inquirer": "^0.0.42", 44 | "@types/ora": "^1.3.4", 45 | "@types/yargs": "^11.0.0", 46 | "ts-node": "^6.1.2", 47 | "typescript": "3.7.4" 48 | }, 49 | "dependencies": { 50 | "JSONStream": "^1.3.3", 51 | "aws-sdk": "^2.343.0", 52 | "bottleneck": "^2.5.0", 53 | "chalk": "^2.4.1", 54 | "delay": "4.3.0", 55 | "fuzzy": "^0.1.3", 56 | "inquirer": "^6.0.0", 57 | "inquirer-autocomplete-prompt": "^0.12.2", 58 | "inquirer-file-path": "^1.0.1", 59 | "inquirer-select-directory": "^1.2.0", 60 | "ora": "^2.1.0", 61 | "yargs": "^11.0.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/cli/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as AWS from 'aws-sdk'; 4 | import * as ora from 'ora'; 5 | 6 | import chalk from 'chalk'; 7 | import { backupUsers, restoreUsers } from '../index'; 8 | import { options } from './options'; 9 | 10 | const red = chalk.red; 11 | const green = chalk.green; 12 | const orange = chalk.keyword('orange'); 13 | 14 | (async () => { 15 | let spinner = ora({ spinner: 'dots4', hideCursor: true }); 16 | try { 17 | const { mode, profile, region, key, secret, userpool, directory, file, password, passwordModulePath, delay, metadata, env} = await options; 18 | 19 | // update the config of aws-sdk based on profile/credentials passed 20 | AWS.config.update({ region }); 21 | 22 | if (profile) { 23 | AWS.config.credentials = new AWS.SharedIniFileCredentials({ profile }); 24 | } else if (key && secret) { 25 | AWS.config.credentials = new AWS.Credentials({ 26 | accessKeyId: key, secretAccessKey: secret 27 | }); 28 | } else if (env) { 29 | AWS.config.credentials = new AWS.EnvironmentCredentials('AWS'); 30 | } else if (metadata) { 31 | AWS.config.credentials = new AWS.EC2MetadataCredentials({}); 32 | } 33 | 34 | const cognitoISP = new AWS.CognitoIdentityServiceProvider(); 35 | 36 | if (mode === 'backup') { 37 | spinner = spinner.start(orange`Backing up userpool`); 38 | await backupUsers(cognitoISP, userpool, directory, delay); 39 | spinner.succeed(green(`JSON Exported successfully to ${directory}/\n`)); 40 | } else if (mode === 'restore') { 41 | spinner = spinner.start(orange`Restoring userpool`); 42 | await restoreUsers(cognitoISP, userpool, file, password, passwordModulePath); 43 | spinner.succeed(green(`Users imported successfully to ${userpool}\n`)); 44 | } else { 45 | spinner.fail(red`Mode passed is invalid, please make sure a valid command is passed here.\n`); 46 | process.exit(1); 47 | } 48 | } catch (error) { 49 | spinner.fail(red(error.message)); 50 | process.exit(1); 51 | } 52 | })(); 53 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "cognito-backup-restore", 3 | "projectOwner": "rahulpsd18", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": false, 11 | "contributors": [ 12 | { 13 | "login": "adityamedhe-cc", 14 | "name": "adityamedhe-cc", 15 | "avatar_url": "https://avatars1.githubusercontent.com/u/30614870?v=4", 16 | "profile": "https://github.com/adityamedhe-cc", 17 | "contributions": [ 18 | "doc", 19 | "code" 20 | ] 21 | }, 22 | { 23 | "login": "juno-visualsquares", 24 | "name": "juno-visualsquares", 25 | "avatar_url": "https://avatars1.githubusercontent.com/u/18159739?v=4", 26 | "profile": "https://github.com/juno-visualsquares", 27 | "contributions": [ 28 | "code", 29 | "ideas" 30 | ] 31 | }, 32 | { 33 | "login": "carbonrobot", 34 | "name": "Charlie Brown", 35 | "avatar_url": "https://avatars2.githubusercontent.com/u/1521394?v=4", 36 | "profile": "http://www.carbonatethis.com", 37 | "contributions": [ 38 | "bug" 39 | ] 40 | }, 41 | { 42 | "login": "alvarodelvalle", 43 | "name": "Alvaro Del Valle", 44 | "avatar_url": "https://avatars3.githubusercontent.com/u/32401961?v=4", 45 | "profile": "http://gardlabs.com", 46 | "contributions": [ 47 | "question" 48 | ] 49 | }, 50 | { 51 | "login": "vladistan", 52 | "name": "Vlad Korolev", 53 | "avatar_url": "https://avatars2.githubusercontent.com/u/36257?v=4", 54 | "profile": "http://blog.v-lad.org", 55 | "contributions": [ 56 | "code" 57 | ] 58 | }, 59 | { 60 | "login": "ashishkujoy", 61 | "name": "ashish kumar ", 62 | "avatar_url": "https://avatars2.githubusercontent.com/u/34642693?v=4", 63 | "profile": "https://github.com/ashishkujoy", 64 | "contributions": [ 65 | "doc", 66 | "code" 67 | ] 68 | }, 69 | { 70 | "login": "ufoo68", 71 | "name": "ufoo68", 72 | "avatar_url": "https://avatars1.githubusercontent.com/u/24458640?v=4", 73 | "profile": "https://qiita.com/ufoo68", 74 | "contributions": [ 75 | "code" 76 | ] 77 | }, 78 | { 79 | "login": "steveizzle", 80 | "name": "steveizzle", 81 | "avatar_url": "https://avatars2.githubusercontent.com/u/45331237?v=4", 82 | "profile": "https://github.com/steveizzle", 83 | "contributions": [ 84 | "doc", 85 | "code" 86 | ] 87 | }, 88 | { 89 | "login": "mholger", 90 | "name": "M. Holger", 91 | "avatar_url": "https://avatars3.githubusercontent.com/u/71354?v=4", 92 | "profile": "https://www.paralleltheory.com/", 93 | "contributions": [ 94 | "code" 95 | ] 96 | } 97 | ], 98 | "contributorsPerLine": 7, 99 | "skipCi": true 100 | } 101 | -------------------------------------------------------------------------------- /src/cli/args.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from 'yargs'; 2 | import chalk from 'chalk'; 3 | 4 | const dimmed = chalk.dim; 5 | const greyed = chalk.gray; 6 | const bold = chalk.bold; 7 | 8 | const version = require('../../package').version; 9 | 10 | export const argv = yargs 11 | // header 12 | .usage(`\nYou can run commands with "cognito-backup-restore" or the shortcut "cbr"\n 13 | Usage: $0 [options]`) 14 | 15 | // backup command 16 | .command('backup', dimmed`Backup/export all users in specified user pool`, (yargs) => { 17 | return yargs.options({ 18 | mode: { 19 | default: 'backup', 20 | hidden: true 21 | }, 22 | directory: { 23 | alias: ['dir'], 24 | describe: dimmed`Directory to export json file to`, 25 | string: true 26 | } 27 | }); 28 | }) 29 | 30 | // restore command 31 | .command('restore', dimmed`Restore/import users to a single user pool`, (yargs) => { 32 | return yargs.options({ 33 | mode: { 34 | default: 'restore', 35 | hidden: true 36 | }, 37 | file: { 38 | alias: ['f'], 39 | describe: dimmed`JSON file to import data from`, 40 | string: true 41 | }, 42 | password: { 43 | alias: ['pwd'], 44 | describe: dimmed`TemporaryPassword of the users imported`, 45 | string: true 46 | }, 47 | passwordModulePath: { 48 | alias: ["pwdModule"], 49 | describe: dimmed`A module that exports an interface getPwdForUsername(username: String) method, fall back to password parameter if throw`, 50 | string: true 51 | } 52 | }); 53 | }) 54 | 55 | // examples 56 | .example('$0 backup', greyed`Follow the interactive guide to backup userpool(s)`) 57 | .example('$0 restore', greyed`Follow the interactive guide to restore userpool`) 58 | .example('$0 backup -p [OPTIONS]', greyed`Backup using the options provided`) 59 | .example('$0 restore -p [OPTIONS]', greyed`Restore using the options provided`) 60 | 61 | // options 62 | .option('region', { 63 | alias: ['r'], 64 | describe: dimmed`The region to use. Overrides config/env settings`, 65 | string: true, 66 | }) 67 | .option('profile', { 68 | alias: ['p'], 69 | describe: dimmed`Use a specific profile from your credential file`, 70 | conflicts: ['aws-access-key', 'aws-secret-key'], 71 | string: true, 72 | }) 73 | .option('aws-access-key', { 74 | alias: ['key', 'k'], 75 | describe: dimmed`The AWS Access Key to use. Overrides config/env settings`, 76 | conflicts: ['profile'], 77 | string: true, 78 | }) 79 | .option('aws-secret-key', { 80 | alias: ['secret', 's'], 81 | describe: dimmed`The AWS Secret Key to use. Overrides config/env settings`, 82 | conflicts: ['profile'], 83 | string: true 84 | }) 85 | .option('use-ec2-metadata', { 86 | alias: ['metadata'], 87 | describe: dimmed`Use iam role in ec2 instance.`, 88 | type: 'boolean' 89 | }) 90 | .option('use-env-vars', { 91 | alias: ['env'], 92 | describe: dimmed`Use credentials from environment variables.`, 93 | type: 'boolean' 94 | }) 95 | .option('userpool', { 96 | alias: ['pool'], 97 | describe: dimmed`The Cognito pool to use. 'all' to backup all userpools.`, 98 | string: true 99 | }) 100 | .option('delay', { 101 | describe: dimmed`delay in millis between alternate users batch(60) backup, to avoid rate limit error`, 102 | number: true 103 | }) 104 | 105 | // help 106 | .help('help', dimmed`Show help`) 107 | .alias('help', 'h') 108 | .showHelpOnFail(false, bold`Specify --help for available options`) 109 | 110 | // version 111 | .version('version', dimmed`Show version number`, (function () { return version; })()) 112 | .alias('version', 'v') 113 | 114 | // footer 115 | .epilog(dimmed`\nPlease report any issues/suggestions here:\nhttps://github.com/rahulpsd18/cognito-backup-restore/issues\n`) 116 | .strict() 117 | .wrap(Math.min(120, yargs.terminalWidth())) 118 | .argv; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as AWS from 'aws-sdk'; 4 | import Bottleneck from 'bottleneck'; 5 | import * as delay from "delay"; 6 | 7 | const JSONStream = require('JSONStream'); 8 | 9 | type CognitoISP = AWS.CognitoIdentityServiceProvider; 10 | type ListUsersRequestTypes = AWS.CognitoIdentityServiceProvider.Types.ListUsersRequest; 11 | type AdminCreateUserRequest = AWS.CognitoIdentityServiceProvider.Types.AdminCreateUserRequest; 12 | type AttributeType = AWS.CognitoIdentityServiceProvider.Types.AttributeType; 13 | 14 | 15 | export const backupUsers = async (cognito: CognitoISP, UserPoolId: string, directory: string, delayDurationInMillis: number = 0) => { 16 | let userPoolList: string[] = []; 17 | 18 | if (UserPoolId == 'all') { 19 | // TODO: handle data.NextToken when exceeding the MaxResult limit 20 | const { UserPools } = await cognito.listUserPools({ MaxResults: 60 }).promise(); 21 | userPoolList = userPoolList.concat(UserPools && UserPools.map(el => el.Id as string) as any); 22 | } else { 23 | userPoolList.push(UserPoolId); 24 | } 25 | 26 | for (let poolId of userPoolList) { 27 | 28 | // create directory if not exists 29 | !fs.existsSync(directory) && fs.mkdirSync(directory) 30 | 31 | const file = path.join(directory, `${poolId}.json`) 32 | const writeStream = fs.createWriteStream(file); 33 | const stringify = JSONStream.stringify(); 34 | 35 | stringify.pipe(writeStream); 36 | 37 | const params: ListUsersRequestTypes = { 38 | UserPoolId: poolId 39 | }; 40 | 41 | try { 42 | const paginationCalls = async () => { 43 | const { Users = [], PaginationToken } = await cognito.listUsers(params).promise(); 44 | Users.forEach(user => stringify.write(user as string)); 45 | 46 | if (PaginationToken) { 47 | params.PaginationToken = PaginationToken; 48 | if(delayDurationInMillis > 0) { 49 | await delay(delayDurationInMillis); 50 | } 51 | await paginationCalls(); 52 | }; 53 | }; 54 | 55 | await paginationCalls(); 56 | } catch (error) { 57 | throw error; // to be catched by calling function 58 | } finally { 59 | stringify.end(); 60 | stringify.on('end', () => { 61 | writeStream.end(); 62 | }); 63 | } 64 | } 65 | }; 66 | 67 | 68 | export const restoreUsers = async (cognito: CognitoISP, UserPoolId: string, file: string, password?: string, passwordModulePath?: String, delayDurationInMillis: number = 0) => { 69 | if (UserPoolId == 'all') throw Error(`'all' is not a acceptable value for UserPoolId`); 70 | let pwdModule: any = null; 71 | if (typeof passwordModulePath === 'string') { 72 | pwdModule = require(passwordModulePath); 73 | } 74 | 75 | const { UserPool } = await cognito.describeUserPool({ UserPoolId }).promise(); 76 | const UsernameAttributes = UserPool && UserPool.UsernameAttributes || []; 77 | 78 | const limiter = new Bottleneck({ minTime: 2000 }); 79 | const readStream = fs.createReadStream(file); 80 | const parser = JSONStream.parse(); 81 | 82 | parser.on('data', async (data: any[]) => { 83 | for (let user of data) { 84 | // filter out non-mutable attributes 85 | const attributes = user.Attributes.filter((attr: AttributeType) => attr.Name !== 'sub'); 86 | 87 | const params: AdminCreateUserRequest = { 88 | UserPoolId, 89 | Username: user.Username, 90 | UserAttributes: attributes 91 | }; 92 | 93 | // Set Username as email if UsernameAttributes of UserPool contains email 94 | if (UsernameAttributes.includes('email')) { 95 | params.Username = pluckValue(user.Attributes, 'email') as string; 96 | params.DesiredDeliveryMediums = ['EMAIL'] 97 | } else if (UsernameAttributes.includes('phone_number')) { 98 | params.Username = pluckValue(user.Attributes, 'phone_number') as string; 99 | params.DesiredDeliveryMediums = ['EMAIL', 'SMS'] 100 | } 101 | 102 | // If password module is specified, use it silently 103 | // if not provided or it throws, we fallback to password if provided 104 | // if password is provided, use it silently 105 | // else set a cognito generated one and send email (default) 106 | let specificPwdExistsForUser = false; 107 | if (pwdModule !== null){ 108 | try { 109 | params.MessageAction = 'SUPPRESS'; 110 | params.TemporaryPassword = pwdModule.getPwdForUsername(user.Username); 111 | specificPwdExistsForUser = true; 112 | } catch (e) { 113 | console.error(`"${e.message}" error occurred for user "${params.Username}" while getting password from ${passwordModulePath}. Falling back to default.`); 114 | } 115 | } 116 | if (!specificPwdExistsForUser && password) { 117 | params.MessageAction = 'SUPPRESS'; 118 | params.TemporaryPassword = password; 119 | } 120 | const wrapped = limiter.wrap(async () => cognito.adminCreateUser(params).promise()); 121 | try { 122 | await wrapped(); 123 | } catch (e) { 124 | if (e.code === 'UsernameExistsException') { 125 | console.log(`Looks like user ${user.Username} already exists, ignoring.`) 126 | } else { 127 | throw e; 128 | } 129 | } 130 | }; 131 | }); 132 | 133 | readStream.pipe(parser); 134 | }; 135 | 136 | const pluckValue = (arr: AttributeType[], key: string) => { 137 | const object = arr.find((attr: AttributeType) => attr.Name == key); 138 | 139 | if (!object) throw Error(`${key} not found in the user attribute`) 140 | 141 | return object.Value; 142 | }; 143 | -------------------------------------------------------------------------------- /src/cli/options.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from 'aws-sdk'; 2 | import * as fuzzy from 'fuzzy'; 3 | import * as inquirer from 'inquirer'; 4 | import chalk from 'chalk'; 5 | import { argv } from './args'; 6 | 7 | inquirer.registerPrompt('directory', require('inquirer-select-directory')); 8 | inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')); 9 | inquirer.registerPrompt('filePath', require('inquirer-file-path')); 10 | 11 | const greenify = chalk.green; 12 | 13 | const searchCognitoRegion = async (_: never, input: string) => { 14 | input = input || ''; 15 | const region = [ 16 | { get name() { return greenify(this.value) + ' :: US East (N. Virginia)' }, value: 'us-east-1' }, 17 | { get name() { return greenify(this.value) + ' :: US East (Ohio)' }, value: 'us-east-2' }, 18 | { get name() { return greenify(this.value) + ' :: US West (Oregon)' }, value: 'us-west-2' }, 19 | { get name() { return greenify(this.value) + ' :: Asia Pacific (Mumbai)' }, value: 'ap-south-1' }, 20 | { get name() { return greenify(this.value) + ' :: Asia Pacific (Tokyo)' }, value: 'ap-northeast-1' }, 21 | { get name() { return greenify(this.value) + ' :: Asia Pacific (Seoul)' }, value: 'ap-northeast-2' }, 22 | { get name() { return greenify(this.value) + ' :: Asia Pacific (Singapore)' }, value: 'ap-southeast-1' }, 23 | { get name() { return greenify(this.value) + ' :: Asia Pacific (Sydney)' }, value: 'ap-southeast-2' }, 24 | { get name() { return greenify(this.value) + ' :: EU (Frankfurt)' }, value: 'eu-central-1' }, 25 | { get name() { return greenify(this.value) + ' :: EU (Ireland)' }, value: 'eu-west-1' }, 26 | { get name() { return greenify(this.value) + ' :: EU (London)' }, value: 'eu-west-2' } 27 | ]; 28 | const fuzzyResult = fuzzy.filter(input, region, { extract: el => el.value }); 29 | return fuzzyResult.map(el => { 30 | return el.original 31 | }); 32 | }; 33 | 34 | const verifyOptions = async () => { 35 | let { mode, profile, region, key, secret, userpool, directory, file, password, passwordModulePath, delay, metadata, env } = argv; 36 | 37 | // choose the mode if not passed through CLI or invalid is passed 38 | if (!mode || !['restore', 'backup'].includes(mode)) { 39 | const modeChoice = await inquirer.prompt<{ selected: string }>({ 40 | type: 'list', 41 | name: 'selected', 42 | message: 'Choose the mode', 43 | choices: ['Backup', 'Restore'], 44 | }); 45 | 46 | mode = modeChoice.selected.toLowerCase(); 47 | } 48 | 49 | if (!metadata && !env) { 50 | 51 | let savedAWSProfiles: string[] = []; 52 | 53 | try { // to read from saved config 54 | const credentials = new AWS.IniLoader().loadFrom({}); 55 | savedAWSProfiles = Object.keys(credentials); 56 | } catch (err) { 57 | // couldn't find saved config 58 | } 59 | 60 | const searchAWSProfile = async (_: never, input: string) => { 61 | input = input || ''; 62 | const fuzzyResult = fuzzy.filter(input, savedAWSProfiles); 63 | return fuzzyResult.map(el => { 64 | return el.original; 65 | }); 66 | }; 67 | 68 | // choose your profile from available AWS profiles if not passed through CLI 69 | // only shown in case when no valid profile or no key && secret is passed. 70 | if (savedAWSProfiles.length && !savedAWSProfiles.includes(profile) && (!key || !secret)) { 71 | const awsProfileChoice = await inquirer.prompt({ 72 | type: 'autocomplete', 73 | name: 'selected', 74 | message: 'Choose your AWS Profile', 75 | source: searchAWSProfile, 76 | } as inquirer.Question); 77 | 78 | profile = awsProfileChoice.selected; 79 | }; 80 | } 81 | // choose your region if not passed through CLI 82 | if (!region) { 83 | const awsRegionChoice = await inquirer.prompt({ 84 | type: 'autocomplete', 85 | name: 'selected', 86 | message: 'Choose your Cognito Region', 87 | source: searchCognitoRegion, 88 | } as inquirer.Question); 89 | 90 | region = awsRegionChoice.selected; 91 | }; 92 | 93 | if (!userpool) { 94 | // update the config of aws-sdk based on profile/credentials passed 95 | AWS.config.update({ region }); 96 | 97 | if (profile) { 98 | AWS.config.credentials = new AWS.SharedIniFileCredentials({ profile }); 99 | } else if (key && secret) { 100 | AWS.config.credentials = new AWS.Credentials({ 101 | accessKeyId: key, secretAccessKey: secret 102 | }); 103 | } else if (env) { 104 | AWS.config.credentials = new AWS.EnvironmentCredentials('AWS'); 105 | } else if (metadata) { 106 | AWS.config.credentials = new AWS.EC2MetadataCredentials({}); 107 | } else { 108 | throw Error('No credential found. Try using --help to read about various configurations supported.') 109 | } 110 | 111 | const cognitoISP = new AWS.CognitoIdentityServiceProvider(); 112 | const { UserPools } = await cognitoISP.listUserPools({ MaxResults: 60 }).promise(); 113 | // TODO: handle data.NextToken when exceeding the MaxResult limit 114 | 115 | const userPoolList = UserPools 116 | && UserPools.map(el => ({ name: el.Name || '', value: el.Id || '' })) || [] 117 | 118 | if (!userPoolList.length) 119 | throw Error(`No userpool found in this region. Are you sure the pool is in "${region}".`); 120 | 121 | if (mode === 'backup') userPoolList.unshift({ name: chalk.magentaBright.bold('ALL'), value: 'all' }); 122 | 123 | const searchCognitoPool = async (_: never, input: string) => { 124 | input = input || ''; 125 | 126 | const fuzzyResult = fuzzy.filter(input, userPoolList, { extract: el => el.value }); 127 | return fuzzyResult.map(el => { 128 | return el.original 129 | }); 130 | }; 131 | 132 | // choose your cognito pool from the region you selected 133 | const cognitoPoolChoice = await inquirer.prompt({ 134 | type: 'autocomplete', 135 | name: 'selected', 136 | message: 'Choose your Cognito Pool', 137 | source: searchCognitoPool, 138 | pageSize: 60 139 | } as inquirer.Question); 140 | 141 | userpool = cognitoPoolChoice.selected; 142 | }; 143 | 144 | if (mode === 'backup' && !directory) { 145 | const directoryLocation = await inquirer.prompt({ 146 | type: 'directory', 147 | name: 'selected', 148 | message: 'Choose your file destination', 149 | basePath: '.' 150 | } as inquirer.Question); 151 | 152 | directory = directoryLocation.selected; 153 | }; 154 | 155 | if (mode === 'restore' && !file) { 156 | const fileLocation = await inquirer.prompt({ 157 | type: 'filePath', 158 | name: 'selected', 159 | message: 'Choose the JSON file', 160 | basePath: '.' 161 | } as inquirer.Question); 162 | 163 | file = fileLocation.selected; 164 | } 165 | 166 | if (mode === 'restore' && passwordModulePath) { 167 | try { 168 | const pwdModule = require(passwordModulePath); 169 | if (typeof pwdModule.getPwdForUsername !== 'function') { 170 | throw Error(`Cannot find getPwdForUsername(username: String) in password module "${passwordModulePath}".`); 171 | }; 172 | } catch (e) { 173 | throw Error(`Cannot load password module path "${passwordModulePath}".`); 174 | } 175 | } 176 | return { mode, profile, region, key, secret, userpool, directory, file, password, passwordModulePath, delay, metadata, env } 177 | }; 178 | 179 | 180 | export const options = verifyOptions(); 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cognito-backup-restore 2 | [![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors) 3 | 4 | AIO Tool for backing up and restoring AWS Cognito User Pools 5 | 6 | Amazon Cognito is awesome, but has its own set of limitations. Currently there is no backup option provided in case we need to take backup of users (to move to another service) or restore them to new Userpool. 7 | 8 | `cognito-backup-restore` tries to overcome this problem by providing a way to backup users from cognito pool(s) to json file and vice-versa. 9 | 10 | > **Please Note:** *There is no way of getting passwords of the users in cognito, so you may need to ask them to make use of ForgotPassword to recover their account.* 11 | 12 | 13 | ## Requirements 14 | 15 | Requires node 6.10 or newer 16 | 17 | ## Installation 18 | 19 | `cognito-backup-restore` is available as a package on [npm](https://www.npmjs.com/package/cognito-backup-restore). 20 | 21 | ```shell 22 | npm install -g cognito-backup-restore 23 | ``` 24 | 25 | ## Usage 26 | 27 | `cognito-backup-restore` can be used by importing it directly or via [CLI](#cli) (recommended). 28 | 29 | ### Imports 30 | 31 | Make sure you have installed it locally `npm install --save cognito-backup-restore`. Typings are available and included. 32 | 33 | ```typescript 34 | import * as AWS from 'aws-sdk'; 35 | import {backupUsers, restoreUsers} from 'cognito-backup-restore'; 36 | 37 | const cognitoISP = new AWS.CognitoIdentityServiceProvider(); 38 | 39 | // you may use async-await too 40 | backupUsers(cognitoISP, , ) 41 | .then(() => console.log(`Backup completed`)) 42 | .catch(console.error) 43 | 44 | restoreUsers(cognitoISP, , , ) 45 | .then(() => console.log(`Restore completed`)) 46 | .catch(console.error) 47 | ``` 48 | 49 | This is useful incase you want to write your own wrapper or script instead of using CLI. 50 | 51 | 52 | ### CLI 53 | Run `cognito-backup-restore` or `cbr` to use it. Make use of `-h` for help. 54 | 55 | ```shell 56 | cbr [options] 57 | ``` 58 | 59 | > Available options are: 60 | > 61 | > `--region` `-r`: The region to use. Overrides config/env settings 62 | > 63 | > `--userpool` `--pool`: The Cognito pool to use. Possible value of `all` is allowed in case of backup. 64 | > 65 | > `--profile` `-p`: Use a specific profile from the credential file. Key and Secret can be passed instead (see below). 66 | > 67 | > `--aws-access-key` `--key`: The AWS Access Key to use. Not to be passed when using `--profile`. 68 | > 69 | > `--aws-secret-key` `--secret`: The AWS Secret Key to use. Not to be passed when using `--profile`. 70 | > 71 | > `--delay`: delay in millis between alternate users batch(60) backup, to avoid rate limit error. 72 | > 73 | > `--use-env-vars`: Use AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN (optional) as environment variables 74 | > 75 | > `--use-ec2-metadata`: Use credentials received from the metadata service on an EC2 instance 76 | 77 | ![Image showing CLI Usage](gifs/demo.png "CLI Usage") 78 | 79 | - **Backup** 80 | ```shell 81 | cbr backup 82 | cbr backup 83 | ``` 84 | `--directory` option is available to export json data to. 85 | 86 | ![GIF for using Backup CLI](gifs/backup-min.gif "Backup Demo") 87 | 88 | - **Restore** 89 | ```shell 90 | cbr restore 91 | cbr restore 92 | ``` 93 | `--file` option is available to read the json file to import from. 94 | 95 | `--pwd` option is available to set TemporaryPassword of the users. If not provided, cognito generated password will be used and email will be sent to the users with One Time Password. 96 | 97 | `--pwdModule` option is available to make use of custom logic to generate password. If not provided, cognito generated password will be used and email will be sent to the users with One Time Password, unless `--pwd` is used. Make sure to pass absolute path of the file. Refer [this](https://github.com/rahulpsd18/cognito-backup-restore/pull/1). 98 | 99 | ![GIF for using Restore CLI](gifs/restore-min.gif "Restore Demo") 100 | 101 | **In case any of the required option is missing, a interactive command line user interface kicks in to select from.** 102 | 103 | ## Todo 104 | 105 | - [X] ~~Fine tune the backup process~~ 106 | - [X] ~~Implement Restore~~ 107 | - [X] ~~Write detailed Readme with examples~~ 108 | - [ ] Convert JSON to CSV 109 | - [ ] Implement Amazon Cognito User Pool Import Job 110 | - [ ] AWS Cross-Region Cognito Replication 111 | 112 | ## Contributors 113 | 114 | Thanks goes to these wonderful people ([emoji key](https://github.com/all-contributors/all-contributors#emoji-key)): 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |

adityamedhe-cc

📖 💻

juno-visualsquares

💻 🤔

Charlie Brown

🐛

Alvaro Del Valle

💬

Vlad Korolev

💻

ashish kumar

📖 💻

ufoo68

💻

steveizzle

📖 💻

M. Holger

💻
134 | 135 | 136 | 137 | 138 | 139 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! --------------------------------------------------------------------------------