Completed
Push — master ( bb7666...882899 )
by Oscar
02:21
created

DigestAuthentication::getUsername()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
namespace Psr7Middlewares\Middleware;
4
5
use Psr\Http\Message\ServerRequestInterface;
6
use Psr7Middlewares\Middleware;
7
use Psr7Middlewares\Utils;
8
use Psr\Http\Message\ResponseInterface;
9
10
/**
11
 * Middleware to create a digest http authentication.
12
 *
13
 * @see https://tools.ietf.org/html/rfc2069#page-10
14
 */
15
class DigestAuthentication
16
{
17
    use Utils\AuthenticationTrait;
18
19
    const KEY = 'USERNAME';
20
21
    /**
22
     * @var string|null The nonce value
23
     */
24
    private $nonce;
25
26
    /**
27
     * Returns the username.
28
     *
29
     * @param ServerRequestInterface $request
30
     *
31
     * @return string|null
32
     */
33
    public static function getUsername(ServerRequestInterface $request)
34
    {
35
        return Middleware::getAttribute($request, self::KEY);
36
    }
37
38
    /**
39
     * Set the nonce value.
40
     *
41
     * @param string $nonce
42
     *
43
     * @return self
44
     */
45
    public function nonce($nonce)
46
    {
47
        $this->nonce = $nonce;
48
49
        return $this;
50
    }
51
52
    /**
53
     * Execute the middleware.
54
     *
55
     * @param ServerRequestInterface  $request
56
     * @param ResponseInterface       $response
57
     * @param callable                $next
58
     *
59
     * @return ResponseInterface
60
     */
61
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
62
    {
63
        if ($this->login($request, $username)) {
64
            return $next(
65
                Middleware::setAttribute($request, self::KEY, $username),
66
                $response
67
            );
68
        }
69
70
        return $response
71
            ->withStatus(401)
72
            ->withHeader('WWW-Authenticate', 'Digest realm="'.$this->realm.'",qop="auth",nonce="'.($this->nonce ?: uniqid()).'",opaque="'.md5($this->realm).'"');
73
    }
74
75
    /**
76
     * Login or check the user credentials.
77
     *
78
     * @param ServerRequestInterface $request
79
     * @param string|null            $username
80
     *
81
     * @return bool
82
     */
83
    private function login(ServerRequestInterface $request, &$username)
84
    {
85
        //Check header
86
        $authorization = self::parseAuthorizationHeader($request->getHeaderLine('Authorization'));
87
88
        if (!$authorization) {
89
            return false;
90
        }
91
92
        //Check whether user exists
93
        if (!isset($this->users[$authorization['username']])) {
94
            return false;
95
        }
96
97
        $username = $authorization['username'];
98
99
        //Check authentication
100
        return $this->checkAuthentication($authorization, $request->getMethod(), $this->users[$username]);
101
    }
102
103
    /**
104
     * Validates the user authentication.
105
     *
106
     * @param array  $authorization
107
     * @param string $method
108
     * @param string $password
109
     *
110
     * @return bool
111
     */
112
    private function checkAuthentication(array $authorization, $method, $password)
113
    {
114
        $A1 = md5("{$authorization['username']}:{$this->realm}:{$password}");
115
        $A2 = md5("{$method}:{$authorization['uri']}");
116
117
        $validResponse = md5("{$A1}:{$authorization['nonce']}:{$authorization['nc']}:{$authorization['cnonce']}:{$authorization['qop']}:{$A2}");
118
119
        return $authorization['response'] === $validResponse;
120
    }
121
122
    /**
123
     * Parses the authorization header for a basic authentication.
124
     *
125
     * @param string $header
126
     *
127
     * @return false|array
128
     */
129
    private static function parseAuthorizationHeader($header)
130
    {
131
        if (strpos($header, 'Digest') !== 0) {
132
            return false;
133
        }
134
135
        $needed_parts = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1];
136
        $data = [];
137
138
        preg_match_all('@('.implode('|', array_keys($needed_parts)).')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', substr($header, 7), $matches, PREG_SET_ORDER);
139
140
        if ($matches) {
141
            foreach ($matches as $m) {
142
                $data[$m[1]] = $m[3] ? $m[3] : $m[4];
143
                unset($needed_parts[$m[1]]);
144
            }
145
        }
146
147
        return empty($needed_parts) ? $data : false;
148
    }
149
}
150