Authenticate Users with Azure OIDC on AWS Application Load Balancers

Securing legacy applications which were deigned for internal use only has become a hot topic in these times of sudden remote working. There isn’t the time or money available to re-architect authentication, and its not always easy to provide remote accessibility to systems when the company doesn’t have the resources to implement it safely.

Thankfully AWS Application Load Balancers provide a really simple method of adding an authentication layer to an application which doesn’t require any application code changes, and can be linked to an auth provider that you already have via OIDC, in this case the extremely common Office 365!

OpenID Connect and AWS Application Load Balancers

OpenID Connect is an identity layer built on top of OAuth2 which can be used natively with AWS ALBs. An ALB requires a listener which has rules on how to forward traffic based on the incomming connection. In the vast majority of cases just a simple default listener rule to “forward” to the application target group is all that is used.

In order to make use of OIDC, all we need to do is add a condition which checks for an OIDC auth cookie, and if its not present it starts the OIDC OAuth2 authentication flow!

Creating an Azure App Registration

We need to create an application registration in Azure Active Directory. This will give us the various URLs and magic values we will need to be able to configure OIDC on our ALB.

Create the App Registration

Log in to Azure at portal.azure.com

Under Azure Services click Azure Active Directory

In the left had menu, under Manage click App Registrations

On the top bar, click New registration

Start by entering a name for your application. This helps identify the purpose of this configuration

Under Supported Account Types select the option Accounts in this organizational directory only ( - Single tenant). This will ensure only your companies’ Office 365 users will be able to use our application.

Finally, click the Register button.

Next we’re going to need to collect some information that we need to supply to the ALB.

Create the Client Secret

In the left side menu, click Certificates & secrets

Under Client Secrets click New client secret

Add a description to remind you what this secret is for. The application registration could be used for multiple applications, but each application should have its own secret, so the description will be helpful if the secret ever needs to be removed.

Select an expiry. Most likely Never is the best choice here.

Finally click the Add button

The Client Secrets list will now contain your new secret. Copy the Value field and add it to the following Terraform code:

variable "oidc_client_secret" {
  default = "<paste secret here>"
}

Collect Our Other Information

On the left hand menu again, click Overview

In the Essentials pane, copy the Application (client) ID and paste it in to the following Terraform code:

variable "oidc_client_id" {
  default = "<paste client id here>"
}

Next copy the Directory (tenant) ID and paste in to the following Terraform code:

variable "oidc_tenant_id" {
  default = "<paste tenant id here>"
}

And we have all the information we need to go ahead and implement OIDC in our listener.

Redirect URIs

Part of the OAuth flow requires the load balancer to redirect to the Azure authorization endpoint. We need to configure this in Azure

In the Overview / Essentials block, click the Redirect URIs link

Click the Add Platform button

Click Web

Enter the url to the root of the load balancer and append /oauth2/idpresponse. e.g. if our app is at https://app.exmaple.com this should show https://app.exmaple.com/oauth2/idpresponse

A little further down the page, under Implicit Grants tick the option Access Tokens, and untick ID Tokens

Create our OIDC ALB Listener(s)

For OIDC to work we need to be using HTTPS. It’s unwise to expose an application over HTTP anyway, so lets enforce this by adding a listener with a default action that redirects http traffic to https:

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.application.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "redirect"

    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

Now we need an HTTPS listener

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.application.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = "arn:aws:iam::012345678999:server-certificate/test_cert_abc"

  default_action {
    type = "authenticate-oidc"

    authenticate_oidc {
      authorization_endpoint = "https://login.microsoftonline.com/${var.oidc_tenant_id}/oauth2/v2.0/authorize"
      client_id              = var.oidc_client_id
      client_secret          = var.oidc_client_secret
      issuer                 = "https://login.microsoftonline.com/${var.oidc_tenant_id}/v2.0"
      token_endpoint         = "https://login.microsoftonline.com/${var.oidc_tenant_id}/oauth2/v2.0/token"
      user_info_endpoint     = "https://graph.microsoft.com/oidc/userinfo"
    }
  }

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.application.arn
  }
}

Outbound Traffic ALB

An essential part to add is an Egress rule to the security group attached to the ALB for HTTPS traffic to allow the ALB to contact Azure to confirm tokens:

resource "aws_security_group_rule" "applications_alb_egress_https" {
  from_port = 443
  to_port = 443
  protocol = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
  security_group_id = aws_security_group.app_alb.id
}

Notes

For brevity I have not included the Terraform code for the load balancer or target group, however this is relatively trivial to add.

Once added you should find a couple of extra headers in the requests forwarded to our application:

x-amzn-oidc-accesstoken holds the full JSON Web Token (JWT) x-amzn-oidc-data is the user claims in JWT format x-amzn-oidc-identity contains the subject field (sub) from the user info endpoint


878 Words

2020-09-22 18:36 +0000