diff --git a/app/assets/javascripts/vue.js b/app/assets/javascripts/vue.js
index 1394abd16d1c22a72ff984c8475972930003e9c2..f812291c676f39a69a6905da16ff171b60e1cf5c 100644
--- a/app/assets/javascripts/vue.js
+++ b/app/assets/javascripts/vue.js
@@ -1,2 +1,2 @@
-//= require vue/dist/vue.global.js
+//= require vue/dist/vue.global.prod.js
 //= require vue-draggable-next/dist/vue-draggable-next.global.js
diff --git a/app/assets/stylesheets/commons/_block.sass b/app/assets/stylesheets/commons/_block.sass
index 573a390e9e1a82760d4a4ab3f93e03cfec8a4c83..4acd08cac2f9e8b41426a464b9c202efc055910c 100644
--- a/app/assets/stylesheets/commons/_block.sass
+++ b/app/assets/stylesheets/commons/_block.sass
@@ -1,5 +1,5 @@
 [v-cloak]
-    form
+    form, .app-form
         display: none
 [data-v-app]
     .spinner-border
diff --git a/app/controllers/server/universities_controller.rb b/app/controllers/server/universities_controller.rb
index db42c8fb802eb74ae03e190555a1b4bc74d9c2df..7993a87cfea8b6c12957c0e7ad198585dba0c135 100644
--- a/app/controllers/server/universities_controller.rb
+++ b/app/controllers/server/universities_controller.rb
@@ -62,7 +62,7 @@ class Server::UniversitiesController < Server::ApplicationController
     params.require(:university).permit(:name,
       :address, :zipcode, :city, :country,
       :private, :identifier, :logo, :logo_delete, :sms_sender_name,
-      :has_sso, :sso_target_url, :sso_cert, :sso_name_identifier_format,
+      :has_sso, :sso_target_url, :sso_cert, :sso_name_identifier_format, :sso_mapping,
       :invoice_date, :invoice_amount)
   end
 end
diff --git a/app/models/user/with_omniauth.rb b/app/models/user/with_omniauth.rb
index f0de9a849352717c3274fbb4eaea58713ed5bfeb..24d8361368f2e7c1cdceb2bdd808f89df7b45fa1 100644
--- a/app/models/user/with_omniauth.rb
+++ b/app/models/user/with_omniauth.rb
@@ -5,10 +5,9 @@ module User::WithOmniauth
 
     def self.from_omniauth(university, attributes)
       mapping = university.sso_mapping
-      email = 'pierreandre.boissinot@noesya.coop'
 
-      # email_sso_key = mapping.select { |elmt| elmt['internal_key'] == 'email' }&.first&.dig('sso_key')
-      email_sso_key = 'email'
+      # first step: we find the email (we are supposed to have an email mapping)
+      email_sso_key = mapping.select { |elmt| elmt['internal_key'] == 'email' }&.first&.dig('sso_key')
       email = attributes.dig(email_sso_key)
       return unless email
       email = email.first if email.is_a?(Array)
@@ -17,6 +16,47 @@ module User::WithOmniauth
       user = User.where(university: university, email: email).first_or_create do |u|
         u.password = "#{Devise.friendly_token[0,20]}!" # meets password complexity requirements
       end
+
+      # update user data according to mapping & infos provided by SSO
+      mapping.select { |elmt| elmt['internal_key'] != 'email' }.each do |mapping_element|
+        user = self.update_data_for_mapping_element(user, mapping_element, attributes)
+      end
+
+      user.save
+      user
+    end
+
+    protected
+
+    def self.update_data_for_mapping_element(user, mapping_element, attributes)
+      sso_key = mapping_element['sso_key']
+      return user if attributes[sso_key].nil? # if not provided by sso, just return
+      internal_key = mapping_element['internal_key']
+      user = self.update_data_for_mapping_element_standard(user, mapping_element, self.get_provided_answer(attributes[sso_key]))
+      user
+    end
+
+    def self.update_data_for_mapping_element_standard(user, mapping_element, sso_value)
+      case mapping_element['internal_key']
+      when 'language'
+        user = self.set_best_id_for(user, mapping_element['type'], sso_value.first)
+      when 'role'
+        value = mapping_element['roles'].select { |key, val| val == sso_value.first }.first&.first
+        user['role'] = value if value
+      else
+        user[mapping_element['internal_key']] = sso_value.first
+      end
+      user
+    end
+
+    def self.get_provided_answer(value)
+      # SAML send an array (even for a single value) where OAuth2 send a string for single values. We harmonize to always get an array
+      value.is_a?(Array) ? value : [value]
+    end
+
+    def self.set_best_id_for(user, type, iso)
+      element_id = eval(type.classify).find_by(iso_code: iso)&.id
+      user["#{type}_id"] = element_id unless element_id.nil?
       user
     end
 
