Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

prod902's avatar

Laravel Spatie Permisions

Hi, I am working on a Laravel 10 project and there are some pages that I want the guests to be able to view, but I keep coming across a 403 User is not Logged in error. I am using spatie/laravel-permission package, and I believe it is that package that is throwing this 403 error. So far this is what my app/Http/Kernel.php file looks like so far

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array<int, class-string|string>
     */
    protected $middleware = [
        // \App\Http\Middleware\TrustHosts::class,
        \App\Http\Middleware\TrustProxies::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array<string, array<int, class-string|string>>
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            // \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

    /**
     * The application's middleware aliases.
     *
     * Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
     *
     * @var array<string, class-string|string>
     */
    protected $middlewareAliases = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
        'signed' => \App\Http\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'cors' => \App\Http\Middleware\Cors::class,
        '2fa' => \App\Http\Middleware\TwoFactorCheck::class,
        'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
        'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
        'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
        'suspension.check' => \App\Http\Middleware\CheckSuspension::class,
    ];
}

When I have a route like the one below it works just fine , but if I have something like the second example I get the 403 error. Any help would be great on this. Thank you.

// Works
Route::get("/test", function(){
     return "Test";   
});

// 403 login error
Route::get('/consent/form/{document}', [ConsentFormController::class, 'document'])
    ->name('client.consent.form');
0 likes
7 replies
nexxai's avatar

Can you show your ConsentFormController class? Is the consent route inside a route group? What do your roles look like? What about your permissions?

Functionally there's nothing wrong with the stuff you already posted so the problem is obviously somewhere else.

prod902's avatar

@nexxai Hi, sure here is the ConsentFormController class:

<?php

namespace App\Http\Controllers\General;

