Skip to content

Commit

Permalink
3.1.2
Browse files Browse the repository at this point in the history
OAuth2 with Microsoft Entra is now available
  • Loading branch information
nilsteampassnet committed Sep 8, 2024
1 parent 4428f3c commit d39f525
Show file tree
Hide file tree
Showing 39 changed files with 685 additions and 290 deletions.
39 changes: 39 additions & 0 deletions docs/features/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,42 @@ If disabled for a user, a red fingerprint symbol is shown in the users list.

![Settings tasks options](../_media/tp3_auth_mfa_3.png)


## Oauth2 with Microsoft Entra (Azure)

Users can authenticate through your Entra AD. The first time a user is authenticate in Teampass through oauth2, his account will be created in Teampass.

👉 Notice that if the user has `group memberships` defined in AD and that those groups also exist in Teampass, then the user will be automatically associated to the matching groups (based upon their names).

### Setting up at Entra side

You need to create a new `App registration` for example called `Teampass`.

This App will have an `Application (client) ID` and a `Directory (tenant) ID`.
A `Client secret` is also expected, not the `Secret ID` but the `Value` (the one that is only seen once).

You will have to define a new `Redirect URIs` with the value provided from Teampass OAuth configuration page.
And it is suggested to use option `Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant)` as `Supported account types`.

Now define `API permissions` with next permissions:

* `Microsoft Graph`:
* `email` with Type `Delegated`
* `Group.Read.All` with Type `Delegated`
* `Group.Read.All` with Type `Application`
* `offline_access` with Type `Delegated`
* `openid` with Type `Delegated`
* `profile` with Type `Delegated`
* `User.Read` with Type `Delegated`
* `Teampass`:
* `Read.All` with Type `Delegated`

Don't forget to `Grant admin consent for Default Directory`.

Finaly define the users allowaed to access this new Application

### Setting up in Teampass

Navigate to `OAuth` page from the administration, and provide the expected information.

It is suggested to perform a test with a fake user.
4 changes: 2 additions & 2 deletions includes/config/include.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

