├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Modules ├── authorization-path.js ├── callback-path.js ├── common.js ├── device-path.js └── token-path.js ├── README.md ├── Resources └── index.html ├── docs ├── CF-Template-deployment.md ├── Detailled-flow.md └── Manual-deployment.md ├── img ├── ACM-Locating-certificate.png ├── CF-Out-DNS.png ├── CF-Out.png ├── CF-Parameters.png ├── CF-Stack.png ├── CF-ack.png ├── CF-complete.png ├── CF-console.png ├── Certificate-ARN.png ├── Certificate-Manager-Console.png ├── Grant_Device_flow_v1.drawio ├── Grant_Device_flow_v1.jpg ├── Grant_Device_flow_v2.drawio ├── Grant_Device_flow_v2.jpg ├── Grant_Device_flow_v3.drawio ├── Grant_Device_flow_v3.jpg ├── Grant_Device_flow_v4.drawio ├── Grant_Device_flow_v4.jpg ├── Grant_Device_flow_v5.drawio ├── Grant_Device_flow_v5.jpg ├── Lambda-Configuration.png ├── Lambda-Console.png ├── Lambda-Function.png ├── Lambda-Variables.png ├── enduser_UI.jpg └── enduser_success.jpg ├── index.js ├── package-lock.json ├── package.json └── template └── CFT-DeviceGrantFlowDemo-latest.yml /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 | -------------------------------------------------------------------------------- /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, or recently closed, 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 *main* 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' 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](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /Modules/authorization-path.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | //Reference to Commnon library 19 | const common = require( __dirname + '/common.js'); 20 | 21 | const { CognitoIdentityProvider } = require("@aws-sdk/client-cognito-identity-provider"); 22 | const { DynamoDB } = require("@aws-sdk/client-dynamodb"); 23 | 24 | //Reference to Crypto library for PKCE challenge 25 | const crypto = require('crypto'); 26 | 27 | var cognitoidentityserviceprovider = new CognitoIdentityProvider({ 28 | // The transformation for apiVersions is not implemented. 29 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 30 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 31 | // The transformation for apiVersions is not implemented. 32 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 33 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 34 | apiVersions: { 35 | cognitoidentityserviceprovider: '2016-04-18', 36 | }, 37 | }); 38 | var dynamodb = new DynamoDB({ 39 | // The transformation for apiVersions is not implemented. 40 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 41 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 42 | // The transformation for apiVersions is not implemented. 43 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 44 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 45 | apiVersions: { 46 | dynamodb: '2012-08-10', 47 | }, 48 | }); 49 | 50 | //Function that processes "Authorize" by an authenticated end user for a valid user code 51 | // client_id: Client ID of the client application that initiated the Authorization request 52 | // device_code: Primary key of the "Authorized" Authorization request in the DynamoDB table 53 | // callback: Callback function to return the message 54 | // dynamodb: Pointer to the DynamoDB SDK request handler 55 | function processAllow(client_id, device_code, callback) { 56 | 57 | //Generating a code verifier and challenge for the PKCE protection of the OAuth2 flow 58 | var code_verifier = common.randomString(32, 'aA#'); 59 | var hash = crypto.createHash('sha256').update(code_verifier).digest(); 60 | var code_challenge = common.base6UurlEncode(hash); 61 | 62 | //Generating a random state for preventing against CSRF attacks 63 | var state = common.randomString(32, 'aA#'); 64 | 65 | //Updating the Authorization request with PKCE code verifier and State 66 | var DynamoDBParams = { 67 | ExpressionAttributeNames: { 68 | "#AuthZ_State": "AuthZ_State", 69 | "#AuthZ_Verif": "AuthZ_Verifier_code", 70 | }, 71 | ExpressionAttributeValues: { 72 | ":authz_state": { 73 | S: state 74 | }, 75 | ":authz_verif": { 76 | S: code_verifier 77 | } 78 | }, 79 | Key: { 80 | "Device_code": { 81 | S: device_code 82 | } 83 | }, 84 | ReturnValues: "ALL_NEW", 85 | TableName: process.env.DYNAMODB_TABLE, 86 | UpdateExpression: "SET #AuthZ_State = :authz_state, #AuthZ_Verif = :authz_verif" 87 | }; 88 | dynamodb.updateItem(DynamoDBParams, function(err, data) { 89 | if (err) { 90 | //There was an error updating the Authorization request 91 | console.log("Unable to set Authorization State and Verifier Code for Device Code = " + device_code); 92 | common.returnHTMLError(400, "

Error, can't update status

", callback); 93 | } 94 | else { 95 | //Update was successful so triggering a standard Authorization Code Grant flow with PKCE to Cognito using the inial Client Application's Client ID 96 | var response = { 97 | statusCode: 302, 98 | headers: {"location": "https://" + process.env.CUP_DOMAIN + ".auth." + process.env.CUP_REGION + ".amazoncognito.com/oauth2/authorize?response_type=code&client_id=" + client_id + "&redirect_uri=" + encodeURIComponent("https://" + process.env.CODE_VERIFICATION_URI + "/callback") + "&state=" + state + "&scope=" + data.Attributes.Scope.S + "&code_challenge_method=S256&code_challenge=" + code_challenge + "&identity_provider=COGNITO"}, 99 | }; 100 | callback(null, response); 101 | } 102 | }); 103 | } 104 | 105 | module.exports = Object.assign({ processAllow }); -------------------------------------------------------------------------------- /Modules/callback-path.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | //Reference to Commnon library 19 | const common = require( __dirname + '/common.js'); 20 | 21 | const { CognitoIdentityProvider } = require("@aws-sdk/client-cognito-identity-provider"); 22 | const { DynamoDB } = require("@aws-sdk/client-dynamodb"); 23 | 24 | var cognitoidentityserviceprovider = new CognitoIdentityProvider({ 25 | // The transformation for apiVersions is not implemented. 26 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 27 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 28 | // The transformation for apiVersions is not implemented. 29 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 30 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 31 | apiVersions: { 32 | cognitoidentityserviceprovider: '2016-04-18', 33 | }, 34 | }); 35 | var dynamodb = new DynamoDB({ 36 | // The transformation for apiVersions is not implemented. 37 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 38 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 39 | // The transformation for apiVersions is not implemented. 40 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 41 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 42 | apiVersions: { 43 | dynamodb: '2012-08-10', 44 | }, 45 | }); 46 | 47 | //Function that processes Cognito callback after end user Authorization Code grant flow with PKCE request 48 | // event: Full event trapped by the Lambda function 49 | // callback: Callback function to return the message 50 | function processAuthZCodeCallback(event, callback) { 51 | console.log("An Authorization Code has been sent back as Callback"); 52 | 53 | //Search the DynamoDb table for Authorization Request with provided State 54 | var DynamoDBParams = { 55 | ExpressionAttributeValues: { 56 | ":authz_state": { 57 | S: event.queryStringParameters.state 58 | } 59 | }, 60 | KeyConditionExpression: "AuthZ_State = :authz_state", 61 | IndexName: process.env.DYNAMODB_AUTHZ_STATE_INDEX, 62 | TableName: process.env.DYNAMODB_TABLE 63 | }; 64 | dynamodb.query(DynamoDBParams, function(err, data) { 65 | if (err) { 66 | //There was an error retrieving the Authorization request 67 | console.log("Authorization State can't be retrieved: " + event.queryStringParameters.state); 68 | console.log(err, err.stack); 69 | common.returnHTMLError(400, "

Error, can't update status

", callback); 70 | } else { 71 | console.log("Successful response"); 72 | //If there is no result set 73 | if (data.Items.length == 0) { 74 | console.log("No AuthZ State was returned"); 75 | common.returnHTMLError(400, "

Error, can't update status

", callback); 76 | //If Result Set is more than 1 entry 77 | } else if (data.Items.length > 1) { 78 | console.log("Too much AuthZ State were returned"); 79 | common.returnHTMLError(400, "

Error, can't update status

", callback); 80 | } else { 81 | console.log("AuthZ State was returned"); 82 | // Updating the Authorization request with the Code returned through the Authorization Code grant flow with PKCE callback 83 | DynamoDBParams = { 84 | ExpressionAttributeNames: { 85 | "#AuthZ_code": "AuthZ_code" 86 | }, 87 | ExpressionAttributeValues: { 88 | ":value": { 89 | S: event.queryStringParameters.code 90 | } 91 | }, 92 | Key: { 93 | "Device_code": { 94 | S: data.Items[0].Device_code.S 95 | } 96 | }, 97 | ReturnValues: "ALL_NEW", 98 | TableName: process.env.DYNAMODB_TABLE, 99 | UpdateExpression: "SET #AuthZ_code = :value" 100 | }; 101 | dynamodb.updateItem(DynamoDBParams, function(err, data) { 102 | if (err) { 103 | //Update was not successful 104 | console.log("Unable to set state to Authorization Code for Device Code"); 105 | console.log(err, err.stack); 106 | common.returnHTMLError(400, "

Error, can't update status

", callback); 107 | } 108 | else { 109 | //Update was successful 110 | console.log("AuthZ Code updated"); 111 | common.returnHTMLSuccess("

Thanks, Device has been Authorized. You can return to your device.

", callback); 112 | } 113 | }); 114 | } 115 | } 116 | }); 117 | } 118 | 119 | module.exports = Object.assign({ processAuthZCodeCallback }); -------------------------------------------------------------------------------- /Modules/common.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | const { CognitoIdentityProvider } = require("@aws-sdk/client-cognito-identity-provider"); 20 | const { DynamoDB } = require("@aws-sdk/client-dynamodb"); 21 | 22 | var cognitoidentityserviceprovider = new CognitoIdentityProvider({ 23 | // The transformation for apiVersions is not implemented. 24 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 25 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 26 | // The transformation for apiVersions is not implemented. 27 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 28 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 29 | apiVersions: { 30 | cognitoidentityserviceprovider: '2016-04-18', 31 | }, 32 | }); 33 | var dynamodb = new DynamoDB({ 34 | // The transformation for apiVersions is not implemented. 35 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 36 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 37 | // The transformation for apiVersions is not implemented. 38 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 39 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 40 | apiVersions: { 41 | dynamodb: '2012-08-10', 42 | }, 43 | }); 44 | 45 | //Function a random string based of the required lenght and format 46 | // length: length of the random string to generate 47 | // client_id: format of the randrom string to generate 48 | // result: string 49 | function randomString(length, chars) { 50 | var mask = ''; 51 | if (chars.indexOf('a') > -1) mask += 'abcdefghijklmnopqrstuvwxyz'; 52 | if (chars.indexOf('b') > -1) mask += 'bcdfghjklmnpqrstvwxz'; 53 | if (chars.indexOf('A') > -1) mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 54 | if (chars.indexOf('B') > -1) mask += 'BCDFGHJKLMNPQRSTVWXZ'; 55 | if (chars.indexOf('#') > -1) mask += '0123456789'; 56 | if (chars.indexOf('!') > -1) mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\'; 57 | var result = ''; 58 | for (var i = length; i > 0; --i) result += mask[Math.floor(Math.random() * mask.length)]; 59 | return result; 60 | } 61 | 62 | //Function that generates cookie value 63 | // result: Cookie value for the domain, valid 5 minutes, and secure 64 | function generateCookieVal() { 65 | //Generate cookie 66 | var date = new Date(); 67 | // Valid for 5 minutes 68 | date.setTime(+ date + (3000)); // 5 \* 60 \* 100 69 | var cookieVal = Math.random().toString(36).substring(7); 70 | 71 | return "myCookie="+cookieVal+"; HttpOnly; Secure; SameSite=Strict; Domain=" + process.env.CODE_VERIFICATION_URI + "; Expires="+date.toGMTString()+";"; 72 | } 73 | 74 | //Function that performs Base64 decoding for URL 75 | // encoded: The Base64 URL encoded value 76 | // result: The decoded value 77 | function base64UrlDecode(encoded) { 78 | encoded = encoded.replace('-', '+').replace('_', '/'); 79 | while (encoded.length % 4) 80 | encoded += '='; 81 | return base64Decode(encoded); 82 | } 83 | 84 | //Function that performs Base64 decoding 85 | // encoded: The Base64 encoded value 86 | // result: The decoded value 87 | function base64Decode(encoded) { 88 | return new Buffer.from(encoded || '', 'base64').toString('utf8'); 89 | } 90 | 91 | //Function that performs Base64 encoding for URL 92 | // unencoded: The decoded value 93 | // result: The Base64 URL encoded value 94 | function base6UurlEncode(unencoded) { 95 | var encoded = base64Encode(unencoded); 96 | return encoded.replace('+', '-').replace('/', '_').replace(/=+$/, ''); 97 | } 98 | 99 | //Function that performs Base64 encoding 100 | // unencoded: The decoded value 101 | // result: The Base64 encoded value 102 | function base64Encode(unencoded) { 103 | return new Buffer.from(unencoded || '').toString('base64'); 104 | } 105 | 106 | //Function that returns an error code as a JSON message 107 | // code: Error code to return 108 | // callback: Callback function to return the message 109 | function returnJSONError(code, callback) { 110 | var response = { 111 | statusCode: code, 112 | headers: {"content-type": "application/json", "cache-control": "no-store"} 113 | }; 114 | callback(null, response); 115 | } 116 | 117 | //Function that returns an error code as a JSON message with a body 118 | // code: Error code to return 119 | // message: Body of the JSON message 120 | // callback: Callback function to return the message 121 | function returnJSONErrorWithMsg(code, message, callback) { 122 | var msg = { 123 | "error": message 124 | }; 125 | var response = { 126 | statusCode: 400, 127 | headers: {"content-type": "application/json", "cache-control": "no-store"}, 128 | body: JSON.stringify(msg), 129 | }; 130 | callback(null, response); 131 | } 132 | 133 | //Function that returns an error code as a HTML message with a body 134 | // code: Error code to return 135 | // HTMLvalue: Body of the HTML message 136 | // callback: Callback function to return the message 137 | function returnHTMLError(code, HTMLvalue, callback) { 138 | var response = { 139 | statusCode: code, 140 | headers: {"content-type": "text/html", "cache-control": "no-store"}, 141 | body: HTMLvalue 142 | }; 143 | callback(null, response); 144 | } 145 | 146 | //Function that returns a specific "Device Code has expired" JSON message 147 | // callback: Callback function to return the message 148 | function returnExpiredDeviceCodeError(callback) { 149 | var msg = { 150 | "error": "expired_token" 151 | }; 152 | var response = { 153 | statusCode: 400, 154 | headers: {"content-type": "application/json", "cache-control": "no-store"}, 155 | body: JSON.stringify(msg), 156 | }; 157 | callback(null, response); 158 | } 159 | 160 | //Function that returns a specific "User Code has expired" HTML message 161 | // callback: Callback function to return the message 162 | function returnExpiredUserCodeError(callback) { 163 | var response = { 164 | statusCode: 400, 165 | headers: {"content-type": "text/html", "cache-control": "no-store"}, 166 | body: "

Sorry, code has expired

