diff --git a/CHANGELOG.md b/CHANGELOG.md
index a4d0f582d4aa88849532df7fc7799d85b497d7dd..3eb4d6417cec6278e06f7e49881d844e104a2583 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,8 +12,21 @@
 
 **💥 Breaking:**
 **⚠️ Deprecation:**
+
+### 🔨 Internal / Developers
+- No modifications / Keep major and minors versions in sync with all parts of the project
 -->
 
+## 3.0.0 (?)
+
+### ✨ Feature
+- Login to Dojo app via Gitlab OAuth
+- User access type management
+    - **student**: default access type for new users
+    - **teachingStaff**: student access + possibility of creating assignment
+    - **admin**: teachingStaff access + access to user access management
+
+
 ## 2.2.0 (2023-10-16)
 
 ### ✨ Feature
diff --git a/NodeApp/.env.vault b/NodeApp/.env.vault
index 08c024706a6acbd81885bf354454c5ddc6d6c4ea..e6e7181907e0b3b11f2a65563dd356f6db2f4ea4 100644
--- a/NodeApp/.env.vault
+++ b/NodeApp/.env.vault
@@ -4,8 +4,8 @@
 #/--------------------------------------------------/
 
 # development
-DOTENV_VAULT_DEVELOPMENT="FvCrekiDgv/1bvov01DTRTEdipq7tduoYf/cAjX3+slE4zZSVG5S7AuQ2bXf4gzSHrfxjsx2kTl5ild1l+pSQGiDrLqdj0EigMleCya/xRiEDuVBeGRLZLJfWS/AGYi31/sNxtCT2FBcYoEZ+6KF0pq7jyMNTwaFFoSZgTBd+kk3GG1iF36pxt+lGHyeI6TOQQtFFgyoVzxLLZeBQp0V2OJAltVvXnCHzgKyY4zyHb5teyNVEKa2HOf5UivnWJqk89HSD8c+ijvS8Jq8JRu7nzI81JJQwGmU3MNUyRkt+jVJxv0H67YU5Cw18Fqa3NEb8K9mDeHJImwfU2HGhv8WZzqWEXKC/so1hjO6qWOHxwWPqko5YYNPOQw0vPwEzXZPcY/OSH70gHYqkJl3zOA1GwdHY1jBmwdPWOu6GSekxPED8gDrtW6x0TsvdSfLGRODDTIXJVCKM3xkkkV7oU+4hO743HwrVQxvyWrLQucPFSMQVEe+XA0kUddNGcgj9A3VfpWJwm2NycQXqYWuOEX0Q3qyLVi75KvZsPr8vBR3EcZfWtYGfC/2a8rl+kAZ9w8wpBoca6F8nnFkh7deB+/fV5uMRSDiEHI+aJufDFcOXq4J+uljQRDlCo8trrRkcqkYLJWg/ctvDWBG7A6ODgWLk31ycCryj/w297UsE7mew84uA5eCFQIbO3PT9MIKMy0hK3pFkoQq3KhL9S9/dl8VtvvyfNu1zfPyNl6ckiBJz0zG7zIV5sxGUcSdClUBluaQBxdU1kA2kkoAYrPTX56XaC9ZLx9VvbloWRAJxMwxnQV6w1vxWxRgtV3tzlZBK2JyWtEu+cykbnLJM4cfhP/7bysZqnidOuwv7cZy33plIFqzXxOiujCxpShI3IoALnx+nRo/vS4QvLJwU9Lh5QsSuKzHXoheobCpqgZM2aflNPmFdUPAgCqXJ5caEjRSdqlGf/lFE84qrwl3XB8rU4Emn1jIK7SwlcNbk4fVq6BccOIzS0tZzB4EPZNg6YYO8jfRNVhr2On8WOAwtVH9Jikvq2ffD88GQMZu+d/YDLwAmgg4eIjQscNHPvERyjlhyKE5"
+DOTENV_VAULT_DEVELOPMENT="Dp7/4qdTYrrd++/Ny+nykGpmkSKB8DCqhJkuX9/ZQkDB+cvsX5wRTgXBnxA4aIp+HX7QZNnKRKbU3kkzwa3sLHqu58E4Ns1gNGxSYeConwM+ebopPujewxhZbnQ8CxVqMA4cG/GB556yBDtbYYngw2EPdxFlAvLinD5MQbsJvpzDDUiPD9S6bxM20ciMmtsIcV+NI9lInwINIS8vLVWaXVdulnvB4SwVlLiEgfGfiMt2L2YahOG76AnACyL+xKC/QCmAhc3tR6nn6xPdovY3TDWOJe/25Qg2kgm1EvY5ry5r0nskxYPxCe/sIIqhIhE5rvp+uvCCG81Ilec1+RSA7EP7vJ0VJ8zOrurYZn09cZSkrLC+EYuQtlRXk0dBTALfuLWnqpwYN3ADC16YMEc1nI7VtPFwDBnO4cUHikot8XXXNpzsQRKE7S2kt5Cl3YOkO+JYIvNSbqc4OEiWEPZHJp0bNhXPaen0sqACD13GFA5WwaNR+bF9JZKYEZk9AJNp4pHYfuYDbCnTIfxXQ7atO9o90RYZynmX2BtbWCSmj2nWRc2UNfM+P7uZnGWxzrCAY3lb0P0BFrPMBiCzHXqUeOWjHc8s2RU55V/P0JNKEL4ePww9lCAq7dWHQcVtPQCMNU73GgJWtSbmXLw3WRXPtu+Ewe8o+zXuQYd4rUaieyhI36+A50lMEfvbdEpxoqd+guOCzLEo7MDXjNrDnaki2iIs1LHXu0X2p4qycfMHy0xs7lU/2maiUyZwEs3Uyrima1U8SofwPB+akpyfi3qTaIkV73yZWP4tdIEZfCz/lpM+uhyiD5g/YLOU+VoMRy+loUn4PyMIc3dDKLox06fbG8MJPS10goYTwHmsK9K2ywgVJmfQr4JVEY4rsgv22/IKfse1bPF12MCIxUSAKBzQA1rzRSkI8BUtU7cphVX50A3oOJeJe+t5/SdKTF4HwIE895rb2u4YceJt5UYKjxxTibmx1q9qzrLeeUbGemO9vvCrkxzSt7OUvMKR52GEwakEO3/+i+0G35SVBKoghRKVtMMvNIp9a4KnOogx5K0AErkbIYgXXoFH5gsiXU5oUBsU4Pz0QPAxCOvA+rLwDsssEPPmO0Yn1cMMb/aBdQl5AtLTY4GG8u9UvTZSo1Ly6z6Q6HxbeJsazfvEJuySSD/ckExRJoS817JvgpeKJViONN3td7qdtxZwMlP+AmSECv6uGUHbphkox17PE5FAN+lFaDyOGR+4VJoTPaeGds4CnDYDBLfYhaWktkvQbierlvj15HuhffP+IeziHd/YkpFtp9yLNRjjCwouCkgTnpo0CqI9V8uXW0aMPZXV6HEsj46sx2no6rWTkCj5u6SUufx/iCLLwCk6nOzW3ftbzP7vO3qTIPuWJ6sdYPSgWw6PyXhwxp3OEiV/jCpbq85pYQbS2YH4wDi9sZCtweud0nctPf+EnW/Imgy49/xuw7HEmay9okwUSsfr/Pw9p78GEJTm/eF9BIlxvly4aDCrYtqQGqOhRqNQICAd0RTw0z7hqAjrCKzhdQt79YSHCVvKy3tE01XDtgpiUL78HVRAYfdYGp1of1fPIpLzgnwOkER5rgGltYNVaJjvHXisyrL4+v4ww5NP2spxGsGEGUuQZes/YkDjafP026x8ZLusWNB1K96+REESXLOWnz7D/QYinZ9MP8f3CGF9ARFnYv9dKJuUIsAPPbZxAd4R9KtMHja5izkg37PHrRs/9cSD2gghu2+S4EYJ68mqGs1b6ao0fCYlcy/m+0U="
 
 # production
