3 * Name: Keycloak Password Auth
4 * Description: Allow password-based authentication via the user's Keycloak credentials.
6 * Author: Ryan <https://verya.pe/profile/ryan>
10 use Friendica\Core\Hook;
11 use Friendica\Core\Logger;
12 use Friendica\Core\Renderer;
13 use Friendica\Database\DBA;
15 use Friendica\Model\User;
17 function keycloakpassword_install()
19 Hook::register('authenticate', __FILE__, 'keycloakpassword_authenticate');
22 function keycloakpassword_request($client_id, $secret, $url, $params = [])
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,
36 $headers[] = 'Content-Type: application/x-www-form-urlencoded';
37 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
38 $res = curl_exec($ch);
40 if (curl_errno($ch)) {
41 Logger::error(curl_error($ch));
48 function keycloakpassword_authenticate($a, &$b)
50 if (empty($b['password'])) {
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);
58 if (!$client_id || !$endpoint || !$secret) {
63 'nickname' => $b['username'],
65 'account_expired' => false,
66 'account_removed' => false
70 $user = DBA::selectFirst('user', ['uid'], $condition);
71 } catch (Exception $e) {
75 $json = keycloakpassword_request(
80 'username' => $b['username'],
81 'password' => $b['password']
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;
90 // Invalidate the Keycloak session we just created, as we have no use for it.
91 keycloakpassword_request(
94 $endpoint . '/logout',
95 [ 'refresh_token' => res['refresh_token'] ]
100 function keycloakpassword_admin_input($key, $label, $description)
106 DI::config()->get('keycloakpassword', $key),
108 true, // all the fields are required
113 function keycloakpassword_addon_admin(&$a, &$o)
116 keycloakpassword_admin_input(
118 DI::l10n()->t('Client ID'),
119 DI::l10n()->t('The name of the OpenID Connect client you created for this addon in Keycloak.'),
121 keycloakpassword_admin_input(
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.'),
126 keycloakpassword_admin_input(
128 DI::l10n()->t('OpenID Connect endpoint'),
130 'URL to the Keycloak endpoint for your client. '
131 . '(E.g., https://example.com/auth/realms/some-realm/protocol/openid-connect)'
135 '$msg' => DI::session()->get('keycloakpassword-msg', false),
136 '$submit' => DI::l10n()->t('Save Settings'),
139 $t = Renderer::getMarkupTemplate('admin.tpl', 'addon/keycloakpassword/');
140 $o = Renderer::replaceMacros($t, $form);
143 function keycloakpassword_addon_admin_post(&$a)
149 $set = function ($key) {
150 $val = (!empty($_POST[$key]) ? trim($_POST[$key]) : '');
151 DI::config()->set('keycloakpassword', $key, $val);