" 167 | }; 168 | callback(null, response); 169 | } 170 | 171 | //Function that returns a specific "Slow down" JSON message when client is polling too frequently for a status 172 | // callback: Callback function to return the message 173 | function returnSlowDownError(callback) { 174 | var msg = { 175 | "error": "slow_down" 176 | }; 177 | var response = { 178 | statusCode: 400, 179 | headers: {"content-type": "application/json", "cache-control": "no-store"}, 180 | body: JSON.stringify(msg), 181 | }; 182 | callback(null, response); 183 | } 184 | 185 | //Function that returns a generic SUCCESS JSON message 186 | // callback: Callback function to return the message 187 | function returnJSONSuccess(JSONvalue, callback) { 188 | var response = { 189 | statusCode: 200, 190 | headers: {"content-type": "application/json", "cache-control": "no-store"}, 191 | body: JSON.stringify(JSONvalue), 192 | }; 193 | callback(null, response); 194 | } 195 | 196 | //Function that returns a generic SUCCESS JSON message 197 | // callback: Callback function to return the message 198 | function returnHTMLSuccess(HTMLvalue, callback) { 199 | var response = { 200 | statusCode: 200, 201 | headers: {"content-type": "text/html", "cache-control": "no-store"}, 202 | body: HTMLvalue 203 | }; 204 | callback(null, response); 205 | } 206 | 207 | module.exports = Object.assign({ cognitoidentityserviceprovider }, { dynamodb }, { randomString }, { generateCookieVal }, { base64UrlDecode }, { base64Decode }, { base6UurlEncode }, { base64Encode }, { returnJSONError }, { returnJSONErrorWithMsg }, { returnHTMLError }, { returnExpiredDeviceCodeError }, { returnExpiredUserCodeError }, { returnSlowDownError } ,{ returnJSONSuccess }, { returnHTMLSuccess }); -------------------------------------------------------------------------------- /Modules/device-path.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | //Reference to Commnon library 19 | const common = require( __dirname + '/common.js'); 20 | 21 | const { CognitoIdentityProvider } = require("@aws-sdk/client-cognito-identity-provider"); 22 | const { DynamoDB } = require("@aws-sdk/client-dynamodb"); 23 | 24 | //Reference to Authorization path library 25 | const authzP = require( __dirname + '/authorization-path.js'); 26 | 27 | //Require a filesystem object to read the HTML page dedicated to end user UI 28 | var fs = require("fs"); 29 | 30 | var cognitoidentityserviceprovider = new CognitoIdentityProvider({ 31 | // The transformation for apiVersions is not implemented. 32 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 33 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 34 | // The transformation for apiVersions is not implemented. 35 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 36 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 37 | apiVersions: { 38 | cognitoidentityserviceprovider: '2016-04-18', 39 | }, 40 | }); 41 | var dynamodb = new DynamoDB({ 42 | // The transformation for apiVersions is not implemented. 43 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 44 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 45 | // The transformation for apiVersions is not implemented. 46 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 47 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 48 | apiVersions: { 49 | dynamodb: '2012-08-10', 50 | }, 51 | }); 52 | 53 | //Function that processes request by an authenticated end user with a user code 54 | // event: Full event trapped by the Lambda function 55 | // callback: Callback function to return the message 56 | function requestUserCodeProcessing(event, callback) { 57 | //Search for an Authorization request related to the provided user code 58 | var DynamoDBParams = { 59 | ExpressionAttributeValues: { 60 | ":User_code": { 61 | S: event.queryStringParameters.code 62 | } 63 | }, 64 | KeyConditionExpression: "User_code = :User_code", 65 | IndexName: process.env.DYNAMODB_USERCODE_INDEX, 66 | TableName: process.env.DYNAMODB_TABLE 67 | }; 68 | dynamodb.query(DynamoDBParams, function(err, data) { 69 | if (err) { 70 | //There was an error retrieving the Authorization request 71 | console.log("User code does not exist: " + event.queryStringParameters.code); 72 | console.log(err, err.stack); 73 | common.returnExpiredUserCodeError(callback); 74 | } else { 75 | console.log("successful response"); 76 | //If no result is returned 77 | if (data.Items.length == 0) { 78 | console.log("no User code was returned"); 79 | common.returnExpiredUserCodeError(callback); 80 | //If too much result is returned 81 | } else if (data.Items.length > 1) { 82 | console.log("Too much User code returned from the request"); 83 | common.returnExpiredUserCodeError(callback); 84 | //If only one result is returned 85 | } else { 86 | var Device_code_ctx = data.Items[0].Device_code.S; 87 | //If the Authorization request is already expired, authorized, or denied 88 | if (data.Items[0].Status.S == "expired" || data.Items[0].Status.S == "authorized" || data.Items[0].Status.S == "denied") { 89 | console.log("The Device code has already expired or been used"); 90 | common.returnExpiredUserCodeError(callback); 91 | //If the Authorization request has not the expired status but has a lifetime that is greater than the maximum one 92 | } else if (Date.now() > parseInt(data.Items[0].Max_expiry.S)) { 93 | console.log("User Code has expired"); 94 | //Update the Authorization request to expire 95 | DynamoDBParams = { 96 | ExpressionAttributeNames: { 97 | "#Status": "Status" 98 | }, 99 | ExpressionAttributeValues: { 100 | ":status": { 101 | S: "expired" 102 | } 103 | }, 104 | Key: { 105 | "Device_code": { 106 | S: Device_code_ctx 107 | } 108 | }, 109 | ReturnValues: "ALL_NEW", 110 | TableName: process.env.DYNAMODB_TABLE, 111 | UpdateExpression: "SET #Status = :status" 112 | }; 113 | dynamodb.updateItem(DynamoDBParams, function(err, data) { 114 | if (err) { 115 | //There was an error updating the Authorization request 116 | console.log("User Code has expired but an error occurend when updating the DB"); 117 | console.log(err, err.stack); 118 | common.returnExpiredUserCodeError(callback); 119 | } else { 120 | //Update was successfull, we return an HTML message to the end-user 121 | console.log("User Code has expired and DB has been updated"); 122 | common.returnExpiredUserCodeError(callback); 123 | } 124 | }); 125 | //If the code has not been redeemed and is still valid 126 | } else { 127 | console.log("User Code is valid and action is Authorize = " + event.queryStringParameters.authorize ); 128 | //Retrieving the OIDC authenticated user attributes set by ALB 129 | var payload = common.base64UrlDecode(event.headers["x-amzn-oidc-data"].split('.')[1]); 130 | //If the end-user "Authorized" the Authorization request 131 | if (event.queryStringParameters.authorize == 'true') { 132 | //Update the Status and Subject of the Authorization request 133 | DynamoDBParams = { 134 | ExpressionAttributeNames: { 135 | "#Status": "Status", 136 | "#Subject": "Subject" 137 | }, 138 | ExpressionAttributeValues: { 139 | ":status": { 140 | S: "authorized" 141 | }, 142 | ":subject": { 143 | S: JSON.parse(payload).username 144 | } 145 | }, 146 | Key: { 147 | "Device_code": { 148 | S: Device_code_ctx 149 | } 150 | }, 151 | ReturnValues: "ALL_NEW", 152 | TableName: process.env.DYNAMODB_TABLE, 153 | UpdateExpression: "SET #Status = :status, #Subject = :subject" 154 | }; 155 | dynamodb.updateItem(DynamoDBParams, function(err, data) { 156 | if (err) { 157 | //There was an error updating the Authorization request 158 | console.log("Unable to set state to autorized for User Code"); 159 | console.log(err, err.stack); 160 | common.returnHTMLError(400, "

Error, can't update status

", callback); 161 | } else { 162 | //Update was successfull, follwoing up with the Authroization path 163 | authzP.processAllow(data.Attributes.Client_id.S, data.Attributes.Device_code.S, callback, dynamodb); 164 | } 165 | }); 166 | //If the end-user "Denied" the Authorization request 167 | } else if (event.queryStringParameters.authorize == 'false') { 168 | console.log("User Code is valid and action is Authorize = " + event.queryStringParameters.authorize ); 169 | //Update the Status and Subject of the Authorization request 170 | DynamoDBParams = { 171 | ExpressionAttributeNames: { 172 | "#Status": "Status", 173 | "#Subject": "Subject" 174 | }, 175 | ExpressionAttributeValues: { 176 | ":status": { 177 | S: "denied" 178 | }, 179 | ":subject": { 180 | S: JSON.parse(payload).username 181 | } 182 | }, 183 | Key: { 184 | "Device_code": { 185 | S: Device_code_ctx 186 | } 187 | }, 188 | ReturnValues: "ALL_NEW", 189 | TableName: process.env.DYNAMODB_TABLE, 190 | UpdateExpression: "SET #Status = :status, #Subject = :subject" 191 | }; 192 | dynamodb.updateItem(DynamoDBParams, function(err, data) { 193 | if (err) { 194 | //There was an error updating the Authorization request 195 | console.log("Unable to set state to autorized for User Code"); 196 | common.returnHTMLError(400, "

Error, can't update status

", callback); 197 | } 198 | else { 199 | //Update was successfull, returning an HTML SUCCESS message 200 | common.returnHTMLSuccess("

Thanks, Device has been unauthorized.

", callback); 201 | } 202 | }); 203 | //If the operation is not supported 204 | } else { 205 | console.log("Unsupported Authorization option"); 206 | common.returnHTMLError(400, "

Error, can't update status

