Search code examples
angulartypescriptamazon-web-servicesaws-api-gatewayamazon-cloudfront

Getting CORS error from api gatweway only when appending Auth0 token to request


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]}"]
}

Solution

  • 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.