Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
1.56% covered (danger)
1.56%
1 / 64
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
OAuthMiddleware
1.56% covered (danger)
1.56%
1 / 64
14.29% covered (danger)
14.29%
1 / 7
1008.75
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 __invoke
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
156
 handleLogin
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
 handleLogout
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 handleRefreshToken
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getAuthUrl
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 withRedirect
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace BO\Slim\Middleware;
4
5use Psr\Http\Message\ServerRequestInterface;
6use Psr\Http\Message\ResponseInterface;
7use Psr\Http\Server\RequestHandlerInterface;
8use BO\Slim\Factory\ResponseFactory;
9use BO\Slim\Middleware\OAuth\KeycloakInstance;
10use BO\Zmsclient\Auth;
11use Slim\Psr7\Factory\StreamFactory;
12
13/**
14 * @SuppressWarnings(PHPMD)
15 */
16
17class OAuthMiddleware
18{
19    /**
20     * List of authentification types to init specific instance
21     *
22     * @var array<string, class-string<KeycloakInstance>>
23     */
24    public static array $authInstances = [
25        'keycloak' => '\BO\Slim\Middleware\OAuth\KeycloakInstance'
26    ];
27
28    /**
29     * List of request pathes with assigned handler in oidc instance
30     *
31     * @var array<string, string>
32     */
33    protected array $handlerList = [
34        'login' => 'handleLogin',
35        'logout' => 'handleLogout',
36        'refresh' => 'handleRefreshToken'
37    ];
38
39    protected string $authentificationHandler = '';
40
41    public function __construct(string $handler = 'login')
42    {
43        $this->authentificationHandler = isset($this->handlerList[$handler]) ? $handler : 'login';
44    }
45
46    /**
47     * Set the authorizsationType attribute to request and init authorization method
48     *
49     * @param ServerRequestInterface $request PSR7 request
50     * @param RequestHandlerInterface $next Next middleware
51     *
52     * @return ResponseInterface
53     */
54    public function __invoke(
55        ServerRequestInterface $request,
56        RequestHandlerInterface $next
57    ): ResponseInterface {
58        $response = (new ResponseFactory())->createResponse(200, '');
59        $request = $request->withAttribute('authentificationHandler', $this->authentificationHandler);
60        $queryParams = $request->getQueryParams();
61        $providerFromQuery = $queryParams['provider'] ?? null;
62        $oidcProviderName = is_string($providerFromQuery) && $providerFromQuery !== ''
63            ? $providerFromQuery
64            : Auth::getOidcProvider();
65
66        if (
67            is_string($oidcProviderName)
68            && $oidcProviderName !== ''
69            && isset(static::$authInstances[$oidcProviderName])
70        ) {
71            $oidcInstance = static::$authInstances[$oidcProviderName];
72            /** @psalm-suppress UnsafeInstantiation */
73            $instance = new $oidcInstance();
74            $response = match ($this->authentificationHandler) {
75                'logout' => $this->handleLogout($request, $response, $instance),
76                'refresh' => $this->handleRefreshToken($request, $response, $instance),
77                default => $this->handleLogin($request, $response, $instance, $next),
78            };
79        } else {
80            \App::$log->error('Unknown OIDC provider requested', [
81                'event' => 'oauth_unknown_provider',
82                'provider' => is_string($oidcProviderName) && $oidcProviderName !== '' ? $oidcProviderName : 'none',
83                'available_providers' => array_keys(static::$authInstances),
84                'handler' => $this->authentificationHandler,
85                'timestamp' => date('c'),
86                'request_uri' => $request->getUri()->getPath(),
87                'session_id' => session_id()
88            ]);
89            $stream = (new StreamFactory())->createStream();
90            $payload = json_encode(['error' => 'Unknown OIDC provider']);
91            $stream->write($payload !== false ? $payload : '{"error":"Unknown OIDC provider"}');
92            return $response->withStatus(400)->withHeader('Content-Type', 'application/json')
93                ->withBody($stream);
94        }
95        return $response;
96    }
97
98    private function handleLogin(
99        ServerRequestInterface $request,
100        ResponseInterface $response,
101        KeycloakInstance $instance,
102        RequestHandlerInterface $next
103    ): ResponseInterface {
104        $query = $request->getQueryParams();
105        $code = $query['code'] ?? null;
106        $state = $query['state'] ?? null;
107        $authKey = Auth::getKey();
108        if (($code === null || $code === '') && ($authKey === null || $authKey === '')) {
109            return $this->withRedirect($response, $this->getAuthUrl($request, $instance), 301);
110        } elseif ($state !== $authKey) {
111            Auth::removeKey();
112            Auth::removeOidcProvider();
113            return $this->withRedirect($response, $this->getAuthUrl($request, $instance), 301);
114        }
115        if ('login' == $request->getAttribute('authentificationHandler')) {
116            $instance->doLogin($request);
117            $response = $next->handle($request);
118            return $response;
119        }
120        return $response;
121    }
122
123    private function handleLogout(
124        ServerRequestInterface $request,
125        ResponseInterface $response,
126        KeycloakInstance $instance
127    ): ResponseInterface {
128        $state = $request->getQueryParams()['state'] ?? null;
129        if (
130            'logout' == $request->getAttribute('authentificationHandler') &&
131            ($state === null || $state === '')
132        ) {
133            return $instance->doLogout($response);
134        }
135        return $response;
136    }
137
138    private function handleRefreshToken(
139        ServerRequestInterface $request,
140        ResponseInterface $response,
141        KeycloakInstance $instance
142    ): ResponseInterface {
143        if (
144            'refresh' == $request->getAttribute('authentificationHandler') &&
145            ! $instance->writeNewAccessTokenIfExpired()
146        ) {
147            return $instance->doLogout($response);
148        }
149        return $response;
150    }
151
152    private function getAuthUrl(ServerRequestInterface $request, KeycloakInstance $instance): string
153    {
154        $authUrl = $instance->getProvider()->getAuthorizationUrl();
155        $provider = $request->getQueryParams()['provider'] ?? null;
156        if (is_string($provider) && $provider !== '') {
157            Auth::setOidcProvider($provider);
158        }
159        Auth::setKey($instance->getProvider()->getState(), time() + \App::SESSION_DURATION);
160        return $authUrl;
161    }
162
163    private function withRedirect(ResponseInterface $response, string $url, int $status): ResponseInterface
164    {
165        return $response->withHeader('Location', $url)->withStatus($status);
166    }
167}