Skip to content

Commit

Permalink
feat: Introduce Auth controller extender (#7)
Browse files Browse the repository at this point in the history
* feat: Add an extender to allow for post auth completion callbacks

* Apply fixes from StyleCI

---------

Co-authored-by: StyleCI Bot <[email protected]>
  • Loading branch information
imorland and StyleCIBot authored Sep 7, 2023
1 parent ae9b322 commit ac8a28d
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 10 deletions.
71 changes: 61 additions & 10 deletions src/Controllers/AbstractOAuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ abstract class AbstractOAuthController implements RequestHandlerInterface
*/
protected $events;

protected static $afterOAuthSuccessCallbacks = [];

public function __construct(
ResponseFactory $response,
SettingsRepositoryInterface $settings,
Expand All @@ -91,6 +93,13 @@ public function handle(ServerRequestInterface $request): ResponseInterface

$session = $this->initializeSession($request, $provider);

if ((bool) $session->get('fastTrack') === true && $session->has('oauth_data')) {
$result = $this->fastTrack($session, $request);
if ($result !== null) {
return $result;
}
}

if (!$this->hasAuthorizationCode($request)) {
return $this->redirectToAuthorizationUrl($provider, $session);
}
Expand All @@ -103,6 +112,38 @@ public function handle(ServerRequestInterface $request): ResponseInterface
return $this->handleOAuthResponse($request, $token, $userResource, $session);
}

/**
* Fast Track OAuth Flow.
*
* The `fastTrack` method provides a mechanism to expedite the OAuth authentication process
* under certain conditions. Specifically, when a session indicates a `fastTrack` state and
* contains the necessary `oauth_data` (i.e., both `token` and `resourceOwner`), this method
* can be utilized to bypass the standard flow and directly handle the OAuth response.
*
* This can be particularly useful in scenarios where the initial OAuth parameters, provided
* by the authentication provider, have expired due to additional steps in the flow (e.g.,
* two-factor authentication). Instead of going through the entire OAuth flow again, the
* `fastTrack` mechanism uses the saved session data to resume and complete the process.
*
* It's essential to ensure the integrity and validity of the saved session data before
* using this method. The method will return the response of the OAuth flow if the conditions
* are met, or null otherwise.
*
* @param Store $session The current session instance containing potential OAuth data.
* @param ServerRequestInterface $request The current server request.
*
* @return ResponseInterface|null The response of the OAuth flow if fast-tracked, or null.
*/
protected function fastTrack(Store $session, ServerRequestInterface $request): ?ResponseInterface
{
$token = Arr::get($session->get('oauth_data'), 'token');
$resourceOwner = Arr::get($session->get('oauth_data'), 'resourceOwner');

if ($token instanceof AccessTokenInterface && $resourceOwner instanceof ResourceOwnerInterface) {
return $this->handleOAuthResponse($request, $token, $resourceOwner, $session);
}
}

protected function identifyProvider(): AbstractProvider
{
$redirectUri = $this->url->to('forum')->route($this->getRouteName());
Expand Down Expand Up @@ -267,25 +308,35 @@ protected function handleOAuthResponse(ServerRequestInterface $request, AccessTo
}

$response = $this->link($actor, $resourceOwner);

$this->dispatchSuccessEvent($token, $resourceOwner, $actor);

return $response;
} else {
$response = $this->response->make(
$this->getProviderName(),
$this->getIdentifier($resourceOwner),
function (Registration $registration) use ($resourceOwner, $token) {
$this->setSuggestions($registration, $resourceOwner, $token);
}
);
}

$response = $this->response->make(
$this->getProviderName(),
$this->getIdentifier($resourceOwner),
function (Registration $registration) use ($resourceOwner, $token) {
$this->setSuggestions($registration, $resourceOwner, $token);
// Execute registered callbacks
foreach (static::$afterOAuthSuccessCallbacks as $callback) {
$result = $callback($request, $token, $resourceOwner, $this->getProviderName());

if ($result !== null) {
return $result;
}
);
}

$this->dispatchSuccessEvent($token, $resourceOwner, $actor);

return $response;
}

public static function setAfterOAuthSuccessCallbacks(array $callbacks)
{
static::$afterOAuthSuccessCallbacks = array_merge(static::$afterOAuthSuccessCallbacks, $callbacks);
}

/**
* Get the display type for the OAuth process.
*
Expand Down
54 changes: 54 additions & 0 deletions src/Extend/OAuthController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

/*
* This file is part of fof/extend.
*
* Copyright (c) FriendsOfFlarum.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FoF\Extend\Extend;

use Flarum\Extend\ExtenderInterface;
use Flarum\Extension\Extension;
use Flarum\Foundation\ContainerUtil;
use FoF\Extend\Controllers\AbstractOAuthController;
use Illuminate\Contracts\Container\Container;

class OAuthController implements ExtenderInterface
{
protected $afterOAuthSuccessCallbacks = [];

/**
* Register a callback to be executed after a successful OAuth login, but before the user is logged in to Flarum.
*
* @param callable|string $callback
*
* The callback can be a closure or an invokable class and should accept:
* - $request: An instance of \Psr\Http\Message\ServerRequestInterface.
* - $token: An instance of \League\OAuth2\Client\Token\AccessTokenInterface, representing the access token.
* - $resourceOwner: An instance of \League\OAuth2\Client\Provider\ResourceOwnerInterface, representing the authenticated user's resource.
* - $identification: A string identifying `fof/oauth` provider, e.g. `github.
*
* It should return either `void` if no further action is required from the callback, or `Psr\Http\Message\ResponseInterface`.
*
* @return $this
*/
public function afterOAuthSuccess($callback)
{
$this->afterOAuthSuccessCallbacks[] = $callback;

return $this;
}

public function extend(Container $container, Extension $extension = null)
{
foreach ($this->afterOAuthSuccessCallbacks as $index => $callback) {
$this->afterOAuthSuccessCallbacks[$index] = ContainerUtil::wrapCallback($callback, $container);
}

AbstractOAuthController::setAfterOAuthSuccessCallbacks($this->afterOAuthSuccessCallbacks);
}
}

0 comments on commit ac8a28d

Please sign in to comment.