diff --git a/app/views/admin/communication/blocks/edit.html.erb b/app/views/admin/communication/blocks/edit.html.erb
index ea57a1e441a8671e0d736fecd7e9064d68ff2385..e126220a12c759bcd08d3d5ee3c88aa013bcc9f3 100644
--- a/app/views/admin/communication/blocks/edit.html.erb
+++ b/app/views/admin/communication/blocks/edit.html.erb
@@ -6,7 +6,7 @@
 %>
 <div id="app" v-cloak>
   <div class="spinner-border text-primary" role="status">
-    <span class="sr-only">Loading...</span>
+    <span class="sr-only"><%= t 'loading' %></span>
   </div>
   <%= simple_form_for [:admin, @block] do |f| %>
     <div class="row">
diff --git a/app/views/server/universities/_form.html.erb b/app/views/server/universities/_form.html.erb
index c3e2a4743b9860e325cb2746712564c7c1375b42..5ef1508748f0d071e6782559b6453803da92fbbb 100644
--- a/app/views/server/universities/_form.html.erb
+++ b/app/views/server/universities/_form.html.erb
@@ -28,16 +28,19 @@
     </div>
   </div>
 
+  <h3 class="mt-5"><%= t('university.sso') %></h3>
   <div class="row">
     <div class="col-md-6">
-      <h3 class="mt-5"><%= t('university.sso') %></h3>
       <%= f.input :has_sso %>
       <div id="sso-inputs">
         <%= f.input :sso_target_url, required: true %>
         <%= f.input :sso_cert, required: true %>
         <%= f.input :sso_name_identifier_format, required: true %>
-
-        <%#= render 'sso_mapping', brand: brand %>
+      </div>
+    </div>
+    <div class="col-md-6">
+      <h4 class="mb-4"><%= University.human_attribute_name('sso_mapping') %></h4>
+      <%= render 'sso_mapping', university: university %>
     </div>
   </div>
 
