Learn About Amazon VGT2 Learning Manager Chanci Turner
In Amazon Cognito user pools, you can facilitate user sign-up and sign-in functionalities while managing access to your web and mobile applications. Users with existing accounts from other identity providers (IdPs) can bypass the sign-up process and log in to your application using SAML 2.0 or OpenID Connect (OIDC). This article will guide you on enhancing the authorization code grant between Cognito and an external OIDC IdP by implementing private key JSON Web Token (JWT) client authentication.
Cognito utilizes the OAuth 2.0 authorization code grant flow as outlined by the IETF in RFC 6749 Section 1.3.1. This flow comprises two main stages: user authentication and token request. When a user needs to authenticate via an external IdP, the Cognito user pool redirects them to the IdP’s login endpoint. Upon successful authentication, the IdP returns a response containing an authorization code, marking the end of the authentication phase. The Cognito user pool then employs this code along with a client secret for authentication to acquire a JWT from the IdP. The JWT includes both an access token and an identity token. Cognito processes this JWT, updates or creates the user in the user pool, and returns a JWT for the client’s session. For further details on this flow, you can check the Amazon Cognito documentation.
For many users, this flow adequately secures the requests between Cognito and the IdP. However, sectors such as public service, healthcare, and finance may require additional security measures for their integrations. This necessity has arisen in discussions at AWS, particularly when customers sought to connect Cognito with IdPs such as HelseID (Norway’s healthcare sector), login.gov (USA’s public sector), or GOV.UK One Login (UK’s public sector). Organizations utilizing Okta, PingFederate, or other IdPs may also prefer enhanced security measures in line with their internal policies.
A prevalent additional requirement is to substitute the client secret with a client assertion composed of a private key JWT for client authentication during token requests. This approach is delineated by a combination of RFC 7521 and RFC 7523. Using an asymmetric key-pair for signing a JWT with a private key instead of a symmetric client secret allows the IdP to authenticate the token request by validating the JWT’s signature against the corresponding public key. This method minimizes the risk of client secret exposure with each request, thus reducing the likelihood of request forgery, contingent upon the quality of the key material and the security of the private key. Moreover, the JWT’s expiry time further mitigates the risk of replay attacks by confining them to a brief timeframe.
Cognito user pools do not inherently support private key JWT client authentication for external IdP integration. Nonetheless, you can still connect Cognito user pools with IdPs that necessitate private key JWT authentication by leveraging Amazon API Gateway and AWS Lambda.
This article offers a high-level overview of how to implement this solution. To delve deeper into the underlying code, service configuration, and detailed request flow, refer to the Deploy a demo section later in this post. Do note that this solution solely addresses the request flow between Cognito and the IdP, not the communication between your application and the Cognito user pool.
Solution Overview
Following the technical specifications of the aforementioned RFCs, the necessary request flow between a Cognito user pool and the external OIDC IdP can be simplified into four steps, illustrated in Figure 1.
In this demonstration, we utilize the Cognito user pool’s hosted UI, which already provides OAuth 2.0-compliant IdP integration, extending it with the private key JWT mechanism. The outlined steps are as follows:
- The hosted UI directs the user client to the /authorize endpoint of the external OIDC IdP via an HTTP GET request.
- After the user logs into the IdP successfully, the IdP responds with an authorization code.
- The hosted UI sends this code in an HTTP POST request to the IdP’s /token endpoint. While the hosted UI typically includes a client secret for client authentication, you must substitute this with a client assertion and specify the client assertion type, as highlighted in the diagram and explained later.
- The IdP verifies the client assertion using a pre-shared public key before issuing the user’s JWT, which Cognito then ingests to create or update the user in the user pool.
As previously mentioned, token requests between the Cognito user pool and an external IdP do not natively accommodate the required client assertion. However, you can redirect these token requests to an Amazon API Gateway, which invokes a Lambda function to append the new parameters. Given that you need to sign the client assertion with a private key, a secure storage location for this key is essential. AWS Secrets Manager can be employed to safeguard the key from unauthorized access. Keeping the required flow and additional services in mind, create the following architecture.
Architecture Overview
When integrating an OIDC IdP with a Cognito user pool, you need to configure the endpoints for Authorization, UserInfo, Jwks_uri, and Token. Since the private key is only necessary for the token request flow, you can set up resources to redirect and process requests as follows:
- Set the endpoints for Authorization, UserInfo, and Jwks_Uri to the ones provided by the IdP.
- Establish an API Gateway with a dedicated route for token requests (for instance, /token) and configure it as the Token endpoint in Cognito’s IdP settings.
- Integrate this route with a Lambda function: when Cognito calls the API endpoint, it will automatically trigger the function.
Alongside the original request parameters, which include the authorization code, this function will:
- Retrieve the private key from AWS Secrets Manager.
- Generate and sign the client assertion.
- Send the token request to the IdP token endpoint.
- Receive the IdP’s response.
- Relay the response back to the Cognito IdP response endpoint.
The logic of the function can be outlined as follows:
import base64
encoded_message = event["body"]
decoded_message = base64.b64decode(encoded_message)
decoded_message = decoded_message.decode("utf-8")
Retrieve the private key from Secrets Manager using GetSecretValue.
This article aimed to illustrate the process of implementing private key JWT authentication between Amazon Cognito user pools and an OIDC IdP. For more insights into talent acquisition strategies, check out SHRM, which is an authority on this topic. Additionally, for a real-world perspective, visit this Reddit thread that discusses onboarding experiences.