Digest::validate()   C
last analyzed

Complexity

Conditions 8
Paths 16

Size

Total Lines 40
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 40
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 24
nc 16
nop 1
1
<?php
2
3
/**
4
 *
5
 * This file is part of the Apix Project.
6
 *
7
 * (c) Franck Cassedanne <franck at ouarz.net>
8
 *
9
 * @license     http://opensource.org/licenses/BSD-3-Clause  New BSD License
10
 *
11
 */
12
13
namespace Apix\Plugin\Auth;
14
15
/**
16
 * HTTP Digest authentication.
17
 *
18
 * Adapted from Paul James's implemenation.
19
 * @link http://www.peej.co.uk/files/httpdigest.phps
20
 *
21
 * @author Franck Cassedanne
22
 * @codeCoverageIgnore
23
 */
24
class Digest extends AbstractAuth
25
{
26
27
    /**
28
     * Holds the salt (private key).
29
     * @var string.
30
     */
31
    protected $salt = null;
32
33
    /**
34
     * Holds the opaque value.
35
     * @var string
36
     */
37
    protected $opaque = null;
38
39
    /**
40
     * Enable a1 hashing (username:realm:password) or use plain text.
41
     * @var boolean
42
     */
43
    protected $a1_hashing = true;
44
45
    /**
46
     * @var int The life of the nonce value in seconds
47
     */
48
    protected $nonce_life = 300;
49
50
    /**
51
     * Constructor
52
     * @param string $realm Perhaps a custom realm. Default is null so the
53
     *                      realm will be $_SERVER['SERVER_NAME']
54
     */
55
    public function __construct($realm = null, $salt='Peppered and Salted', $opaque='opaque')
0 ignored issues
show
Coding Style introduced by
__construct uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
56
    {
57
        $this->realm = null !== $realm
58
                     ? $realm
59
                     : $_SERVER['SERVER_NAME'];
60
61
        $this->salt = $salt;
62
        $this->opaque = md5($opaque);
63
    }
64
65
    /**
66
     * @{@inheritdoc}
67
     */
68
    public function send()
69
    {
70
        $digest = sprintf(
71
            'Digest realm="%s", domain="%s", qop=auth, algorithm=MD5,'
72
            . ' nonce="%s", opaque="%s"',
73
            $this->realm, $this->base_url,
74
            $this->getNonce(), $this->opaque
75
        );
76
77
        header('WWW-Authenticate: ' . $digest);
78
        header('HTTP/1.0 401 Unauthorized');
79
    }
80
81
    /**
82
     * @{@inheritdoc}
83
     *
84
     * @link    http://www.peej.co.uk/projects/phphttpdigest.html
85
     * @link    http://www.faqs.org/rfcs/rfc2617.html
86
     */
87
    public function authenticate()
0 ignored issues
show
Coding Style introduced by
authenticate uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
88
    {
89
        if (
90
            isset($_SERVER['PHP_AUTH_DIGEST'])
91
            && $this->parseDigest($_SERVER['PHP_AUTH_DIGEST'])
92
        ) {
93
            $token = $this->getToken($this->digest);
0 ignored issues
show
Bug introduced by
The property digest does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
94
            if (!isset($token)) {
95
                return $this->send();
96
            }
97
98
            return $this->validate($token);
99
        }
100
101
        return $this->send();
102
    }
103
104
    /**
105
     * Gets the nonce value for HTTP Digest.
106
     *
107
     * @return string
108
     */
109
    public function getNonce()
0 ignored issues
show
Coding Style introduced by
getNonce uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
110
    {
111
        $time = ceil(time() / $this->nonce_life) * $this->nonce_life;
112
113
        $ip = isset($_SERVER['HTTP_X_FORWARDED_FOR'])
114
              ? $_SERVER['HTTP_X_FORWARDED_FOR']
115
              : $_SERVER['REMOTE_ADDR'];
116
117
        return md5(date('Y-m-d H:i', $time) . ':' . $ip . ':' . $this->salt);
118
    }
119
120
    protected function parseDigest($digest)
121
    {
122
        // username="test", realm="test.dev", nonce="e8ae165a8fa2a10bb09303012556952c", uri="/", response="dcfe5fb7a2e3160155dc46f5eb590035", opaque="94619f8a70068b2591c2eed622525b0e", algorithm="MD5", cnonce="f976912c5322bfc760a13155b254d5b3", nc=00000001, qop="auth"
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
123
        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
124
        echo $digest;
125
        preg_match('/username="([^"]+)", realm="([^"]+)", nonce="([^"]+)"/', $digest, $m);
126
        echo '<hr>';
127
        echo "<pre>";
128
        print_r($m);
129
        exit;
130
        */
131
132
        if (preg_match('/username="([^"]+)"/', $digest, $username)
133
            && preg_match('/[,| ]nonce="([^"]+)"/', $digest, $nonce)
134
            && preg_match('/response="([^"]+)"/', $digest, $response)
135
            && preg_match('/opaque="([^"]+)"/', $digest, $opaque)
136
            && preg_match('/uri="([^"]+)"/', $digest, $uri))
137
        {
138
            $this->digest = array_map(
139
                function ($a) {return array_pop($a);},
140
                compact('username', 'nonce', 'response', 'opaque', 'uri')
141
            );
142
143
            $this->username = $this->digest['username'];
144
145
            // check for quality of protection
146
            if (
147
                preg_match('/qop="?([^,\s"]+)/', $digest, $qop)
148
                && preg_match('/nc=([^,\s"]+)/', $digest, $nc)
149
                && preg_match('/cnonce="([^"]+)"/', $digest, $cnonce)
150
            ) {
151
                $this->digest['qop'] = $nc[1] . ':' . $cnonce[1] . ':' . $qop[1];
152
            }
153
154
            return true;
155
        }
156
157
        return false;
158
    }
159
160
    protected function validate($token)
0 ignored issues
show
Coding Style introduced by
validate uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
161
    {
162
        $uri = $_SERVER['REQUEST_URI'];
163
164
        // IE hack (remove querystring from response hash)
165
        if (strpos($uri, '?') !== false) {
166
            $uri = substr($uri, 0, strlen($this->digest['uri']));
167
        }
168
169
        if (
170
            $this->opaque == $this->digest['opaque']
171
            && $uri == $this->digest['uri']
172
            && $this->getNonce() == $this->digest['nonce']
173
        ) {
174
            $passphrase = hash(
175
                'md5',
176
                "{$this->digest['username']}:{$this->realm}:{$token}"
177
            );
178
179
            $pass = $this->a1_hashing
180
                    ? $passphrase
181
                    : md5(
182
                            $this->digest['username']
183
                            . ':' . $this->realm
184
                            . ':' . $passphrase
185
                        );
186
187
            $expected = $pass . ':' . $this->digest['nonce'] . ':';
188
            if (isset($this->digest['qop'])) {
189
                $expected .= $this->digest['qop'] . ':';
190
            }
191
            $expected .= md5($_SERVER['REQUEST_METHOD'] . ':' . $uri);
192
193
            if ($this->digest['response'] == md5($expected)) {
194
                return $this->digest['username'];
195
            }
196
        }
197
198
        return $this->send();
199
    }
200
201
    /** TODO: RM? Get the HTTP Auth header
202
     * @return str
203
     */
204
/*
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
205
    public function getAuthHeader()
206
    {
207
        if (isset($_SERVER['Authorization'])) {
208
            return $_SERVER['Authorization'];
209
        } elseif (function_exists('apache_request_headers')) {
210
            $headers = apache_request_headers();
211
            if (isset($headers['Authorization'])) {
212
                return $headers['Authorization'];
213
            }
214
        }
215
216
        return NULL;
217
    }
218
*/
219
220
}
221