Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.42% covered (warning)
88.42%
84 / 95
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
UserEntity
88.42% covered (warning)
88.42%
84 / 95
50.00% covered (danger)
50.00%
1 / 2
29.22
0.00% covered (danger)
0.00%
0 / 1
 __construct
35.29% covered (danger)
35.29%
6 / 17
0.00% covered (danger)
0.00%
0 / 1
15.75
 getClaims
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
1 / 1
22
1<?php
2
3/**
4 * OAuth2 user entity implementation.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) The National Library of Finland 2022-2024.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  OAuth2
25 * @author   Ere Maijala <ere.maijala@helsinki.fi>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org Main Site
28 */
29
30namespace VuFind\OAuth2\Entity;
31
32use League\OAuth2\Server\Entities\Traits\EntityTrait;
33use League\OAuth2\Server\Entities\UserEntityInterface as OAuth2UserEntityInterface;
34use OpenIDConnectServer\Entities\ClaimSetInterface;
35use VuFind\Auth\ILSAuthenticator;
36use VuFind\Db\Entity\UserEntityInterface as DbUserEntityInterface;
37use VuFind\Db\Service\AccessTokenServiceInterface;
38use VuFind\ILS\Connection;
39
40/**
41 * OAuth2 user entity implementation.
42 *
43 * @category VuFind
44 * @package  OAuth2
45 * @author   Ere Maijala <ere.maijala@helsinki.fi>
46 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
47 * @link     https://vufind.org Main Site
48 */
49class UserEntity implements OAuth2UserEntityInterface, ClaimSetInterface
50{
51    use EntityTrait;
52
53    /**
54     * Constructor
55     *
56     * @param DbUserEntityInterface       $user               User
57     * @param ?Connection                 $ils                ILS connection
58     * @param array                       $oauth2Config       OAuth2 configuration
59     * @param AccessTokenServiceInterface $accessTokenService Access token service
60     * @param ILSAuthenticator            $ilsAuthenticator   ILS authenticator
61     */
62    public function __construct(
63        protected DbUserEntityInterface $user,
64        protected ?Connection $ils,
65        protected array $oauth2Config,
66        protected AccessTokenServiceInterface $accessTokenService,
67        protected ILSAuthenticator $ilsAuthenticator
68    ) {
69        $userIdentifierField = $oauth2Config['Server']['userIdentifierField'] ?? 'id';
70        switch ($userIdentifierField) {
71            case 'id':
72                $userIdentifier = $user->getId();
73                break;
74            case 'username':
75                $userIdentifier = $user->getUsername();
76                break;
77            case 'cat_id':
78                $userIdentifier = $user->getCatId();
79                break;
80            default:
81                $userIdentifier = null;
82        }
83        if ($userIdentifier === null) {
84            throw new \VuFind\Exception\BadConfig(
85                "$userIdentifierField empty for user {$user->id}."
86                . ' The configured user identifier field has to be required.'
87            );
88        }
89        $this->setIdentifier($userIdentifier);
90    }
91
92    /**
93     * Get claims (attributes) for OpenID Connect
94     *
95     * @return array
96     */
97    public function getClaims()
98    {
99        // Get catalog information if the user has credentials:
100        $profile = [];
101        $blocked = null;
102        if ($this->ils && !empty($this->user->getCatUsername())) {
103            try {
104                $patron = $this->ils->patronLogin(
105                    $this->user->getCatUsername(),
106                    $this->ilsAuthenticator->getCatPasswordForUser($this->user)
107                );
108                $profile = $this->ils->getMyProfile($patron);
109                $blocksSupported = $this->ils
110                    ->checkCapability('getAccountBlocks', compact('patron'));
111                if ($blocksSupported) {
112                    $blocks = $this->ils->getAccountBlocks($patron);
113                    $blocked = !empty($blocks);
114                }
115            } catch (\Exception $e) {
116                // fall through since we don't know if any of the information is
117                // actually required
118            }
119        }
120
121        $result = [];
122        if ($nonce = $this->accessTokenService->getNonce($this->user->getId())) {
123            $result['nonce'] = $nonce;
124        }
125
126        foreach ($this->oauth2Config['ClaimMappings'] as $claim => $field) {
127            // Map legacy table field names to entity interface methods
128            $field = match ($field) {
129                'id' => 'getId',
130                'firstname' => 'getFirstname',
131                'lastname' => 'getLastname',
132                'email' => 'getEmail',
133                'last_language' => 'getLastLanguage',
134                default => $field
135            };
136
137            switch ($field) {
138                case 'age':
139                    if ($birthDate = $profile['birthdate'] ?? '') {
140                        $date = \DateTime::createFromFormat('Y-m-d', $birthDate);
141                        if ($date) {
142                            $diff = $date->diff(new \DateTimeImmutable(), true);
143                            $result[$claim] = (int)$diff->format('%y');
144                        }
145                    }
146                    break;
147                case 'address_json':
148                    if ($profile) {
149                        // address_json is a specially formatted field for address
150                        // information:
151                        $street = array_filter(
152                            [
153                                $profile['address1'] ?? '',
154                                $profile['address2'] ?? '',
155                            ]
156                        );
157                        $address = [
158                            'street_address' => implode("\n", $street),
159                            'locality' => $profile['city'] ?? '',
160                            'postal_code' => $profile['zip'] ?? '',
161                            'country' => $profile['country'] ?? '',
162                        ];
163                        $result[$claim] = json_encode($address);
164                    }
165                    break;
166                case 'block_status':
167                    // block_status is a flag indicating whether the patron has
168                    // blocks:
169                    $result[$claim] = $blocked;
170                    break;
171                case 'full_name':
172                    // full_name is a special field for firstname + lastname:
173                    $result[$claim] = trim(
174                        $this->user->getFirstname() . ' ' . $this->user->getLastname()
175                    );
176                    break;
177                case 'getLastLanguage':
178                    // Make sure any country code is in uppercase:
179                    $value = $this->user->getLastLanguage();
180                    $parts = explode('-', $value);
181                    if (isset($parts[1])) {
182                        $value = $parts[0] . '-' . strtoupper($parts[1]);
183                    }
184                    $result[$claim] = $value;
185                    break;
186                case 'library_user_id_hash':
187                    $id = $profile['cat_id'] ?? $this->user->getCatUsername() ?? null;
188                    if ($id) {
189                        $result[$claim] = hash(
190                            'sha256',
191                            $id . $this->oauth2Config['Server']['hashSalt']
192                        );
193                    }
194                    break;
195                default:
196                    if (
197                        (method_exists($this->user, $field)
198                        && $value = $this->user->{$field}())
199                        || ($value = $profile[$field] ?? null)
200                    ) {
201                        $result[$claim] = $value;
202                    }
203                    break;
204            }
205        }
206
207        return $result;
208    }
209}