From 344e2c6978fdb4882feb07e9fa21459650035942 Mon Sep 17 00:00:00 2001 From: very-ape Date: Tue, 18 May 2021 15:51:30 -0700 Subject: [PATCH] Add an addon for password-based authentication against Keycloak. --- keycloakpassword/README.md | 9 ++ keycloakpassword/keycloakpassword.php | 156 ++++++++++++++++++++++++++ keycloakpassword/templates/admin.tpl | 4 + 3 files changed, 169 insertions(+) create mode 100644 keycloakpassword/README.md create mode 100644 keycloakpassword/keycloakpassword.php create mode 100755 keycloakpassword/templates/admin.tpl diff --git a/keycloakpassword/README.md b/keycloakpassword/README.md new file mode 100644 index 000000000..0dc60c683 --- /dev/null +++ b/keycloakpassword/README.md @@ -0,0 +1,9 @@ +Keycloak Password Auth +====================== + +Allows for password-based authentication against a Keycloak backend. (Should in +theory work with any OpenID Connect provider with "direct grant" enabled, but +it's only been tested against Keycloak.) + +Setting up Keycloak for use with this addon is detailed [in this RedHat +blog entry](https://developers.redhat.com/blog/2020/01/29/api-login-and-jwt-token-generation-using-keycloak#set_up_a_client). diff --git a/keycloakpassword/keycloakpassword.php b/keycloakpassword/keycloakpassword.php new file mode 100644 index 000000000..c9ae892bb --- /dev/null +++ b/keycloakpassword/keycloakpassword.php @@ -0,0 +1,156 @@ + + */ + +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'); +} diff --git a/keycloakpassword/templates/admin.tpl b/keycloakpassword/templates/admin.tpl new file mode 100755 index 000000000..449bf7ec7 --- /dev/null +++ b/keycloakpassword/templates/admin.tpl @@ -0,0 +1,4 @@ +{{include file="field_input.tpl" field=$client_id}} +{{include file="field_input.tpl" field=$secret}} +{{include file="field_input.tpl" field=$endpoint}} +