Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
18 changes: 5 additions & 13 deletions app/Http/Middleware/AuthApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

namespace App\Http\Middleware;

use App\Libraries\OAuth\AccessTokenRepository;
use App\Libraries\SessionVerification;
use Closure;
use Illuminate\Auth\AuthenticationException;
use Laravel\Passport\ClientRepository;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\ResourceServer;
use Nyholm\Psr7\Factory\Psr17Factory;
Expand All @@ -18,12 +18,10 @@ class AuthApi
{
const REQUEST_OAUTH_TOKEN_KEY = 'oauth_token';

protected $clients;
protected $server;

public function __construct(ResourceServer $server, ClientRepository $clients)
public function __construct(ResourceServer $server)
{
$this->clients = $clients;
$this->server = $server;
}

Expand Down Expand Up @@ -68,19 +66,13 @@ private function validTokenFromRequest($psr)
{
$psrClientId = $psr->getAttribute('oauth_client_id');
$psrUserId = get_int($psr->getAttribute('oauth_user_id'));
$psrTokenId = $psr->getAttribute('oauth_access_token_id');

$client = $this->clients->findActive($psrClientId);
if ($client === null) {
throw new AuthenticationException('invalid client');
}

$token = $client->tokens()->validAt(now())->find($psrTokenId);
if ($token === null) {
// stashed by AccessTokenRepository::isAccessTokenRevoked during ResourceServer validation
$token = AccessTokenRepository::loadedToken();
if ($token === null || (string) $token->client_id !== (string) $psrClientId) {
throw new AuthenticationException('invalid token');
}

$token->setRelation('client', $client);
$token->validate();

// increment hit count for about every 10 hits
Expand Down
38 changes: 38 additions & 0 deletions app/Libraries/OAuth/AccessTokenRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

declare(strict_types=1);

namespace App\Libraries\OAuth;

use App\Models\OAuth\Token;
use Laravel\Passport\Bridge\AccessTokenRepository as BaseRepository;

class AccessTokenRepository extends BaseRepository
{
private const LOADED_TOKEN_ATTRIBUTE = 'oauth_loaded_token';

public static function loadedToken(): ?Token
{
return app()->bound('request')
? request()->attributes->get(self::LOADED_TOKEN_ATTRIBUTE)
: null;
}

public function isAccessTokenRevoked($tokenId)
{
$token = Token::with('client')->find($tokenId);

if ($token === null || $token->revoked || $token->client === null || $token->client->revoked) {
return true;
}

if (app()->bound('request')) {
request()->attributes->set(self::LOADED_TOKEN_ATTRIBUTE, $token);
}

return false;
}
}
4 changes: 4 additions & 0 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use App\Hashing\OsuBcryptHasher;
use App\Libraries\MorphMap;
use App\Libraries\OAuth\AccessTokenRepository;
use App\Libraries\OsuCookieJar;
use App\Libraries\OsuMessageSelector;
use App\Libraries\RateLimiter;
Expand All @@ -19,6 +20,7 @@
use Laravel\Octane\Contracts\DispatchesTasks;
use Laravel\Octane\SequentialTaskDispatcher;
use Laravel\Octane\Swoole\SwooleTaskDispatcher;
use Laravel\Passport\Bridge\AccessTokenRepository as PassportAccessTokenRepository;
use Queue;
use Swoole\Http\Server;

Expand Down Expand Up @@ -125,6 +127,8 @@ public function register()
fn ($app) => $app->bound(Server::class) ? new SwooleTaskDispatcher() : new SequentialTaskDispatcher()
);

$this->app->bind(PassportAccessTokenRepository::class, AccessTokenRepository::class);

$env = $this->app->environment();
if ($env === 'testing' || $env === 'dusk.local') {
// This is needed for testing with Dusk.
Expand Down
95 changes: 95 additions & 0 deletions tests/Libraries/OAuth/AccessTokenRepositoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

declare(strict_types=1);

namespace Tests\Libraries\OAuth;

use App\Libraries\OAuth\AccessTokenRepository;
use App\Models\OAuth\Client;
use App\Models\OAuth\Token;
use App\Models\User;
use Laravel\Passport\Bridge\AccessTokenRepository as PassportAccessTokenRepository;
use Tests\TestCase;

class AccessTokenRepositoryTest extends TestCase
{
protected AccessTokenRepository $repository;
protected Client $client;
protected User $user;

public function testValidTokenIsExposed(): void
{
$token = $this->makeToken();

$this->assertFalse($this->repository->isAccessTokenRevoked($token->getKey()));

$loaded = AccessTokenRepository::loadedToken();
$this->assertNotNull($loaded);
$this->assertSame($token->getKey(), $loaded->getKey());
$this->assertTrue($loaded->relationLoaded('client'));
}

public function testClientCredentialsTokenIsExposed(): void
{
$token = $this->makeToken(['user_id' => null, 'scopes' => ['public']]);

$this->assertFalse($this->repository->isAccessTokenRevoked($token->getKey()));

$loaded = AccessTokenRepository::loadedToken();
$this->assertNotNull($loaded);
$this->assertTrue($loaded->isClientCredentials());
}

public function testRevokedTokenIsConsideredRevoked(): void
{
$token = $this->makeToken(['revoked' => true]);

$this->assertTrue($this->repository->isAccessTokenRevoked($token->getKey()));
$this->assertNull(AccessTokenRepository::loadedToken());
}

public function testNonExistentTokenIsConsideredRevoked(): void
{
$this->assertTrue($this->repository->isAccessTokenRevoked('does-not-exist'));
$this->assertNull(AccessTokenRepository::loadedToken());
}

public function testRevokedClientMakesTokenRevoked(): void
{
$token = $this->makeToken();
$this->client->update(['revoked' => true]);

$this->assertTrue($this->repository->isAccessTokenRevoked($token->getKey()));
$this->assertNull(AccessTokenRepository::loadedToken());
}

public function testMiddlewareAcceptsValidToken(): void
{
$token = $this->makeToken();
$this->actingWithToken($token)->get(route('api.me'))->assertSuccessful();
}

protected function setUp(): void
{
parent::setUp();

$this->repository = app(PassportAccessTokenRepository::class);
$this->user = User::factory()->create();
$this->client = Client::factory()->create(['user_id' => $this->user]);
}

protected function makeToken(array $overrides = []): Token
{
return $this->client->tokens()->create(array_merge([
'expires_at' => now()->addDay(),
'id' => uniqid(),
'revoked' => false,
'scopes' => ['*'],
'user_id' => $this->user->getKey(),
'verified' => true,
], $overrides));
}
}