Can't access api post method, request always OPTIONS - php

i'm trying to create login form with nuxtjs as front end and slim php as backend api, when i tried to access the API, i see the request method that i send is OPTIONS not POST, and in chrome dev console error shown that the request is blocked (CORS).
I know i have to set Access-Control-Allow-Methods add OPTIONS into it, but still failed, where to set that in nuxt js(nuxt.config) or slim php?
I have tried to access the api from postman and it work's just fine, i can see Access-Control-Allow-Methods headers has OPTIONS in it, but still it failed when i tried in vue apps
commons.app.js:434 OPTIONS http://localhost:8080/login 401 (Unauthorized)
Access to XMLHttpRequest at 'http://localhost:8080/login' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Code from slim php (API)
routes.php
// login routes
$app->post('/login', function (Request $request, Response $response, array $args) {
$input = $request->getParsedBody();
$sql = "SELECT * FROM groups
LEFT JOIN users_groups ON groups.groups_id = users_groups.users_groups_id
LEFT JOIN users ON users_groups.users_groups_id = users.id
WHERE users.email = :email";
$sth = $this->db->prepare($sql);
$sth->bindParam("email", $input['email']);
$sth->execute();
$user = $sth->fetchObject();
// verify email address.
if (!$user) {
$container->get('logger')->info("Error trying to login with email : ".$input['email']);
return $this->response->withJson(['error' => true, 'message' => 'These credentials do not match our records.']);
}
// verify password.
if (!password_verify($input['password'], $user->password)) {
$container->get('logger')->info("Error trying to login with password : ".$input['password']);
return $this->response->withJson(['error' => true, 'message' => 'These credentials do not match our records.']);
}
// cek apakah sudah login sebelumnya hari ini untuk user ini
$absensiSql = "SELECT * FROM users_presensi WHERE user_id = :userID";
$sth = $this->db->prepare($absensiSql);
$sth->bindParam("userID", $user->id);
$sth->execute();
$absensi = $sth->fetchObject();
// jika belum absen, masukan user ke absensi
if(!$absensi){
$status = 1;
$absensiSql = "INSERT INTO users_presensi(user_id, statuss) VALUES (:userID, :statuss)";
$sth = $this->db->prepare($absensiSql);
$sth->bindParam("userID", $user->id);
$sth->bindParam("statuss", $status);
$sth->execute();
// $absensi = $sth->fetchObject();s
}
$settings = $this->get('settings'); // get settings array.
$token = JWT::encode(['id' => $user->id, 'email' => $user->email, 'level' => $user->groups_level], $settings['jwt']['secret'], "HS256");
// i already set the header here
return $this->response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
->withHeader('Set-Cookie', "token=$token; httpOnly")
->withJson(['token' => $token]);
})->setName('login');
my nuxtjs auth config
nuxt.config.js
/*
** Nuxt.js modules
*/
modules: [
'#nuxtjs/vuetify',
// Doc: https://axios.nuxtjs.org/usage
'#nuxtjs/axios',
'#nuxtjs/pwa',
'#nuxtjs/eslint-module',
'#nuxtjs/auth'
],
/*
** Axios module configuration
** See https://axios.nuxtjs.org/options
*/
axios: {
},
/**
* set auth middleware
*/
router: {
middleware: ['auth']
},
auth: {
strategies: {
local: {
endpoints: {
login: { url: 'http://localhost:8080/login', method: 'post', propertyName: 'token' }
}
// tokenRequired: true,
// tokenType: 'bearer'
}
}
}
method login from login.vue
loginPost: function () {
this.$auth.loginWith('local', {
data: {
username: this.loginData.username,
password: this.loginData.password
}
})
}
in postman, the result is token itself, and i think there shouldn't be cors error happen, but who's know.

I don't know about other browsers, but I know that Chrome does not support using localhost in your Access-Control-Allow-Origin header. What you should do is in your dev environment only tell it to accept all origins ->withHeader('Access-Control-Allow-Origin', '*')
The OPTIONS request is what is sent out in preparation for the browser sending out the real request to determine what the CORS rules are.

Related

Angular9 http post CORS issue