", callback); 207 | } 208 | } 209 | } 210 | } 211 | }); 212 | } 213 | 214 | //Function that processes a request to show the Authorization UI 215 | // event: Full event trapped by the Lambda function 216 | // callback: Callback function to return the message 217 | function requestUI(event, callback){ 218 | //Retrieving the OIDC authenticated user attributes set by ALB 219 | var payload = common.base64UrlDecode(event.headers["x-amzn-oidc-data"].split('.')[1]); 220 | 221 | //Reading the HTML page 222 | fs.readFile('Resources/index.html', 'utf8', function(err, data) { 223 | if (err) { 224 | //There was an error reading the page, returning an HTML error 225 | console.log("Error reading Resources/index.html"); 226 | console.log(err, err.stack); 227 | common.returnHTMLError(500, "", callback); 228 | } else { 229 | console.log("Success reading Resources/index.html " + data); 230 | //Sucessful, returning page and setting the username correctly 231 | var response = { 232 | statusCode: 200, 233 | headers: {"content-type": "text/html", "cache-control": "no-store"}, 234 | body: data.replace("$Username", JSON.parse(payload).username) 235 | }; 236 | callback(null, response); 237 | } 238 | }); 239 | } 240 | 241 | module.exports = Object.assign({ requestUserCodeProcessing }, { requestUI }); -------------------------------------------------------------------------------- /Modules/token-path.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | //Reference to Commnon library 19 | const common = require( __dirname + '/common.js'); 20 | 21 | const { CognitoIdentityProvider } = require("@aws-sdk/client-cognito-identity-provider"); 22 | const { DynamoDB } = require("@aws-sdk/client-dynamodb"); 23 | 24 | //Reference to https library for retrieving JWT tokens from Cognito using the Athorization Code grant flow with PKCE 25 | const https = require('https'); 26 | 27 | var cognitoidentityserviceprovider = new CognitoIdentityProvider({ 28 | // The transformation for apiVersions is not implemented. 29 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 30 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 31 | // The transformation for apiVersions is not implemented. 32 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 33 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 34 | apiVersions: { 35 | cognitoidentityserviceprovider: '2016-04-18', 36 | }, 37 | }); 38 | var dynamodb = new DynamoDB({ 39 | // The transformation for apiVersions is not implemented. 40 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 41 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 42 | // The transformation for apiVersions is not implemented. 43 | // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. 44 | // Please create/upvote feature request on aws-sdk-js-codemod for apiVersions. 45 | apiVersions: { 46 | dynamodb: '2012-08-10', 47 | }, 48 | }); 49 | 50 | //Function to process a POST request pm /token 51 | // event: Full event trapped by the Lambda function 52 | // callback: Callback function to return the message 53 | function processPostRequest(event, callback) { 54 | 55 | //Preparing the request to acquire Cognito Client App configuration 56 | var params = { 57 | ClientId: event.queryStringParameters.client_id, 58 | UserPoolId: process.env.CUP_ID, 59 | }; 60 | 61 | //Acquiring Cognito Client App configuration 62 | cognitoidentityserviceprovider.describeUserPoolClient(params, function(err, data) { 63 | if (err) { 64 | console.log("There was an error acquiring the Cognito Client App configuration " + event.queryStringParameters.client_id); 65 | console.log(err, err.stack); 66 | common.returnJSONError(401, callback); 67 | } else { 68 | console.log("Acquired Cognito Client App Configuration"); 69 | //Configuration has been acquired 70 | // An Authorization header has been provided, so this is a OAuth2 private client 71 | if (event.headers.authorization && event.headers.authorization != '') { 72 | console.log("this is a Private Client"); 73 | //If it is a Basic Authentication value in the Authorization header 74 | if (event.headers.authorization.startsWith("Basic ")){ 75 | console.log("Private Client has a Basic Authorizaiton header") 76 | var HeaderClientAppId = common.base64Decode(event.headers.authorization.replace("Basic ", "")).split(':')[0]; 77 | var HeaderClientAppSecret = common.base64Decode(event.headers.authorization.replace("Basic ", "")).split(':')[1]; 78 | 79 | //Check if there is no credentials abuse 80 | if (HeaderClientAppId == event.queryStringParameters.client_id && HeaderClientAppId != "") { 81 | //Check if header matches the Cognito Client App Configuration 82 | if (HeaderClientAppId == data.UserPoolClient.ClientId && HeaderClientAppSecret == data.UserPoolClient.ClientSecret && HeaderClientAppSecret != "") { 83 | console.log("Authorization header is valid"); 84 | if (!event.queryStringParameters.device_code && !event.queryStringParameters.grant_type) { 85 | // If it is a POST on /token with valid client_id but no code parameter and no grant type, this is a request for codes 86 | requestSetOfCodes(event, callback); 87 | } else if (event.queryStringParameters.device_code && event.queryStringParameters.device_code != '' &&event.queryStringParameters.grant_type == "urn:ietf:params:oauth:grant-type:device_code") { 88 | // If it is a POST on /token with valid client_id, a code parameter, and a grant type being "urn:ietf:params:oauth:grant-type:device_code", this is a request to get JWTs with a device code 89 | requestJWTs(event, callback); 90 | } else { 91 | // If it is a POST on /token with valid client_id but missing a 92 | // code parameter or a grant type being "urn:ietf:params:oauth:grant-type:device_code", 93 | // this is a bad request 94 | console.log("POST Call on /token with valid client_id but missing code or correct grant type"); 95 | common.returnJSONError(405, callback); 96 | } 97 | } else { 98 | console.log("Authorization header is unvalid"); 99 | console.log("POST Call on /token with invalid client_id"); 100 | common.returnJSONError(401, callback); 101 | } 102 | } else { 103 | console.log("Authorization header Client Id does not match paramater Client Id"); 104 | console.log("POST Call on /token with invalid client_id"); 105 | common.returnJSONError(401, callback); 106 | } 107 | 108 | //If something else, it is not supported 109 | } else { 110 | console.log("Authorization header is using an unsupported authentication scheme"); 111 | console.log("POST Call on /token with invalid client_id"); 112 | common.returnJSONError(401, callback); 113 | } 114 | // Otherwise this is a OAuth2 public client 115 | } else { 116 | //Check if request matches the Cognito Client App Configuration 117 | if (HeaderClientAppId == data.UserPoolClient.ClientId && data.UserPoolClient.ClientSecret == "") { 118 | console.log("Cognito Client App configuration is valid"); 119 | if (!event.queryStringParameters.device_code && !event.queryStringParameters.grant_type) { 120 | // If it is a POST on /token with valid client_id but no code parameter and no grant type, this is a request for codes 121 | requestSetOfCodes(event, callback); 122 | } else if (event.queryStringParameters.device_code && event.queryStringParameters.device_code != '' && event.queryStringParameters.grant_type == "urn:ietf:params:oauth:grant-type:device_code") { 123 | // If it is a POST on /token with valid client_id, a code parameter, and a grant type being "urn:ietf:params:oauth:grant-type:device_code", this is a request to get JWTs with a device code 124 | requestJWTs(event, callback); 125 | } else { 126 | // If it is a POST on /token with valid client_id but missing a 127 | // code parameter or a grant type being "urn:ietf:params:oauth:grant-type:device_code", 128 | // this is a bad request 129 | console.log("POST Call on /token with valid client_id but missing code or correct grant type"); 130 | common.returnJSONError(405, callback); 131 | } 132 | } else { 133 | console.log("Cognito Client App configuration is a private client while request try to pass as a public client"); 134 | console.log("POST Call on /token with invalid client_id"); 135 | common.returnJSONError(401, callback); 136 | } 137 | } 138 | } 139 | }); 140 | } 141 | 142 | //Function that processes request by a client applicaiton to get codes generated 143 | // event: Full event trapped by the Lambda function 144 | // callback: Callback function to return the message 145 | function requestSetOfCodes(event, callback) { 146 | // Generating the user code (a unique code to return to the end user) and device code (a unique code for future device calls) 147 | var user_code = common.randomString(process.env.USER_CODE_LENGTH, process.env.USER_CODE_FORMAT); 148 | var device_code = common.randomString(process.env.DEVICE_CODE_LENGTH, process.env.DEVICE_CODE_FORMAT); 149 | var scope = 'openid'; 150 | 151 | // Creating a JSON structure to return codes to the device 152 | var codes = { 153 | device_code: device_code, 154 | user_code: user_code, 155 | verification_uri: "https://" + process.env.CODE_VERIFICATION_URI + "/device", 156 | verification_uri_complete: "https://" + process.env.CODE_VERIFICATION_URI + "/device?code=" + user_code + "&authorize=true", 157 | interval: parseInt(process.env.POLLING_INTERVAL), 158 | expires_in: parseInt(process.env.CODE_EXPIRATION) 159 | }; 160 | 161 | if (event.queryStringParameters.scope && event.queryStringParameters.scope != '' ) { 162 | scope = event.queryStringParameters.scope; 163 | } else { 164 | scope = 'openid'; 165 | } 166 | 167 | // Prepare the stroage of the codes in the DynamoDB table 168 | var DynamoDBParams = { 169 | Item: { 170 | "Device_code": { 171 | S: device_code 172 | }, 173 | "User_code": { 174 | S: user_code 175 | }, 176 | "Status": { 177 | S: "authorization_pending" 178 | }, 179 | "Client_id": { 180 | S: event.queryStringParameters.client_id 181 | }, 182 | "Max_expiry": { 183 | S: (Date.now() + process.env.CODE_EXPIRATION * 1000).toString() 184 | }, 185 | "Last_checked": { 186 | S: (Date.now()).toString() 187 | }, 188 | "Scope":  { 189 | S: scope 190 | } 191 | }, 192 | ReturnConsumedCapacity: "TOTAL", 193 | TableName: process.env.DYNAMODB_TABLE 194 | }; 195 | // Insert the item in DynamoDB 196 | dynamodb.putItem(DynamoDBParams, function(err, data) { 197 | if (err) { 198 | //There was an error 199 | console.log(err, err.stack); 200 | console.log("Error inserting Codes item in the DynamoDB table"); 201 | common.returnJSONError(500, callback); 202 | } else { 203 | //Successful, the Authorization request has been written to the DynamoDB table 204 | console.log("Inserting Codes item in the DynamoDB table: " + data); 205 | common.returnJSONSuccess(codes, callback); 206 | } 207 | }); 208 | } 209 | 210 | //Function that processes request to retrieve JWT tokens from Cognito using the Athorization Code grant flow with PKCE if status is "Authorized" 211 | // event: Full event trapped by the Lambda function 212 | // callback: Callback function to return the message 213 | function requestJWTs(event, callback) { 214 | //Retrieving the Authorization Request based on the device code provided by the client application 215 | var DynamoDBParams = { 216 | Key: { 217 | "Device_code": { 218 | S: event.queryStringParameters.device_code 219 | } 220 | }, 221 | TableName: process.env.DYNAMODB_TABLE 222 | }; 223 | dynamodb.getItem(DynamoDBParams, function(err, data) { 224 | if (err) { 225 | //There was an error 226 | console.log("Error occured while retrieving device code"); 227 | console.log(err, err.stack); 228 | common.returnJSONError(500, callback); 229 | } else { 230 | //Sucessful 231 | console.log("Sucessful request"); 232 | //If Result Set has no value 233 | if (data.Item.length == 0) { 234 | console.log("No item matching device code exists"); 235 | common.returnExpiredDeviceCodeError(callback); 236 | //If Result Set has more than one value 237 | } else if (data.Item.length > 1) { 238 | console.log("More than one device code has been returned"); 239 | common.returnExpiredDeviceCodeError(callback); 240 | //If Result Set has only one value but it is with an "Expired" status 241 | } else if (data.Item.Status.S == "expired") { 242 | console.log("The Device code has already expired"); 243 | common.returnExpiredDeviceCodeError(callback); 244 | //If Result Set has only one value, is not explicitely expired, but has not been requested initally by the same client application 245 | } else if (data.Item.Client_id.S != event.queryStringParameters.client_id) { 246 | console.log("The Client id does not match the initial requestor client id"); 247 | common.returnExpiredDeviceCodeError(callback); 248 | //If Result Set has only one value, is not explicitely expired, has been requested initally by the same client application, but has a lifetime older than the maximum lifetime 249 | } else if (Date.now() > parseInt(data.Item.Max_expiry.S)) { 250 | //Update status of the Authorization request to "Expired" 251 | console.log("The Device code has expired"); 252 | DynamoDBParams = { 253 | ExpressionAttributeNames: { 254 | "#Status": "Status" 255 | }, 256 | ExpressionAttributeValues: { 257 | ":status": { 258 | S: "expired" 259 | } 260 | }, 261 | Key: { 262 | "Device_code": { 263 | S: event.queryStringParameters.device_code 264 | } 265 | }, 266 | ReturnValues: "ALL_NEW", 267 | TableName: process.env.DYNAMODB_TABLE, 268 | UpdateExpression: "SET #Status = :status" 269 | }; 270 | dynamodb.updateItem(DynamoDBParams, function(err, data) { 271 | if (err) { 272 | //There was an error, so return an JSON error message the Code has expired 273 | console.log("The Device code has expired, but an error occured when updating the DB"); 274 | console.log(err, err.stack); 275 | common.returnExpiredDeviceCodeError(callback); 276 | } else { 277 | //Return an JSON error message the Code has expired 278 | common.returnExpiredDeviceCodeError(callback); 279 | } 280 | }); 281 | //If Result Set has only one value, is not expired, has been requested initally by the same client application, but application client request a status too quickly 282 | } else if (Date.now() <= (parseInt(data.Item.Last_checked.S) + parseInt(process.env.POLLING_INTERVAL) * 1000) ) { 283 | //Update last checked timestamp of the Authorization request to Now 284 | DynamoDBParams = { 285 | ExpressionAttributeNames: { 286 | "#LC": "Last_checked" 287 | }, 288 | ExpressionAttributeValues: { 289 | ":lc": { 290 | S: (Date.now()).toString() 291 | } 292 | }, 293 | Key: { 294 | "Device_code": { 295 | S: event.queryStringParameters.device_code 296 | } 297 | }, 298 | ReturnValues: "ALL_NEW", 299 | TableName: process.env.DYNAMODB_TABLE, 300 | UpdateExpression: "SET #LC = :lc" 301 | }; 302 | dynamodb.updateItem(DynamoDBParams, function(err, data) { 303 | if (err) { 304 | //There was an error, so return an JSON error message the client application has to slow down 305 | console.log("Client makes too much API calls, but an error occured while updated the last check timestamp in the DB"); 306 | console.log(err, err.stack); 307 | common.returnSlowDownError(callback); 308 | } else { 309 | //Return an JSON error message the client application has to slow down 310 | console.log("Client makes too much API calls"); 311 | common.returnSlowDownError(callback); 312 | } 313 | }); 314 | //If all is good 315 | } else { 316 | //Must check the status 317 | //But first update last checked timestamp of the Authorization request to Now 318 | DynamoDBParams = { 319 | ExpressionAttributeNames: { 320 | "#LC": "Last_checked" 321 | }, 322 | ExpressionAttributeValues: { 323 | ":lc": { 324 | S: (Date.now()).toString() 325 | } 326 | }, 327 | Key: { 328 | "Device_code": { 329 | S: event.queryStringParameters.device_code 330 | } 331 | }, 332 | ReturnValues: "ALL_NEW", 333 | TableName: process.env.DYNAMODB_TABLE, 334 | UpdateExpression: "SET #LC = :lc" 335 | }; 336 | dynamodb.updateItem(DynamoDBParams, function(err, data) { 337 | if (err) { 338 | //There was an error, so return an JSON error message the client application has to slow down 339 | console.log("Client is on time for checking, but an error occured while updated the last check timestamp in the DB"); 340 | console.log(err, err.stack); 341 | common.returnSlowDownError(callback); 342 | } 343 | else { 344 | //Sucessfull 345 | console.log("Client is on time for checking, we got a status"); 346 | //If the Status is authorization_pending or denied, return the status to the Client application 347 | if (data.Attributes.Status.S == 'authorization_pending' || data.Attributes.Status.S == 'denied') { 348 | console.log("Client is on time for checking, we got a status: " + data.Attributes.Status.S); 349 | common.returnJSONErrorWithMsg(400, data.Attributes.Status.S, callback); 350 | //If the Status is authorized 351 | } else if (data.Attributes.Status.S == 'authorized') { 352 | console.log("Client is on time for checking, we got a status: " + data.Attributes.Status.S); 353 | console.log("Token Set is empty"); 354 | //Prepare the retrieving of JWT tokens from Cognito using the Athorization Code grant flow with PKCE 355 | var options = { 356 | hostname: process.env.CUP_DOMAIN + ".auth." + process.env.CUP_REGION + ".amazoncognito.com", 357 | port: 443, 358 | path: '/oauth2/token', 359 | method: 'POST', 360 | headers: { 361 | 'Content-Type': 'application/x-www-form-urlencoded', 362 | } 363 | }; 364 | //If client application is private, has a Client Secret, and had provided it in the initial request, add it as an Authorization header to this request 365 | if (event.headers.authorization != undefined) { 366 | console.log("Setting Authorization header"); 367 | // Client knows authentication is required for Private Client, has been issued a Client secret, and therefore present an authentication header 368 | // Otherwise Client knows authentication is not necessary for Public Client or has made an error 369 | options.headers.authorization = event.headers.authorization; 370 | } 371 | 372 | console.log("Launching Request for Tokens"); 373 | //Request JWT tokens 374 | const req = https.request(options, res => { 375 | console.log('statusCode:', res.statusCode); 376 | 377 | //Reading request's response data 378 | res.on('data', (d) => { 379 | //Prepare JWT Tokens blob 380 | if (d.error) { 381 | console.log("Cognito User Pool returned an error"); 382 | common.returnExpiredDeviceCodeError(callback); 383 | } else { 384 | var result = JSON.parse(d.toString()); 385 | var response = {} 386 | 387 | var rts = process.env.RESULT_TOKEN_SET.split('+'); 388 | for (token_type in rts) { 389 | if (rts[token_type] == 'ID') response.id_token = result.id_token; 390 | if (rts[token_type] == 'ACCESS') response.access_token = result.access_token; 391 | if (rts[token_type] == 'REFRESH') response.refresh_token = result.refresh_token; 392 | } 393 | 394 | response.expires_in = result.expires_in; 395 | 396 | //Update the status of the Authorization request to "Denied" to prevent replay 397 | DynamoDBParams = { 398 | ExpressionAttributeNames: { 399 | "#Status": "Status" 400 | }, 401 | ExpressionAttributeValues: { 402 | ":status": { 403 | S: "expired" 404 | } 405 | }, 406 | Key: { 407 | "Device_code": { 408 | S: event.queryStringParameters.device_code 409 | } 410 | }, 411 | ReturnValues: "ALL_NEW", 412 | TableName: process.env.DYNAMODB_TABLE, 413 | UpdateExpression: "SET #Status = :status" 414 | }; 415 | dynamodb.updateItem(DynamoDBParams, function(err, data) { 416 | if (err) { 417 | //There was an error, return expired message as Authroization code has been used 418 | console.log("We got the tokens but we got an error updating the DB"); 419 | console.log(err, err.stack); 420 | common.returnExpiredDeviceCodeError(callback); 421 | } else { 422 | //Return the JWT tokens 423 | common.returnJSONSuccess(response, callback); 424 | } 425 | }); 426 | } 427 | }); 428 | }); 429 | 430 | //There was an error retrieving JWT Tokens 431 | req.on('error', (e) => { 432 | console.log("Got an error"); 433 | console.log(e); 434 | common.returnExpiredDeviceCodeError(callback); 435 | }); 436 | 437 | //Writing Body of the request 438 | req.write('grant_type=authorization_code&client_id=' + data.Attributes.Client_id.S + '&scope=' + data.Attributes.Scope.S + '&redirect_uri=' + encodeURIComponent("https://" + process.env.CODE_VERIFICATION_URI + '/callback') + '&code=' + data.Attributes.AuthZ_code.S + '&code_verifier=' + data.Attributes.AuthZ_Verifier_code.S); 439 | 440 | //When request is finalized 441 | req.end((e) => { 442 | console.log("Finished"); 443 | }); 444 | //If Status is not suppoted 445 | } else { 446 | common.returnExpiredDeviceCodeError(callback); 447 | } 448 | } 449 | }); 450 | } 451 | } 452 | }); 453 | } 454 | 455 | module.exports = Object.assign({ processPostRequest }, { requestSetOfCodes }, { requestJWTs }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Purpose 2 | This repository is a demonstration on how to realize Device Grant flow ([RFC 8628](https://tools.ietf.org/html/rfc8628)) using Cognito, Lambda, and DynamoDB. 3 | 4 | It is tied to an [AWS Blog Post](https://aws.amazon.com/blogs/security/implement-oauth-2-0-device-grant-flow-by-using-amazon-cognito-and-aws-lambda/) 5 | 6 | # How does it work? 7 | This new flow is implemented using: 8 | - AWS Lambda serverless functions to interact with the client application (aka the device) through an additional `/token` endpoint and the end user trough additional `/device` and `/callback` endpoints. 9 | - Amazon DynamoDB table to persist Authorization requests state and status. 10 | - Amazon Cognito to deliver the JWT tokens and to support the Authorization Code Grant flow with PKCE for end user authentication. 11 | 12 | # How to deploy it? 13 | You can choose to: 14 | - [Deploy it through a CloudFormation Template](docs/CF-Template-deployment.md) 15 | - [Deploy it manually](docs/Manual-deployment.md) using the code base of this repository 16 | 17 | # How to use it? 18 | 19 | > All the Client Application calls can be performed using Curl library, Postman client, or any HTTP request library or SDK available in the Client Application coding language. 20 | > 21 | > Note that OAuth2 Clients can be: 22 | > - Public, therefore the client only owns a `Client ID`. If so the client only has to provide the `Client ID` as parameter of the request. 23 | > - Private, therefore the client owns a `Client ID` and a `Client Secret`. If so the client has to provide the `Client ID` as parameter of the request and the Base64 encoded `Client ID:Client Secret` as an `Authorization` Header 24 | > 25 | > All the following Client Application HTTPS requests are made with the assumption it is a private OAuth2 client. 26 | 27 | While this project lets you decide how the user will ask the Client Application to start the Authorization request and will be presented with the User Code and URI for it to be verified, you can emulate the Client Application behavior by generating the following HTTPS ‘POST’ request to the ALB protected Lambda function `/token` endpoint with the appropriate HTTP Authorization header: 28 | 29 | ``` 30 | POST /token?client_id=AIDACKCEVSQ6C2EXAMPLE HTTP/1.1 31 | User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT) 32 | Host: 33 | Accept: */* 34 | Accept-Encoding: gzip, deflate 35 | Connection: Keep-Alive 36 | Authorization: Basic QUlEÉEXAMPLEQTEVLRVkg 37 | ``` 38 | 39 | The Client Application will then be returned with a JSON message: 40 | 41 | ``` 42 | Server: awselb/2.0 43 | Date: Tue, 06 Apr 2021 19:57:31 GMT 44 | Content-Type: application/json 45 | Content-Length: 33 46 | Connection: keep-alive 47 | cache-control: no-store 48 | { 49 | "device_code": "APKAEIBAERJR2EXAMPLE", 50 | "user_code": "ANPAJ2UCCR6DPCEXAMPLE", 51 | "verification_uri": "https:///device", 52 | "verification_uri_complete": "https:///device?code=ANPAJ2UCCR6DPCEXAMPLE&authorize=true", 53 | "interval": , 54 | "expires_in": 55 | } 56 | ``` 57 | 58 | To act as the user, you need to open a browser and to navigate to the `verification_uri` provided by the Client application. 59 | As this URI is protected by Cognito, you will have to authenticate with the Cognito User created previously. 60 | Note, this segment relying on Amazon Cognito, all the following situations can be handled: 61 | - If the users are already authenticated, they can benefit from automatic Single Sign-On. 62 | - If External IdP authentication is activated, they can use their social or Enterprise login. 63 | - If Advanced Security Feature are activated, they can benefit from Multi-Factor authentication 64 | Once authenticated, you will be presented with the following UI where `$Username`will be the username of the Cognito user authenticated: 65 | 66 | ![End User UI screen capture](img/enduser_UI.jpg) 67 | 68 | You will have to fill in the User Code provided by the Client application as the previous step, then click on `Authorize`. 69 | 70 | ![End User UI successful capture](img/enduser_success.jpg) 71 | 72 | While you can emulate the Client App regularly checking for the Authorization Request status with the following HTTPS ‘POST’ request to the ALB protected Lambda function `/token` endpoint with the appropriate HTTP Authorization header: 73 | 74 | ``` 75 | POST /token?client_id=AIDACKCEVSQ6C2EXAMPLE&device_code=APKAEIBAERJR2EXAMPLE&grant_type=urn:ietf:params:oauth:grant-type:device_code HTTP/1.1 76 | User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT) 77 | Host: 78 | Accept: */* 79 | Accept-Encoding: gzip, deflate 80 | Connection: Keep-Alive 81 | Authorization: Basic QUlEÉEXAMPLEQTEVLRVkg 82 | ``` 83 | 84 | Until the Authorization Request has been approved, the Client Application will be returned with `authorization_pending`, `slow_down` if the polling is too frequent, or `expired` is the maximum lifetime of the code has been reached. For example: 85 | 86 | ``` 87 | HTTP/1.1 400 Bad Request 88 | Server: awselb/2.0 89 | Date: Tue, 06 Apr 2021 20:57:31 GMT 90 | Content-Type: application/json 91 | Content-Length: 33 92 | Connection: keep-alive 93 | cache-control: no-store 94 | {"error":"authorization_pending"} 95 | ``` 96 | 97 | Once approved, the Client Application will be returned with JWT tokens: 98 | 99 | ``` 100 | HTTP/1.1 200 OK 101 | Server: awselb/2.0 102 | Date: Tue, 06 Apr 2021 21:41:50 GMT 103 | Content-Type: application/json 104 | Content-Length: 3501 105 | Connection: keep-alive 106 | cache-control: no-store 107 | {"id_token":"eyJraEXAMPLEHEADJTMjU2In0.eyJhdFEXAMPLEPAYLOADNvbSJ9.RfEzbli4EXAMPLESIG3M2Wr_Nf7BwuxdWg","access_token":"eyJraEXAMPLEHEAD2In0.eyJzEXAMPLEPAYLOADyJ9.eYEEXAMPLESIGKHLCPltw","refresh_token":"eyJjdHkiOiEXAMPLEREFRESHYdhpw","expires_in":3600} 108 | ``` 109 | 110 | The client application can now consume resources on behalf of the user thanks to the Access Token and can refresh the Access Token autonomously thanks to the Refresh Token. 111 | 112 | # Can I have more details onto the flow? 113 | 114 | [This page](docs/Detailled-flow.md) will describe the detailled flow implemented. 115 | 116 | # How to go further? 117 | 118 | This project is delivered with a default configuration. You can change the Lambda function environment variables to customize the experience provided by this Blog Post, for example: 119 | 120 | | Name | Function | Default value | Type | 121 | | ---- | ---- | ---- | ---- | 122 | | CODE_EXPIRATION | Represents the lifetime of the codes generated | 1800 | Seconds | 123 | | DEVICE_CODE_FORMAT | Represents the format for the device code | #aA | String where: `#` represents numbers, `a` lowercase letters, `A` uppercase letters, `!` special characters | 124 | | DEVICE_CODE_LENGTH | Represents the device code length | 64 | Number | 125 | | POLLING_INTERVAL | Represents the minimum time in seconds between two polling from the client application | 5 | Seconds | 126 | | USER_CODE_FORMAT | Represents the format for the user code | #B | String where: `#` represents numbers, `a` lowercase letters, `b` lowercase letters without vowels, `A` uppercase letters, `B` uppercase letters without vowels, `!` special characters | 127 | | USER_CODE_LENGTH | Represents the user code length | 8 | Number | 128 | | RESULT_TOKEN_SET | Represents what should be returned in the Token Set to the Client Application | `ACCESS+REFRESH` | String including only `ID`,`ACCESS`, and `REFRESH` values separated with `+` | 129 | 130 | To change those values: 131 | 1. From the Lambda console: 132 | 133 | ![Lambda-Console](img/Lambda-Console.png) 134 | 135 | 2. Select DeviceGrant-token function: 136 | 137 | ![Lambda-Function](img/Lambda-Function.png) 138 | 139 | 3. Go to the Configuration tab: 140 | 141 | ![Lambda-Configuration](img/Lambda-Configuration.png) 142 | 143 | 4. Select the Environment variables tab, then click Edit to change the values you want: 144 | 145 | ![Lambda-Variables](img/Lambda-Variables.png) 146 | 147 | 5. Generate new codes as the Device and see how the experience changes 148 | 149 | # Additional references 150 | 151 | - Device Grant Flow: https://www.oauth.com/oauth2-servers/device-flow/ 152 | - Cognito Endpoints: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-userpools-server-contract-reference.html 153 | - PKCE in NodeJS: https://developers.onelogin.com/openid-connect/guides/auth-flow-pkce 154 | 155 | # Security 156 | 157 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 158 | 159 | # License 160 | 161 | This library is licensed under the MIT-0 License. See the LICENSE file. 162 | 163 | # Contributor 164 | 165 | The provided solution recognized the following contributors: 166 | - @LennartC 167 | -------------------------------------------------------------------------------- /Resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 19 |
20 | 21 | 22 |
23 | 24 |
25 | 55 |
56 |

