Skip to content

Commit

Permalink
Merge pull request #167 from buggregator/feature/139
Browse files Browse the repository at this point in the history
Adds Kinde auth support
  • Loading branch information
butschster committed May 1, 2024
2 parents 8a0ae69 + 9acdc14 commit c0964c8
Show file tree
Hide file tree
Showing 38 changed files with 1,382 additions and 229 deletions.
2 changes: 1 addition & 1 deletion app/src/Application/Auth/JWTTokenStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public function load(string $id): ?TokenInterface
);
}

public function create(array $payload, \DateTimeInterface $expiresAt = null): TokenInterface
public function create(array|\JsonSerializable $payload, \DateTimeInterface $expiresAt = null): TokenInterface
{
$issuedAt = ($this->time)('now');
$expiresAt = $expiresAt ?? ($this->time)($this->expiresAt);
Expand Down
33 changes: 11 additions & 22 deletions app/src/Application/Bootloader/AuthBootloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,31 @@
use App\Application\Auth\JWTTokenStorage;
use App\Application\Auth\SuccessRedirect;
use App\Application\OAuth\ActorProvider;
use App\Application\OAuth\SessionStore;
use App\Application\OAuth\AuthProviderInterface;
use App\Application\OAuth\AuthProviderRegistryInterface;
use App\Application\OAuth\AuthProviderService;
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 defineBindings(): array
public function defineSingletons(): array
{
return [
Auth0::class => static fn(SdkConfiguration $config, SessionScope $session) => new Auth0(
$config->setTransientStorage(new SessionStore($session)),
AuthProviderInterface::class => static fn(
EnvironmentInterface $env,
AuthProviderService $service,
) => $service->get(
name: $env->get('AUTH_PROVIDER', 'auth0'),
),

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'),
),
];
}
AuthProviderService::class => AuthProviderService::class,
AuthProviderRegistryInterface::class => AuthProviderService::class,

public function defineSingletons(): array
{
return [
AuthSettings::class => static fn(
EnvironmentInterface $env,
UriFactoryInterface $factory,
Expand Down
4 changes: 2 additions & 2 deletions app/src/Application/Bootloader/RoutesBootloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
namespace App\Application\Bootloader;

use App\Application\Auth\AuthSettings;
use App\Application\HTTP\Middleware\ApiAuthMiddleware;
use App\Application\HTTP\Middleware\DetectEventTypeMiddleware;
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;
Expand Down Expand Up @@ -56,7 +56,7 @@ protected function middlewareGroups(): array
{
return [
'auth' => [
AuthMiddleware::class,
ApiAuthMiddleware::class,
],
'guest' => [
'middleware:auth',
Expand Down
7 changes: 7 additions & 0 deletions app/src/Application/Exception/AuthProviderException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace App\Application\Exception;

class AuthProviderException extends \Exception {}
7 changes: 7 additions & 0 deletions app/src/Application/Exception/AuthProviderNotFound.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace App\Application\Exception;

final class AuthProviderNotFound extends AuthProviderException {}
7 changes: 7 additions & 0 deletions app/src/Application/Exception/InvalidCredentialsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace App\Application\Exception;

final class InvalidCredentialsException extends AuthProviderException {}
37 changes: 37 additions & 0 deletions app/src/Application/HTTP/Middleware/ApiAuthMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace App\Application\HTTP\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Spiral\Auth\Middleware\AuthTransportWithStorageMiddleware;
use Spiral\Core\FactoryInterface;

final class ApiAuthMiddleware implements MiddlewareInterface
{
private ?MiddlewareInterface $middleware = null;

public function __construct(
private readonly FactoryInterface $factory,
) {}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($this->middleware === null) {
$this->initMiddleware();
}

return $this->middleware->process($request, $handler);
}

private function initMiddleware(): void
{
$this->middleware = $this->factory->make(AuthTransportWithStorageMiddleware::class, [
'transportName' => 'header',
]);
}
}
10 changes: 7 additions & 3 deletions app/src/Application/HTTP/Response/UserResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,25 @@
namespace App\Application\HTTP\Response;

use App\Application\OAuth\User;
use Psr\Http\Message\UriInterface;

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

protected function mapData(): array
{
return [
'username' => $this->user->getUsername(),
'avatar' => $this->user->getAvatar(),
'email' => $this->user->getEmail(),
'provider' => $this->user->provider,
'username' => $this->user->username,
'avatar' => $this->user->avatar,
'email' => $this->user->email,
'logout' => $this->logoutUrl ? (string) $this->logoutUrl : null,
];
}
}
7 changes: 6 additions & 1 deletion app/src/Application/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use App\Application\Bootloader\HttpHandlerBootloader;
use App\Application\Bootloader\MongoDBBootloader;
use App\Application\Bootloader\PersistenceBootloader;
use App\Integration\Auth0\Auth0Bootloader;
use App\Integration\Kinde\KindeBootloader;
use Modules\Events\Application\EventsBootloader;
use Modules\Inspector\Application\InspectorBootloader;
use Modules\Metrics\Application\MetricsBootloader;
Expand Down Expand Up @@ -83,6 +85,10 @@ protected function defineBootloaders(): array
SerializerBootloader::class,
BroadcastingBootloader::class,

// Auth
Auth0Bootloader::class,
KindeBootloader::class,

// Modules
HttpHandlerBootloader::class,
AppBootloader::class,
Expand All @@ -95,7 +101,6 @@ protected function defineBootloaders(): array
ProfilerBootloader::class,
MongoDBBootloader::class,
PersistenceBootloader::class,
AuthBootloader::class,
WebhooksBootloader::class,
ProjectBootloader::class,
EventsBootloader::class,
Expand Down
19 changes: 10 additions & 9 deletions app/src/Application/OAuth/ActorProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,25 @@
use Spiral\Auth\ActorProviderInterface;
use Spiral\Auth\TokenInterface;

final class ActorProvider implements ActorProviderInterface
final readonly class ActorProvider implements ActorProviderInterface
{
public function getActor(TokenInterface $token): ?User
{
$payload = $token->getPayload();
if ($payload === []) {
$payload = self::getGuestPayload();
return self::getGuestPayload();
}

return new User($payload);
return User::fromArray($payload);
}

public static function getGuestPayload(): array
public static function getGuestPayload(): User
{
return [
'nickname' => 'guest',
'email' => '',
'picture' => '<svg xmlns="http:https://www.w3.org/2000/svg" xmlns:xlink="http:https://www.w3.org/1999/xlink" viewBox="0 0 144.8 144.8" xml:space="preserve"><circle style="fill:#f5c002" cx="72.4" cy="72.4" r="72.4"/><defs><circle id="a" cx="72.4" cy="72.4" r="72.4"/></defs><clipPath id="b"><use xlink:href="#a" style="overflow:visible"/></clipPath><g style="clip-path:url(#b)"><path style="fill:#f1c9a5" d="M107 117c-5-9-35-14-35-14s-30 5-34 14l-7 28h82s-2-17-6-28z"/><path style="fill:#e4b692" d="M72 103s30 5 35 14c4 11 6 28 6 28H72v-42z"/><path style="fill:#f1c9a5" d="M64 85h17v27H64z"/><path style="fill:#e4b692" d="M72 85h9v27h-9z"/><path style="opacity:.1;fill:#ddac8c" d="M64 97c2 4 8 7 12 7l5-1V85H64v12z"/><path style="fill:#f1c9a5" d="M93 67c0-17-9-26-21-26-11 0-21 9-21 26 0 23 10 31 21 31 12 0 21-9 21-31z"/><path style="fill:#e4b692" d="M90 79c-4 0-6-4-6-9 1-5 5-8 9-8 3 1 6 5 5 9 0 5-4 9-8 8z"/><path style="fill:#f1c9a5" d="M47 71c-1-4 2-8 5-9 4 0 8 3 8 8 1 5-1 9-5 9-4 1-8-3-8-8z"/><path style="fill:#e4b692" d="M93 67c0-17-9-26-21-26v57c12 0 21-9 21-31z"/><path style="fill:#303030" d="M91 82c-1 3-3 7-6 7-5 0-8-4-13-4s-8 4-12 4c-3 0-5-4-7-7v-6 7s1 8 4 11c3 2 11 6 15 6 5 0 13-4 15-6 4-3 5-11 5-11v-7l-1 6zM62 44s4 16 26 24l-3-8 10 7c3-7 7-16-2-21-8-6-28-15-31-2z"/><path style="fill:#303030" d="M55 66s2-18 8-22c-5-2-14 5-13 10l5 12z"/><path style="fill:#fb621e" d="M107 117c-3-5-14-9-23-12a12 12 0 0 1-23 0c-9 3-21 7-23 12l-7 28h82s-2-17-6-28z"/><path style="opacity:.2;fill:#e53d0c" d="M60 108c0 6 6 11 12 11 7 0 12-5 13-11 8 2 18 6 22 10v-1c-3-5-14-9-23-12a12 12 0 0 1-23 0c-9 3-21 7-23 12l-1 1c5-4 15-8 23-10z"/><path style="fill:#e53d0c" d="M57 106a15 15 0 0 0 30 0l-3-1a12 12 0 0 1-23 0l-4 1z"/><path style="fill:#fff" d="M76 91s-1-3-4-3c-2 0-3 3-3 3h7z"/></g></svg>',
];
return new User(
provider: null,
username: 'guest',
avatar: '<svg xmlns="http:https://www.w3.org/2000/svg" xmlns:xlink="http:https://www.w3.org/1999/xlink" viewBox="0 0 144.8 144.8" xml:space="preserve"><circle style="fill:#f5c002" cx="72.4" cy="72.4" r="72.4"/><defs><circle id="a" cx="72.4" cy="72.4" r="72.4"/></defs><clipPath id="b"><use xlink:href="#a" style="overflow:visible"/></clipPath><g style="clip-path:url(#b)"><path style="fill:#f1c9a5" d="M107 117c-5-9-35-14-35-14s-30 5-34 14l-7 28h82s-2-17-6-28z"/><path style="fill:#e4b692" d="M72 103s30 5 35 14c4 11 6 28 6 28H72v-42z"/><path style="fill:#f1c9a5" d="M64 85h17v27H64z"/><path style="fill:#e4b692" d="M72 85h9v27h-9z"/><path style="opacity:.1;fill:#ddac8c" d="M64 97c2 4 8 7 12 7l5-1V85H64v12z"/><path style="fill:#f1c9a5" d="M93 67c0-17-9-26-21-26-11 0-21 9-21 26 0 23 10 31 21 31 12 0 21-9 21-31z"/><path style="fill:#e4b692" d="M90 79c-4 0-6-4-6-9 1-5 5-8 9-8 3 1 6 5 5 9 0 5-4 9-8 8z"/><path style="fill:#f1c9a5" d="M47 71c-1-4 2-8 5-9 4 0 8 3 8 8 1 5-1 9-5 9-4 1-8-3-8-8z"/><path style="fill:#e4b692" d="M93 67c0-17-9-26-21-26v57c12 0 21-9 21-31z"/><path style="fill:#303030" d="M91 82c-1 3-3 7-6 7-5 0-8-4-13-4s-8 4-12 4c-3 0-5-4-7-7v-6 7s1 8 4 11c3 2 11 6 15 6 5 0 13-4 15-6 4-3 5-11 5-11v-7l-1 6zM62 44s4 16 26 24l-3-8 10 7c3-7 7-16-2-21-8-6-28-15-31-2z"/><path style="fill:#303030" d="M55 66s2-18 8-22c-5-2-14 5-13 10l5 12z"/><path style="fill:#fb621e" d="M107 117c-3-5-14-9-23-12a12 12 0 0 1-23 0c-9 3-21 7-23 12l-7 28h82s-2-17-6-28z"/><path style="opacity:.2;fill:#e53d0c" d="M60 108c0 6 6 11 12 11 7 0 12-5 13-11 8 2 18 6 22 10v-1c-3-5-14-9-23-12a12 12 0 0 1-23 0c-9 3-21 7-23 12l-1 1c5-4 15-8 23-10z"/><path style="fill:#e53d0c" d="M57 106a15 15 0 0 0 30 0l-3-1a12 12 0 0 1-23 0l-4 1z"/><path style="fill:#fff" d="M76 91s-1-3-4-3c-2 0-3 3-3 3h7z"/></g></svg>',
email: '',
);
}
}
23 changes: 23 additions & 0 deletions app/src/Application/OAuth/AuthProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace App\Application\OAuth;

use Psr\Http\Message\UriInterface;
use Spiral\Http\Request\InputManager;

interface AuthProviderInterface
{
public function getLoginUrl(): UriInterface;

public function isAuthenticated(): bool;

public function getUser(): ?User;

public function authenticate(InputManager $input): void;

public function getLogoutUrl(): ?UriInterface;

public function logout(): void;
}
20 changes: 20 additions & 0 deletions app/src/Application/OAuth/AuthProviderRegistryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace App\Application\OAuth;

use App\Application\Exception\AuthProviderNotFound;

interface AuthProviderRegistryInterface
{
/**
* @param class-string<AuthProviderInterface> $provider
*/
public function register(string $name, string $provider): void;

/**
* @throws AuthProviderNotFound
*/
public function get(string $name): AuthProviderInterface;
}
41 changes: 41 additions & 0 deletions app/src/Application/OAuth/AuthProviderService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace App\Application\OAuth;

use App\Application\Exception\AuthProviderException;
use App\Application\Exception\AuthProviderNotFound;
use Psr\Container\ContainerInterface;
use Spiral\Core\Attribute\Singleton;

#[Singleton]
final class AuthProviderService implements AuthProviderRegistryInterface
{
/** @var array<non-empty-string, class-string<AuthProviderInterface>> */
private array $providers = [];

public function __construct(
private readonly ContainerInterface $container,
) {}

public function register(string $name, string $provider): void
{
if (!\is_subclass_of($provider, AuthProviderInterface::class)) {
throw new AuthProviderException(
\sprintf('Provider "%s" must implement AuthProviderInterface', $provider),
);
}

$this->providers[$name] = $provider;
}

public function get(string $name): AuthProviderInterface
{
if (!isset($this->providers[$name])) {
throw new AuthProviderNotFound(\sprintf('Auth provider "%s" not found', $name));
}

return $this->container->get($this->providers[$name]);
}
}
34 changes: 21 additions & 13 deletions app/src/Application/OAuth/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,32 @@

namespace App\Application\OAuth;

final readonly class User
readonly class User implements \JsonSerializable
{
public function __construct(
private array $data,
) {}

public function getUsername(): string
public static function fromArray(array $data): self
{
return $this->data['nickname'] ?? 'guest';
return new self(
provider: $data['provider'],
username: $data['username'],
avatar: $data['avatar'],
email: $data['email'],
);
}

public function getAvatar(): string
{
return $this->data['picture'];
}
public function __construct(
public ?string $provider,
public string $username,
public string $avatar,
public string $email,
) {}

public function getEmail(): string
public function jsonSerialize(): array
{
return $this->data['email'];
return [
'provider' => $this->provider,
'username' => $this->username,
'avatar' => $this->avatar,
'email' => $this->email,
];
}
}
Loading

0 comments on commit c0964c8

Please sign in to comment.