Skip to content

Commit

Permalink
Merge pull request #122 from buggregator/feature/oauth
Browse files Browse the repository at this point in the history
Add support for Auth0 user authentication
  • Loading branch information
butschster committed Apr 8, 2024
2 parents 6aa0365 + 2237d2d commit 8596a0a
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 8596a0a

Please sign in to comment.