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