I'm trying to create REST API using Laravel. I'm using JWT (Tymon\JWTAuth) to authenticate users.
Here is part of my api.php file with /api routes:
Route::middleware('auth:api')->get("match/{id}", "ApiMatchController#getMatch");
Route::middleware('auth:api')->put("match/{id}", "ApiMatchController#editMatch");
Now, I'm sending GET request to /api/match/7. Authorized user gets match details as expected. Unauthorized user is redirected to root url / but I want user to stay on the url, I just want to return HTTP code 401 - Unauthorized. Where can I change this? I can do that inside of ApiMatchController#getMatch method but I would like middleware auth:api to do that for me. Is there any way how to do this?
Then, I'm sending PUT request to /api/match/7 with some data. Request from authorized user works just fine but unauthorized user now gets HTTP code 405 - Method Not Allowed (with debug info: Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException: The PUT method is not supported for this route. Supported methods: GET, HEAD.). Why? I cleared the route cache and as you can see, there IS a defined route in api.php. This behaviour really happens just with unauthorized user.
About the first part:
Authorized user gets match details as expected. Unauthorized user is redirected to root url / but I want user to stay on the url, I just want to return HTTP code 401 - Unauthorized. Where can I change this?
This is because your default guard is web, so in that case when a user tries to access a protected route it will be redirected to the home page (by default, this can also customized of course).
To change the default guard to api go to config/auth.php and change it like this:
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
Note: When making HTTP Requests, add this headers:
/** The following tells Laravel that you want a response in json format. */
Accept: application/json
/** The following is for POST/PUT/PATCH requests, it tells the request payload format. */
Content-type: application/json
About the second part:
Request from authorized user works just fine but unauthorized user now gets HTTP code 405 - Method Not Allowed
PHP doesn't handle well the PUT/PATCH/DELETE methods, in order to bypass this inconvinience do a POST request and then add a hidden _method field to the form.
The value sent with the _method field will be used as the HTTP request method:
Request body (the method is case sentitive):
Endpoint:
/api/match/7
Headers:
Accept: application/json
Content-type: application/json
Payload or Body:
_method: PUT
...
I have tried the combination of authorize method under controller, and Throwable mentioned in laravel docs:
https://laravel.com/docs/8.x/errors
try{
$this->authorize('create',Client::class);
} catch(Throwable $e)
{
echo $e->getMessage();
return false;
}
you can use same approach for your api responses.
Related
I am building a Web App along with API for Android using CI4.
For the Web App, I have a filter to check whether the user already logged in with some exceptions, one of them is to ignore the filter if the URL consists api/* (The url for API is http://localip/api/)
The API is working fine if the request method is GET. I can get the data from API. But when I tried to insert a data to database using POST method, it redirects me to login page (I'm using Postman to test the API)
How do I fix this?
What I have tried so far was adding login filter alias to
public $methods = [
'post' => ['csrf', 'loginfilter']
]; But still not working
Here is the full code
Filters.php
<?php
namespace Config;
use App\Filters\CorsFilter;
use App\Filters\LoginFilter;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Filters\CSRF;
use CodeIgniter\Filters\DebugToolbar;
use CodeIgniter\Filters\Honeypot;
use CodeIgniter\Filters\InvalidChars;
use CodeIgniter\Filters\SecureHeaders;
class Filters extends BaseConfig
{
/**
* Configures aliases for Filter classes to
* make reading things nicer and simpler.
*
* #var array
*/
public $aliases = [
'loginfilter' => LoginFilter::class,
'cors' => CorsFilter::class
];
/**
* List of filter aliases that are always
* applied before and after every request.
*
* #var array
*/
public $globals = [
'before' => [
// 'honeypot',
'csrf',
'loginfilter' => ['except' => ['/', '/login', 'api/*']],
'cors'
// 'invalidchars',
],
'after' => [
'toolbar',
// 'honeypot',
// 'secureheaders',
],
];
/**
* List of filter aliases that works on a
* particular HTTP method (GET, POST, etc.).
*
* Example:
* 'post' => ['csrf', 'throttle']
*
* #var array
*/
public $methods = [
'post' => ['csrf','loginfilter]
];
/**
* List of filter aliases that should run on any
* before or after URI patterns.
*
* Example:
* 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
*
* #var array
*/
public $filters = [];
}
LoginFilter.php
<?php
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
class LoginFilter implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null)
{
$session = session();
if (!$session->has('user_id')) {
return redirect()->to(base_url() . '/');
}
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
}
}
Routes.php
$routes->resource("api/role", ['controller' => 'apis\MasterDataRoleApi']);
MasterDataRoleApi.php (Controller)
<?php
namespace App\Controllers\apis;
use App\Models\GeneralModels;
use App\Models\RoleModel;
use CodeIgniter\API\ResponseTrait;
use CodeIgniter\RESTful\ResourceController;
class MasterDataRoleApi extends ResourceController
{
use ResponseTrait;
protected $model;
protected $generalModel;
public function __construct()
{
$this->model = new RoleModel();
$this->generalModel = new GeneralModels();
}
public function index()
{
$role= $this->request->getVar('role');
$data = $this->model->getRoleApi($role);
return $this->respond($data, 200);
}
public function create()
{
$roleName = $this->request->getPost('role_name');
$supervisor = $this->request->getPost('supervisor');
$userId = $this->request->getVar("userId");
helper('idgenerator');
$maxCode = $this->generalModel->getMaxData('tmrole', 'role_id');
$generatedId = idGenerator($maxCode[0]['role_id'], 4, 3, "JAB-");
$this->model->insertTmRole($generatedId, $roleName, $userId, $userId);
$data = array();
$dataArr = array(
"response" => "Success",
"response_details" => "Saved Successfully"
);
$data[] = $dataArr;
return $this->respondCreated($data, 201);
}
}
Image below shows Json returned when request method is GET https://i.stack.imgur.com/2ZKPd.png
Image below shows login page returned when request method is POST
https://i.stack.imgur.com/11R0z.png
Thank you in advance.
PART A: CSRF
Explanation 1:
Image below shows Json returned when request method is GET
https://i.stack.imgur.com/2ZKPd.png
Your API GET requests work just fine because they're not protected.
Cross-site request forgery (CSRF)
The CSRF Protection is only available for
POST/PUT/PATCH/DELETE requests. Requests for other methods are
not protected.
Explanation 2:
Image below shows login page returned when request method is POST
https://i.stack.imgur.com/11R0z.png
Error
The action you requested is not allowed.
That error comes from system/Security/Security.php::verify()
throw SecurityException::forDisallowedAction();
You normally receive this error in 2 scenarios:
When you forgot to submit the CSRF token along with your request (POST/PUT/etc.).
When the CSRF token submitted along with your HTML form/request body doesn't match the one that exists in the CSRF cookie.
After having turned on the CSRF filter, when you make an HTTP request and are missing a CSRF cookie, one is auto-generated for you and sent in the HTTP Set-Cookie: '...' response header.
In case you're using the default Cookie-based CSRF Protection, with any further (POST/PUT/etc.) requests made, you will be expected to submit a matching CSRF token along with your request.
Double Submit Cookie
This technique is easy to implement and is stateless. In this
technique, we send a random value in both a cookie and as a request
parameter, with the server verifying if the cookie value and request
value match. When a user visits (even before authenticating to prevent
login CSRF), the site should generate a (cryptographically strong)
pseudorandom value and set it as a cookie on the user's machine
separate from the session identifier. The site then requires that
every transaction request include this pseudorandom value as a hidden
form value (or other request parameter/header). If both of them match
at server side, the server accepts it as legitimate request and if
they don't, it would reject the request.
CSRF Solution:
To pass the Double Submit Cookie test, every time you make a (POST/PUT/etc.) request, you will need to submit a CSRF cookie and a request parameter/header having a matching CSRF token. I.e:
Requirement 1: Cookie: csrf_cookie_name=ccd8facfa8229bdba5e0160c108d1a02; HTTP request header.
Requirement 2:
The Order of Token Sent by Users
The order of checking the availability of the CSRF token is as follows:
$_POST array (I.e: <input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>" />).
HTTP header (I.e: X-CSRF-TOKEN: ccd8facfa8229bdba5e0160c108d1a02).
php://input (JSON request) - bear in mind that this approach is the slowest one since we have to decode JSON and then re-encode it. (I.e: {"csrf_cookie_name": "ccd8facfa8229bdba5e0160c108d1a02"})
Requirement 1 is normally auto-fulfilled by the browser if that's where you're initiating the request from. Nonetheless, you can manually fulfil it. I.e: In your PHP main view template, you can use Ajax:
<script>
$.ajaxSetup({
headers: {
"<?php echo csrf_header();?>": "<?php echo csrf_hash();?>",
"Cookie": "<?php echo csrf_token();?>"="<?php echo csrf_hash();?>"; "<?php echo session_name();?>":"<?php echo session_id();?>"
}
});
<script>
Below are my personal .env file configurations that worked for me.
Change myapp.local to your application's domain.
.env file.
app.baseURL = 'http://myapp.local'
app.sessionExpiration = 86400
cookie.domain = '.myapp.local'
cookie.httponly = false
security.expires = 86400
security.regenerate = false
The rest of the .env file configurations related to SECURITY/CSRF, COOKIE, APP/SESSION were maintained in their default setting.
As I conclude the CSRF section, if you still have issues, install & configure Xdebug, add break points inside this method system/Security/Security.php::verify() and confirm if your HTTP request is passing the conditions in there. Make sure your Xdebug configurations are similar to: (xdebug.log_level = 0 | xdebug.mode = debug,develop | xdebug.start_with_request = yes | xdebug.client_port = 9004 | xdebug.client_host = "localhost" | xdebug.trace_output_name = "trace.%c.%t-%s.%H_%R" | xdebug.profiler_output_name = "cachegrind.out.%t-%s.%H_%R" | xdebug.remote_handler = "dbgp" | xdebug.show_local_vars = 9)
PART B: Postman OR Insomnia
This section covers setting up your REST API client application tool to be able to test your project's end-points.
HTTP Request Header Requirements:
X-Requested-With: XMLHttpRequest
X-CSRF-TOKEN: 62b04a891414ef789bee7108f94ad97a
Content-Type: application/x-www-form-urlencoded
Accept: */*
Cookie: csrf_cookie_name=62b04a891414ef789bee7108f94ad97a; ci_session=4ji7amn186ckbo0gutdoe3ai6ufumk4e
User-Agent: insomnia/2022.1.1
Host: myapp.local
The above headers are mandatory. You may add other HTTP request headers after the ones listed above.
The User-Agent can vary depending on if you are using one tool or another (I.e Insomnia or Postman).
The Host can vary as well, depending on how you set up your application's base URL.
The Content-Type can vary, depending on the kind of data you're submitting to the server. (I.e. HTML FORM -> application/x-www-form-urlencoded and JSON -> application/json).
As for the Cookie header, you may need to attach the ci_session (session_id) cookie if in case your endpoint is behind an Authentication system or Login filter. Of course, this may not be necessary if your API uses a different Auth mechanism (i.e: using Bearer/Access tokens with an extra Authorization: Bearer xxx_my_bearer_token_here header).
In addition, you may need to attach a csrf_cookie_name cookie as shown above if your application has enabled the csrf filter. In this case, you may want to create a dedicated GET API route endpoint specifically to allow you receive a csrf cookie name and value (hash). Alternatively, if you're too lazy to set up a dedicated endpoint for sending out CSRF tokens and you already have the web app running, open your browser's console while logging in/navigating through your app, in the Application TAB -> Cookies -> your domain, you should be able to see and utilize these cookies in Postman / Insomnia.
The X-CSRF-TOKEN HTTP request header value should be the same as the csrf_cookie_name cookie value.
HTTP Request Call Requirements:
I.e:
POST http://myapp.local/api/companies
Request Body. Don't forget to add the csrf_cookie_name token as part of the request body.
Key
Value
company_name
Tesla, Inc.
csrf_cookie_name
62b04a891414ef789bee7108f94ad97a
As I conclude with PART B:
Important items
Description
ci_session cookie or an Authorization: Bearer xxxxxxx HTTP request header.
Allows you to authenticate with your application/project only for Auth protected API endpoints. In your particular case, I believe your loginfilter is working with a ci_session cookie and the cookie is expected to be sent along with every request with the help of the Cookie HTTP request header.
csrf_cookie_name cookie and (a X-CSRF-TOKEN HTTP request header or CSRF token request parameter).
The CSRF cookie and (X-CSRF-TOKEN HTTP request header or CSRF token request parameter) values MUST match. This is a requirement if you've turned on the csrf filter.
From what I can see is, you have loginfilter as a fallback for every POST method. That might be the culprit over there.
That being said, here is an alternate solution. You could group the routes in routes.php and apply loginfilter to those routes. Additionally you can nest them and partition them as you want.
Example :
$routes->group(
'api',
['filter' => 'loginfilter'],
function ($routes) {
$routes->resource("role", ['controller' => 'apis\MasterDataRoleApi']);
}
);
You can remove the global filters while using this method.
In my routes/api.php I have a POST method route as such:
Route::post('hello', 'MyController#greet');
In the app/Http/Controllers/MyController.php the greet() method is defined as such:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class MyController extends Controller
{
public function greet(Request $request)
{
return response()->json(['not found'], 404);
}
}
Now, depending on the URL protocol that was calling from, the response is returning totally different messages:
Calling from POST https://localhost/hello will return 404 Not Found response:
[
"not found"
]
as expected.
Wheres, when calling from POST http://localhost/hello (without https) it will return 405 Method Not Allowed response instead:
{
"message": "The GET method is not supported for this route. Supported methods: POST."
}
This is a problem because I'm setting up my server behind AWS ELB which accepts HTTPS traffic only but will route back to the Auto Scaling instances via HTTP within the local VPC network.
So my question is what is causing this and how can I prevent Laravel from returning a different response based on URL protocol?
EDIT: Note that this only happens when I specify the status code in the response()->json($data, [status code]). Otherwise, the response works normally as expected.
None of those has to work, because you have defined your route as a post. Your requests are 'get' requests. It seems there is middleware in a higher priority and it's being called when it's an HTTP request to control illegal requests.
Try returning different things like dd("Hello World"); if it's loading the page there has to be a bug or something.
Temporarily add a middleware to your route to redirect requests to one of HTTP or HTTPS protocols
I solved it.
It turns out that I was using the nginx configuration from https://laravel.com/docs/5.8/deployment#server-configuration and included the following line:
error_page 404 /index.php;
This causes all 404 responses to redirect to the /index.php page, which is not defined in my Laravel routes, and the path expects a GET instead of POST method. Hence, it returns the error message stated in my question above.
After removing the above config line in my nginx configuration, my POST requests with 404 responses are working correctly again.
Thanks for all the help anyway.
I'm new to laravel and sorry for the silly question I've posted a request via postman with two parameters,In my routes api.php file,
Route::post('/mail', 'TripsController#mail');
with header,
Accept:application/json
And exclude the token verify in VerifyCSRKToken as,
protected $except = ['api/*',
];
and my url:
http://localhost/Movecabadmin/api/mail
It returns the message as {"message":"Unauthenticated."}
Question 1:Is I need to pass any authentication value with the request?If it is then How?
Question 2:How to get the passed parameters in my controller?
You are getting this {"message":"Unauthenticated."} because the Route::post('/mail', 'TripsController#mail');
going under some kind of authentication or middleware.
Remove that and you will get your desired result.
So I have an application that sends an AJAX request to an external server which does some stuff then makes a post call to the laravel application that made the initial call.
So I am trying to do a POST call to the laravel application. Now from the AJAX request i am sending the csrf_token()
Here are the headers I've put into my post request:
X-CSRF-TOKEN: LO8Dg7j1jZssXXGSLIa8inBgh2Y1QSsp6Birc1Ui
X-Requested-With: XMLHttpRequest
Content-Type: application/x-www-form-urlencoded
That token belongs to the logged in user that made the AJAX request. Now the problem i'm getting is from this i get a TokenMismatchException500 error.
Is there something else I need to do in my external post calls headers in order to not encounter this error?
As i've got the CSRF token i am using a rest client to try send a test post using those headers and I get the same error?
The aim is on the recieving laravel app controller will then be able to use the token and i'll be able to use $request->user(); to get the user.
Note the recieving route has the web middleware attached to it.
Since you are making request from another server external url.Because of this you are getting erorr.Csrf token works with the same application not working if you try to exicute from other application. so you can disable csrf token.
if you want to disable token for all request then add this in VerifyCsrfToken
protected $except = [
'/*',
];
Excluding URIs From CSRF Protection
Sometimes you may wish to exclude a set of URIs from CSRF protection. For example, if you are using Stripe to process payments and are utilizing their webhook system, you will need to exclude your Stripe webhook handler route from CSRF protection since Stripe will not know what CSRF token to send to your routes.
Typically, you should place these kinds of routes outside of the web middleware group that the RouteServiceProvider applies to all routes in the routes/web.php file. However, you may also exclude the routes by adding their URIs to the $except property of the VerifyCsrfToken middleware:
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* #var array
*/
protected $except = [
'stripe/*',
];
}
Ref:
https://laravel.com/docs/5.5/csrf
I have a laravel api that I'm trying to run a custom artisan command to process a transaction. The api is suppose to check for pending transactions in our merchant database and post them in our transaction database.
I get the following error:
[GuzzleHttp\Exception\ClientException]
Client error: `POST http://paycentral.mymarket.com/transactions/bulk` resulted in a `405 Method Not Allowed` response:
{"error":{"message":"405 Method Not Allowed","status_code":405,"debug":{"line":446,"file":"\/var\/www\/vhosts\/maindomai (truncated...)
The API I'm using is located at api.mymarket.com. Searching for errors like this has me believing it's a CORS-related issue. I'm using laravel-cors and added Header set Access-Control-Allow-Origin "*" to the .htaccess in both the public folder for api.mymarket.com and paycentral.mymarket.com. The error is still persisting though. Is there any other possible workaround? We are currently using plesk for our hosting services.
UPDATE: I tried doing a preflight request in the pay subdomain
Origin: api.mymarket.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: MM
It returned a 500 Internal Error which is progress I guess.
UPDATE Here is the routes.php for paycentral. The cors-library is registered in the app.php.
paycentral routes.php
<?php
$api = app('Dingo\Api\Routing\Router');
// all routes are protected by the Authenticate middleware which makes sure the client
// is authenticated as *somebody* - each resource is further protected by the authorization
// policies in the App\Api\V1\Policies files to limit the method calls by which client
// type is attempting to access the resource - these must be mapped in the AuthServiceProvider
$api->group([
'version' => 'v1',
'namespace' => 'App\Api\V1\Controllers',
'middleware' => 'auth' // use the Authenticate middleware
], function($api) {
/*
* partial CRUD resource routes
*/
$api->get('transactions/{id}', 'TransactionController#show');
$api->post('transactions', 'TransactionController#store');
$api->put('transactions/{id}', 'TransactionController#update');
$api->post('transactions/bulk', 'TransactionController#store_bulk');
$api->post('transactions/get_updates', 'TransactionController#get_updates');
I solved the issue. It was an issue with one of the routes not pointing to transactions/bulk. The previous developer made undocumented changes to a couple files without following our version control methods so the production branch was broken.
Assuming that your route is defined well in the routes.php, and that everything else is fine. Then you may try adding the following line in your filters.php
App::before(function ($request) {
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS');
}