Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): aws application load balancer plugin #5581

Merged
merged 12 commits into from
Dec 11, 2024
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
Loading