Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Add guard assignment helper middleware #362

Merged
merged 5 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
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\HtpController\Example::class)->name('example');
evansims marked this conversation as resolved.
Show resolved Hide resolved
// 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this assertion done above? On L43.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies if this is intentional.

Copy link
Member Author

@evansims evansims Apr 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Widcket 👋 No worries! It's repeated a few times to ensure the guard changes are properly persisted between requests. Here's the breakdown of what it's doing:

// Ensure the active guard is the default/unspecified one, 'web' ($defaultGuardClass' value.)
$this->get($routeUnspecifiedGuard)
     ->assertStatus(Response::HTTP_OK)
     ->assertSee($defaultGuardClass);

// Attempt to change the active guard to one that doesn't exist; throws an error.
$this->get($routeNonexistentGuard)
     ->assertStatus(Response::HTTP_INTERNAL_SERVER_ERROR);

// Change the guard to 'testGuard' ($sdkGuardClass' value.)
$this->get($routeMiddlewareAssignedGuard)
     ->assertStatus(Response::HTTP_OK)
     ->assertSee($sdkGuardClass);

// Ensure the active guard was correctly persisted as `testGuard` following the previous request.
$this->get($routeUnspecifiedGuard)
     ->assertStatus(Response::HTTP_OK)
     ->assertSee($sdkGuardClass);

// Clear the active guard we specified, resetting it to the app's default, `web`.
$this->get($routeMiddlewareUnassignedGuard)
     ->assertStatus(Response::HTTP_OK)
     ->assertSee($defaultGuardClass);

// Ensure the active guard was correctly persisted as `web` following the previous request.
$this->get($routeUnspecifiedGuard)
     ->assertStatus(Response::HTTP_OK)
     ->assertSee($defaultGuardClass);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification!

});

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