skip to Main Content

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


  1. Chosen as BEST ANSWER

    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.


  2. 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 to proxy.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 of s3_origin_config. Also read about ordered_cache_behavior, you might need to tweak it.

    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 = <>
        origin_id   = <some_id>
    
        custom_origin_config {
          ...
        }
      }
    
      ordered_cache_behavior {
        path_pattern     = "/api/*"
        ...
        target_origin_id = <some_id>
        ...
      }
    
    ...
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search