use PDF;
use Carbon\Carbon;
use Ramsey\Uuid\Uuid;
use App\Models\Client;
use App\Components\Search\ConsentFilters;
use App\Components\Consent\ConsentNotification;
use App\Models\ConsentForm;
use App\Models\ConsentSignature;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class ConsentFormController extends Controller
{

    const COUNSELOR_TYPE = 'counselor';

    public function __construct(ConsentNotification $notification)
    {
        $this->notification = $notification;   
    }


    /** 
     * This view will load in another layout file
     * called "external". This view will be sent to a 
     * legal guardian via Twilio.
     * 
     * documentID should be a uuid generated when doc is created
     * 
     * @param string $documentID 
     * @return view 
     */
    public function document($documentID)
    {   
        $document = ConsentForm::with(['signatures', 'client'])
            ->where('document_id', $documentID)
            ->where('current', true)
            ->where('expiry', '>', Carbon::now())
            ->first();

        if (!$document) {
            abort(404);
        }

        return view('general.clients.consent', [
            'document' => $document
        ]); 
    }

    /**
     * This view will be the view that displays a thank message
     * and a form to provide additional info for the client
     * ie medical id, medical info, etc.
     * 
     * @param string $documentID
     * @return view
     */
    public function documentOptionalInfo($documentID)
    {
        $document = ConsentForm::where('document_id', $documentID)
            ->where('current', true)
            ->where('expiry', '>', Carbon::now()) 
            ->first();
 
        if ( ! $document  ) {
            abort(404);
        }

        if ( ! $this->checkIfComplete($document) ) {
            return redirect()->back()->with('completeError', 'Both the parent and client must sign before completing');
        }

        return view('general.clients.consent_additional', [
            'document' => $document
        ]);
    }

    /**
     * Stores additional info to client table from the 
     * additional/thank you page.
     * 
     * @param Request $request
     * @param string $documentID
     * @return response 
     */ 
    public function storeAdditionalInfo(Request $request, $documentID) 
    {
        $document = ConsentForm::where('document_id', $documentID)
            ->where('current', true)
            ->first();
            
        if ( ! $document  ) {
            abort(404);
        }  
 
        $client = Client::findOrFail($document->client_id);
        $client->has_medical = $request->input('insurance_type');
        $client->consent_ssn_or_medical_number = $request->input('medical_number');
        $client->save();
        

        // add expiry. quick way to redirect back with message without
        // 404-ing the page
        $document->expiry = Carbon::now()->addSeconds(5);
        $document->save();

        return redirect()->back()->with('completeSuccess', 'Thank you for submitting! Your counselor should reach out to you shortly');
    }

    /**
     * List of pending consent forms and their status
     * 
     * only cousnelors can see this view
     * 
     * @return view
     */
    public function documentList(ConsentFilters $filters)
    {
        $documents = ConsentForm::filter($filters)->latest()->paginate(50);

        return view('general.consent.list', ['documents' => $documents]);
    }

    /**
     * Document for counselor to sign.
     * Only counselor's can view and is auth'd
     * 
     * @param string $documentID
     * @return view 
     */
    public function documentCounselor($documentID) 
    {
        $document = ConsentForm::where('document_id', $documentID)->first();
        
        return view('general.consent.detail', ['document' => $document]);
    }

    /**
     * Grabs details of a document if it exists, otherwise, it returns null or 404?
     * 
     * @param int $clientID
     * @return response
     */
    public function documentDetails($clientID) 
    {
        $document = ConsentForm::where('client_id', $clientID)
            ->where('current', true)
            ->latest() // get latest record in case there are multiple
            ->first();

        return response()->json($document);
    }

    /**
     * Grabs signature details for public consent view
     * 
     * @param int $document
     * @return response
     */
    public function pubSignatureDetails($document)
    {
        $types = ConsentSignature::select('type')
            ->where('consent_form_id', $document) 
            ->get();
            
        return response()->json($types);
    }

    /**
     * Generates new document for client
     * 
     * @note: should this be in it's down service class method? ie ClientRepository? 
     * @param Request $request
     * @return response
     */
    public function findDocOrStore(Request $request) 
    {
        // ajax request
        // if doc exists, grab document, or acknowledge it's existance
        // if not, generate unique uuid to track document 
            // send over client details 
            // create store document
            // current === true -> means client has not been discharged

        // check if client exists. if not, throw 404
        $client = Client::findOrFail($request->get('client_id'));

        $document = $this->findDocument($request->get('client_id')); 

        if ( !$document ) {
            $uuid = Uuid::uuid4()->toString();

            $consent = new ConsentForm;
            $consent->document_id = $uuid;
            $consent->client_id = $request->get('client_id');
            $consent->expiry = Carbon::now()->addDays(5);
            $consent->save();

            $this->notification->notify($client, $consent->document_id);

            return response()->json('consent form created', 201);
        }

        $this->notification->notify($client, $document->document_id);
        // reset expiry
        $document->expiry = Carbon::now()->addDays(5);
        $document->save();
        return response()->json($document);
    }

    /**
     * Stores a signature from consent form document.
     * 
     * @param Request $request
     * @param int $documentID
     * @return response
     */
    public function sign(Request $request, $documentID)
    {
        // completed signature validation
        $counselorSignature = $request->get('type') == self::COUNSELOR_TYPE;

        if ($counselorSignature) {
            $document = ConsentForm::findOrFail($documentID);
            if ($this->checkIfComplete($document)) {
                $document->completed = true;
                $document->expiry = Carbon::now(); // just make sure link is expired
                $document->save();
            } else {
                return response()->json('Could not complete form. Client and guardian both must sign', 400);
            }
        }

        $signature = new ConsentSignature;

        $signature->consent_form_id = $documentID;
        $signature->type = $request->get('type');
        $signature->signature = $request->get('signature');
        $signature->created_at = Carbon::parse($request->get('created_at'));
        $signature->save();
        
        return response()->json('consent form signature created', 201);
    }

    public function retrieveSignature($documentID, $type)
    {
        return ConsentSignature::where('consent_form_id', $documentID)
            ->where('type', $type)
            ->first();
    }

    /**
     * Removes a signature from a consent form.
     * 
     * @param int $id the id of signature (ie from CounselorSignatureComponent.vue state).
     * @return response
     */
    public function removeSignature($id)
    {
        $signature = ConsentSignature::findOrFail($id);
        
        // set form as incomplete status, then delete
        $signature->document->completed = false;
        $signature->document->save();

        if(!is_null($signature)){
            $signature->delete();
        }

        return response()->json('Signature has been removed from consent form');
    }

    public function pdf($id)
    {
        $doc = ConsentForm::findOrFail($id);

        $clientSignature = ConsentSignature::where('consent_form_id', $doc->id)
            ->where('type', 'client')
            ->first();

        $guardianSignature = ConsentSignature::where('consent_form_id', $doc->id)
            ->where('type', 'guardian')
            ->first();

        $counselorSignature = ConsentSignature::where('consent_form_id', $doc->id)
            ->where('type', 'counselor')
            ->first();

        $pdf = PDF::loadView('general.consent.pdf', [
            'doc' => $doc,
            'client_signature' => $clientSignature,
            'guardian_signature' => $guardianSignature,
            'counselor_signature' => $counselorSignature
        ]);

        $name = 'consent_form_' . $doc->document_id . '.pdf';

        return $pdf->download($name);
    }

    private function findDocument($clientID) 
    {
        return ConsentForm::where('client_id', $clientID)
            ->where('completed', false)
            ->where('current', true)
            ->first(); 
    }

    public function checkIfComplete($doc)
    {
        $completedTypes = collect($doc->signatures)->map(function($signature) {
            return $signature->type;
        });

        if ($completedTypes->contains('guardian') && $completedTypes->contains('client')) {
            return true;
        }

        return false;
    }
}

