Laravel generate signed route for Nuxt JS front-end - php

I'm using Laravel as an API for my Nuxt JS front-end project. My front-end has a URL structure like this:
http://localhost:3000/omboarding/
http://localhost:3000/onboarding/{signature}/company/
http://localhost:3000/
When a user lands on my index onboarding page, they click a button, and I make a post request to my Laravel's function to generate a signed route. I then need to redirect the user to the company page, and check that the signature hash is valid, and if it is, allow the user to continue, eventually they also end up on the user page.
The problem, is my function generates a signature based entirely on my back-end API, and not my front-end routing at all, how can I resolve this or just pluck the signature part and validate it?
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$signature = URL::signedRoute('onboarding.show', ['is_new_journey' => true]);
return new ApiSuccessResponse($signature, [
'message' => 'Onboarding process started.'
], 201);
}

Related

Adding card to Laravel Jetstream / Livewire profile form

I'm attempting to add a new profile section to the default laravel jetstream profile view. I've created a new livewire component called SetContactPreferences and am calling it from resources/views/profile/show.blade.php by adding:
#livewire('profile.set-contact-preferences')
The form I created shows up as expected in the profile as a new card.
However, I'm unclear on what the submit action should look like. The docs say that the form will submit the current authenticated user as well as the form input, so I created a method in my component like:
public function setContactPreferences($user, $input) {
dd($input);
}
I added <x-jet-form-section submit="setShowingPreferences"> to the top of the set-contact-preferences.blade.php file.
Submitting the form throws the following error:
Illuminate\Contracts\Container\BindingResolutionException
Unable to resolve dependency [Parameter #0 [ <required> $user ]] in class App\Http\Livewire\Profile\SetContactPreferences
The way it works in Jetstream is that when the user profile information form is submitted, that submission is processed by the update method on the ProfileInformationController located at Laravel\Fortify\Http\Controllers\ProfileInformationController.
The update method expects two parameters, a Request object and an UpdatesUserProfileInformation (which is an instance of App\Actions\Fortify\UpdateUserProfileInformation), both of which are injected from the service container.
Jetstream obtains the current user from the Request object using $request->user() which is then passed to the UpdatesUserProfileInformation along with any form input using $request->input().
If you are not concerned with other people updating user profile information (for example a system admin), you can type-hint a Request object in your method signature and it will automatically be injected:
public function setContactPreferences(Request $request)
{
// Dump the currently authenticated user
dd($request->user());
}
Alternatively you could provide your component with a User for scenarios where you might want to update the information of a user that is not the currently authenticated user.
public User $user;
public function setContactPreferences()
{
dd($this->user);
}
Then passing a User to the component:
#livewire('profile.set-contact-preferences', ['user' => Auth::user()])
or
#livewire('profile.set-contact-preferences', ['user' => User::find(1)])

Laravel API verification/protection on subsequent requests: no login / logout and no "users" table

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);

Laravel JWT Multi-Page Structure

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!

Laravel - Getting user details for method used in both web and api routes

I have a project in Laravel where I am using the same method for both a web and api route. So for example i would have the following:
//routes/web.php
Route::post("/import-results", "ImportController#doImport");
//routes/api.php
Route::post("/import-results/{auto}", "ImportController#doImport");
So regardless of the route the doImport() function will be called in the ImportContoller.
The import controller has a function like this (there is validation in the function but I have taken it out for simplicity):
public function doImport($auto = null){
$importData = new DataImport();
$importData->type = $_POST['type'];
$importData->data = $_POST['data'];
$importData->user = Auth::user()->name;
$importData->save();
$data = [
"success" => true,
"message" => "Message Here!"
]
if($auto){
return json_encode($data);
}else{
return view('import-message', $data);
}
}
As you can see this method uses Auth::user()->name; to identify which user imported the data. This is fine if i am logging in and using a regular web route but what about if i'm using an API and using basic auth where no sessions are created and I don't want sessions to persist if the api routes are called.
How do i get the user info when calling API routes?
Also for the web routes i have customised my login as i'm using ldap but essentially the login happens by doing $this->guard()->login($user, false); in a class with the AuthenticatesUsers trait.
I could do this for my API routes too but does this creates a session and how do i clear this session once the request has ended? Or is there a better way??
To make session work for both page submitting and api. You need work in web.php
Make both route in web.php
//Web.php
Route::post("/import-results", "ImportController#doImport");
Route::post("/api/import-results/{auto}", "ImportController#doImport");
api.php is stateless mean no session exist in api.php . It's working with token based like JWT.

