Skip to content

Commit

Permalink
feat(auth): aws application load balancer plugin (#5581)
Browse files Browse the repository at this point in the history
* feat: add aws alb auth plugin

* feat: add debug logs

* docs: add documentation for the provider

* chore: better config var naming

* chore: extra debug line

* docs: improve alb plugins comment

Signed-off-by: Rob Lambell <[email protected]>

* chore: linting

---------

Signed-off-by: Rob Lambell <[email protected]>
  • Loading branch information
roblambell authored Dec 11, 2024
1 parent 04292d9 commit 0e4a64f
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 0 deletions.
22 changes: 22 additions & 0 deletions docs/docs/administration/settings/server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,28 @@ Make sure the reverse proxy strips this header from incoming requests (i.e. user

> The HTTP request header to use as the user name, this value is case-insensitive.
#### Configuration for `dispatch-auth-provider-aws-alb`

> Authenticate users based on [AWS Application Load Balancer authenticate](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html).
#### `DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN`

> ARN of your Load Balancer, used to validate the signer.
> The format is `arn:aws:elasticloadbalancing:region-code:account-id:loadbalancer/app/load-balancer-name/load-balancer-id`.
> This is required when using the `dispatch-auth-provider-aws-alb` auth provider.
#### `DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_EMAIL_CLAIM` \['default': email\]

> Override where Dispatch should find the user email in the users claims.
#### `DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_PUBLIC_KEY_CACHE_SECONDS` \['default': 300\]

> Override how long Dispatch should cache the public key, used to validate the payload.
:::info
Add a ALB listener action without authenticate for `/api/v1/{organization}/events/*` if you want plugins to be public. Plugins determine their own authentication.
:::

### Persistence

#### `DATABASE_HOSTNAME`
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ def run(self):
"dispatch_atlassian_confluence = dispatch.plugins.dispatch_atlassian_confluence.plugin:ConfluencePagePlugin",
"dispatch_atlassian_confluence_document = dispatch.plugins.dispatch_atlassian_confluence.docs.plugin:ConfluencePageDocPlugin",
"dispatch_aws_sqs = dispatch.plugins.dispatch_aws.plugin:AWSSQSSignalConsumerPlugin",
"dispatch_aws_alb_auth = dispatch.plugins.dispatch_core.plugin:AwsAlbAuthProviderPlugin",
"dispatch_auth_mfa = dispatch.plugins.dispatch_core.plugin:DispatchMfaPlugin",
"dispatch_basic_auth = dispatch.plugins.dispatch_core.plugin:BasicAuthProviderPlugin",
"dispatch_contact = dispatch.plugins.dispatch_core.plugin:DispatchContactPlugin",
Expand Down
10 changes: 10 additions & 0 deletions src/dispatch/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@ def __str__(self) -> str:
"DISPATCH_AUTHENTICATION_PROVIDER_HEADER_NAME", default="remote-user"
)

DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN = config(
"DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN", default=None
)
DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_EMAIL_CLAIM = config(
"DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_EMAIL_CLAIM", default="email"
)
DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_PUBLIC_KEY_CACHE_SECONDS = config(
"DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_PUBLIC_KEY_CACHE_SECONDS", cast=int, default=300
)

# sentry middleware
SENTRY_ENABLED = config("SENTRY_ENABLED", default="")
SENTRY_DSN = config("SENTRY_DSN", default="")
Expand Down
63 changes: 63 additions & 0 deletions src/dispatch/plugins/dispatch_core/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from uuid import UUID

import requests
from cachetools import cached, TTLCache
from fastapi import HTTPException
from fastapi.security.utils import get_authorization_scheme_param
from jose import JWTError, jwt
Expand All @@ -24,6 +25,9 @@
from dispatch.auth.models import DispatchUser, MfaChallenge, MfaChallengeStatus, MfaPayload
from dispatch.case import service as case_service
from dispatch.config import (
DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN,
DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_EMAIL_CLAIM,
DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_PUBLIC_KEY_CACHE_SECONDS,
DISPATCH_AUTHENTICATION_PROVIDER_HEADER_NAME,
DISPATCH_AUTHENTICATION_PROVIDER_PKCE_JWKS,
DISPATCH_JWT_AUDIENCE,
Expand Down Expand Up @@ -165,6 +169,65 @@ def get_current_user(self, request: Request, **kwargs):
return value


class AwsAlbAuthProviderPlugin(AuthenticationProviderPlugin):
title = "Dispatch Plugin - AWS ALB Authentication Provider"
slug = "dispatch-auth-provider-aws-alb"
description = "AWS Application Load Balancer authentication provider."
version = dispatch_plugin.__version__

author = "ManyPets"
author_url = "https://manypets.com/"

@cached(cache=TTLCache(maxsize=1024, ttl=DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_PUBLIC_KEY_CACHE_SECONDS))
def get_public_key(self, kid: str, region: str):
log.debug("Cache miss. Requesting key from AWS endpoint.")
url = f"https://public-keys.auth.elb.{region}.amazonaws.com/{kid}"
req = requests.get(url)
return req.text

def get_current_user(self, request: Request, **kwargs):
credentials_exception = HTTPException(
status_code=HTTP_401_UNAUTHORIZED, detail=[{"msg": "Could not validate credentials"}]
)

encoded_jwt: str = request.headers.get('x-amzn-oidc-data')
if not encoded_jwt:
log.error(
"Unable to authenticate. Header x-amzn-oidc-data not found."
)
raise credentials_exception

log.debug(f"Header x-amzn-oidc-data header received: {encoded_jwt}")

# Validate the signer
jwt_headers = encoded_jwt.split('.')[0]
decoded_jwt_headers = base64.b64decode(jwt_headers)
decoded_json = json.loads(decoded_jwt_headers)
received_alb_arn = decoded_json['signer']

if received_alb_arn != DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN:
log.error(
f"Unable to authenticate. ALB ARN {received_alb_arn} does not match expected ARN {DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN}"
)
raise credentials_exception

# Get the key id from JWT headers (the kid field)
kid = decoded_json['kid']

# Get the region from the ARN
region = DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN.split(':')[3]

# Get the public key from regional endpoint
log.debug(f"Getting public key for kid {kid} in region {region}.")
pub_key = self.get_public_key(kid, region)

# Get the payload
log.debug(f"Decoding {encoded_jwt} with public key {pub_key}.")
payload = jwt.decode(encoded_jwt, pub_key, algorithms=['ES256'])

return payload[DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_EMAIL_CLAIM]


class DispatchTicketPlugin(TicketPlugin):
title = "Dispatch Plugin - Ticket Management"
slug = "dispatch-ticket"
Expand Down

0 comments on commit 0e4a64f

Please sign in to comment.