|
3 | 3 | namespace App\Services; |
4 | 4 |
|
5 | 5 | use Sabre\DAV\Auth\Backend\AbstractBasic; |
| 6 | +use Sabre\HTTP\RequestInterface; |
| 7 | +use Sabre\HTTP\ResponseInterface; |
6 | 8 |
|
7 | 9 | /** |
8 | 10 | * Common base for the HTTP Basic authentication backends (internal, IMAP, LDAP). |
|
12 | 14 | * and an empty password means an *unauthenticated bind* for LDAP servers, which Active |
13 | 15 | * Directory (and OpenLDAP with `allow bind_anon_cred`) answers with success, i.e. it |
14 | 16 | * would log the caller in as any user. |
| 17 | + * |
| 18 | + * It also rejects usernames that would not survive being put in a principal URI. sabre |
| 19 | + * derives the principal from the login name (`principals/<username>`), so a name containing |
| 20 | + * a slash would address a different, possibly existing, node: `alice/calendar-proxy-write` |
| 21 | + * is exactly the URI Davis uses for alice's delegation proxy. Only structural characters are |
| 22 | + * refused here, not the stricter set required when creating an account, so that an unusual |
| 23 | + * but working username keeps authenticating. |
15 | 24 | */ |
16 | 25 | abstract class AbstractAuth extends AbstractBasic |
17 | 26 | { |
| 27 | + /** |
| 28 | + * The username as the backend spells it, when that differs from what the client sent. |
| 29 | + */ |
| 30 | + private ?string $canonicalUsername = null; |
| 31 | + |
18 | 32 | /** |
19 | 33 | * @param string $username |
20 | 34 | * @param string $password |
21 | 35 | */ |
22 | 36 | final protected function validateUserPass($username, $password): bool |
23 | 37 | { |
| 38 | + $this->canonicalUsername = null; |
| 39 | + |
24 | 40 | if (!is_string($username) || !is_string($password) || '' === $username || '' === $password) { |
25 | 41 | return false; |
26 | 42 | } |
27 | 43 |
|
| 44 | + if (self::breaksPrincipalUri($username)) { |
| 45 | + return false; |
| 46 | + } |
| 47 | + |
28 | 48 | return $this->checkCredentials($username, $password); |
29 | 49 | } |
30 | 50 |
|
31 | 51 | /** |
32 | 52 | * Validates a non-empty username and password against the backend. |
33 | 53 | */ |
34 | 54 | abstract protected function checkCredentials(string $username, string $password): bool; |
| 55 | + |
| 56 | + /** |
| 57 | + * Backends call this when the directory spells the username differently from what the |
| 58 | + * client sent — LDAP matches `ALICE` against `uid=alice` quite happily. The principal is |
| 59 | + * then built from that spelling instead, so the login, the account and the principal URI |
| 60 | + * cannot drift apart and produce a second, empty account. |
| 61 | + */ |
| 62 | + protected function setCanonicalUsername(string $username): void |
| 63 | + { |
| 64 | + // It ends up in a principal URI like any other username |
| 65 | + if ('' !== $username && !self::breaksPrincipalUri($username)) { |
| 66 | + $this->canonicalUsername = $username; |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + /** |
| 71 | + * @return array{0: bool, 1: string} |
| 72 | + */ |
| 73 | + public function check(RequestInterface $request, ResponseInterface $response) |
| 74 | + { |
| 75 | + $result = parent::check($request, $response); |
| 76 | + |
| 77 | + if (true === $result[0] && null !== $this->canonicalUsername) { |
| 78 | + return [true, $this->principalPrefix.$this->canonicalUsername]; |
| 79 | + } |
| 80 | + |
| 81 | + return $result; |
| 82 | + } |
| 83 | + |
| 84 | + private static function breaksPrincipalUri(string $username): bool |
| 85 | + { |
| 86 | + // Anything that would change the shape of `principals/<username>`: |
| 87 | + // [/\\] a forward or back slash, which would add a path segment |
| 88 | + // [\x00-\x20\x7f] any control character, plus space (0x20) and DEL (0x7f) |
| 89 | + return 1 === preg_match('~[/\\\\]|[\\x00-\\x20\\x7f]~', $username); |
| 90 | + } |
35 | 91 | } |
0 commit comments