Can I override the AuthenticatesUsers login() method to implement a custom login involving a call to a REST web service in this Laravel application?

I am new to PHP and Laravel and I have the following problem.
I know that Laravel provides an ready-to-use login system, created using the statement:
php artisan make:auth
The problem is that this system directly interacts with the database.
My situation is different because my Laravel application implements only the front-end. All of the business logic is handled by a Java back-end application exposing REST web services.
Basically the Laravel front-end application is something like this:
1) A view showing the login form (username and password).
2) A controller class that contains a method that receives the submission from the previous form and then calls the REST web service of the back-end application (sending a request which has an authorization header containing the inserted username and password).
The back-end application will return (into the previous Laravel controller method) a JSON object containing the user information as the response, like this (if the user is authorized)...
{
"userName": "Painkiller",
"email": "painkiller#gmail.com",
"enabled": true
}
...or, in the case that the user is not authorized, something like this...
{
"timestamp": 1485183649134,
"status": 401,
"error": "Unauthorized",
"message": "Credenziali non valide",
"path": "/Extranet/login"
}
My original idea was to write a custom controller that carries out these operations:
The controller handles the login form submission (containing the credentials entered by the user).
The controller calls my back-end web service and obtains a JSON object, if the JSON object represents an authorized user convert it into a PHP model object representing a user.
The controller puts this model object in the session and redirects to the next user page where this information can be retrieved from the session.
I am not so into front-end development but I think that it should work but...I am moving away from Laravel architecture and Laravel logic.
So, my Laravel application can't directly talk with the database but I am thinking that maybe I can try to adopt Laravel architecture.
So I was thinking that in my Laravel project by default I have the \app\Http\Controllers\Auth\LoginController class representing the standard Laravel login system, this one:
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* #var string
*/
protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('guest', ['except' => 'logout']);
}
}
It contains this line:
use AuthenticatesUsers;
I am not so into PHP, what exactly does this line do? At first, I thought that it added the AuthenticatesUsers functionality to the LoginController but it is more a behavior related to extends and inheritance concept.
Anyway it seems that the AuthenticatesUsers class contains the implementation of the logic to handle the login, this method:
public function login(Request $request)
{
$this->validateLogin($request);
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and
// the IP address of the client making these requests into this application.
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
if ($this->attemptLogin($request)) {
return $this->sendLoginResponse($request);
}
// If the login attempt was unsuccessful we will increment the number of attempts
// to login and redirect the user back to the login form. Of course, when this
// user surpasses their maximum number of attempts they will get locked out.
$this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse($request);
}
So I am thinking: can I override this method to perform the call to my web service, obtain the JSON object related to the logged-in user, put this information into the session and redirect to the user page?
Would this be a smart solution for my purpose?
You don't have to do so all you have to do is just make your routes and just override the login method or create your own.
Routes :
// authentication routes
Route::get('/login', 'Auth\LoginController#showLoginForm')->name('adminGetLogin');
Route::post('/login', 'Auth\LoginController#login')->name('adminPostLogin');
Route::get('/logout', 'Auth\LoginController#logout')->name('adminLogout');
// reset password routes
Route::get('/password/reset', 'Auth\ForgotPasswordController#showLinkRequestForm')->name('showResetForm');
Route::post('/password/email', 'Auth\ForgotPasswordController#sendResetLinkEmail')->name('sendResetEmail');
Route::get('/password/reset/{token}', 'Auth\ResetPasswordController#showResetForm')->name('showResetForm');
Route::post('/password/reset', 'Auth\ResetPasswordController#reset')->name('resetPassword');
Web
public function login(Request $request){
if (! Auth::attempt(['email' => $request->email, 'password' => $request->password, 'is_active' => 1])) {
// do whatever yo desire
return redirect('/home');
}
REST API
public function login(Request $request){
if (Auth::attempt(['email' => $request->email, 'password' => $request->password, 'is_active' => 1])) {
// do whatever yo desire
$user = Auth::user();
return response()->json([
'Error' => ['type' => 'success', 'desc' => 'Login successful', 'code' => 0],
'Response' => $user
]);
}

Categories