Its to my knowledge that a JWT based authorization system is usually reserved for SPA'S ( you know, one view, one React/Angular/Vue app, with one bloated app.js file), however I'm attempting to utilize the magic of JWT with a slightly separate structured application.
Structure
Rather than serving up one blade.php view from my Laravel app that garners one Vue app and instance, I'm attempting to serve up TWO separate blade.php views, that each operate as their own separate Vue SPA: one for the exterior of the application (pre-auth) and another for the interior of the app (post-auth).
Current State of App
To power my app's authentication system, I've utilized Tymon's jwt-auth lib ( a beautiful lib btw ) and tie everything together on the front with (as previously stated) Vue/Vuex. Everything works as expected, in my Register and Login components I'm able to hit my api, get a JWT in response, store it locally then annex said token into my Axios headers allowing all subsequent requests to harbor this token.
Dilemma
Now I'm at a crossroads. The post-auth route/view that I want to serve up is protected by a custom JWT middleware that redirects if a valid token is not presented:
Route::get('/home', 'Auth\HomeController#home')->middleware('jwt');
middleware
class JWT
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
JWTAuth::parseToken()->authenticate();
return $next($request);
}
}
and my pre-auth view and all its routes are protected by Laravel's native guest RedirectIfAuthenticated middleware, which is Guarded by JWT now:
class RedirectIfAuthenticated
{
/**
* 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)
{
if (Auth::guard($guard)->check()) {
return redirect('/home');
}
return $next($request);
}
}
Questions
So this begs down to the following questions:
1) after a successful register/login on the front-end and a JWT is generated, stored locally and in Axios headers, How do I then redirect to my post-auth route with this valid token available?
2) How do I then make sure that Valid JWT persist and is present when the guest routes are hit to successfully redirect back to my post-auth route?
I'd prefer to keep all redirects and persistance checks on the backend if feasible
On successful login, you will have the token, let's say its called $jwt_token
You can redirect to the page you are protecting once authorized and set the cookie in the response:
return redirect('/home')->cookie(
'access_token', //name
$jwt_token, //value
config('session.lifetime'), //expiration in minutes (matches laravel)
config('app.url'), // your app url
true // HttpsOnly
);
From here, Axios can have access to the cookie by parsing the cookies on the document and retrieving the access_token
let token = document.cookie.split(';') // get all your cookies
.find(cookie => cookie.includes('access_token')) // take only the one that matches our access_token name
.split('=')[1] // get just the value after =
// terrible code example above for you
Now you can use this in your Axios requests by adding it as the value to Bearer in the Authorization header:
Authorization: `Bearer ${token}`
Your JWT middleware already leverage the authenticate method and therefore it should be handling the expiry for you as it stands:
JWTAuth::parseToken()->authenticate();
Under the hood this will attempt to validate the token's expiry based on the current TTL set in the config/jwt.php file. Given your work flow, I would also blacklist the token if it expires. You can add an Event Listener that listens for expired tokens and blacklists them by listening to Event::listen('tymon.jwt.expired');.
Please excuse any syntax errors, formatting issues or misspellings, I'm on my pone and will edit later to resolve those.
So there are a couple of ways you can make sure the JWT token is available everywhere for Axios or indeed any frontend to use.
The most common way is to store the token in either a cookie or in the Web Storage of the browser (localStorage / sessionStorage)
The difference between localStorage and sessionStorage is that data stored in localStorage persists through browser sessions, sessionStorage is cleared when the page session ends.
The general consensus is that cookies are slightly more secure because they have a smaller attack vector, though neither method is completely secure. If you want to go more in depth you can start by reading this article.
To get more specific concerning your problem, first you want to setup token storage using one of the methods explained above, recommended method is cookies, you can find examples of how to do it with pure Javascript here.
Now that you have the token on every page you can redirect the user whichever way you like. Though I would suggest that instead of using your own middleware for JWT authentication, you can use the one that the JWT library provides: jwt.auth.
This middleware will automatically respond with error codes if something is wrong with the token, if there is it will return one of the following HTTP responses:
token_not_provided
token_expired
token_invalid
user_not_found
If one of these responses are returned (or if the request status code is 400) you can simply use the frontend to redirect the user back to your pre-auth routes.
When signing in, after saving the token to a cookie, use the frontend to redirect to the post-auth routes.
I know you said you wanted to keep redirect logic in the backend but that doesn't really make sense when you're for example calling the API when you're signing in, you can't really both return the token and cause a redirect at the same time from just the backend.
UPDATE
Very simple example of how you can authenticate with only guard and still get a token for the API. Borrowing from the redirect example from #Ohgodwhy, you can put the following inside your RedirectIfAuthenticated middleware.
public function handle($request, Closure $next, $guard = null)
if (Auth::guard($guard)->check()) {
if ((\Cookie::get('access_token') == null)) {
$cookie = \Cookie::make(
'access_token',
\JWTAuth::fromUser(Auth::user()),
config('session.lifetime'),
null,
$request->refeerer,
false, // to make the cookie available in javascript
false // to make the cookie available in javascript
);
return redirect('/home')->cookie($cookie);
} else {
return redirect('/home');
}
}
return $next($request);
}
Just make sure that your $redirectTo in app/Http/Controllers/Auth/LoginController.php is set to a path that implements the RedirectIfAuthenticated middleware.
So here's the logic I ended up implementing:
In my LoginController.php login function, after successful authentication and generation of JWT, I return a response with Json and a new cookie, both with new token passed:
public function login(Request $request)
{
$creds = $request->only(['email', 'password']);
if (!$token = auth()->attempt($creds)) {
return response()->json([
'errors' => [
'root' => 'Incorrect Credentials. Try again'
]
], 401);
}
return $this->respondWithToken($token);
}
protected function respondWithToken($token)
{
return response()->json([
'meta' => [
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60
]
], 200)
->withCookie(cookie('access_token', $token, auth()->factory()->getTTL()));
}
In my guest RedirectIfAuthenticated middleware check for cookie, if exists, setToken which in turn sets Guard to Authenticated and will always redirect to /home if token is available and valid:
public function handle($request, Closure $next, $guard = null)
{
if ($request->hasCookie('access_token')) {
Auth::setToken($request->cookie('access_token'));
}
if (Auth::guard($guard)->check()) {
return redirect('/home');
}
return $next($request);
}
And In my post-auth Routes middleware I also setToken and if its valid and exists, will allow access, otherwise will throw a range of JWT errors which just redirect to pre-auth view:
public function handle($request, Closure $next)
{
JWTAuth::setToken($request->cookie('access_token'))->authenticate();
return $next($request);
}
Finally, I decided to handle redirection in the front-end being I'm using Axios which is promised based and can assure that cookie will be set before redirecting to post-auth view so no funny business happens! Cheers! Hope this helps anyone on their quest to Multi-Page SPA magic!
Related
I am coding a Laravel API hosted on api.url.com for an application hosted on www.myurl.com on a server different to the Lravel API using Laravel Fortify.
The problem comes when the user verifies their email on the generated link, they are redirected not to the external application but again back to the API, but on their browser.
THe docs state that this configuration in the Authenticate.pgp Middleware would work:
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* #param \Illuminate\Http\Request $request
* #return string|null
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return url(env('SPA_URL') . '/dashboard');
}
}
}
Where SPA_URL is set in the .env to www.myurl.com.
This redirects to api.url.com/www.myurl.com/dashboard Obviously causing a 404 response.
I then tried to call an action on a controller as such
public function redirectDashboard(Request $request){
return redirect()->away('www.myurl.com/dashboard');
}
This again redirects to api.url.com/www.myurl.com/dashboard Obviously causing a 404 response.
I have no clue why redirect()->away would still redirect to a url on the same API domain.
Any help would be appreciated, thank you.
The RedirectTo method will redirect to request.
Try to write new middleware
public function handle($request, Closure $next)
{
//check here if the user is authenticated
if ( ! $this->auth->user() )
{
// here you should redirect to another site
return redirect(env('SPA_URL') . '/dashboard');
}
return $next($request);
}
I finally realised what was happening with the redirect: I was using the redirect() helper function rather than the Redirect::class. The helper function appends the current domain to any url or away method call whereas the Redirect::class allows you to redirect to anywhere you need.
TLDR; see image below 3 - is that possible and how?
I read about API protection - Sanctum & Passport, but none of these seems what I can accomplish with my app since it's a little specific and simplified in a way.
For example, Sanctum's way of authenticating sounds like something I'd like, but without the /login part (i have a custom /auth part, see below.): https://laravel.com/docs/8.x/sanctum#spa-authenticating.
If the login request is successful, you will be authenticated and
subsequent requests to your API routes will automatically be
authenticated via the session cookie that the Laravel backend issued
to your client.
My app has no login per se - we log-in the user if they have a specified cookie token verified by the 3rd party API (i know token-auth is not the best way to go, but it is quite a specific application/use). It's on /auth, so Sanctum's description above could work, I guess if I knew where to fiddle with it. Our logic:
VueJS: a mobile device sends an encrypted cookie token - app reads it in JS, sends it to my Laravel API for verification.
Get the token in Laravel API, decrypt, send to 2nd API (not in my control), verifying the token, and sends back an OK or NOT OK response with some data.
If the response was OK, the user is "logged-in."
The user can navigate the app, and additional API responses occur - how do I verify it's him and not an imposter or some1 accessing the API directly in the browser?
I guess the session could work for that, but it's my 1st time using Laravel, and nothing seemed to work as expected. Also, sessions stored in files or DB are not something I'm looking forward to if required.
For example, I tried setting a simple session parameter when step 3 above happened and sending it back, but the session store was not set up, yet it seemed at that point. Then I could check that session value to make sure he's the same user that was just verified.
For an easier understanding of what I'm trying to accomplish and if it's even feasible:
The main question is, what is the easiest way to have basic API protection/authentication/verification whilst sending the token for authentication to 3rd party API only on 1st request (and if the app is reopened/refreshed of course) - keeping in mind, that no actual users exist on my Laravel API.
Or would it be best to do the token-auth to the 3rd party API on each request?
If I understand your case correctly there's no real User model involved, right? If so, you'll not be able to use any of Laravel's built-in authentication methods as they all rely on the existence of such a model.
In that case you'll need one endpoint and a custom authentication Middleware that you'll need to create yourself in Laravel in order to handle everything:
The endpoint definition:
Route::post('authenticate', [TokenController::class, 'login']);
The controller:
class TokenController extends Controller
{
public function login(Request $request)
{
// First read the token and decrypt it.
// Here you'll need to replace "some_decryption()" with the required decrypter based on how your VueJS app encrypts the token.
$token = some_decryption( $request->input('token') );
// Then make the request to the verification API, for example using Guzzle.
$isTokenOk = Http::post('http://your-endpoint.net', [
'token' => $token,
])->successful();
// Now issue a Laravel API token only if the verification succeeded.
if (! $isTokenOk) {
abort(400, 'Verification failed');
}
// In order to not store any token in a database, I've chosen something arbitrary and reversibly encrypted.
return response()->json([
'api-token' => Crypt::encrypt('authenticated'),
]);
}
}
Subsequent requests should pass the api token in the Authorization header as a Bearer token. And then in the Middleware you'll check for Bearer token and check if it matches our expected value:
class AuthTokenAuthenticationMiddleware
{
public function handle($request, Closure $next)
{
$authToken = $request->bearerToken();
if (! $authToken || ! Crypt::decrypt($authToken) === 'authenticated') {
abort(401, 'Unauthenticated');
}
return $next($request);
}
}
The Middleware needs to be registered in app/Http/Kernel.php:
protected $routeMiddleware = [
...
'auth-token' => AuthTokenAuthenticationMiddleware::class,
];
And finally apply this new middleware to any of your routes that should be authenticated:
Route::middleware('auth-token')->get('some/api/route', SomeController::class);
Warning: this authentication mechanism relies on reversible encryption. Anyone able to decrypt or in possession of your APP_KEY will ultimately be able to access your protected endpoints!
Of course this is one way to deal with custom userless authentication and there are many more. You could for example insert an expiration date in the encrypted token instead of the string "authenticated" and verify if it's expired in the middleware. But you get the gist of the steps to be followed...
If you do have a User model in place, then you could use Laravel Sanctum and issue an API token after User retrieval instead of forging a custom encrypted token. See https://laravel.com/docs/8.x/sanctum#issuing-mobile-api-tokens
// Fetch the corresponding user...
$user = User::where('token', $request->input('token'))->first();
return $user->createToken('vuejs_app')->plainTextToken;
Subsequent requests should pass the token in the Authorization header as a Bearer token.
Protect routes using the middleware provided by Sanctum:
Route::middleware('auth:sanctum')->get('some/api/route', SomeController::class);
I'm working on a personal project using Laravel 5.6 and Axios library (standard Laravel 5.6 package).
I need to send first GET then POST request using Laravel's API and axios' http requests, but I'm not using Passport or any library like that since it's an internal API serving only VueJS to obtain stuff from the database.
If I set my API routes using auth:api middleware, I always get unauthorized response, whichever is the request type, this is the error output :
Error: Request failed with status code 401
The message :
message: "Unauthenticated."
But, as I read in the documentation, axios headers are set inside laravel's bootstrap.js to read the authorization token from meta tags (and the code is there) :
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// further down the file...
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
While, if needed, here's the http request code :
axios.post('/api/latest', formData)
So, why am I unauthenticated if those are set?
I've tried removing the auth:api middleware and, of course, it's working:
- Route::middleware('auth:api')->get('/latest', 'InternalApiController#latestEp');
+ Route::get('/latest', 'InternalApiController#latestEp');
What am I doing wrong?
I'm not using Passort or any library like that since it's an internal API serving only VueJS to obtain stuff from the database.
If the API is not stateless, meaning that the user is known to be logged in with a standard session cookie, then you can just use the default 'web' middleware for the API routes.
In the default RouteServiceProvider, change the mapApiRoutes function to use the web middleware instead:
protected function mapApiRoutes()
{
Route::prefix('api')
// ->middleware('api')
->middleware('web')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
}
That being said, you should really put the API routes behind the default 'auth' middleware since they're not throttled by default.
In the routes/api.php file:
Route::group(['middleware' => 'auth'], function() {
Route::get('/latest', 'InternalApiController#latest');
});
And if you want to ensure it's an AJAX request, you can create a simple middleware that checks that the request has the X-Requested-With header set to XMLHttpRequest.
class RequestIsAjax
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if (!$request->ajax()) {
return redirect()->route('login.index');
}
return $next($request);
}
}
And register it within the $routeMiddleware array inside the \App\Http\Kernel class.
protected $routeMiddleware = [
'ajax' => \App\Http\Middleware\RequestIsAjax::class,
With Laravel 8, I was getting a 401 error when trying to request something from the backend api. Following the accepted answer got me close, but it still didn't work. I believe this was because I have the dashboard on a subdomain as users.mydomain.tld and where I was using Axios was on the primary domain as mydomain.tld/some/path. The issue is the path for the session cookies. When re-configuring this, you need to clear all the cookies (dev toolbar). Then re-login like normal after you have made your changes. Else, the cookies will be messed up and you won't see it fixed or you wont be able to login at all until you clear them.
I reverted all the changes I made (following the accepted answer) and pinned down the SESSION_DOMAIN environment variable being the key ingredient, at least when it comes to the main user's area being on a sub-domain.
In your .env file:
SESSION_DOMAIN=".yourdomain.tld"
In app/Providers/RouteServiceProvider.php change api to web in the boot() method:
Route::prefix('api')
//->middleware('api')
->middleware('web')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
In routes/api.php:
Route::middleware('auth')->get('/user', function (Request $request) {
return $request->user();
});
In your resources/js/bootstrap.js:
window.axios.defaults.withCredentials = true;
After modifying the bootstrap.js file, you will need to run npm run dev again to recompile.
Now, you should be able to use Axios in your frontend JavaScript. Whether it be jQuery or Vue, whatever you are using.
axios.get('/api/user')
.then(response => console.dir(response.data))
.catch(error => console.log(error));
CSRF-token is not the same as an authorization token. Most likely you will manually need to add the authorization token, which is probably like this, depending on your method of authorization.
I'm trying to understand how the middleware works in Laravel. Here's my class can any one explain how does its works.?
<?php
namespace App\Http\Middleware;
use Closure;
class CheckAge
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if ($request->age <= 200) {
return redirect('home');
}
return $next($request);
}
}
Thanks
Middleware provide a convenient mechanism for filtering HTTP requests entering your application. For example, Laravel includes a middleware that verifies the user of your application is authenticated. If the user is not authenticated, the middleware will redirect the user to the login screen. However, if the user is authenticated, the middleware will allow the request to proceed further into the application.
Reference
Edit: As explained by #num8er
Middleware is the function (or logic) that stands between router and route handler.
In your code:
public function handle($request, Closure $next)
{
if ($request->age <= 200) {
return redirect('home');
}
return $next($request);
}
$request->age is a variable that provided in request and can be checked on each HTTP request, if its value <= 200 then user redirects to home route.
As you can see what the middleware is, now lets see the code
public function handle($request, Closure $next)
{
if ($request->age <= 200) {
return redirect('home');
}
return $next($request);
}
This code check every request and check the age variable in the request. If the age is less than 200 then the request will be redirect to the home otherwise it will go to the requesting page. Suppose you are requesting /about page but if you can not pass the middleware condition you will be redirected to /home otherwise to /about i.e. given by return $next($request);. Similary works with auth and cors middleware. You can similarly do some check like $request->user->role=='admin' and redirect to admin page or to other page.
return $next($request); this gives you the next requesting route (the original route that have requested)
Middleware provide a convenient mechanism for filtering HTTP requests entering your application. For example, Laravel includes a middleware that verifies the user of your application is authenticated. If the user is not authenticated, the middleware will redirect the user to the login screen. However, if the user is authenticated, the middleware will allow the request to proceed further into the application.
Of course, additional middleware can be written to perform a variety of tasks besides authentication. A CORS middleware might be responsible for adding the proper headers to all responses leaving your application. A logging middleware might log all incoming requests to your application.
https://laravel.com/docs/5.4/middleware#introduction
Middleware is a series of wrappers around your application that decorate the requests and the responses in a way that isn't a part of your application logic.
https://mattstauffer.co/blog/laravel-5.0-middleware-filter-style
Middleware main objective is to restrict the unwanted action and here you can check the user given input values and you can allow is valid only.
I have followed the steps of this tutorial in order to setup API authentication in Laravel 5.3 using the auth:api middleware.
Everything is working as expected apart from I am finding that in order to successfully access my API, I only need to provide an API token for any user in my users table rather than the token linked to the user currently logged in.
Is this expected behaviour because this middleware is stateless? I am using AdLdap2 to authenticate users but I can't see what I could be doing wrong especially as I have followed the steps in the tutorial above.
I have also had a look at TokenGuard which deals with API token validation but can't see any logic that ensures the token matches that of the user logged in.
Any help is much appreciated.
I think the authentication works like expected. The only thing auth:api does is making sure you are authenticated to access the protected routes. If you want to protect a specific route to only one user you could use Gates
So if you have a route to access a user like below and you want the user that's logged in to only access herself you can add a gate.
routes/web.php
Route::get('/user/{id}', function ($id) {
$userYourTryingToView = User::find($id);
if (Gate::allows('get-user', $userYourTryingToView)) {
// The user is allowed to access herself, return the user.
return response()->json($userYourTryingToView);
}
// User is not authenticated to view some other user.
return response('Unauthenticated', 403);
});
And in your app/Providers/AuthServiceProvider.php
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
Gate::define('get-user', function ($authenticatedUser, $userYourTryingToView) {
return $authenticatedUser->id == $userYourTryingToView->user_id;
});
}