Enhancing API Security: Laravel Policies and Frontend Integrity Hashing for SPA and Mobile Apps

When building secure applications, balancing sensitive API protection and maintaining accessibility for public features like leaderboards is critical. This challenge becomes even more complex when supporting both single-page applications (SPA) and mobile apps, each with unique request patterns.

In this post, I’ll share how I secured a Laravel API using a combination of custom policies and a frontend integrity hashing mechanism. The approach was tailored to provide robust protection across both SPA and mobile platforms, avoiding vulnerabilities inherent in traditional methods like EnsureFrontendRequestsAreStateful.

The Problem: Securing Sensitive Endpoints for SPA and Mobile Apps

My application had an API endpoint for transactions that included sensitive data. Laravel's policies helped enforce access controls, but I needed to account for the following:

  1. Allow authenticated users to access data under specific conditions.
  2. Block unauthorized users entirely.
  3. Permit public access for specific features like leaderboards while ensuring requests originated legitimately from my frontend (SPA or mobile app).

Laravel's EnsureFrontendRequestsAreStateful middleware can be manipulated by crafting requests with the correct headers, making it unsuitable for this scenario. I needed a custom solution that could handle both SPA and mobile app requests securely.

The Laravel Policy: Adding a Security Layer

The TransactionPolicy class controls access to the transactions endpoint. Here’s how I implemented it:

class TransactionPolicy extends BasePolicy
{
    public function viewAny(?User $user): bool
    {
        $accountIdFilter = $this->filterByKey('account_id', function () use ($user) {
            return Account::findOrFail($this->request->query('filter')['account_id'])->user()->is($user);
        });

        return $accountIdFilter
            || $this->guessPermission(Transaction::class, __METHOD__)
            || request()->fromFrontendHashIsIntegral();
    }

    public function view(User $user, Transaction $transaction): bool
    {
        return false;
    }
    
    // Other methods omitted for brevity...
}

Here, the viewAny method adds an extra layer of validation:


Read also : Cyber security 101 : Basics.


  • Authenticated users are verified using a filter or guessed permissions.
  • Guests (like leaderboard users) must pass the fromFrontendHashIsIntegral check to ensure their request originates from a verified frontend.

Frontend Integrity Hash: SPA and Mobile Support

To secure requests across both SPA and mobile platforms, I introduced a frontend integrity hash. This mechanism verifies that requests come from trusted devices without relying on potentially spoofable stateful headers.

Updated Frontend Code: Generating the Integrity Hash

In the frontend, I generate a hash using the User-Agent string, which varies by device and platform, ensuring the hash is unique and hard to replicate:

import crypto from 'crypto';

export async function useBackendRequest(parameters: { path: string; options?: UseFetchOptions }) {
    const config = useRuntimeConfig();

    let headers: any = {
        accept: "application/json",
        referer: config.public.appURL,
    };

    const frontendIntegrityHash = crypto
        .createHmac('sha256', config.frontend.integritySecretKey)
        .update(String(useDevice().userAgent)) // Hash includes the User-Agent
        .digest('hex');

    headers['frontend-integrity-hash'] = frontendIntegrityHash;

    // Add other headers and make the request...
    return await useFetch(config.public.backendURL + parameters.path, { headers, ...parameters.options });
}

By including the User-Agent in the hash, each device generates a unique signature, enhancing security across SPA and mobile platforms.

Backend Validation: Checking the Integrity Hash

On the backend, I updated the hash verification method to match the frontend logic:

public function fromFrontendHashIsIntegral(): bool
{
    $correctIntegrityHash = hash_hmac(
        'sha256', 
        $this->headers->get('User-Agent'), // Use the User-Agent for hashing
        config('app.frontend.integrity_secret_key')
    );

    return $this->headers->get('FRONTEND-INTEGRITY-HASH') === $correctIntegrityHash;
}

Here’s how it works:


Read also : Debugging Laravel Sanctum SPA Authentication Issues.


  1. The frontend sends the User-Agent and the hash in the request headers.
  2. The backend regenerates the hash using the same secret key and User-Agent.
  3. If the hashes match, the request is verified as originating from the legitimate frontend.

Why This Approach?

  1. Cross-Platform Security: By incorporating the User-Agent in the hash, this approach secures both SPA and mobile app requests, ensuring platform-agnostic protection.
  2. Avoids Stateful Middleware Pitfalls: Unlike EnsureFrontendRequestsAreStateful, this method does not rely on headers that can be easily spoofed.
  3. Layered Security: Combined with Laravel policies, the hash validation adds an extra security layer, ensuring sensitive endpoints are accessible only to legitimate users or guests.

Key Considerations

  1. Device Consistency: Ensure the User-Agent string is consistently accessible and reliable across all devices and platforms.
  2. Secret Key Management: Secure the integrity secret key in environment variables and avoid exposing it in the codebase.
  3. Testing: Test extensively across SPA and mobile clients to verify that requests pass the integrity check seamlessly.

Conclusion

Securing sensitive API endpoints while supporting both SPA and mobile applications requires a robust, flexible approach. By implementing a frontend integrity hashing mechanism tied to the User-Agent, I was able to:

  • Restrict unauthorized access.
  • Enable legitimate guest access for public features.
  • Secure requests across multiple platforms.

This method offers a scalable solution to API security challenges. If you're working on a similar problem, give this approach a try and share your results. Let's continue building safer, more secure applications!