1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace App\Http\Middleware;
7
8use App\Models\OAuth\Token;
9use Closure;
10use Illuminate\Auth\AuthenticationException;
11use Illuminate\Http\Request;
12use Laravel\Passport\Exceptions\MissingScopeException;
13
14class RequireScopes
15{
16 const NO_TOKEN_REQUIRED = [
17 'api/v2/changelog/',
18 'api/v2/comments/',
19 'api/v2/news/',
20 'api/v2/seasonal-backgrounds/',
21 'api/v2/wiki/',
22 ];
23
24 // TODO: this should be definable per-controller or action.
25 public static function noTokenRequired($request)
26 {
27 $path = "{$request->decodedPath()}/";
28
29 return $request->isMethod('GET') && starts_with($path, static::NO_TOKEN_REQUIRED);
30 }
31
32 public function handle($request, Closure $next, ...$scopes)
33 {
34 if (!is_api_request() || static::noTokenRequired($request)) {
35 return $next($request);
36 }
37
38 $this->validateScopes(oauth_token(), $scopes);
39
40 return $next($request);
41 }
42
43 protected function validateScopes(?Token $token, $scopes)
44 {
45 if ($token === null) {
46 throw new AuthenticationException();
47 }
48
49 if (!$this->requestHasScopedMiddleware(request())) {
50 if (!$token->can('*')) {
51 throw new MissingScopeException();
52 }
53 } else {
54 foreach ($scopes as $scope) {
55 if ($scope !== 'any' && !$token->can($scope)) {
56 throw new MissingScopeException([$scope], 'A required scope is missing.');
57 }
58 }
59 }
60 }
61
62 /**
63 * Returns if the request contains this middleware with scope parameter checks.
64 *
65 * @param Request $request
66 *
67 * @return bool
68 */
69 private function requestHasScopedMiddleware(Request $request): bool
70 {
71 $value = $request->attributes->get('requestHasScopedMiddleware');
72 if ($value === null) {
73 $value = $this->containsScoped($request);
74 $request->attributes->set('requestHasScopedMiddleware', $value);
75 }
76
77 return $value;
78 }
79
80 private function containsScoped(Request $request)
81 {
82 foreach ($request->route()->gatherMiddleware() as $middleware) {
83 if (is_string($middleware) && starts_with($middleware, 'require-scopes:')) {
84 return true;
85 }
86 }
87
88 return false;
89 }
90}