Configure CakePHP 3.x and React so 401 Unauthorized Errors are Caught by fetch()

Written by James McDonald

February 2, 2018

Update: Another way of globally implementing CORS is to add a .htaccess with the following in it


Header set Access-Control-Allow-Origin "http://localhost:3000"

Doing JWT authentication with CakePHP and when I put the wrong password in I was getting a generic “TypeError: Failed to Fetch” Error message.

I’m using this to post to the API endpoint to pick up a token:

     var myHeaders = new Headers({
			"Content-Type": "application/json",
			"Accept" : "application/json"
		});

     let fetchConfig = {
	 method: 'POST',
         headers: myHeaders,
         mode: 'cors',
         cache: 'default',
         body: JSON.stringify(creds)};

		fetch('http://clam.tgn/api/users/token', fetchConfig)
		.then(response => {
                      if (!response.ok) {
                          throw new Error(response.status + ': ' + response.statusText);
                      }
                   return response;
                  }
                )
		.then(response => response.json())
		.then(data => {
			const { token } = data.data
                        // this dispatches an action to the Redux Store
			dispatch(authLogin({ ...creds, isAuthenticated: true }))
			dispatch(readToken(token))
                        // this stores the token
			localStorage.setItem('id_token', token)
		}).catch((e) => {
			dispatch( authError(e.message) )
			console.error("INCATCH", e)
		})

In the above request I am sending a POST to http://clam.tgn/api/users/token from a React App located at http://localhost:3000

So the browser was displaying a 401 (Unauthorized) message in the console but this was not available to the fetch catch statement as you can see in the following screenshot but notice straight after the 401 message there is a No Access-Control-Allow-Origin error

I was sending the correct headers for authenticated API calls using HttpOptionsMiddleware but when authentication was unsuccessful the Access-Control-Allow-Origin header was missing. And without the ‘Access-Control-Allow-Origin’ header in the returned 401 (UnAuthorized) HTTP response fetch would only display the generic “Failed to fetch” error.

The 401 error was returned in my UsersController token action by throwing an UnauthorizedException error but how to get it to send back the header?


public function token() {

$user = $this->Auth->identify();

if (!$user) {
// when a bad username and / or password is entered this error is thrown
throw new UnauthorizedException('Invalid username or password.');
}

// this send the JWT token
$this->set([
'success' => true,
'data' => [
'token' => JWT::encode([
'sub' => $user['id'],
'exp' => time() + 604800,
'user' => $user['username']
], Security::salt())
],
'_serialize' => ['success', 'data']
]);

The answer was to extend the UnauthorizedException method and use it’s responseHeader method.

firstly I created a directory in src/Exception and a file named TgnUnauthorizedException.php with the following contents

<?php 
namespace App\Exception; 
use Cake\Log\LogTrait; 
use Cake\Network\Exception\UnauthorizedException; 
class TgnUnauthorizedException extends UnauthorizedException { 
        protected $_defaultCode = 401 
        public function __construct($message = null, $code = null, $previous = null) { 
          if (empty($message)) { 
              $message = 'Unauthorized'; 
          } 
        parent::__construct($message, $code, $previous); 
        // this is the special sauce to get your header returned to fetch
        $this->responseHeader( "Access-Control-Allow-Origin", "*");
}

Then you have to get the UsersController to reference the new class with the added call to $this->responseHeader


namespace App\Controller\Api;
use Cake\Event\Event;
use App\Exception\TgnUnauthorizedException; //add the extended class reference here
//use Cake\Network\Exception\UnauthorizedException;
use Cake\Utility\Security;
use Firebase\JWT\JWT;
use App\Controller\Api\AppController;

public function token() {
        $user = $this->Auth->identify();
	if (!$user) {
	// extended to send access control allow origin header
        throw new TgnUnauthorizedException('Invalid username or password.');
        }
         // rest of token action
}

Once you have created an extended UnauthorizedException error which returns the correct header you will then have the correct header in the CORS fetch request and the 401 Unauthorized error will be available in the catch statement of your fetch request.

But all of the above is still pretty rudimentary as it only returns response.status and response.statusText but cake sends back a nice object with a lot more information which you can customize:

{
 "message": "Invalid username or password.",
 "url": "/api/users/token",
 "code": 401
}


In the end you want to resolve and reject the promise and get access to that message from cake to tell you what is really going on! So this is how you can do it in fetch:


fetch('http://clam.tgn/api/users/token', fetchConfig)
  .then(
    if (response.ok) {
      return response.json()
    } else {
      return response.json().then(err => Promise.reject(err))
    })
  .then(data => {
    const {
      token
    } = data.data
    dispatch(authLogin({ ...creds,
      isAuthenticated: true
    }))
    dispatch(readToken(token))
    localStorage.setItem('id_token', token)
  }).catch((e) => {
    dispatch(authError(e.message))
    console.error("INCATCH", e)
  })

Now you have the full object returned by CakePHP

And you can insert that into your react app UI

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

The reCAPTCHA verification period has expired. Please reload the page.

You May Also Like…

Squarespace Image Export

To gain continued access to your Squarespace website images after cancelling your subscription you have several...

MySQL 8.x GRANT ALL STATEMENT

-- CREATE CREATE USER 'tgnrestoreuser'@'localhost' IDENTIFIED BY 'AppleSauceLoveBird2024'; GRANT ALL PRIVILEGES ON...

Exetel Opt-Out of CGNAT

If your port forwards and inbound and/or outbound site-to-site VPN's have failed when switching to Exetel due to their...