diff --git a/app/views/server/universities/_sso_mapping.html.erb b/app/views/server/universities/_sso_mapping.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..a1fc7362a035003a0e4e5cc843a494d284017d58
--- /dev/null
+++ b/app/views/server/universities/_sso_mapping.html.erb
@@ -0,0 +1,82 @@
+<% mapping_keys = ['email', 'first_name', 'last_name', 'role', 'mobile_phone', 'language', 'picture_url'] %>
+
+<%# Include vue.js before call Vue.createApp %>
+<%= javascript_include_tag 'vue' %>
+
+<div id="app" v-cloak>
+  <div class="spinner-border text-primary" role="status">
+    <span class="sr-only"><%= t('loading') %></span>
+  </div>
+
+  <div class="app-form">
+    <draggable :list="fields">
+      <div v-for="(field, index) in fields">
+        <div class="card">
+          <div class="card-header d-flex justify-content-between">
+            <a data-bs-toggle="collapse" :href="'#sso_mapping_collapse_' + index ">
+              {{index + 1}}. {{ field.sso_key }} -> {{ keys[field.internal_key]}}
+            </a>
+            <a
+              v-on:click="fields.splice(fields.indexOf(field), 1)"
+              title="Remove field">
+              <i class="far fa-trash-alt"></i>
+            </a>
+          </div>
+          <div class="card-body collapse pt-0" :id="'sso_mapping_collapse_' + index ">
+            <hr class="mt-0">
+            <div class="form-group">
+              <label for="" class="form-control-label"><%= t('university.sso_key') %> <abbr title="required">*</abbr></label>
+              <input
+                v-model="field.sso_key"
+                type="text" class="form-control">
+            </div>
+            <div class="form-group">
+              <label for="" class="form-control-label"><%= t('university.internal_key') %> <abbr title="required">*</abbr></label>
+              <select v-model="field.internal_key" id="" class="form-select" required>
+                <option v-for="(label, key) in keys" :value="key">{{ label }}</option>
+              </select>
+            </div>
+            <div v-if="field.internal_key === 'role'">
+              <hr class="mt-4">
+              <% User.roles.keys.each do |role| %>
+                <div class="form-group">
+                  <label for="" class="form-label"><%= t("activerecord.attributes.user.roles.#{role}") %></label>
+                  <input v-model="field.roles.<%= role %>" type="text" class="form-control">
+                </div>
+              <% end %>
+            </div>
+          </div>
+        </div>
+      </div>
+    </draggable>
+
+    <a v-on:click="fields.push({sso_key: 'key', internal_key: 'email', roles: {}})" class="btn btn-primary btn-sm">
+      <%= t('add_field') %>
+    </a>
+  </div>
+
+  <textarea name="university[sso_mapping]" id="university_sso_mapping" rows="20" cols="200" class="d-none">
+    {{ JSON.stringify(fields) }}
+  </textarea>
+
+</div>
+
+<script>
+  var app = Vue.createApp({
+    components: {
+      draggable: VueDraggableNext.VueDraggableNext,
+    },
+    data() {
+      return {
+        fields: <%= university.sso_mapping.blank? ? '[]' : university.sso_mapping.to_json.html_safe %>,
+        keys: <%= mapping_keys.map { |key| [key, User.human_attribute_name(key)] }.to_h.to_json.html_safe %>
+      }
+    }
+  });
+
+  window.addEventListener('load', function(){
+    setTimeout(function() {
+      app.mount('#app');
+    }, 1000);
+  });
+</script>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index aeaae6cf586c5b579138ff01856c8448b6fc191c..49071105af0eb9cde4fe42461b5a176d54940a54 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -38,6 +38,7 @@ en:
         one: User
         other: Users
   add: Add
+  add_field: Add field
   admin:
     attachment_not_available: Attachment not available
     dashboard: Dashboard
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 98572ab5182adbebcfee951ea6724522753a471a..ab257fa73827bc04141c59c0edb70995dd5fcf76 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -38,6 +38,7 @@ fr:
         one: Utilisateur·rice
         other: Utilisateur·rice·s
   add: Ajouter
+  add_field: Ajouter un champ
   admin:
     attachment_not_available: Impossible d'accéder à l'élément
     dashboard: Tableau de bord
diff --git a/config/locales/university/en.yml b/config/locales/university/en.yml
index 63a83a4e13d8231a460c218b51e5f82d13f3b9b9..8b3633d360145d4a543319a7e49e7a7415508b2a 100644
--- a/config/locales/university/en.yml
+++ b/config/locales/university/en.yml
@@ -16,6 +16,7 @@ en:
         public_or_private: Public/private
         sms_sender_name: SMS sender name
         sso_cert: Certificate
+        sso_mapping: Mapping
         sso_name_identifier_format: Name Identifier Format
         sso_target_url: Target URL
         url: URL
@@ -125,8 +126,10 @@ en:
           non_profit: Association
           government: Government
   university:
+    internal_key: Internal Key
     invoice_informations: Invoice informations
     person:
       administrator_roles: Administrator roles
       taught_programs: Taught programs
     sso: SSO
+    sso_key: SSO Key
diff --git a/config/locales/university/fr.yml b/config/locales/university/fr.yml
index f263aead1db38c3fbd9063474e72394808a2a9e9..049859fb4d3a08af294c74cf74428c0d05e2e548 100644
--- a/config/locales/university/fr.yml
+++ b/config/locales/university/fr.yml
@@ -16,6 +16,7 @@ fr:
         public_or_private: Public/privé
         sms_sender_name: Nom de l'expéditeur SMS
         sso_cert: Certificat
+        sso_mapping: Mapping
         sso_name_identifier_format: Name Identifier Format
         sso_target_url: URL cible
         url: 'URL'
@@ -125,8 +126,10 @@ fr:
           non_profit: Association
           government: Structure gouvernementale
   university:
+    internal_key: Clé interne
     invoice_informations: Données de facturation
     person:
       administrator_roles: Rôles administratifs
       taught_programs: Formations enseignées
     sso: SSO
+    sso_key: Clé sur le SSO