Skip to content

Commit

Permalink
feature: Add guard assignment helper middleware (#362)
Browse files Browse the repository at this point in the history
  • Loading branch information
evansims authored Apr 11, 2023
1 parent 844b6c9 commit 4f7a770
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 29 deletions.
88 changes: 70 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ auth0 apis create \

You will receive a response with details about your new API.

Please make a note of your new API's **identifier**. This will be required in the configuration step, and will be referred to as the `audience`.
Please make a note of your new API's **identifier**. This will be required in the configuration step and will be referred to as the `audience`.

### Configuring the SDK

Expand Down Expand Up @@ -169,18 +169,22 @@ Find the `providers` section, and add a new provider to the `providers` array th
The SDK provides routing controllers that handle the authentication flow for your application. Add these routes where most appropriate for your configuration. `routes/web.php` is a common location for many Laravel applications.

```php
<?php

use Auth0\Laravel\Http\Controller\Stateful\{Login, Logout, Callback};

Route::get('/login', Login::class)->name('login');
Route::get('/logout', Logout::class)->name('logout');
Route::get('/callback', Callback::class)->name('callback');
```
Route::middleware('guard:someGuardName')->group(function () {
Route::get('/login', Login::class)->name('login');
Route::get('/logout', Logout::class)->name('logout');
Route::get('/callback', Callback::class)->name('callback');
});

Please ensure requests for these routes are managed by an Auth0 guard configured by your application.
// Other routes...
```

### Protecting Routes
### Routing Middleware

The SDK provides a series of routing middleware to help you secure your application's routes. Any routes you wish to protect should be wrapped in the appropriate middleware.
The SDK provides a collection of routing middleware to help you secure your application's routes. Any routes you wish to protect should be wrapped in the appropriate middleware.

#### Stateful Applications

Expand All @@ -189,10 +193,10 @@ The SDK provides a series of routing middleware to help you secure your applicat
```php
Route::get('/required', function () {
return view(/* Authenticated */);
})->middleware(['auth0.authenticate']);
})->middleware('auth0.authenticate');
```

**`auth0.authenticate.optional` allows anyone to access a route.** It will check if a user is logged in, and will make sure `Auth::user()` is available to the route if so. This is useful when you wish to display different content to logged-in users and guests.
**`auth0.authenticate.optional` allows anyone to access a route.** It will check if a user is logged in and will make sure `Auth::user()` is available to the route if so. This is useful when you wish to display different content to logged-in users and guests.

```php
Route::get('/', function () {
Expand All @@ -201,7 +205,7 @@ Route::get('/', function () {
}

return view(/* Guest */)
})->middleware(['auth0.authenticate.optional']);
})->middleware('auth0.authenticate.optional');
```

#### Stateless Services
Expand All @@ -211,39 +215,87 @@ Route::get('/', function () {
```php
Route::get('/api/private', function () {
return response()->json([
'message' => 'Hello from a private endpoint! You need to be authenticated to see this.',
'message' => 'Hello from a private endpoint! You need to be authorized to see this.',
'authorized' => Auth::check(),
'user' => Auth::check() ? json_decode(json_encode((array) Auth::user(), JSON_THROW_ON_ERROR), true) : null,
], 200, [], JSON_PRETTY_PRINT);
})->middleware(['auth0.authorize']);
})->middleware(['auth0.authorize');
```

**`auth0.authorize` can further require access tokens to have a specific scope.** If the scope is not present for the token, it will return a `403 Forbidden` response.

```php
Route::get('/api/private-scoped', function () {
return response()->json([
'message' => 'Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this.',
'message' => 'Hello from a private endpoint! You need to be authorized and have a scope of read:messages to see this.',
'authorized' => Auth::check(),
'user' => Auth::check() ? json_decode(json_encode((array) Auth::user(), JSON_THROW_ON_ERROR), true) : null,
], 200, [], JSON_PRETTY_PRINT);
})->middleware(['auth0.authorize:read:messages']);
})->middleware('auth0.authorize:read:messages');
```

**`auth0.authorize.optional` allows anyone to access a route.** It will check if a valid access token is present, and will make sure `Auth::user()` is available to the route if so. This is useful when you wish to return different responses to authenticated and unauthenticated requests.
**`auth0.authorize.optional` allows anyone to access a route.** It will check if a valid access token is present and will make sure `Auth::user()` is available to the route if so. This is useful when you wish to return different responses to authenticated and unauthenticated requests.

```php
Route::get('/api/public', function () {
return response()->json([
'message' => 'Hello from a public endpoint! You don\'t need to be authenticated to see this.',
'message' => 'Hello from a public endpoint! You don\'t need to be authorized to see this.',
'authorized' => Auth::check(),
'user' => Auth::check() ? json_decode(json_encode((array) Auth::user(), JSON_THROW_ON_ERROR), true) : null,
], 200, [], JSON_PRETTY_PRINT);
})->middleware(['auth0.authorize.optional']);
})->middleware('auth0.authorize.optional');
```

</details>

### Guard Assignment

For the SDK to function, requests must be routed through a guard configured with the `auth0.guard` driver. This can be done in several ways.

#### Default Guard

You can set the `default` guard in your `config/auth.php` file to the guard you have configured for the SDK.

```php
<?php

return [
'defaults' => [
'guard' => 'someGuardName'
],

// ...
];
```

#### Route Middleware

As a convenience, the SDK provides a `guard` middleware that can be used to assign a specific guard to handle a route group.

```php
<?php

Route::middleware('guard:someGuardName')->group(function () {
Route::get('/example', \App\HtpController\Example::class)->name('example');
// Other routes...
});
```

#### AuthManager `shouldUse()` Method

You can also use the `shouldUse()` method on the `AuthManager` to assign a specific guard.

This is the equivalent of setting the `default` guard in your `config/auth.php` file, but can be done at runtime.

```php
<?php

auth()->shouldUse('someGuardName');

Route::get('/example', \App\HttpController\Example::class)->name('example');
// Other routes..
```

## Support Policy

Our support lifecycle mirrors the [Laravel release support](https://laravel.com/docs/releases#support-policy) and [PHP release support](https://www.php.net/supported-versions.php) schedules.
Expand Down
5 changes: 3 additions & 2 deletions src/Auth/Guard.php
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,9 @@ public function setUser(
$this->pushState($credential);
}

/**
* @codeCoverageIgnore
*/
public function user(): ?Authenticatable
{
$legacyBehavior = config('auth0.behavior.legacyGuardUserMethod', true);
Expand All @@ -568,7 +571,6 @@ public function user(): ?Authenticatable
return $currentUser;
}

// @codeCoverageIgnoreStart
if (true === $legacyBehavior) {
$token = $this->find(self::SOURCE_TOKEN);

Expand All @@ -586,7 +588,6 @@ public function user(): ?Authenticatable
return $this->getCredential()?->getUser();
}
}
// @codeCoverageIgnoreFalse

return null;
}
Expand Down
47 changes: 47 additions & 0 deletions src/Http/Middleware/Guard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Auth0\Laravel\Http\Middleware;

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

/**
* This helper middleware assigns a specific guard for use in a routing group.
* Note that this middleware is not required for the Auth0 Laravel SDK to function,
* but can be used to simplify configuration of multiple guards.
*/
final class Guard
{
private string $defaultGuard = '';

public function __construct()
{
$guard = config('auth.defaults.guard');

if (is_string($guard)) {
$this->defaultGuard = $guard;
}
}

public function handle(
Request $request,
Closure $next,
?string $guard = null,
): Response {
$guard = trim($guard ?? '');

if ('' === $guard) {
auth()->shouldUse($this->defaultGuard);

return $next($request);
}

auth()->shouldUse($guard);

return $next($request);
}
}
6 changes: 5 additions & 1 deletion src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Auth0\Laravel\Auth\User\{Provider, Repository};
use Auth0\Laravel\Contract\ServiceProvider as ServiceProviderContract;
use Auth0\Laravel\Http\Controller\Stateful\{Callback, Login, Logout};
use Auth0\Laravel\Http\Middleware\Guard as ShouldUseMiddleware;
use Auth0\Laravel\Http\Middleware\Stateful\{Authenticate, AuthenticateOptional};
use Auth0\Laravel\Http\Middleware\Stateless\{Authorize, AuthorizeOptional};
use Auth0\Laravel\Store\LaravelSession;
Expand All @@ -32,6 +33,7 @@ public function boot(
$router->aliasMiddleware('auth0.authenticate', Authenticate::class);
$router->aliasMiddleware('auth0.authorize.optional', AuthorizeOptional::class);
$router->aliasMiddleware('auth0.authorize', Authorize::class);
$router->aliasMiddleware('guard', ShouldUseMiddleware::class);

return $this;
}
Expand All @@ -50,7 +52,8 @@ public function provides()
Login::class,
Logout::class,
Provider::class,
Repository::class
Repository::class,
ShouldUseMiddleware::class
];
}

Expand All @@ -66,6 +69,7 @@ public function register(): self
$this->app->singleton(Logout::class, static fn (): Logout => new Logout());
$this->app->singleton(Provider::class, static fn (): Provider => new Provider());
$this->app->singleton(Repository::class, static fn (): Repository => new Repository());
$this->app->singleton(ShouldUseMiddleware::class, static fn (): ShouldUseMiddleware => new ShouldUseMiddleware());

$this->app->singleton('auth0', static fn (): Auth0 => app(Auth0::class));
$this->app->singleton('auth0.guard', static fn (): Guard => app(Guard::class));
Expand Down
95 changes: 95 additions & 0 deletions tests/Unit/Http/Middleware/GuardTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

use Illuminate\Http\Response;
use Illuminate\Support\Facades\Route;

uses()->group('middleware', 'middleware.guard');

beforeEach(function (): void {
$this->laravel = app('auth0');
});

it('assigns the guard for route handling', function (): void {
$routeNonexistentGuard = '/' . uniqid();
$routeMiddlewareAssignedGuard = '/' . uniqid();
$routeMiddlewareUnassignedGuard = '/' . uniqid();
$routeUnspecifiedGuard = '/' . uniqid();

$defaultGuardClass = 'Illuminate\Auth\SessionGuard';
$sdkGuardClass = 'Auth0\Laravel\Auth\Guard';

config(['auth.defaults.guard' => 'web']);

Route::get($routeUnspecifiedGuard, function (): string {
return get_class(auth()->guard());
});

Route::middleware('guard:nonexistent')->get($routeNonexistentGuard, function (): string {
return get_class(auth()->guard());
});

Route::middleware('guard:testGuard')->get($routeMiddlewareAssignedGuard, function (): string {
return get_class(auth()->guard());
});

Route::middleware('guard')->get($routeMiddlewareUnassignedGuard, function (): string {
return get_class(auth()->guard());
});

$this->get($routeUnspecifiedGuard)
->assertStatus(Response::HTTP_OK)
->assertSee($defaultGuardClass);

$this->get($routeNonexistentGuard)
->assertStatus(Response::HTTP_INTERNAL_SERVER_ERROR);

$this->get($routeMiddlewareAssignedGuard)
->assertStatus(Response::HTTP_OK)
->assertSee($sdkGuardClass);

$this->get($routeUnspecifiedGuard)
->assertStatus(Response::HTTP_OK)
->assertSee($sdkGuardClass);

$this->get($routeMiddlewareUnassignedGuard)
->assertStatus(Response::HTTP_OK)
->assertSee($defaultGuardClass);

$this->get($routeUnspecifiedGuard)
->assertStatus(Response::HTTP_OK)
->assertSee($defaultGuardClass);
});

it('assigns the guard for route group handling', function (): void {
$routeMiddlewareUnassignedGuard = '/' . uniqid();
$routeUnspecifiedGuard = '/' . uniqid();

$defaultGuardClass = 'Illuminate\Auth\SessionGuard';
$sdkGuardClass = 'Auth0\Laravel\Auth\Guard';

config(['auth.defaults.guard' => 'web']);

Route::middleware('guard:testGuard')->group(function () use ($routeUnspecifiedGuard, $routeMiddlewareUnassignedGuard) {
Route::get($routeUnspecifiedGuard, function (): string {
return get_class(auth()->guard());
});

Route::middleware('guard')->get($routeMiddlewareUnassignedGuard, function (): string {
return get_class(auth()->guard());
});
});

$this->get($routeUnspecifiedGuard)
->assertStatus(Response::HTTP_OK)
->assertSee($sdkGuardClass);

$this->get($routeMiddlewareUnassignedGuard)
->assertStatus(Response::HTTP_OK)
->assertSee($defaultGuardClass);

$this->get($routeUnspecifiedGuard)
->assertStatus(Response::HTTP_OK)
->assertSee($sdkGuardClass);
});
14 changes: 6 additions & 8 deletions tests/Unit/ServiceProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@
use Auth0\Laravel\Auth\User\Provider;
use Auth0\Laravel\Auth\User\Repository;
use Auth0\Laravel\Auth0;
use Auth0\Laravel\Http\Controller\Stateful\Callback;
use Auth0\Laravel\Http\Controller\Stateful\Login;
use Auth0\Laravel\Http\Controller\Stateful\Logout;
use Auth0\Laravel\Http\Middleware\Stateful\Authenticate;
use Auth0\Laravel\Http\Middleware\Stateful\AuthenticateOptional;
use Auth0\Laravel\Http\Middleware\Stateless\Authorize;
use Auth0\Laravel\Http\Middleware\Stateless\AuthorizeOptional;
use Auth0\Laravel\Http\Controller\Stateful\{Callback, Login, Logout};
use Auth0\Laravel\Http\Middleware\Stateful\{Authenticate, AuthenticateOptional};
use Auth0\Laravel\Http\Middleware\Stateless\{Authorize, AuthorizeOptional};
use Auth0\Laravel\Http\Middleware\Guard as ShouldUseMiddleware;
use Auth0\Laravel\Store\LaravelSession;

uses()->group('service-provider');
Expand All @@ -33,7 +30,8 @@
Login::class,
Logout::class,
Provider::class,
Repository::class
Repository::class,
ShouldUseMiddleware::class
]);
});

Expand Down

0 comments on commit 4f7a770

Please sign in to comment.