Authentication
verifying who a user is
&
Authorization
verifying what a user has access to
= verifying who a user is
Auth::login()
config/auth.php
users table with Eloquentroutes/web.php routes, not for routes/api.php routes
FortifyServiceProvider
laravel new name-of-my-app
… explain Laravel's authentication without pre-built authentication logic
… for a web application
users and password_resets table in your database
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken(); // can store the hash of a login cookie
$table->timestamps();
});
UserSeeder to the users table's model
class UserSeeder extends Seeder
{
public function run(): void
{
DB::table('users')->insert([
'name' => Str::random(10),
'email' => 'theboss@gmail.com',
'password' => Hash::make('Azerty123'),
// add more column values here e.g. a role, a company_id, ...
]);
}
}
User model (app/Models/User.php)
class User extends Authenticatable
{
// The attributes that are mass assignable.
protected $fillable = ['name', 'email', 'password', ];
// The attributes that should be hidden for serialization
protected $hidden = ['password', 'remember_token', ];
// Get the attributes that should be cast.
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
Route::get('/login', [AuthController::class, 'showLogin'])->middleware('guest')->name('login');
Route::post('/login', [AuthController::class, 'login'])->middleware('guest');
Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth')->name('logout');
public function showLogin(): View
{
return view('login');
}
public function login(Request $request): RedirectResponse
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (Auth::attempt($credentials, $request->filled('remember'))) { // attempt = Auth::hasValidCredentials + Auth::login
$request->session()->regenerate(); // regenerate session id; prevents a session fixation attack
return redirect()->intended('dashboard'); // intended = attempted URL before auth interception
}
return back()->withErrors(['email' => 'The provided credentials do not match our records.'])->onlyInput('email');
}
public function logout(Request $request): RedirectResponse
{
Auth::logout();
$request->session()->invalidate(); // remove session data and regenerate session id
$request->session()->regenerateToken(); // regenerate CSRF token
return redirect('/');
}
<x-layout>
<x-errors/>
<form action="{{ route('login') }}" method="post">
@csrf
<label for="email">Email</label>
<input type="email" name="email" id="email" value="{{ old('email') }}" required="required">
<label for="password">Password</label>
<input type="password" name="password" id="password" value="">
<label for="remember_me">Remember me</label>
<input type="checkbox" name="remember" id="remember_me">
<button type="submit">Log in</button>
</form>
</x-layout>
bootstrap/app.php
// by default it redirects the unauth. user to the route *named* login
->withMiddleware(function (Middleware $middleware) {
$middleware->redirectGuestsTo('/login');
// Or, using a closure...
$middleware->redirectGuestsTo(fn (Request $request) => route('login'));
})
auth middleware to each action requiring authentication
guest middleware to each action requiring not being authenticated
Auth facade in your code
use Illuminate\Support\Facades\Auth;
$user = Auth::user(); // retrieves the authenticated user; same as $user = $request->user();
$tasks = Auth::user()->tasks; // oh yes! it's an Eloquent object!
$id = Auth::id(); // retrieves the currently authenticated user's ID
if (Auth::check()) {
// the user is logged in...
// you don't need to check this when the route is behind the auth middleware
}
Auth::logout();
@auth
// The user is authenticated...
@endauth
@guest
// The user is not authenticated...
@endguest
routes/auth.php included from routes/web.phpAuthenticatedSessionController for login composer require laravel/fortify
php artisan fortify:install
php artisan migrate
= verifying what a user has access to
boot() of App\Providers\AppServiceProvider
User (will be bound to the Eloquent model of the authenticated user)
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->user_id;
});
}
if (! Gate::allows('update-post', $post)) {
abort(403);
}
// Update the post...
Gate::authorize('update-post', $post);
// Update the post...
authorize() will automatically generate 403 when it fails
public function authorize(): bool
{
return Gate::allows('update-post', $post);
}
Gate::allows() and Gate::denies()Gate::check(), Gate::any() and Gate::none() Gate::forUser($user)App\Models\Product modelphp artisan make:policy ProductPolicy --model=Product
The option --model adds example methods related to viewing, creating, updating, and deleting a Product
app/Policies
model class name + Policy (auto-discovery) App\Providers\AppServiceProvider
public function boot(): void
{
Gate::policy(Product::class, ProductPolicy::class);
}
or, alternatively, place the UsePolicy attribute on a model class
#[UsePolicy(ProductPolicy::class)]
class Product extends Model
<?php
namespace App\Policies;
use App\Models\Product;
use App\Models\User;
class ProductPolicy
{
/**
* Determine if the given product can be updated by the user.
*/
public function update(User $user, Product $product): bool
{
return $user->id === $product->user_id;
}
/**
* Determine if the given user can create products.
*/
public function create(User $user): bool
{
return $user->role == 'admin';
}
}
can() and cannot()
if ($request->user()->cannot('update', $product)) { // Auth::user() is fine too ;-)
abort(403);
}
// Update the product...
Gate::authorize('update', $product); // confusing though
// Update the product...
authorize() will automatically generate 403 when it fails
public function authorize(): bool
{
$product = Product::find($this->product_id); // given product_id is an input value of the Request
return $this->user()->can('update', $product);
}
if ($request->user()->cannot('create', Product::class))
can middleware 😀
use App\Models\Product;
Route::put('/product/{product}', function (Product $product) {
// The current user may update the product...
})->middleware('can:update,product');
Route::post('/product', function () {
// The current user may create products...
})->middleware('can:create,App\Models\Product');
use App\Models\Product;
Route::put('/product/{product}', function (Product $product) {
// The current user may update the product...
})->can('update', 'product');
Route::post('/product', function () {
// The current user may create products...
})->can('create', Product::class);
@can('update', $post)
<!-- The current user can update the post... -->
@elsecan('create', App\Models\Post::class)
<!-- The current user can create new posts... -->
@else
<!-- ... -->
@endcan
@cannot('update', $post)
<!-- The current user cannot update the post... -->
@elsecannot('create', App\Models\Post::class)
<!-- The current user can now create new posts... -->
@endcannot
/products, /products/{id} and /products/create ?
/products/create from unauthenticated & unauthorized (= customer) usersAuthorization header
EnsureFrontendRequestsAreStateful to api group
Origin or Referer header from stateful domain)
X-XSRF-TOKEN
GET /sanctum/csrf-cookie handing out a XSRF-TOKEN cookie
auth:sanctum to protect your Web API routes
php artisan install:api (we already did so)bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->statefulApi();
})
config/sanctum.php and make sure that your front end domain:port combination is in SANCTUM_STATEFUL_DOMAINS. This setting determines for which domains authentication cookies are set.
localhost:5173 is not the same as 127.0.0.1:5173
config/cors.php by php artisan config:publish cors.
'supports_credentials' => true.
Access-Control-Allow-Credentials: true
SESSION_DOMAIN=.my-domain.com in .env.
The leading . will ensure session cookies are set for any subdomain of the root domain.
GET /sanctum/csrf-cookie hands out CSRF tokens as it sets an XSRF-TOKEN cookie
X-XSRF-TOKEN header(!) of subsequent requestsroutes/web.php (!!!) or use Fortify
POST /api/login
Route::post('/api/login', function (Request $request): Response {
$credentials = $request->only('email', 'password');
if (Auth::attempt($credentials)) {
$request->session()->regenerate(); // do your tests first without this line
return response(['message' => 'The user has been authenticated successfully'], 200);
}
return response(['message' => 'The provided credentials do not match our records.'], 401);
});
web mechanisms apply here, after successful login Laravel sets a session cookie (e.g. laravel_session), of which the server remembers there is successful authentication and by whomroutes/api.php by plugging the auth:sanctum middleware:
Route::get('/user', function (Request $request): User {
return $request->user();
})->middleware('auth:sanctum');
laravel_session, the user is authenticated in the routeOrigin or Referer header so Sanctum can recognize them as frontend requestsroutes/web.php (!!!)
POST /api/logout
Route::post('/api/logout', function (Request $request): Response {
Auth::logout();
$request->session()->invalidate();
return response(['message' => 'The user has been logged out successfully'], 200);
});
Route::post('/v4/products', function (Request $request): array {
Gate::authorize('add-product');
$request->validate([
'name' => 'required|unique:products|max:125',
'price' => 'required|numeric|min:0.10',
'description' => 'required',
'brand_id' => 'required|exists:brands,id'
]);
$product = new Product($request->all());
$product->user()->associate(Auth::user());
$product->save();
return ['message' => 'The product has been created'];
})->middleware('auth:sanctum');