Hello $Username,

Please provide the code of the Device that wants to be linked to your account and choose an action:

57 |
58 |
59 |

60 |

61 | 62 | 63 | 64 |
65 |

66 |
67 |
68 | -------------------------------------------------------------------------------- /docs/CF-Template-deployment.md: -------------------------------------------------------------------------------- 1 | # CloudFormation template deployment 2 | The implementation requires 3 steps: 3 | 1. Defining the public Fully Qualified Domain Name for AWS ALB public endpoint and obtaining a X.509 associated to it 4 | 2. Deploying the provided AWS CloudFormation Template 5 | 3. Configuring the DNS to point to the AWS ALB public endpoint for the public Fully Qualified Domain Name 6 | 7 | ## Choosing a DNS name and storing certificate for it in AWS certificate Manager 8 | Your Lambda function endpoints will need to be publicly resolvable when exposed by AWS ALB through a HTTPS/443 Listener. In order to be able to configure the AWS ALB component: 9 | - Choose a Fully Qualified DNS Name in a DNS zone you own; 10 | - Generate the associated X.509 certificate and private key: 11 | - Directly within AWS Certificate Manager; 12 | - Alternatively, at least, imported them in AWS Certificate Manager. 13 | - Copy the ARN of this certificate in AWS Certificate Manager. 14 | For more information on “How to request a public certificate”, please refer to the [guide for AWS Certificate Manager](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html). 15 | 16 | Once imported in ACM: 17 | 1. From the ACM console: 18 | 19 | ![Certificate Manager Console](../img/Certificate-Manager-Console.png) 20 | 21 | 2. Click on the ► icon next to your certificate: 22 | 23 | ![Locating Certificate](../img/ACM-Locating-certificate.png) 24 | 25 | 3. Copy the associated ARN in a text file: 26 | 27 | ![Certificate ARN](../img/Certificate-ARN.png) 28 | 29 | ## Deploying the solution using a CloudFormation Template 30 | To configure the workshop you will need to deploy the master workshop template. 31 | > Before you deploy the CloudFormation template feel free to view it [here](../template/CFT-DeviceGrantFlowDemo-latest.yml) 32 | 33 | It is best to deploy in `us-east-1` using Cloud Formation. 34 | 35 | During the configuration, you will be asked to: 36 | - Provide a name for the stack 37 | - Provide the ARN of the certificate created/imported in AWS Certificate Manager at the previous step 38 | - Provide a prefix for the Cognito Hosted UI 39 | - Provide a valid email address you own for the Cognito User’s initial password to be sent to you; 40 | - Provide the FQDN you choose and that is associated to the certificate created/imported in AWS Certificate Manager at the previous step 41 | 42 | ![CloudFormation Parameters](../img/CF-Parameters.png) 43 | 44 | - Once configured, click on “Next” two times. Finally, on the “Review” page tick the box that authorizes CloudFormation to create IAM resources for the stack: 45 | 46 | ![CloudFormation acknowledgment](../img/CF-ack.png) 47 | 48 | - Deploy the stack by clicking on “Create stack”. 49 | - The Deployment of the CloudFormation stack will take several minutes. Wait for the result to be “CREATE_COMPLETE”. 50 | 51 | ![CloudFormation Complete](../img/CF-complete.png) 52 | 53 | ## Finalizing the configuration 54 | Once everything is set up, you have to finalize the configuration by ensuring the DNS name for the zone your own is pointing to the ALB DNS by creating an appropriate CNAME entry in your DNS system of choice. 55 | 56 | 1. From the CloudFormation console: 57 | 58 | ![CF-console](../img/CF-console.png) 59 | 60 | 2. Locate your stack and click on it: 61 | 62 | ![CF-Stack](../img/CF-Stack.png) 63 | 64 | 3. Click on the Outputs tab: 65 | 66 | ![CF-Out](../img/CF-Out.png) 67 | 68 | 4. Copy the value for the Key ALBCNAMEForDNSConfiguration: 69 | 70 | ![CF-Out-DNS](../img/CF-Out-DNS.png) 71 | 72 | 5. Configure a CNAME DNS entry into your DNS Hosted zone based on this value: 73 | 74 | > For more information on how to create a CNAME entry to AWS ALB in DNS a DNS zone, please refer to the [guide for Amazon Route 53](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-creating.html). 75 | 76 | 6. Please note the other values in the Output tab, they will serve you for the next section: 77 | 78 | | Output parameter | Usage | 79 | | ---- | ---- | 80 | | DeviceCognitoClientClientID | App Client ID to be use by the simulated Device to interact with the authorization server | 81 | | DeviceCognitoClientClientSecret | App Client Secret to be use by simulated Device to interact with the authorization server | 82 | | TestEndPointForDevice | HTTPS Endpoint for the simulated DEVICE to make their requests | 83 | | TestEndPointForUser | HTTPS Endpoint for the USER to make their requests | 84 | | UserPassword | Password for the Test Cognito User | 85 | | UserUserName | Username for the Test Cognito User | -------------------------------------------------------------------------------- /docs/Detailled-flow.md: -------------------------------------------------------------------------------- 1 | # Detailled flow 2 | > All the Client Application calls can be performed using Curl library, Postman client, or any HTTP request library or SDK available in the Client Applicaiton coding language. 3 | > 4 | > Note that OAuth2 Clients can be: 5 | > - Public, therefore the client only owns a `Client ID`. If so the client only has to provide the `Client ID` as parameter of the request. 6 | > - Private, therefore the client owns a `Client ID` and a `Client Secret`. If so the client has to provide the `Client ID` as parameter of the request and the Base64 encoded `Client ID:Client Secret` as an `Authorization` Header 7 | > 8 | > All the following HTTP requests are made with the assumption the OAuth2 client is private. 9 | 10 | ![General Flow](../img/Grant_Device_flow_v1.jpg) 11 | 12 | 1.a - [Outside of the scope of this PoC] The user interacts with the client application to check if it is already is enrolled. 13 | 14 | 1.b - [Outside of the scope of this PoC] To establish a status, the client application checks if it is in possession of a set of JWT tokens (ID, Access, and Refresh). 15 | > This flow assume that client application is not already enrolled and therefore not in possession of a set of JWT Tokens 16 | 17 | 2.a - The Client Application does a `POST` to the ALB protected Lambda function `/token` endpoint: 18 | 19 | ``` 20 | POST /token?client_id=AIDACKCEVSQ6C2EXAMPLE HTTP/1.1 21 | User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT) 22 | Host: 23 | Accept: */* 24 | Accept-Encoding: gzip, deflate 25 | Connection: Keep-Alive 26 | Authorization: Basic QUlEÉEXAMPLEQTEVLRVkg 27 | ``` 28 | 29 | 3.a - If valid, the Lambda function generates some device code and user code, 30 | 31 | 3.b - The Lambda function creates an entry in the DynamoDB table 32 | 33 | 3.c - The Lambda function returns a JSON message to the client application: 34 | 35 | ``` 36 | { 37 | "device_code": "APKAEIBAERJR2EXAMPLE", 38 | "user_code": "ANPAJ2UCCR6DPCEXAMPLE", 39 | "verification_uri": "https:///device", 40 | "verification_uri_complete": "https:///device?code=ANPAJ2UCCR6DPCEXAMPLE&authorize=true", 41 | "interval": , 42 | "expires_in": 43 | } 44 | ``` 45 | 46 | 4.a - [Outside of the scope of this PoC] The client application stores the `device_code` internally 47 | 48 | 4.b - [Outside of the scope of this PoC] The client application displays to the user the `user_code` (and if necessary the `verification_uri`) 49 | 50 | 5.a - The client application regularly checks if the Authorization request has been accepted by the user by doing a `POST` to the ALB protected Lambda function `/token` endpoint: 51 | 52 | ``` 53 | POST /token?client_id=AIDACKCEVSQ6C2EXAMPLE&device_code=APKAEIBAERJR2EXAMPLE&grant_type=urn:ietf:params:oauth:grant-type:device_code HTTP/1.1 54 | User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT) 55 | Host: 56 | Accept: */* 57 | Accept-Encoding: gzip, deflate 58 | Connection: Keep-Alive 59 | Authorization: Basic QUlEÉEXAMPLEQTEVLRVkg 60 | ``` 61 | 62 | 5.b - Then the Lambda function will return, until the Authorization request has been accepted or denied by the user, a JSON message with `authorization_pending` if the request is still valid, `expired` otherwise. For example: 63 | 64 | ``` 65 | HTTP/1.1 400 Bad Request 66 | Server: awselb/2.0 67 | Date: Tue, 06 Apr 2021 20:57:31 GMT 68 | Content-Type: application/json 69 | Content-Length: 33 70 | Connection: keep-alive 71 | cache-control: no-store 72 | {"error":"authorization_pending"} 73 | ``` 74 | 75 | > Note that if the client application is checking too quiclky the status of the Authorization request, it might receive a `slow_down` JSON message 76 | 77 | 6.a - The user will open a browser and navigate to the `verification_uri` 78 | 79 | > Note that the `verification_uri` can be customized with a Mobile Appplication handler like `myapp://`, a QRCode, or anything fitting the user context for a better experience 80 | 81 | 7.a - The `verification_uri`, being protected by Cognito User Pool, will require that the user 82 | authenticates. 83 | 84 | 7.b - The user authenticates through the Cognito User Pool 85 | 86 | > As this segment relies on Amazon Cognito, all the following situations can be handled: 87 | > - If the users are already authenticated, they can benefit from automatic Single Sign-On. 88 | > - If External IdP authentication is activated, they can use their social or Enterprise login. 89 | > - If Advanced Security Feature are activated, they can benefit from Multi-Factor authentication 90 | 91 | 7.c - The Cognito User Pool returns an Authorization code to the ALB protected Lambda function 92 | 93 | 7.d - The ALB protected Lambda function exchanges the Authorization Code for JWT tokens 94 | 95 | 8.a - Once authenticated, the user will be able to fill in the `user_code` provided by the client application and to choose to Authorize or Deny the Authorization request. 96 | 97 | ![End User UI screen capture](../img/enduser_ui.jpg) 98 | 99 | > The rest of the flow will take the assumption the user authorized the Authorization request 100 | 101 | 9.a - The Lambda function will trigger an Authorization Code grant flow with PKCE request to the Cognito User Pool on behalf of the client application for the user 102 | 103 | 9.b - The user is authenticated seamlessly through the Cognito User Pool cause a session already exists 104 | 105 | 10.a - The user is redirected to the Lambda function `callback` endpoint with an Authorization Code 106 | 107 | 10.b - The Lambda function will store the Authorization Code, the user Action (Authorized), as long as the username taking the Action (Authorized) within the Authorization request in the DynamoDB table 108 | 109 | 11.a - The next time the client application checks if the Authorization request has been accepted by the user by doing a `POST` to the ALB protected Lambda function `/token` endpoint: 110 | 111 | ``` 112 | POST /token?client_id=AIDACKCEVSQ6C2EXAMPLE&device_code=APKAEIBAERJR2EXAMPLE&grant_type=urn:ietf:params:oauth:grant-type:device_code HTTP/1.1 113 | User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT) 114 | Host: 115 | Accept: */* 116 | Accept-Encoding: gzip, deflate 117 | Connection: Keep-Alive 118 | Authorization: Basic QUlEÉEXAMPLEQTEVLRVkg 119 | ``` 120 | 121 | 11.b - Cause the Authorization Request has been 'Authorized', the Lambda function retrieves the Authorization Code stored within the Authorization request in the DynamoDB table 122 | 123 | 11.c - The Lambda function will use the client application credentials Authorization header and the Authorization Code to to call the Cognito User Pool and forward the result JWT tokens: 124 | 125 | ``` 126 | HTTP/1.1 200 OK 127 | Server: awselb/2.0 128 | Date: Tue, 06 Apr 2021 18:41:50 GMT 129 | Content-Type: application/json 130 | Content-Length: 3501 131 | Connection: keep-alive 132 | cache-control: no-store 133 | {"id_token":"eyJraEXAMPLEHEADJTMjU2In0.eyJhdFEXAMPLEPAYLOADNvbSJ9.RfEzbli4EXAMPLESIG3M2Wr_Nf7BwuxdWg","access_token":"eyJraEXAMPLEHEAD2In0.eyJzEXAMPLEPAYLOADyJ9.eYEEXAMPLESIGKHLCPltw","refresh_token":"eyJjdHkiOiEXAMPLEREFRESHYdhpw","expires_in":3600} 134 | ``` 135 | 136 | 12.a - The Lambda function returns the JWT Tokens JSON object to the Client Application 137 | 138 | 12.b - [Outside of the scope of this PoC] The client application can now consumme resources on behalf of the user thanks to the Access Token and can refresh the Access Token autonomously thanks to the Refresh Token. 139 | 140 | # What would have happened if the user has denied the Authorization request at Step 8.a? 141 | 142 | The Lambda function will have only updated the Authorization request status to `denied` in the DynamoDB table without triggering any Authorization Code grant flow with PKCE request. 143 | 144 | The next time the client application checks if the Authorization request has been accepted by the user by doing a `POST` to the ALB protected Lambda function `/token` endpoint, it will have received the `denied` status as part of a JSON message: 145 | 146 | ``` 147 | HTTP/1.1 400 Bad Request 148 | Server: awselb/2.0 149 | Date: Tue, 06 Apr 2021 20:57:31 GMT 150 | Content-Type: application/json 151 | Content-Length: 33 152 | Connection: keep-alive 153 | cache-control: no-store 154 | {"error":"authorization_pending"} 155 | ``` 156 | -------------------------------------------------------------------------------- /docs/Manual-deployment.md: -------------------------------------------------------------------------------- 1 | # Manual Deployment 2 | - Create a DynamoDB table where: 3 | - The key of the schema will be the device code 4 | - One global secondary index will index the user code 5 | - One global secondary index will index the state of the OAuth2 Authorization Code grant flow request/response 6 | - Create a new Cognito User Pool 7 | - Create an Authentication Domain 8 | - Create a new Lambda function (NodeJS based, tested with nodejs10.x) 9 | - Deploy the files in this repository 10 | - Associate an IAM Execution role that allows: 11 | - Basic Lambda Execution permissions 12 | - Access to the DynamoDB table for Read, Update, and Delete operations 13 | - Access to the Cognito User Pool as Power User 14 | - Create thirteen environment variable: 15 | - `CODE_EXPIRATION` that represents the lifetime in seconds of the codes generated 16 | - `CODE_VERIFICATION_URI` that references the URI where the end user should authorize or deny the authorization request using the user code 17 | - `CUP_DOMAIN` that references the Cognito User Pool prefixed domain name 18 | - `CUP_ID` that references the Cognito User Pool ID 19 | - `CUP_REGION` that references the Cognito User Pool region 20 | - `DEVICE_CODE_FORMAT` that represents the format for the device code (for example: `#aA` where `#` represents numbers, `a` lowercase letters, `A` uppercase letters, and `!` special characters) 21 | - `DEVICE_CODE_LENGTH` that represents the device code length 22 | - `DYNAMODB_AUTHZ_STATE_INDEX` that references the name of the global secondary index will index the state of the OAuth2 Authorization Code grant flow request/response in the DynamoDB table 23 | - `DYNAMODB_TABLE` that references the DynamoDB table 24 | - `DYNAMODB_USERCODE_INDEX` that references the name of the global secondary index will index the user code in the DynamoDB table 25 | - `POLLING_INTERVAL` that represents the minimum time in seconds between two polling from the client application 26 | - `USER_CODE_FORMAT` that represents the format for the user code (for example: `#aA` where `#` represents numbers, `a` lowercase letters, `b` lowercase letters without vowels,`A` uppercase letters, `B` uppercase letters without vowels, and `!` special characters) 27 | - `USER_CODE_LENGTH` that represents the user code length 28 | - `RESULT_TOKEN_SET` that represents the structure of the Token Set to be returned to the Device. String can only include `ID`,`ACCESS`, and `REFRESH` values separated with `+` 29 | - Create one ALB instance 30 | - Create or import in ACM one certificate and its private key that can be used to protect the HTTPS listener of the ALB instance 31 | - Configure your DNS to ensure a proper routing to the DNS name that is part of the certificate to the ALB instance 32 | - This DNS name should be in line with what configured in the Lambda function's `CODE_VERIFICATION_URI` environment variable 33 | - Create two client app credetials in Cognito User Pool with Secret: 34 | - One for an ALB instance with a callback URL as described in [ALB documentation](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html) 35 | - One for the client application with a call URL pointing to the same FQDN as in he Lambda function's `CODE_VERIFICATION_URI` environment variable but on the `/callback` endpoint 36 | - Create Target Group in EC2 that will target the Lambda function created previously 37 | - Configure the ALB instance with the following listeners: 38 | - Listener on Port `80` and protocol `HTTP` doing a default redicrect rule to the HTTPS/443 endpoint 39 | - Listener on Port `443` and protocol `HTTPS` using the certificate created or imported into ACM previously and with: 40 | - A default fixed `503` response rule 41 | - A number `1` rule matching `Path` of `/device`: 42 | - Authenticating with the Cognito User Pool created previously and using the appropriate Client Application credentials 43 | - Forward to the Target Group created previously 44 | - A number `2` rule matching `Path` of `/token` or `/callback`: 45 | - Forward to the Target Group created previously -------------------------------------------------------------------------------- /img/ACM-Locating-certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/ACM-Locating-certificate.png -------------------------------------------------------------------------------- /img/CF-Out-DNS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/CF-Out-DNS.png -------------------------------------------------------------------------------- /img/CF-Out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/CF-Out.png -------------------------------------------------------------------------------- /img/CF-Parameters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/CF-Parameters.png -------------------------------------------------------------------------------- /img/CF-Stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/CF-Stack.png -------------------------------------------------------------------------------- /img/CF-ack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/CF-ack.png -------------------------------------------------------------------------------- /img/CF-complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/CF-complete.png -------------------------------------------------------------------------------- /img/CF-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/CF-console.png -------------------------------------------------------------------------------- /img/Certificate-ARN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/Certificate-ARN.png -------------------------------------------------------------------------------- /img/Certificate-Manager-Console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/Certificate-Manager-Console.png -------------------------------------------------------------------------------- /img/Grant_Device_flow_v1.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /img/Grant_Device_flow_v1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/Grant_Device_flow_v1.jpg -------------------------------------------------------------------------------- /img/Grant_Device_flow_v2.drawio: -------------------------------------------------------------------------------- 1 | 7VxrV+M2E/41fCRHkuXbRxJIty1teQs9u9svHMdWEhfHCrYDZH/9K9nyTVJuJE5YgJ7TjceSL3pmnhmNRj4zBrOXXxJvPv2DBiQ6QyB4OTMuzxBCLkTsHy5ZFhIIDaOQTJIwELJacBv+IEIIhHQRBiRtNcwojbJw3hb6NI6Jn7VkXpLQ53azMY3ad517E6IIbn0vUqVfwyCbFlLHBLX8Cwkn0/LOEIgzM69sLATp1Avoc0NkXJ0Zg4TSrPg1exmQiI9eOS6zwX/D+9vl3eTrKHy6tuiX//3547y42HCXLtUrJCTOXn1p/Dj6fudPHoKnu7n/6NEvNxf2eYnSkxctxICdIStid+mP2I8J/3FJnkKfsEYDph3sH6Yp7DEYFBEbDdGY3bZqL0YrW5YQJHQRB4Q/BmCnn6dhRm7nns/PPjOtY7JpNovYEWQ/xzTOhBYx3TP6XhROYnbgs3cnCW8QRtGARjTJL26Mx8TyfSZPs4Q+kMaZwHZHAFQPtHH4xDA/kSQjLw3lEcP5C6EzkiVL1kScPUeG2TOLXsI8TEMM6HOta7huNW1omitaekLBJ9X1axDZD4HjDpgaUEGABMwmxCFNsimd0NiLrmppv41R3eaa0rlA5j+SZUsBjbfIaBu3NPOS7IIbLBPENCalbBjyx88vW0BUmqGhQVsCFwDHA0AHLgCm/QpwU7pIfLJm8JAtOMpLJkRc8Bn+e3Nj2A/LJPr3Nrbg71++350LRPnIrtWVhEReFj612UiHed6VDaC3bDSY0zDO0saVb7igVkFYapHQPxtIJCC1x3u2x3B9e2itbc9+FG9Ya3c1VK9X+BKzFokdjYOErgdeOs2vDztkHNeV+cZR+QbaQMc3zgH4RvsWaKULCcKnFgrW44I7y3x8z9N8gC9YA4jnL/XJ2o0UF+GN979K5Z3+HrK3BI6FHL3vqmTFffXPUkY55d3g6huncy/m4U3kpWmrz3RNJ37utW/818WCaQICqMcAB5Xz5lKahD8YGdG49uLV6+aPI49B8fBbDQ0T53C3pRrRKNm5a0LGhJmSL957mmU8eLzgaouGPJpMeyHJxj2aTPibcENGw2TsSyi3btIFP0RkXF+8g3gD2212RarxV4QwbQe1+9u+1guqtj+gkzhkwQEC/6SMKxG4YfgcbLhRdSyuBTURoeMTfUQ4ckxsdhoRGpK/RLYKkYs1EHUXDRrvKBo0ARnro0F42R/sFQ2uC/KawaB+kN9GMIjl6Qi2NoR3thxQ7N7Dctf3MPD+PbCzvsc5ROt77B14rlOPdXEnXWRRGDNdLbMbQOh5Q3vZf0N+1/4k8YKQ1OeE4UhmMLChAYeKGchxKL8RZ67QZ3brjUh0Q9Mwd/7G5YhmGZ01GlwID5Zx+1bj3TW+r0nG/JZeOi9edBy+8Ofop1Nvzk/OXiY8xdTznlPcI3GQa3OHXAwRlnTCwKZCxjouxp1xsXNyLs7j0NCX6Bi+ho49NjUfa+nYZtPKQQd0bHVCsxo2cdkMCtR/WNIj2VkX/kFcRdKRA9CM9Ukzb5dmzlkULvs3oEkCdsQzWoXR5UOGnpgAEkV52JtmMmVoINckPRQEZaBnYRDkLKYL8dvM1pkfMOVownBUP2Br8EFd4ePo8MnYiMcfDJtzZKOeLaOjWk9X6OjXRKzTeOlN7ndLP35ML30N+vfmbGiN6Rh6g3vy3XRuxDLU5gz6sby5Ad02PTt4K/e96/TrHJmSH9g0a6kWSfUdOsqWg1OoN1OtZPmN9+9BZvBC8D0XOI5dCi5fxC2Ko2Xz6IYkIXt5zmUif3CIHEP3yq9FwT208m9LeeueeucIs+soUBPUGT3f0ywDm8jFg+Hq4LYzD2bgtg07mhVgaLm6FRlY5zkO78Q+5wxvec4A5TnDyXMTaib/o8ak0JATRxY+cUhqfFrzG7ZmQ04+m+DExox1xsxGPxp5/sMHs2dbXhlwjzf714KzRbXKpzGfbNVAXa0C+LTGrM0WBaKu40OZsqNgg1VsjpsrchUIfqYVnd2A2lg8KQajOfldN9088XK5CWV1gmBDeSNP8Wzo01HS5vQrhz9vTa9GLdfWkZ66jMNsZxRssWzcqYK5m4OShmJsjBAUbxLxln0WfU5yrWwV8PO/lVFFW7+QXm8cMARF0FDs0FByU3I4EdLU7oU+jdPeIi1Keveixu19GFaWoyxdDS/SeDEIO8sWqTEp6rHjX+kd+38ZazC9f1yQlOsu8Cnf1YN4LS1fWGSDxKDLuIi9Rl5/Ooh4UMovcskbxUFTdkv8hGSKitWMBVcEGnvsFCEwMImtUyDXsg3P6nKKiKVMvw2qaUkTdV3xptVZ7KIGlpCD/k/cRJTDtigqORvwM4E2/jwsgF3BYckJHreuajsZHEhN2WKtDWaLJOYYiPpasSsrL7ktgHofmDg2rpaGTweKmhZ1OSh1bhSEszlJUhZXFewnwaUjw4GOCQVDymX6TXQnebvfvt6lH4c4kWPI+RtNpvzISnGagizzwgB986fJ4ORhLkmungiPdleu5+GeN59H3NWwR7yPqBfcj7zIi/1O2QYBS87ZujqPbBqqXpWyw+uVGoSbvZroJzQnmJwKGhkgOQRrkE65FYM3mSf0Kd+JLTmOQU09c95Gt1fo78LxH5d1GOc4AdaxjoNGhtUl6yAlM2BDeGLSMVRPZDWUI0zz2CAIE2aFedBW6slFXffHsb69/UsTs0/DGd851Xn08IYwVpKJusKzI2Osbiiwt8K4XtopbV8fRxwVW484Y+02LMt3yGjcJbbKJhXdvvwjY/tZVfiuqgods2e7wHScovYI4vbkxbB7tlRmWLyiUmao2X4gbaK35K3bBytYNNo3cjZ8DUB5sp07bPrewPmGDxp0k/w0dEuyyi5kRzMb96fEf8gTIpmXLXZIiGnngHUsJn9zRZn/7bip+b3NCoG8DGNr9kJ3RvBa4um64o6dG5rYxm7j3GUeChRzu5i7BxWmFev+zcT7m5s6aqeJCSkI9FefP0+fHRa/2q18MefpMpcrsSbSxBa2o2peKdtH8x5vbJj8Nukv/rju/07ta9f4hl5dvbuL5tnOFcC7ad4lMAdQSxDvUvMibzYKvErxFIXaVhfXFCZKztQsMxHNyhSsKh7GHSneESgPX9rs5G6Kx/5w3/0wihdQfzFjzxCM7nmkcT+j8YSyA5/O5izEHYVRmC271EvDhNJkyzotIaqpEnXuFQdSYUPrA05rvj5S72Q5a+1iqTa1rNjDop2XvG6CtM4YN1Y5QKBHs5n11MRNpWzfeZP8DRuofJxm63kSRtKlgBzcHW5jtnbI1TLbKuUqf+uoivUv5vPDxunyzBwBgNRVAp52GfK97Cr9VqTYHUFIe2SQpgS3+u6mTukOzhBqou3PQqsbWXORdvtYWMmFCqeHaovFtleReQ7Ia8mcvITZt8bvRi92VHfiB80+6h5GubppdxU5oGPZNvO2mg1P5ldsaVZmyvXYW7uVKt1WbRjoKP2GpXjeFdtGVjs8qYOBDpsc08K6xXaFA9gffJ39wR7Ip6UNG0ToZ7fCbYtYV3u2U1mhbIQuqmt4drVD+VqGbNAHMkNLLs3D61PUcvs9jZB/qbL6hHjRvP4Su3H1fw== -------------------------------------------------------------------------------- /img/Grant_Device_flow_v2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/Grant_Device_flow_v2.jpg -------------------------------------------------------------------------------- /img/Grant_Device_flow_v3.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /img/Grant_Device_flow_v3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/Grant_Device_flow_v3.jpg -------------------------------------------------------------------------------- /img/Grant_Device_flow_v4.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /img/Grant_Device_flow_v4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/Grant_Device_flow_v4.jpg -------------------------------------------------------------------------------- /img/Grant_Device_flow_v5.drawio: -------------------------------------------------------------------------------- 1 | 7V1rm5q4Hv8081KfhACBl6PW7Z6d3Z2z055e3syDEJUtEovojP30J+GikGSEUcBpdfqiJCQB8r/k978k3qDh4vm3yFnO/6QeCW404D3foNGNpiFsauw/XrNNayDUsppZ5HtZ3b7iwf9BskqQ1a59j6xKDWNKg9hflitdGobEjUt1ThTRp3KzKQ3KT106MyJVPLhOINd+8r14ntZaBtjXvyf+bJ4/GYLszsLJG2cVq7nj0adCFXp3g4YRpXF6tXgekoDPXj4vi+G/48eH7YfZp4m/uTPp+//+9aOXDjZ+TZfdJ0QkjI8e+gl+vb9H+Ns2Cr4+hCb84/2XD72MlhsnWGfzdaOZAXvIwPM37HLGL/OqSSTWsKcp2v3MXW8Xzg8asikZ0lnox1Tdcb0iEacIY+SMN+JtznARXYce4ZMOWMunuR+Th6Xj8rtPTMZY3TxeBKwE2eWUhnEmM9qunI2VlP0gGNKARsnYaGq5xHVZ/SqO6DdSuDOxDN0AyYvW4pWMpzYkislzQVIy3vmN0AWJoy1rkt3V9UwMMkWg5eWnvVTthH5ekCicy4+TSfJsN/aeW9lFxrBq5tW/T758cGffvM2Hpfvdoe/vb3Evf1qJexuiBjRZ2Qn8WcgKLptLRnCZHFNiqsnhYXsC+BP5BPtMG91mQ8V02SKRegKRdFNBJKQgkg6Nloik4RaJpFURKaQh4QLurObJ+LDNyYeoPPs7zi/MPtQVsw/NBkTkIfwcRuQfc7lxe1+mf2xWH8H/FPr9dh3PaeT/cGI/0XMR+b4mq1hWmavYiUlCJxrxNbZRyRIEaQI9bwpUggQBRjZRaMbzCpZS+6kEC9ptaT8EJZIQj0GfrEgjRucZDZ3g3b52UCbavs0d5dOXTOy/JI63Ga2cNVsBS4RkXBHFtxyX7aUrqRv7/PVHeyLmaAspZFYgPwCWA5TkB8DA4PXL2oquI5ccmLxMKbEXn5H4AD7KtCKf2INsEpGACdSmjDlVJE+6svlztoUGS+qH8aow8j2vKHCfLWgWCwlYr6qDUdUBAu3UHrZ5uAeCp7W3rFJ7dpHO416EdgSpJVVKBqoAxAVpM7+vOfJPeLu3Spj7ljWAePmcQsvsvgg2eftGBpqcNMo/YzZBwDI1qwBxJwq8nL5vS9+wWjp8EXIDZ7UqDTSHxU4NPIkPeMIwf/N1kzXQ+owJwYhsfJcvjuJqyqzoMC5MXvIZ4oymH11rog/YK01wUNTIMO28ZESmhGl7N2s3j2PuMbjl8q+NuQth1fdJPO3TaMYnmi9R2jiaugJLl96sKQiDC7gzINP94G0gDyxADxl5GJYKecDWkAf6hZCHAchUjTzgaDA8CXkcQhRF5KGe5DeBPDRbWIONw2u22N6uRhHg1B6GXgWGjBM7WHbFV5untccIN4psDjHeITucruPAD5kU5K5YkElQQS7YvzF/6mAWOZ5P9vcykRQEbIghgmNJwES7vOgquXMmJLinKz9ZU9FoQuOYLl40+ST7/4CvoGhE8kc6q2X6oVP/mb/HYDV3lvzm4nnG/eF952ml90noJXLSopbXgVbmIIWWx5as5PO65nW8dXYdn4BC3xXUPDxGzTvMvJwq1Txm5uWwBTVv1lTzsBU1L2kcA9t93d7/GSVu0w0BKqSvnY2x56RTVw/TsF61euia2b5eNK968e3qxR42yxzUoV5UcovKmz12MitQdpKyz4xFFaegt8JlLZFPpPLC97xE66rslrImFr2vbVELSbjJkqmlMFWaiBApqWWpqBWz+Q8vnlJ2iVLI7I5S6nXQPA/eqAISNRFJl3jj+z2G0X9mg/Wfd4M/KL6z0Wct5/RKh7bZEd6AZYRhiWGQ9IMkhCENpBuwNBDW9VagSk8TvM0GrDASxQ5m2TQ+Gayoo6l5LspP6HphjB5tP/PmfZOxR1bxhVX0QB9AlNeMnrMh09K2WLonkc/mkKvZ0evDQpVo3O5GOno7h10OdPGR4iEHekQN3ZR8iK+MKqI3PTtPHVN3aEc+8oXwCubfIJgXnRzIwjLo6NLJoV3h4UupLNAqL25AYXd1iQ/RVbDfrmBDwe4zVXZfl4KtqwSbzX0wcdxvFy7bGtDLxMJnNv1qJAheRft8DrjyQoCx1jfOK9xKp46XZWRctGj3oJCStdvmcDa3ji2R5OexV5/9ODVXNQNm5S9JGWj5/b2tygvbQuE0S7UygTGb16JFe8gvdPYERiGyo1VEyHuGaRzqULYeFc4Zo9zdxnZfyHB/wTBuzFNz/gDqz5ueq+DuQ9Dh3NyNxdzZKt+h2AFXOVOgfWIHC3TgfbGrkVyBVythlbQEB7zlgEH4WSIopZ04/O9FKFZmeU3NyhYYgxRppdv8gIgDRQzm0xXu+y4NV/1kQ5gsSO0t9ACKHkR4XlSmnSeObtwiMDB+GhifqBISvdsQrlEyna2C9s5yGbB34a/4GFDHe5w4gRO6yQt0Bh8xkuGjgWSmyuua9/rISWtY4qr9oglfQONqiiFLRV2BvzyHWFPlFj/TtchkKhC97lbdI9KAxRxDxdY+FbQ3WssQu0Zsf6mIrbDD7eiIrSmsTFgXBmoqIqWLoa+qtF6xg1kZ4sWndsDNoi4lg7Qd82L3xoaOdbtwb+RHbKB0/Qy5GNd2sBXB2ptbnpVLcURS1v/d5e8zYMX0qtzKzc4LaG95NoAp8LvWGeRTarAa2+9PZjxsvQP66xhvBIxhskPnMhgvcBYTz2mR71CZ7XRLATy6ZLsO9J0+wuzm69iO/ekD+2LYzqPuesHewZs8Pvnx/HFBwxllBZculgyHTPzAj7fdcSU6tzLUqrmShJ7gmiudS3HgWBgZ9TYMRutuQygagipjI6s7EYsiYYcXhMfmR2Fho4NVD4seAc2U0yrHoV86X6jmVlxz+SzvlHUDrqL4Vy2XEtMdax2rjulwNAA02dnCjeExAEChZHeqrxMjeecEy4MGChsZHuLbxrWC7L/4KxUM7mkRT1yReQOkB01dSSodAlFJ0iaO1FGStIaf8yhFDzVJ0StiMTXotY9bmja+KcUt9ZvuwpYnhSO7WWd6SHB62EDvYyFOWHepMQTvgsSBjWXi6qj8IK1yx/Sr2ksfojXrxFAyRo0koOOlqpB5rpUSz+FNRc55Jko8QR0YJVkyDPRqYWpCuo8XyJPyCOoehNSN4GKBRW3teLkVx0Jt+SshELSNAfUqwRX1U0XahAHsQ+3bEV1VPlYde7xta1hh3KK+S6dTQh6XKboWYomarQ/HCo9AHnrsKrprCnG43faQIuhRnbUJQQOROOVBgqqMWoHI7jra7LRxQVnvYzC14zgqoFodoKkZ+WkY0oCaGvRtm9i78mvVp7jnV7KlGtugJyhPUIF6hPbIgo2qQqWU1EiAaVdKapxndJWS+lICheMHDGj0LVvHyLR0pmwNYS9qXZkRh0W28GYNyQwUtxlWwAeIRZkx2pcZKPtIZEeIIERvL60bCw4RrS1woOsCS+Y6pngSLVCIRBM53moCyh6R208PrOIujUxdiVfMSy4Tzzw78WTDeyd9o23oLOhIPjfxSsG9isx3OJyPgrL9NcwCAqKDuZBUeKVpgaZYAMO6IW9xUtpbrRE1H7hA1I+qUMAlU80W0Ipunp1qcmpCuhLe3l2VaEmJCloUK7aXt6ZF1+hJ8x7A787kb2f7fvFxE3/FilwmOb213URjg1ierrLcLG2CTLOrgFvPFMTKyk9t6CDRWEkaTSKNcaGkwUIOuAnPTBr5VAZ5v2erpCGQEUeZ62ebGDndkUYXDyVWZcm1FKZWkkZ22Mq+qMsgjRjkVSYwdkka2eEhH6h+GaQxBT+YJRtTnVJG8YtiedWl0UYTD01X7AXrljYyuIZtk6abVV2KoCiOxep0puVlXf8lZtoyQB+WT79B2rknW16o5ZMDLmKLo3iK1C4q0jy6ZcX977amgYz9z9+id/8H -------------------------------------------------------------------------------- /img/Grant_Device_flow_v5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/Grant_Device_flow_v5.jpg -------------------------------------------------------------------------------- /img/Lambda-Configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/Lambda-Configuration.png -------------------------------------------------------------------------------- /img/Lambda-Console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/Lambda-Console.png -------------------------------------------------------------------------------- /img/Lambda-Function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/Lambda-Function.png -------------------------------------------------------------------------------- /img/Lambda-Variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/Lambda-Variables.png -------------------------------------------------------------------------------- /img/enduser_UI.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/enduser_UI.jpg -------------------------------------------------------------------------------- /img/enduser_success.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cognito-device-grant-flow/0e5af82cb7ca5848cc05d3c0adcadaf876462cfa/img/enduser_success.jpg -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | console.log("Loading function"); 20 | const common = require( __dirname + '/Modules/common.js'); 21 | const tokenP = require( __dirname + '/Modules/token-path.js'); 22 | const deviceP = require( __dirname + '/Modules/device-path.js'); 23 | const callbackP = require( __dirname + '/Modules/callback-path.js'); 24 | const authzP = require( __dirname + '/Modules/authorization-path.js'); 25 | 26 | exports.handler = (event, context, callback) => { 27 | console.log("Received event:", JSON.stringify(event, null, 2)); 28 | 29 | //Initialize default settings if needed 30 | if (!process.env.CODE_EXPIRATION || process.env.CODE_EXPIRATION == '') process.env.CODE_EXPIRATION = "1800"; 31 | if (!process.env.DYNAMODB_TABLE || process.env.DYNAMODB_TABLE == '') process.env.DYNAMODB_TABLE = "DeviceGrant"; 32 | if (!process.env.DYNAMODB_AUTHZ_STATE_INDEX || process.env.DYNAMODB_AUTHZ_STATE_INDEX == '') process.env.DYNAMODB_AUTHZ_STATE_INDEX = "AuthZ_state-index"; 33 | if (!process.env.DYNAMODB_USERCODE_INDEX || process.env.DYNAMODB_USERCODE_INDEX == '') process.env.DYNAMODB_USERCODE_INDEX = "User_code-index"; 34 | if (!process.env.POLLING_INTERVAL || process.env.POLLING_INTERVAL == '') process.env.POLLING_INTERVAL = "5"; 35 | if (!process.env.DEVICE_CODE_FORMAT || process.env.DEVICE_CODE_FORMAT == '') process.env.DEVICE_CODE_FORMAT = "#aA"; 36 | if (!process.env.DEVICE_CODE_LENGTH || process.env.DEVICE_CODE_LENGTH == '') process.env.DEVICE_CODE_LENGTH = "64"; 37 | if (!process.env.USER_CODE_FORMAT || process.env.USER_CODE_FORMAT == '') process.env.USER_CODE_FORMAT = "#B"; 38 | if (!process.env.USER_CODE_LENGTH || process.env.USER_CODE_LENGTH == '') process.env.USER_CODE_LENGTH = "8"; 39 | if (!process.env.RESULT_TOKEN_SET || process.env.RESULT_TOKEN_SET == '') process.env.RESULT_TOKEN_SET = "ACCESS+REFRESH"; 40 | 41 | switch(event.path) { 42 | //Call the Token endpoint either for getting codes or using a device code to get a JWTs 43 | case '/token': 44 | // If it is a POST on /token with client_id provided 45 | if(event.httpMethod == 'POST' && event.queryStringParameters.client_id && event.queryStringParameters.client_id != '') { 46 | tokenP.processPostRequest(event, callback); 47 | } else { // If it is something else than a POST on /token with client_id provided 48 | console.log("Unsupported Call on /token"); 49 | common.returnJSONError(405, callback); 50 | } 51 | break; 52 | 53 | case "/device": 54 | if(event.httpMethod == 'GET') { 55 | // This is a POST Call on /device whit represent the end user wanting 56 | // to delegate access to a device by providing the User code 57 | if (event.headers['x-amzn-oidc-accesstoken'] && event.headers['x-amzn-oidc-accesstoken'] != '' && event.headers["x-amzn-oidc-data"] && event.headers["x-amzn-oidc-data"] != '') { 58 | // If the request contains Authorize and Code as Query Parameters 59 | if (event.queryStringParameters.authorize && event.queryStringParameters.authorize != '' && event.queryStringParameters.code && event.queryStringParameters.code != '' ) { 60 | // If Code Query Parameter is NULL 61 | if ( event.queryStringParameters.code == '' ) { 62 | console.log("End user submitted an empty user code"); 63 | common.returnExpiredUserCodeError(callback); 64 | } else { 65 | // If Code Query Parameter is not NULL 66 | deviceP.requestUserCodeProcessing(event, callback); 67 | } 68 | } else { 69 | // If the request does not contain Authorize and Code as Query Parameters 70 | // The end user has been authenticated at the ALB and the Access token flows to the /device endpoint 71 | deviceP.requestUI(event, callback); 72 | } 73 | } else { 74 | // Request went through ALB but miss the necessary Access Token 75 | console.log("Call passed to /device without x-amzn-oidc-accesstoken "); 76 | common.returnJSONError(405, callback); 77 | } 78 | } else { 79 | // If it is something else than a GET on /device 80 | console.log("Unsupported Call on /device"); 81 | common.returnJSONError(405, callback); 82 | } 83 | break; 84 | 85 | case "/callback": 86 | if(event.httpMethod == 'GET') { 87 | if (event.queryStringParameters.code && event.queryStringParameters.code != '' && event.queryStringParameters.state && event.queryStringParameters.state != '' ) { 88 | callbackP.processAuthZCodeCallback(event, callback); 89 | } else { 90 | // ÉMissing necessary Query Parameter 91 | console.log("Unsupported Call on /callback"); 92 | common.returnJSONError(405, callback); 93 | } 94 | } else { 95 | // If it is something else than a GET on /callback 96 | console.log("Unsupported Call on /callback"); 97 | common.returnJSONError(405, callback); 98 | } 99 | break; 100 | 101 | default: 102 | // If it is an unsupported call to this API 103 | console.log("Unsupported Call"); 104 | common.returnJSONError(405, callback); 105 | } 106 | }; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cognito-device-grant-flow", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "1.0.0", 9 | "license": "ISC" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cognito-device-grant-flow", 3 | "version": "1.0.0", 4 | "description": "This repository is a demonstration on how to realize Device Grant flow ([RFC 8628](https://tools.ietf.org/html/rfc8628)) using Cognito, Lambda, and DynamoDB.", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/aws-samples/cognito-device-grant-flow.git" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/aws-samples/cognito-device-grant-flow/issues" 20 | }, 21 | "homepage": "https://github.com/aws-samples/cognito-device-grant-flow#readme" 22 | } 23 | -------------------------------------------------------------------------------- /template/CFT-DeviceGrantFlowDemo-latest.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Parameters: 3 | ACredsEmail: 4 | Type: String 5 | MinLength: 8 6 | MaxLength: 42 7 | Description: Email fo the Cognito user that act as the device owner 8 | ACertificateARN: 9 | Type: String 10 | Description: >- 11 | ARN of the certificate imported in AWS Certificate Manager for the choosen 12 | FQDN 13 | AFullyQualifiedDomainName: 14 | Type: String 15 | Description: Choosen FQDN 16 | ResultTokenSet: 17 | Type: String 18 | Default: ACCESS+REFRESH 19 | AllowedValues: 20 | - ID+ACCESS+REFRESH 21 | - ACCESS+REFRESH 22 | - ACCESS 23 | Description: Content of the Token Set returned to the Client Application 24 | ACognitoDomain: 25 | Type: String 26 | MinLength: 8 27 | MaxLength: 42 28 | Description: Custom domain for the Cognito user pool 29 | ZCodeExpiration: 30 | Type: String 31 | Default: '1800' 32 | AllowedValues: 33 | - '1800' 34 | - '3600' 35 | Description: Maximum lifetime for the generated code generated in seconds 36 | ZCleaningRate: 37 | Type: String 38 | Default: 1 hour 39 | AllowedValues: 40 | - 1 hour 41 | - 5 hours 42 | - 1 day 43 | - 5 days 44 | Description: Time between two cleaning sweeps in the DynamoDB table 45 | ZCodePollingInterval: 46 | Type: String 47 | Default: '5' 48 | AllowedValues: 49 | - '5' 50 | - '15' 51 | - '30' 52 | - '60' 53 | Description: Minimum time between two codes verification in seconds 54 | ZDeviceCodeFormat: 55 | Type: String 56 | Default: '#aA' 57 | AllowedValues: 58 | - '#' 59 | - '#a' 60 | - '#A' 61 | - '#!' 62 | - '#aA' 63 | - '#aA!' 64 | - aA! 65 | - a! 66 | - A! 67 | Description: Format for the code generated for Device usage where numeric is 68 | ZDeviceCodeLength: 69 | Type: String 70 | Default: '64' 71 | AllowedValues: 72 | - '8' 73 | - '16' 74 | - '32' 75 | - '64' 76 | - '128' 77 | Description: Length for the code generated for Device usage 78 | ZUserCodeFormat: 79 | Type: String 80 | Default: '#B' 81 | AllowedValues: 82 | - '#' 83 | - '#b' 84 | - '#B' 85 | - '#!' 86 | - '#bB' 87 | - '#bB!' 88 | - bB! 89 | - b! 90 | - A! 91 | Description: Format for the code generated for User usage where numeric is 92 | ZUserCodeLength: 93 | Type: String 94 | Default: '8' 95 | AllowedValues: 96 | - '8' 97 | - '16' 98 | - '32' 99 | - '64' 100 | - '128' 101 | Description: Length for the code generated for User usage 102 | Resources: 103 | DeviceGrantVPC: 104 | Type: 'AWS::EC2::VPC' 105 | Properties: 106 | CidrBlock: 10.192.0.0/16 107 | EnableDnsSupport: true 108 | EnableDnsHostnames: true 109 | Metadata: 110 | 'AWS::CloudFormation::Designer': 111 | id: 3655b27c-5c0f-40d8-aba0-8a26053368c6 112 | DeviceGrantInternetGateway: 113 | Type: 'AWS::EC2::InternetGateway' 114 | Properties: {} 115 | Metadata: 116 | 'AWS::CloudFormation::Designer': 117 | id: e6dc5429-4691-473a-94c9-377cee3f1aac 118 | DeviceGrantInternetGatewayAttachment: 119 | Type: 'AWS::EC2::VPCGatewayAttachment' 120 | Properties: 121 | InternetGatewayId: !Ref DeviceGrantInternetGateway 122 | VpcId: !Ref DeviceGrantVPC 123 | Metadata: 124 | 'AWS::CloudFormation::Designer': 125 | id: 530958bf-802a-4ae1-a4c4-21bd7222ba06 126 | DeviceGrantPublicSubnet1: 127 | Type: 'AWS::EC2::Subnet' 128 | Properties: 129 | VpcId: !Ref DeviceGrantVPC 130 | AvailabilityZone: !Select 131 | - 0 132 | - !GetAZs '' 133 | CidrBlock: 10.192.10.0/24 134 | MapPublicIpOnLaunch: false 135 | Metadata: 136 | 'AWS::CloudFormation::Designer': 137 | id: b454fe1c-c2c2-4db8-9fdc-7269515cbfcc 138 | DeviceGrantPublicSubnet2: 139 | Type: 'AWS::EC2::Subnet' 140 | Properties: 141 | VpcId: !Ref DeviceGrantVPC 142 | AvailabilityZone: !Select 143 | - 1 144 | - !GetAZs '' 145 | CidrBlock: 10.192.11.0/24 146 | MapPublicIpOnLaunch: false 147 | Metadata: 148 | 'AWS::CloudFormation::Designer': 149 | id: 619bcabc-91df-42bb-886c-a1cb473d8143 150 | DeviceGrantPublicRouteTable: 151 | Type: 'AWS::EC2::RouteTable' 152 | Properties: 153 | VpcId: !Ref DeviceGrantVPC 154 | Metadata: 155 | 'AWS::CloudFormation::Designer': 156 | id: 8b00c55b-9917-494a-ac43-24935414ed6a 157 | DeviceGrantDefaultPublicRoute: 158 | Type: 'AWS::EC2::Route' 159 | DependsOn: DeviceGrantInternetGatewayAttachment 160 | Properties: 161 | RouteTableId: !Ref DeviceGrantPublicRouteTable 162 | DestinationCidrBlock: 0.0.0.0/0 163 | GatewayId: !Ref DeviceGrantInternetGateway 164 | Metadata: 165 | 'AWS::CloudFormation::Designer': 166 | id: b9f4936a-92b1-4bba-bd36-798214a3709b 167 | DeviceGrantPublicSubnet1RouteTableAssociation: 168 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 169 | Properties: 170 | RouteTableId: !Ref DeviceGrantPublicRouteTable 171 | SubnetId: !Ref DeviceGrantPublicSubnet1 172 | Metadata: 173 | 'AWS::CloudFormation::Designer': 174 | id: f10ef958-9c73-4ce0-a0ee-dffe34ad342a 175 | DeviceGrantPublicSubnet2RouteTableAssociation: 176 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 177 | Properties: 178 | RouteTableId: !Ref DeviceGrantPublicRouteTable 179 | SubnetId: !Ref DeviceGrantPublicSubnet2 180 | Metadata: 181 | 'AWS::CloudFormation::Designer': 182 | id: 12c20ae6-a6b9-48ba-9ec4-99ea2a349be0 183 | DeployTokenCodeToS3: 184 | Type: 'Custom::deploytokencodetos3' 185 | Properties: 186 | ServiceToken: !GetAtt 187 | - LoadS3 188 | - Arn 189 | bucketname: !Ref S3B4NNNP 190 | packageURL: >- 191 | https://github.com/aws-samples/cognito-device-grant-flow/releases/download/v1.1.0/cognito-device-grant-flow.zip 192 | packageName: DeviceGrant-token.zip 193 | Metadata: 194 | 'AWS::CloudFormation::Designer': 195 | id: 2085180f-da86-44f5-9ece-43dab0c49343 196 | DeployCleaningCodeToS3: 197 | Type: 'Custom::deploycleaningcodetos3' 198 | Properties: 199 | ServiceToken: !GetAtt 200 | - LoadS3 201 | - Arn 202 | bucketname: !Ref S3B4NNNP 203 | packageURL: >- 204 | https://github.com/aws-samples/cognito-device-grant-flow-cleaning/releases/download/v1.0.0/cognito-device-grant-flow-cleaning.zip 205 | packageName: DeviceGrant-cleaning.zip 206 | Metadata: 207 | 'AWS::CloudFormation::Designer': 208 | id: e8ee5b08-fdb6-498d-9e4f-fb1a606e7586 209 | S3B4NNNP: 210 | Type: 'AWS::S3::Bucket' 211 | Properties: 212 | BucketEncryption: 213 | ServerSideEncryptionConfiguration: 214 | - ServerSideEncryptionByDefault: 215 | SSEAlgorithm: AES256 216 | PublicAccessBlockConfiguration: 217 | BlockPublicAcls: true 218 | BlockPublicPolicy: true 219 | IgnorePublicAcls: true 220 | RestrictPublicBuckets: true 221 | Metadata: 222 | 'AWS::CloudFormation::Designer': 223 | id: 58f0e53c-91d9-4403-86df-bc654006379b 224 | LoadS3: 225 | Type: 'AWS::Lambda::Function' 226 | Properties: 227 | FunctionName: LoadS3 228 | ReservedConcurrentExecutions: 2 229 | Role: !GetAtt 230 | - LoadS3IAMRole 231 | - Arn 232 | Runtime: python3.11 233 | Code: 234 | ZipFile: | 235 | # * * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 236 | # * * SPDX-License-Identifier: MIT-0 237 | # * Permission is hereby granted, free of charge, to any person obtaining a copy of this 238 | # * software and associated documentation files (the "Software"), to deal in the Software 239 | # * without restriction, including without limitation the rights to use, copy, modify, 240 | # * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 241 | # * permit persons to whom the Software is furnished to do so. 242 | # * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 243 | # * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 244 | # * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 245 | # * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 246 | # * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 247 | # * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 248 | 249 | import cfnresponse 250 | from urllib.request import urlopen 251 | from http.client import HTTPResponse 252 | import boto3 253 | import json 254 | 255 | 256 | def lambda_handler(event, context): 257 | print("start") 258 | print(json.dumps(event)) 259 | myBucket = event['ResourceProperties']['bucketname'] 260 | packageName = event['ResourceProperties']['packageName'] 261 | packageURL = event['ResourceProperties']['packageURL'] 262 | print("bucketname: " + myBucket + ", path: " + packageURL + ", package: " + packageName); 263 | if event['RequestType'] == 'Create': 264 | print("in Create") 265 | with urlopen(packageURL) as response: 266 | print("Reponse:") 267 | print(response) 268 | headers = response.getheaders() 269 | print("Headers:") 270 | print(headers) 271 | putS3Object(myBucket, packageName, response.read()) 272 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) 273 | elif event['RequestType'] == 'Delete': 274 | print("in Delete") 275 | s3 = boto3.client('s3') 276 | try: 277 | bucket = s3.list_objects_v2(Bucket=myBucket) 278 | if 'Contents' in bucket: 279 | for obj in bucket['Contents']: 280 | s3.delete_object(Bucket=myBucket, Key=obj['Key']) 281 | print("deleted: " + obj['Key']) 282 | except: 283 | print('Bucket already deleted') 284 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) 285 | else: 286 | print(event) 287 | print(context) 288 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) 289 | 290 | 291 | def putS3Object(bucketName, objectName, objectData): 292 | s3 = boto3.client('s3') 293 | return s3.put_object(Bucket=bucketName, Key=objectName, Body=objectData) 294 | 295 | 296 | def deleteBucket(bucketName): 297 | s3 = boto3.client('s3') 298 | return s3.delete_bucket(Bucket=bucketName) 299 | Handler: index.lambda_handler 300 | MemorySize: 128 301 | Timeout: 30 302 | Metadata: 303 | 'AWS::CloudFormation::Designer': 304 | id: 169c43ee-4d68-4cb1-88a1-a932d8b5e5b3 305 | DependsOn: 306 | - S3B4NNNP 307 | LoadS3IAMRole: 308 | Type: 'AWS::IAM::Role' 309 | Properties: 310 | Policies: 311 | - PolicyName: GrantDeviceS3BucketPolicy 312 | PolicyDocument: 313 | Version: 2012-10-17 314 | Statement: 315 | - Effect: Allow 316 | Action: 317 | - 's3:PutObject' 318 | - 's3:GetObject' 319 | - 's3:ListBucket' 320 | - 's3:DeleteObject' 321 | - 's3:DeleteBucket' 322 | Resource: 323 | - 'arn:aws:s3:::*/*' 324 | - !GetAtt S3B4NNNP.Arn 325 | AssumeRolePolicyDocument: 326 | Version: 2012-10-17 327 | Statement: 328 | - Effect: Allow 329 | Principal: 330 | Service: 331 | - lambda.amazonaws.com 332 | Action: 333 | - 'sts:AssumeRole' 334 | ManagedPolicyArns: 335 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 336 | Metadata: 337 | 'AWS::CloudFormation::Designer': 338 | id: a68582b5-c650-4cd6-af81-8ad80e15f6f4 339 | CUP532AI: 340 | Type: 'AWS::Cognito::UserPool' 341 | Properties: {} 342 | Metadata: 343 | 'AWS::CloudFormation::Designer': 344 | id: 8862289d-cb63-4bbf-9713-e07214fdeaea 345 | TestCognitoUser: 346 | Type: 'AWS::Cognito::UserPoolUser' 347 | Properties: 348 | Username: !Ref ACredsEmail 349 | UserPoolId: !Ref CUP532AI 350 | DesiredDeliveryMediums: 351 | - EMAIL 352 | UserAttributes: 353 | - Name: email 354 | Value: !Ref ACredsEmail 355 | Metadata: 356 | 'AWS::CloudFormation::Designer': 357 | id: c148ff3a-8479-4574-a093-9387547f29a3 358 | DeviceGrantALB: 359 | Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer' 360 | Properties: 361 | IpAddressType: ipv4 362 | Scheme: internet-facing 363 | Type: application 364 | SecurityGroups: 365 | - !Ref DeviceGrantALBSG 366 | Subnets: 367 | - !Ref DeviceGrantPublicSubnet1 368 | - !Ref DeviceGrantPublicSubnet2 369 | DependsOn: DeviceGrantInternetGateway 370 | Metadata: 371 | 'AWS::CloudFormation::Designer': 372 | id: 00c1e791-1793-45b8-a5e4-1fc6d277b572 373 | GrantDeviceALBCognitoClient: 374 | Type: 'AWS::Cognito::UserPoolClient' 375 | Properties: 376 | AllowedOAuthFlowsUserPoolClient: true 377 | AllowedOAuthFlows: 378 | - code 379 | AllowedOAuthScopes: 380 | - openid 381 | CallbackURLs: 382 | - !Join 383 | - '' 384 | - - 'https://' 385 | - !Ref AFullyQualifiedDomainName 386 | - /oauth2/idpresponse 387 | ClientName: GrantDevice-Authroizer-ALB 388 | ExplicitAuthFlows: 389 | - ALLOW_CUSTOM_AUTH 390 | - ALLOW_USER_SRP_AUTH 391 | - ALLOW_REFRESH_TOKEN_AUTH 392 | GenerateSecret: true 393 | SupportedIdentityProviders: 394 | - COGNITO 395 | UserPoolId: !Ref CUP532AI 396 | Metadata: 397 | 'AWS::CloudFormation::Designer': 398 | id: 5cd7fa36-acbc-4fea-82c1-83e88f342cb9 399 | DeviceCognitoClient: 400 | Type: 'AWS::Cognito::UserPoolClient' 401 | Properties: 402 | AllowedOAuthFlowsUserPoolClient: true 403 | AllowedOAuthFlows: 404 | - code 405 | AllowedOAuthScopes: 406 | - openid 407 | CallbackURLs: 408 | - !Join 409 | - '' 410 | - - 'https://' 411 | - !Ref AFullyQualifiedDomainName 412 | - /callback 413 | ClientName: IoT-Device 414 | ExplicitAuthFlows: 415 | - ALLOW_CUSTOM_AUTH 416 | - ALLOW_USER_SRP_AUTH 417 | - ALLOW_REFRESH_TOKEN_AUTH 418 | GenerateSecret: true 419 | SupportedIdentityProviders: 420 | - COGNITO 421 | UserPoolId: !Ref CUP532AI 422 | Metadata: 423 | 'AWS::CloudFormation::Designer': 424 | id: 97639ded-e3a4-4128-874d-fc6c47316548 425 | RetrieveCognitoSecretsLambda: 426 | Type: 'AWS::Lambda::Function' 427 | Properties: 428 | FunctionName: RetrieveCognitoSecrets 429 | ReservedConcurrentExecutions: 1 430 | Role: !GetAtt 431 | - RetrieveCognitoSecretsIAMRole 432 | - Arn 433 | Runtime: python3.11 434 | Code: 435 | ZipFile: | 436 | # * * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 437 | # * * SPDX-License-Identifier: MIT-0 438 | # * Permission is hereby granted, free of charge, to any person obtaining a copy of this 439 | # * software and associated documentation files (the "Software"), to deal in the Software 440 | # * without restriction, including without limitation the rights to use, copy, modify, 441 | # * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 442 | # * permit persons to whom the Software is furnished to do so. 443 | # * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 444 | # * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 445 | # * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 446 | # * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 447 | # * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 448 | # * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 449 | 450 | import cfnresponse 451 | import boto3 452 | import json 453 | 454 | def lambda_handler(event, context): 455 | print("start") 456 | print(json.dumps(event)) 457 | 458 | if event['RequestType'] == 'Create': 459 | client = boto3.client('cognito-idp') 460 | ALBClientID = event['ResourceProperties']['albauthorizerid'] 461 | DeviceCognitoClientID = event['ResourceProperties']['DeviceCognitoClientid'] 462 | userPoolId = event['ResourceProperties']['cupid'] 463 | 464 | responseData = {} 465 | 466 | try: 467 | response = client.describe_user_pool_client( 468 | UserPoolId=userPoolId, 469 | ClientId=ALBClientID 470 | ) 471 | 472 | responseData['ALBAuthorizerSecret'] = response['UserPoolClient']['ClientSecret'] 473 | 474 | except: 475 | print('Cannot retrive Cognito User Pool Client information for ALB') 476 | cfnresponse.send(event, context, cfnresponse.FAILED, {}) 477 | 478 | try: 479 | response = client.describe_user_pool_client( 480 | UserPoolId=userPoolId, 481 | ClientId=DeviceCognitoClientID 482 | ) 483 | 484 | responseData['DeviceCognitoClientSecret'] = response['UserPoolClient']['ClientSecret'] 485 | 486 | except: 487 | print('Cannot retrive Cognito User Pool Client information for Device') 488 | cfnresponse.send(event, context, cfnresponse.FAILED, {}) 489 | 490 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) 491 | else: 492 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) 493 | Handler: index.lambda_handler 494 | MemorySize: 128 495 | Timeout: 30 496 | Metadata: 497 | 'AWS::CloudFormation::Designer': 498 | id: 1fac8dff-416c-4f3e-921a-19bd3e8d1e45 499 | RetrieveCognitoSecrets: 500 | Type: 'Custom::retrievecognitosecrets' 501 | Properties: 502 | ServiceToken: !GetAtt 503 | - RetrieveCognitoSecretsLambda 504 | - Arn 505 | cupid: !Ref CUP532AI 506 | albauthorizerid: !Ref GrantDeviceALBCognitoClient 507 | DeviceCognitoClientid: !Ref DeviceCognitoClient 508 | Metadata: 509 | 'AWS::CloudFormation::Designer': 510 | id: cc3d4f93-d107-4b88-9f2a-64eed50cd645 511 | RetrieveCognitoSecretsIAMRole: 512 | Type: 'AWS::IAM::Role' 513 | Properties: 514 | Policies: 515 | - PolicyName: RetrieveCognitoSecretsPolicy 516 | PolicyDocument: 517 | Version: 2012-10-17 518 | Statement: 519 | - Effect: Allow 520 | Action: 521 | - 'cognito-idp:ListUserPoolClients' 522 | - 'cognito-idp:DescribeUserPoolClient' 523 | Resource: !GetAtt CUP532AI.Arn 524 | AssumeRolePolicyDocument: 525 | Version: 2012-10-17 526 | Statement: 527 | - Effect: Allow 528 | Principal: 529 | Service: 530 | - lambda.amazonaws.com 531 | Action: 532 | - 'sts:AssumeRole' 533 | ManagedPolicyArns: 534 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 535 | Metadata: 536 | 'AWS::CloudFormation::Designer': 537 | id: 041e645e-9d6f-4939-af47-fd6f9fa87e42 538 | DeviceGrantToken: 539 | Type: 'AWS::Lambda::Function' 540 | Properties: 541 | FunctionName: DeviceGrant-token 542 | ReservedConcurrentExecutions: 10 543 | Role: !GetAtt 544 | - DeviceGrantTokenIAMRole 545 | - Arn 546 | Runtime: nodejs20.x 547 | Code: 548 | S3Bucket: !Ref S3B4NNNP 549 | S3Key: DeviceGrant-token.zip 550 | Environment: 551 | Variables: 552 | APP_CLIENT_ID: !Ref GrantDeviceALBCognitoClient 553 | APP_CLIENT_SECRET: !GetAtt 554 | - RetrieveCognitoSecrets 555 | - ALBAuthorizerSecret 556 | CODE_EXPIRATION: !Ref ZCodeExpiration 557 | CODE_VERIFICATION_URI: !Ref AFullyQualifiedDomainName 558 | CUP_DOMAIN: !Ref ACognitoDomain 559 | CUP_ID: !Ref CUP532AI 560 | CUP_REGION: !Select 561 | - 0 562 | - !Split 563 | - _ 564 | - !Ref CUP532AI 565 | DEVICE_CODE_FORMAT: !Ref ZDeviceCodeFormat 566 | DEVICE_CODE_LENGTH: !Ref ZDeviceCodeLength 567 | DYNAMODB_AUTHZ_STATE_INDEX: AuthZ_state-index 568 | DYNAMODB_TABLE: DeviceGrant 569 | DYNAMODB_USERCODE_INDEX: User_code-index 570 | POLLING_INTERVAL: !Ref ZCodePollingInterval 571 | USER_CODE_FORMAT: !Ref ZUserCodeFormat 572 | USER_CODE_LENGTH: !Ref ZUserCodeLength 573 | Handler: index.handler 574 | MemorySize: 128 575 | Timeout: 30 576 | DependsOn: DeployTokenCodeToS3 577 | Metadata: 578 | 'AWS::CloudFormation::Designer': 579 | id: 7f018f35-89a1-45c8-baca-cae84cb55b86 580 | DeviceGrantTokenCleaning: 581 | Type: 'AWS::Lambda::Function' 582 | Properties: 583 | FunctionName: DeviceGrant-token-cleaning 584 | ReservedConcurrentExecutions: 1 585 | Role: !GetAtt 586 | - DeviceGrantCleaningIAMRole 587 | - Arn 588 | Runtime: nodejs20.x 589 | Code: 590 | S3Bucket: !Ref S3B4NNNP 591 | S3Key: DeviceGrant-cleaning.zip 592 | Environment: 593 | Variables: 594 | DYNAMODB_TABLE: DeviceGrant 595 | Handler: index.handler 596 | MemorySize: 128 597 | Timeout: 30 598 | DependsOn: DeployTokenCodeToS3 599 | Metadata: 600 | 'AWS::CloudFormation::Designer': 601 | id: 56f7d7ef-3eb2-4cf4-946e-1a2576da4dcd 602 | CognitoUserPoolDomain: 603 | Type: 'AWS::Cognito::UserPoolDomain' 604 | Properties: 605 | Domain: !Ref ACognitoDomain 606 | UserPoolId: !Ref CUP532AI 607 | Metadata: 608 | 'AWS::CloudFormation::Designer': 609 | id: aeb53dc1-966d-4b39-bf32-8ad628cf3d4d 610 | DeviceGrantALBTarget: 611 | Type: 'AWS::ElasticLoadBalancingV2::TargetGroup' 612 | Properties: 613 | HealthCheckEnabled: false 614 | Name: DeviceGrant-TG 615 | TargetType: lambda 616 | Targets: 617 | - Id: !GetAtt DeviceGrantToken.Arn 618 | Metadata: 619 | 'AWS::CloudFormation::Designer': 620 | id: a22b0ef1-78f6-4dae-b688-01424e9b225d 621 | DependsOn: 622 | - ALBToLambdaPerms 623 | DevicegrantALB443: 624 | Type: 'AWS::ElasticLoadBalancingV2::Listener' 625 | Properties: 626 | Port: 443 627 | Protocol: HTTPS 628 | SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06 629 | Certificates: 630 | - CertificateArn: !Ref ACertificateARN 631 | LoadBalancerArn: !Ref DeviceGrantALB 632 | DefaultActions: 633 | - Type: fixed-response 634 | FixedResponseConfig: 635 | ContentType: text/html 636 | MessageBody: '' 637 | StatusCode: '503' 638 | Metadata: 639 | 'AWS::CloudFormation::Designer': 640 | id: 4f4eb818-1ca9-41c0-b5f8-b3cd14e15f17 641 | DevicegrantALB443Device: 642 | Type: 'AWS::ElasticLoadBalancingV2::ListenerRule' 643 | Properties: 644 | ListenerArn: !Ref DevicegrantALB443 645 | Priority: 1 646 | Conditions: 647 | - Field: path-pattern 648 | PathPatternConfig: 649 | Values: 650 | - /device 651 | Actions: 652 | - Type: authenticate-cognito 653 | Order: 1 654 | AuthenticateCognitoConfig: 655 | OnUnauthenticatedRequest: authenticate 656 | Scope: openid 657 | UserPoolArn: !GetAtt CUP532AI.Arn 658 | UserPoolClientId: !Ref GrantDeviceALBCognitoClient 659 | UserPoolDomain: !Ref CognitoUserPoolDomain 660 | - Type: forward 661 | Order: 2 662 | TargetGroupArn: !Ref DeviceGrantALBTarget 663 | Metadata: 664 | 'AWS::CloudFormation::Designer': 665 | id: 2d62d0e6-fadf-4f9a-bef2-10588911df8e 666 | DevicegrantALB443TokenOrCallback: 667 | Type: 'AWS::ElasticLoadBalancingV2::ListenerRule' 668 | Properties: 669 | ListenerArn: !Ref DevicegrantALB443 670 | Priority: 2 671 | Conditions: 672 | - Field: path-pattern 673 | PathPatternConfig: 674 | Values: 675 | - /token 676 | - /callback 677 | Actions: 678 | - Type: forward 679 | Order: 1 680 | TargetGroupArn: !Ref DeviceGrantALBTarget 681 | Metadata: 682 | 'AWS::CloudFormation::Designer': 683 | id: d0a28ebe-21de-401c-a246-1abe77c4717b 684 | ALBToLambdaPerms: 685 | Type: 'AWS::Lambda::Permission' 686 | Properties: 687 | Action: 'lambda:InvokeFunction' 688 | FunctionName: !GetAtt DeviceGrantToken.Arn 689 | Principal: elasticloadbalancing.amazonaws.com 690 | Metadata: 691 | 'AWS::CloudFormation::Designer': 692 | id: 0a44a94c-2418-43f5-9e25-4df53fb76afb 693 | CWRuleForCleaning: 694 | Type: 'AWS::Events::Rule' 695 | Properties: 696 | Description: Invoke Cleaning Lambda 697 | State: ENABLED 698 | ScheduleExpression: !Join 699 | - '' 700 | - - rate( 701 | - !Ref ZCleaningRate 702 | - ) 703 | Targets: 704 | - Id: Cleaning-Table 705 | Arn: !GetAtt DeviceGrantTokenCleaning.Arn 706 | Metadata: 707 | 'AWS::CloudFormation::Designer': 708 | id: a3dffc32-2a71-4f5e-a7a9-1eddddec727e 709 | CWRuleToLambda: 710 | Type: 'AWS::Lambda::Permission' 711 | Properties: 712 | Action: 'lambda:InvokeFunction' 713 | FunctionName: !GetAtt DeviceGrantTokenCleaning.Arn 714 | Principal: events.amazonaws.com 715 | SourceArn: !GetAtt CWRuleForCleaning.Arn 716 | Metadata: 717 | 'AWS::CloudFormation::Designer': 718 | id: b4d69f67-f8c3-4267-b429-554a12a0d447 719 | DeviceGrantALBSG: 720 | Type: 'AWS::EC2::SecurityGroup' 721 | Properties: 722 | GroupDescription: SG for Device grant ALB 723 | GroupName: DevicegrantALBSG 724 | VpcId: !Ref DeviceGrantVPC 725 | SecurityGroupIngress: 726 | - IpProtocol: tcp 727 | FromPort: 443 728 | ToPort: 443 729 | CidrIp: 0.0.0.0/0 730 | Description: HTTPS going in 731 | SecurityGroupEgress: 732 | - IpProtocol: tcp 733 | FromPort: 443 734 | ToPort: 443 735 | CidrIp: 0.0.0.0/0 736 | Description: Whatever going out 737 | Metadata: 738 | 'AWS::CloudFormation::Designer': 739 | id: 307a84b8-850e-4f6d-b558-62360952c757 740 | DeviceGrantDynamoDBTable: 741 | Type: 'AWS::DynamoDB::Table' 742 | Properties: 743 | BillingMode: PROVISIONED 744 | SSESpecification: 745 | SSEEnabled: false 746 | PointInTimeRecoverySpecification: 747 | PointInTimeRecoveryEnabled: true 748 | AttributeDefinitions: 749 | - AttributeName: Device_code 750 | AttributeType: S 751 | - AttributeName: User_code 752 | AttributeType: S 753 | - AttributeName: AuthZ_State 754 | AttributeType: S 755 | GlobalSecondaryIndexes: 756 | - IndexName: AuthZ_state-index 757 | KeySchema: 758 | - AttributeName: AuthZ_State 759 | KeyType: HASH 760 | Projection: 761 | ProjectionType: ALL 762 | ProvisionedThroughput: 763 | ReadCapacityUnits: '5' 764 | WriteCapacityUnits: '5' 765 | - IndexName: User_code-index 766 | KeySchema: 767 | - AttributeName: User_code 768 | KeyType: HASH 769 | Projection: 770 | ProjectionType: ALL 771 | ProvisionedThroughput: 772 | ReadCapacityUnits: '5' 773 | WriteCapacityUnits: '5' 774 | KeySchema: 775 | - AttributeName: Device_code 776 | KeyType: HASH 777 | ProvisionedThroughput: 778 | ReadCapacityUnits: '5' 779 | WriteCapacityUnits: '5' 780 | TableName: DeviceGrant 781 | Metadata: 782 | 'AWS::CloudFormation::Designer': 783 | id: 690a45bd-fa42-4b86-bf43-1d7bd84f710c 784 | DeviceGrantTokenIAMRole: 785 | Type: 'AWS::IAM::Role' 786 | Properties: 787 | Policies: 788 | - PolicyName: DeviceGrantTokenTablePolicy 789 | PolicyDocument: 790 | Version: 2012-10-17 791 | Statement: 792 | - Effect: Allow 793 | Action: 794 | - 'dynamodb:PutItem' 795 | - 'dynamodb:DeleteItem' 796 | - 'dynamodb:GetItem' 797 | - 'dynamodb:Scan' 798 | - 'dynamodb:Query' 799 | - 'dynamodb:UpdateItem' 800 | Resource: 801 | - !GetAtt DeviceGrantDynamoDBTable.Arn 802 | - !Join 803 | - '' 804 | - - !GetAtt DeviceGrantDynamoDBTable.Arn 805 | - /index/User_code-index 806 | - !Join 807 | - '' 808 | - - !GetAtt DeviceGrantDynamoDBTable.Arn 809 | - /index/AuthZ_state-index 810 | - PolicyName: DevicegrantTokenRetrieveSecretsPolicy 811 | PolicyDocument: 812 | Version: 2012-10-17 813 | Statement: 814 | - Effect: Allow 815 | Action: 816 | - 'cognito-idp:ListUserPoolClients' 817 | - 'cognito-idp:DescribeUserPoolClient' 818 | Resource: !GetAtt CUP532AI.Arn 819 | AssumeRolePolicyDocument: 820 | Version: 2012-10-17 821 | Statement: 822 | - Effect: Allow 823 | Principal: 824 | Service: 825 | - lambda.amazonaws.com 826 | Action: 827 | - 'sts:AssumeRole' 828 | ManagedPolicyArns: 829 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 830 | Metadata: 831 | 'AWS::CloudFormation::Designer': 832 | id: 34109532-dba4-4dc7-8a96-ad3b585ebab7 833 | DeviceGrantCleaningIAMRole: 834 | Type: 'AWS::IAM::Role' 835 | Properties: 836 | Policies: 837 | - PolicyName: CleaningTablePolicy 838 | PolicyDocument: 839 | Version: 2012-10-17 840 | Statement: 841 | - Effect: Allow 842 | Action: 843 | - 'dynamodb:PutItem' 844 | - 'dynamodb:DeleteItem' 845 | - 'dynamodb:GetItem' 846 | - 'dynamodb:Scan' 847 | - 'dynamodb:Query' 848 | - 'dynamodb:UpdateItem' 849 | Resource: !GetAtt DeviceGrantDynamoDBTable.Arn 850 | AssumeRolePolicyDocument: 851 | Version: 2012-10-17 852 | Statement: 853 | - Effect: Allow 854 | Principal: 855 | Service: 856 | - lambda.amazonaws.com 857 | Action: 858 | - 'sts:AssumeRole' 859 | ManagedPolicyArns: 860 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 861 | Metadata: 862 | 'AWS::CloudFormation::Designer': 863 | id: 3cc2c718-243b-4034-a328-a6422f8134d5 864 | Outputs: 865 | TestEndPointForDevice: 866 | Value: !Join 867 | - '' 868 | - - 'https://' 869 | - !Ref AFullyQualifiedDomainName 870 | - /token 871 | Description: HTTPS Endpoint for the simulated DEVICE to make their requests 872 | TestEndPointForUser: 873 | Value: !Join 874 | - '' 875 | - - 'https://' 876 | - !Ref AFullyQualifiedDomainName 877 | - /device 878 | Description: HTTPS Endpoint for the USER to make their requests 879 | ALBCNAMEForDNSConfiguration: 880 | Value: !GetAtt DeviceGrantALB.DNSName 881 | Description: CNAME of the ALB Endpoint to point your DNS to 882 | UserUserName: 883 | Value: !Ref ACredsEmail 884 | Description: Username for the Test Cognito User 885 | UserPassword: 886 | Value: Will be sent to the email provided as username 887 | Description: Password for the Test Cognito User 888 | DeviceCognitoClientClientID: 889 | Description: >- 890 | App Client ID to be use by the simulated Device to interact with the 891 | authorization server 892 | Value: !Ref DeviceCognitoClient 893 | DeviceCognitoClientClientSecret: 894 | Description: >- 895 | App Client Secret to be use by simulated Device to interact with the 896 | authorization server 897 | Value: !GetAtt 898 | - RetrieveCognitoSecrets 899 | - DeviceCognitoClientSecret 900 | --------------------------------------------------------------------------------