I am trying to build my API with API Platform (Symfony 4) and everything seems to be fine but when I am using admin component to access secured endpoints (after I have been logged in successfully) server always returns 401 "JWT Token not found". I've found that Admin component is not sending the 'Authorization' header with token so server can't check token to authorize a request.
Since the Admin login works fine and token is always stored in localStorage (after successful login) I've tried to send a request with cURL and Postman with the stored token and server always returns valid response.
Any idea?
I am using code from API Platform documentation (https://api-platform.com/docs/admin/authentication-support/#authentication-support).
App.js
import React from 'react';
import parseHydraDocumentation from '@api-platform/api-doc-parser/lib/hydra/parseHydraDocumentation';
import {fetchHydra as baseFetchHydra, HydraAdmin, hydraClient} from '@api-platform/admin';
import authProvider from './authProvider';
import {Redirect, Route} from 'react-router-dom';
const entrypoint = process.env.REACT_APP_API_ENTRYPOINT;
const fetchHeaders = {'Authorization': `Bearer ${localStorage.getItem('token')}`};
const fetchHydra = (url, options = {}) => baseFetchHydra(url, {
...options,
headers: new Headers(fetchHeaders),
});
const dataProvider = api => hydraClient(api, fetchHydra);
const apiDocumentationParser = entrypoint =>
parseHydraDocumentation(entrypoint, {
headers: new Headers(fetchHeaders),
}).then(
({api}) => ({api}),
result => {
const {api, status} = result;
if (status === 401) {
return Promise.resolve({
api,
status,
customRoutes: [
<Route path="/" render={() => <Redirect to="/login"/>}/>,
],
});
}
return Promise.reject(result);
}
);
export default () => (
<HydraAdmin
apiDocumentationParser={apiDocumentationParser}
authProvider={authProvider}
entrypoint={entrypoint}
dataProvider={dataProvider}
/>
);
authProvider.js
import {AUTH_CHECK, AUTH_ERROR, AUTH_LOGIN, AUTH_LOGOUT} from 'react-admin';
const authenticationTokenUri = `${process.env.REACT_APP_API_ENTRYPOINT}/authentication_token`;
export default (type, params) => {
switch (type) {
case AUTH_LOGIN:
const {username, password} = params;
const request = new Request(authenticationTokenUri, {
method: 'POST',
body: JSON.stringify({email: username, password}),
headers: new Headers({'Content-Type': 'application/json'}),
});
return fetch(request)
.then(response => {
if (response.status < 200 || response.status >= 300) throw new Error(response.statusText);
return response.json();
})
.then(({token}) => {
localStorage.setItem('token', token);
window.location.replace('/');
});
case AUTH_LOGOUT:
localStorage.removeItem('token');
break;
case AUTH_ERROR:
if (401 === params.status || 403 === params.status) {
localStorage.removeItem('token');
return Promise.reject();
}
break;
case AUTH_CHECK:
return localStorage.getItem('token') ? Promise.resolve() : Promise.reject();
default:
return Promise.resolve();
}
}
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
serviceWorker.unregister();
I have found that the authorization header with token is sent with some requests, but all requests to API endpoints are sent without authorization header that results in 401.
a endpoint request headers - miss authorization header img
security.yaml
security:
encoders:
App\Entity\User:
algorithm: bcrypt
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
stateless: true
provider: app_user_provider
json_login:
check_path: /api/authentication_token
username_path: email
password_path: password
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
access_control:
# api platform has prefix /api
- { path: ^/api/authentication_token$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/, roles: IS_AUTHENTICATED_FULLY }
lexik_jwt_authentication.yaml
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'
Greeting entity
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* This is a dummy entity. Remove it!
*
* @ApiResource
* @ORM\Entity
*/
class Greeting
{
/**
* @var int The entity Id
*
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @var string A nice person
*
* @ORM\Column
* @Assert\NotBlank
*/
public $name = '';
public function getId(): int
{
return $this->id;
}
}
As you can see a request to a secured endpoint with Postman works. Postman printscreen
You need to set your Auth Header yourself. It won't be done by default.
const fetchHeaders = {'Authorization': `Bearer ${localStorage.getItem('token')}`};
const fetchHydra = (url, options = {}) => baseFetchHydra(url, {
...options,
headers: new Headers(fetchHeaders),
});