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