Are Sanctum and Laravel's default auth the same if not used for tokens? - laravel

I'm not quite sure about what is meant in the Laravel documentation, so I'm asking to be sure.
We have the default authentication of Laravel on one side and Sanctum on the other.
It is stated that Sanctum can either do Tokens or simply implement auth. :
For this feature, Sanctum does not use tokens of any kind. Instead, Sanctum uses Laravel's built-in cookie based session authentication services. This provides the benefits of CSRF protection, session authentication, as well as protects against leakage of the authentication credentials via XSS. Sanctum will only attempt to authenticate using cookies when the incoming request originates from your own SPA frontend (Vue.js).
Therefor if Tokens are nevers used, Sanctum is basically the same as the default Authentication method, am I correct? Basically, does it implement the default authentication and add tokens if needed on top of that? If so, what is the difference between sanctum and passport since they do the same thing but Sanctum is said to be lightweight. What does that actually mean?
Thanks for reading

Therefore if Tokens are never used, Sanctum is basically the same as the default Authentication method, am I correct?
Yes, under the hood it uses laravel's default auth.
Taking a look at the sanctum guard (below code taken fro github. It was last commited on Apr 11, sanctum 2.x)
<?php
namespace Laravel\Sanctum;
use Illuminate\Contracts\Auth\Factory as AuthFactory;
use Illuminate\Http\Request;
class Guard
{
/**
* The authentication factory implementation.
*
* #var \Illuminate\Contracts\Auth\Factory
*/
protected $auth;
/**
* The number of minutes tokens should be allowed to remain valid.
*
* #var int
*/
protected $expiration;
/**
* Create a new guard instance.
*
* #param \Illuminate\Contracts\Auth\Factory $auth
* #param int $expiration
* #return void
*/
public function __construct(AuthFactory $auth, $expiration = null)
{
$this->auth = $auth;
$this->expiration = $expiration;
}
/**
* Retrieve the authenticated user for the incoming request.
*
* #param \Illuminate\Http\Request $request
* #return mixed
*/
public function __invoke(Request $request)
{
if ($user = $this->auth->guard(config('sanctum.guard', 'web'))->user()) {
return $this->supportsTokens($user)
? $user->withAccessToken(new TransientToken)
: $user;
}
if ($token = $request->bearerToken()) {
$model = Sanctum::$personalAccessTokenModel;
$accessToken = $model::findToken($token);
if (! $accessToken ||
($this->expiration &&
$accessToken->created_at->lte(now()->subMinutes($this->expiration)))) {
return;
}
return $this->supportsTokens($accessToken->tokenable) ? $accessToken->tokenable->withAccessToken(
tap($accessToken->forceFill(['last_used_at' => now()]))->save()
) : null;
}
}
/**
* Determine if the tokenable model supports API tokens.
*
* #param mixed $tokenable
* #return bool
*/
protected function supportsTokens($tokenable = null)
{
return $tokenable && in_array(HasApiTokens::class, class_uses_recursive(
get_class($tokenable)
));
}
}
If you check the _invoke() method,
if ($user = $this->auth->guard(config('sanctum.guard', 'web'))->user()) {
return $this->supportsTokens($user)
? $user->withAccessToken(new TransientToken)
: $user;
}
the authenticated user is found using
$user = $this->auth->guard(config('sanctum.guard', 'web'))->user()
After checking the sanctum config file, there is no sanctum.guard config currently (it's probably meant for some future version), so sanctum checks with the web guard by default, so it's basically doing the same thing as your default web routes.
But you've misunderstood the use of Sanctum. Sanctum is for API authentication and not for web auth (though it can be used web auth as well). Sanctum's non-token auth is for your SPA's to be able to use the same API as mobile applications ( which use token authentication ) without needing tokens and providing the benefits of csrf and session based auth.
To help you understand better, suppose you have build an API which uses tokens (if it's already using sanctum for tokens, that makes things simpler) for authentication. Now you wish to build an SPA ( which may be build inside the laravel project itself, or a seperate project, on same domain or on different domain ) which will use the same API's, but since this will be built by you, it is a trusted site so you don't want it to use tokens but instead use laravel's default session based auth along with csrf protection while also using the same api routes. The SPA will communicate with the server through ajax. You also want to ensure that only your SPA is allowed to use session based auth and not allow other third party sites to use it.
So this is where Sanctum comes in. You would just need to add the Sanctum middleware to your api route group in app/Http/Kernel.php
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
'api' => [
EnsureFrontendRequestsAreStateful::class,
'throttle:60,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
Then configure sanctum to allow your SPA's domain and configure cors (check the docs to learn how to do this). Then just add the auth:sanctum middleware to your route and you're done with the serverside setup.
Now these routes will authenticate users if the request has a token or if it is stateful (session cookie).
Now your SPA can communicate with your API without tokens.
To get csrf protection, call the csrf-cookie request first, this will set up a csrf token in your cookies, and axios will automatically attach it to subsequent requests
axios.get('/sanctum/csrf-cookie').then(response => {
// Login...
})
What is the difference between sanctum and passport since they do the same thing but Sanctum is said to be lightweight.
Well it's just like it says, sanctum is lightweight. This is because Passport provides full Oauth functionality while Sanctum only focuses on creating and managing tokens. To explain Oauth in a simple way, you must have seen those Sign in with Google, Sign in with Facebook, Sign in with Github on different sites, and you can then sign it to those sites using your google/facebook/github account. This is possible because Google, Facebook and Github provide Oauth functionality (just a simple example, not going in to too much detail). For most websites, you don't really need Passport as it provides a lot features that you don't need. For simple api authentication Sanctum is more than enough

NOTE: This answer is for Laravel Sanctum + same-domain SPA
To add to these answers, the default Laravel auth uses the web guard, so you must use that for your auth routes (for same-domain SPA app).
For example, you can create your own routes that point to Laravel's RegistersUsers trait and AuthenticatesUsers trait:
web.php
Route::group(['middleware' => ['guest', 'throttle:10,5']], function () {
Route::post('register', 'Auth\RegisterController#register')->name('register');
Route::post('login', 'Auth\LoginController#login')->name('login');
Route::post('password/email', 'Auth\ForgotPasswordController#sendResetLinkEmail');
Route::post('password/reset', 'Auth\ResetPasswordController#reset');
Route::post('email/verify/{user}', 'Auth\VerificationController#verify')->name('verification.verify');
Route::post('email/resend', 'Auth\VerificationController#resend');
Route::post('oauth/{driver}', 'Auth\OAuthController#redirectToProvider')->name('oauth.redirect');
Route::get('oauth/{driver}/callback', 'Auth\OAuthController#handleProviderCallback')->name('oauth.callback');
});
Route::post('logout', 'Auth\LoginController#logout')->name('logout');
But make sure they are in the web.php file. For example if they are in the api.php file, I saw some weird errors about session store not on request and RequestGuard::logout() is not a function. I believe this has something to do with the default auth guard via $this->guard() in the auth traits, and something to do with api.php's /api prefix.
The /api prefix seemed related because if you use the composer package Ziggy to achieve route('login') and route('logout'), they actually resolve to /api/login and /api/logout.
I suspect that caused an issue with Sanctum. The fix was to make sure the routes were in web.php. A person may reproduce this error if their config is similar, or for example, if they have Auth::routes() declared in api.php.
Double check your Kernel.php (it should be like this):
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
'throttle:60,1',
],
];
If you have StartSession in your api middleware group, your config is incorrect or unnecessarily-convoluted.
Here is my working ./config/auth.php file for your comparison:
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
...
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
Then, you can use the guest middleware for login/registration routes, and very importantly, you should then declare all your JSON-serving endpoints in api.php and use the auth:sanctum middleware on those routes.
Once you think you have it working, I have two test/debug steps for you:
One:
open Chrome > dev tools pane
goto the Applications tab
check to ensure there are two cookies: <app_name>_session, and XSRF-TOKEN
with the remember me checkbox and remember: true in login payload, ensure there is a third cookie for remember_web_<hash>
make sure the session cookie is httpOnly, and make sure the CSRF cookie is not (so your JavaScript can access it)
Two, in your unit tests, ensure that after $this->postJson(route('login'), $credentials), you see this:
Auth::check() should return true
Auth::user() should return the user object
Auth::logout() should log the user out, and immediately following that, $this->assertGuest('web'); should return true
Don't get too excited until you verify those two steps, and do get excited once you successfully verify those steps. That will mean you are using Laravel's default auth logic.
For good measure, here is an example of attaching the CSRF token via JavaScript:
import Cookies from 'js-cookie';
axios.interceptors.request.use((request) => {
try {
const csrf = Cookies.get('XSRF-TOKEN');
request.withCredentials = true;
if (csrf) {
request.headers.common['XSRF-TOKEN'] = csrf;
}
return request;
} catch (err) {
throw new Error(`axios# Problem with request during pre-flight phase: ${err}.`);
}
});

Sanctum has two separate authentication systems, there is the cookie based session authentication meant to use for single page applications, where you are sending api requests (ajax, fetch etc.) instead of server side rendering (which reads the session cookie on every page load), sanctum makes it possible to use this cookie (the default authentication) without page reloads.
The second authentication system is token based and is meant to use for mobile applications.

Related

How to use laravel's can() on routes when using JetStream

I'm trying to work out how to use ->can() on a route when combined with JetStream's permissions.
I can see how to create permissions in the JetStreamServiceProvider.php:
protected function configurePermissions()
{
Jetstream::defaultApiTokenPermissions(['read']);
Jetstream::role('admin', __('Administrator'), [
'export:orders'
])->description(__('Administrator users can perform any action.'));
However I can work out how to combine this with a route:
Route::get('/generate-export/orders', [OrderExportController::class,'show'])->name('exportOrders.show')->can(' -- not sure what to put here --');
I've tried a few things and got nowhere.
Those tokens are used for requests authenticated with sanctum. As their documentation states in:
Sanctum 9.x - Token Ability Middleware
Yoiu need to first add 2 entries to the route middleware array in your App\Http\Kernel.php file.
protected $routeMiddleware = [
...,
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
];
And then, you can use those middlewares in your routes.
Route::get('/generate-export/orders', [OrderExportController::class,'show'])
->name('exportOrders.show')
->middleware(['auth:sanctum', 'ability:export:orders']); // check if user has export:orders ability
As for the difference between abilities and ability, to quote the documentation:
The abilities middleware may be assigned to a route to verify that the incoming request's token has all of the listed abilities:
Route::get('/orders', function () {
// Token has both "check-status" and "place-orders" abilities...
})->middleware(['auth:sanctum', 'abilities:check-status,place-orders']);
The ability middleware may be assigned to a route to verify that the incoming request's token has at least one of the listed abilities:
Route::get('/orders', function () {
// Token has the "check-status" or "place-orders" ability...
})->middleware(['auth:sanctum', 'ability:check-status,place-orders']);

Route type delete does not work in Laravel

I have following route and works
Route::post("delete-role", [RoleApiController::class, "Remove"]);
I tested it through postman like this
http://localhost/delete-role?api_token=hcvhjbhjb12khjbjhc876
Now, if I change above route and convert to type delete..
Route::delete("delete-role/{role_id}", [RoleApiController::class, "Remove"]);
it does not work. I get below error. It seems to be the reason that the api_token is missing.
I get same error when trying to update route like below
Route::delete("delete-role/{role_id}/{api_token}", [RoleApiController::class, "Remove"]);
You have to set header of your request as:
"Accept": "application/json"
in postman.
If you don't set the required header for api, Laravel Passport can't understand request as an API client and so it will redirect to a /login page for the web.
Or you can set a middleware to check it in code:
public function handle($request, Closure $next)
{
if(!in_array($request->headers->get('accept'), ['application/json', 'Application/Json']))
return response()->json(['message' => 'Unauthenticated.'], 401);
return $next($request);
}
You have an incomplete details. but I see few issues here.
You seem to be using web routes for your API requests which is a bad set-up
You do not have a route with login name.
based on the error you posted, your request seems to successfully destroyed the token and logged you out, then called the middleware App\Http\Middleware\Authenticate which supposed to redirect your request to login route which does not exist and the reason you are getting that error.
You can see from that Authenticate middleware it will supposed to redirect you to login route for unauthenticated request. thats why you need to use the api routes so you can handle the response manually
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 route('login');
}
}
}
Also, I'm not so sure about this, but the reason you are not getting the same issue with your POST request is probably because your POST request does not call the Authenticate middleware or whatever in your routes file or class function that calls the authenticate middleware.
But again, just use api routes if you don't want un-authenticated request to automatically redirect your request to login routes which does not exist in your application
The problem is that he doesn't define route ('login'),
add in Exceptions/Handler.php
$this->renderable(function (AuthenticationException $e, $request) {
if ($request->is('api/*')) {
return response()->json(['meassge' => 'Unauthenticated']);
}
});
Then you should use Passport Or Sanctum for auth with Api,
Continue from here https://laravel.com/docs/9.x/passport
Probably, this thread could help you. Route [login] not defined
(OR)
You need to setup auth scaffolding to get login route defined.
Important: your project will be overridden if you setup auth scaffold.
So, I would only recommend doing this if it is a new project or a testing app.
See this for detail doc but install Laravel Breeze would be suffice.
It Appears you have called route('login') without having defined it in your routes, Please remove route('login') from your code or define it in your routes. eg::
Route::get('login', [YourController::class, 'methodName'])->name('login');

