2

My API server runs on Lumen framework and I have a CORS middleware installed there, accompanied with a middleware which manages JSON POST data. On the server side, everything is perfect.

Now, for the UI, I use Angular 9 with Material layout. I have a pretty simple login component: it basically validates the input and then calls a service AuthService which communicates with my API server.

AuthService is pretty straightforward:

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { HttpClient, HttpHeaders, HttpErrorResponse, HttpParams } from '@angular/common/http';

export interface Token {
  token: string;
  token_type: string;
  expires_in: number;
}

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  public isAuthenticated = new BehaviorSubject<boolean>(false);

  constructor(
    private http: HttpClient,
    private router: Router
  ) {}

  async checkAuthenticated() {
    const authToken = localStorage.getItem('access_token');
    return (authToken !== null);
  }

  getToken() {
    return localStorage.getItem('access_token');
  }

  async login(username: string, password: string) {
    const api = 'http://api.elektron.test/v1/login';
    const headers = new HttpHeaders({'Content-Type': 'application/json; charset=UTF-8'});

    const body = JSON.stringify({
      email: username,
      password
    });

    // tslint:disable-next-line:max-line-length
    return this.http.post(api, body)
      .subscribe((res: any) => {
        if(res.token) {
          this.isAuthenticated.next(true);
          localStorage.setItem('access_token', res.token);
        }
      }, error => {
        console.error(error);
      }, () => {
        console.log('etstamente');
      });
  }

  async logout(redirect: string) {
    const removeToken = localStorage.removeItem('access_token');

    if (removeToken == null) {
      this.isAuthenticated.next(false);
      this.router.navigate([redirect]);
    }
  }

  handleError(error: HttpErrorResponse) {
    let msg = '';
    if (error.error instanceof ErrorEvent) {
      // client-side error
      msg = error.error.message;
    } else {
      // server-side error
      msg = `Error Code: ${error.status}\nMessage: ${error.message}`;
    }
    return throwError(msg);
  }
}

There in the this.http.post I can pass additional headers in the third argument, and I've tried that already, but I face the problem that whatever header I pass into it, and when I call that request, the Content-Type is never sent.

Another idea I tried was with an interceptor:

import {HttpInterceptor, HttpRequest, HttpHandler, HttpHeaders} from '@angular/common/http';
import { AuthService } from './auth.service';

@Injectable()

export class AuthInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) { }

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const token = this.authService.getToken();

    req = req.clone({
      responseType: 'json'
    });

    if (token) {
      req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + token) });
    }

    if (!req.headers.has('Content-Type')) {
      req = req.clone({
        setHeaders: {
          'Content-Type': 'application/json',
        }
      });
    }

    // setting the accept header
    req = req.clone({ headers: req.headers.set('Accept', 'application/json') });

    return next.handle(req);
  }
}

When it reaches any of the req.clone statements there, I end up having behavior same as explained before for the POST request.

So, I am clueless what I'm doing wrong here or what's causing this. In the Chrome browser, in the Console under Network tab when I try to see request headers, when these cases from above are applied, it states Provisional headers are shown - and I've found some Stack Overflow answers on that issue, but none of them solved my issue.

I've spent last 5-6 hours searching the net for a solution, tried some of my ideas, all to no avail.


EDIT: This problem is not related to Angular, but to the server and backend configuration and handling preflight requests.

Zlatan Omerović
  • 3,863
  • 4
  • 39
  • 67

2 Answers2

2

After a sleepless night, I finally resolved the issue, and it was not a problem with Angular or my code in it, but rather configuration of the web server.

If anyone experiences anything similar, this answer may provide a potential solution, and this is a book example of a preflight request:

A CORS preflight request is a CORS request that checks to see if the CORS protocol is understood and a server is aware using specific methods and headers.

It is an OPTIONS request, using three HTTP request headers: Access-Control-Request-Method, Access-Control-Request-Headers, and the Origin header.

Meaning that before each of the requests made by the the Angular app, there was an another one which would check the CORS options against the server, via the OPTIONS request.

My server setup and Lumen/Laravel CORS setup was failing all the time. This is how my CorsMiddleware looks like from the beginning:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

/**
 * Class CorsMiddleware
 * @package App\Http\Middleware
 */
class CorsMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param Request $request
     * @param Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $headers = [
            'Access-Control-Allow-Credentials' => 'true',
            'Access-Control-Max-Age' => '86400',
            'Access-Control-Allow-Methods' => 'POST, GET, OPTIONS, PUT, PATCH, HEAD, DELETE',
            'Access-Control-Allow-Headers' => 'X-Requested-With, Content-Type, Accept, Origin, Authorization',
            'Access-Control-Expose-Headers' => 'X-Requested-With, Content-Type, Accept, Origin, Authorization',
            'Access-Control-Allow-Origin' => '*',
        ];

        if ($request->isMethod('OPTIONS')) {
            return response()->json(['method' => 'OPTIONS'], 200, $headers);
        }

        $response = $next($request);

        foreach($headers as $key => $value) {
            $response->header($key, $value);
        }

        return $response;
    }
}

Then this middlware is properly injected in bootstrap/app.php as:

$app->routeMiddleware([
    'cors' => App\Http\Middleware\CorsMiddleware::class,
    ...,
]);

When this middlware runs, all the headers are sent in the request response, but, this statement in the CorsMiddleware never reached this condition, even if I tried with Postman:

if ($request->isMethod('OPTIONS')) {
    return response()->json(['method' => 'OPTIONS'], 200, $headers);
}

Reason for that was the the routes/web.php didn't handle any OPTIONS request, so that was being handled by the web server itself, and since I used Apache with .htaccess, that also caused some minor issues, but I didn't want to stick to having Apache handling this. I wanted to keep a convention that most of the things are handled by Lumen/Laravel.

So, in order to have my CorsMiddleware catch the OPTIONS request, I resolved this entire thing with adding this on top of my main routes/web.php file:

Route::options('/{any:.*}', ['middleware' => ['cors']]);

Now the pre-mentioned OPTIONS case in the CorsMiddleware which never happend, how does, and this resolved my entire problem.

Zlatan Omerović
  • 3,863
  • 4
  • 39
  • 67
0

this could be the problem req = req.clone({ responseType: 'json' });

try clonig the request into a new var like: var newReq = req.clone({ responseType: 'json' });

....rest of code... and then return the newReq.

Duki
  • 91
  • 4