-DOTENV_VAULT_PRODUCTION="XJI0mhrbeefm0pw+Ii2qnr8DLtnxVYRAt+jdzIudrQmrepJBuj7ujWT3+e4Fydw5zgqQeSZHLolSNV/hCh97cNmTd4+vEipb6pfoEPlTDoiv869kcOW9oJ2St5RcWK4ZtTTlJNXqD9AWp5ST+Ox83SUsGjZpqTdz7pN9YnnlnoWaCDxnWxPI25CqKfwI66ALvbZ3/GYplT8nWC9cVll9UnwgpFF8ol+AXO57cccLz1dzEjGI4bODbrjhxO9ZkgNbVYHpemY10hV7BeVsSR5wwuxXI8B0cG4jhlXX6Uf/GnGSJ++4LsIraA7FnNmtBpxIXnsImU6G4j3ILJvtnjhl6aIL39zenlZep7ZeTQAtQPhHUmS/tF9xPBqFSUTghE+ZrLOHnqvoXl1/n4ynPCz20VzJtQ4MaoxaO4qOoktyMXU9c1YpqyG7qfLD3N2z6DK3jnIz8RhAhYimKpRb9QuH8gbKHu4BsQlNrl1VQz78sFo9XxtrrxLIOQgMNoANtqROfiVQdcpl4MdK/H3iPpPI/H2esovjjuL+h42tDLbjPq7H2ghjHzckK14ZwO20zyStK9hRY5fAfeO61v6n5EOrsFsjbO/6tIuyG0JQniUnDFWtyxFq78i8F/hom6UbJunHvyV69uSfqdST61vAU/yFtgBZ0WosTUQB7aBJ5nkRRfliZhYu/UCAJ/nJDtjnKgv0kbl0KmTMDTa3HeOfDpK6Yd53IrvPxgfaTkALB0yfAm6W9vGdV4675aLBIJ8A7PjdLz+Ydw6QmDWUROe+XGXGDKlQe2YGBdjT9fO9qEf7Hiv68xizdh3XYdkbr6cj8YDDI84ZpswoUjhVnzEb5erg1OIgHFAYtVV9jdmI8N+c426QcyEOXXSQI1/AM0rDKitdftZZahiaCRKtHDs1Zfnn+LulOKaJP7abgYlPYFkj7LpDVUaPZqo34uMbWatoltQpUbGIYGc="
+DOTENV_VAULT_PRODUCTION="eAPpmIk1d1HWEthoyl/LYayruvbFk/uJ7uotJLFO+a5DsD6pm6jImtbHfVy7v2AZ2ts37H0CK0H+zUpKEKbkqTeCkFxup2ogmhN+/grERHrjdhajxzOHzD7mHGLgWtI2ZOcrZYbpeB6pzbK/a1HG/jLqgHld93oq2J354u6GVbZcjYNQIAfDntlqKmrAaXTh9t5HpwiXmuAueKqsjoH6DGKVXC7UNeenP0OTOCYpYUlJlONba9ebmNPYwmM7ppnwS4ccOvtDdB42Enfd73XtS/ZzW93NnPP2kabINt3TcZdQtKHEd+YHAuRW+50bu1Wgbj1kyeWGRBWVe59wm7C6rhrTBjBRDBjxX7QMhznCIaTUbHFdx6by/O2p5XGzWSpzOlJwFfE4jfHOroxWnVIwN4hFUVu6g3eU+rvKTMtFpOSZ3FJlRiZtvgD7HqoOngEykFNgEEE07UGoOz91nKHia3msD/gzbAadgILB6G/GtuCSlUjW/rQyi8ujVTNxR8I09EKsjiVzRwp0uLGfqgUWEQPSncdkkRS0+82PSKrQXbOoLS+5WvnN95kVFWuHus+5RRup8wnO8CfJIX0TPxxJ9qWy3okku+fIuLbbIxLlxz6DtypwqqthzjaI21Qx+6qjr4vTE0Wc+uWH1csxFNcoo+CzBCt5lbb+QHFjXfmhITXBVz6Bbpt3F0e/yyr/xDhpIp8jo+VxsmOO6jHZd2L2IdUU3SPk+1d1HW0OkocS3s6Iya9CHyDD6tMDUgSdTDVayab6B1DWQYNcTo6a1WMcV/oRtynJnnYEsIEvU/WRT59syMsuoPzDFHTxgow4q1icNCkUHcHBKNKxYdrlLmCLuUwp1lggDyrAzFqNjcYVb/73K3mphi+njtDJdBq8sWrMGtjOScnrJ1rHmdmhT0ub2Qgwxgg6jZ2aKlZC2TFr41IkAtqDAI1moaV/OEsgBfIiDUusn6xKKrDxLiGM7vcQI0szd7Vkr2mfPWiQLOmhxKj11c+7QkUNF6QLQxVYEzwvHcB+RTusSlvJTXN9YfVjqrxBdvuV2UHk85zOYOkBKVIZcj3xojgnDkWVhHGnhfi8nn8d+ROIQIMhQ5izgai5PVO51MOcmgevMYj7L8Jf1HEuwD2ykwaS8KeewvTihEflY8YjOA6fjY70Q4Uqa0LIjsTNegeUPvvQ3E8s1w+4xrgRkA4uW1wRRs7NfoEt02cq2/ZBckXTjdXuuS2Xcdr6LFWEjp5iPlccZV7qBW7+AteROIRjEc/uzvhII5cKRLhgv3y2sehUXEdv04hrO5pKGStrwySZ93P2pnUk9UOdoNtCH5HMZ4jLGnb2hFDoP2o6b57khJZcanjpRvvhkWjpn9eMwjxWK+CX6n/uDaXeYBA9FZtSJyUAniXxNFBz/Zurisl74Bjl8CRCVaqq7ATPXeR+IsVd6B8Cs1QPWWuxU5Z1PCiot6kWTp69p45nHQMmUEdl5eUSATaG4PgwLlZ1vysB/O4J6npmYHF4IFAE8yLNbiWg7IuhlEFTYk9UgvUC+gLaymDTTtr/DqwjU3F/oFYkjR3/Go7L2s03VU3pU5PShua+AWg8Wfli1NQ9n3/rbwXAnsrS2J0qV6n3oFm9yv/0g/+RiOIko39UgJ1syZzbTpDRMtqJOg=="
 
