From f0689e08d96f29c6e2161b1677c398187330c3b1 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 12 Apr 2023 11:21:29 +0200 Subject: [PATCH] [Web] iam - add switch for direct login flow --- data/web/inc/functions.auth.inc.php | 95 ++++++++++--------- data/web/inc/functions.inc.php | 4 +- data/web/js/site/mailbox.js | 1 - data/web/lang/lang.en-gb.json | 6 ++ .../admin/tab-config-identity-provider.twig | 25 ++++- 5 files changed, 83 insertions(+), 48 deletions(-) diff --git a/data/web/inc/functions.auth.inc.php b/data/web/inc/functions.auth.inc.php index c125b5d68..83c9ba8b1 100644 --- a/data/web/inc/functions.auth.inc.php +++ b/data/web/inc/functions.auth.inc.php @@ -52,7 +52,12 @@ function check_login($user, $pass, $app_passwd_data = false) { $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row){ // mbox does not exist, call keycloak login and create mbox if possible - $result = keycloak_mbox_login_rest($user, $pass, $is_dovecot, true); + $identity_provider_settings = identity_provider('get'); + if ($identity_provider_settings['login_flow'] == 'ropc'){ + $result = keycloak_mbox_login_ropc($user, $pass, $identity_provider_settings, $is_dovecot, true); + } else { + $result = keycloak_mbox_login_rest($user, $pass, $identity_provider_settings, $is_dovecot, true); + } if ($result){ return $result; } @@ -65,7 +70,12 @@ function check_login($user, $pass, $app_passwd_data = false) { } } - $result = keycloak_mbox_login_rest($user, $pass, $is_dovecot); + $identity_provider_settings = identity_provider('get'); + if ($identity_provider_settings['login_flow'] == 'ropc'){ + $result = keycloak_mbox_login_ropc($user, $pass, $identity_provider_settings, $is_dovecot); + } else { + $result = keycloak_mbox_login_rest($user, $pass, $identity_provider_settings, $is_dovecot); + } if ($result){ return $result; } @@ -318,15 +328,14 @@ function mailcow_mbox_apppass_login($user, $pass, $app_passwd_data, $is_internal // ROPC Flow (deprecated oAuth2.1) // uses direct user credentials for UI, IMAP and SMTP Auth -function keycloak_mbox_login_ropc($user, $pass, $is_internal = false, $create = false){ +function keycloak_mbox_login_ropc($user, $pass, $iam_settings, $is_internal = false, $create = false){ global $pdo; - $identity_provider_settings = identity_provider('get'); - $url = "{$identity_provider_settings['server_url']}/realms/{$identity_provider_settings['realm']}/protocol/openid-connect/token"; + $url = "{$iam_settings['server_url']}/realms/{$iam_settings['realm']}/protocol/openid-connect/token"; $req = http_build_query(array( 'grant_type' => 'password', - 'client_id' => $identity_provider_settings['client_id'], - 'client_secret' => $identity_provider_settings['client_secret'], + 'client_id' => $iam_settings['client_id'], + 'client_secret' => $iam_settings['client_secret'], 'username' => $user, 'password' => $pass, )); @@ -347,36 +356,38 @@ function keycloak_mbox_login_ropc($user, $pass, $is_internal = false, $create = // check if $user is email address, only accept email address as username return false; } - if ($create && !empty($identity_provider_settings['mappers'])){ + if ($create && !empty($iam_settings['mappers'])){ // try to create mbox on successfull login - $user_roles = $user_data['realm_access']['roles']; $mbox_template = null; - // check if matching rolemapping exist - foreach ($user_roles as $index => $role){ - if (in_array($role, $identity_provider_settings['roles'])) { - $mbox_template = $identity_provider_settings['templates'][$index]; + // check if matching attribute mapping exists + foreach ($iam_settings['mappers'] as $index => $mapper){ + if (in_array($mapper, $iam_settings['mappers'])) { + $mbox_template = $iam_settings['templates'][$index]; break; } } - if ($mbox_template){ - $stmt = $pdo->prepare("SELECT * FROM `templates` - WHERE `template` = :template AND type = 'mailbox'"); - $stmt->execute(array( - ":template" => $mbox_template - )); - $mbox_template_data = $stmt->fetch(PDO::FETCH_ASSOC); + if (!$mbox_template){ + // no matching template found + return false; + } + + $stmt = $pdo->prepare("SELECT * FROM `templates` + WHERE `template` = :template AND type = 'mailbox'"); + $stmt->execute(array( + ":template" => $mbox_template + )); + $mbox_template_data = $stmt->fetch(PDO::FETCH_ASSOC); - if (!empty($mbox_template_data)){ - $mbox_template_data = json_decode($mbox_template_data["attributes"], true); - $mbox_template_data['domain'] = explode('@', $user)[1]; - $mbox_template_data['local_part'] = explode('@', $user)[0]; - $mbox_template_data['authsource'] = 'keycloak'; - $_SESSION['iam_create_login'] = true; - $create_res = mailbox('add', 'mailbox', $mbox_template_data); - $_SESSION['iam_create_login'] = false; - if (!$create_res){ - return false; - } + if (!empty($mbox_template_data)){ + $mbox_template_data = json_decode($mbox_template_data["attributes"], true); + $mbox_template_data['domain'] = explode('@', $user)[1]; + $mbox_template_data['local_part'] = explode('@', $user)[0]; + $mbox_template_data['authsource'] = 'keycloak'; + $_SESSION['iam_create_login'] = true; + $create_res = mailbox('add', 'mailbox', $mbox_template_data); + $_SESSION['iam_create_login'] = false; + if (!$create_res){ + return false; } } } @@ -394,17 +405,15 @@ function keycloak_mbox_login_ropc($user, $pass, $is_internal = false, $create = // Keycloak REST Api Flow - auth user by mailcow_password attribute // This password will be used for direct UI, IMAP and SMTP Auth // To use direct user credentials, only Authorization Code Flow is valid -function keycloak_mbox_login_rest($user, $pass, $is_internal = false, $create = false){ +function keycloak_mbox_login_rest($user, $pass, $iam_settings, $is_internal = false, $create = false){ global $pdo; - $identity_provider_settings = identity_provider('get'); - // get access_token for service account of mailcow client - $url = "{$identity_provider_settings['server_url']}/realms/{$identity_provider_settings['realm']}/protocol/openid-connect/token"; + $url = "{$iam_settings['server_url']}/realms/{$iam_settings['realm']}/protocol/openid-connect/token"; $req = http_build_query(array( 'grant_type' => 'client_credentials', - 'client_id' => $identity_provider_settings['client_id'], - 'client_secret' => $identity_provider_settings['client_secret'] + 'client_id' => $iam_settings['client_id'], + 'client_secret' => $iam_settings['client_secret'] )); $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); @@ -420,7 +429,7 @@ function keycloak_mbox_login_rest($user, $pass, $is_internal = false, $create = } // get the mailcow_password attribute from keycloak user - $url = "{$identity_provider_settings['server_url']}/admin/realms/{$identity_provider_settings['realm']}/users"; + $url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users"; $queryParams = array('email' => $user, 'exact' => true); $queryString = http_build_query($queryParams); $curl = curl_init(); @@ -448,7 +457,7 @@ function keycloak_mbox_login_rest($user, $pass, $is_internal = false, $create = // get mapped template, if not set return false // also return false if no mappers were defined $user_template = $user_data['attributes']['mailcow_template'][0]; - if ($create && (empty($identity_provider_settings['mappers']) || $user_template)){ + if ($create && (empty($iam_settings['mappers']) || $user_template)){ return false; } else if (!$create) { // login success - dont create mailbox @@ -462,10 +471,10 @@ function keycloak_mbox_login_rest($user, $pass, $is_internal = false, $create = // try to create mbox on successfull login $mbox_template = null; - // check if matching rolemapping exist - foreach ($identity_provider_settings['mappers'] as $index => $mapper){ - if (in_array($mapper, $identity_provider_settings['mappers'])) { - $mbox_template = $identity_provider_settings['templates'][$index]; + // check if matching attribute mapping exists + foreach ($iam_settings['mappers'] as $index => $mapper){ + if (in_array($mapper, $iam_settings['mappers'])) { + $mbox_template = $iam_settings['templates'][$index]; break; } } diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 1e4cc8c1d..9ee4d0d09 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -2104,8 +2104,10 @@ function identity_provider($_action, $_data = null, $hide_secret = false) { $data_log['client_secret'] = '*'; $stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES (:key, :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);"); + $_data['login_flow'] = (isset($_data['login_flow']) && $_data['login_flow'] == 'ropc') ? 'ropc' : 'rest'; + // add connection settings - $required_settings = array('server_url', 'authsource', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version'); + $required_settings = array('server_url', 'authsource', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'login_flow'); foreach($required_settings as $setting){ if (!$_data[$setting]){ $_SESSION['return'][] = array( diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index e562ab8cf..06c1444d3 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -407,7 +407,6 @@ $(document).ready(function() { $('#rl_frame').selectpicker('val', template.rl_frame); } - console.log(template.active) if (template.active){ $('#mbox_active').selectpicker('val', template.active.toString()); } else { diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index ebebd3dc8..9825ab9d2 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -212,11 +212,17 @@ "host": "Host", "html": "HTML", "iam": "Identity Provider", + "iam_auth_flow": "Authentication Flow", + "iam_auth_flow_info": "In addition to the Authorization Code Flow (Standard Flow in Keycloak), which is used for Single-Sign On login, mailcow also supports Authentication Flows with direct Credentials", + "iam_auth_flow_rest_info": "1. Mailpassword Flow (Default)
The Mailpassword Flow attempts to validate the user's credentials by using the Keycloak Admin REST API. mailcow retrieves the hashed password from the mailcow_password attribute, which is mapped in Keycloak. If this attribute is not found, the user needs to log in to the mailcow UI via Single-Sign On and create an App Password to use a mail client.
To enable this flow, the mailcow client in Keycloak must have Service accounts roles checked under Authentication Flow.", + "iam_auth_flow_ropc_info": "2. Resource Owner Password Flow
We do not recommend using this flow, as it is probably deprecated in the new OAuth 2.1 protocol. The Resource Owner Password Flow allows direct validation of the user's credentials. Therefore, the user has to trust mailcow to handle their external credentials securely. No Mailpassword or App Password is required to use a mail client.
To enable this flow, the mailcow client in Keycloak must have Direct access grants checked under Authentication Flow.", "iam_client_id": "Client Id", "iam_client_secret": "Client Secret", "iam_description": "Here, you can configure the integration with an external Keycloak service. The Keycloak user's mailboxes will be automatically created upon their first login, provided that a attribute mapping has been set.", "iam_realm": "Realm", "iam_redirect_url": "Redirect Url", + "iam_ropc_flow": "Resource Owner Password Flow", + "iam_rest_flow": "Mailpassword Flow", "iam_mapping": "Attribute Mapping", "iam_server_url": "Server Url", "iam_sso": "SSO", diff --git a/data/web/templates/admin/tab-config-identity-provider.twig b/data/web/templates/admin/tab-config-identity-provider.twig index 13bbc7553..1eb9aef39 100644 --- a/data/web/templates/admin/tab-config-identity-provider.twig +++ b/data/web/templates/admin/tab-config-identity-provider.twig @@ -49,7 +49,7 @@ -
+
Attribute @@ -83,13 +83,32 @@
{% endif %}
+
+ +
+
+ + + + + +
+

+ + {{ lang.admin.iam_auth_flow_info|raw }}
+ {{ lang.admin.iam_auth_flow_rest_info|raw }}
+ {{ lang.admin.iam_auth_flow_ropc_info|raw }} +
+

+
+
-
+
- +