How to authenticate a user by its user id in Laravel in a custom middleware

How would you guys go about implementing something like this? I use Laravel as an API only. We have other framework where the login was implemented where it saves an httpOnly cookie (sessionId) after the user logs in. That's the main framework. We're migrating away from that old framework (Zend).
With the sessionId sent to Laravel from, say, a JS frontend, I'm able to lookup the current user based on the sessionId. That sessionId is then used to query the session database. I've created a middleware called "CheckForCurrentUser.php":
[..]
public function handle(Request $request, Closure $next)
{
// The reason for this is that the OPTIONS (request) does not include the cookie in the request.
$method = $request->method();
// SESSIONID is the name of the cookie created from the main framework
// once a user is logged in.
// SESSIONID is an exception in EncryptCookies.php
$sessionId = request()->cookie('SESSIONID');
if ($method === 'POST' && $sessionId !== '') {
// This function is only to get the logged in user id from the session database
$userId = $this->notImportantFunction($sessionId);
if ($userId) {
// User id found so make current user for this Laravel API
Auth::loginUsingId($userId);
} else {
// Instructs the frontend to let user log back in.
return abort(401);
}
}
return $next($request);
}
To recap, a user cannot authenticate from this Laravel application nor can I send an authentication token. They logged in from another framework. Laravel has access to the main framework databases.
This setup works. Using use Illuminate\Support\Facades\Auth; I use that as the "current user" in any controller/model.
Kernel.php looks something like:
[..]
protected $middleware = [
[..]
\App\Http\Middleware\CheckCurrentUser::class,
[..]
];
Before I go any further, is that how you'd implement something like this? It does work (ish) but I do not get the SESSIONID unless I check for a POST request.
I do not like this setup. I'm now using lighthouse and having issues using both the #auth and #inject directives. This is due to how I authenticate a use with Laravel so sorting how I authenticate a user should sort Lighthouse. Any tips on how to refactor this the right way? My routes are with /api/some-string
Lighthouse is not the issue. Should I send a authentication header, Lighthouse works. I believe Laravel does something behind the scenes with it sees a token in the header. I cannot send an authentication. I can only rely on the cookie. It's httpOnly so I have no access to that from JavaScript.

