I am using the Microsoft Graph and I need to set up a webhook to receive changes to email and calendar events. I was able to get it working with my PHP Laravel application, but now that I am trying to subscribe to notifications, I am running into issues with validating the notificationUrl, which is pointing to a public server of mine.
The script for creating the webhook is returning the following error:
Client error: POST https://graph.microsoft.com/v1.0/subscriptions resulted in a 400 Bad Request response:
{
"error": {
"code": "InvalidRequest",
"message": "Subscription validation request failed. Response must ex (truncated...)
The truncated part I believe is
Subscription validation request failed. Must respond with 200 OK to this request.
Here is my code for creating the subscription:
$data = [
"changeType" => "created",
"notificationUrl" => "https://anatbanielmethod.successengine.net/office365/webhooks/events",
"resource" => "me/events",
"expirationDateTime" => "2018-12-20T18:23:45.9356913Z",
"clientState" => "secret",
];
$result = $graph->createRequest('POST', '/subscriptions')
->attachBody($data)
->execute();
and here is my method for my notificationUrl:
public function events()
{
//if validationToken exists return that to validate notificationUrl
if(isset($_REQUEST['validationToken'])){
return response($_REQUEST['validationToken'], 200)
->header('Content-Type', 'text/plain');
}
//process event normally for those that have already been validated
}
Once again this URL is public and live and I have tested it by using Postman to send it test posts and it is working fine. Also, I added this route to my VerifyCsrfToken middleware to allow a third party post to hit this URL.
Originally I set up a simple single page PHP script to test validating the notificationUrl and that simple script worked fine. It successfully validates Webhooks created that point to it. Here is that one page script code:
<?php
if(isset($_REQUEST['validationToken'])){
echo $_REQUEST['validationToken']; // needed only once when subscribing
} else {
//process like normal not a validation Token request...
}
}
So I would expect that the Laravel endpoint would work like the simple one page PHP script, and it is when I test both URLs in Postman, but the Laravel endpoint is not validating when Office365 attempts to validate it when creating a new webhook.
I have searched all over for help on this and read through all of the Microsoft developer documentation I can find on webhooks and these are some of the more helpful parts of the documentation but I am still not finding an answer to this issue:
https://learn.microsoft.com/en-us/graph/api/subscription-post-subscriptions?view=graph-rest-1.0
https://learn.microsoft.com/en-us/graph/webhooks#notification-endpoint-validation
Any ideas of this?
Thanks Marc! You were correct about the linefeed being the issue, I am still not sure where the line feed is coming from, some how Laravel appears to be adding it. Needless to say I found a solution by adding an "ob_clean();" right before returning the response. Below is my updated notificationUrl method:
public function events()
{
//if validationToken exists return that to validate notificationUrl
if(isset($_REQUEST['validationToken'])){
ob_clean();//this line is cleaning out that previously added linefeed
return response($_REQUEST['validationToken'], 200)
->header('Content-Type', 'text/plain');
}
//process event normally for those that have already been validated
}
It's odd that JakeD's answer requires the use of ob_clean(). here is my webhook controller method in my Laravel 5.7.x app:
use Illuminate\Http\Request;
public function webhook (Request $request) {
if (filled($request->input('validationToken'))) {
return response($request->input('validationToken'))
->header('Content-Type', 'text/plain');
}
// code to process the webhook after validation is complete
}
I don't see an extra linefeed character and the Microsoft Graph API subscription is validated and created.
Related
I have been trying to set up WhatsApp cloud API on my Laravel MVC project. I'm stuck trying to set up webhook to receive WhatsApp notifications when someone sends a message. The below is my code and it is not simply working giving server error or 405 method not allowed error, in the WhatsApp cloud api side it does not pass the validation point.
API
Route::GET('/webhook' , 'admin\InventoryInvoiceController#webhook')->name('webhook');
Controller
public function webhook() {
if($_SERVER['REQUEST_METHOD']=="GET"){
echo $_GET['hub_challenge']; //respond back hub_callenge key
http_response_code(200);
}else{
$data = json_decode(file_get_contents('php://input'), true);
error_log(json_encode($data)); //print inbound message
}
}
I have added this route to the exception so it's run with the need for CSRF validation. Error received on the Whatsapp API Cloud side:
The callback URL or verify token couldn't be validated. Please verify
the provided information or try again later.
What can I try next?
The problem was that I had not added my route to the CSRF exception which would invalidate the request send by Whatsapp cloud api. Make sure to do that before testing! Below is the code:
public function webhook(Request $request) {
$mode = $request->hub_mode;
$challenge = $request->hub_challenge;
$token = $request->hub_verify_token;
echo $challenge;
}
public function __construct() {
$this->middleware('auth:admin',
['except' => ['webhook', 'webhookpost']]
);
I'm trying to comunicate with an already working RestAPI server developed in PHP (CakePHP framework); i'm trying to make a simple login action in Angular 7 application and if success i will proceed with the implementations.
This is the Angular App call code:
constructor(protected cli: HttpClient) {
this.tablet_couple = new TabletCoupleModule();
}
ngOnInit() {
this.cli.get('http://work.local/grai/api-angular/api/v1/tablet_couples/1.json')
.subscribe(
data => { console.log(data) }
)
this.cli.post('http://work.local/grai/api-angular/api/v1/tablet_couples/login.json',{
username: 'xxxxxxxx',
passoword: '123456789',
})
.subscribe(
data => { console.log(data) }
)
}
The actual problem is that the GET call work fine, but the POST call still no working.
I'm sure the REST API is working correctly because if i use tool like Insomnia the response is correct for both calls.
I try to find why but the problem is every time the CORS implementation:
I have try to force headers in Cakephp as you can see above but still not working.
public function beforeFilter(\Cake\Event\Event $event)
{
parent::beforeFilter($event);
if($this->request->is('OPTIONS')) {
$this->response->header('Access-Control-Allow-Origin','*');
$this->response->header('Access-Control-Allow-Methods','*');
$this->response->header('Access-Control-Allow-Headers','Content-Type, Authorization');
}
else {
$this->response = $this->response->withHeader("Access-Control-Allow-Origin","*");
$this->response = $this->response->withHeader("Access-Control-Allow-Headers","Origin, X-Requested-With, Content-Type, Accept");
$this->response = $this->response->withHeader("Access-Control-Allow-Methods","*");
}
}
UPDATE 1
I had find a library to integrate CORS with cakephp : cakephp-cors
This help but i still have a problem: i can't use Rest API if they are not on the same domain (ok is CORS) but i need.
IF i deploy the application and put my Angular App on the same domain it works; but i want deploy app that can access remote REST API.
Any suggestion?
Ok, i had a "solution" tanks to the #JensV.
1th :: cakephp 3.1.x is too old
I'm using an OLD version of CakePHP (3.1.14); the 3.1.x is simply too old to manager correctly an OPTIONS Method Call.
Using CakePHP 3.7.* with the plugin cakephp-cors it works correctly and now i can make remote call from localhost:4200 .
2nd :: the Method OPTIONS
This is a method that give me so much pain; this method is called by browsers before a POST method call.
The correct response at this method is a HTTP Code 200 from the server.
If you don't manage this call correctly the browser truncate the POST call.
Again i post some reference about this call and how to find a solution:
CakePHP, preflight & CORS
Handling “XMLHttpRequest” OPTIONS Pre-flight Request in Laravel
I hope it helps someone.
I am trying to implement a REST API for basic CRUD operations with Laravel 5.4
I have a post method as below.
public function storeReport(StoreReportRequest $request){
$inputs = $request->only('company_name',
'company_experience',
'service_start_time',
'service_end_time',
'latitude',
'longitude',
'additional_info',
'user_id'
);
//save image content to folder and image name to database
$image = $request->file('picture');
$fileName = $image->getClientOriginalName();
$image->storeAs('images', $fileName);
$inputs['picture_url'] = $fileName;
$report = Report::create($inputs);
return 'success';
}
I do not have any problems with storing the data.I have some rules for validation and i do validation via StoreReportRequest class.What i want to learn is how can i handle the response after POST request. For example if validation fails or any exception occurs what should i return and how it should be. I have done some research but couldnt find any proper answers. Any help would be appreciated.
Well, I normally do a response in json wich is always easy to parse, on the end of your function if everything was correctly done you can do this:
return response()->json(["response" => true]);
and if failed for some reason
return response()->json(["response" => false, errors => ["This can be an array or whatever"] ]);
The validation will return a json response with the error messages with a status code of 422. So you don't have to worry about the validation failure exceptions.
As for what you want to do after creating the report is upto you. You could return a json response with the newly created report or send a success message with the status code 200 (set by default).
Laravel does most of the work for you and you just need to define the responses based on your API requirements.
If the incoming request was an AJAX request, no redirect will be
generated. Instead, an HTTP response with a 422 status code will be
returned to the browser containing a JSON representation of the
validation errors.
This is not working! I am trying to access the route via an ajax request and it redirects back.
If validation passes, your code will keep executing normally. However, if validation fails, an Illuminate\Contracts\Validation\ValidationException will be thrown. This exception is automatically caught and a redirect is generated to the user's previous location. The validation errors are even automatically flashed to the session!
Now I want to know where does laravel catch this exception so that I can modify it?
This is handled inside the FormRequest class:
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException($this->response(
$this->formatErrors($validator)
));
}
You can override this function in your own Request object and handle a failed validation any way you like.
After been researching for a while I will post my results so anyone with this problem saves a lot of time.
#Faiz, you technically shouldn't change a thing if you want to stick to laravel behavior (I'll always try to follow taylor's recommendations). So, to receive a 422 response code status you need to tell phpunit you will send a XMLHttpRequest. That said, this works on laravel 5
$response = $this->call('POST', $url, [], [], [],
['HTTP_X_Requested-With' => 'XMLHttpRequest']);
More information at Github Issues. Besides, if you look at Symfony\Component\HttpFoundation\Request#isXmlHttpRequest you will find that this header is used by "common JavaScript frameworks" and refers to this link
Haven't tested on the browser yet, but I think this should work too.
I understand how to validate requests by type-hinting the class name in the controller method. However for Ajax requests, According to the documentation, I should validate data in the controller, because using a validator class will redirect rather than send a response.
The main part I'm looking at is this:
If the incoming request was an AJAX request, no redirect will be
generated. Instead, an HTTP response with a 422 status code will be
returned to the browser containing a JSON representation of the validation errors.
However, my controller is as follows:
public function update(App\Permission $permission, Request $request)
{
$this->validate($request, [
'permission_description' => 'required|string'
]);
...
}
And I can't for the life of me get it to respond with JSON. The documentation states that if it fails, it throws an Illuminate\Contracts\Validation\ValidationException exception, but I can't catch it.
Whenever it fails, it always redirects back to the edit page. Obviously I don't want this, I want the json response.
I have just tried "manually writing it out" with the whole $v = Validator::make($request->all(), ...); which does work, but what's the point in using the $this->validate() way if it doesn't work?
Does the $this->validate() method just not work with AJAX and I have to write it the long way each time? Am I doing something wrong?!
Below is what I've tried:
public function update(App\Permission $permission, UpdatePermissionRequest $request)
{
/** Redirects rather than returns JSON if the validation fails **/
}
----------------------------------
public function update(App\Permission $permission, Request $request)
{
$this->validate($request, [
'permission_description' => 'required|string'
]);
/** AND I've also tried: **/
try {
$this->validate($request, ['permission_description' => 'required|string']);
} catch (\Illuminate\Contracts\Validation\ValidationException $e {
echo $e; /** Echoing for debug reasons **/
exit;
}
...
/** Still redirects the browser, even if it is an AJAX request **/
}
-----------------------------------------
use Validator;
...
public function update(App\Permission $permission, Request $request)
{
$v = Validator::make($request->all(), [
'permission_description' => 'required|string'
]);
if($v->fails())
{
return response()->json(['reply' => false]);
}
/** Works **/
}
UPDATE
The documentation is incorrect. It states that the $this->validate() method throws a Illuminate\Contracts\Validation\ValidationException but it doesn't. It throws a Illuminate\Http\Exception\HttpResponseException exception.
Simply telling that you want json in the header should also fix this. Laravel checks if the request is ajax of if json is requested.
if ($this->ajax() || $this->wantsJson())
{
return new JsonResponse($errors, 422);
}
Solution:
Add header
Accept: application/json
Your statement that the docs say it is best to validate AJAX requests in the controller is simply incorrect.
If you scroll a bit further down from what you linked - you'll see this under the FormValidation section
If validation fails, a redirect response will be generated to send the
user back to their previous location. The errors will also be flashed
to the session so they are available for display. If the request was
an AJAX request, a HTTP response with a 422 status code will be
returned to the user including a JSON representation of the validation
errors.
In other words - there is no reason you cannot do this in a simple FormRequest and simply your code significantly. It will automatically handle the fact it is an AJAX call and return the appropriate HTTP responses.
I do this all the time in my L5 apps - it works flawlessly.
Ok, so it looks like there were 2 contributing factors.
The reason it was redirecting instead of responding with JSON is because the X-Requested-With field wasn't set on the AJAX request. This the AJAX wrapper for the application was setup to deal with Cross Domain requests, which seems to strip out the X-Requested-With field, which makes the ultimate request look non-ajax to the server - hence the redirection.
The reason why it wasn't catching the Illuminate\Contracts\Validation\ValidationException exception is because that exception is not thrown. If you open up Illuminate\Foundation\Validation\ValidatesRequest.php, and search for the function throwValidationException(), it actually throws a HttpResponseException instead. I attempted to catch the HttpResponseException and it was caught successfully - I assume the documentation is wrong.
The solution was to either remove the cross domain attributes on the ajax request, add the X-Requested-With header manually, as per the answer in this post. This would make the application see that it was an AJAX request.
And if you wanted to manually catch the exception, you need to catch the HttpResponseException, not the ValidationException.