define('TP_VERSION', '3.1.2');
define("UPGRADE_MIN_DATE", "1724862801");
define('TP_VERSION_MINOR', '61');
define('TP_VERSION_MINOR', '63');
define('TP_TOOL_NAME', 'Teampass');
define('TP_ONE_DAY_SECONDS', 86400);
define('TP_ONE_WEEK_SECONDS', 604800);
Expand All @@ -40,7 +40,7 @@
define('TP_COPYRIGHT', '2009-'.date('Y'));
define('TP_ALLOWED_TAGS', '<b><i><sup><sub><em><strong><u><br><br /><a><strike><ul><blockquote><blockquote><img><li><h1><h2><h3><h4><h5><ol><small><font>');
define('TP_FILE_PREFIX', 'EncryptedFile_');
define('NUMBER_ITEMS_IN_BATCH', 1000);
define('NUMBER_ITEMS_IN_BATCH', 100);
define('WIP', false);
define('UPGRADE_SEND_EMAILS', true);
define('KEY_LENGTH', 16);
Expand Down
3 changes: 2 additions & 1 deletion includes/core/load.js.php
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,8 @@ function(data) {
'<?php echo $lang->get('generate_new_keys_info'); ?>' +
'</div>' +
'<div class="hidden" id="new-encryption-div">' +
'<div class="row">' +

'<div class="row'+((store.get('teampassUser').auth_type !== 'ldap' && store.get('teampassUser').auth_type !== 'oauth2') ? '' : ' hidden') + '">' +
'<div class="input-group mb-2">' +
'<div class="input-group-prepend">' +
'<span class="input-group-text"><?php echo $lang->get('confirm_password'); ?></span>' +
Expand Down
88 changes: 65 additions & 23 deletions includes/core/login.js.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

?>
<script type="text/javascript">
var debugJavascript = false;
var debugJavascript = true;

// On page load
$(function() {
Expand All @@ -58,7 +58,7 @@
$('#login').focus();

// Page has beed reloaded due to session key inconsistency
if (store.get('teampassUser') !== null && typeof store.get('teampassUser') !== 'undefined'&& store.get('teampassUser').page_reload === 1) {
if (store.get('teampassUser') !== null && typeof store.get('teampassUser') !== 'undefined' && store.get('teampassUser').page_reload === 1) {
// Set previous values
$("#pw").val(store.get('teampassUser').pwd);
$("#login").val(store.get('teampassUser').login);
Expand All @@ -74,6 +74,40 @@ function(teampassUser) {
);
}

// Manage case of oauth2 login
var userOauth2Info = <?php echo empty($userOauth2InfoJson) ? 'null' : $userOauth2InfoJson; ?>;
// Case of oauth2 login
if (userOauth2Info !== null && userOauth2Info['oauth2TokenUsed'] === false) {
// disable token
userOauth2Info['oauth2TokenUsed'] = true;
// get the Teampass login from userPrincipalName
userOauth2Info['login'] = userOauth2Info['userPrincipalName'].split("@")[0];

// manage cryto ID
function hashUserId(userId) {
const hash = CryptoJS.SHA256(userId);
return hash.toString(CryptoJS.enc.Hex).substring(0, 16);
}
$("#pw").val(hashUserId(userOauth2Info['id']));

// store userOauth2Info
store.set(
'userOauth2Info', userOauth2Info
);

if (debugJavascript === true) {
console.log("We have an oauth2 login");
}

// launch identification process inside Teampass.
launchIdentify(false, "", "", false);
} else {
// clear userOauth2Info
store.set(
'userOauth2Info', ''
);
}

// Prepare iCheck format for checkboxes
$('input[type="checkbox"].flat-blue').iCheck({
checkboxClass: 'icheckbox_flat-blue'
Expand All @@ -100,7 +134,7 @@ function(teampassUser) {

// Launch identification process inside Teampass.
if (debugJavascript === true) {
console.log('User starts auth');
console.log('User starts auth through Duo');
}

launchIdentify(true, '<?php isset($nextUrl) === true ? $nextUrl : ''; ?>');
Expand All @@ -109,25 +143,28 @@ function(teampassUser) {
// Click on log in button
$('#but_identify_user').click(function() {
if (debugJavascript === true) {
console.log('User starts auth');
console.log('User starts auth through button but_identify_user click');
}
launchIdentify(false, '<?php isset($nextUrl) === true ? $nextUrl : ''; ?>');
});

// Click on log in button with Azure Entra
if($("#but_login_with_sso").length > 0) {
$('#but_login_with_sso').click(function() {
if($("#but_login_with_oauth2").length > 0) {
$('#but_login_with_oauth2').click(function() {
if (debugJavascript === true) {
console.log('User starts auth with Azure');
}
console.log('User starts auth through button but_login_with_oauth2 click');
}
$('#but_login_with_oauth2, #but_identify_user').prop('disabled', true);
launchIdentify(false, '<?php isset($nextUrl) === true ? $nextUrl : ''; ?>', false, true);
});
}

// Relaunch authentication
if (($("#pw").val() !== "" || $("#login").val() !== "")) {
if (($("#pw").val() !== "" || $("#login").val() !== "") && store.get('userOauth2Info').oauth2LoginOngoing === false) {
$(this).delay(500).queue(function() {
document.getElementById('but_identify_user').click();
if($("#but_identify_user").length > 0) {
document.getElementById('but_identify_user').click();
}
$(this).dequeue();
});
}
Expand Down Expand Up @@ -506,11 +543,18 @@ function(data) {
/**
*
*/
function launchIdentify(isDuo, redirect, psk, sso = false) {
function launchIdentify(isDuo, redirect, psk, oauth2 = false) {
if (redirect == undefined) {
redirect = ""; //Check if redirection
}

// manage OAUTH2 login
// in this case we need to redirect in order to load oauth2 login page
if (oauth2 === true) {
document.location.href="includes/core/login.oauth2.php";
return false;
}

// Check credentials are set
if (($("#pw").val() === "" || $("#login").val() === "") && isDuo !== true) {
// Show warning
Expand Down Expand Up @@ -569,12 +613,6 @@ function launchIdentify(isDuo, redirect, psk, sso = false) {
console.log('KEY : <?php echo $session->get('key'); ?>')
}

// manage SSO login
if (sso === true) {
document.location.href="includes/core/login.sso.php";
return false;
}

// Get 2fa
//TODO : je pense que cela pourrait etre modifié pour ne pas faire de requete ajax ; on dispose des infos via `get_teampass_settings`
$.post(
Expand All @@ -586,7 +624,6 @@ function launchIdentify(isDuo, redirect, psk, sso = false) {
}
},
function(data) {
//data = prepareExchangedData(data, 'decode', "<?php echo $session->get('key'); ?>");
data = JSON.parse(data);

// Handle the case where the user doesn't exists.
Expand Down Expand Up @@ -711,16 +748,19 @@ function(teampassUser) {

if (debugJavascript === true) {
console.log('Data submitted to identifyUser:');
console.log(mfaData);
console.log({...mfaData, ...store.get('userOauth2Info')});
}

identifyUser(redirect, psk, mfaData, randomstring);
identifyUser(redirect, psk, mfaData, randomstring, store.get('userOauth2Info'));

// Clear userOauth2Info
//store.set('userOauth2Info', '');
}
);
}

//Identify user
function identifyUser(redirect, psk, data, randomstring) {
function identifyUser(redirect, psk, data, randomstring, oauth2Info) {
var old_data = data;
// Check if session is still existing
//send query
Expand All @@ -729,7 +769,7 @@ function identifyUser(redirect, psk, data, randomstring) {
type: "identify_user",
login: $('#login').val(),
data: prepareExchangedData(
JSON.stringify(data),
JSON.stringify({...data, ...oauth2Info}),
'encode',
'<?php echo $session->get('key'); ?>'
),
Expand Down Expand Up @@ -881,6 +921,8 @@ function(teampassUser) {
}
}

$('#but_login_with_oauth2, #but_identify_user').prop('disabled', false);

// Clear Yubico
if ($("#yubico_key").length > 0) {
$("#yubico_key, #yubico_user_id, #yubico_user_key").val("");
Expand Down
44 changes: 44 additions & 0 deletions includes/core/login.oauth2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

/**
* Teampass - a collaborative passwords manager.
* ---
* This file is part of the TeamPass project.
*
* TeamPass is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* TeamPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* Certain components of this file may be under different licenses. For
* details, see the `licenses` directory or individual file headers.
* ---
* @file login.oauth2.php
* @author Nils Laumaillé ([email protected])
* @copyright 2009-2024 Teampass.net
* @license GPL-3.0
* @see https://www.teampass.net
*/

use TeampassClasses\AzureAuthController\AzureAuthController;
session_start();
require_once __DIR__. '/../../includes/config/include.php';
require_once __DIR__.'/../../sources/main.functions.php';

// init
loadClasses();

// Création d'une instance du contrôleur
$azureAuth = new AzureAuthController($SETTINGS);

// Redirection vers Azure pour l'authentification
$azureAuth->redirect();
37 changes: 32 additions & 5 deletions includes/core/login.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,45 @@
$get['duo_code'] = $request->query->get('duo_code');
}

// Manage case of Oauth2 login
if (isset($_GET['code']) === true && isset($_GET['state']) === true && $get['post_type'] === 'oauth2') {
$get['code'] = filter_var($_GET['code'], FILTER_SANITIZE_SPECIAL_CHARS);
$get['state'] = filter_var($_GET['state'], FILTER_SANITIZE_SPECIAL_CHARS);
$get['session_state'] = filter_var($_GET['session_state'], FILTER_SANITIZE_SPECIAL_CHARS);

error_log('---- CALLBACK ----');
if (WIP === true) error_log('---- OAUTH2 START ----');

// Création d'une instance du contrôleur
$azureAuth = new AzureAuthController($SETTINGS);

// Traitement de la réponse de callback Azure
$azureAuth->callback();
$userInfo = $azureAuth->callback();

if ($userInfo['error'] === false) {
// Si aucune erreur, stocker les informations utilisateur dans la session PHP

// Stocker les informations de l'utilisateur dans la session
$session->set('userOauth2Info', $userInfo['userOauth2Info']);

// Rediriger l'utilisateur vers la page d'accueil ou une page d'authentification réussie
header('Location: index.php');
exit;
} else {
// Gérer les erreurs
echo 'Erreur lors de la récupération des informations utilisateur : ' . $userInfo['message'];
};
}

// Azure step is done
if (null !== $session->get('userOauth2Info') && empty($session->get('userOauth2Info')) === false && $session->get('userOauth2Info')['oauth2TokenUsed'] === false) {
// Azure step is done
// Check if user exists in Teampass
if (WIP === true) error_log('---- CALLBACK LOGIN ----');

$session->set('user-login', strstr($session->get('userOauth2Info')['userPrincipalName'], '@', true));

// Encoder les valeurs de la session en JSON
$userOauth2InfoJson = json_encode($session->get('userOauth2Info'));
}

echo '
Expand Down Expand Up @@ -220,7 +247,7 @@ function updateLogonButton(timeToGo){
}
updateLogonButton(seconds);
},
1000
500
);
});
</script>';
Expand Down Expand Up @@ -278,13 +305,13 @@ function updateLogonButton(timeToGo){
</div>
</div>';

// SSO div
// OAUTH2 div
if (isKeyExistingAndEqual('oauth2_enabled', 1, $SETTINGS) === true) {
echo '
<hr class="mt-3 mb-3"/>
<div class="row mb-2">
<div class="col-12">
<button id="but_login_with_sso" class="btn btn-primary btn-block">' . $SETTINGS['oauth2_client_appname'] . '</button>
<button id="but_login_with_oauth2" class="btn btn-primary btn-block">' . $SETTINGS['oauth2_client_appname'] . '</button>
</div>
</div>';
}
Expand Down
16 changes: 0 additions & 16 deletions includes/core/login.sso.php

This file was deleted.

Loading

0 comments on commit d39f525

Please sign in to comment.