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)
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
openidscope
# 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-bucketBucket 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"
}
}
}
]
}# Create ALB
aws elbv2 create-load-balancer \
--name spa-auth-alb \
--subnets subnet-12345 subnet-67890 \
--security-groups sg-12345 \
--scheme internet-facing \
--type applicationCreate 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.htmlCreate 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.jsonlistener-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"
}
]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;
};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/0In 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);
}
};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
}
}- SSL Certificate: Ensure you have a valid SSL certificate in AWS Certificate Manager
- DNS: Point your domain to the ALB using Route 53 or your DNS provider
- CORS: Configure your backend APIs to accept requests from your authenticated domain
- Session Management: ALB handles session cookies automatically
- 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.