From aa847ee3cdddff15d760121ebf5c11c93fab24c3 Mon Sep 17 00:00:00 2001
From: "florian.azizian" <florian.azizian@maarch.org>
Date: Wed, 2 Dec 2020 23:52:54 +0100
Subject: [PATCH] FEAT #14566 TIME 6 INCA + Kerberos authentication

---
 lang/en.json                                  |  4 +++
 lang/fr.json                                  |  4 +++
 sql/2010.sql                                  |  3 ++
 sql/structure.sql                             |  1 +
 .../controllers/ConfigurationController.php   | 13 +++++---
 .../controllers/AuthenticationController.php  | 30 +++++++++++++++----
 .../connection/connection.component.html      |  5 ++--
 .../connection/connection.component.ts        |  5 ++--
 src/frontend/app/login/login.component.html   |  4 +++
 src/frontend/app/login/login.component.ts     |  9 ++++++
 src/frontend/app/service/auth.guard.ts        |  6 ++++
 .../app/sidebar/sidebar.component.html        |  2 +-
 12 files changed, 71 insertions(+), 15 deletions(-)

diff --git a/lang/en.json b/lang/en.json
index 195af096cd..60169e99bf 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -191,6 +191,8 @@
         "yes": "Yes",
         "no": "No",
         "ldapEnabled": "LDAP enabled",
+        "kerberosEnabled": "Kerberos enabled",
+		"x509Enabled": "Certificate x509 enbaled",
         "userCreation": "Add a user",
         "manage_ldap_configurations": "Ldap",
         "manage_ldap_configurationsDesc": "Add / Update / Delete ldap entries in order to authenticate users.",
@@ -211,6 +213,8 @@
         "manage_connectionsDesc": "Manage all available connections modes in application.",
         "defaultConnection": "Default",
         "ldapConnection": "Ldap",
+        "kerberosConnection" : "Kerberos",
+		"x509Connection" : "Certificate x509",
         "connectionMode": "Connection mode",
         "connectionModeUpdated": "Connection mode updated",
         "changeConnectionWarn": "This cause a change in login mechanics for users !",
diff --git a/lang/fr.json b/lang/fr.json
index f58f603500..8230de8272 100755
--- a/lang/fr.json
+++ b/lang/fr.json
@@ -191,6 +191,8 @@
 		"yes": "Oui",
 		"no": "Non",
 		"ldapEnabled": "LDAP activé",
+		"kerberosEnabled": "Kerberos activé",
+		"x509Enabled": "Certificat x509 activé",
 		"userCreation": "Ajouter un utilisateur",
 		"manage_ldap_configurations": "Annuaires ldap",
 		"manage_ldap_configurationsAlt": "Annuaire(s) ldap",
@@ -212,6 +214,8 @@
 		"manage_connectionsDesc" : "Administrer les différents modes de connexion disponibles de l'application.",
 		"defaultConnection" : "Standard",
 		"ldapConnection" : "Ldap",
+		"kerberosConnection" : "Kerberos",
+		"x509Connection" : "Certificat x509",
 		"connectionMode" : "Mode de connexion",
 		"connectionModeUpdated" : "Mode de connexion mis à jour",
 		"changeConnectionWarn" : "Cela modifie le mode de connexion à l'application !",
diff --git a/sql/2010.sql b/sql/2010.sql
index a69d8c9178..619d079072 100755
--- a/sql/2010.sql
+++ b/sql/2010.sql
@@ -54,6 +54,9 @@ END$$;
 ALTER TABLE users DROP COLUMN IF EXISTS signature_modes;
 ALTER TABLE users ADD COLUMN signature_modes jsonb DEFAULT '["stamp"]';
 
+ALTER TABLE users DROP COLUMN IF EXISTS x509_fingerprint;
+ALTER TABLE users ADD COLUMN x509_fingerprint text;
+
 ALTER TABLE workflows DROP COLUMN IF EXISTS signature_positions;
 ALTER TABLE workflows ADD COLUMN signature_positions jsonb DEFAULT '[]';
 
diff --git a/sql/structure.sql b/sql/structure.sql
index c0974338ca..4cb13347c5 100755
--- a/sql/structure.sql
+++ b/sql/structure.sql
@@ -213,6 +213,7 @@ CREATE TABLE users
   failed_authentication INTEGER DEFAULT 0,
   locked_until TIMESTAMP without time zone,
   signature_modes jsonb DEFAULT '["stamp"]',
+  x509_fingerprint text,
   CONSTRAINT users_pkey PRIMARY KEY (id),
   CONSTRAINT users_login_key UNIQUE (login)
 )
