├── images ├── large_s.png ├── small_s.png └── DeviceMapping.png ├── NOTICE ├── .gitignore ├── .github └── PULL_REQUEST_TEMPLATE.md ├── deploy-lambda.sh ├── .ask └── config.template ├── CODE_OF_CONDUCT.md ├── skill.json ├── lambda └── custom │ ├── package.json │ ├── Handlers │ ├── sms.js │ ├── speechHandler.js │ └── auth.js │ ├── package-lock.json │ └── index.js ├── hooks └── pre_deploy_hook.sh ├── template-Dev.yaml ├── skill_final.json ├── CONTRIBUTING.md ├── LICENSE ├── models └── en-US.json └── README.md /images/large_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/alexa-skill-authentication/master/images/large_s.png -------------------------------------------------------------------------------- /images/small_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/alexa-skill-authentication/master/images/small_s.png -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Alexa Skill Authentication Sample 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Ignore packaged YAML files as they are generated at run time 2 | packagedDev.yaml 3 | **/node_modules 4 | **config -------------------------------------------------------------------------------- /images/DeviceMapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/alexa-skill-authentication/master/images/DeviceMapping.png -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /deploy-lambda.sh: -------------------------------------------------------------------------------- 1 | sam package --template-file template-Dev.yaml --s3-bucket secure.alexa.skill --output-template-file packagedDev.yaml 2 | aws cloudformation deploy --template-file ${PWD}/packagedDev.yaml --stack-name BusinessResultsSkill --capabilities CAPABILITY_IAM 3 | -------------------------------------------------------------------------------- /.ask/config.template: -------------------------------------------------------------------------------- 1 | { 2 | "deploy_settings": { 3 | "default": { 4 | "skill_id": "", 5 | "was_cloned": false, 6 | "merge": { 7 | "manifest": { 8 | "apis": { 9 | "custom": {} 10 | } 11 | } 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "en-US": { 6 | "summary": "Financial KPIs at a glance", 7 | "name": "Business Insights" 8 | } 9 | } 10 | }, 11 | "apis": { 12 | "custom": {} 13 | }, 14 | "manifestVersion": "1.0" 15 | } 16 | } -------------------------------------------------------------------------------- /lambda/custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secure-alexa-skill", 3 | "version": "1.0.0", 4 | "description": "Business results alexa skill", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "aravind", 10 | "license": "ISC", 11 | "dependencies": { 12 | "ask-sdk": "^2.0.7", 13 | "ask-sdk-core": "^2.0.7", 14 | "ask-sdk-model": "^1.4.1", 15 | "comma-number": "^2.0.0", 16 | "moment": "^2.22.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lambda/custom/Handlers/sms.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Amazon Software License 3 | // http://aws.amazon.com/asl/ 4 | 5 | // Load the AWS SDK for Node.js 6 | var AWS = require('aws-sdk'); 7 | 8 | 9 | // Set region 10 | AWS.config.update({ region: process.env.AWSREGION }); 11 | const sns = new AWS.SNS(); 12 | 13 | async function sendSMS(phoneNumber, pin) { 14 | 15 | // Create SMS Attribute parameters 16 | const params1 = { 17 | attributes: { 18 | 'DefaultSMSType': 'Transactional' 19 | } 20 | }; 21 | 22 | // Create promise and SNS service object 23 | await sns.setSMSAttributes(params1).promise(); 24 | 25 | // Create publish parameters 26 | const params = { 27 | Message: `Business Insights: Your security code is ${pin}`, 28 | PhoneNumber: phoneNumber, 29 | }; 30 | 31 | // Create promise and SNS service object 32 | return sns.publish(params).promise(); 33 | } 34 | 35 | exports.sendSMS = sendSMS; 36 | -------------------------------------------------------------------------------- /hooks/pre_deploy_hook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Shell script for ask-cli pre-deploy hook for Node.js 3 | # Script Usage: pre_deploy_hook.sh 4 | 5 | # SKILL_NAME is the preformatted name passed from the CLI, after removing special characters. 6 | # DO_DEBUG is boolean value for debug logging 7 | # TARGET is the deploy TARGET provided to the CLI. (eg: all, skill, lambda etc.) 8 | 9 | # Run this script under skill root folder 10 | 11 | # The script does the following: 12 | # - Run "npm install" in each sourceDir in skill.json 13 | 14 | SKILL_NAME=$1 15 | DO_DEBUG=${2:-false} 16 | TARGET=${3:-"all"} 17 | 18 | if [ $DO_DEBUG == false ] 19 | then 20 | exec > /dev/null 2>&1 21 | fi 22 | 23 | install_dependencies() { 24 | npm install --prefix "$1" >/dev/null 2>&1 25 | return $? 26 | } 27 | 28 | echo "###########################" 29 | echo "##### pre-deploy hook #####" 30 | echo "###########################" 31 | 32 | if [[ $TARGET == "all" || $TARGET == "lambda" ]]; then 33 | grep "sourceDir" ./skill.json | cut -d: -f2 | sed 's/"//g' | sed 's/,//g' | while read -r SOURCE_DIR; do 34 | if install_dependencies $SOURCE_DIR; then 35 | echo "Codebase ($SOURCE_DIR) built successfully." 36 | else 37 | echo "There was a problem installing dependencies for ($SOURCE_DIR)." 38 | exit 1 39 | fi 40 | done 41 | echo "###########################" 42 | fi 43 | 44 | exit 0 45 | -------------------------------------------------------------------------------- /template-Dev.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | Description: 'Business Results' 4 | Resources: 5 | DynamoDBTable: 6 | Type: AWS::Serverless::SimpleTable 7 | Properties: 8 | TableName: UserAuth 9 | PrimaryKey: 10 | Name: id 11 | Type: String 12 | SSESpecification: 13 | SSEEnabled: true 14 | 15 | SMSDynamoDBTable: 16 | Type: AWS::Serverless::SimpleTable 17 | Properties: 18 | TableName: DeviceContactMapping 19 | PrimaryKey: 20 | Name: id 21 | Type: String 22 | SSESpecification: 23 | SSEEnabled: true 24 | 25 | BusinessResults: 26 | Type: 'AWS::Serverless::Function' 27 | Properties: 28 | Handler: index.handler 29 | Runtime: nodejs8.10 30 | CodeUri: ./lambda/custom 31 | Description: 'Business Results' 32 | MemorySize: 256 33 | Timeout: 120 34 | Environment: 35 | Variables: 36 | AWSREGION: us-east-1 37 | PIN_TIMEOUT_IN_MINS: 5 38 | Policies: 39 | - Version: "2012-10-17" 40 | Statement: 41 | - 42 | Effect: Allow 43 | Action: 44 | - logs:CreateLogGroup 45 | - logs:CreateLogStream 46 | - logs:PutLogEvents 47 | - s3:* 48 | - dynamodb:* 49 | - sns:* 50 | Resource: "*" 51 | Events: 52 | AlexaSkillEvent: 53 | Type: AlexaSkill -------------------------------------------------------------------------------- /skill_final.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "privacyAndCompliance":{ 4 | "allowsPurchases": false, 5 | "usesPersonalInfo": false, 6 | "isChildDirected": false, 7 | "isExportCompliant": true, 8 | "containsAds": false, 9 | "locales": { 10 | "en-US": { 11 | "privacyPolicyUrl": "https://url/privacy.html", 12 | "termsOfUseUrl": "https://url/termsofuse.html" 13 | } 14 | } 15 | }, 16 | "publishingInformation": { 17 | "locales": { 18 | "en-US": { 19 | "summary": "Financial KPIs at a glance", 20 | "examplePhrases": [ 21 | "Alexa, How is Sales doing", 22 | "Alexa, How is my Net profit Margin", 23 | "Alexa, What was the Net Profit Margin last year" 24 | ], 25 | "name": "Business Insights", 26 | "description": "This skill provides answers to the most common financial KPIs within a business.", 27 | "smallIconUri": "https://s3.amazonaws.com/secure.alexa.skill/small_s.png", 28 | "largeIconUri": "https://s3.amazonaws.com/secure.alexa.skill/large_s.png" 29 | } 30 | }, 31 | "isAvailableWorldwide": false, 32 | "testingInstructions": "How is Sales doing", 33 | "category": "BUSINESS_AND_FINANCE", 34 | "distributionMode": "PRIVATE", 35 | "distributionCountries":["US"] 36 | }, 37 | "apis": { 38 | "custom": { 39 | "endpoint": { 40 | "uri": "LAMBDA_ARN" 41 | } 42 | } 43 | }, 44 | "manifestVersion": "1.0" 45 | } 46 | } -------------------------------------------------------------------------------- /lambda/custom/Handlers/speechHandler.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Amazon Software License 3 | // http://aws.amazon.com/asl/ 4 | 5 | function noMobileNumberRegistered(handlerInput) { 6 | const responseBuilder = handlerInput.responseBuilder; 7 | return responseBuilder 8 | .speak('Your mobile number is not registered with this device. Please contact the skill administrator.') 9 | .withShouldEndSession(true) 10 | .getResponse(); 11 | } 12 | 13 | function elicitSlotResponse(slotName, handlerInput, speech, repromptSpeech) { 14 | const responseBuilder = handlerInput.responseBuilder; 15 | const currentIntent = handlerInput.requestEnvelope.request.intent; 16 | 17 | return responseBuilder 18 | .speak(speech) 19 | .reprompt(repromptSpeech) 20 | .addElicitSlotDirective(slotName, currentIntent) 21 | .getResponse(); 22 | 23 | } 24 | 25 | function promptForPin(handlerInput) { 26 | return elicitSlotResponse( 27 | 'PIN', 28 | handlerInput, 29 | `Please provide the Security code`, 30 | `Try providing the Security code`); 31 | } 32 | 33 | function promptWithValidPin(handlerInput, speechResponse, wasPINProvidedAndValidated) { 34 | const responseBuilder = handlerInput.responseBuilder; 35 | let speechText = speechResponse; 36 | if (wasPINProvidedAndValidated) { 37 | speechText = ` `; 39 | } 40 | return responseBuilder 41 | .speak(speechText) 42 | .reprompt(`Would you like to know anything else?`) 43 | .getResponse(); 44 | 45 | } 46 | 47 | function promptForInvalidPin(handlerInput) { 48 | return elicitSlotResponse( 49 | 'PIN', 50 | handlerInput, 51 | ``, 53 | `Try providing the Security code`); 54 | } 55 | 56 | exports.promptForPin = promptForPin; 57 | exports.promptForInvalidPin = promptForInvalidPin; 58 | exports.noMobileNumberRegistered = noMobileNumberRegistered; 59 | exports.promptWithValidPin = promptWithValidPin; -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/alexa-skill-authentication/issues), or [recently closed](https://github.com/aws-samples/alexa-skill-authentication/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/alexa-skill-authentication/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-samples/alexa-skill-authentication/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Amazon Software License 1.0 2 | 3 | This Amazon Software License ("License") governs your use, reproduction, and 4 | distribution of the accompanying software as specified below. 5 | 6 | 1. Definitions 7 | 8 | "Licensor" means any person or entity that distributes its Work. 9 | 10 | "Software" means the original work of authorship made available under this 11 | License. 12 | 13 | "Work" means the Software and any additions to or derivative works of the 14 | Software that are made available under this License. 15 | 16 | The terms "reproduce," "reproduction," "derivative works," and 17 | "distribution" have the meaning as provided under U.S. copyright law; 18 | provided, however, that for the purposes of this License, derivative works 19 | shall not include works that remain separable from, or merely link (or bind 20 | by name) to the interfaces of, the Work. 21 | 22 | Works, including the Software, are "made available" under this License by 23 | including in or with the Work either (a) a copyright notice referencing the 24 | applicability of this License to the Work, or (b) a copy of this License. 25 | 26 | 2. License Grants 27 | 28 | 2.1 Copyright Grant. Subject to the terms and conditions of this License, 29 | each Licensor grants to you a perpetual, worldwide, non-exclusive, 30 | royalty-free, copyright license to reproduce, prepare derivative works of, 31 | publicly display, publicly perform, sublicense and distribute its Work and 32 | any resulting derivative works in any form. 33 | 34 | 2.2 Patent Grant. Subject to the terms and conditions of this License, each 35 | Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free 36 | patent license to make, have made, use, sell, offer for sale, import, and 37 | otherwise transfer its Work, in whole or in part. The foregoing license 38 | applies only to the patent claims licensable by Licensor that would be 39 | infringed by Licensor's Work (or portion thereof) individually and 40 | excluding any combinations with any other materials or technology. 41 | 42 | 3. Limitations 43 | 44 | 3.1 Redistribution. You may reproduce or distribute the Work only if 45 | (a) you do so under this License, (b) you include a complete copy of this 46 | License with your distribution, and (c) you retain without modification 47 | any copyright, patent, trademark, or attribution notices that are present 48 | in the Work. 49 | 50 | 3.2 Derivative Works. You may specify that additional or different terms 51 | apply to the use, reproduction, and distribution of your derivative works 52 | of the Work ("Your Terms") only if (a) Your Terms provide that the use 53 | limitation in Section 3.3 applies to your derivative works, and (b) you 54 | identify the specific derivative works that are subject to Your Terms. 55 | Notwithstanding Your Terms, this License (including the redistribution 56 | requirements in Section 3.1) will continue to apply to the Work itself. 57 | 58 | 3.3 Use Limitation. The Work and any derivative works thereof only may be 59 | used or intended for use with the web services, computing platforms or 60 | applications provided by Amazon.com, Inc. or its affiliates, including 61 | Amazon Web Services, Inc. 62 | 63 | 3.4 Patent Claims. If you bring or threaten to bring a patent claim against 64 | any Licensor (including any claim, cross-claim or counterclaim in a 65 | lawsuit) to enforce any patents that you allege are infringed by any Work, 66 | then your rights under this License from such Licensor (including the 67 | grants in Sections 2.1 and 2.2) will terminate immediately. 68 | 69 | 3.5 Trademarks. This License does not grant any rights to use any 70 | Licensor's or its affiliates' names, logos, or trademarks, except as 71 | necessary to reproduce the notices described in this License. 72 | 73 | 3.6 Termination. If you violate any term of this License, then your rights 74 | under this License (including the grants in Sections 2.1 and 2.2) will 75 | terminate immediately. 76 | 77 | 4. Disclaimer of Warranty. 78 | 79 | THE WORK IS PROVIDED "AS IS" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 80 | EITHER EXPRESS OR IMPLIED, INCLUDING WARRANTIES OR CONDITIONS OF 81 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR 82 | NON-INFRINGEMENT. YOU BEAR THE RISK OF UNDERTAKING ANY ACTIVITIES UNDER 83 | THIS LICENSE. SOME STATES' CONSUMER LAWS DO NOT ALLOW EXCLUSION OF AN 84 | IMPLIED WARRANTY, SO THIS DISCLAIMER MAY NOT APPLY TO YOU. 85 | 86 | 5. Limitation of Liability. 87 | 88 | EXCEPT AS PROHIBITED BY APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL 89 | THEORY, WHETHER IN TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE 90 | SHALL ANY LICENSOR BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, 91 | INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR 92 | RELATED TO THIS LICENSE, THE USE OR INABILITY TO USE THE WORK (INCLUDING 93 | BUT NOT LIMITED TO LOSS OF GOODWILL, BUSINESS INTERRUPTION, LOST PROFITS 94 | OR DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY OTHER COMM ERCIAL DAMAGES 95 | OR LOSSES), EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF 96 | SUCH DAMAGES. 97 | -------------------------------------------------------------------------------- /lambda/custom/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secure-alexa-skill", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ask-sdk": { 8 | "version": "2.3.0", 9 | "resolved": "https://registry.npmjs.org/ask-sdk/-/ask-sdk-2.3.0.tgz", 10 | "integrity": "sha512-yJqGeYSRscp4II9v5j/FK5D5t/ywXwvnBKDepstZZ0tlzByYHBxMH55LpL/F5mcfnYmnqzrIS4nrGiX87Xnpfw==", 11 | "requires": { 12 | "ask-sdk-core": "^2.3.0", 13 | "ask-sdk-dynamodb-persistence-adapter": "^2.3.0", 14 | "ask-sdk-model": "^1.0.0" 15 | } 16 | }, 17 | "ask-sdk-core": { 18 | "version": "2.3.0", 19 | "resolved": "https://registry.npmjs.org/ask-sdk-core/-/ask-sdk-core-2.3.0.tgz", 20 | "integrity": "sha512-aSc/xaY0lHZu8pmkYPR1ms72WPH73aggo7uL/k0dut6AhP3CmdU3Y9ui7IOAvRJnha0cToQ8TYU0jLEOH6MMNw==", 21 | "requires": { 22 | "ask-sdk-runtime": "^2.2.0" 23 | } 24 | }, 25 | "ask-sdk-dynamodb-persistence-adapter": { 26 | "version": "2.3.0", 27 | "resolved": "https://registry.npmjs.org/ask-sdk-dynamodb-persistence-adapter/-/ask-sdk-dynamodb-persistence-adapter-2.3.0.tgz", 28 | "integrity": "sha512-NeXeXH2YsgsthaijQPej/Peago7oNPxXChPtC6whdPFMvBKQNGWJWI8hPN8o8DWQg4kXSDDPJsjLaUSpBjFFmg==", 29 | "requires": { 30 | "aws-sdk": "^2.163.0" 31 | } 32 | }, 33 | "ask-sdk-model": { 34 | "version": "1.11.2", 35 | "resolved": "https://registry.npmjs.org/ask-sdk-model/-/ask-sdk-model-1.11.2.tgz", 36 | "integrity": "sha512-nxXvf3NRfKbsKVegHrYqDaCn0EMylSuPKXn7SsMmgRJyi5HrxVycb1X/moQVoBJ06ZwGtvbhcI4AhtcWDbZFnw==" 37 | }, 38 | "ask-sdk-runtime": { 39 | "version": "2.2.0", 40 | "resolved": "https://registry.npmjs.org/ask-sdk-runtime/-/ask-sdk-runtime-2.2.0.tgz", 41 | "integrity": "sha512-pYqTizpk+13l7sU/bdyIZKRptyR7fP05T8OdY0/tpD8WQaCyIvPigIEHjYcXN0Yz23FkKECbIRvfXN9OQ7Ka5w==" 42 | }, 43 | "aws-sdk": { 44 | "version": "2.385.0", 45 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.385.0.tgz", 46 | "integrity": "sha512-ensf8QVbFdHQYLwhHgkH0FpE7nVxDl1SYJMM09p7neveW48rxQEwSOi/BmXDuI2/xsm7aHASUEZqK6zzeEx77g==", 47 | "requires": { 48 | "buffer": "4.9.1", 49 | "events": "1.1.1", 50 | "ieee754": "1.1.8", 51 | "jmespath": "0.15.0", 52 | "querystring": "0.2.0", 53 | "sax": "1.2.1", 54 | "url": "0.10.3", 55 | "uuid": "3.3.2", 56 | "xml2js": "0.4.19" 57 | } 58 | }, 59 | "base64-js": { 60 | "version": "1.3.0", 61 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", 62 | "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" 63 | }, 64 | "buffer": { 65 | "version": "4.9.1", 66 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", 67 | "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", 68 | "requires": { 69 | "base64-js": "^1.0.2", 70 | "ieee754": "^1.1.4", 71 | "isarray": "^1.0.0" 72 | } 73 | }, 74 | "comma-number": { 75 | "version": "2.0.0", 76 | "resolved": "https://registry.npmjs.org/comma-number/-/comma-number-2.0.0.tgz", 77 | "integrity": "sha1-JN+oPOlY1oqdIa9I4g5S3AcnBL0=" 78 | }, 79 | "events": { 80 | "version": "1.1.1", 81 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 82 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" 83 | }, 84 | "ieee754": { 85 | "version": "1.1.8", 86 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", 87 | "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" 88 | }, 89 | "isarray": { 90 | "version": "1.0.0", 91 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 92 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 93 | }, 94 | "jmespath": { 95 | "version": "0.15.0", 96 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", 97 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" 98 | }, 99 | "moment": { 100 | "version": "2.23.0", 101 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.23.0.tgz", 102 | "integrity": "sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA==" 103 | }, 104 | "punycode": { 105 | "version": "1.3.2", 106 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 107 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 108 | }, 109 | "querystring": { 110 | "version": "0.2.0", 111 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 112 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 113 | }, 114 | "sax": { 115 | "version": "1.2.1", 116 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 117 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" 118 | }, 119 | "url": { 120 | "version": "0.10.3", 121 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 122 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 123 | "requires": { 124 | "punycode": "1.3.2", 125 | "querystring": "0.2.0" 126 | } 127 | }, 128 | "uuid": { 129 | "version": "3.3.2", 130 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 131 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 132 | }, 133 | "xml2js": { 134 | "version": "0.4.19", 135 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", 136 | "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", 137 | "requires": { 138 | "sax": ">=0.6.0", 139 | "xmlbuilder": "~9.0.1" 140 | } 141 | }, 142 | "xmlbuilder": { 143 | "version": "9.0.7", 144 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", 145 | "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /models/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "business insights", 5 | "intents": [ 6 | { 7 | "name": "AMAZON.FallbackIntent", 8 | "samples": [] 9 | }, 10 | { 11 | "name": "AMAZON.CancelIntent", 12 | "samples": [] 13 | }, 14 | { 15 | "name": "AMAZON.HelpIntent", 16 | "samples": [] 17 | }, 18 | { 19 | "name": "AMAZON.StopIntent", 20 | "samples": [] 21 | }, 22 | { 23 | "name": "AMAZON.NavigateHomeIntent", 24 | "samples": [] 25 | }, 26 | { 27 | "name": "SalesTrend", 28 | "slots": [ 29 | { 30 | "name": "When", 31 | "type": "WhenType" 32 | }, 33 | { 34 | "name": "Period", 35 | "type": "PeriodType" 36 | }, 37 | { 38 | "name": "PIN", 39 | "type": "AMAZON.FOUR_DIGIT_NUMBER" 40 | } 41 | ], 42 | "samples": [ 43 | "How was Sales {When} {Period}", 44 | "How is Sales doing", 45 | "How is the Sales growth" 46 | ] 47 | }, 48 | { 49 | "name": "FinancialTrend", 50 | "slots": [ 51 | { 52 | "name": "When", 53 | "type": "WhenType" 54 | }, 55 | { 56 | "name": "Period", 57 | "type": "PeriodType" 58 | }, 59 | { 60 | "name": "PIN", 61 | "type": "AMAZON.FOUR_DIGIT_NUMBER" 62 | } 63 | ], 64 | "samples": [ 65 | "What was the Net Profit Margin {When} {Period}", 66 | "How is the Net profit Margin" 67 | ] 68 | }, 69 | { 70 | "name": "SignOut", 71 | "slots": [], 72 | "samples": [ 73 | "Log out", 74 | "Sign out", 75 | "Sign me off", 76 | "End Session" 77 | ] 78 | }, 79 | { 80 | "name": "UnusedIntent", 81 | "slots": [ 82 | { 83 | "name": "When", 84 | "type": "WhenType" 85 | } 86 | ], 87 | "samples": [ 88 | "{When}" 89 | ] 90 | }, 91 | { 92 | "name": "AMAZON.YesIntent", 93 | "samples": [] 94 | }, 95 | { 96 | "name": "AMAZON.NoIntent", 97 | "samples": [] 98 | } 99 | ], 100 | "types": [ 101 | { 102 | "name": "WhenType", 103 | "values": [ 104 | { 105 | "name": { 106 | "value": "Last", 107 | "synonyms": [ 108 | "Previous" 109 | ] 110 | } 111 | } 112 | ] 113 | }, 114 | { 115 | "name": "PeriodType", 116 | "values": [ 117 | { 118 | "name": { 119 | "value": "Year", 120 | "synonyms": [ 121 | "Year" 122 | ] 123 | } 124 | }, 125 | { 126 | "name": { 127 | "value": "Month", 128 | "synonyms": [ 129 | "Month" 130 | ] 131 | } 132 | } 133 | ] 134 | } 135 | ] 136 | }, 137 | "dialog": { 138 | "intents": [ 139 | { 140 | "name": "UnusedIntent", 141 | "confirmationRequired": false, 142 | "prompts": {}, 143 | "slots": [ 144 | { 145 | "name": "When", 146 | "type": "WhenType", 147 | "confirmationRequired": false, 148 | "elicitationRequired": true, 149 | "prompts": { 150 | "elicitation": "Elicit.Slot.798851683430.824164465241" 151 | } 152 | } 153 | ] 154 | } 155 | ], 156 | "delegationStrategy": "ALWAYS" 157 | }, 158 | "prompts": [ 159 | { 160 | "id": "Elicit.Slot.798851683430.824164465241", 161 | "variations": [ 162 | { 163 | "type": "PlainText", 164 | "value": "This Intent is only created to enable Dialog Model with a required Slot" 165 | } 166 | ] 167 | } 168 | ] 169 | } 170 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alexa Skill Authentication Sample 2 | 3 | ## About the Alexa skill 4 | 5 | This Alexa Skill provides users the ability to get flash briefings on Company's KPI's. The Skill demonstrates how to use AWS Lambda and SNS to enable Voice + SMS based Authentication within a Skill. You will also learn to Publish this Skill as a Private skill to an Alexa for Business account rather than the public Alexa Skills Store. 6 | 7 | ## Skill Deployment 8 | This demo assumes you have your developer environment ready to go and that you have some familiarity with CLI (Command Line Interface)Tools, AWS, and the ASK Developer Portal. 9 | 10 | ### Pre-requisites 11 | 12 | * Node.js (> v8) 13 | * Register for an [AWS Account](https://aws.amazon.com/) 14 | * Register for an [Amazon Developer Account](https://www.amazon.com/ap/signin?clientContext=133-6034629-5039663&openid.return_to=https%3A%2F%2Fdeveloper.amazon.com%2F&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.assoc_handle=mas_dev_portal&openid.mode=checkid_setup&marketPlaceId=ATVPDKIKX0DER&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&pageId=amzn_developer_portal&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&siteState=clientContext%3D145-5959710-0079802%2CsourceUrl%3Dhttps%253A%252F%252Fdeveloper.amazon.com%252F%2Csignature%3D2Hg5BEEq0ZTVj2FnmzejmIGaPLwIsj3D&language=en_US) 15 | * Install and Setup [ASK CLI](https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html) 16 | 17 | ### Installation 18 | 1. **Clone** the repository. 19 | 20 | ```bash 21 | $ git clone https://github.com/aws-samples/alexa-skill-authentication 22 | ``` 23 | 24 | 2. If it's your first time using it, **initiatialize** the ASK CLI by navigating into the repository and running ask command: `ask init`. Follow the prompts. This will initialize credentials profile and help you configure credentials to access Alexa Skills Kit Developer Console and the AWS Console. 25 | 26 | ```bash 27 | $ cd alexa-skill-authentication 28 | $ ask init 29 | ``` 30 | 31 | 3. Install npm dependencies by navigating into the `/lambda/custom` directory and running the npm command: `npm install` 32 | 33 | ```bash 34 | $ cd lambda/custom 35 | $ npm install 36 | ``` 37 | 38 | ### Deploy the Skill 39 | 40 | 1. Deploy the skill by running the following command in the `alexa-skill-authentication` folder. 41 | 42 | ```bash 43 | $ ask deploy 44 | ``` 45 | The Skill would be deployed. Ignore any errors related to enabling the skill. You will Enable it in the subsequent steps. Make a note of the Skill Id as shown in the output below. 46 | 47 | ```bash 48 | Skill Id: amzn1.ask.skill.fede81a9-09a7-4b46-85f3-1fea4a96f 49 | Skill deployment finished. 50 | Model deployment finished. 51 | [Info]: No lambda functions need to be deployed. 52 | [Info]: No in-skill product to be deployed. 53 | ``` 54 | 55 | 2. In the root directory `alexa-skill-authentication`, you should see a file named 'deploy-lambda.sh'. This will let you deploy the Skill's backend code as a Lambda function. Ensure you edit this file to replace the name of the S3 bucket as found in your AWS account. 56 | 57 | 3. Deploy the lambda function by executing the `deploy-lambda.sh` file (Change permissions of this file by executing the command `chmod +x deploy-lambda.sh` if need be.). Make note of the name of the Lambda function and its ARN. You will need it in subsequent steps. 58 | 59 | 4. Configure the Alexa Skill Kit as a Trigger to the Lambda function by running this command. 60 | 61 | 62 | ```bash 63 | $ aws lambda add-permission \ 64 | --function-name LAMBDA_FUNCTION_NAME \ 65 | --statement-id 123 \ 66 | --action lambda:InvokeFunction \ 67 | --principal alexa-appkit.amazon.com \ 68 | --event-source-token SKILL_ID 69 | ``` 70 | **Note**: Replace the LAMBDA_FUNCTION_NAME and SKILL_ID values appropriately. 71 | 72 | 5. In the root directory `alexa-skill-authentication`, edit the `skill_final.json` by replacing the ARN of the Lambda function created in Step 3. Look for the place holder LAMBDA_ARN in this file. 73 | 74 | 6. Replace the contents of the `skill.json` with the contents in `skill_final.json`. 75 | 76 | 7. Re-deploy the skill by running the following command. 77 | 78 | ```bash 79 | $ ask deploy --force 80 | ``` 81 | The Skill should be deployed and linked to the Lambda function. 82 | 83 | ```bash 84 | Skill Id: amzn1.ask.skill.fede81a9-09a7-4b46-85f3-1fea4a96f 85 | Skill deployment finished. 86 | Model deployment finished. 87 | [Info]: No lambda functions need to be deployed. 88 | [Info]: No in-skill product to be deployed. 89 | Your skill is now deployed and enabled in the development stage. Try simulate your Alexa skill skill using "ask dialog" command. 90 | ``` 91 | 92 | 8. Test the Skill. Refer to ```Testing the Skill``` section for Instructions on testing the skill. 93 | 94 | 9. Submit the skill. The submission process is similar to certification for public skills. Just issue the following command: 95 | ```bash 96 | ask api submit -s SKILL_ID 97 | ``` 98 | **Note**: Replace the value of SKILL_ID. 99 | 100 | This submission process may take about 90 minutes to complete and once completed the skill will be available in the live stage. 101 | 102 | ### Distribute as Private Skill 103 | Ensure that the Skill's Status is `Live` before executing this step. The final step is to distribute the skill to an Alexa for Business organization. To do this, you’ll need the ARN of the AWS account for the organization which you want to deploy the skill. Then enter the following command: 104 | 105 | ```bash 106 | $ ask api add-private-distribution-account -s SKILL_ID --stage live --account-id 107 | ``` 108 | 109 | For example 110 | 111 | ```bash 112 | ask api add-private-distribution-account -s amzn1.ask.skill.fede81a9-09a7-4b46-85f3-1fea4a96f --stage live --account-id arn:aws:iam::1234567890:root 113 | ``` 114 | 115 | ### Testing the Skill 116 | 117 | 1. Invoke the Skill by saying "Alexa, Open Business Insights" 118 | 2. Then ask "How is the Net profit Margin?". Alexa will respond with 119 | "Your mobile number is not registered with this device. Please contact the skill administrator." 120 | 3. You will need to locate the **DeviceId** and map it to your Mobile Number in a DynamoDB Table. 121 | 4. To locate the DeviceId, login to the AWS Management Console. Choose Lambda and click on Functions in the left navigation pane. Locate the Lambda function configured with this Skill. 122 | 5. Choose the Lambda function and click on the **Monitoring** tab. 123 | 6. Click on **View logs in CloudWatch** and click on the first "Log Stream". 124 | 7. Look for the **DeviceId** in the logs. It starts with the prefix "amzn1.ask.device.". Note this DeviceId. 125 | 8. Navigate to **DynamoDB** in the AWS Management Console. 126 | 9. Choose **Tables** in the left navigation pane. 127 | 10. Locate the **DeviceContactMapping** table and click on **CreateItem**. 128 | 11. Add the value of DeviceId into the Id Attribute and also add a new attribute called PhoneNumber. Enter your Mobile Number starting with the CountryCode.Click on **Save** to save this Item. 129 | 130 | ![SaveItem](images/DeviceMapping.png) 131 | 132 | 13. Invoke the Skill by saying "Alexa, Open Business Insights" 133 | 14. Then ask "How is the Net profit Margin?". 134 | 15. The Alexa Skill will send a 4 Digit PIN via SMS. The Skill will prompt you to Utter the 4 Digit PIN. 135 | 16. Provide a valid PIN and Alexa should now respond with the KPI Information. 136 | 137 | 138 | ## License 139 | 140 | This library is licensed under the Amazon Software License. 141 | 142 | 143 | -------------------------------------------------------------------------------- /lambda/custom/Handlers/auth.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Amazon Software License 3 | // http://aws.amazon.com/asl/ 4 | const AWS = require('aws-sdk'); 5 | const moment = require('moment'); 6 | const sms = require('./sms.js'); 7 | 8 | // Set the region 9 | AWS.config.update({ region: process.env.AWSREGION }); 10 | const dynamoDb = new AWS.DynamoDB.DocumentClient(); 11 | const TABLE_NAME = 'UserAuth' 12 | const SMS_TABLE_NAME = 'DeviceContactMapping' 13 | 14 | const PIN_VALIDTY_IN_MINS = process.env.PIN_TIMEOUT_IN_MINS; 15 | 16 | function putAuthInfo(deviceId, pin) { 17 | const creationTime = moment.utc(new Date()).valueOf() 18 | const expirationTime = moment.utc(moment(new Date()).add(PIN_VALIDTY_IN_MINS, 'm').toDate()).valueOf(); 19 | 20 | var params = { 21 | TableName: TABLE_NAME, 22 | Item: { 23 | id: deviceId, 24 | generatedPIN: pin, 25 | CreationTime: creationTime, 26 | ExpirationTime: expirationTime, 27 | PINValidated: 'No' 28 | } 29 | }; 30 | // return dynamo result directly 31 | return dynamoDb.put(params).promise(); 32 | } 33 | 34 | function updateAuthInfo(deviceId, generatedPIN, pinValidated, creationTime, expirationTime) { 35 | 36 | var params = { 37 | TableName: TABLE_NAME, 38 | Item: { 39 | id: deviceId, 40 | generatedPIN: generatedPIN, 41 | CreationTime: creationTime, 42 | ExpirationTime: expirationTime, 43 | PINValidated: pinValidated 44 | } 45 | }; 46 | // return dynamo result directly 47 | return dynamoDb.put(params).promise(); 48 | } 49 | 50 | 51 | async function getAuthInfo(deviceId) { 52 | const params = {}; 53 | params.TableName = TABLE_NAME; 54 | const key = { id: deviceId }; 55 | params.Key = key; 56 | 57 | // Return the Item from DDB 58 | return dynamoDb.get(params).promise(); 59 | } 60 | 61 | async function deleteAuthInfo(deviceId) { 62 | const params = {}; 63 | params.TableName = TABLE_NAME; 64 | const key = { id: deviceId }; 65 | params.Key = key; 66 | 67 | // Return the Item from DDB 68 | return dynamoDb.delete(params).promise(); 69 | } 70 | 71 | function generatePin() { 72 | return (Math.floor(Math.random() * 10000) + 10000).toString().substring(1); 73 | } 74 | 75 | function getPhoneNumber(deviceId) { 76 | 77 | params = { 78 | TableName: SMS_TABLE_NAME, 79 | Key: { 80 | id: deviceId 81 | } 82 | }; 83 | 84 | // post-process dynamo result before returning 85 | return dynamoDb.get(params).promise(); 86 | } 87 | 88 | async function isPinValid(deviceId, pin) { 89 | // 1. If no record exists in DDB for this DeviceId - Generate a new Token and store in DDB with PINValidated set to 'NO'. Send SMS to User. 90 | // User may enter the PIN immediately or after some time. 91 | const authResponse = await getAuthInfo(deviceId); 92 | const authInfo = authResponse.Item; 93 | if (authInfo) { 94 | // If PIN matches, make sure to update DDB 95 | console.log(`Gen pin ${authInfo.generatedPIN}, entered pin ${pin}`); 96 | pin = pin.length === 3 ? '0'+pin: pin; 97 | 98 | if (authInfo.generatedPIN === pin) { 99 | console.log(`PIN Matches .. Updating DDB`); 100 | await updateAuthInfo(deviceId, authInfo.generatedPIN, 'Yes', authInfo.CreationTime, authInfo.ExpirationTime); 101 | console.log(`PIN Matches .. Updated DDB`); 102 | return new Promise((resolve, reject) => { 103 | resolve(true); 104 | }); 105 | } 106 | } 107 | return new Promise((resolve, reject) => { 108 | resolve(false); 109 | }); 110 | 111 | 112 | } 113 | 114 | async function authenticate(deviceId) { 115 | // 1. If no record exists in DDB for this DeviceId - Generate a new Token and store in DDB with PINValidated set to 'NO'. Send SMS to User. 116 | // Prompt user for a PIN. User may enter the PIN immediately or after some time. 117 | console.log('calling getAuthInfo'); 118 | const authResponse = await getAuthInfo(deviceId); 119 | const authInfo = authResponse.Item; 120 | console.log(`authInfo = ${authInfo}`); 121 | 122 | if (!authInfo) { 123 | // Get Phone Number based on DeviceId 124 | const res = await getPhoneNumber(deviceId); 125 | console.log(JSON.stringify(res.Item)); 126 | if (!res.Item) { 127 | return new Promise((resolve, reject) => { 128 | resolve({ action: 'EndSession' }); 129 | }); 130 | } 131 | console.log(`Creating new entry as no Auth record found for device ${deviceId}`); 132 | 133 | // Generate PIN 134 | const pin = generatePin(); 135 | 136 | // Save in DDB 137 | await putAuthInfo(deviceId, pin); 138 | console.log('New Entry created'); 139 | 140 | const phoneNumber = res.Item.PhoneNumber; 141 | console.log(`Obtained phone number from Mapping Table ${phoneNumber}`); 142 | 143 | //Send SMS 144 | await sms.sendSMS(phoneNumber, pin); 145 | console.log(`Sent SMS to ${phoneNumber}`); 146 | 147 | // Prompt the User to enter the PIN if the PIN hasn't been validated yet 148 | return new Promise((resolve, reject) => { 149 | resolve({ action: 'PromptForPin' }); 150 | }); 151 | } 152 | else { 153 | console.log(`Auth record was found.Checking if its valid`); 154 | /* 155 | 2. DDB get based on DeviceId/UserId. If record exists and PINValidated is 'No', check if previous Token has expired. 156 | If yes,Generate a new Token and store in DDB with PINValidated set to 'NO'. Send SMS to user. 157 | If no, prompt the user for the PIN. If wrong PIN entered, re-prompt. If valid PIN entered, update DDB PINValidated and speechText 158 | Session attribute PINValidated = 'Yes'. Continue. 159 | */ 160 | const currentDatetime = moment.utc(new Date()).valueOf(); 161 | const currentTime = moment(currentDatetime); 162 | const expTime = moment(authInfo.ExpirationTime); 163 | 164 | differenceInMs = expTime.diff(currentTime); // diff yields milliseconds 165 | duration = moment.duration(differenceInMs); // moment.duration accepts ms 166 | differenceInMinutes = duration.asMinutes(); // Diference in Current and Expiration time in Minutes 167 | 168 | console.log(`PIN expires in ${differenceInMinutes} minutes`); 169 | 170 | // Check if the PINValidated is 'No'. User either never tried the PIN or provided wrong PIN 171 | if (authInfo.PINValidated === 'No') { 172 | console.log(`PIN has not been validated yet`); 173 | // Get Phone Number based on DeviceId 174 | const res = await getPhoneNumber(deviceId); 175 | console.log(JSON.stringify(res.Item)); 176 | if (!res.Item) { 177 | return new Promise((resolve, reject) => { 178 | resolve({ action: 'EndSession' }); 179 | }); 180 | } 181 | const phoneNumber = res.Item.PhoneNumber; 182 | console.log(`Obtained phone number from Mapping Table ${phoneNumber}`); 183 | 184 | // if the Previous PIN has expired, generate a new one and send SMS 185 | if (differenceInMinutes < 0) { 186 | console.log(`PIN has expired. Generating a new one and updating DDB`); 187 | // Generate PIN 188 | const pin = generatePin(); 189 | 190 | // Save in DDB 191 | await putAuthInfo(deviceId, pin); 192 | console.log('New Entry created'); 193 | 194 | //Send SMS 195 | await sms.sendSMS(phoneNumber, pin); 196 | console.log(`Sent SMS to ${phoneNumber}`); 197 | console.log(`PIN had expired. Generated a new one and updated DDB, sent SMS`); 198 | } 199 | console.log(`Returning a PromptForPIN action`); 200 | // Prompt the User to enter the PIN if the PIN hasn't been validated yet 201 | return new Promise((resolve, reject) => { 202 | resolve({ action: 'PromptForPin' }); 203 | }); 204 | 205 | } 206 | else { 207 | /* 208 | 3. DDB get based on DeviceId/UserId. If record exists and PINValidated is 'Yes', check if previous Token has expired. 209 | If yes,Generate a new Token and store in DDB with PINValidated set to 'NO'. Send SMS to user. 210 | If no, Session attribute PINValidated = 'Yes'. 211 | */ 212 | 213 | console.log(`PIN has ben Validated.`); 214 | 215 | // If Previous PIN was validated but has expired, generate a new one and send SMS 216 | if (differenceInMinutes < 0) { 217 | console.log(`PIN has expired. Generating a new one`); 218 | // Get Phone Number based on DeviceId 219 | const res = await getPhoneNumber(deviceId); 220 | console.log(JSON.stringify(res.Item)); 221 | if (!res.Item) { 222 | return new Promise((resolve, reject) => { 223 | resolve({ action: 'EndSession' }); 224 | }); 225 | } 226 | const phoneNumber = res.Item.PhoneNumber; 227 | 228 | // Generate PIN 229 | const pin = generatePin(); 230 | 231 | // Save in DDB 232 | await putAuthInfo(deviceId, pin); 233 | 234 | //Send SMS 235 | await sms.sendSMS(phoneNumber, pin); 236 | console.log(`Obtained phone number from Mapping Table`); 237 | console.log(`Returning a PromptForPin action`); 238 | // Prompt the User to enter the PIN 239 | return new Promise((resolve, reject) => { 240 | resolve({ action: 'PromptForPin' }); 241 | }); 242 | 243 | } else { 244 | console.log(`PIN has been validated and has not expired. Returning a Continue action`); 245 | // If Previous PIN was validated but has expired, nothing to do !! 246 | return new Promise((resolve, reject) => { 247 | resolve({ action: 'Continue' }); 248 | }); 249 | } 250 | 251 | } 252 | } 253 | } 254 | 255 | exports.authenticate = authenticate; 256 | exports.isPinValid = isPinValid; 257 | exports.deleteAuthInfo = deleteAuthInfo; -------------------------------------------------------------------------------- /lambda/custom/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Amazon Software License 3 | // http://aws.amazon.com/asl/ 4 | const Alexa = require("ask-sdk"); 5 | const authMgr = require('./Handlers/auth.js'); 6 | const speechHandler = require('./Handlers/speechHandler.js'); 7 | 8 | const LaunchRequest_Handler = { 9 | canHandle(handlerInput) { 10 | const request = handlerInput.requestEnvelope.request; 11 | return request.type === "LaunchRequest"; 12 | }, 13 | async handle(handlerInput) { 14 | const responseBuilder = handlerInput.responseBuilder; 15 | return responseBuilder 16 | .speak("Welcome to Business Insights. How can I help you?") 17 | .reprompt("Try asking me about your company's sales growth") 18 | .getResponse(); 19 | } 20 | }; 21 | 22 | const SalesTrend_Handler = { 23 | canHandle(handlerInput) { 24 | const request = handlerInput.requestEnvelope.request; 25 | return request.intent.name === "SalesTrend"; 26 | }, 27 | async handle(handlerInput) { 28 | const request = handlerInput.requestEnvelope.request; 29 | const responseBuilder = handlerInput.responseBuilder; 30 | const currentIntent = request.intent; 31 | 32 | const slotValues = getSlotValues(currentIntent.slots); 33 | console.log(JSON.stringify(slotValues)); 34 | let speechResponse; 35 | 36 | ///// SECURITY CODE 37 | let wasPINProvidedAndValidated = false; 38 | const deviceId = 39 | handlerInput.requestEnvelope.context.System.device.deviceId; 40 | console.log(`deviceId = ${JSON.stringify(deviceId)}`); 41 | 42 | // If PIN Slot is not filled 43 | if (!slotValues.PIN.heardAs) { 44 | console.log("To be authenticated."); 45 | const result = await authMgr.authenticate(deviceId); 46 | 47 | if (result.action === "EndSession") { 48 | console.log("No Mapping Mobile Number found for the device"); 49 | return speechHandler.noMobileNumberRegistered(handlerInput); 50 | } 51 | 52 | if (result.action !== "Continue") { 53 | console.log("Not authenticated. PIN Reqd"); 54 | return speechHandler.promptForPin(handlerInput); 55 | } 56 | } else { 57 | const pinValid = await authMgr.isPinValid(deviceId, slotValues.PIN.heardAs); 58 | 59 | if (!pinValid) { 60 | console.log("PIN is Invalid"); 61 | return speechHandler.promptForInvalidPin(handlerInput); 62 | } 63 | wasPINProvidedAndValidated = true; 64 | } 65 | ///// SECURITY CODE END /////////////////// 66 | 67 | // User wants Year to date Sales Growth 68 | if (!slotValues.When.heardAs && !slotValues.Period.heardAs) { 69 | speechResponse = 70 | "Good news!! Sales growth has been strong so far. There has been a 10% increase compared to last year"; 71 | return speechHandler.promptWithValidPin(handlerInput, speechResponse, wasPINProvidedAndValidated) 72 | } 73 | 74 | // User wants to know Sales growth Last Year or Month 75 | if (slotValues.When.resolved && slotValues.Period.resolved) { 76 | speechResponse = `Sales growth ${slotValues.When.resolved} ${slotValues.Period.resolved} compared to current ${slotValues.Period.resolved} was lower by 7%.`; 77 | return speechHandler.promptWithValidPin(handlerInput, speechResponse, wasPINProvidedAndValidated) 78 | } 79 | } 80 | }; 81 | 82 | const FinancialTrend_Handler = { 83 | canHandle(handlerInput) { 84 | const request = handlerInput.requestEnvelope.request; 85 | return request.intent.name === "FinancialTrend"; 86 | }, 87 | async handle(handlerInput) { 88 | const request = handlerInput.requestEnvelope.request; 89 | const responseBuilder = handlerInput.responseBuilder; 90 | const currentIntent = request.intent; 91 | 92 | const slotValues = getSlotValues(currentIntent.slots); 93 | console.log(JSON.stringify(slotValues)); 94 | let speechResponse; 95 | 96 | ///// SECURITY CODE 97 | const deviceId = 98 | handlerInput.requestEnvelope.context.System.device.deviceId; 99 | console.log(`deviceId = ${JSON.stringify(deviceId)}`); 100 | 101 | let wasPINProvidedAndValidated = false; 102 | if (!slotValues.PIN.heardAs) { 103 | console.log("To be authenticated."); 104 | const result = await authMgr.authenticate(deviceId); 105 | 106 | if (result.action === "EndSession") { 107 | console.log("No Mapping Mobile Number found for the device"); 108 | return speechHandler.noMobileNumberRegistered(handlerInput); 109 | } 110 | 111 | if (result.action !== "Continue") { 112 | console.log("Not authenticated. PIN Reqd"); 113 | return speechHandler.promptForPin(handlerInput); 114 | } 115 | } else { 116 | const pinValid = await authMgr.isPinValid( 117 | deviceId, 118 | slotValues.PIN.heardAs 119 | ); 120 | if (!pinValid) { 121 | console.log("PIN is Invalid"); 122 | return speechHandler.promptForInvalidPin(handlerInput); 123 | } 124 | wasPINProvidedAndValidated = true; 125 | } 126 | ///// SECURITY CODE END 127 | 128 | // User wants to know Net Profit Margin till date for current year 129 | if (!slotValues.When.heardAs && !slotValues.Period.heardAs) { 130 | speechResponse = "Its looking good ! Net profit margin is at 60%."; 131 | return speechHandler.promptWithValidPin(handlerInput, speechResponse, wasPINProvidedAndValidated) 132 | } 133 | // User wants to know Net Profit Margin Lst year or Month 134 | if (slotValues.When.resolved && slotValues.Period.resolved) { 135 | speechResponse = `Net profit margin ${slotValues.When.resolved} ${ 136 | slotValues.Period.resolved 137 | } compared to current ${slotValues.Period.resolved} was lower by 15%.`; 138 | return speechHandler.promptWithValidPin(handlerInput, speechResponse, wasPINProvidedAndValidated) 139 | } 140 | 141 | } 142 | }; 143 | 144 | const SignOutIntent_Handler = { 145 | canHandle(handlerInput) { 146 | const request = handlerInput.requestEnvelope.request; 147 | return ( 148 | request.type === "IntentRequest" && request.intent.name === "SignOut" 149 | ); 150 | }, 151 | async handle(handlerInput) { 152 | const responseBuilder = handlerInput.responseBuilder; 153 | console.log(`Original Request was: ${JSON.stringify(handlerInput.requestEnvelope.request, null, 2)}`); 154 | 155 | const deviceId = handlerInput.requestEnvelope.context.System.device.deviceId; 156 | await authMgr.deleteAuthInfo(deviceId); 157 | 158 | return responseBuilder 159 | .speak(`Okay, I have signed you out. Talk to you later!`) 160 | .withShouldEndSession(true) 161 | .getResponse(); 162 | } 163 | }; 164 | 165 | const AMAZON_FallbackIntent_Handler = { 166 | canHandle(handlerInput) { 167 | const request = handlerInput.requestEnvelope.request; 168 | return ( 169 | request.type === "IntentRequest" && 170 | request.intent.name === "AMAZON.FallbackIntent" 171 | ); 172 | }, 173 | handle(handlerInput) { 174 | const responseBuilder = handlerInput.responseBuilder; 175 | console.log( 176 | `Original Request was: ${JSON.stringify( 177 | handlerInput.requestEnvelope.request, 178 | null, 179 | 2 180 | )}` 181 | ); 182 | 183 | return responseBuilder 184 | .speak("Please check with your admin for this information.") 185 | .getResponse(); 186 | } 187 | }; 188 | 189 | const AMAZON_CancelIntent_Handler = { 190 | canHandle(handlerInput) { 191 | const request = handlerInput.requestEnvelope.request; 192 | return ( 193 | request.type === "IntentRequest" && 194 | request.intent.name === "AMAZON.CancelIntent" 195 | ); 196 | }, 197 | handle(handlerInput) { 198 | const responseBuilder = handlerInput.responseBuilder; 199 | let say = "Okay, talk to you later! "; 200 | 201 | return responseBuilder 202 | .speak(say) 203 | .withShouldEndSession(true) 204 | .getResponse(); 205 | } 206 | }; 207 | 208 | const AMAZON_HelpIntent_Handler = { 209 | canHandle(handlerInput) { 210 | const request = handlerInput.requestEnvelope.request; 211 | return ( 212 | request.type === "IntentRequest" && 213 | request.intent.name === "AMAZON.HelpIntent" 214 | ); 215 | }, 216 | handle(handlerInput) { 217 | const responseBuilder = handlerInput.responseBuilder; 218 | 219 | return responseBuilder 220 | .speak("Try asking me about your company's sales growth") 221 | .reprompt("Try asking me about your company's sales growth") 222 | .getResponse(); 223 | } 224 | }; 225 | 226 | const AMAZON_StopIntent_Handler = { 227 | canHandle(handlerInput) { 228 | const request = handlerInput.requestEnvelope.request; 229 | return ( 230 | request.type === "IntentRequest" && 231 | request.intent.name === "AMAZON.StopIntent" 232 | ); 233 | }, 234 | handle(handlerInput) { 235 | const responseBuilder = handlerInput.responseBuilder; 236 | let say = "Okay, talk to you later! "; 237 | 238 | return responseBuilder 239 | .speak(say) 240 | .withShouldEndSession(true) 241 | .getResponse(); 242 | } 243 | }; 244 | 245 | const SessionEndedHandler = { 246 | canHandle(handlerInput) { 247 | const request = handlerInput.requestEnvelope.request; 248 | return request.type === "SessionEndedRequest"; 249 | }, 250 | handle(handlerInput) { 251 | console.log( 252 | `Session ended with reason: ${ 253 | handlerInput.requestEnvelope.request.reason 254 | }` 255 | ); 256 | return handlerInput.responseBuilder.getResponse(); 257 | } 258 | }; 259 | 260 | const ErrorHandler = { 261 | canHandle() { 262 | return true; 263 | }, 264 | handle(handlerInput, error) { 265 | const request = handlerInput.requestEnvelope.request; 266 | console.log(`Error handled: ${error}`); 267 | console.log(`Original Request was: ${JSON.stringify(request, null, 2)}`); 268 | 269 | return handlerInput.responseBuilder 270 | .speak("Please check with your admin for this information.") 271 | .getResponse(); 272 | } 273 | }; 274 | 275 | function getSlotValues(filledSlots) { 276 | const slotValues = {}; 277 | 278 | Object.keys(filledSlots).forEach(item => { 279 | const name = filledSlots[item].name; 280 | 281 | if ( 282 | filledSlots[item] && 283 | filledSlots[item].resolutions && 284 | filledSlots[item].resolutions.resolutionsPerAuthority[0] && 285 | filledSlots[item].resolutions.resolutionsPerAuthority[0].status && 286 | filledSlots[item].resolutions.resolutionsPerAuthority[0].status.code 287 | ) { 288 | switch ( 289 | filledSlots[item].resolutions.resolutionsPerAuthority[0].status.code 290 | ) { 291 | case "ER_SUCCESS_MATCH": 292 | slotValues[name] = { 293 | heardAs: filledSlots[item].value, 294 | resolved: 295 | filledSlots[item].resolutions.resolutionsPerAuthority[0].values[0] 296 | .value.name, 297 | ERstatus: "ER_SUCCESS_MATCH" 298 | }; 299 | break; 300 | case "ER_SUCCESS_NO_MATCH": 301 | slotValues[name] = { 302 | heardAs: filledSlots[item].value, 303 | resolved: "", 304 | ERstatus: "ER_SUCCESS_NO_MATCH" 305 | }; 306 | break; 307 | default: 308 | break; 309 | } 310 | } else { 311 | slotValues[name] = { 312 | heardAs: filledSlots[item] != null ? filledSlots[item].value : "", // may be null 313 | resolved: "", 314 | ERstatus: "" 315 | }; 316 | } 317 | }, this); 318 | 319 | return slotValues; 320 | } 321 | 322 | const YesIntent = { 323 | canHandle(handlerInput) { 324 | const request = handlerInput.requestEnvelope.request; 325 | return request.type === 'IntentRequest' && request.intent.name === 'AMAZON.YesIntent'; 326 | }, 327 | handle(handlerInput) { 328 | const responseBuilder = handlerInput.responseBuilder; 329 | console.log(`Original Request was: ${JSON.stringify(handlerInput.requestEnvelope.request, null, 2)}`); 330 | 331 | return responseBuilder 332 | .speak(`Okay, Try asking me about your company's sales growth.`) 333 | .reprompt('You can ask me how Sales is doing or about net profit margin. What do you prefer?') 334 | .getResponse(); 335 | }, 336 | }; 337 | 338 | const NoIntent = { 339 | canHandle(handlerInput) { 340 | const request = handlerInput.requestEnvelope.request; 341 | return request.type === 'IntentRequest' && request.intent.name === 'AMAZON.NoIntent'; 342 | }, 343 | async handle(handlerInput) { 344 | const responseBuilder = handlerInput.responseBuilder; 345 | console.log(`Original Request was: ${JSON.stringify(handlerInput.requestEnvelope.request, null, 2)}`); 346 | 347 | return responseBuilder 348 | .speak('Okay, talk to you later!') 349 | .getResponse(); 350 | }, 351 | }; 352 | 353 | // 4. Exports handler function and setup =================================================== 354 | const skillBuilder = Alexa.SkillBuilders.standard(); 355 | exports.handler = skillBuilder 356 | .addRequestHandlers( 357 | AMAZON_FallbackIntent_Handler, 358 | AMAZON_CancelIntent_Handler, 359 | AMAZON_HelpIntent_Handler, 360 | AMAZON_StopIntent_Handler, 361 | LaunchRequest_Handler, 362 | SessionEndedHandler, 363 | SignOutIntent_Handler, 364 | SalesTrend_Handler, 365 | FinancialTrend_Handler, 366 | YesIntent, 367 | NoIntent 368 | ) 369 | .addErrorHandlers(ErrorHandler) 370 | .lambda(); 371 | --------------------------------------------------------------------------------