Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
OidcHandler
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
3 / 3
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 handleCallback
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
6
 authenticateWorkstation
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace BO\Zmsclient;
4
5use BO\Zmsclient\Auth;
6use BO\Zmsclient\Http;
7use BO\Zmsentities\Schema\Entity;
8
9/**
10 * Shared OIDC callback handler used by zmsadmin and zmsstatistic.
11 *
12 * Validates the state parameter against the session auth key with a
13 * constant-time comparison and resolves the workstation/department state
14 * needed by the application controller to decide on the next redirect.
15 */
16class OidcHandler
17{
18    private Http $http;
19
20    public function __construct(Http $http)
21    {
22        $this->http = $http;
23    }
24
25    /**
26     * Handle OIDC callback with secure state validation.
27     *
28     * @param string|null $state       State parameter from the OIDC callback
29     * @param string      $application Application name for logging (e.g. zmsadmin)
30     *
31     * @throws \BO\Slim\Exception\OAuthInvalid when the state does not match
32     * @throws \Throwable                      for downstream workstation errors
33     *
34     * @return array{
35     *     workstation: mixed,
36     *     department_count: int,
37     *     redirect_to_index: bool
38     * }
39     */
40    public function handleCallback(?string $state, string $application): array
41    {
42        $authKey = Auth::getKey();
43        $sessionHash = hash('sha256', (string) $authKey);
44
45        $stateIsValid = is_string($state)
46            && is_string($authKey)
47            && $state !== ''
48            && $authKey !== ''
49            && hash_equals($authKey, $state);
50
51        \App::$log->info('OIDC Login state validation', [
52            'event' => 'oauth_login_state_validation',
53            'timestamp' => date('c'),
54            'provider' => Auth::getOidcProvider(),
55            'application' => $application,
56            'state_match' => $stateIsValid,
57            'hashed_session_token' => $sessionHash,
58        ]);
59
60        if (!$stateIsValid) {
61            \App::$log->error('OIDC Login invalid state', [
62                'event' => 'oauth_login_invalid_state',
63                'timestamp' => date('c'),
64                'provider' => Auth::getOidcProvider(),
65                'application' => $application,
66            ]);
67            throw new \BO\Slim\Exception\OAuthInvalid();
68        }
69
70        return $this->authenticateWorkstation($application, $sessionHash);
71    }
72
73    /**
74     * @return array{workstation: mixed, department_count: int, redirect_to_index: bool}
75     */
76    private function authenticateWorkstation(string $application, string $sessionHash): array
77    {
78        try {
79            $workstation = $this->http
80                ->readGetResult('/workstation/', ['resolveReferences' => 2])
81                ->getEntity();
82
83            if (!$workstation instanceof Entity) {
84                throw new \RuntimeException('OIDC workstation lookup returned no entity');
85            }
86
87            $username = $workstation->getUseraccount()->id;
88            $workstationAuthKey = $workstation['authkey'] ?? Auth::getKey() ?? '';
89            $workstationHash = hash('sha256', (string) $workstationAuthKey);
90
91            \App::$log->info('OIDC Login workstation access', [
92                'event' => 'oauth_login_workstation_access',
93                'timestamp' => date('c'),
94                'provider' => Auth::getOidcProvider(),
95                'application' => $application,
96                'username' => $username,
97                'workstation_id' => $workstation->id ?? 'unknown',
98                'hashed_workstation_key' => $workstationHash,
99            ]);
100
101            $departmentCount = $workstation->getUseraccount()->getDepartmentList()->count();
102
103            \App::$log->info('OIDC Login department check', [
104                'event' => 'oauth_login_department_check',
105                'timestamp' => date('c'),
106                'provider' => Auth::getOidcProvider(),
107                'application' => $application,
108                'username' => $username,
109                'department_count' => $departmentCount,
110                'has_departments' => ($departmentCount > 0),
111                'hashed_session_token' => $sessionHash,
112            ]);
113
114            return [
115                'workstation' => $workstation,
116                'department_count' => $departmentCount,
117                'redirect_to_index' => (0 === $departmentCount),
118            ];
119        } catch (\Throwable $e) {
120            \App::$log->error('OIDC Login workstation error', [
121                'event' => 'oauth_login_workstation_error',
122                'timestamp' => date('c'),
123                'provider' => Auth::getOidcProvider(),
124                'application' => $application,
125                'error' => $e->getMessage(),
126                'code' => $e->getCode(),
127            ]);
128            throw $e;
129        }
130    }
131}