Failed Conditions
Push — master ( 533be3...555777 )
by Sébastien
02:47
created

ApiTokenLoginHandler::getLanguage()   A

Complexity

Conditions 5
Paths 11

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 14
nc 11
nop 1
dl 0
loc 27
ccs 0
cts 21
cp 0
crap 30
rs 9.4888
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace App\Handler;
6
7
use App\Entity\AccessLog;
8
use App\Infra\Log\AccessLogger;
9
use App\Security\ContredanseProductAccess;
10
use App\Security\Exception\NoProductAccessException;
11
use App\Security\Exception\ProductAccessExpiredException;
12
use App\Security\Exception\ProductPaymentIssueException;
13
use App\Security\UserProviderInterface;
14
use App\Service\Auth\AuthManager;
15
use App\Service\Auth\Exception\AuthExceptionInterface;
16
use App\Service\Token\TokenManager;
17
use Fig\Http\Message\StatusCodeInterface;
18
use Negotiation\AcceptLanguage;
19
use Negotiation\LanguageNegotiator;
20
use Psr\Http\Message\ResponseInterface;
21
use Psr\Http\Message\ServerRequestInterface;
22
use Psr\Http\Server\RequestHandlerInterface;
23
use Zend\Diactoros\Response\JsonResponse;
24
25
class ApiTokenLoginHandler implements RequestHandlerInterface
26
{
27
    /**
28
     * @var UserProviderInterface
29
     */
30
    private $userProvider;
31
32
    /**
33
     * @var TokenManager
34
     */
35
    private $tokenManager;
36
37
    /**
38
     * @var array<string, mixed>
39
     */
40
    private $authParams;
41
42
    /*
43
     * @var ContredanseProductAccess
44
     */
45
    private $productAccess;
46
47
    /*
48
     * @var AccessLogger
49
     */
50
    private $accessLogger;
51
52
    /**
53
     * @param array<string, mixed> $authParams
54
     */
55
    public function __construct(UserProviderInterface $userProvider, TokenManager $tokenManager, ContredanseProductAccess $productAccess, array $authParams, ?AccessLogger $accessLogger)
56
    {
57
        $this->userProvider  = $userProvider;
58
        $this->tokenManager  = $tokenManager;
59
        $this->authParams    = $authParams;
60
        $this->productAccess = $productAccess;
61
        $this->accessLogger  = $accessLogger;
62
    }
63
64
    public function handle(ServerRequestInterface $request): ResponseInterface
65
    {
66
        $authExpiry = $this->authParams['token_expiry'] ?? TokenManager::DEFAULT_EXPIRY;
67
68
        $method = $request->getMethod();
69
70
        if ($method !== 'POST') {
71
            throw new \RuntimeException('Unsupported http method');
72
        }
73
        // Authorization...
74
        //
75
        // Valid users are
76
        // - either admins
77
        // - or valid paying users
78
        //
79
80
        $body     = $request->getParsedBody();
81
        $email    = trim($body['email'] ?? '');
82
        $password = trim($body['password'] ?? '');
83
        $language = trim($body['language'] ?? '');
84
        if ($language === '') {
85
            $language = $this->getLanguage($request);
86
        }
87
88
        // @todo Must be removed when production
89
        if ($email === '[email protected]' && $password === 'demo') {
90
            // This is for demo only
91
            $this->logAccess($request, AccessLog::TYPE_LOGIN_SUCCESS, $email, $language);
92
93
            return $this->getResponseWithAccessToken($email, $authExpiry);
94
        }
95
96
        $authenticationManager = new AuthManager($this->userProvider);
97
98
        try {
99
            // Authenticate, wil throw exception if failed
100
            $user = $authenticationManager->getAuthenticatedUser($email, $password);
101
102
            // Ensure authorization
103
            $this->productAccess->ensureAccess(ContredanseProductAccess::PAXTON_PRODUCT, $user);
104
            $this->logAccess($request, AccessLog::TYPE_LOGIN_SUCCESS, $email, $language);
105
106
            return $this->getResponseWithAccessToken($user->getDetail('user_id'), $authExpiry);
107
        } catch (\Throwable $e) {
108
            $type = $this->getAccessLoggerTypeFromException($e);
109
            $this->logAccess($request, $type, $email, $language);
110
111
            return (new JsonResponse([
112
                'success'    => false,
113
                'reason'     => $e->getMessage(),
114
                'error_type' => $type,
115
            ]))->withStatus(StatusCodeInterface::STATUS_UNAUTHORIZED);
116
        }
117
    }
118
119
    private function getAccessLoggerTypeFromException(\Throwable $e): string
120
    {
121
        switch (true) {
122
            case $e instanceof AuthExceptionInterface:
123
                return AccessLog::TYPE_LOGIN_FAILURE_CREDENTIALS;
124
            case $e instanceof NoProductAccessException:
125
                return AccessLog::TYPE_LOGIN_FAILURE_NO_ACCESS;
126
            case $e instanceof ProductPaymentIssueException:
127
                return AccessLog::TYPE_LOGIN_FAILURE_PAYMENT_ISSUE;
128
            case $e instanceof ProductAccessExpiredException:
129
                return AccessLog::TYPE_LOGIN_FAILURE_EXPIRY;
130
            default:
131
                return AccessLog::TYPE_LOGIN_FAILURE;
132
        }
133
    }
134
135
    private function logAccess(ServerRequestInterface $request, string $type, string $email, ?string $language): void
136
    {
137
        if ($this->accessLogger !== null) {
138
            ['REMOTE_ADDR' => $ipAddress, 'HTTP_USER_AGENT' => $userAgent] = $request->getServerParams();
139
            try {
140
                $this->accessLogger->log(
141
                    $type,
142
                    $email,
143
                    $language,
144
                    $ipAddress,
145
                    $userAgent
146
                );
147
            } catch (\Throwable $e) {
148
                // Discard any error
149
                //var_dump($e->getMessage());
150
                //die();
151
                error_log('AuthLoggerMiddleware failure' . $e->getMessage());
152
            }
153
        }
154
    }
155
156
    private function getLanguage(ServerRequestInterface $request): ?string
157
    {
158
        try {
159
            $acceptLanguageHeader = trim($request->getHeaderLine('Accept-Language'));
160
161
            if (trim($acceptLanguageHeader) !== '') {
162
                $negotiator = new LanguageNegotiator();
163
                /**
164
                 * @var AcceptLanguage|null $acceptLanguage
165
                 */
166
                $acceptLanguage = $negotiator->getBest($acceptLanguageHeader, ['fr', 'en']);
167
                if ($acceptLanguage !== null) {
168
                    $parts = array_filter([
169
                        $acceptLanguage->getBasePart(), // lang
170
                        $acceptLanguage->getSubPart() // region
171
                    ]);
172
173
                    if (count($parts) > 0) {
174
                        return implode('_', $parts);
175
                    }
176
                }
177
            }
178
        } catch (\Throwable $e) {
179
            return null;
180
        }
181
182
        return null;
183
    }
184
185
    private function getResponseWithAccessToken(string $user_id, int $authExpiry): ResponseInterface
186
    {
187
        $token = $this->tokenManager->createNewToken([
188
            'user_id' => $user_id
189
            //'email'    => $email,
190
        ], $authExpiry);
191
192
        return (new JsonResponse([
193
            'access_token' => (string) $token,
194
            'token_type'   => 'api_auth',
195
            'success'      => true,
196
        ]))->withStatus(StatusCodeInterface::STATUS_OK);
197
    }
198
}
199