Skip to content

Instantly share code, notes, and snippets.

@riccardomerolla
Created May 28, 2025 19:23
Show Gist options
  • Select an option

  • Save riccardomerolla/be7ad2060764d53352a62493db065520 to your computer and use it in GitHub Desktop.

Select an option

Save riccardomerolla/be7ad2060764d53352a62493db065520 to your computer and use it in GitHub Desktop.

AWS Architecture Overview

Your setup will include:

  • Application Load Balancer (ALB) - Acts as the authentication gatekeeper
  • S3 Static Website - Hosts your SPA
  • External IdP - Handles user authentication (e.g., Auth0, Okta, Azure AD)

Step-by-Step Implementation

1. Configure Your External IdP

First, register your application with your IdP:

For OIDC/OAuth2 IdPs (Auth0, Okta, etc.):

  • Create a new application/client
  • Set the Redirect URI to: https://your-domain.com/oauth2/idpresponse
  • Note down your Client ID and Client Secret
  • Ensure the IdP supports the openid scope

2. Set Up S3 Static Website Hosting

# Create S3 bucket
aws s3 mb s3://your-spa-bucket

# Enable static website hosting
aws s3 website s3://your-spa-bucket --index-document index.html --error-document error.html

# Upload your SPA files
aws s3 sync ./dist s3://your-spa-bucket

Bucket Policy (restrict direct access, only allow ALB):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowALBAccess",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::ACCOUNT-ID:root"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::your-spa-bucket/*",
            "Condition": {
                "StringEquals": {
                    "aws:SourceVpce": "vpce-your-vpc-endpoint-id"
                }
            }
        }
    ]
}

3. Create Application Load Balancer

# Create ALB
aws elbv2 create-load-balancer \
    --name spa-auth-alb \
    --subnets subnet-12345 subnet-67890 \
    --security-groups sg-12345 \
    --scheme internet-facing \
    --type application

4. Configure ALB Authentication

Create Target Group pointing to S3:

aws elbv2 create-target-group \
    --name spa-s3-targets \
    --protocol HTTP \
    --port 80 \
    --vpc-id vpc-12345 \
    --target-type ip \
    --health-check-path /index.html

Create Listener with Authentication:

aws elbv2 create-listener \
    --load-balancer-arn arn:aws:elasticloadbalancing:region:account:loadbalancer/app/spa-auth-alb/xyz \
    --protocol HTTPS \
    --port 443 \
    --certificates CertificateArn=arn:aws:acm:region:account:certificate/cert-id \
    --default-actions file://listener-actions.json

listener-actions.json:

[
    {
        "Type": "authenticate-oidc",
        "Order": 1,
        "AuthenticateOidcConfig": {
            "Issuer": "https://your-idp.com",
            "AuthorizationEndpoint": "https://your-idp.com/authorize",
            "TokenEndpoint": "https://your-idp.com/token",
            "UserInfoEndpoint": "https://your-idp.com/userinfo",
            "ClientId": "your-client-id",
            "ClientSecret": "your-client-secret",
            "Scope": "openid profile email",
            "SessionCookieName": "AWSELBAuthSessionCookie",
            "SessionTimeout": 3600,
            "OnUnauthenticatedRequest": "authenticate"
        }
    },
    {
        "Type": "forward",
        "Order": 2,
        "TargetGroupArn": "arn:aws:elasticloadbalancing:region:account:targetgroup/spa-s3-targets/xyz"
    }
]

5. Alternative: Use CloudFront + Lambda@Edge

For better performance and S3 integration, consider this approach:

CloudFront Distribution with Lambda@Edge:

// Lambda@Edge function for authentication
exports.handler = async (event) => {
    const request = event.Records[0].cf.request;
    const headers = request.headers;
    
    // Check for authentication cookie
    const cookies = headers.cookie;
    if (!cookies || !isValidAuthCookie(cookies)) {
        // Redirect to IdP
        return {
            status: '302',
            statusDescription: 'Found',
            headers: {
                location: [{
                    key: 'Location',
                    value: `https://your-idp.com/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=openid`
                }]
            }
        };
    }
    
    return request;
};

6. Enhanced Security Configuration

Security Group for ALB:

aws ec2 create-security-group \
    --group-name spa-alb-sg \
    --description "Security group for SPA ALB" \
    --vpc-id vpc-12345

# Allow HTTPS traffic
aws ec2 authorize-security-group-ingress \
    --group-id sg-12345 \
    --protocol tcp \
    --port 443 \
    --cidr 0.0.0.0/0

7. SPA Code Integration

In your SPA, handle the authentication context:

// Check for authentication headers/cookies
const getAuthContext = () => {
    // ALB injects user info in headers
    const userHeaders = {
        'x-amzn-oidc-identity': '', // User ID
        'x-amzn-oidc-data': '',     // JWT with user claims
        'x-amzn-oidc-accesstoken': '' // Access token
    };
    
    return parseUserInfo(userHeaders);
};

// Initialize your SPA with auth context
const initApp = () => {
    const authContext = getAuthContext();
    if (authContext.user) {
        // User is authenticated, load app
        loadApplication(authContext);
    }
};

8. Terraform Configuration (Alternative)

resource "aws_lb" "spa_alb" {
  name               = "spa-auth-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb_sg.id]
  subnets           = var.public_subnet_ids
}

resource "aws_lb_listener" "spa_listener" {
  load_balancer_arn = aws_lb.spa_alb.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS-1-2-2017-01"
  certificate_arn   = var.certificate_arn

  default_action {
    type = "authenticate-oidc"
    
    authenticate_oidc {
      authorization_endpoint = "https://your-idp.com/authorize"
      client_id             = var.oidc_client_id
      client_secret         = var.oidc_client_secret
      issuer                = "https://your-idp.com"
      token_endpoint        = "https://your-idp.com/token"
      user_info_endpoint    = "https://your-idp.com/userinfo"
      
      on_unauthenticated_request = "authenticate"
      scope                     = "openid profile email"
      session_cookie_name       = "AWSELBAuthSessionCookie"
      session_timeout          = 3600
    }
  }

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

Key Considerations

  1. SSL Certificate: Ensure you have a valid SSL certificate in AWS Certificate Manager
  2. DNS: Point your domain to the ALB using Route 53 or your DNS provider
  3. CORS: Configure your backend APIs to accept requests from your authenticated domain
  4. Session Management: ALB handles session cookies automatically
  5. Token Refresh: Implement token refresh logic in your SPA if needed

This setup provides enterprise-grade security by ensuring users authenticate before accessing your SPA code, while leveraging AWS's managed services for scalability and reliability.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment