Why I have issuse with redirect() after I call app()->handle($request) - laravel-8

I try to call internal JWT API for check token(expired or not..) and I stored token in browser session
public function index(){
$check = $this->check_login();
if($check != 1){
return Redirect::to('login');
}
return view('main_pages.home');
}
function check_login(){
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$token = $_SESSION['token'];
$request = Request::create('/api/user', 'POST');
$request->headers->set('Accept', 'application/json');
$request->headers->set('Authorization', 'Bearer '.$token);
$response = app()->handle($request);
$res = json_decode($response->getContent()); // convert to json object
return $res->status;
}
when I got $res->status = 0; it's redirect to localhost/login (without port) this issuse come from when I called app()->handle($resquest)
So how can I redirect('login') after I accessed to internal JWT API by app()->handle($resquest)
or have another way to access to internal JWT API and then redirect some route

Related

Varnish error "Uncached req.body can only be consumed once" by extracting response header from subrequest and add it to the original request

I have an api with jwt authentication (bearer token). The jwt is sent on every api request. For validating the jwt I have a specific route in my backend (GET /_jwt_user_sub). A request to this route with a valid jwt returns a X-User response header with code 200 and Content-Type: application/vnd.user-sub. I configured Varnish to make on every api call (GET and POST) a sub request to this route, extract the X-User header from the response and add it to the originally api request. This jwt user id response should be cached by varnish. For api requests with GET method this works fine but not for api calls with POST method because Varnish by default do all backend requests with GET. So I override this behaviour in the vcl_backend_fetch sub routine (see following vcl configuration).
With my vcl configuration I get a Error 503 Backend fetch failed. By debugging with varnishlog I see that a vcl error is thrown: VCL_Error Uncached req.body can only be consumed once.. I am not sure how to rewrite the configuration correctly, as the error says at one point the configuration tries to consume a request body twice without caching it. Varnish should only cache GET api calls, no POST calls.
This is my vcl configuration:
vcl 4.1;
import std;
import xkey;
backend app {
.host = "nginx";
.port = "8080";
}
sub vcl_recv {
set req.backend_hint = app;
// Retrieve user id and add it to the forwarded request.
call jwt_user_sub;
// Don't cache requests other than GET and HEAD.
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
return (hash);
}
sub vcl_hit {
if (obj.ttl >= 0s) {
return (deliver);
}
if (obj.ttl + obj.grace > 0s) {
if (!std.healthy(req.backend_hint)) {
return (deliver);
} else if (req.http.cookie) {
return (miss);
}
return (deliver);
}
return (miss);
}
// Called when the requested object has been retrieved from the backend
sub vcl_backend_response {
if (bereq.http.accept ~ "application/vnd.user-sub"
&& beresp.status >= 500
) {
return (abandon);
}
}
// Sub-routine to get jwt user id
sub jwt_user_sub {
// Prevent tampering attacks on the mechanism
if (req.restarts == 0
&& (req.http.accept ~ "application/vnd.user-sub"
|| req.http.x-user
)
) {
return (synth(400));
}
if (req.restarts == 0) {
// Backup accept header, if set
if (req.http.accept) {
set req.http.x-fos-original-accept = req.http.accept;
}
set req.http.accept = "application/vnd.user-sub";
// Backup original URL
set req.http.x-fos-original-url = req.url;
set req.http.x-fos-original-method = req.method;
set req.url = "/_jwt_user_sub";
set req.method = "GET";
return (hash);
}
// Rebuild the original request which now has the hash.
if (req.restarts > 0
&& req.http.accept == "application/vnd.user-sub"
) {
set req.url = req.http.x-fos-original-url;
set req.method = req.http.x-fos-original-method;
unset req.http.x-fos-original-url;
if (req.http.x-fos-original-accept) {
set req.http.accept = req.http.x-fos-original-accept;
unset req.http.x-fos-original-accept;
} else {
// If accept header was not set in original request, remove the header here.
unset req.http.accept;
}
if (req.http.x-fos-original-method == "POST") {
return (pass);
}
return (hash);
}
}
sub vcl_backend_fetch {
if (bereq.http.x-fos-original-method == "POST") {
set bereq.method = "POST";
}
}
sub vcl_deliver {
// On receiving the hash response, copy the hash header to the original
// request and restart.
if (req.restarts == 0
&& resp.http.content-type ~ "application/vnd.user-sub"
) {
set req.http.x-user = resp.http.x-user;
return (restart);
}
}
The built-in VCL says that only GET and HEAD requests are cacheable by default. You can override this and cache POST calls, but unless you perform explicit caching of the request body (which is stored in req.body), the body itself will only be processed once.
Because you perform restart calls in your VCL, the request body is processed multiple times. Unfortunately, at the 2nd attempt it is no longer available.
To tackle this issue, you can use the std.std.cache_req_body(BYTES size) function to explicitly cache the request body. Most of the times this will be for POST calls but the HTTP spec also allows you to have request bodies for GET calls.
See https://varnish-cache.org/docs/6.0/reference/vmod_generated.html#func-cache-req-body for more information.
Here's how to implement this function in your vcl_recv subroutine:
sub vcl_recv {
set req.backend_hint = app;
// Cache the request body for POST calls
if(req.method == "POST") {
std.cache_req_body(10K);
}
// Retrieve user id and add it to the forwarded request.
call jwt_user_sub;
// Don't cache requests other than GET and HEAD.
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
return (hash);
}

Storing claims on cookie while redirecting to other URL and also without identity authentication

I just need advise if this is feasible. I am developing an authorization for my Shopify app and I need to somewhat store the access token from shopify auth for future verification of my front-end app.
So the first end-point the shopify is calling is this one:
[HttpGet("install")]
public async Task<IActionResult> Install()
{
try
{
if (ModelState.IsValid)
{
var queryString = Request.QueryString.Value;
var isValid = _shopifyService.VerifyRequest(queryString);
if (isValid)
{
var shopifyUrl = Request.Query["shop"];
var authUrl = _shopifyService.BuildAuthUrl(shopifyUrl,
$"{Request.Scheme}://{Request.Host.Value}/api/shopify/authorize",
Program.Settings.Shopify.AuthorizationScope);
return Redirect(authUrl);
}
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
This works fine and the result of this api call will actually redirect to same link to my api, but this one will authorize the app:
[HttpGet("authorize")]
public async Task<IActionResult> AuthorizeStore()
{
try
{
if (ModelState.IsValid)
{
var code = Request.Query["code"];
var shopifyUrl = Request.Query["shop"];
var accessToken = await _shopifyService.AuthorizeStore(code, shopifyUrl).ConfigureAwait(false);
var identity = User.Identity as ClaimsIdentity;
identity.AddClaim(new Claim(Constants.Claims.AccessToken, accessToken));
// genereate the new ClaimsPrincipal
var claimsPrincipal = new ClaimsPrincipal(identity);
// store the original tokens in the AuthenticationProperties
var props = new AuthenticationProperties {
AllowRefresh = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(1),
IsPersistent = false,
IssuedUtc = DateTimeOffset.UtcNow,
};
// sign in using the built-in Authentication Manager and ClaimsPrincipal
// this will create a cookie as defined in CookieAuthentication middleware
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, props).ConfigureAwait(false);
Uri uri = new Uri($"{Program.Settings.Shopify.RedirectUrl}?token={accessToken}");
return Redirect(uri.ToString());
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
So the above api will authorize my app in shopify and will return an access token. The accessToken is the one I want to save in the claims identity with Cookie authentication type(this is without authorizing user credentials). Still no errors at that point and after calling the HttpContext.SignInAsync function, I can still view using debugger the newly added claims.
As, you can see in the code, after assigning claims, I call to redirect the app to front-end link(Note: front-end and back-end has different url)
In my front-end app, I have a Nuxt middleware that I put a logic to check the token received from back-end since I only pass the token to the front-end app using query params. Here's my middleware code:
export default function ({ app, route, next, store, error, req }) {
if (process.browser) {
const shopifyAccessToken = store.get('cache/shopifyAccessToken', null)
if (!shopifyAccessToken && route.query.token) {
// if has token on query params but not yet in cache, store token and redirect
store.set('cache/shopifyAccessToken', route.query.token)
app.router.push({
path: '/',
query: {}
})
// verify access token on the route
app.$axios
.get(`/shopify/verifyaccess/${route.query.token}`)
.catch((err) => {
error(err)
})
} else if (!shopifyAccessToken && !route.query.token) {
// if does not have both, throw error
error({
statusCode: 401,
message: 'Unauthorized access to this app'
})
}
} else {
next()
}
}
In my middleware, when the route has query params equal to token= It calls another api to verify the accessToken saved in my claims identity:
[HttpGet("verifyaccess/{accessToken}")]
public async Task<IActionResult> VerifyAccess(string accessToken)
{
try
{
if (ModelState.IsValid)
{
var principal = HttpContext.User;
if (principal?.Claims == null)
return Unauthorized(_localizer["NotAuthenticated"]);
var accessTokenClaim = principal.FindFirstValue(Constants.Claims.AccessToken);
if (accessToken == accessTokenClaim)
{
return Ok();
}
else
{
return Unauthorized(_localizer["NotAuthenticated"]);
}
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
Looking at the code above, it always fails me because the claims identity that I saved on the authorize endpoint was not there or in short the ClaimsIdentity is always empty.
Here's how I register the Cookie config:
private void ConfigureAuthCookie(IServiceCollection services)
{
services.AddAuthentication(option =>
{
option.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
option.RequireAuthenticatedSignIn = false;
})
.AddCookie(options => {
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.SlidingExpiration = true;
options.Cookie.Name = "shopifytoken";
});
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
});
}
and I also put a app.UseAuthentication() and app.UseAuthorization() on my Startup.Configure
Please let me know if this seems confusing so I can revised it. My main goal here is to be able to access that accessToken that I saved in the ClaimsIdentity so that I can verify the token. The reason why I did this because currently the shopify does not have an API for verifying access token. So when a user access my app link like this one http://example.com/?token=<any incorrect token> then they can already access my app.

PHP Fatal error: Uncaught InvalidArgumentException: Invalid token format in vendor/google/apiclient/src/Google/Client.php:423

Below code used for the edit google spreadsheet, but we are getting issue related to the access token, let me know if anyone has a solution for this issue. For the Outh client id we have flow these instructions:
https://developers.google.com/analytics/devguides/reporting/core/v3/quickstart/web-php
[29-Sep-2019 04:10:36 UTC] PHP Fatal error: Uncaught InvalidArgumentException: Invalid token format in /home/help4dl9/public_html/doamin.com/wp-content/themes/twentynineteen/google-sheet/vendor/google/apiclient/src/Google/Client.php:423
Here getClient function:
function getClient()
{
$redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];
$client = new Google_Client();
$client->setApplicationName('APP NAME');
$client->setScopes( 'https://www.googleapis.com/auth/spreadsheets' );
$client->setAuthConfig( __DIR__ . '/credentials.json' );
$client->setAccessType('offline');
$client->setPrompt('select_account consent');
// Load previously authorized token from a file, if it exists.
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
$tokenPath = __DIR__ . '/token.json';
if (file_exists($tokenPath)) {
$accessToken = json_decode(file_get_contents($tokenPath), true);
$client->setAccessToken($accessToken);
}
// If there is no previous token or it's expired.
if ($client->isAccessTokenExpired()) {
// Refresh the token if possible, else fetch a new one.
if ($client->getRefreshToken()) {
$client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
$authObj = $client->getAccessToken();
if(array_key_exists("access_token",$authObj)) {
update_field( 'authentication_code', $authObj['access_token'],'option' ); // Save Token in Theme Option
}
} else {
// Request authorization from the user.
$authCode = get_field('authentication_code','option');
// Exchange authorization code for an access token.
$accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
$client->setAccessToken($accessToken);
// Check to see if there was an error.
if (array_key_exists('error', $accessToken)) {
throw new Exception(join(', ', $accessToken));
}
}
// Save the token to a file.
if (!file_exists(dirname($tokenPath))) {
mkdir(dirname($tokenPath), 0700, true);
}
file_put_contents($tokenPath, json_encode($client->getAccessToken()));
}
return $client;
}```
Follow instead the instructions of PHP Quickstarts for Sheets API without $redirect_uri and get_field:
<?php
require __DIR__ . '/vendor/autoload.php';
if (php_sapi_name() != 'cli') {
throw new Exception('This application must be run on the command line.');
}
/**
* Returns an authorized API client.
* #return Google_Client the authorized client object
*/
function getClient()
{
$client = new Google_Client();
$client->setApplicationName('Google Sheets API PHP Quickstart');
$client->setScopes(Google_Service_Sheets::SPREADSHEETS_READONLY);
$client->setAuthConfig('credentials.json');
$client->setAccessType('offline');
$client->setPrompt('select_account consent');
// Load previously authorized token from a file, if it exists.
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
$tokenPath = 'token.json';
if (file_exists($tokenPath)) {
$accessToken = json_decode(file_get_contents($tokenPath), true);
$client->setAccessToken($accessToken);
}
// If there is no previous token or it's expired.
if ($client->isAccessTokenExpired()) {
// Refresh the token if possible, else fetch a new one.
if ($client->getRefreshToken()) {
$client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
} else {
// Request authorization from the user.
$authUrl = $client->createAuthUrl();
printf("Open the following link in your browser:\n%s\n", $authUrl);
print 'Enter verification code: ';
$authCode = trim(fgets(STDIN));
// Exchange authorization code for an access token.
$accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
$client->setAccessToken($accessToken);
// Check to see if there was an error.
if (array_key_exists('error', $accessToken)) {
throw new Exception(join(', ', $accessToken));
}
}
// Save the token to a file.
if (!file_exists(dirname($tokenPath))) {
mkdir(dirname($tokenPath), 0700, true);
}
file_put_contents($tokenPath, json_encode($client->getAccessToken()));
}
return $client;
}
Make sure sure to enable the sheets API and paste the credentials.json file into the right folder, as explained in the quickstart. Also, whenever you change the required scopes, you need to manually delete the token.json file from your drive.

adal.js inifnite loop when refreshing token

I am using the latest adal.js to query data from MicroSoft Dynamics CRM. The code gets into an infinite loop when renewing the token.
Additionally after loging into microsoft and being redirected back to my page the adaljs tries to refresh the token.
Note - this is javascript in an ASP.NET MVC web app. It is not using angular js.
This is also similar to the SO question Adal & Adal-Angular - refresh token infinite loop
var endpoints = {
orgUri: "https://<tenant>.crm6.dynamics.com/"
};
var config = {
clientId: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
tenant: '<tenant>.onmicrosoft.com',
redirectUri: 'http://localhost:53290/home/AuthenticatedByCrm/',
endpoints: endpoints,
cacheLocation: 'localStorage'
};
var x = new AuthenticationContext(config);
var isCallback = x.isCallback(window.location.hash);
if (isCallback) {
x.handleWindowCallback();
x.acquireToken(endpoints.orgUri, retrieveAccounts);
} else {
x.login();
}
function retrieveAccounts(error, token) {
// Handle ADAL Errors.
if (error || !token) {
alert('ADAL error occurred: ' + error);
return;
}
var req = new XMLHttpRequest();
req.open("GET", encodeURI(organizationURI + "/api/data/v8.0/accounts?$select=name,address1_city&$top=10"), true);
//Set Bearer token
req.setRequestHeader("Authorization", "Bearer " + token);
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.onreadystatechange = function () {
if (this.readyState == 4 /* complete */) {
req.onreadystatechange = null;
if (this.status == 200) {
var accounts = JSON.parse(this.response).value;
//renderAccounts(accounts);
}
else {
var error = JSON.parse(this.response).error;
console.log(error.message);
//errorMessage.textContent = error.message;
}
}
};
req.send();
}
The Active Directory Authentication Library (ADAL) for JavaScript helps you to use Azure AD for handling authentication in your single page applications. This library is optimized for working together with AngularJS.
Based on the investigation, this issue is caused by the handleWindowCallback. The response not able to run into the branch for if ((requestInfo.requestType === this.REQUEST_TYPE.RENEW_TOKEN) && window.parent && (window.parent !== window)) since it is not used in the Angular enviroment.
To integrate Azure AD with MVC application, I suggest that you using the Active Directory Authentication Library. And you can refer the code sample here.
Update
if (isCallback) {
// x.handleWindowCallback();
var requestInfo=x.getRequestInfo(window.location.hash);
//get the token provided resource. to get the id_token, we need to pass the client id
var token = x.getCachedToken("{clientId}")
x.saveTokenFromHash(requestInfo);
} else {
x.login();
}

How to make JWT cookie authentication in Laravel

I want to have JWT authentication in Laravel >=5.2, using this (Tymon JWT-auth) library but I want to put JWT token into HttpOnly Cookies - to protect JWT token from steal from XSS attack.
I set up Tymon library and... in project: app/Providers/RouteServiceProvider#mapWebRoutes i deactivate execution 'web' middelware group for all requests (which is default laravel behavior - you can see it by php artisan route:list) by remove 'middleware' => 'web' (If I don't do it, i will see CSRF problem with post request).
in routes.php i write:
Route::group(['middleware' =>'api', 'prefix' => '/api/v1', 'namespace' => 'Api\V1'], function () {
Route::post('/login', 'Auth\AuthController#postLogin');
...
Route::get('/projects', 'ProjectsController#getProjects');
}
In may Api\V1\Auth\AuthController#postLogin i generate token and send it back as httpOnly cookie:
...
try
{
$user = User::where('email','=',$credentials['email'])->first();
if ( !($user && Hash::check($credentials['password'], $user->password) ))
{
return response()->json(['error' => 'invalid_credentials'], 401);
}
$customClaims = ['sub' => $user->id, 'role'=> $user->role, 'csrf-token' => str_random(32) ];
$payload = JWTFactory::make($customClaims);
$token = JWTAuth::encode($payload);
} catch(...) {...}
return response()->json($payload->toArray())->withCookie('token', $token, config('jwt.ttl'), "/", null, false, true);
And, yeah here question starts. I would like to do something (may be modifiy laravel Auth class) on each request:
get coookie from request
decode it
check is right (if not trhow 401)
get user from DB
and make that method Auth::user() works every where like in usual way in laravel (so i can use it in each Controller for example)
Any ideas how to do point 4 ?
UPDATE
I also add here protection for CSRF attack - csrf-token is in JWT, and it is also return in body of response for login request (so JS have acces to this csrf-token) (i return only public part of JWT token in login response, whole JWT is return only in cookie, so it is XSS safe) - then front JS must copy csrf-token into header of each request. Then the middelware JWTAuthentiacate (in my answer below) compare csrf-token header with csrf-token field in JWT payload - if they are similar then request pass csrf test.
You can do it simple by creating middleware.
In handle() method just get cookie from request, decode it and login a user using id with this Laravel method:
Auth::loginUsingId($userIdFromToken);
I implement #ƁukaszKuczmaja idea in this way an it works! :) . So i create file in app/Http/Middleware/JWTAuthenticate.php :
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use JWTAuth;
use Tymon\JWTAuth\Token;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Illuminate\Session\TokenMismatchException;
class JWTAuthenticate
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string|null $guard
* #return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
try {
if(!$request->headers->has('csrf-token')) throw new TokenMismatchException();
$rawToken = $request->cookie('token');
$token = new Token($rawToken);
$payload = JWTAuth::decode($token);
if($payload['csrf-token'] != $request->headers->get('csrf-token')) throw new TokenMismatchException();
Auth::loginUsingId($payload['sub']);
} catch(\Exception $e) {
if( $e instanceof TokenExpiredException) {
// TODO token refresh here
}
return response('Unauthorized.', 401);
}
return $next($request);
}
}
In app\Http\Kernel.php#$routeMiddelware I add line:
'jwt.auth' => \App\Http\Middleware\JWTAuthenticate::class,
My routing file looks like this now:
Route::group(['middleware' =>'api', 'prefix' => '/api/v1', 'namespace' => 'Api\V1'], function () {
Route::post('/login', 'Auth\AuthController#postLogin');
Route::group(['middleware' =>'jwt.auth'], function () {
Route::post('/projects', 'ProjectsController#postProjects');
Route::get('/projects', 'ProjectsController#getProjects');
Route::put('/projects/{project}', 'ProjectsController#putProjects');
Route::delete('/projects/{project}', 'ProjectsController#deleteProjects');
});
});
And for instance in app/Http/Controllers/Api/V1/ProjectsController.php i have:
public function getProjects() {
$uid = Auth::user()->id;
return Project::where('user_id','=',$uid)->get();
}
Actually you can put every route that needs authentication within a route group and add the middleware like this:
Route::group(['middleware' => ['jwt.auth']], function () {
Route::patch('/profile', 'UserController#update');
});
The middleware already does what you wanted so there is no need to write additional logic. Don't use an additional handle method.
Within your i.e. UserController you can then i.e.
$user = \Auth::user();
And i.e. depending what you need...
// assign fields
$user->save();
return 'success'; // or whatever you need
Don't reinvent the wheel and keep things DRY.