As for the routes, none of them are in a route group.

# Web.php

Route::get('/consent/form/{document}', [ConsentFormController::class, 'document'])
    ->name('client.consent.form');

Route::get('/consent/form/{document}/additional', [ConsentFormController::class, 'documentOptionalInfo'])
    ->name('client.consent.additional');

Route::post('/consent/form/{document}/additional/submit', [ConsentFormController::class, 'storeAdditionalInfo'])
    ->name('client.consent.additional.submit');

// External signature
Route::post('/consent/sign/{document}', [ConsentFormController::class, 'sign'])
    ->name('consent.sign');

// Signature type checks
Route::get('', [ConsentFormController::class, 'pubSignatureDetails'])
    ->name('consent.public.signature.types');

As for permissions, we do not have any setup. We just have 4 roles that we have setup which are admin,mdf, limited and superuser all using the guard_name: web. Also for the general.clients.consent blade template is a simple blade page with 2 Vue 3 signature components that the client has to sign.

Snapey's avatar

check the layout for this file general.clients.consent, you may have something in the header which is expecting the user to have a certain role?

prod902's avatar

This is the layout file that its using below. Would it be the csrf_token() that is causing trouble?

<!doctype html>
<html lang="{{ app()->getLocale() }}">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-token" content="{{ csrf_token() }}">

        <title>Test</title>

        <link href="{{ asset('css/external.css') }}" rel="stylesheet" type="text/css" >


        <!-- Fonts -->
        <link href="https://fonts.googleapis.com/css?family=Raleway:100,600" rel="stylesheet" type="text/css">

    </head>
    <body>
 
    <div class="container-fluid" id="prodigy-app">
         @yield('content')
    </div>

	<script src="{{ asset('js/app.js') }}"></script>
    </body>
</html>

general.clients.consent

@extends('layouts.external')

@section('content')

	@if(Session::has('completeError'))
		<p class="alert {{ Session::get('alert-class', 'alert-danger') }}">{{ Session::get('completeError') }}</p>
	@endif

	<div class="row">
		<img src="{{ asset('img/logo.png') }}"  />
	</div>

	<div class="ext-doc-content">
		<h1 class="ext-doc-title">....</h1>

			....


		<div class="row ext-doc-section">
			<div class="col-md-12">
				<h2 class="ext-section-title">...</h2>
				<p class="ext-section-text">
					....
				</p>
			</div>
		</div>

		<div class="row ext-doc-section">
			<div class="col-md-12">
				<h2 class="ext-section-title">....:</h2>
				<p class="ext-section-text">
				  ....
				</p>
			</div>
		</div>

		<div class="row ext-doc-section">
			<div class="col-md-12">
				<h2 class="ext-section-title">For Additional Information:</h2>
				<p class="ext-section-text">
					....
				</p>
				<p class="ext-section-text">
	               ....
				</p>
			</div>
		</div>
	</div>

	<consent-signature type="client" :document="{{ $document->id }}" :sigwidth="'275px'" :sigheight="'200px'"></consent-signature>
	<consent-signature type="guardian" :document="{{ $document->id }}" :sigwidth="'275px'" :sigheight="'200px'"></consent-signature>

	<a href="{{ route('client.consent.additional', $document->document_id) }}" class="btn btn-primary btn-success" style="margin: 50px 0;" :class="{ disabled: !consentContinue }">Complete Form</a>

@endsection
prod902's avatar

@Snapey I also edited the answer and added in the general.clients.consent template as well. Anything with ... is just plain text.

prod902's avatar

Thanks for the help. I was able to figure out the issue. Among the hundred's of other routes there was already a route group already with a /consent prefix that was already there, so I moved out the public consent routes out the route group.

Please or to participate in this conversation.