So I am trying to host a full stack web application in AWS. I have a angular frontend sitting in an s3 bucket behind cloudfront and using a domain from route53. On the backend I have an typescript express project that is sitting behind api gateway. I am using auth0 for authentication and the sample frontend/backend apps they provide on their websites. I will link them here:
https://github.com/auth0-developer-hub/spa_angular_typescript_hello-world
https://github.com/auth0-developer-hub/api_express_typescript_hello-world
When I make requests not hit by the interceptor they will work, but when I try requests that get modified by the interceptor (when I log in using auth0) I will get the below error:
"Access to XMLHttpRequest at ‘https://api-gateway.com/dev/api/messages/protected’ from origin ‘https://website.link’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource."
Interceptor code in the auth.module below:
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AuthModule, AuthHttpInterceptor } from '@auth0/auth0-angular';
import { environment as env } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { SharedModule } from './shared';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
AppRoutingModule,
SharedModule,
HttpClientModule,
AuthModule.forRoot({
...env.auth0,
httpInterceptor: {
allowedList: [`${env.api.serverUrl}/api/messages/admin`, `${env.api.serverUrl}/api/messages/protected`],
//allowedList: [`${env.api.serverUrl}/api/messages/admin`],
},
}),
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthHttpInterceptor,
multi: true,
},
],
bootstrap: [AppComponent],
})
export class AppModule {}
Below is my index.ts from the express backend where I explicitly allow the "Authorization" header that the interceptor is attaching. I have confirmed the tokens it is passing are valid as well.
import cors from "cors";
import * as dotenv from "dotenv";
import * as awsServerlessExpress from 'aws-serverless-express';
import express from "express";
import helmet from "helmet";
import nocache from "nocache";
import { messagesRouter } from "./messages/messages.router";
import { errorHandler } from "./middleware/error.middleware";
import { notFoundHandler } from "./middleware/not-found.middleware";
dotenv.config();
if (!(process.env.PORT && process.env.CLIENT_ORIGIN_URL)) {
throw new Error(
"Missing required environment variables. Check docs for more info."
);
}
const PORT = parseInt(process.env.PORT, 10);
const CLIENT_ORIGIN_URL = process.env.CLIENT_ORIGIN_URL;
const app = express();
const apiRouter = express.Router();
app.use(express.json());
app.set("json spaces", 2);
app.use(
helmet({
hsts: {
maxAge: 31536000,
},
contentSecurityPolicy: {
useDefaults: false,
directives: {
"default-src": ["'none'"],
"frame-ancestors": ["'none'"],
},
},
frameguard: {
action: "deny",
},
})
);
app.use((req, res, next) => {
res.contentType("application/json; charset=utf-8");
next();
});
app.use(nocache());
app.use(
cors({
origin: CLIENT_ORIGIN_URL,
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Authorization", "Content-Type"],
maxAge: 86400,
})
);
app.use("/api", apiRouter);
apiRouter.use("/messages", messagesRouter);
app.use(errorHandler);
app.use(notFoundHandler);
// create serverless express
const server = awsServerlessExpress.createServer(app);
// export the handler function for AWS Lambda
export const handler = (event: any, context: any) => awsServerlessExpress.proxy(server, event, context);
I have tried various things like setting the ‘Access-Control-AllowOrigin’ header on the routes and it still hasn’t worked, such as below:
messagesRouter.get("/protected", validateAccessToken, (req, res) => {
res.set("Access-Control-Allow-Origin", CLIENT_ORIGIN_URL);
try {
logger.info(JSON.stringify(req.auth));
logger.info(`Token: ${JSON.stringify(req.auth?.token)}`)
//console.log(req.auth?.token)
const message = getProtectedMessage();
res.status(200).json(message);
}
catch (err){
console.log(err);
res.status(500).json('Error')
}
});
I’ve also tried various CORS configurations in the index.ts such as below:
app.use(cors());
app.use(cors({
origin: CLIENT_ORIGIN_URL,
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Authorization", "Content-Type"],
credentials: true,
}));
These resulted in the same error
Edit: including my cloudfront configuration from terraform below:
resource "aws_cloudfront_distribution" "website_distribution" {
origin {
domain_name = aws_s3_bucket.frontend_bucket.bucket_regional_domain_name
origin_id = aws_s3_bucket.frontend_bucket.id
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.oai.cloudfront_access_identity_path
}
}
origin {
domain_name = replace(aws_api_gateway_deployment.example.invoke_url, "/^https?://([^/]*).*/", "$1")
origin_id = aws_api_gateway_deployment.example.id
origin_path = "/${terraform.workspace}"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1", "TLSv1.1", "TLSv1.2"]
}
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS", "POST", "DELETE", "PUT", "PATCH"]
cached_methods = ["GET", "HEAD", "OPTIONS"]
target_origin_id = aws_s3_bucket.frontend_bucket.id
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
ordered_cache_behavior {
path_pattern = "/api/*"
target_origin_id = aws_api_gateway_deployment.example.id
allowed_methods = ["GET", "HEAD", "OPTIONS", "POST", "DELETE", "PUT", "PATCH"]
cached_methods = ["GET", "HEAD", "OPTIONS"]
forwarded_values {
query_string = true
cookies {
forward = "none"
}
headers = ["Authorization"]
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
restrictions {
geo_restriction {
restriction_type = "none"
locations = []
}
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.cert.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2018"
cloudfront_default_certificate = false
}
custom_error_response {
error_code = 403
response_page_path = "/index.html"
response_code = "200"
error_caching_min_ttl = 300
}
enabled = true
is_ipv6_enabled = true
http_version = "http2"
price_class = "PriceClass_100"
default_root_object = "index.html"
aliases = [var.domain_name[terraform.workspace], "www.${var.domain_name[terraform.workspace]}"]
}
2
Answers
I fixed this issue by enabling CORS for my resource in API Gateway. I also made sure to specify that the "Authorization" header is allowed.
In order to access API Gateway from Cloudfront, which happens when you issue
https://cloudfront-host/api/endpoint
, you need to define another origin other than S3, which is the api gateway.The reason you can access directly through Postman or Api Gateway is because Cloudfront is the one who is blocking the request. You’ll also need to setup
ordered_cache_behavior
, so that Cloudfront can proxy requests to your custom origin. This is very similar toproxy.conf.json
in Angular, where if all your api endpoints are following the pattern/api/<endpoint>
, then your ordered cache behavior will route requests to/api/*
to api gateway.Inside of the new the origin definition you shall use
custom_origin_config
instead ofs3_origin_config
. Also read aboutordered_cache_behavior
, you might need to tweak it.