├── 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 | 
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 | 
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 | 
134 |
135 | 2. Select DeviceGrant-token function:
136 |
137 | 
138 |
139 | 3. Go to the Configuration tab:
140 |
141 | 
142 |
143 | 4. Select the Environment variables tab, then click Edit to change the values you want:
144 |
145 | 
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 |
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 |
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 | 
20 |
21 | 2. Click on the ► icon next to your certificate:
22 |
23 | 
24 |
25 | 3. Copy the associated ARN in a text file:
26 |
27 | 
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 | 
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 | 
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 | 
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 | 
59 |
60 | 2. Locate your stack and click on it:
61 |
62 | 
63 |
64 | 3. Click on the Outputs tab:
65 |
66 | 
67 |
68 | 4. Copy the value for the Key ALBCNAMEForDNSConfiguration:
69 |
70 | 
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 | 
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 | 
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 | 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
--------------------------------------------------------------------------------
/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 | 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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------