I am new to Angular9, I have tried to save data by calling a php API from my angular9 application, but geting the following error.
I have test this service from postman, which is working fine and save data successfully
Access to XMLHttpRequest at 'http://127.0.0.1/angularCRUDservices/user/saveEmployee' from origin 'http://localhost:4200' has been blocked by CORS policy: Request header field access-control-allow-origin is not allowed by Access-Control-Allow-Headers in preflight response.
**Please Note:** get method is working fine to fetch data,
my Angular service:
reqHeader = new HttpHeaders(
{
'Content-Type': 'application/json',
'No-Auth':'True',
'Access-Control-Allow-Origin' : '*',
'Access-Control-Allow-Header':'Access-Control-Allow-Origin,X-Requested-With,Content-Type,Access',
'Access-Control-Allow-Methods':'GET,POST,DELETE,PATCH,PUT,OPTIONS'
}
);
constructor(private httpClient:HttpClient){}
saveEmployee(formData:Employee){
console.log(formData);
this.httpClient.post<{response:any,success:boolean}>("http://127.0.0.1/angularCRUDservices/user/saveEmployee",formData,{
headers : this.reqHeader
})
.subscribe((response)=>{
console.log(response);
})
}
php service code:
setting headers in constructor:
public function __construct()
{
parent::__construct();
$this->load->helper('url');
$this->load->model('user_model','usr');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Header: Access-Control-Allow-Origin,X-Requested-With,Content-Type,Access');
header("Access-Control-Allow-Methods: GET,POST,DELETE,PATCH,PUT,OPTIONS");
header('Content-Type: application/json, charset=utf-8');
}
service:
public function saveEmployee()
{
$input = array();
try
{
$UserID = $this->input->post('id');
$input['name'] = $this->input->post('name');
$input['gender'] = $this->input->post('gender');
$input['email'] = $this->input->post('email');
$input['mobile'] = $this->input->post('mobile');
$input['department'] = 'test';//$this->input->post('department');
$input['isActive'] = 1;//$this->input->post('isActive');
$input['photo'] = 'testphoto';//$this->input->post('photo');
$output["response"] = $this->usr->saveUser($input,$UserID); // call model query to save data into DB
$output["success"] = true;
}
catch (Exception $ex)
{
$output["success"] = false;
$output["error"] = $ex->getMessage();
}
echo json_encode((object)$output);
exit;
}
if this is for development and not production,you can use a proxy to get arround the issue. add a json file to your root project named 'proxy.configuration.json'
remember to change the target to your api in the example below
{
"/api/*": {
"target": "https://localhost:44362",
"secure": false,
"logLevel": "debug",
"changeOrigin": true
}
}
then run ng serve --proxy-config proxy.config.json
you have to add Authorization header key in Access-Control-Allow-Headers(in the backend)
Access-Control-Allow-Headers: Authorization (when you give like this it accepts specified headers only.means if you want to accept another key like authorization you have to mention that key name)
or
you can give like this also
Access-Control-Allow-Headers: *

Passing response from social provider back to API endpoint

I am trying to add social authentication to a laravel 5.8 API application using socialite. Following the documentation here https://laravel.com/docs/5.8/socialite#routing I created a SocialAuthController that wiill redirect the user to the provider auth page and handle the callback like this
...
use Socialite;
...
public function redirectToProvider($provider)
{
return Socialite::driver($provider)->redirect();
}
public function handleProviderCallback($provider)
{
// retrieve social user info
$socialUser = Socialite::driver($provider)->stateless()->user();
// check if social user provider record is stored
$userSocialAccount = SocialAccount::where('provider_id', $socialUser->id)->where('provider_name', $provider)->first();
if ($userSocialAccount) {
// retrieve the user from users store
$user = User::find($userSocialAccount->user_id);
// assign access token to user
$token = $user->createToken('string')->accessToken;
// return access token & user data
return response()->json([
'token' => $token,
'user' => (new UserResource($user))
]);
} else {
// store the new user record
$user = User::create([...]);
// store user social provider info
if ($user) {
SocialAccount::create([...]);
}
// assign passport token to user
$token = $user->createToken('string')->accessToken;
$newUser = new UserResource($user);
$responseMessage = 'Successfully Registered.';
$responseStatus = 201;
// return response
return response()->json([
'responseMessage' => $responseMessage,
'responseStatus' => $responseStatus,
'token' => $token,
'user' => $newUser
]);
}
}
Added the routes to web.php
Route::get('/auth/{provider}', 'SocialAuthController#redirectToProvider');
Route::get('/auth/{provider}/callback', 'SocialAuthController#handleProviderCallback');
Then I set the GOOGLE_CALLBACK_URL=http://localhost:8000/api/v1/user in my env file.
When a user is successfully authenticated using email/password, they will be redirected to a dashboard that will consume the endpoint http://localhost:8000/api/v1/user. So in the google app, I set the URI that users will be redirected to after they are successfully authenticated to the same endpoint http://localhost:8000/api/v1/user
Now when a user tries to login with google, the app throws a 401 unauthenticated error.
// 20190803205528
// http://localhost:8000/api/v1/user?state=lCZ52RKuBQJX8EGhz1kiMWTUzB5yx4IZY2dYmHyJ&code=4/lgFLWpfJsUC51a9yQRh6mKjQhcM7eMoYbINluA58mYjs5NUm-yLLQARTDtfBn4fXgQx9MvOIlclrCeARG0NC7L8&scope=email+profile+openid+https://www.googleapis.com/auth/userinfo.profile+https://www.googleapis.com/auth/userinfo.email&authuser=0&session_state=359516252b9d6dadaae740d0d704580aa1940f1d..10ea&prompt=none
{
"responseMessage": "Unauthenticated",
"responseStatus": 401
}
If I change the URI where google authenticated users should be redirect to like this GOOGLE_CALLBACK_URL=http://localhost:8000/auth/google/callback the social user information is returned.
So how should I be doing it. I have been on this for a couple of days now.
That is because you haven't put authorization in your header with your request.
you don't need to redirect user if you are working with token, your app should be a spa project, so you will redirect him from your side using js frameworks.
You need to send Authorization in your headers plus you need to specify it with your token which you returned it in your response like this:
jQuery.ajaxSetup({
headers: {
'Authorization': 'Bearer '+token
}
});
or if you are using axios
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;

