[CI] Add PHP 8.3
[friendica-addons.git/.git] / keycloakpassword / keycloakpassword.php
1 <?php
2 /**
3  * Name: Keycloak Password Auth
4  * Description: Allow password-based authentication via the user's Keycloak credentials.
5  * Version: 1.0
6  * Author: Ryan <https://verya.pe/profile/ryan>
7  */
8
9 use Friendica\App;
10 use Friendica\Core\Hook;
11 use Friendica\Core\Logger;
12 use Friendica\Core\Renderer;
13 use Friendica\Database\DBA;
14 use Friendica\DI;
15 use Friendica\Model\User;
16
17 function keycloakpassword_install()
18 {
19         Hook::register('authenticate', __FILE__, 'keycloakpassword_authenticate');
20 }
21
22 function keycloakpassword_request($client_id, $secret, $url, $params = [])
23 {
24         $ch = curl_init();
25         curl_setopt($ch, CURLOPT_URL, $url);
26         curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
27         curl_setopt($ch, CURLOPT_POST, 1);
28         curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
29                 'client_id' => $client_id,
30                 'grant_type' => 'password',
31                 'client_secret' => $secret,
32                 'scope' => 'openid',
33         ] + $params));
34
35         $headers = array();
36         $headers[] = 'Content-Type: application/x-www-form-urlencoded';
37         curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
38         $res = curl_exec($ch);
39
40         if (curl_errno($ch)) {
41                 Logger::error(curl_error($ch));
42         }
43         curl_close($ch);
44
45         return $res;
46 }
47
48 function keycloakpassword_authenticate(array &$b)
49 {
50         if (empty($b['password'])) {
51                 return;
52         }
53
54         $client_id = DI::config()->get('keycloakpassword', 'client_id', null);
55         $endpoint = DI::config()->get('keycloakpassword', 'endpoint', null);
56         $secret = DI::config()->get('keycloakpassword', 'secret', null);
57
58         if (!$client_id || !$endpoint || !$secret) {
59                 return;
60         }
61
62         $condition = [
63                 'nickname' => $b['username'],
64                 'blocked' => false,
65                 'account_expired' => false,
66                 'account_removed' => false
67         ];
68
69         try {
70                 $user = DBA::selectFirst('user', ['uid'], $condition);
71         } catch (Exception $e) {
72                 return;
73         }
74
75         $json = keycloakpassword_request(
76                 $client_id,
77                 $secret,
78                 $endpoint . '/token',
79                 [
80                         'username' => $b['username'],
81                         'password' => $b['password']
82                 ]
83         );
84
85         $res = json_decode($json, true);
86         if (array_key_exists('access_token', $res) && !array_key_exists('error', $res)) {
87                 $b['user_record'] = User::getById($user['uid']);
88                 $b['authenticated'] = 1;
89
90                 // Invalidate the Keycloak session we just created, as we have no use for it.
91                 keycloakpassword_request(
92                         $client_id,
93                         $secret,
94                         $endpoint . '/logout',
95                         [ 'refresh_token' => res['refresh_token'] ]
96                 );
97         }
98 }
99
100 function keycloakpassword_admin_input($key, $label, $description)
101 {
102         return [
103                 '$' . $key => [
104                         $key,
105                         $label,
106                         DI::config()->get('keycloakpassword', $key),
107                         $description,
108                         true, // all the fields are required
109                 ]
110         ];
111 }
112
113 function keycloakpassword_addon_admin(string &$o)
114 {
115         $form =
116                 keycloakpassword_admin_input(
117                         'client_id',
118                         DI::l10n()->t('Client ID'),
119                         DI::l10n()->t('The name of the OpenID Connect client you created for this addon in Keycloak.'),
120                 ) +
121                 keycloakpassword_admin_input(
122                         'secret',
123                         DI::l10n()->t('Client secret'),
124                         DI::l10n()->t('The secret assigned to the OpenID Connect client you created for this addon in Keycloak.'),
125                 ) +
126                 keycloakpassword_admin_input(
127                         'endpoint',
128                         DI::l10n()->t('OpenID Connect endpoint'),
129                         DI::l10n()->t(
130                                 'URL to the Keycloak endpoint for your client. '
131                                 . '(E.g., https://example.com/auth/realms/some-realm/protocol/openid-connect)'
132                         ),
133                 ) +
134                 [
135                         '$msg' => DI::session()->get('keycloakpassword-msg', false),
136                         '$submit'  => DI::l10n()->t('Save Settings'),
137                 ];
138
139         $t = Renderer::getMarkupTemplate('admin.tpl', 'addon/keycloakpassword/');
140         $o = Renderer::replaceMacros($t, $form);
141 }
142
143 function keycloakpassword_addon_admin_post()
144 {
145         if (!DI::userSession()->getLocalUserId()) {
146                 return;
147         }
148
149         $set = function ($key) {
150                 $val = (!empty($_POST[$key]) ? trim($_POST[$key]) : '');
151                 DI::config()->set('keycloakpassword', $key, $val);
152         };
153         $set('client_id');
154         $set('secret');
155         $set('endpoint');
156 }