I understand the process of using nonces when I create my own templates.
But I am developing a ReactJS App which uses ONLY the Wordpress REST API for pulling data, so the user never gets to the index.php, but does Ajax calls to the WP Rest Api.
Now I cannot get the nonce stuff to work.
This is what I have done so far:
I added the following endpoints:
register_rest_route('frontend', '/customer/', array(
'methods' => 'GET',
'callback' => 'get_customer'
));
register_rest_route('frontend', '/customer/', array(
'methods' => 'POST',
'callback' => 'create_user_and_login'
));
These are my functions:
function get_customer()
{
return get_current_user_id();
}
function create_user_and_login(){
// dummy data for testing
$credentials = ['user_login' => 'mail#mymail.de', 'user_password' => 'XXXX', 'remember' => true];
// create a new user/customer via woocommerce
$customerId = wc_create_new_customer($credentials['user_login'], $credentials['user_login'], $credentials['user_password']);
if(is_a($customerId,'WP_Error')) {
return $customerId;
}
// get the user & log in
$user = get_user_by( 'id', $customerId );
if( $user ) {
wp_set_current_user( $customerId);
wp_set_auth_cookie( $customerId );
}
// create new nonce and return it
$my_nonce = wp_create_nonce('wp_rest');
return $my_nonce;
}
If I now run a POST to /customer which triggers create_user_and_login(), the newly created nonce is returned in the ajax response. Then I use the returned nonce to run my next request, a GET to /customer?_wpnonce=MY-NONCE, but I get the error:
{
"code": "rest_cookie_invalid_nonce",
"message": "Cookie nonce is invalid",
"data": {
"status": 403
}
}
I checked the nonce documentation but I could not find a solution for my problem. Could it be that the sessions are out of sync? So that the nonce is created on the wrong session or wp_set_auth_cookie and wp_set_current_user are not called correctly? Or do I have to use the wp_localize_script function? This will get problematic, as I want to have the ReactJS and the Wordpress backend separated.
I got two cookies after the POST, a wordpress cookie and a wordpress_logged_in cookie.
What am I missing?
Check this answer
It seems that when you call $my_nonce = wp_create_nonce('wp_rest'); the nonce is created with the old session cookie, even when you call wp_set_auth_cookie and wp_set_current_user. But in the next request the session is updated, meaning that the nonce is wrong.
As in the answer, add the following hook (functions.php for example) to force an update of the cookie:
function my_update_cookie( $logged_in_cookie ){
$_COOKIE[LOGGED_IN_COOKIE] = $logged_in_cookie;
}
add_action( 'set_logged_in_cookie', 'my_update_cookie' );
Related
I've been searching for this for hours, I hope someone here can help.
I built a subscription based site on Laravel and PayPal subscriptions using the PayPal PHP SDK.
Everything works perfectly except on thing:
I created a webhook for when a user cancels the payment on his end. I'm getting this error:
Got Http response code 400 when accessing https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature.{"name":"VALIDATION_ERROR","message":"Invalid data provided","debug_id":"7225cebfec35a","information_link":"https://developer.paypal.com/docs/api/webhooks/#errors","details":[{"field":"webhook_id","location":"body","issue":"Required field cannot be blank"}],"links":[]}
Here is my code:
public function webhook()
{
/**
* Receive the entire body that you received from PayPal webhook.
*/
$bodyReceived = file_get_contents('php://input');
// Receive HTTP headers that you received from PayPal webhook.
$headers = getallheaders();
/**
* Uppercase all the headers for consistency
*/
$headers = array_change_key_case($headers, CASE_UPPER);
$signatureVerification = new \PayPal\Api\VerifyWebhookSignature();
$signatureVerification->setWebhookId(env('PAYPAL_WEBHOOK_ID'));
$signatureVerification->setAuthAlgo($headers['PAYPAL-AUTH-ALGO']);
$signatureVerification->setTransmissionId($headers['PAYPAL-TRANSMISSION-ID']);
$signatureVerification->setCertUrl($headers['PAYPAL-CERT-URL']);
$signatureVerification->setTransmissionSig($headers['PAYPAL-TRANSMISSION-SIG']);
$signatureVerification->setTransmissionTime($headers['PAYPAL-TRANSMISSION-TIME']);
$webhookEvent = new \PayPal\Api\WebhookEvent();
$webhookEvent->fromJson($bodyReceived);
$signatureVerification->setWebhookEvent($webhookEvent);
$request = clone $signatureVerification;
try {
$output = $signatureVerification->post($this->apiContext);
} catch(\Exception $ex) {
//This is where it fails
print_r($ex->getMessage());
exit(1);
}
$verificationStatus = $output->getVerificationStatus();
$responseArray = json_decode($request->toJSON(), true);
$event = $responseArray['webhook_event']['event_type'];
if ($verificationStatus == 'SUCCESS')
{
switch($event)
{
case 'BILLING.SUBSCRIPTION.CANCELLED':
case 'BILLING.SUBSCRIPTION.SUSPENDED':
case 'BILLING.SUBSCRIPTION.EXPIRED':
case 'BILLING_AGREEMENTS.AGREEMENT.CANCELLED':
// $user = User::where('payer_id',$responseArray['webhook_event']['resource']['payer']['payer_info']['payer_id'])->first();
$this->updateStatus($responseArray['webhook_event']['resource']['payer']['payer_info']['payer_id'], 0,1);
break;
}
}
echo $verificationStatus;
exit(0);
}
And here is the $this->apiContext:
trait PayPalApiCredentialsTrait {
private $apiContext;
public function setCredentials()
{
$this->apiContext = new \PayPal\Rest\ApiContext(
new \PayPal\Auth\OAuthTokenCredential(
env('PAYPAL_CLIENT_ID'), // ClientID
env('PAYPAL_CLIENT_SECRET') // ClientSecret
)
);
$this->apiContext->setConfig(
array(
'mode' => env("PAYPAL_MODE"),
'log.LogEnabled' => true,
'log.FileName' => '../PayPal.log',
'log.LogLevel' => 'INFO', // PLEASE USE `INFO` LEVEL FOR LOGGING IN LIVE ENVIRONMENTS
)
);
}
}
This is the error I get from the paypal.log:
[01-09-2020 15:54:18] PayPal\Core\PayPalHttpConnection : INFO: POST https://api.sandbox.paypal.com/v1/oauth2/token
[01-09-2020 15:54:18] PayPal\Core\PayPalHttpConnection : INFO: Response Status : 200
[01-09-2020 15:54:18] PayPal\Core\PayPalHttpConnection : INFO: POST https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature
[01-09-2020 15:54:19] PayPal\Core\PayPalHttpConnection : INFO: Response Status : 400
[01-09-2020 15:54:19] PayPal\Core\PayPalHttpConnection : ERROR: Got Http response code 400 when accessing https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature. {"name":"VALIDATION_ERROR","message":"Invalid data provided","debug_id":"26b12ee43cddd","information_link":"https://developer.paypal.com/docs/api/webhooks/#errors","details":[{"field":"webhook_id","location":"body","issue":"Required field cannot be blank"}],"links":[]}
I must mention that everything else works fine.
Creating plans, agreements, cancelling the both, showing active plans, and more...
Everything works smoothly.
This is the only thing that I can't seem to fix.
If anyone could figure this out for me, I'd really appreciate it.
Thank you!
The PayPal-PHP-SDK is deprecated and no longer maintained; it should not be used for new integrations. Its implementation of billing plans and subscriptions is old, and not compatible with the current version of the Subscriptions API.
In its place, a direct HTTPS integration (no SDK) should be used for Subscriptions, and for verifying webhooks.
(For the one time payment use case, there is a new v2 Checkout-PHP-SDK https://developer.paypal.com/docs/api/rest-sdks/ )
I am working on a project for my self learning that has laravel in the backend and running react native in the front end. I have implemented the login and register screen for my apps. Now I am trying to connect it to my laravel server through its api routes. I was first having CORS issue so I solved it by making a new middleware and editing the kernel.php file as stated in this thread.
CORS Issue with React app and Laravel API
Now I tried to run some tests first with get request my submit function in react is
handleSubmit = async() => {
const email = this.state.email
const pass = this.state.password
const proxyurl = "https://cors-anywhere.herokuapp.com/";
/*TEST FOR GET REQUEST */
let response = await fetch(`http://mobileprediction/api/user?email=${email}&pass=${pass}`, {
headers: {
"Content-Type" : "application/json",
"Accept" : "application/json"
},
})
let result = await response.json()
console.log(result)
}
and my api.php file in the routes of laravel was
Route::get("/user", function (Request $request) {
return $request;
});
and it gave the desired output, but then when I tested the same way with a post request I am getting an empty array no matter what and I am unable to figure out what the problem is
the handlesubmit function in my react native app is
handleSubmit = async() => {
const email = this.state.email
const pass = this.state.password
/*TEST FOR POST REQUEST */
let response = await fetch(`http://mobileprediction/api/user`, {
method: "POST",
header : {
"Content-Type" : "application/json",
"Accept" : "application/json"
},
body : JSON.stringify({
emailid : email,
password : pass
}),
})
let result = await response.json()
console.log(result)
}
and api.php file in laravel is
Route::get("/user", function (Request $request) {
return $request;
});
Route::post("/user", function(Request $request) {
return $request;
});
I think you write your routes in web.php, for your API could write the endpoints in api.php.
Try to comment VerifyCsrfToken middleware in app/Http/Kenrel.php.
it has security issue, but you can do it in your learning steps.
[ https://laravel.com/docs/6.x/routing ] [search CSRF in this link]
Any routes pointing to POST, PUT, or DELETE routes that are defined in the web routes file should include a CSRF token field.
So what I understood is that from the fetch request in react native its not sending just the inputs but rather a page with a body that has json formatted key values. So I cant access data in my server as
$request->param
or with
request("param")
you could get the json formatted string with
$request->json()->all()
but still I couldnt get to the individual values
for me what was working is to get all the contents and then access the values with
$postInput = file_get_contents('php://input');
$data = json_decode($postInput, true);
return ["email" => $data["emailid"] ];
I am using Vuejs SPA with Laravel API as backend. I successfully got the personal access token and store in localStorage and Vuex state like below.
token: localStorage.getItem('token') || '',
expiresAt: localStorage.getItem('expiresAt') || '',
I use the access token every time I send axios request to laravel api. Every thing works well. However, initially the token was set to 1 year expiration so when I develop I didn't care about token being expired and today suddenly I thought what is going to happen if token expired. So I set token expiry to 10 seconds in laravel AuthServiceProvier.php.
Passport::personalAccessTokensExpireIn(Carbon::now()->addSecond(10));
and then I logged in and after 10 seconds, every requests stopped working because the token was expired and got 401 unauthorised error.
In this case, how can I know if the token is expired? I would like to redirect the user to login page if token is expired when the user is using the website.
Be as user friendly as possible. Rather than waiting until the token expires, receiving a 401 error response, and then redirecting, set up a token verification check on the mounted hook of your main SPA instance and have it make a ajax call to e.g. /validatePersonalToken on the server, then do something like this in your routes or controller.
Route::get('/validatePersonalToken', function () {
return ['message' => 'is valid'];
})->middleware('auth:api');
This should return "error": "Unauthenticated" if the token is not valid. This way the user will be directed to authenticate before continuing to use the app and submitting data and then potentially losing work (like submitting a form) which is not very user friendly.
You could potentially do this on a component by component basis rather than the main instance by using a Vue Mixin. This would work better for very short lived tokens that might expire while the app is being used. Put the check in the mounted() hook of the mixin and then use that mixin in any component that makes api calls so that the check is run when that component is mounted. https://v2.vuejs.org/v2/guide/mixins.html
This is what I do. Axios will throw error if the response code is 4xx or 5xx, and then I add an if to check if response status is 401, then redirect to login page.
export default {
methods: {
loadData () {
axios
.request({
method: 'get',
url: 'https://mysite/api/route',
})
.then(response => {
// assign response.data to a variable
})
.catch(error => {
if (error.response.status === 401) {
this.$router.replace({name: 'login'})
}
})
}
}
}
But if you do it like this, you have to copy paste the catch on all axios call inside your programs.
The way I did it is to put the code above to a javascript files api.js, import the class to main.js, and assign it to Vue.prototype.$api
import api from './api'
Object.defineProperty(Vue.prototype, '$api', { value: api })
So that in my component, I just call the axios like this.
this.$api.GET(url, params)
.then(response => {
// do something
})
The error is handled on api.js.
This is my full api.js
import Vue from 'vue'
import axios from 'axios'
import router from '#/router'
let config = {
baseURL : process.env.VUE_APP_BASE_API,
timeout : 30000,
headers : {
Accept : 'application/json',
'Content-Type' : 'application/json',
},
}
const GET = (url, params) => REQUEST({ method: 'get', url, params })
const POST = (url, data) => REQUEST({ method: 'post', url, data })
const PUT = (url, data) => REQUEST({ method: 'put', url, data })
const PATCH = (url, data) => REQUEST({ method: 'patch', url, data })
const DELETE = url => REQUEST({ method: 'delete', url })
const REQUEST = conf => {
conf = { ...conf, ...config }
conf = setAccessTokenHeader(conf)
return new Promise((resolve, reject) => {
axios
.request(conf)
.then(response => {
resolve(response.data)
})
.catch(error => {
outputError(error)
reject(error)
})
})
}
function setAccessTokenHeader (config) {
const access_token = Vue.cookie.get('access_token')
if (access_token) {
config.headers.Authorization = 'Bearer ' + access_token
}
return config
}
/* https://github.com/axios/axios#handling-errors */
function outputError (error) {
if (error.response) {
/**
* The request was made and the server responded with a
* status code that falls out of the range of 2xx
*/
if (error.response.status === 401) {
router.replace({ name: 'login' })
return
}
else {
/* other response status such as 403, 404, 422, etc */
}
}
else if (error.request) {
/**
* The request was made but no response was received
* `error.request` is an instance of XMLHttpRequest in the browser
* and an instance of http.ClientRequest in node.js
*/
}
else {
/* Something happened in setting up the request that triggered an Error */
}
}
export default {
GET,
POST,
DELETE,
PUT,
PATCH,
REQUEST,
}
You could use an interceptor with axios. Catch the 401s and clear the local storage when you do then redirect user to appropriate page.
I have installed Laravel Passport and configured it according to the documentation. When calling axios.get from my VueJS file, the first call works as expected. the laravel_session Request Cookie is injected into the request, and the authentication passes, returning the resource.
My problem arises when I try to call the axios.get method again. My use case here is a search function. I'm making a call to /api/banking/accounts/search/{search-term} whenever the user types into a text field, using the code below:
remoteMethod(query) {
if (query !== '') {
this.loading = true;
axios.get(
`/api/banking/accounts/search/${escape(query)}`
).then(res => {
this.destinationAccountDirectory = res.data;
this.loading = false;
});
} else {
this.destinationAccountDirectory = [];
}
},
This code works fine without any auth:api middleware on the route, and for the first time with auth:api middleware. As can be seen from the screenshots below, the laravel_token value changes and is rejected on subsequent calls to the API.
**I've tried to removed the \Laravel\Passport\Http\Middleware\CreateFreshApiToken that was added to the web middleware group during passport installation, which seemed to have temporarily solved the issue, until I receive a 419 on a request shortly after. What could be causing the new laravel_tokens to be rejected? **
I solved this by removing the web middleware from my API route. Why it was there in the first place, I have no idea.
I changed my api.php from
Route::group([
'middleware' => [
'web',
'auth:api']], function() {
Route::post('/banking/transactions', 'TransactionController#store');
Route::get('/banking/accounts', 'BankAccountDirectoryController#index');
Route::get('/accounts/{account}', 'BankAccountDirectoryController#show');
Route::get('/banking/accounts/search/{term?}', 'BankAccountDirectoryController#search');
});
to
Route::group([
'middleware' => [
'auth:api']], function() {
Route::post('/banking/transactions', 'TransactionController#store');
Route::get('/banking/accounts', 'BankAccountDirectoryController#index');
Route::get('/accounts/{account}', 'BankAccountDirectoryController#show');
Route::get('/banking/accounts/search/{term?}', 'BankAccountDirectoryController#search');
});
Should the API routes be under the web group to benefit from the middleware, or is it purely for UI? Am I safe to do this?
I have created a custom module in Drupal 8 that allows a user to authenticate using facebook login. Their access token is checked against one stored in the database and if it matches authenticates the user and if it doesn't then redirects them to a page that allows them to link their Facebook account to a Drupal user.
The button for login is:
<button id="login_fb" onclick="logIt()">Log in with Facebook</button>
The "logit" function with the ajax request to the Drupal controller is:
function logIt()
{
FB.login(function(response) {
if (response.authResponse) {
if(response.authResponse.accessToken)
{
var request = $.ajax({
url: "/user/token",
method: "POST",
data: { access_token : response.authResponse.accessToken},
dataType: "json"
});
request.done(function( msg ) {
window.location.replace(msg['redirect_url']);
});
request.fail(function( jqXHR, textStatus ) {
alert( "Request failed: " + textStatus );
});
}
}
}
And the controller code that handles this ajax call is:
public function token() {
$fb_token = $_POST['access_token'];
$query = db_select('user__field_fb_token', 'u');
$query
->fields('u')
->condition('u.field_fb_token_value', $fb_token,'=');
$res = $query->execute();
$res->allowRowCount = TRUE;
$count = $res->rowCount();
//See if anybody has this access token
if($count > 0)
{
$user = $res->fetchAssoc();
//TODO: Refresh access token and update
$login_id = $user['entity_id'];
//Redirect the user to topics
user_login_finalize(user_load($login_id));
$response_arr = array("status" => "authorised","redirect_url" => "/topics");
}
else
{
$_SESSION['access_token'] = $fb_token;
$response_arr = array("status" => "unauthorised","redirect_url" => "/user/auth","token" => $fb_token);
}
$response = new Response();
$response->setContent(json_encode($response_arr));
$response->headers->set('Content-Type', 'application/json');
return $response;
}
The weird thing is that the db_select query always returns 0 and therefore does not authenticate the user account that has this token. However replacing
$fb_token = $_POST['access_token'];
with
$fb_token = '** hard coded access token **';
yields the correct result. I have checked that the post variable being passed in and it is present (that's why I pass it back with the unauthorised response to check that it's not blank).
I think it may have something to do with the async nature of FB.Login method but not sure.
Any help on this matter would be greatly appreciated!