Session not being initiated from Guzzle Post

I am integrating Laravel into a legacy php app. The login page used to directly post to verifyUser.php which also started a Symfony Session.
The new architecture now posts to a laravel api which makes a Guzzle post to verifyUser.php.
javascript:
$(document).ready(function(){
$('#signIn').submit(function(){
var a = $('#email').val();
$.post('/api/login', { //this used to post to verifyUser.php
Username: $('#email').val(),
Password: $('#password').val()
}, function(data){
if(data['credentials'] == true){
console.log('credentials true');
console.log(data['uri']);
window.location.href=data['uri'];
} else {
$('#errMsg').html(data['errMsg']);
$('.alert').show();
}
}, 'json');
return false;
});
controller functions:
public function authenticate(Request $request) //aka api/login endpoint
{
//...
$legacyRes = $this->authenticateLegacy($request);
//...
}
private function authenticateLegacy(Request $request)
{
$response = null;
try {
$response = $this->client->post('/admin/verifyUser.php', [
'form_params' => ['Username' => $request->get('Username'),
'Password' => $request->get('Password')]
]);
}
catch(Exception $exception){
Log::error('Errrererererer', [$exception->getMessage()]);
}
$body = (string)$response->getBody();
Log::info('BODY:', [$body]);
return $body;
}
I have left out verifyUser.php because I have tested it and it returns the expected results.
When using the browser, the session information doesn't seem to get set. But according to my post responses, everything should be working.
Is this because I am routing the request through guzzle?
Posting under my answer to show updated code:
private function authenticateLegacy(Request $request)
{
//...
//parse cookie id from guzzle response
$body = (string)$response->getBody();
$cookie = $response->getHeader('Set-Cookie'); //PHPSESSID=SOMEID; path=/
$cookieBite = explode(';', $cookie)[0]; ////PHPSESSID=SOMEID
$cookieId = explode('=', $cookieBite)[1];
$data = json_decode($body, true);
$data['session'] = $cookieId;
return $data;
}
In the action:
public function authenticate(Request $request)
{
//...
$legacyRes = $this->authenticateLegacy($request);
//...
// this will have the session id in the body but will also
// set the cookie for the client so I don't have
// to set document.cookie w/ js
return response($legacyRes, 200)
->withCookie('PHPSESSID', $legacyRes['session']);
}
I assume your legacy endpoint uses cookies to identify a user's session.
A successfull request to the legacy endpoint returns a Set-Cookie header.
Guzzle doesn't forward this Set-Cookie header from the API response to the browser - you'll have to program this behaviour into the "wrapping" application.
You will need to tell guzzle to explicitly pass the corresponding Cookie header to the legacy api (to maintain the user's login state) when sending any further requests.
In order to achieve this you'll need to save this cookie within your new application (i.e. in the user's session or in database) and then pass it within a Cookie header along with all further requests you make to the legacy API.

POST request using Angular HttpClient not working

I am trying to post user data to a PHP RESTful API from my ionic app. I tried searching for a solution but was of no help. I have created a provider containing a function namely "onSignup(signupForm)" which is being called on button click.
The code is as follows:
signup(username: string,email: string,password: string): void {
let headers = new HttpHeaders();
headers = headers.set("Content-Type","application/json; charset=UTF-8");
let body= {
name:username, email:email, password:password
};
this.http.post('http://www.something.com/register', JSON.stringify(body),
{headers: headers})
.subscribe(data => {
console.log(data);
});
this.storage.set(this.HAS_LOGGED_IN, true);
this.setUsername(username);
this.events.publish('user:signup');
};
The code for the api is as:
<?php
header("Access-Control-Allow-Origin:*");
header("Content-Type: application/json; charset=UTF-8");
require_once '../include/DbHandler.php';
require_once '../include/PassHash.php';
require '.././libs/Slim/Slim.php';
\Slim\Slim::registerAutoloader();
$app = new \Slim\Slim();
/**
* ----------- METHODS WITHOUT AUTHENTICATION -------------------------------
--
*/
/**
* User Registration
* url - /register
* method - POST
* params - name, email, password
*/
$app->post('/register', function() use ($app) {
file_put_contents("logs.txt","/register Route has been visited");
// check for required params
verifyRequiredParams(array('name', 'email', 'password'));
$response = array();
// reading post params
$name = $app->request->post('name');
$email = $app->request->post('email');
$password = $app->request->post('password');
// validating email address
validateEmail($email);
$db = new DbHandler();
$res = $db->createUser($name, $email, $password);
if ($res == USER_CREATED_SUCCESSFULLY) {
$response["error"] = false;
$response["message"] = "You are successfully registered";
} else if ($res == USER_CREATE_FAILED) {
$response["error"] = true;
$response["message"] = "Oops! An error occurred while registereing";
} else if ($res == USER_ALREADY_EXISTED) {
$response["error"] = true;
$response["message"] = "Sorry, this email already existed";
}
// echo json response
echoRespnse(201, $response);
});
The error I receive is
Failed to load resource: the server responded with a status of 404 (Not Found).
Failed to load http://www.something.com/register: Response for preflight has invalid HTTP status code 404
This API is working perfectly in Postman, but is facing the issue when I am running the app in Chrome.
Is there something I am missing in the API or during the POST call?
Please help. Thanks in Advance.
Edit:
I have added the Network Tab screenshot. This is what I am getting in my Request and Response Headers. I guess there might be a mismatch in the two headers and definitely it can't be a CORS issue because I can make GET calls without any CORS issue.
Added the console tab screenshot with the errors:
Since it is working from Postman but not your application I would take a good look into CORS. You need to set your headers when a request comes in, for post requests the Angular HttpClient will send an OPTIONS request.
I don't use PHP much, but maybe something like this would work
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Content-Length: 0');
header('Content-Type: application/json');
die();
}
Edit: Since you are using the Slim Framework I assume by your provided code. You can address OPTIONS requests like so as described per the Slim Framework v2 Docs (Not sure what version you are using).
$app->options('/register', function ($app) {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Content-Length: 0');
header('Content-Type: application/json');
die();
});
Or you could maybe set the headers like so, as described by
https://www.slimframework.com/docs/v2/response/headers.html
$app->response->headers->set('Access-Control-Allow-Origin', '*');
$app->response->headers->set('Access-Control-Allow-Methods', 'POST, OPTIONS');
$app->response->headers->set('Access-Control-Allow-Headers', 'Content-Type');
$app->response->headers->set('Content-Type', 'application/json');
try this
header('Access-Control-Allow-Origin: *');
$dados = file_get_contents('php://input');
you will get a JSON from your angular request...

Laravel JWT tokens are Invalid after refresh them in a authentication JWT approach

EDIT:
Read the discussion about the bug at: https://github.com/tymondesigns/jwt-auth/issues/83
MY ORIGINAL QUESTION:
I'm implement with jwt-auth my protected resources that require an authenticated user with bellow code:
Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function() {
// Protected routes
});
When user 'sign in' on API an Authorization token is created, and sent on response Authorization header to client application that call the resource. So, client applications when intercept a Authorization token on header of any response, set a variable/session/whatever with this token value, to send again to API on next request.
The first request for a protected resource after 'login' works fine, but the next client application request to API with a refreshed token, gives the following error (API mount all responses in json format):
{
"error": "token_invalid"
}
What can be happen with refreshed tokens? My refresh token implementation (set as a after middleware) is wrong? Or isn't necessary to manually refresh all Authorization token that come with client apps requests?
UPDATE:
I update the jwt-auth RefreshToken middleware as propose here, but the token_invalid persist.
BUG:
I guess that I found what happens. Note that in the refresh method, old token is added to blacklist cache case enabled:
// Tymon\JWTAuth\JWTManager
public function refresh(Token $token)
{
$payload = $this->decode($token);
if ($this->blacklistEnabled) {
// invalidate old token
$this->blacklist->add($payload);
}
// return the new token
return $this->encode(
$this->payloadFactory->setRefreshFlow()->make([
'sub' => $payload['sub'],
'iat' => $payload['iat']
])
);
}
And note that in add to blacklist method the key is the jti param from old token payload:
// Tymon\JWTAuth\Blacklist
public function add(Payload $payload)
{
$exp = Utils::timestamp($payload['exp']);
// there is no need to add the token to the blacklist
// if the token has already expired
if ($exp->isPast()) {
return false;
}
// add a minute to abate potential overlap
$minutes = $exp->diffInMinutes(Utils::now()->subMinute());
$this->storage->add($payload['jti'], [], $minutes);
return true;
}
Thus, when has on blacklist method is called, the old token jti param is the same that the new, so the new token is in blacklist:
// Tymon\JWTAuth\Blacklist
public function has(Payload $payload)
{
return $this->storage->has($payload['jti']);
}
If you don't need the blacklist functionality just set to false on jwt.php configuration file. But I can't say if it expose to some security vulnerability.
Read the discussion about the bug at: https://github.com/tymondesigns/jwt-auth/issues/83
When I get this issue, the solution that I found to get my project working was to generate a new token with data from older token on each new request.
My solution, that works for me, is bad, ugly, and can generate more issues if you have many async requests and your API(or business core) server is slow.
For now is working, but I will investigate more this issue, cause after 0.5.3 version the issue continues.
E.g:
Request 1 (GET /login):
Some guest data on token
Request 2 (POST /login response):
User data merged with guest data on old token generating a new token
Procedural code example(you can do better =) ), you can run this on routes.php out of routes, I say that is ugly haha:
// ----------------------------------------------------------------
// AUTH TOKEN WORK
// ----------------------------------------------------------------
$authToken = null;
$getAuthToken = function() use ($authToken, $Response) {
if($authToken === null) {
$authToken = JWTAuth::parseToken();
}
return $authToken;
};
$getLoggedUser = function() use ($getAuthToken) {
return $getAuthToken()->authenticate();
};
$getAuthPayload = function() use ($getAuthToken) {
try {
return $getAuthToken()->getPayload();
} catch (Exception $e) {
return [];
}
};
$mountAuthPayload = function($customPayload) use ($getLoggedUser, $getAuthPayload) {
$currentPayload = [];
try {
$currentAuthPayload = $getAuthPayload();
if(count($currentAuthPayload)) {
$currentPayload = $currentAuthPayload->toArray();
}
try {
if($user = $getLoggedUser()) {
$currentPayload['user'] = $user;
}
$currentPayload['isGuest'] = false;
} catch (Exception $e) {
// is guest
}
} catch(Exception $e) {
// Impossible to parse token
}
foreach ($customPayload as $key => $value) {
$currentPayload[$key] = $value;
}
return $currentPayload;
};
// ----------------------------------------------------------------
// AUTH TOKEN PAYLOAD
// ----------------------------------------------------------------
try {
$getLoggedUser();
$payload = ['isGuest' => false];
} catch (Exception $e) {
$payload = ['isGuest' => true];
}
try {
$payload = $mountAuthPayload($payload);
} catch (Exception $e) {
// Make nothing cause token is invalid, expired, etc., or not exists.
// Like a guest session. Create a token without user data.
}
Some route(simple example to save user mobile device):
Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function () use ($getLoggedUser, $mountAuthPayload) {
Route::post('/session/device', function () use ($Response, $getLoggedUser, $mountAuthPayload) {
$Response = new \Illuminate\Http\Response();
$user = $getLoggedUser();
// code to save on database the user device from current "session"...
$payload = app('tymon.jwt.payload.factory')->make($mountAuthPayload(['device' => $user->device->last()->toArray()]));
$token = JWTAuth::encode($payload);
$Response->header('Authorization', 'Bearer ' . $token);
$responseContent = ['setted' => 'true'];
$Response->setContent($responseContent);
return $Response;
});
});

Categories