<?php /** * Name: Keycloak Password Auth * Description: Allow password-based authentication via the user's Keycloak credentials. * Version: 1.0 * Author: Ryan <https://verya.pe/profile/ryan> */ use Friendica\App; use Friendica\Core\Hook; use Friendica\Core\Logger; use Friendica\Core\Renderer; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\User; function keycloakpassword_install() { Hook::register('authenticate', __FILE__, 'keycloakpassword_authenticate'); } function keycloakpassword_request($client_id, $secret, $url, $params = []) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ 'client_id' => $client_id, 'grant_type' => 'password', 'client_secret' => $secret, 'scope' => 'openid', ] + $params)); $headers = array(); $headers[] = 'Content-Type: application/x-www-form-urlencoded'; curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $res = curl_exec($ch); if (curl_errno($ch)) { Logger::error(curl_error($ch)); } curl_close($ch); return $res; } function keycloakpassword_authenticate($a, &$b) { if (empty($b['password'])) { return; } $client_id = DI::config()->get('keycloakpassword', 'client_id', null); $endpoint = DI::config()->get('keycloakpassword', 'endpoint', null); $secret = DI::config()->get('keycloakpassword', 'secret', null); if (!$client_id || !$endpoint || !$secret) { return; } $condition = [ 'nickname' => $b['username'], 'blocked' => false, 'account_expired' => false, 'account_removed' => false ]; try { $user = DBA::selectFirst('user', ['uid'], $condition); } catch (Exception $e) { return; } $json = keycloakpassword_request( $client_id, $secret, $endpoint . '/token', [ 'username' => $b['username'], 'password' => $b['password'] ] ); $res = json_decode($json, true); if (array_key_exists('access_token', $res) && !array_key_exists('error', $res)) { $b['user_record'] = User::getById($user['uid']); $b['authenticated'] = 1; // Invalidate the Keycloak session we just created, as we have no use for it. keycloakpassword_request( $client_id, $secret, $endpoint . '/logout', [ 'refresh_token' => res['refresh_token'] ] ); } } function keycloakpassword_admin_input($key, $label, $description) { return [ '$' . $key => [ $key, $label, DI::config()->get('keycloakpassword', $key), $description, true, // all the fields are required ] ]; } function keycloakpassword_addon_admin(&$a, &$o) { $form = keycloakpassword_admin_input( 'client_id', DI::l10n()->t('Client ID'), DI::l10n()->t('The name of the OpenID Connect client you created for this addon in Keycloak.'), ) + keycloakpassword_admin_input( 'secret', DI::l10n()->t('Client secret'), DI::l10n()->t('The secret assigned to the OpenID Connect client you created for this addon in Keycloak.'), ) + keycloakpassword_admin_input( 'endpoint', DI::l10n()->t('OpenID Connect endpoint'), DI::l10n()->t( 'URL to the Keycloak endpoint for your client. ' . '(E.g., https://example.com/auth/realms/some-realm/protocol/openid-connect)' ), ) + [ '$msg' => DI::session()->get('keycloakpassword-msg', false), '$submit' => DI::l10n()->t('Save Settings'), ]; $t = Renderer::getMarkupTemplate('admin.tpl', 'addon/keycloakpassword/'); $o = Renderer::replaceMacros($t, $form); } function keycloakpassword_addon_admin_post(&$a) { if (!local_user()) { return; } $set = function ($key) { $val = (!empty($_POST[$key]) ? trim($_POST[$key]) : ''); DI::config()->set('keycloakpassword', $key, $val); }; $set('client_id'); $set('secret'); $set('endpoint'); }