diff --git a/NodeApp/package-lock.json b/NodeApp/package-lock.json
index 38eccb9553557d7eef62152401c803a1503e3983..bed60a32df6a7f752acf5f8c5e5d9541ad70c8b2 100644
--- a/NodeApp/package-lock.json
+++ b/NodeApp/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "dojo_cli",
-    "version": "2.2.0",
+    "version": "3.0.0",
     "lockfileVersion": 3,
     "requires": true,
     "packages": {
         "": {
             "name": "dojo_cli",
-            "version": "2.2.0",
+            "version": "3.0.0",
             "license": "AGPLv3",
             "dependencies": {
                 "ajv": "^8.12.0",
@@ -22,6 +22,7 @@
                 "inquirer": "^8.2.5",
                 "json5": "^2.2.3",
                 "jsonwebtoken": "^8.5.1",
+                "openurl": "^1.1.1",
                 "ora": "^5.4.1",
                 "tar-stream": "^3.1.6",
                 "winston": "^3.10.0",
@@ -35,6 +36,7 @@
                 "@types/inquirer": "^8.2.6",
                 "@types/jsonwebtoken": "^8.5.9",
                 "@types/node": "^18.17.2",
+                "@types/openurl": "^1.0.1",
                 "@types/tar-stream": "^2.2.2",
                 "dotenv-vault": "^1.25.0",
                 "genversion": "^3.1.1",
@@ -683,6 +685,15 @@
             "integrity": "sha512-0OVfGupTl3NBFr8+iXpfZ8NR7jfFO+P1Q+IO/q0wbo02wYkP5gy36phojeYWpLQ6WAMjl+VfmqUk2YbUfp0irA==",
             "dev": true
         },
+        "node_modules/@types/openurl": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/@types/openurl/-/openurl-1.0.1.tgz",
+            "integrity": "sha512-1XWIANTcgHenp3tboMdCiyzc2hBFfhf7Us2LHJ7X+kPiw648trTuV+lMsd0NUG3hv/44VM38r2vLYvtFpbc9sw==",
+            "dev": true,
+            "dependencies": {
+                "@types/node": "*"
+            }
+        },
         "node_modules/@types/tar-stream": {
             "version": "2.2.3",
             "resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-2.2.3.tgz",
@@ -2600,6 +2611,11 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
+        "node_modules/openurl": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz",
+            "integrity": "sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA=="
+        },
         "node_modules/ora": {
             "version": "5.4.1",
             "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
diff --git a/NodeApp/package.json b/NodeApp/package.json
index b4f8edfe6b9748cec7c7440185ce460496da54cd..ff0f2a74b32053f8e01a435960914e6872be964a 100644
--- a/NodeApp/package.json
+++ b/NodeApp/package.json
@@ -1,7 +1,7 @@
 {
     "name"           : "dojo_cli",
     "description"    : "CLI of the Dojo project",
-    "version"        : "2.2.0",
+    "version"        : "3.0.0",
     "license"        : "AGPLv3",
     "author"         : "Michaël Minelli <dojo@minelli.me>",
     "main"           : "dist/app.js",
@@ -45,6 +45,7 @@
         "inquirer"         : "^8.2.5",
         "json5"            : "^2.2.3",
         "jsonwebtoken"     : "^8.5.1",
+        "openurl"          : "^1.1.1",
         "ora"              : "^5.4.1",
         "tar-stream"       : "^3.1.6",
         "winston"          : "^3.10.0",
@@ -55,6 +56,7 @@
         "@types/inquirer"    : "^8.2.6",
         "@types/jsonwebtoken": "^8.5.9",
         "@types/node"        : "^18.17.2",
+        "@types/openurl"     : "^1.0.1",
         "@types/tar-stream"  : "^2.2.2",
         "dotenv-vault"       : "^1.25.0",
         "genversion"         : "^3.1.1",
diff --git a/NodeApp/src/app.ts b/NodeApp/src/app.ts
index 634f276293e465b7a1e2fd599d502d6aeb371586..236e2d79bff4df0a07333e01c129ca008994c0cc 100644
--- a/NodeApp/src/app.ts
+++ b/NodeApp/src/app.ts
@@ -11,10 +11,8 @@ require('./shared/helpers/TypeScriptExtensions'); // ATTENTION : This line MUST
 
 import CommanderApp from './commander/CommanderApp';
 import HttpManager  from './managers/HttpManager';
-import LocalConfig  from './config/LocalConfig';
 
 
-LocalConfig.loadConfig();
 HttpManager.registerAxiosInterceptor();
 
 new CommanderApp();
\ No newline at end of file
diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts
index 0b28a1c7ff9302c5b0416043e8bc3421cbbfb96b..7a5bea4403c7e963a948ccc04c945ee17e35fa4d 100644
--- a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts
+++ b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts
@@ -61,14 +61,15 @@ class AssignmentCreateCommand extends CommanderCommand {
 
         // Create the assignment
         {
-            console.log(chalk.cyan('Please wait while we are creating the assignment...'));
+            console.log(chalk.cyan('Please wait while we are creating the assignment (approximately 10 seconds)...'));
 
             try {
                 const assignment: Assignment = await DojoBackendManager.createAssignment(options.name, members, templateIdOrNamespace);
 
                 const oraInfo = (message: string) => {
                     ora({
-                            text: message, indent: 4
+                            text  : message,
+                            indent: 4
                         }).start().info();
                 };
 
diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts
index 41593c08b1ac4967b10ebdd62d953016af74e907..acc361c3ffd9e01d2adf65a55bc187c7180720c5 100644
--- a/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts
+++ b/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts
@@ -62,7 +62,7 @@ abstract class AssignmentPublishUnpublishCommandBase extends CommanderCommand {
                                                                   text  : 'Checking accesses',
                                                                   indent: 8
                                                               }).start();
-            if ( !assignment.staff.some(staff => staff.gitlabId === SessionManager.profile?.gitlabId) ) {
+            if ( !assignment.staff.some(staff => staff.id === SessionManager.profile?.id) ) {
                 assignmentCheckAccessSpinner.fail(`You are not in the staff of this assignment`);
                 return;
             }
diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts
index bb51c2e6ece83361bd4fa5bd011b8d2e5731f97b..5b547df82b9c26433806d861a5ae5f7da581a8c7 100644
--- a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts
+++ b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts
@@ -40,7 +40,8 @@ class ExerciseCreateCommand extends CommanderCommand {
 
             ora('Checking assignment:').start().info();
             const assignmentGetSpinner: ora.Ora = ora({
-                                                          text: 'Checking if assignment exists', indent: 4
+                                                          text  : 'Checking if assignment exists',
+                                                          indent: 4
                                                       }).start();
             assignment = await DojoBackendManager.getAssignment(options.assignment);
             if ( !assignment ) {
@@ -50,7 +51,8 @@ class ExerciseCreateCommand extends CommanderCommand {
             assignmentGetSpinner.succeed(`Assignment "${ options.assignment }" exists`);
 
             const assignmentPublishedSpinner: ora.Ora = ora({
-                                                                text: 'Checking if assignment is published', indent: 4
+                                                                text  : 'Checking if assignment is published',
+                                                                indent: 4
                                                             }).start();
             if ( !assignment.published ) {
                 assignmentPublishedSpinner.fail(`Assignment "${ assignment.name }" isn't published`);
@@ -61,14 +63,15 @@ class ExerciseCreateCommand extends CommanderCommand {
 
         //Create the exercise
         {
-            console.log(chalk.cyan('Please wait while we are creating the exercise...'));
+            console.log(chalk.cyan('Please wait while we are creating the exercise (approximately 10 seconds)...'));
 
             try {
                 const exercise: Exercise = await DojoBackendManager.createExercise((assignment as Assignment).name, members);
 
                 const oraInfo = (message: string) => {
                     ora({
-                            text: message, indent: 4
+                            text  : message,
+                            indent: 4
                         }).start().info();
                 };
 
diff --git a/NodeApp/src/commander/session/SessionCommand.ts b/NodeApp/src/commander/session/SessionCommand.ts
index 8ec16854a4bc15f6cdfb8534f57c28c2bef128c8..bbf0374ee9617ccc44e4463bd84554db2f8d4aa7 100644
--- a/NodeApp/src/commander/session/SessionCommand.ts
+++ b/NodeApp/src/commander/session/SessionCommand.ts
@@ -1,7 +1,7 @@
 import CommanderCommand     from '../CommanderCommand';
 import SessionTestCommand   from './subcommands/SessionTestCommand';
-import SessionAppCommand    from './subcommands/SessionAppCommand';
-import SessionGitlabCommand from './subcommands/SessionGitlabCommand';
+import SessionLoginCommand  from './subcommands/SessionLoginCommand';
+import SessionLogoutCommand from './subcommands/SessionLogoutCommand';
 
 
 class SessionCommand extends CommanderCommand {
@@ -13,8 +13,8 @@ class SessionCommand extends CommanderCommand {
     }
 
     protected defineSubCommands() {
-        SessionAppCommand.registerOnCommand(this.command);
-        SessionGitlabCommand.registerOnCommand(this.command);
+        SessionLoginCommand.registerOnCommand(this.command);
+        SessionLogoutCommand.registerOnCommand(this.command);
         SessionTestCommand.registerOnCommand(this.command);
     }
 
diff --git a/NodeApp/src/commander/session/subcommands/SessionAppCommand.ts b/NodeApp/src/commander/session/subcommands/SessionAppCommand.ts
deleted file mode 100644
index f8d179efead33b4652c0a994d8aed4cdc9e28e81..0000000000000000000000000000000000000000
--- a/NodeApp/src/commander/session/subcommands/SessionAppCommand.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import CommanderCommand        from '../../CommanderCommand';
-import SessionAppLoginCommand  from './application/SessionAppLoginCommand';
-import SessionAppLogoutCommand from './application/SessionAppLogoutCommand';
-
-
-class SessionAppCommand extends CommanderCommand {
-    protected commandName: string = 'application';
-
-    protected defineCommand() {
-        this.command
-        .description('manage application session');
-    }
-
-    protected defineSubCommands() {
-        SessionAppLoginCommand.registerOnCommand(this.command);
-        SessionAppLogoutCommand.registerOnCommand(this.command);
-    }
-
-    protected async commandAction(): Promise<void> { }
-}
-
-
-export default new SessionAppCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/session/subcommands/SessionGitlabCommand.ts b/NodeApp/src/commander/session/subcommands/SessionGitlabCommand.ts
deleted file mode 100644
index e80ceb264f1d16f458b1f4cb770dc82dd93da817..0000000000000000000000000000000000000000
--- a/NodeApp/src/commander/session/subcommands/SessionGitlabCommand.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import CommanderCommand           from '../../CommanderCommand';
-import SessionGitlabLoginCommand  from './gitlab/SessionGitlabLoginCommand';
-import SessionGitlabLogoutCommand from './gitlab/SessionGitlabLogoutCommand';
-
-
-class SessionGitlabCommand extends CommanderCommand {
-    protected commandName: string = 'gitlab';
-
-    protected defineCommand() {
-        this.command
-        .description('manage Gitlab session');
-    }
-
-    protected defineSubCommands() {
-        SessionGitlabLoginCommand.registerOnCommand(this.command);
-        SessionGitlabLogoutCommand.registerOnCommand(this.command);
-    }
-
-    protected async commandAction(): Promise<void> { }
-}
-
-
-export default new SessionGitlabCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/session/subcommands/SessionLoginCommand.ts b/NodeApp/src/commander/session/subcommands/SessionLoginCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..53610a2fab179d5577f373cce51f3633209464df
--- /dev/null
+++ b/NodeApp/src/commander/session/subcommands/SessionLoginCommand.ts
@@ -0,0 +1,27 @@
+import chalk            from 'chalk';
+import CommanderCommand from '../../CommanderCommand';
+import SessionManager   from '../../../managers/SessionManager';
+
+
+class SessionLoginCommand extends CommanderCommand {
+    protected commandName: string = 'login';
+
+    protected defineCommand() {
+        this.command
+        .description('login to Dojo')
+        .option('-c, --cli', 'proceed to the login in headless mode (do not try to open web browser).')
+        .action(this.commandAction.bind(this));
+    }
+
+    protected async commandAction(options: { cli: boolean }): Promise<void> {
+        try {
+            console.log(chalk.cyan('Please wait while we login you into Dojo...'));
+            await SessionManager.login(options.cli);
+        } catch ( error ) {
+
+        }
+    }
+}
+
+
+export default new SessionLoginCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/session/subcommands/application/SessionAppLogoutCommand.ts b/NodeApp/src/commander/session/subcommands/SessionLogoutCommand.ts
similarity index 63%
rename from NodeApp/src/commander/session/subcommands/application/SessionAppLogoutCommand.ts
rename to NodeApp/src/commander/session/subcommands/SessionLogoutCommand.ts
index 7d051a31a7ebf761ef5fd119b0fb1bdd4b8aa6d6..e0c7bb374c5138cd01ee3fd08db04d38e1e6472c 100644
--- a/NodeApp/src/commander/session/subcommands/application/SessionAppLogoutCommand.ts
+++ b/NodeApp/src/commander/session/subcommands/SessionLogoutCommand.ts
@@ -1,15 +1,15 @@
-import CommanderCommand from '../../../CommanderCommand';
+import CommanderCommand from '../../CommanderCommand';
 import inquirer         from 'inquirer';
-import SessionManager   from '../../../../managers/SessionManager';
 import ora              from 'ora';
+import SessionManager   from '../../../managers/SessionManager';
 
 
-class SessionAppLogoutCommand extends CommanderCommand {
+class SessionLogoutCommand extends CommanderCommand {
     protected commandName: string = 'logout';
 
     protected defineCommand() {
         this.command
-        .description('logout of the application')
+        .description('logout of Dojo')
         .option('-f, --force', 'attempt to logout without prompting for confirmation')
         .action(this.commandAction.bind(this));
     }
@@ -17,7 +17,10 @@ class SessionAppLogoutCommand extends CommanderCommand {
     protected async commandAction(options: any): Promise<void> {
         if ( !options.force ) {
             const confirm: boolean = (await inquirer.prompt({
-                                                                name: 'confirm', message: 'Are you sure?', type: 'confirm', default: false
+                                                                name   : 'confirm',
+                                                                message: 'Are you sure?',
+                                                                type   : 'confirm',
+                                                                default: false
                                                             })).confirm;
 
             if ( !confirm ) {
@@ -32,4 +35,4 @@ class SessionAppLogoutCommand extends CommanderCommand {
 }
 
 
-export default new SessionAppLogoutCommand();
\ No newline at end of file
+export default new SessionLogoutCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/session/subcommands/application/SessionAppLoginCommand.ts b/NodeApp/src/commander/session/subcommands/application/SessionAppLoginCommand.ts
deleted file mode 100644
index 88deb2e8b9a40cdc8d399393a81257ff327e4992..0000000000000000000000000000000000000000
--- a/NodeApp/src/commander/session/subcommands/application/SessionAppLoginCommand.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import chalk            from 'chalk';
-import CommanderCommand from '../../../CommanderCommand';
-import inquirer         from 'inquirer';
-import SessionManager   from '../../../../managers/SessionManager';
-
-
-class SessionAppLoginCommand extends CommanderCommand {
-    protected commandName: string = 'login';
-
-    protected defineCommand() {
-        this.command
-        .description('login into the application')
-        .requiredOption('-u, --user <string>', '[required] username to use when connecting to server')
-        .option('-p, --password <string>', 'password to use when connecting to server. If password is not given it\'s asked')
-        .action(this.commandAction.bind(this));
-    }
-
-    protected async commandAction(options: { user: string, password: string }): Promise<void> {
-        if ( !options.password ) {
-            options.password = (await inquirer.prompt({
-                                                          type: 'password', name: 'password', message: 'Please enter your password', mask: ''
-                                                      })).password;
-        }
-
-        console.log(chalk.cyan('Please wait while we are logging in you to Dojo...'));
-
-        await SessionManager.login(options.user, options.password);
-
-        SessionManager.checkPermissions(true, 4);
-    }
-}
-
-
-export default new SessionAppLoginCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/session/subcommands/gitlab/SessionGitlabLoginCommand.ts b/NodeApp/src/commander/session/subcommands/gitlab/SessionGitlabLoginCommand.ts
deleted file mode 100644
index fae91b6cf934f21e1c40a64f878c9a526b6f8241..0000000000000000000000000000000000000000
--- a/NodeApp/src/commander/session/subcommands/gitlab/SessionGitlabLoginCommand.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import chalk            from 'chalk';
-import CommanderCommand from '../../../CommanderCommand';
-import GitlabManager    from '../../../../managers/GitlabManager';
-import inquirer         from 'inquirer';
-
-
-class SessionGitlabLoginCommand extends CommanderCommand {
-    protected commandName: string = 'login';
-
-    protected defineCommand() {
-        this.command
-        .description('register the gitlab token')
-        .option('-t, --token <string>', 'personal access token from GitLab with api scope')
-        .action(this.commandAction.bind(this));
-    }
-
-    protected async commandAction(options: { token: string }): Promise<void> {
-        if ( !options.token ) {
-            options.token = (await inquirer.prompt({
-                                                       type: 'password', name: 'token', message: 'Please enter your gitlab token', mask: ''
-                                                   })).token;
-        }
-
-        console.log(chalk.cyan('Please wait while we are testing your Gitlab token...'));
-
-        GitlabManager.login(options.token);
-
-        await GitlabManager.testToken();
-    }
-}
-
-
-export default new SessionGitlabLoginCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/session/subcommands/gitlab/SessionGitlabLogoutCommand.ts b/NodeApp/src/commander/session/subcommands/gitlab/SessionGitlabLogoutCommand.ts
deleted file mode 100644
index 4d43d379fbbc185cb571cf1039460e1c620965dc..0000000000000000000000000000000000000000
--- a/NodeApp/src/commander/session/subcommands/gitlab/SessionGitlabLogoutCommand.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import CommanderCommand from '../../../CommanderCommand';
-import inquirer         from 'inquirer';
-import ora              from 'ora';
-import GitlabManager    from '../../../../managers/GitlabManager';
-
-
-class SessionGitlabLogoutCommand extends CommanderCommand {
-    protected commandName: string = 'logout';
-
-    protected defineCommand() {
-        this.command
-        .description('logout of Gitlab')
-        .option('-f, --force', 'attempt to logout without prompting for confirmation')
-        .action(this.commandAction.bind(this));
-    }
-
-    protected async commandAction(options: any): Promise<void> {
-        if ( !options.force ) {
-            const confirm: boolean = (await inquirer.prompt({
-                                                                name: 'confirm', message: 'Are you sure?', type: 'confirm', default: false
-                                                            })).confirm;
-
-            if ( !confirm ) {
-                return;
-            }
-        }
-
-        const spinner: ora.Ora = ora('Please wait while we are logout you from Gitlab...').start();
-        GitlabManager.logout();
-        spinner.succeed('You are now logged out from Gitlab.');
-    }
-}
-
-
-export default new SessionGitlabLogoutCommand();
\ No newline at end of file
diff --git a/NodeApp/src/config/Config.ts b/NodeApp/src/config/Config.ts
index d1935ab6335da38804364d3065639f65f1629882..15b13e064b562939785bb669f6a8ab6c52e63542 100644
--- a/NodeApp/src/config/Config.ts
+++ b/NodeApp/src/config/Config.ts
@@ -3,13 +3,23 @@ import getAppDataPath from 'appdata-path';
 
 class Config {
     public readonly localConfig: {
-        folder: string; file: string;
+        folder: string; sessionFile: string;
     };
 
     public readonly gitlab: {
         cliReleasePage: string
     };
 
+    public readonly login: {
+        server: {
+            port: number, route: string
+        }, gitlab: {
+            url: {
+                code: string
+            }
+        }
+    };
+
     public readonly folders: {
         defaultLocalExercise: string
     };
@@ -20,14 +30,26 @@ class Config {
 
     constructor() {
         this.localConfig = {
-            folder: getAppDataPath('DojoCLI'),
-            file  : process.env.LOCAL_CONFIG_FILE || ''
+            folder     : getAppDataPath('DojoCLI'),
+            sessionFile: process.env.LOCAL_CONFIG_FILE_SESSION || ''
         };
 
         this.gitlab = {
             cliReleasePage: process.env.GITLAB_CLI_RELEASE_PAGE || ''
         };
 
+        this.login = {
+            server: {
+                port : Number(process.env.LOGIN_SERVER_PORT || 30992),
+                route: process.env.LOGIN_SERVER_ROUTE || ''
+            },
+            gitlab: {
+                url: {
+                    code: process.env.LOGIN_GITLAB_URL_CODE || ''
+                }
+            }
+        };
+
         this.folders = {
             defaultLocalExercise: process.env.LOCAL_EXERCISE_DEFAULT_FOLDER || './'
         };
diff --git a/NodeApp/src/config/LocalConfig.ts b/NodeApp/src/config/LocalConfig.ts
deleted file mode 100644
index 656c0c6bc47dc684556b816178548989086c9313..0000000000000000000000000000000000000000
--- a/NodeApp/src/config/LocalConfig.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import * as fs             from 'fs';
-import SessionManager      from '../managers/SessionManager';
-import Config              from './Config';
-import LocalConfigKeys     from '../types/LocalConfigKeys';
-import GitlabManager       from '../managers/GitlabManager';
-import JSON5               from 'json5';
-import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig';
-
-
-class LocalConfig {
-    private get configPath(): string {
-        return `${ Config.localConfig.folder }/${ Config.localConfig.file }`;
-    }
-
-    private _config: { [key in LocalConfigKeys]?: any } = {};
-
-    loadConfig() {
-        if ( !fs.existsSync(this.configPath) ) {
-            fs.mkdirSync(Config.localConfig.folder, { recursive: true });
-
-            fs.writeFileSync(this.configPath, JSON5.stringify({}));
-        }
-
-        try {
-            this._config = JSON5.parse(fs.readFileSync(this.configPath).toString());
-
-            if ( LocalConfigKeys.API_TOKEN_ENV in this._config && ClientsSharedConfig.apiURL in this._config[LocalConfigKeys.API_TOKEN_ENV] ) {
-                SessionManager.token = this._config[LocalConfigKeys.API_TOKEN_ENV][ClientsSharedConfig.apiURL];
-            } else {
-                SessionManager.token = this._config[LocalConfigKeys.API_TOKEN];
-            }
-
-            GitlabManager.token = this._config[LocalConfigKeys.GITLAB_PERSONAL_TOKEN];
-        } catch ( error ) {
-            console.log(error);
-        }
-    }
-
-    updateConfig(key: LocalConfigKeys, value: any) {
-        let previousValue = (this._config as any)[key];
-        if ( key === LocalConfigKeys.API_TOKEN && (!(LocalConfigKeys.API_TOKEN_ENV in this._config) || !(ClientsSharedConfig.apiURL in this._config[LocalConfigKeys.API_TOKEN_ENV])) ) {
-            previousValue = null;
-        }
-
-        if ( previousValue === value ) {
-            return;
-        }
-
-        if ( key === LocalConfigKeys.API_TOKEN ) {
-            delete (this._config as any)[LocalConfigKeys.API_TOKEN];
-
-            if ( !(LocalConfigKeys.API_TOKEN_ENV in this._config) ) {
-                (this._config as any)[LocalConfigKeys.API_TOKEN_ENV] = {};
-            }
-
-            (this._config as any)[LocalConfigKeys.API_TOKEN_ENV][ClientsSharedConfig.apiURL] = value;
-        } else {
-            (this._config as any)[key] = value;
-        }
-
-        try {
-            fs.writeFileSync(this.configPath, JSON5.stringify(this._config, null, 4));
-        } catch ( error ) { }
-    }
-}
-
-
-export default new LocalConfig();
\ No newline at end of file
diff --git a/NodeApp/src/config/LocalConfigFile.ts b/NodeApp/src/config/LocalConfigFile.ts
new file mode 100644
index 0000000000000000000000000000000000000000..33cabe255728f5c990ceab23a4143fb0271877ed
--- /dev/null
+++ b/NodeApp/src/config/LocalConfigFile.ts
@@ -0,0 +1,57 @@
+import * as fs from 'fs';
+import Config  from './Config';
+import JSON5   from 'json5';
+
+
+class LocalConfigFile {
+    constructor(private filename: string) {}
+
+    private get configPath(): string {
+        return `${ Config.localConfig.folder }/${ this.filename }`;
+    }
+
+    private _config: { [key: string]: any } = {};
+
+    loadConfig() {
+        if ( !fs.existsSync(this.configPath) ) {
+            fs.mkdirSync(Config.localConfig.folder, { recursive: true });
+
+            fs.writeFileSync(this.configPath, JSON5.stringify({}));
+        }
+
+        try {
+            this._config = JSON5.parse(fs.readFileSync(this.configPath).toString());
+        } catch ( error ) {
+            console.log(error);
+        }
+    }
+
+    getParam(key: string): any | null {
+        const value = key in this._config ? this._config[key] : null;
+        if ( value === null ) {
+            return null;
+        } else if ( typeof value === 'object' ) {
+            return { ...value };
+        } else {
+            return value;
+        }
+    }
+
+    setParam(key: string, value: any): void {
+        let previousValue = this.getParam(key);
+        if ( JSON5.stringify(previousValue) === JSON5.stringify(value) ) {
+            return;
+        }
+
+        this._config[key] = value;
+
+        try {
+            fs.writeFileSync(this.configPath, JSON5.stringify(this._config, null, 4));
+        } catch ( error ) {
+            console.log(error);
+        }
+    }
+}
+
+
+export default LocalConfigFile;
\ No newline at end of file
diff --git a/NodeApp/src/managers/DojoBackendManager.ts b/NodeApp/src/managers/DojoBackendManager.ts
index 64c56cb34e1e25cc4b0fb3198d09d94e4b5993dd..42bda3fa67ec31b4f9be6bba3e5da406c8f8224e 100644
--- a/NodeApp/src/managers/DojoBackendManager.ts
+++ b/NodeApp/src/managers/DojoBackendManager.ts
@@ -7,6 +7,9 @@ import ClientsSharedConfig   from '../sharedByClients/config/ClientsSharedConfig
 import Assignment            from '../sharedByClients/models/Assignment';
 import DojoBackendResponse   from '../shared/types/Dojo/DojoBackendResponse';
 import Exercise              from '../sharedByClients/models/Exercise';
+import GitlabToken           from '../shared/types/Gitlab/GitlabToken';
+import User                  from '../sharedByClients/models/User';
+import DojoStatusCode        from '../shared/types/Dojo/DojoStatusCode';
 
 
 class DojoBackendManager {
@@ -14,6 +17,26 @@ class DojoBackendManager {
         return `${ ClientsSharedConfig.apiURL }${ route }`;
     }
 
+
+    public async login(gitlabTokens: GitlabToken): Promise<User | undefined> {
+        try {
+            return (await axios.post<DojoBackendResponse<User>>(this.getApiUrl(ApiRoute.LOGIN), {
+                accessToken : gitlabTokens.access_token,
+                refreshToken: gitlabTokens.refresh_token
+            })).data.data;
+        } catch ( error ) {
+            return undefined;
+        }
+    }
+
+
+    public async refreshTokens(refreshToken: string): Promise<GitlabToken> {
+        return (await axios.post<DojoBackendResponse<GitlabToken>>(this.getApiUrl(ApiRoute.REFRESH_TOKENS), {
+            refreshToken: refreshToken
+        })).data.data;
+    }
+
+
     public async getAssignment(nameOrUrl: string): Promise<Assignment | undefined> {
         try {
             return (await axios.get<DojoBackendResponse<Assignment>>(this.getApiUrl(ApiRoute.ASSIGNMENT_GET).replace('{{nameOrUrl}}', encodeURIComponent(nameOrUrl)))).data.data;
@@ -84,7 +107,11 @@ class DojoBackendManager {
                         if ( error.response.status === StatusCodes.CONFLICT ) {
                             spinner.fail(`The assignment name is already used. Please choose another one.`);
                         } else {
-                            spinner.fail(`Assignment creation error: ${ error.response.statusText }`);
+                            if ( (error.response.data as DojoBackendResponse<any>).code === DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR ) {
+                                spinner.fail(`Assignment creation error: An unknown error occurred while creating the assignment on Gitlab. Please try again later or contact an administrator.`);
+                            } else {
+                                spinner.fail(`Assignment creation error: An unknown error occurred while creating the assignment on Dojo server. Please try again later or contact an administrator.`);
+                            }
                         }
                     }
                 } else {
@@ -118,7 +145,11 @@ class DojoBackendManager {
                         if ( error.response.status === StatusCodes.CONFLICT ) {
                             spinner.fail(`You've already reached the max number of exercise of this assignment.`);
                         } else {
-                            spinner.fail(`Exercise creation error: ${ error.response.statusText }`);
+                            if ( (error.response.data as DojoBackendResponse<any>).code === DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR ) {
+                                spinner.fail(`Exercise creation error: An unknown error occurred while creating the exercise on Gitlab. Please try again later or contact an administrator.`);
+                            } else {
+                                spinner.fail(`Exercise creation error: An unknown error occurred while creating the exercise on Dojo server. Please try again later or contact an administrator.`);
+                            }
                         }
                     }
                 } else {
diff --git a/NodeApp/src/managers/GitlabManager.ts b/NodeApp/src/managers/GitlabManager.ts
index 92ce994a5027dfbac721c413d1625c44621f32b7..6562ad9980914e02cb7ef3b67b96cd85342e0748 100644
--- a/NodeApp/src/managers/GitlabManager.ts
+++ b/NodeApp/src/managers/GitlabManager.ts
@@ -1,41 +1,15 @@
-import LocalConfig     from '../config/LocalConfig';
-import LocalConfigKeys from '../types/LocalConfigKeys';
-import axios           from 'axios';
-import ora             from 'ora';
-import GitlabUser      from '../shared/types/Gitlab/GitlabUser';
-import GitlabRoute     from '../shared/types/Gitlab/GitlabRoute';
-import SharedConfig    from '../shared/config/SharedConfig';
+import axios        from 'axios';
+import ora          from 'ora';
+import GitlabUser   from '../shared/types/Gitlab/GitlabUser';
+import GitlabRoute  from '../shared/types/Gitlab/GitlabRoute';
+import SharedConfig from '../shared/config/SharedConfig';
 
 
 class GitlabManager {
-    private _token: string | null = null;
-
     private getApiUrl(route: GitlabRoute): string {
         return `${ SharedConfig.gitlab.apiURL }${ route }`;
     }
 
-    get isLogged(): boolean {
-        return this._token !== null;
-    }
-
-    get token(): string {
-        return this._token || '';
-    }
-
-    set token(token: string) {
-        this._token = token;
-
-        LocalConfig.updateConfig(LocalConfigKeys.GITLAB_PERSONAL_TOKEN, token);
-    }
-
-    login(token: string): void {
-        this.token = token;
-    }
-
-    logout(): void {
-        this.token = '';
-    }
-
     public async testToken(verbose: boolean = true): Promise<[ boolean, boolean ]> {
         if ( verbose ) {
             ora('Checking Gitlab token: ').start().info();
diff --git a/NodeApp/src/managers/HttpManager.ts b/NodeApp/src/managers/HttpManager.ts
index 5b8ea67c2a7d802a599ecb304bc04056737219dc..e90f666c4967739d857af9a604a39ca0e82fad34 100644
--- a/NodeApp/src/managers/HttpManager.ts
+++ b/NodeApp/src/managers/HttpManager.ts
@@ -1,7 +1,6 @@
 import axios, { AxiosRequestHeaders } from 'axios';
 import SessionManager                 from './SessionManager';
 import FormData                       from 'form-data';
-import GitlabManager                  from './GitlabManager';
 import { StatusCodes }                from 'http-status-codes';
 import ClientsSharedConfig            from '../sharedByClients/config/ClientsSharedConfig';
 import { version }                    from '../config/Version';
@@ -35,15 +34,15 @@ class HttpManager {
                 }
 
                 if ( SessionManager.isLogged ) {
-                    config.headers.Authorization = `Bearer ${ SessionManager.token }`;
+                    config.headers.Authorization = `Bearer ${ SessionManager.apiToken }`;
                 }
 
                 config.headers['client'] = 'DojoCLI';
                 config.headers['client-version'] = version;
             }
 
-            if ( GitlabManager.isLogged && config.url && config.url.indexOf(SharedConfig.gitlab.apiURL) !== -1 ) {
-                config.headers['PRIVATE-TOKEN'] = GitlabManager.token;
+            if ( SessionManager.gitlabCredentials.accessToken && config.url && config.url.indexOf(SharedConfig.gitlab.apiURL) !== -1 ) {
+                config.headers.Authorization = `Bearer ${ SessionManager.gitlabCredentials.accessToken }`;
             }
 
             return config;
@@ -66,13 +65,35 @@ class HttpManager {
     private registerResponseInterceptor() {
         axios.interceptors.response.use((response) => {
             if ( response.data && response.data.sessionToken ) {
-                SessionManager.token = response.data.sessionToken;
+                SessionManager.apiToken = response.data.sessionToken;
             }
 
             return response;
-        }, (error) => {
+        }, async (error) => {
             if ( error.response ) {
-                if ( error.response.status === StatusCodes.METHOD_NOT_ALLOWED && error.response.data ) {
+                const originalConfig = error.config;
+
+                const isFromApi = error.response.config.url && error.response.config.url.indexOf(ClientsSharedConfig.apiURL) !== -1;
+                const isFromGitlab = error.response.config.url && error.response.config.url.indexOf(SharedConfig.gitlab.URL) !== -1;
+
+                // Try to refresh the Gitlab tokens if the request have failed with a 401 error
+                if ( error.response.status === StatusCodes.UNAUTHORIZED && isFromGitlab && !originalConfig._retry ) {
+                    originalConfig._retry = true;
+
+                    try {
+                        await SessionManager.refreshTokens();
+
+                        return axios(originalConfig);
+                    } catch ( _error: any ) {
+                        if ( _error.response && _error.response.data ) {
+                            return Promise.reject(_error.response.data);
+                        }
+
+                        return Promise.reject(_error);
+                    }
+                }
+
+                if ( error.response.status === StatusCodes.METHOD_NOT_ALLOWED && isFromApi && error.response.data ) {
                     const data: DojoBackendResponse<{}> = error.response.data;
 
                     switch ( data.code ) {
@@ -88,7 +109,7 @@ class HttpManager {
                 }
 
                 if ( this.handleAuthorizationCommandErrors ) {
-                    if ( error.response.url && error.response.url.indexOf(ClientsSharedConfig.apiURL) !== -1 ) {
+                    if ( isFromApi ) {
                         switch ( error.response.status ) {
                             case StatusCodes.UNAUTHORIZED:
                                 this.requestError('Session expired or does not exist. Please login again.');
diff --git a/NodeApp/src/managers/SessionManager.ts b/NodeApp/src/managers/SessionManager.ts
index 28082f59a9345997c53ddb8cb4a04eeab60e43eb..580b6d854da7fa201243682ee8cd126aff84e8af 100644
--- a/NodeApp/src/managers/SessionManager.ts
+++ b/NodeApp/src/managers/SessionManager.ts
@@ -1,31 +1,105 @@
-import * as jwt              from 'jsonwebtoken';
-import User                  from '../sharedByClients/models/User';
-import LocalConfig           from '../config/LocalConfig';
-import LocalConfigKeys       from '../types/LocalConfigKeys';
-import axios, { AxiosError } from 'axios';
-import HttpManager           from './HttpManager';
-import ora                   from 'ora';
-import Permissions           from '../types/Permissions';
-import ApiRoute              from '../sharedByClients/types/Dojo/ApiRoute';
-import DojoBackendManager    from './DojoBackendManager';
-import { StatusCodes }       from 'http-status-codes';
+import * as jwt                  from 'jsonwebtoken';
+import User                      from '../sharedByClients/models/User';
+import LocalConfigFile           from '../config/LocalConfigFile';
+import LocalConfigKeys           from '../types/LocalConfigKeys';
+import axios, { HttpStatusCode } from 'axios';
+import HttpManager               from './HttpManager';
+import ora                       from 'ora';
+import Permissions               from '../types/Permissions';
+import ApiRoute                  from '../sharedByClients/types/Dojo/ApiRoute';
+import DojoBackendManager        from './DojoBackendManager';
+import Config                    from '../config/Config';
+import ClientsSharedConfig       from '../sharedByClients/config/ClientsSharedConfig';
+import DojoGitlabCredentials     from '../sharedByClients/types/Dojo/DojoGitlabCredentials';
+import * as http                 from 'http';
+import * as openurl              from 'openurl';
+import EventEmitter              from 'events';
+import SharedConfig              from '../shared/config/SharedConfig';
+import chalk                     from 'chalk';
+import inquirer                  from 'inquirer';
+import SharedGitlabManager       from '../shared/managers/SharedGitlabManager';
+import GitlabManager             from './GitlabManager';
+import GitlabToken               from '../shared/types/Gitlab/GitlabToken';
+
+
+class LoginServer {
+    readonly events: EventEmitter = new EventEmitter();
+    private server: http.Server;
+
+    constructor() {
+        this.server = http.createServer((req, res) => {
+            const sendError = (error: string) => {
+                this.events.emit('error', error);
+                res.writeHead(HttpStatusCode.InternalServerError, { 'Content-Type': 'text/html' });
+                res.write(`<html lang="en"><body><h1 style="color: red">DojoCLI login error</h1><h3>Please look at your CLI for more informations.</h3></body></html>`);
+                res.end();
+            };
+
+            if ( req.url?.match(Config.login.server.route) ) {
+                let urlParts = req.url.split('=');
+                if ( urlParts.length > 0 ) {
+                    this.events.emit('code', urlParts[1]);
+
+                    res.writeHead(HttpStatusCode.Ok, { 'Content-Type': 'text/html' });
+                    res.write(`<html lang="en"><body><h1 style="color: green">DojoCLI login successful</h1><h3>You can close this window.</h3></body></html>`);
+                    res.end();
+                    return;
+                }
+
+                sendError(`Incorrect call => ${ req.url }`);
+                return;
+            }
+
+            //sendError(`Unknown route call => ${ req.url }`);
+        });
+    }
+
+    start() {
+        try {
+            this.server.listen(Config.login.server.port);
+            this.events.emit('started');
+        } catch ( error ) {
+            this.events.emit('error', error);
+        }
+    }
+
+    stop() {
+        try {
+            this.server.close();
+            this.events.emit('stopped');
+        } catch ( error ) {
+            this.events.emit('error', error);
+        }
+    }
+}
 
 
 class SessionManager {
-    private _token: string | null = null;
+    private configFile: LocalConfigFile = new LocalConfigFile(Config.localConfig.sessionFile);
 
     public profile: User | undefined = undefined;
 
     get isLogged(): boolean {
-        return this._token !== null;
+        return this.apiToken !== null && this.apiToken !== '';
     }
 
-    get token(): string {
-        return this._token || '';
+    get apiToken(): string {
+        const apisToken = this.configFile.getParam(LocalConfigKeys.APIS_TOKEN);
+
+        if ( apisToken !== null && ClientsSharedConfig.apiURL in apisToken ) {
+            return apisToken[ClientsSharedConfig.apiURL];
+        }
+
+        return '';
     }
 
-    set token(token: string) {
-        this._token = token;
+    set apiToken(token: string) {
+        let apisToken = this.configFile.getParam(LocalConfigKeys.APIS_TOKEN);
+        if ( apisToken === null ) {
+            apisToken = {};
+        }
+        apisToken[ClientsSharedConfig.apiURL] = token;
+        this.configFile.setParam(LocalConfigKeys.APIS_TOKEN, apisToken);
 
         try {
             const payload = jwt.decode(token);
@@ -36,38 +110,166 @@ class SessionManager {
         } catch ( error ) {
             this.profile = undefined;
         }
+    }
 
-        LocalConfig.updateConfig(LocalConfigKeys.API_TOKEN, token);
+    get gitlabCredentials(): DojoGitlabCredentials {
+        return this.configFile.getParam(LocalConfigKeys.GITLAB);
     }
 
-    async login(user: string, password: string) {
-        const spinner: ora.Ora = ora('Logging in').start();
-        try {
-            this.profile = undefined;
+    set gitlabCredentials(credentials: DojoGitlabCredentials) {
+        this.configFile.setParam(LocalConfigKeys.GITLAB, credentials);
+    }
+
+    constructor() {
+        this.configFile.loadConfig();
+    }
+
+    private async getGitlabCodeFromHeadlessEnvironment(): Promise<string> {
+        const indent: string = '    ';
+        console.log(`${ indent }Please open the following URL in your web browser and accept to give the requested permissions to Dojo:`);
+        console.log(chalk.blue(`${ indent }${ Config.login.gitlab.url.code }`));
+        console.log(`${ indent }Then, copy the code at the end of the redirected url and paste it bellow.`);
+        console.log(`${ indent }Example of url (here the code is 123456): ${ chalk.blue(`${ SharedConfig.login.gitlab.url.redirect }?code=`) }${ chalk.green('123456') }`);
+        return (await inquirer.prompt({
+                                          type   : 'password',
+                                          name   : 'code',
+                                          message: `${ chalk.green('?') } Please paste the Gitlab code here`,
+                                          mask   : '*',
+                                          prefix : '   '
+                                      })).code;
+    }
+
+    private getGitlabCodeFromGraphicEnvironment(): Promise<string> {
+        return new Promise<string>((resolve, reject) => {
+            let currentSpinner: ora.Ora = ora({
+                                                  text  : 'Starting login server',
+                                                  indent: 4
+                                              }).start();
+
+            const loginServer = new LoginServer();
+            loginServer.events.on('started', () => {
+                currentSpinner.succeed('Login server started');
+                currentSpinner = ora({
+                                         text  : 'Waiting for user to authorize the application in his web browser',
+                                         indent: 4
+                                     }).start();
+                openurl.open(Config.login.gitlab.url.code, (error) => {
+                    if ( error ) {
+                        currentSpinner.warn('Error while opening the web browser. Attempting the login in CLI only mode (headless mode).');
+                        currentSpinner = ora({
+                                                 text  : 'Stopping login server',
+                                                 indent: 4
+                                             }).start();
+                        loginServer.events.on('stopped', () => {
+                            try {
+                                resolve(this.getGitlabCodeFromHeadlessEnvironment());
+                            } catch ( error ) {
+                                reject(error);
+                            }
+                        });
+                        loginServer.stop();
+                    }
+                });
+            });
+            loginServer.events.on('error', (error: string) => {
+                currentSpinner.fail(`Login server error: ${ error }`);
+                reject();
+            });
+            loginServer.events.on('stopped', () => {
+                currentSpinner.succeed('Login server stopped');
+            });
+
+
+            loginServer.events.on('code', (code: string) => {
+                currentSpinner.succeed('Login code received');
+                currentSpinner = ora({
+                                         text  : 'Stopping login server',
+                                         indent: 4
+                                     }).start();
+                loginServer.events.on('stopped', () => {
+                    resolve(code);
+                });
+                loginServer.stop();
 
-            const response = await axios.post(DojoBackendManager.getApiUrl(ApiRoute.LOGIN), {
-                user    : user,
-                password: password
+                resolve(code);
             });
 
-            spinner.succeed('Logged in');
+            loginServer.start();
+        });
+    }
+
+    async login(headless: boolean = false) {
+        try {
+            this.logout();
         } catch ( error ) {
-            if ( error instanceof AxiosError ) {
-                if ( error.response ) {
-                    if ( error.response.status === StatusCodes.NOT_FOUND ) {
-                        spinner.fail('User not found or password incorrect');
-                    } else {
-                        spinner.fail(`Login error: ${ error.response.statusText }`);
-                    }
-                }
-            } else {
-                spinner.fail(`Login error: ${ error }`);
-            }
+            console.log(error);
+            ora('Unknown error').start().fail();
+            throw error;
+        }
+
+        ora(`Login with Gitlab (${ SharedConfig.gitlab.URL }):`).start().info();
+
+        let gitlabCode: string;
+        if ( !headless ) {
+            gitlabCode = await this.getGitlabCodeFromGraphicEnvironment();
+        } else {
+            gitlabCode = await this.getGitlabCodeFromHeadlessEnvironment();
         }
+
+        let gitlabTokensSpinner = ora({
+                                          text  : 'Retrieving gitlab tokens',
+                                          indent: 4
+                                      }).start();
+        let gitlabTokens: GitlabToken;
+        try {
+            gitlabTokens = await SharedGitlabManager.getTokens(gitlabCode);
+            this.gitlabCredentials = {
+                refreshToken: gitlabTokens.refresh_token,
+                accessToken : gitlabTokens.access_token
+            };
+            gitlabTokensSpinner.succeed('Gitlab tokens retrieved');
+        } catch ( error ) {
+            gitlabTokensSpinner.fail('Error while retrieving gitlab tokens');
+            throw error;
+        }
+
+        const isGitlabTokensValid = (await GitlabManager.testToken()).every((value) => value);
+        if ( !isGitlabTokensValid ) {
+            throw new Error('Gitlab tokens are invalid');
+        }
+
+        ora(`Login to Dojo backend:`).start().info();
+        let dojoLoginSpinner = ora({
+                                       text  : 'Login to Dojo backend',
+                                       indent: 4
+                                   }).start();
+
+        try {
+            await DojoBackendManager.login(gitlabTokens);
+            dojoLoginSpinner.succeed('Logged in');
+        } catch ( error ) {
+            dojoLoginSpinner.fail('Login failed');
+            throw error;
+        }
+
+        await this.testSession(true);
+    }
+
+    async refreshTokens() {
+        let gitlabTokens = await DojoBackendManager.refreshTokens(this.gitlabCredentials.refreshToken!);
+
+        this.gitlabCredentials = {
+            refreshToken: gitlabTokens.refresh_token,
+            accessToken : gitlabTokens.access_token
+        };
     }
 
     logout() {
-        this.token = '';
+        this.apiToken = '';
+        this.gitlabCredentials = {
+            refreshToken: '',
+            accessToken : ''
+        };
     }
 
     checkPermissions(verbose: boolean = true, indent: number = 8, checkPermissions: Array<string> | null = []): Permissions {
@@ -86,8 +288,9 @@ class SessionManager {
         };
 
         return {
+            student      : checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('student')) ? hasPermission(() => true, 'Student permissions') : false,
             teachingStaff: checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('teachingStaff')) ? hasPermission(() => this.profile?.isTeachingStaff ?? false, 'Teaching staff permissions') : false,
-            student      : checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('student')) ? hasPermission(() => true, 'Student permissions') : false
+            admin        : checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('admin')) ? hasPermission(() => this.profile?.isAdmin ?? false, 'Admin permissions') : false
         };
     }
 
diff --git a/NodeApp/src/shared b/NodeApp/src/shared
index efe1bf313f57d1826faf935c183d37a0835f8c2d..4a5eb68209ae9204b6d4cc8020bd62cf6a5be989 160000
--- a/NodeApp/src/shared
+++ b/NodeApp/src/shared
@@ -1 +1 @@
-Subproject commit efe1bf313f57d1826faf935c183d37a0835f8c2d
+Subproject commit 4a5eb68209ae9204b6d4cc8020bd62cf6a5be989
diff --git a/NodeApp/src/sharedByClients b/NodeApp/src/sharedByClients
index d9379b055a4626e4b35cf4cc4a7429040a4aeaf7..dc12d17660bf9e92656c6abcb24ec7ce6ab3d675 160000
--- a/NodeApp/src/sharedByClients
+++ b/NodeApp/src/sharedByClients
@@ -1 +1 @@
-Subproject commit d9379b055a4626e4b35cf4cc4a7429040a4aeaf7
+Subproject commit dc12d17660bf9e92656c6abcb24ec7ce6ab3d675
diff --git a/NodeApp/src/types/LocalConfigKeys.ts b/NodeApp/src/types/LocalConfigKeys.ts
index f64751d4b278c48815dbab243a567257cc21ed04..eba97d6dc199b45ad1ca2b7b48f7c89ea3c27a3f 100644
--- a/NodeApp/src/types/LocalConfigKeys.ts
+++ b/NodeApp/src/types/LocalConfigKeys.ts
@@ -1,7 +1,6 @@
 enum LocalConfigKeys {
-    API_TOKEN             = 'apiToken',
-    API_TOKEN_ENV         = 'apiTokenEnv',
-    GITLAB_PERSONAL_TOKEN = 'gitlabPersonalToken',
+    APIS_TOKEN = 'apisToken',
+    GITLAB     = 'gitlab',
 }
 
 
diff --git a/NodeApp/src/types/Permissions.ts b/NodeApp/src/types/Permissions.ts
index 8d91a16df47d6f9abb395281e71fbd64cf4ed331..b77f2f4b967d210f22992561eb7503d55e6e59fc 100644
--- a/NodeApp/src/types/Permissions.ts
+++ b/NodeApp/src/types/Permissions.ts
@@ -1,6 +1,7 @@
 interface Permissions {
+    student: boolean,
     teachingStaff: boolean,
-    student: boolean
+    admin: boolean
 }