Search code examples
jsonangularcorshttprequest

HTTP Request: Can see JSON response in browser and REST Client but not Browser


I'm trying to hit my own endpoint on a subdomain controlled by Nginx, I expect the request to fail and return a JSON payload like this:

{
    "hasError": true,
    "data": null,
    "error": {
        "statusCode": 401,
        "statusDescription": null,
        "message": "Could not find Session Reference in Request Headers"
    }
}

When I make this request in a browser, it returns a 401 with this in the network tools (Brave Browser):

enter image description here

And this error in the console:

Access to fetch at 'https://services.mfwebdev.net/api/authentication/validate-session' from origin 'https://mfwebdev.net' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

When I hit the URL in question in the browser, I see the correct JSON response, if I hit the URL in a REST client like insomnia, I can see the JSON response.

The headers that browser is sending are:

:authority: services.mfwebdev.net
:method: GET
:path: /api/authentication/validate-session
:scheme: https
accept: application/json, text/plain, */*
accept-encoding: gzip, deflate, br
accept-language: en-GB,en-US;q=0.9,en;q=0.8
origin: https://mfwebdev.net
referer: https://mfwebdev.net/
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: same-site
sec-gpc: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36

I've actually used these headers in the REST client as well and I can still see the correct JSON result.

The request (in code) is as follows (Using Angular):

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiResponse } from 'src/app/api/types/response/api-response.class';
import { ISessionApiService } from 'src/app/api/types/session/session-api-service.interface';
import { SessionResponse } from 'src/app/api/types/session/session-response.class';
import { environment } from '../../../environments/environment';

@Injectable()
export class SessionApiService implements ISessionApiService {

    private readonly _http: HttpClient;

    constructor(http: HttpClient) {
        this._http = http;
    }

    public createSession(): Observable<ApiResponse<SessionResponse>> {
        return this._http.post<ApiResponse<SessionResponse>>(`${environment.servicesApiUrl}/authentication/authorise`, {
            reference: environment.applicationReference,
            applicationName: environment.applicationName,
            referrer: environment.applicationReferrer
         });
    }

    public validateSession(): Observable<ApiResponse<boolean>> {
        return this._http.get<ApiResponse<boolean>>(`${environment.servicesApiUrl}/authentication/validate-session`);
    }
}

Could someone please help, I'm at a complete loss here.

EDIT!! For anyone using NginX who may come across this problem. The issue was in my nginx.conf file. I am leaving an example of my(now working) server-side configuration.

The reason it wasn't working was because I was not bothering to actually handle the request if an OPTIONS request came through.

I now handle every request type (or will) and append the ACCESS-CONTROL-ALLOW-ORIGIN header to the request.

user root;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {}
http {
    include        /etc/nginx/proxy.conf;
    limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
    server_tokens  off;

    sendfile on;
    # Adjust keepalive_timeout to the lowest possible value that makes sense
    # for your use case.
    keepalive_timeout   1000;
    client_body_timeout 1000;
    client_header_timeout 10;
    send_timeout 10;

    upstream upstreamExample{
        server 127.0.0.1:5001;
    }

    server {
        listen                    443 ssl http2;
        listen                    [::]:443 ssl http2;
        server_name               example.net *.example.net;
        ssl_certificate           /etc/letsencrypt/live/example.net/cert.pem;
        ssl_certificate_key       /etc/letsencrypt/live/example.net/privkey.pem;
        ssl_session_timeout       1d;
        ssl_protocols             TLSv1.2 TLSv1.3;
        ssl_prefer_server_ciphers off;
        ssl_ciphers               ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
        ssl_session_cache         shared:SSL:10m;
        ssl_session_tickets       off;
        ssl_stapling              off;

        location / {
            if ($request_method = 'OPTIONS') {
                add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
                add_header 'Access-Control-Max-Age' 1728000;
                add_header 'Content-Type' 'text/plain; charset=utf-8';
                add_header 'Content-Length' 0;
                add_header 'Access-Control-Allow-Origin' '*';
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
                add_header 'Access-Control-Allow-Headers' '*';

                return 204;
            }

            if ($request_method = 'POST') {
                add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
                add_header 'Access-Control-Max-Age' 1728000;
                add_header 'Access-Control-Allow-Origin' '*' always;
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
                add_header 'Access-Control-Allow-Headers' '*';
            }

            if ($request_method = 'GET') {
                add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
                add_header 'Access-Control-Max-Age' 1728000;
                add_header 'Access-Control-Allow-Origin' '*' always;
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
                add_header 'Access-Control-Allow-Headers' '*';
            }

            if ($request_method = 'DELETE') {
                add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
                add_header 'Access-Control-Max-Age' 1728000;
                add_header 'Access-Control-Allow-Origin' '*' always;
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
                add_header 'Access-Control-Allow-Headers' '*';
            }

            proxy_pass https://upstreamExample;
            limit_req  zone=one burst=10 nodelay;
        }
    }
}


Solution

  • You would need to enable CORS(Cross-Origin Resource Sharing) by sending appropriate response headers, one of them being Access-Control-Allow-Origin header, which tells the browser what all origins can access the resource.

    CORS policy is something imposed by the browsers as a security measure, and not by REST clients such as Insomnia/Postman. Hence the HTTP request works in insomnia, but not in browser.

    From MDN:

    For security reasons, browsers restrict cross-origin HTTP requests initiated from scripts. For example, XMLHttpRequest and the Fetch API follow the same-origin policy. This means that a web application using those APIs can only request resources from the same origin the application was loaded from unless the response from other origins includes the right CORS headers.

    HTTP request to subdomain doesn't fall under same-origin policy, and hence you would need to enable CORS.

    Resources: