Skip to content

Commit

Permalink
Add support for Auth0 user authentication
Browse files Browse the repository at this point in the history
This commit introduces the integration of the Auth0 authentication provider into our application. Users can now authenticate using Auth0, which provides a secure and reliable means of handling user authentication.

To enable authentication you need to set ENV variables

```
AUTH_ENABLED=true
AUTH_PROVIDER_URL=https://xxx.auth0.com
AUTH_CLIENT_ID=xxx
AUTH_CLIENT_SECRET=xxx
AUTH_CALLBACK_URL=http://local.server/auth/sso/callback
AUTH_SCOPES=openid,email,profile
```
  • Loading branch information
butschster committed Apr 7, 2024
1 parent 4072c08 commit 2237d2d
Show file tree
Hide file tree
Showing 18 changed files with 601 additions and 12 deletions.
16 changes: 16 additions & 0 deletions app/src/Application/Auth/AuthSettings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace App\Application\Auth;

use Psr\Http\Message\UriInterface;

final class AuthSettings
{
public function __construct(
public readonly bool $enabled,
public readonly UriInterface $loginUrl,
) {
}
}
79 changes: 79 additions & 0 deletions app/src/Application/Auth/JWTTokenStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace App\Application\Auth;

use Spiral\Auth\TokenInterface;
use Spiral\Auth\TokenStorageInterface;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;

final class JWTTokenStorage implements TokenStorageInterface
{
/** @var callable */
private $time;

public function __construct(
private readonly string $secret,
private readonly string $algorithm = 'HS256',
private readonly string $expiresAt = '+30 days',
callable $time = null
) {
$this->time = $time ?? static function (string $offset): \DateTimeImmutable {
return new \DateTimeImmutable($offset);
};
}

public function load(string $id): ?TokenInterface
{
try {
$token = (array) JWT::decode($id, new Key($this->secret, $this->algorithm));
} catch (ExpiredException $exception) {
throw $exception;
} catch (\Throwable $exception) {
return null;
}

if (
false === isset($token['data'])
|| false === isset($token['iat'])
|| false === isset($token['exp'])
) {
return null;
}

return new Token(
$id,
$token,
(array) $token['data'],
(new \DateTimeImmutable())->setTimestamp($token['iat']),
(new \DateTimeImmutable())->setTimestamp($token['exp'])
);
}

public function create(array $payload, \DateTimeInterface $expiresAt = null): TokenInterface
{
$issuedAt = ($this->time)('now');
$expiresAt = $expiresAt ?? ($this->time)($this->expiresAt);
$token = [
'iat' => $issuedAt->getTimestamp(),
'exp' => $expiresAt->getTimestamp(),
'data' => $payload,
];

return new Token(
JWT::encode($token,$this->secret,$this->algorithm),
$token,
$payload,
$issuedAt,
$expiresAt
);
}

public function delete(TokenInterface $token): void
{
// We don't need to do anything here since JWT tokens are self-contained.
}
}
23 changes: 23 additions & 0 deletions app/src/Application/Auth/SuccessRedirect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace App\Application\Auth;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use Spiral\Http\ResponseWrapper;

final class SuccessRedirect
{
public function __construct(
private readonly ResponseWrapper $response,
private readonly UriInterface $redirectUrl,
) {
}

public function makeResponse(string $token): ResponseInterface
{
return $this->response->redirect($this->redirectUrl . '?token=' . $token);
}
}
44 changes: 44 additions & 0 deletions app/src/Application/Auth/Token.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace App\Application\Auth;

use Spiral\Auth\TokenInterface;

final class Token implements TokenInterface
{
public function __construct(
private readonly string $id,
private readonly array $token,
private readonly array $payload,
private readonly \DateTimeImmutable $issuedAt,
private readonly \DateTimeImmutable $expiresAt,
) {
}

public function getID(): string
{
return $this->id;
}

public function getToken(): array
{
return $this->token;
}

public function getPayload(): array
{
return $this->payload;
}

public function getIssuedAt(): \DateTimeImmutable
{
return $this->issuedAt;
}

public function getExpiresAt(): \DateTimeInterface
{
return $this->expiresAt;
}
}
76 changes: 76 additions & 0 deletions app/src/Application/Bootloader/AuthBootloader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace App\Application\Bootloader;

use App\Application\Auth\AuthSettings;
use App\Application\Auth\JWTTokenStorage;
use App\Application\Auth\SuccessRedirect;
use App\Application\OAuth\ActorProvider;
use App\Application\OAuth\SessionStore;
use Psr\Http\Message\UriFactoryInterface;
use Spiral\Boot\Bootloader\Bootloader;

use Auth0\SDK\Auth0;
use Auth0\SDK\Configuration\SdkConfiguration;
use Spiral\Boot\EnvironmentInterface;
use Spiral\Bootloader\Auth\HttpAuthBootloader;
use Spiral\Core\Container\Autowire;
use Spiral\Http\ResponseWrapper;
use Spiral\Session\SessionScope;

final class AuthBootloader extends Bootloader
{
public function defineSingletons(): array
{
return [
SdkConfiguration::class => static fn(EnvironmentInterface $env) => new SdkConfiguration(
strategy: $env->get('AUTH_STRATEGY', SdkConfiguration::STRATEGY_REGULAR),
domain: $env->get('AUTH_PROVIDER_URL'),
clientId: $env->get('AUTH_CLIENT_ID'),
redirectUri: $env->get('AUTH_CALLBACK_URL'),
clientSecret: $env->get('AUTH_CLIENT_SECRET'),
scope: \explode(',', $env->get('AUTH_SCOPES', 'openid,profile,email')),
cookieSecret: $env->get('AUTH_COOKIE_SECRET', $env->get('ENCRYPTER_KEY') ?? 'secret'),
),

Auth0::class => static fn(SdkConfiguration $config, SessionScope $session) => new Auth0(
$config->setTransientStorage(new SessionStore($session)),
),

AuthSettings::class => static fn(
EnvironmentInterface $env,
UriFactoryInterface $factory,
) => new AuthSettings(
enabled: $env->get('AUTH_ENABLED', false),
loginUrl: $factory->createUri('/auth/sso/login'),
),

SuccessRedirect::class => static fn(
UriFactoryInterface $factory,
ResponseWrapper $response,
) => new SuccessRedirect(
response: $response,
redirectUrl: $factory->createUri('/#/login'),
),
];
}

public function init(
HttpAuthBootloader $httpAuth,
EnvironmentInterface $env,
\Spiral\Bootloader\Auth\AuthBootloader $auth,
): void {
$auth->addActorProvider(new Autowire(ActorProvider::class));
$httpAuth->addTokenStorage(
'jwt',
new Autowire(
JWTTokenStorage::class,
[
'secret' => $env->get('AUTH_JWT_SECRET', $env->get('ENCRYPTER_KEY')),
],
),
);
}
}
48 changes: 43 additions & 5 deletions app/src/Application/Bootloader/RoutesBootloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,40 @@

namespace App\Application\Bootloader;

use App\Application\Auth\AuthSettings;
use App\Application\HTTP\Middleware\JsonPayloadMiddleware;
use App\Interfaces\Http\EventHandlerAction;
use Spiral\Auth\Middleware\AuthMiddleware;
use Spiral\Auth\Middleware\Firewall\ExceptionFirewall;
use Spiral\Bootloader\Http\RoutesBootloader as BaseRoutesBootloader;
use Spiral\Core\Container;
use Spiral\Filter\ValidationHandlerMiddleware;
use Spiral\Http\Exception\ClientException\ForbiddenException;
use Spiral\Http\Middleware\ErrorHandlerMiddleware;
use Spiral\Router\Bootloader\AnnotatedRoutesBootloader;
use Spiral\Router\GroupRegistry;
use Spiral\Router\Loader\Configurator\RoutingConfigurator;
use Spiral\OpenApi\Controller\DocumentationController;
use Spiral\Session\Middleware\SessionMiddleware;

final class RoutesBootloader extends BaseRoutesBootloader
{
protected const DEPENDENCIES = [
AnnotatedRoutesBootloader::class,
];
public function __construct(
private readonly Container $container,
) {
}

public function defineDependencies(): array
{
return [
AnnotatedRoutesBootloader::class,
];
}

protected function globalMiddleware(): array
{
return [
SessionMiddleware::class,
JsonPayloadMiddleware::class,
ErrorHandlerMiddleware::class,
ValidationHandlerMiddleware::class,
Expand All @@ -32,8 +47,21 @@ protected function globalMiddleware(): array
protected function middlewareGroups(): array
{
return [
'web' => [],
'api' => [],
'auth' => [
AuthMiddleware::class,
],
'guest' => [
'middleware:auth',
],
'api_guest' => [
'middleware:auth',
],
'web' => [
'middleware:auth',
],
'api' => [
'middleware:auth',
],
'docs' => [],
];
}
Expand All @@ -44,6 +72,16 @@ protected function middlewareGroups(): array
protected function configureRouteGroups(GroupRegistry $groups): void
{
$groups->getGroup('api')->setPrefix('api/');
$groups->getGroup('api_guest')->setPrefix('api/');

$settings = $this->container->get(AuthSettings::class);

if ($settings->enabled) {
$groups->getGroup('api')
->addMiddleware(new ExceptionFirewall(new ForbiddenException()));
$groups->getGroup('web')
->addMiddleware(new ExceptionFirewall(new ForbiddenException()));
}
}

protected function defineRoutes(RoutingConfigurator $routes): void
Expand Down
25 changes: 25 additions & 0 deletions app/src/Application/HTTP/Response/UserResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace App\Application\HTTP\Response;

use App\Application\OAuth\User;

final class UserResource extends JsonResource
{
public function __construct(
private readonly User $user,
) {
parent::__construct();
}

protected function mapData(): array
{
return [
'username' => $this->user->getUsername(),
'avatar' => $this->user->getAvatar(),
'email' => $this->user->getEmail(),
];
}
}
3 changes: 3 additions & 0 deletions app/src/Application/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Application\Bootloader\AppBootloader;
use App\Application\Bootloader\AttributesBootloader;
use App\Application\Bootloader\AuthBootloader;
use App\Application\Bootloader\HttpHandlerBootloader;
use App\Application\Bootloader\MongoDBBootloader;
use App\Application\Bootloader\PersistenceBootloader;
Expand Down Expand Up @@ -84,6 +85,7 @@ protected function defineBootloaders(): array
EventsBootloader::class,
EventBootloader::class,
CqrsBootloader::class,
Framework\Http\SessionBootloader::class,

// Console commands
Framework\CommandBootloader::class,
Expand All @@ -106,6 +108,7 @@ protected function defineBootloaders(): array
ProfilerBootloader::class,
MongoDBBootloader::class,
PersistenceBootloader::class,
AuthBootloader::class,
];
}
}
Loading

0 comments on commit 2237d2d

Please sign in to comment.