Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions app/Http/Middleware/BasicAuth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class BasicAuth
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$username = 'ohdear';
$password = 'best-monitoring-service-ever';

if (! hash_equals($username, $request->getUser() ?? '') || ! hash_equals($password, $request->getPassword() ?? '')) {
return response('Unauthorized', 401, [
'WWW-Authenticate' => 'Basic realm="Protected Area"',
]);
}

return $next($request);
}
}
59 changes: 59 additions & 0 deletions resources/views/examples/basic-auth.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@use('Illuminate\Support\Str')
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>Protected Page - Basic Auth Example</title>

@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-white dark:bg-gray-1000 text-gray-900 dark:text-gray-100 min-h-screen">
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="text-center mb-12">
<div class="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-6">
<svg class="w-8 h-8 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
</svg>
</div>
<h1 class="text-3xl sm:text-4xl font-semibold text-gray-900 dark:text-white mb-4">Authentication Successful</h1>
<p class="text-lg text-gray-600 dark:text-gray-400">
You have successfully authenticated using HTTP Basic Authentication.
</p>
</div>

<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-8 shadow-sm">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Request Details</h2>

<div class="space-y-4">
<div class="flex items-center justify-between py-3 border-b border-gray-100 dark:border-gray-800">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Authenticated User</span>
<span class="text-sm font-mono text-gray-900 dark:text-white">{{ request()->getUser() }}</span>
</div>
<div class="flex items-center justify-between py-3 border-b border-gray-100 dark:border-gray-800">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Authentication Method</span>
<span class="text-sm font-mono text-gray-900 dark:text-white">HTTP Basic</span>
</div>
<div class="flex items-center justify-between py-3 border-b border-gray-100 dark:border-gray-800">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Your IP Address</span>
<span class="text-sm font-mono text-gray-900 dark:text-white">{{ request()->ip() }}</span>
</div>
<div class="flex items-center justify-between py-3">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">User Agent</span>
<span class="text-sm font-mono text-gray-900 dark:text-white truncate max-w-xs" title="{{ request()->userAgent() }}">{{ Str::limit(request()->userAgent(), 50) }}</span>
</div>
</div>
</div>

<div class="mt-8 text-center">
<a href="{{ url('/') }}" class="inline-flex items-center text-primary hover:text-primary-light transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Back to homepage
</a>
</div>
</div>
</body>
</html>
25 changes: 25 additions & 0 deletions resources/views/home.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,14 @@
<div class="route-link"><a href="/form">/form</a></div>
<div class="route-description">Test form validation and browser automation</div>
</div>
<div class="py-2">
<div class="route-link"><a href="/basic-auth">/basic-auth</a></div>
<div class="route-description">HTTP Basic Auth protected page</div>
<ul class="mt-1 text-xs text-gray-500 dark:text-gray-400 list-['-'] list-inside">
<li>user: <code class="bg-gray-100 dark:bg-gray-800 px-1 rounded">ohdear</code></li>
<li>pass: <code class="bg-gray-100 dark:bg-gray-800 px-1 rounded">best-monitoring-service-ever</code></li>
</ul>
</div>
</div>
</div>

Expand Down Expand Up @@ -637,6 +645,23 @@
</div>
</div>
</div>

<!-- Basic Auth -->
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-8 shadow-sm">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Test HTTP Basic Authentication</h3>
<div class="bg-gray-900 dark:bg-gray-800 rounded-xl p-6 border border-gray-800">
<div class="flex items-center justify-between mb-4">
<div class="text-xs text-gray-400 font-mono">Command</div>
</div>
<pre class="text-green-400 font-mono text-sm mb-6"><code># Access a page protected by HTTP Basic Auth
curl -u ohdear:best-monitoring-service-ever {{ url('/basic-auth') }}</code></pre>
<div class="border-t border-gray-700 pt-4">
<div class="text-xs text-gray-400 font-mono mb-2">Response</div>
<pre class="text-xs text-gray-300 overflow-x-auto"><code># Returns HTML page confirming successful authentication
# Without credentials, returns 401 Unauthorized</code></pre>
</div>
</div>
</div>
</div>
</section>

Expand Down
5 changes: 5 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use App\Http\Controllers\FormController;
use App\Http\Middleware\BasicAuth;
use App\Http\Middleware\NoIndexHeader;
use Illuminate\Support\Facades\Route;

Expand Down Expand Up @@ -45,6 +46,10 @@
Route::view('/contact', 'examples.contact')->name('examples.contact');
});

Route::view('/basic-auth', 'examples.basic-auth')
->middleware([NoIndexHeader::class, BasicAuth::class])
->name('basic-auth');

Route::get('/sitemap.xml', function () {
$baseUrl = config('app.url');

Expand Down
34 changes: 34 additions & 0 deletions tests/Feature/BasicAuthTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

it('returns 401 without credentials', function () {
$response = $this->get('/basic-auth');

$response->assertUnauthorized()
->assertHeader('WWW-Authenticate', 'Basic realm="Protected Area"');
});

it('returns 401 with wrong credentials', function () {
$response = $this->withHeaders([
'Authorization' => 'Basic ' . base64_encode('wrong:credentials'),
])->get('/basic-auth');

$response->assertUnauthorized();
});

it('returns 200 with correct credentials', function () {
$response = $this->withHeaders([
'Authorization' => 'Basic ' . base64_encode('ohdear:best-monitoring-service-ever'),
])->get('/basic-auth');

$response->assertOk()
->assertViewIs('examples.basic-auth')
->assertSee('Authentication Successful');
});

it('returns noindex header', function () {
$response = $this->withHeaders([
'Authorization' => 'Basic ' . base64_encode('ohdear:best-monitoring-service-ever'),
])->get('/basic-auth');

$response->assertHeader('X-Robots-Tag', 'noindex, nofollow');
});
Loading