Passed
Push — master ( 693cc1...43872d )
by Alexander
02:10
created

HttpBasic::withTokenType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Auth\Method;
6
7
use Psr\Http\Message\ResponseInterface;
8
use Psr\Http\Message\ServerRequestInterface;
9
use Yiisoft\Auth\AuthenticationMethodInterface;
10
use Yiisoft\Auth\IdentityInterface;
11
use Yiisoft\Auth\IdentityWithTokenRepositoryInterface;
12
use Yiisoft\Http\Header;
13
14
use function call_user_func;
15
use function count;
16
17
/**
18
 * HTTP Basic authentication method.
19
 *
20
 * @see https://tools.ietf.org/html/rfc7617
21
 *
22
 * In case authentication does not work as expected, make sure your web server passes username and password
23
 * to `$request->getServerParams()['PHP_AUTH_USER']` and `$request->getServerParams()['PHP_AUTH_PW']`
24
 * parameters. If you are using Apache with PHP-CGI, you might need to add this line to your `.htaccess` file:
25
 *
26
 * ```
27
 * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]
28
 * ```
29
 */
30
final class HttpBasic implements AuthenticationMethodInterface
31
{
32
    private string $realm = 'api';
33
    private ?string $tokenType = null;
34
35
    /**
36
     * @var callable|null
37
     */
38
    private $authenticationCallback;
39
40
    private IdentityWithTokenRepositoryInterface $identityRepository;
41
42 18
    public function __construct(IdentityWithTokenRepositoryInterface $identityRepository)
43
    {
44 18
        $this->identityRepository = $identityRepository;
45 18
    }
46
47 15
    public function authenticate(ServerRequestInterface $request): ?IdentityInterface
48
    {
49 15
        [$username, $password] = $this->getAuthenticationCredentials($request);
50
51 15
        if ($this->authenticationCallback !== null && ($username !== null || $password !== null)) {
52 7
            return call_user_func($this->authenticationCallback, $username, $password, $this->identityRepository);
53
        }
54
55 8
        if ($username !== null) {
56 5
            return $this->identityRepository->findIdentityByToken($username, $this->tokenType);
57
        }
58
59 3
        return null;
60
    }
61
62 2
    public function challenge(ResponseInterface $response): ResponseInterface
63
    {
64 2
        return $response->withHeader(Header::WWW_AUTHENTICATE, "Basic realm=\"{$this->realm}\"");
65
    }
66
67
    /**
68
     * @param callable $authenticationCallback A PHP callable that will authenticate the user with the HTTP basic
69
     * authentication information. The callable should have the following signature:
70
     *
71
     * ```php
72
     * static function (
73
     *     string $username,
74
     *     string $password,
75
     *     \Yiisoft\Auth\IdentityRepositoryInterface $identityRepository
76
     * ): ?\Yiisoft\Auth\IdentityInterface
77
     * ```
78
     *
79
     * It should return an identity object that matches the username and password.
80
     * Null should be returned if there is no such identity.
81
     * The callable will be called only if current user is not authenticated.
82
     *
83
     * If not set, the username information will be considered as an access token
84
     * while the password information will be ignored.
85
     * The {@see \Yiisoft\Auth\IdentityWithTokenRepositoryInterface::findIdentityByToken()}
86
     * method will be called to authenticate an identity.
87
     *
88
     * @return self
89
     */
90 8
    public function withAuthenticationCallback(callable $authenticationCallback): self
91
    {
92 8
        $new = clone $this;
93 8
        $new->authenticationCallback = $authenticationCallback;
94 8
        return $new;
95
    }
96
97
    /**
98
     * @param string $realm The HTTP authentication realm.
99
     *
100
     * @return $this
101
     *
102
     * @psalm-immutable
103
     */
104 2
    public function withRealm(string $realm): self
105
    {
106 2
        $new = clone $this;
107 2
        $new->realm = $realm;
108 2
        return $new;
109
    }
110
111
    /**
112
     * @param string|null $type Identity token type
113
     *
114
     * @return $this
115
     *
116
     * @psalm-immutable
117
     */
118 2
    public function withTokenType(?string $type): self
119
    {
120 2
        $new = clone $this;
121 2
        $new->tokenType = $type;
122 2
        return $new;
123
    }
124
125
    /**
126
     * Obtains authentication credentials from request.
127
     *
128
     * @param ServerRequestInterface $request
129
     *
130
     * @return array ['username', 'password'] array.
131
     */
132 15
    private function getAuthenticationCredentials(ServerRequestInterface $request): array
133
    {
134 15
        $username = $request->getServerParams()['PHP_AUTH_USER'] ?? null;
135 15
        $password = $request->getServerParams()['PHP_AUTH_PW'] ?? null;
136 15
        if ($username !== null || $password !== null) {
137 6
            return [$username, $password];
138
        }
139
140
        /*
141
         * Apache with php-cgi does not pass HTTP Basic authentication to PHP by default.
142
         * To make it work, add the following line to to your .htaccess file:
143
         *
144
         * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
145
         */
146 9
        $token = $this->getTokenFromHeaders($request);
147 9
        if ($token !== null && $this->isBasicToken($token)) {
148 7
            $credentials = $this->extractCredentialsFromHeader($token);
149 7
            if (count($credentials) < 2) {
150 3
                return [$credentials[0], null];
151
            }
152
153 4
            return $credentials;
154
        }
155
156 2
        return [null, null];
157
    }
158
159 9
    private function getTokenFromHeaders(ServerRequestInterface $request): ?string
160
    {
161 9
        $header = $request->getHeaderLine(Header::AUTHORIZATION);
162 9
        if (!empty($header)) {
163 7
            return $header;
164
        }
165
166 2
        return $request->getServerParams()['REDIRECT_HTTP_AUTHORIZATION'] ?? null;
167
    }
168
169 7
    private function extractCredentialsFromHeader(string $authToken): array
170
    {
171 7
        return array_map(
172 7
            static fn ($value) => $value === '' ? null : $value,
173 7
            explode(':', base64_decode(substr($authToken, 6)), 2)
174
        );
175
    }
176
177 8
    private function isBasicToken(string $token): bool
178
    {
179 8
        return strncasecmp($token, 'basic', 5) === 0;
180
    }
181
}
182