diff --git a/src/app/configuration/controllers/ConfigurationController.php b/src/app/configuration/controllers/ConfigurationController.php
index 2076b45f5c..4633b741d6 100755
--- a/src/app/configuration/controllers/ConfigurationController.php
+++ b/src/app/configuration/controllers/ConfigurationController.php
@@ -24,7 +24,7 @@ use SrcCore\models\AuthenticationModel;
 
 class ConfigurationController
 {
-    const CONNECTION_MODES  = ['default', 'ldap'];
+    const CONNECTION_MODES  = ['default', 'ldap', 'kerberos', 'x509'];
 
     public function get(Request $request, Response $response)
     {
@@ -45,10 +45,15 @@ class ConfigurationController
 
         $configurations = ConfigurationModel::getByIdentifier(['identifier' => $queryParams['identifier'], 'select' => $select]);
         if ($queryParams['identifier'] == 'connection') {
-            $ldapConfigurations = ConfigurationModel::getByIdentifier(['identifier' => 'ldapServer', 'select' => [1]]);
-            $configurations = $configurations[0];
+            $ldapConfigurations      = ConfigurationModel::getByIdentifier(['identifier' => 'ldapServer', 'select' => [1]]);
+            $configurations          = $configurations[0];
             $configurations['value'] = json_decode($configurations['value']);
-            $configurations['availableConnections'] = [['id' => 'default', 'allowed' => true], ['id' => 'ldap', 'allowed' => !empty($ldapConfigurations)]];
+            $configurations['availableConnections'] = [
+                ['id' => 'default',  'allowed' => true],
+                ['id' => 'kerberos', 'allowed' => true],
+                ['id' => 'x509',     'allowed' => true],
+                ['id' => 'ldap',     'allowed' => !empty($ldapConfigurations)]
+            ];
         }
 
         return $response->withJson(['configurations' => $configurations]);
diff --git a/src/core/controllers/AuthenticationController.php b/src/core/controllers/AuthenticationController.php
index 84c4b7c835..46b3017b61 100755
--- a/src/core/controllers/AuthenticationController.php
+++ b/src/core/controllers/AuthenticationController.php
@@ -88,14 +88,15 @@ class AuthenticationController
     {
         $body = $request->getParsedBody();
 
-        $check = Validator::stringType()->notEmpty()->validate($body['login']);
-        $check = $check && Validator::stringType()->notEmpty()->validate($body['password']);
-        if (!$check) {
-            return $response->withStatus(400)->withJson(['errors' => 'Bad Request']);
+        $connection = ConfigurationModel::getConnection();
+        if (in_array($connection, ['default', 'ldap'])) {
+            if (!Validator::stringType()->notEmpty()->validate($body['login']) || !Validator::stringType()->notEmpty()->validate($body['password'])) {
+                return $response->withStatus(400)->withJson(['errors' => 'Bad Request']);
+            }
         }
 
         $login = strtolower($body['login']);
-        $connection = ConfigurationModel::getConnection();
+        
         if ($connection == 'ldap') {
             $ldapConfigurations = ConfigurationModel::getByIdentifier(['identifier' => 'ldapServer', 'select' => ['value']]);
             if (empty($ldapConfigurations)) {
@@ -131,6 +132,25 @@ class AuthenticationController
             if (empty($authenticated) && !empty($error) && $error != 'Invalid credentials') {
                 return $response->withStatus(400)->withJson(['errors' => $error]);
             }
+        } elseif ($connection == 'x509') {
+            if (!empty($_SERVER['SSL_CLIENT_CERT'])) {
+                $x509Fingerprint = openssl_x509_fingerprint($_SERVER["SSL_CLIENT_CERT"]);
+                $user = UserModel::get(['select' => ['login'], 'where' => ['x509_fingerprint = ?'], 'data' => [strtoupper(wordwrap($x509Fingerprint, 2, " ", true))]]);
+                if (empty($user)) {
+                    return $response->withStatus(401)->withJson(['errors' => 'Authentication unauthorized']);
+                }
+                $login = $user[0]['login'];
+            } else {
+                return $response->withStatus(401)->withJson(['errors' => 'No certificate detected']);
+            }
+            $authenticated = true;
+        } elseif ($connection == 'kerberos') {
+            if (!empty($_SERVER['REMOTE_USER']) && $_SERVER['AUTH_TYPE'] == 'Negotiate') {
+                $login = strtolower($_SERVER['REMOTE_USER']);
+            } else {
+                return $response->withStatus(401)->withJson(['errors' => 'No identifier detected for kerberos']);
+            }
+            $authenticated = true;
         } else {
             $authenticated = AuthenticationModel::authentication(['login' => $login, 'password' => $body['password']]);
         }
diff --git a/src/frontend/app/administration/connection/connection.component.html b/src/frontend/app/administration/connection/connection.component.html
index 060a961b80..850c2668ec 100644
--- a/src/frontend/app/administration/connection/connection.component.html
+++ b/src/frontend/app/administration/connection/connection.component.html
@@ -9,8 +9,9 @@
 <ion-content #mainContent>
     <ion-item>
         <ion-label color="secondary">{{'lang.connectionMode' | translate}}</ion-label>
-        <ion-select *ngIf="currentConnection" [value]="currentConnection.id" cancelText="Annuler">
-            <ion-select-option *ngFor="let connection of connectionList" [value]="connection.id">
+        <ion-select *ngIf="currentConnection" [value]="currentConnection.id" cancelText="{{'lang.cancel' | translate}}" (ionChange)="changeConnection($event.detail.value)">
+            <ion-select-option *ngFor="let connection of connectionList" [value]="connection.id" 
+               [disabled]="!connection.allowed">
                 {{'lang.' + connection.id + 'Connection' | translate}}</ion-select-option>
         </ion-select>
     </ion-item>
diff --git a/src/frontend/app/administration/connection/connection.component.ts b/src/frontend/app/administration/connection/connection.component.ts
index e8cb938cc8..2fac384a65 100644
--- a/src/frontend/app/administration/connection/connection.component.ts
+++ b/src/frontend/app/administration/connection/connection.component.ts
@@ -77,14 +77,13 @@ export class ConnectionComponent implements OnInit {
         dialogRef.afterClosed().subscribe(result => {
             if (result === 'yes') {
                 this.loading = true;
-                this.http.patch('../rest/configurations/' + this.id, { label: this.label, value: connection.id })
+                this.http.patch('../rest/configurations/' + this.id, { label: this.label, value: connection })
                     .pipe(
                         finalize(() => this.loading = false)
                     )
                     .subscribe({
                         next: () => {
-                            this.authService.authMode = connection.id;
-                            this.currentConnection = connection;
+                            this.authService.authMode = connection;
                             this.notificationService.success('lang.connectionModeUpdated');
                         },
                     });
diff --git a/src/frontend/app/login/login.component.html b/src/frontend/app/login/login.component.html
index 812c73c9ff..059b878088 100644
--- a/src/frontend/app/login/login.component.html
+++ b/src/frontend/app/login/login.component.html
@@ -15,6 +15,10 @@
                     class="forgot-password">{{'lang.forgotPassword' | translate}}</a>
                 <a href="" *ngIf="authService.authMode === 'ldap'"
                     class="forgot-password">{{'lang.ldapEnabled' | translate}}</a>
+                <a href="" *ngIf="authService.authMode === 'kerberos'"
+                    class="forgot-password">{{'lang.kerberosEnabled' | translate}}</a>
+                <a href="" *ngIf="authService.authMode === 'x509'"
+                    class="forgot-password">{{'lang.x509Enabled' | translate}}</a>  
                 <ion-button type="submit" expand="block" [disabled]="loginForm.invalid || loading">{{'lang.connect' | translate}}
                 </ion-button>
             </form>
diff --git a/src/frontend/app/login/login.component.ts b/src/frontend/app/login/login.component.ts
index 04d80d673b..efc1a8650f 100644
--- a/src/frontend/app/login/login.component.ts
+++ b/src/frontend/app/login/login.component.ts
@@ -58,6 +58,7 @@ export class LoginComponent implements OnInit, AfterViewInit {
         setTimeout(() => {
             this.showForm = true;
             this.fixAutoFill();
+            this.initConnection();
         }, 500);
     }
 
@@ -100,4 +101,12 @@ export class LoginComponent implements OnInit, AfterViewInit {
             )
             .subscribe();
     }
+
+    initConnection() {
+        if (['kerberos', 'x509'].indexOf(this.authService.authMode) > -1) {
+            this.loginForm.disable();
+            this.loginForm.setValidators(null);
+            this.onSubmit();
+        }
+    }
 }
diff --git a/src/frontend/app/service/auth.guard.ts b/src/frontend/app/service/auth.guard.ts
index 29d692715d..820b09254d 100644
--- a/src/frontend/app/service/auth.guard.ts
+++ b/src/frontend/app/service/auth.guard.ts
@@ -34,6 +34,12 @@ export class AuthGuard implements CanActivate {
                 this.router.navigate(['/home']);
                 return false;
             } else {
+                this.http.get('../rest/authenticationInformations').pipe(
+                    map((data: any) => {
+                        this.authService.authMode = data.connection;
+                        this.authService.changeKey = data.changeKey;
+                    })
+                ). subscribe();
                 return true;
             }
         } else {
diff --git a/src/frontend/app/sidebar/sidebar.component.html b/src/frontend/app/sidebar/sidebar.component.html
index a86094a17d..d64fac48a2 100755
--- a/src/frontend/app/sidebar/sidebar.component.html
+++ b/src/frontend/app/sidebar/sidebar.component.html
@@ -10,7 +10,7 @@
 <ion-header [translucent]="true">
     <ion-toolbar color="primary">
         <ion-buttons slot="start">
-            <ion-button (click)="authService.logout()" [title]="'lang.logout' | translate">
+            <ion-button (click)="authService.logout()" [title]="'lang.logout' | translate" *ngIf="authService.authMode !== 'x509' && authService.authMode !== 'kerberos'">
                 <ion-icon slot="icon-only" name="power"></ion-icon>
             </ion-button>
             <ion-button *ngIf="!isAdminRoute() && authService.user.administrativePrivileges.length > 0" (click)="openAdmin()" [title]="'lang.administration' | translate">
-- 
GitLab