# Jira Webhook Authorizer Lambda

AWS Lambda authorizer function for validating Jira webhook signatures according to [Atlassian's webhook security documentation](https://developer.atlassian.com/cloud/jira/platform/webhooks/#secure-admin-webhooks).

## Overview

This Lambda function serves as an API Gateway authorizer that validates incoming Jira webhook requests by verifying the HMAC signature in the `X-Hub-Signature` header. It ensures that only authentic requests from Jira are processed by your webhook endpoint.

## How It Works

1. **Signature Validation**: Jira webhooks include an `X-Hub-Signature` header with format `sha256=<signature>`, where the signature is an HMAC-SHA256 hash of the request body signed with a shared secret.

2. **Secret Retrieval**: The Lambda retrieves the shared secret from AWS Secrets Manager. The secret is cached across invocations in the same execution context for performance.

3. **Authorization**: If the signature is valid, the Lambda returns an IAM policy allowing the request to proceed. Invalid signatures result in a deny policy.

## Signature Algorithm

Per [Atlassian's documentation](https://developer.atlassian.com/cloud/jira/platform/webhooks/#secure-admin-webhooks):

```
HMAC-SHA256(secret, request_body) = signature
```

Example from Atlassian docs:
- **Secret**: `It's a Secret to Everybody`
- **Payload**: `Hello World!`
- **Expected Signature**: `sha256=a4771c39fbe90f317c7824e83ddef3caae9cb3d976c214ace1f2937e133263c9`

## Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `ENVIRONMENT` | Deployment environment (dev, staging, prod) | `dev` |
| `LOGGING_LEVEL` | Python logging level | `INFO` |
| `SENTRY_DSN` | Sentry error tracking DSN (optional) | - |

**Note**: The secret name is constructed automatically as `{ENVIRONMENT}/jira-webhook-github-issues-authorizer/JIRA_SHARED_SECRET` based on the `ENVIRONMENT` variable.

## API Gateway Configuration

This Lambda **requires** an API Gateway REQUEST authorizer configuration:

### REQUEST Authorizer (Required)

- **Type**: REQUEST
- **Authorization Caching**: Disabled (signatures are unique per request, TTL = 0)
- **Invocation Role**: API Gateway needs an IAM role to invoke the Lambda authorizer

With REQUEST authorizer, the Lambda has access to headers, request body, and full request context, enabling proper HMAC signature validation. TOKEN authorizers are explicitly rejected as they do not provide the request body needed for validation.

## Response Format

The Lambda returns an IAM policy document:

### Allow Policy (Valid Signature)

```json
{
  "principalId": "jira-webhook",
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "execute-api:Invoke",
        "Effect": "Allow",
        "Resource": "arn:aws:execute-api:..."
      }
    ]
  },
  "context": {
    "validated": "true",
    "source": "jira-webhook"
  }
}
```

### Deny Policy (Invalid Signature)

```json
{
  "principalId": "user",
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "execute-api:Invoke",
        "Effect": "Deny",
        "Resource": "arn:aws:execute-api:..."
      }
    ]
  }
}
```

## Development

### Prerequisites

- Python 3.13
- [uv](https://docs.astral.sh/uv/) package manager
- Docker (for container builds and testing)

### Installation

```bash
# Install dependencies
uv sync

# Install development dependencies
uv sync --dev
```

### Running Tests

```bash
# Run all tests with coverage
make test

# Run tests with uv directly
uv run pytest tests/ -v --cov=src --cov-report=term-missing

# Run in Docker
docker-compose run test
```

### Linting

```bash
# Run linter
make lint

# Auto-format code
make format
```

### Local Testing

```bash
# Build Docker image
make build

# Run locally (requires AWS credentials)
docker-compose up jira-webhook-authorizer

# Test with Lambda Runtime Interface Emulator
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" \
  -d '{
    "type": "REQUEST",
    "methodArn": "arn:aws:execute-api:us-east-1:123456789012:api/prod/POST/webhook",
    "headers": {
      "X-Hub-Signature": "sha256=...",
      "Content-Type": "application/json"
    },
    "body": "{\"webhookEvent\":\"jira:issue_updated\"}"
  }'
```

## Deployment

This Lambda is deployed via Terraform configuration in `terraform-infra/dev/jira-webhook-github-issues/`.

### Terraform Configuration

Lambda authorizer module:

```hcl
module "lambda_authorizer" {
  source = "git@github.com:theorchard/terraform-lambda.git//?ref=4.3.0"

  environment                                    = var.environment
  lambda_function_timeout                        = 30
  use_container_image                            = true
  lambda_description                             = "Custom authorizer for Jira webhook API Gateway"
  lambda_name                                    = "${var.service_name}-authorizer"
  vpc_id                                         = module.vpc_info.vpc_id
  vpc_subnet_ids                                 = module.vpc_info.default_private_subnet_ids
  application_family                             = var.application_family
  lambda_function_memory_size                    = 256
  lambda_function_reserved_concurrent_executions = 25

  iam_managed_policy_attachments = [
    aws_iam_policy.authorizer_secrets_policy.arn
  ]
}
```

API Gateway authorizer configuration:

```hcl
resource "aws_api_gateway_authorizer" "jira_webhook_rest_api_authorizer" {
  name                             = "${var.environment}-${var.service_name}-authorizer"
  rest_api_id                      = aws_api_gateway_rest_api.jira_webhook_rest_api.id
  authorizer_uri                   = module.lambda_authorizer.lambda_invoke_arn
  authorizer_credentials           = aws_iam_role.authorizer_invocation_role.arn
  type                             = "REQUEST"
  authorizer_result_ttl_in_seconds = 0  # Disable caching
}
```

### Required IAM Permissions

The Lambda needs the following permissions:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": "arn:aws:secretsmanager:*:*:secret:dev/jira-webhook-authorizer/*"
    }
  ]
}
```

## Secrets Management

The shared secret must be stored in AWS Secrets Manager:

```bash
# Create secret in AWS Secrets Manager
aws secretsmanager create-secret \
  --name "dev/jira-webhook-github-issues-authorizer/JIRA_SHARED_SECRET" \
  --secret-string "your-webhook-secret-from-jira"
```

To configure the webhook in Jira:
1. Go to Jira Settings → System → WebHooks
2. Create a new webhook with your API Gateway URL
3. Set the same secret value in the webhook configuration
4. Jira will include the `X-Hub-Signature` header in all webhook requests

## Security Considerations

1. **Constant-Time Comparison**: Uses `hmac.compare_digest()` to prevent timing attacks
2. **Secret Caching**: Secrets are cached per execution context to minimize Secrets Manager API calls
3. **VPC Deployment**: Lambda runs in private subnets with no internet access (if configured)
4. **Reserved Concurrency**: Limited to 25 concurrent executions to prevent resource exhaustion
5. **Error Handling**: Any errors during validation result in deny policies

## Monitoring and Logging

- **CloudWatch Logs**: All requests and validation results are logged
- **Sentry Integration**: Optional error tracking for production monitoring
- **Metrics**: Lambda duration, invocations, errors, and throttles

## Testing the Example

The code includes a test that validates the exact example from Atlassian's documentation:

```python
def test_atlassian_documentation_example():
    secret = "It's a Secret to Everybody"
    payload = "Hello World!"
    expected_signature = (
        "sha256=a4771c39fbe90f317c7824e83ddef3caae9cb3d976c214ace1f2937e133263c9"
    )
    
    result = validate_jira_signature(payload, expected_signature, secret)
    
    assert result is True  # ✓ Passes
```

## Troubleshooting

### Signature Validation Failures

Check CloudWatch Logs for detailed error messages:

```bash
aws logs tail /aws/lambda/jira-webhook-authorizer --follow
```

Common issues:
- **Wrong secret**: Ensure the secret in Secrets Manager matches Jira's webhook configuration
- **Incorrect body**: The signature must be calculated on the exact raw request body
- **Header format**: Must be `sha256=<hex_signature>` (lowercase "sha256")

### Secret Retrieval Failures

Ensure:
1. Secret exists in Secrets Manager with name `{ENVIRONMENT}/jira-webhook-github-issues-authorizer/JIRA_SHARED_SECRET`
2. Lambda has `secretsmanager:GetSecretValue` permission for that secret
3. The `ENVIRONMENT` variable is set correctly (dev, staging, prod)

## References

- [Atlassian Webhook Security Documentation](https://developer.atlassian.com/cloud/jira/platform/webhooks/#secure-admin-webhooks)
- [API Gateway Lambda Authorizers](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-input.html)
- [AWS Lambda Best Practices](https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html)

## License

Internal use only - The Orchard