Laravel passport change header authentication

I am using Laravel passport and it requires to send in every request the header Authentication to be sent.
Is it possible to change the name of the header to X-Access-Token?
I saw passport uses the package
League\OAuth2\Server\AuthorizationValidators;
method:
/**
* {#inheritdoc}
*/
public function validateAuthorization(ServerRequestInterface $request)
{
dd($request);
if ($request->hasHeader('authorization') === false) {
throw OAuthServerException::accessDenied('Missing "Authorization" header');
}
$header = $request->getHeader('authorization');
$jwt = trim(preg_replace('/^(?:\s+)?Bearer\s/', '', $header[0]));
I tried to change here but seems the validation of the headers happen before this method.
There are many fundamental pieces of code that rely on the existence of the authorization header.
You could roll your own if you felt so inclined.
Note also that authorization is a web standard request header. X-Access-Token is a response header pattern.
*Edit**
Given our conversation below, you can use Middleware and Middleware priority to dictate which runs first, observe requests that have an X-Access-Token and use addHeader to convert the value of that header to authorization:
php artisan make:middleware AuthorizationToolMiddleware
Then in the handle function:
public function handle($request, Closure $next)
{
$request->headers->set('Authorization', $request->headers->get('X-Access-Token'));
return $next($request);
}
This middleware should execute before other middleware in order to ensure the headers are set by the time that passport handles the request.
For Laravel 5.8 you'd have to force your custom middleware to always be on top of the call chain
So in your app\kernel.php add this -
protected $middlewarePriority = [
\App\Http\Middleware\AuthorizationToolMiddleware::class,
];

Sending POST request while using jwt with laravel 5.2

I am using jwt:auth with laravel 5.2 to authenticate in a secure manner against CSRF attacks.
You can find how to do here >> http://blog.nedex.io/create-an-api-server-for-mobile-apps-using-laravel-5-1/
I have edited the except array in VerifyCsrfToken.php middleware and added api/login and api/signup, so i can skip jwt tokens for these two actions because while i am trying to login or signup, so i still have no keys, yet.
Route::group(['prefix' => 'api'], function() {
Route::post('login', 'Api\AuthController#login');
Route::post('signup', 'Api\UsersController#signup');
Route::group(['middleware' => ['jwt.auth', 'jwt.refresh']], function() {
Route::post('logout', 'Api\AuthController#logout');
Route::get('test', function(){
return response()->json(['foo'=>'bar']);
});
Route::get('hospitals', 'Api\EmergenciesController#getHospitals');
Route::get('emergencyDetails/{id}', 'Api\EmergenciesController#getEmergencyDetails');
Route::get('profile/{id}', 'Api\UsersController#get_profile');
Route::post('submit_profile/{id}', 'Api\UsersController#submit_profile_data');
Route::post('update_property/{id}/{property}/{value}', 'Api\UsersController#update_property');
Route::post('pregnant/{id}', 'Api\UsersController#update_is_pregnant');
});
});
it works perfectly, when i send GET requests with ?token=
But it gives me a TokenMismatchException when i try to send POST request although i am sending the token.
check the screenshots here >> https://www.dropbox.com/sh/a901epayh1liapd/AABCwxxBN4pSG735SxQlC2jha?dl=0
Why POST requests fail?? please help.!
It happens because Laravel CSRF Protection is enabled default for all routes.You can disable it for you POST request or send CSRF token throw 'X-XSRF-TOKEN' header. But it seems that you don't have it, because you use jwt token(it isn't same). I suggest you exclude you API paths from csrf middleware :
Excluding URIs From CSRF Protection
Sometimes you may wish to exclude a set of URIs from CSRF protection. For example, if you are using Stripe to process payments and are utilizing their webhook system, you will need to exclude your webhook handler route from Laravel's CSRF protection.
You may exclude URIs by defining their routes outside of the web middleware group that is included in the default routes.php file, or by adding the URIs to the $except property of the VerifyCsrfToken middleware:
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* #var array
*/
protected $except = [
'stripe/*',
];
}
Just add new paths like
protected $except = [ 'api/login', 'api/signup', 'api'/logout' , 'api/test', 'api/hospitals']; //etc
For post request you need to pass _token hidden field.
<input type="hidden" name="_token" value="{{ csrf_token() }}" />

Resources