Implement OAuth 2.0 device grant flow by using Amazon Cognito and AWS Lambda
When you implement the OAuth 2.0 authorization framework (RFC 6749) for internet-connected devices with limited input capabilities or that lack a user-friendly browser—such as wearables, smart assistants, video-streaming devices, smart-home automation, and health or medical devices—you should consider using the OAuth 2.0 device authorization grant (RFC 8628). This authorization flow makes it possible for the device user to review the authorization request on a secondary device, such as a smartphone, that has more advanced input and browser capabilities. By using this flow, you can work around the limits of the authorization code grant flow with Proof Key for Code Exchange (PKCE)-defined OpenID Connect Core specifications. This will help you to avoid scenarios such as:
- Forcing end users to define a dedicated application password or use an on-screen keyboard with a remote control
- Degrading the security posture of the end users by exposing their credentials to the client application or external observers
One common example of this type of scenario is a TV HDMI streaming device where, to be able to consume videos, the user must slowly select each letter of their user name and password with the remote control, which exposes these values to other people in the room during the operation.
The OAuth 2.0 device authorization grant (RFC 8628) is an IETF standard that enables Internet of Things (IoT) devices to initiate a unique transaction that authenticated end users can securely confirm through their native browsers. After the user authorizes the transaction, the solution will issue a delegated OAuth 2.0 access token that represents the end user to the requesting device through a back-channel call, as shown in Figure 1.
The workflow is as follows:
- An unauthenticated user requests service from the device.
- The device requests a pair of random codes (one for the device and one for the user) by authenticating with the client ID and client secret.
- The Lambda function creates an authorization request that stores the device code, user code, scope, and requestor’s client ID.
- The device provides the user code to the user.
- The user enters their user code on an authenticated web page to authorize the client application.
- The user is redirected to the Amazon Cognito user pool /authorize endpoint to request an authorization code.
- The user is returned to the Lambda function /callback endpoint with an authorization code.
- The Lambda function stores the authorization code in the authorization request.
- The device uses the device code to check the status of the authorization request regularly. And, after the authorization request is approved, the device uses the device code to retrieve a set of JSON web tokens from the Lambda function.
- In this case, the Lambda function impersonates the device to the Amazon Cognito user pool /token endpoint by using the authorization code that is stored in the authorization request, and returns the JSON web tokens to the device.
To achieve this flow, this blog post provides a solution that is composed of:
- An AWS Lambda function with three additional endpoints:
- The /token endpoint, which will handle client application requests such as generation of codes, the authorization request status check, and retrieval of the JSON web tokens.
- The /device endpoint, which will handle user requests such as delivering the UI for approval or denial of the authorization request, or retrieving an authorization code.
- The /callback endpoint, which will handle the reception of the authorization code associated with the user who is approving or denying the authorization request.
- An Amazon Cognito user pool with:
- Finally, an Amazon DynamoDB table to store the state of all the processed authorization requests.
Implement the solution
The implementation of this solution requires three steps:
- Define the public fully qualified domain name (FQDN) for the Application Load Balancer public endpoint and associate an X.509 certificate to the FQDN
- Deploy the provided AWS CloudFormation template
- Configure the DNS to point to the Application Load Balancer public endpoint for the public FQDN
Step 1: Choose a DNS name and create an SSL certificate
Your Lambda function endpoints must be publicly resolvable when they are exposed by the Application Load Balancer through an HTTPS/443 listener.
To configure the Application Load Balancer component
- Choose an FQDN in a DNS zone that you own.
- Associate an X.509 certificate and private key to the FQDN by doing one of the following:
- After you have the certificate in ACM, navigate to the Certificates page in the ACM console.
- Choose the right arrow (►) icon next to your certificate to show the certificate details.
- Copy the Amazon Resource Name (ARN) of the certificate and save it in a text file.
Step 2: Deploy the solution by using a CloudFormation template
To configure this solution, you’ll need to deploy the solution CloudFormation template.
Before you deploy the CloudFormation template, you can view it in its GitHub repository.
To deploy the CloudFormation template
- Choose the following Launch Stack button to launch a CloudFormation stack in your account.
Note: The stack will launch in the N. Virginia (us-east-1) Region. To deploy this solution into other AWS Regions, download the solution’s CloudFormation template, modify it, and deploy it to the selected Region.
- During the stack configuration, provide the following information:
- A name for the stack.
- The ARN of the certificate that you created or imported in AWS Certificate Manager.
- A valid email address that you own. The initial password for the Amazon Cognito test user will be sent to this address.
- The FQDN that you chose earlier, and that is associated to the certificate that you created or imported in AWS Certificate Manager.
- After the stack is configured, choose Next, and then choose Next again. On the Review page, select the check box that authorizes CloudFormation to create AWS Identity and Access Management (IAM) resources for the stack.
- Choose Create stack to deploy the stack. The deployment will take several minutes. When the status says CREATE_COMPLETE, the deployment is complete.
Step 3: Finalize the configuration
After the stack is set up, you must finalize the configuration by creating a DNS CNAME entry in the DNS zone you own that points to the Application Load Balancer DNS name.
To create the DNS CNAME entry
- In the CloudFormation console, on the Stacks page, locate your stack and choose it.
- Choose the Outputs tab.
- Copy the value for the key ALBCNAMEForDNSConfiguration.
- Configure a CNAME DNS entry into your DNS hosted zone based on this value. For more information on how to create a CNAME entry to the Application Load Balancer in a DNS zone, see Creating records by using the Amazon Route 53 console.
- Note the other values in the Output tab, which you will use in the next section of this post.
Output key Output value and function DeviceCognitoClientClientID The app client ID, to be used by the simulated device to interact with the authorization server DeviceCognitoClientClientSecret The app client secret, to be used by the simulated device to interact with the authorization server TestEndPointForDevice The HTTPS endpoint that the simulated device will use to make its requests TestEndPointForUser The HTTPS endpoint that the user will use to make their requests UserPassword The password for the Amazon Cognito test user UserUserName The user name for the Amazon Cognito test user
Evaluate the solution
Now that you’ve deployed and configured the solution, you can initiate the OAuth 2.0 device code grant flow.
Until you implement your own device logic, you can perform all of the device calls by using the curl library, a Postman client, or any HTTP request library or SDK that is available in the client application coding language.
All of the following device HTTPS requests are made with the assumption that the device is a private OAuth 2.0 client. Therefore, an HTTP Authorization Basic header will be present and formed with a base64-encoded Client ID:Client Secret value.
You can retrieve the URI of the endpoints, the client ID, and the client secret from the CloudFormation Output table for the deployed stack, as described in the previous section.
Initialize the flow from the client application
The solution in this blog post lets you decide how the user will ask the device to start the authorization request and how the user will be presented with the user code and URI in order to verify the request. However, you can emulate the device behavior by generating the following HTTPS POST request to the Application Load Balancer–protected Lambda function /token endpoint with the appropriate HTTP Authorization header. The Authorization header is composed of:
- The prefix Basic, describing the type of Authorization header
- A space character as separator
- The base64 encoding of the concatenation of:
- The client ID
- The colon character as a separator
- The client secret
The following JSON message will be returned to the client application.
Check the status of the authorization request from the client application
You can emulate the process where the client app regularly checks for the authorization request status by using the following HTTPS POST request to the Application Load Balancer–protected Lambda function /token endpoint. The request should have the same HTTP Authorization header that was defined in the previous section.
Until the authorization request is approved, the client application will receive an error message that includes the reason for the error: authorization_pending if the request is not yet authorized, slow_down if the polling is too frequent, or expired if the maximum lifetime of the code has been reached. The following example shows the authorization_pending error message.
Approve the authorization request with the user code
Next, you can approve the authorization request with the user code. To act as the user, you need to open a browser and navigate to the verification_uri that was provided by the client application.
If you don’t have a session with the Amazon Cognito user pool, you will be required to sign in.
Note: Remember that the initial password was sent to the email address you provided when you deployed the CloudFormation stack.
If you used the initial password, you’ll be asked to change it. Make sure to respect the password policy when you set a new password. After you’re authenticated, you’ll be presented with an authorization page, as shown in Figure 8.
Fill in the user code that was provided by the client application, as in the previous step, and then choose Authorize.
When the operation is successful, you’ll see a message similar to the one in Figure 9.
Finalize the flow from the client app
After the request has been approved, you can emulate the final client app check for the authorization request status by using the following HTTPS POST request to the Application Load Balancer–protected Lambda function /token endpoint. The request should have the same HTTP Authorization header that was defined in the previous section.
The JSON web token set will then be returned to the client application, as follows.
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.
Going further with this solution
This project is delivered with a default configuration that can be extended to support additional security capabilities or to and adapted the experience to your end-users’ context.
Extending security capabilities
Through this solution, you can:
- Use an AWS KMS key issued by AWS KMS to:
- Encrypt the data in the database;
- Protect the configuration in the Amazon Lambda function;
- Use AWS Secret Manager to:
- Securely store sensitive information like Cognito application client’s credentials;
- Enforce Cognito application client’s credentials rotation;
- Implement additional Amazon Lambda’s code to enforce data integrity on changes;
- Activate AWS WAF WebACLs to protect your endpoints against attacks;
Customizing the end-user experience
The following table shows some of the variables you can work with.
|CODE_EXPIRATION||Represents the lifetime of the codes generated||1800||Seconds|
|DEVICE_CODE_FORMAT||Represents the format for the device code||#aA||A string where:
# represents numbers
a lowercase letters
A uppercase letters
! special characters
|DEVICE_CODE_LENGTH||Represents the device code length||64||Number|
|POLLING_INTERVAL||Represents the minimum time, in seconds, between two polling events from the client application||5||Seconds|
|USER_CODE_FORMAT||Represents the format for the user code||#B||A string where:
# represents numbers
a lowercase letters
b lowercase letters that aren’t vowels
A uppercase letters
B uppercase letters that aren’t vowels
! special characters
|USER_CODE_LENGTH||Represents the user code length||8||Number|
|RESULT_TOKEN_SET||Represents what should be returned in the token set to the client application||ACCESS+REFRESH||A string that includes only ID, ACCESS, and REFRESH values separated with a + symbol|
To change the values of the Lambda function variables
- In the Lambda console, navigate to the Functions page.
- Select the DeviceGrant-token function.
- Choose the Configuration tab.
- Select the Environment variables tab, and then choose Edit to change the values for the variables.
- Generate new codes as the device and see how the experience changes based on how you’ve set the environment variables.
Although your business and security requirements can be more complex than the example shown in this post, this blog post will give you a good way to bootstrap your own implementation of the Device Grant Flow (RFC 8628) by using Amazon Cognito, AWS Lambda, and Amazon DynamoDB.
Your end users can now benefit from the same level of security and the same experience as they have when they enroll their identity in their mobile applications, including the following features:
- Credentials will be provided through a full-featured application on the user’s mobile device or their computer
- Credentials will be checked against the source of authority only
- The authentication experience will match the typical authentication process chosen by the end user
- Upon consent by the end user, IoT devices will be provided with end-user delegated dynamic credentials that are bound to the exact scope of tasks for that device
If you have feedback about this post, submit comments in the Comments section below. If you have questions about this post, start a new thread on the Amazon Cognito forum or reach out through the post’s GitHub repository.
Want more AWS Security how-to content, news, and feature announcements? Follow us on Twitter.