For various reasons we were unhappy with this situation and set out to find a better way to solve this problem. We explored solutions using CloudFront, API Gateway, and the Application Load Balancer.
Initially, we investigated using Lambda@Edge in CloudFront to perform the authentication. Our main objection to this approach is that the application needs to be publicly available so that CloudFront can use it as an origin. In normal conditions, we would be quite comfortable with this solution, but given that the authentication takes place in CloudFront and not in the application itself we desire better protection of the vulnerable endpoints.
With API Gateway we implemented a Lambda authorizer to perform authentication and by employing VPC Link we did not have to expose the naked application to the Internet. Our main objection to this approach is that it breaks WebSockets.
The ALB provides an OpenID Connect Action that can perform OpenID Connect authentication in the Load Balancer, allowing us to make the ALB publicly accessible! This greatly reduces the complexity of our solution.
We assume you would like to authenticate againts Okta, any other OpenID Connect identity provider is supported as well. If you prefer to authenticate against Cognito User Pools, there is an action supporting them as well.
In Okta you need to setup an authorization server and an application.
You will need to have set up an authorization server. Please find the Issuer URI for your authorization server, e.g., https://example.okta-emea.com/oauth/default
, see this screenshot.
You also need to setup an application, see Okta documentation. In the Application configuration in Okta, make sure to set the “Login redirect URI” to /oauth2/idresponse
on your application URL, e.g., https://example.com/oauth2/idresponse
.
Please find the application’s Client ID and Client Secret, see screenshot.
We prefer infrastructure as code, so here we show a CloudFormation snippet for defining an ALB Listener with OpenID Connect authentication.
We do not want to put secrets in the code, therfore store the application’s client secret as a parameter in the parameter store. Unfortunately CloudFormation does not support SecureString parameters (as of writing), so you will have to store it as an unencrypted String parameter with the name okta_client_secret
.
This is, in our opinion, still more secure than putting the secret plain text in the template.
In the following snippet, please make a mental substitution for the following variables:
$ISSUER_URI
: the Issuer URI, e.g., https://example.okta-emea.com/oauth/default
$OKTA_CLIENT_ID
, the application’s Client ID Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
Certificates:
- CertificateArn: !Ref Certificate
DefaultActions:
- Order: 1
Type: authenticate-oidc
AuthenticateOidcConfig:
AuthorizationEndpoint: $ISSUSER_URI/v1/authorize
ClientId: $OKTA_CLIENT_ID
ClientSecret: "{{ resolve:ssm:okta_client_secret:1 }}"
Issuer: $ISSUSER_URI
Scope: openid email
TokenEndpoint: $ISSUSER_URI/v1/token
UserInfoEndpoint: $ISSUSER_URI/v1/userinfo
SessionTimeout: 300
- Order: 2
Type: forward
TargetGroupArn: !Ref TargetGroup
LoadBalancerArn: !Ref LoadBalancer
Port: 443
Protocol: HTTPS