diff --git a/Gemfile b/Gemfile
index 4a42a43119ec930d1f161e54c5f2af473dad0a8e..c9678d58eb34c8551601e31f5e9d6b9b6a02d2ff 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,60 +4,58 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
 ruby '2.7.5'
 
 # Infrastructure
+gem 'angularjs-rails'
 gem 'aws-sdk-s3'
-gem 'bugsnag'
-gem 'gitlab'
-gem 'image_processing'
-gem 'mini_magick'
-gem 'octokit'
-gem 'pg', '~> 1.1'
-gem 'puma'
-
-# Back-end
-gem 'cancancan'
 gem 'bootsnap', '>= 1.4.4', require: false
-gem 'curation'#, path: '../../arnaudlevy/curation'
-gem 'delayed_job_active_record'
-gem 'delayed_job_web'
-gem 'faceted_search'#, path: '../faceted_search'
-gem 'has_scope', '~> 0.8.0'
-gem 'hash_dot'
-gem 'rails', '~> 6.1'
-gem 'rails-i18n'
-gem 'sanitize'
-gem 'sib-api-v3-sdk'
-gem 'two_factor_authentication', git: 'https://github.com/noesya/two_factor_authentication.git'
-# gem 'two_factor_authentication', path: '../two_factor_authentication'
-
-# Front-end
-gem 'angularjs-rails'
 gem 'bootstrap'
 gem 'bootstrap5-kaminari-views'
 gem 'breadcrumbs_on_rails'
+gem 'bugsnag'
+gem 'cancancan'
 gem 'cocoon', '~> 1.2'
 gem 'country_select'
+gem 'curation'#, path: '../../arnaudlevy/curation'
+gem 'delayed_job_active_record'
+gem 'delayed_job_web'
 gem 'devise'
 gem 'devise-i18n'
 gem 'enum_help'
+gem 'faceted_search'#, path: '../faceted_search'
 gem 'front_matter_parser'
 gem 'gdpr'
+gem 'gitlab'
+gem 'has_scope', '~> 0.8.0'
+gem 'hash_dot'
+gem 'image_processing'
 gem 'jbuilder'
 gem 'jquery-rails'
 gem 'kamifusen'#, path: '../kamifusen'
 gem 'kaminari'
+gem 'mini_magick'
+gem 'octokit'
+gem 'omniauth-saml'
+gem 'pg', '~> 1.1'
+gem 'puma'
+gem 'rails', '~> 6.1'
+gem 'rails-i18n'
+gem 'sanitize'
 gem 'sassc-rails'
+gem 'sib-api-v3-sdk'
+gem 'simple-navigation'
 gem 'simple_form'
 gem 'simple_form_bs5_file_input'#, path: '../simple_form_bs5_file_input'
 gem 'simple_form_password_with_hints'#, path: '../simple_form_password_with_hints'
-gem 'simple-navigation'
 gem 'summernote-rails', git: 'https://github.com/noesya/summernote-rails.git', branch: 'activestorage'
 # gem 'summernote-rails', path: '../summernote-rails'
+gem 'two_factor_authentication', git: 'https://github.com/noesya/two_factor_authentication.git'
+# gem 'two_factor_authentication', path: '../two_factor_authentication'
+
 
 group :development, :test do
   gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
   gem 'figaro'
-  gem 'webmock'
   gem 'vcr'
+  gem 'webmock'
 end
 
 group :development do
diff --git a/Gemfile.lock b/Gemfile.lock
index 106b97d221040a40d7d50ad3ec093bc5b3b43faa..7a8d96bb71c06f190cf877abac06ad6408937b8b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -228,6 +228,7 @@ GEM
       activesupport (>= 5.2)
     hash_dot (2.5.0)
     hashdiff (1.0.1)
+    hashie (5.0.0)
     http-cookie (1.0.4)
       domain_name (~> 0.5)
     httparty (0.20.0)
@@ -307,6 +308,12 @@ GEM
     octokit (4.22.0)
       faraday (>= 0.9)
       sawyer (~> 0.8.0, >= 0.5.3)
+    omniauth (1.9.1)
+      hashie (>= 3.4.6)
+      rack (>= 1.6.2, < 3)
+    omniauth-saml (1.10.3)
+      omniauth (~> 1.3, >= 1.3.2)
+      ruby-saml (~> 1.9)
     orm_adapter (0.5.0)
     pg (1.3.5)
     popper_js (2.9.3)
@@ -361,6 +368,9 @@ GEM
       railties (>= 5.0)
     rexml (3.2.5)
     rotp (6.2.0)
+    ruby-saml (1.13.0)
+      nokogiri (>= 1.10.5)
+      rexml
     ruby-vips (2.1.4)
       ffi (~> 1.12)
     ruby2_keywords (0.0.5)
@@ -489,6 +499,7 @@ DEPENDENCIES
   listen (~> 3.3)
   mini_magick
   octokit
+  omniauth-saml
   pg (~> 1.1)
   puma
   rack-mini-profiler (~> 2.0)
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index fd05f8daf796de400418bcf938fa3f32b819e3f3..43e04d1b6d9290f7cb778462f5841c181a75b45a 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -17,5 +17,6 @@
 //= require_tree ./admin/commons
 //= require_tree ./admin/plugins
 //= require ./admin/communication/init
+//= require ./admin/university/init
 
 window.osuny = {};
diff --git a/app/assets/javascripts/admin/commons/batch-selectable.js b/app/assets/javascripts/admin/commons/batch-selectable.js
index 09f9f4f6c896865bf5639443f4c6acc33a2d39a3..24d954826cc31c9de41dc7a56129be4a6aa656b9 100644
--- a/app/assets/javascripts/admin/commons/batch-selectable.js
+++ b/app/assets/javascripts/admin/commons/batch-selectable.js
@@ -4,25 +4,46 @@ window.osuny.BatchSelectable = function BatchSelectable (element) {
     this.element = element;
     this.selectAllInput = this.element.querySelector('[data-batch-selectable-role="select-all"]');
     this.selectSingleInputs = this.element.querySelectorAll('[data-batch-selectable-role="select-single"]');
+    this.actionsContainer = this.element.querySelector('[data-batch-selectable-role="actions-container"]');
     this.initEvents();
+    this.toggleActionsContainer();
 };
 
 window.osuny.BatchSelectable.prototype.initEvents = function () {
     'use strict';
-    if (this.selectAllInput === null) {
-        return;
+    var i;
+    if (this.selectAllInput !== null) {
+        this.selectAllInput.addEventListener('change', this.toggleSingleInputs.bind(this));
+    }
+    if (this.actionsContainer !== null) {
+        for (i = 0; i < this.selectSingleInputs.length; i += 1) {
+            this.selectSingleInputs[i].addEventListener('change', this.toggleActionsContainer.bind(this));
+        }
     }
-    this.selectAllInput.addEventListener('change', function () {
-        this.toggleSingleInputs(this.selectAllInput.checked);
-    }.bind(this));
 };
 
-window.osuny.BatchSelectable.prototype.toggleSingleInputs = function (checked) {
+window.osuny.BatchSelectable.prototype.toggleSingleInputs = function (event) {
     'use strict';
-    var i;
+    var checked = event.currentTarget.checked,
+        i;
     for (i = 0; i < this.selectSingleInputs.length; i += 1) {
         this.selectSingleInputs[i].checked = checked;
     }
+    if (this.actionsContainer !== null) {
+        this.toggleActionsContainer();
+    }
+};
+
+window.osuny.BatchSelectable.prototype.toggleActionsContainer = function () {
+    'use strict';
+    var i;
+    for (i = 0; i < this.selectSingleInputs.length; i += 1) {
+        if (this.selectSingleInputs[i].checked) {
+            this.actionsContainer.classList.remove('d-none');
+            return;
+        }
+    }
+    this.actionsContainer.classList.add('d-none');
 };
 
 window.addEventListener('DOMContentLoaded', function () {
diff --git a/app/assets/javascripts/admin/university/edit.js b/app/assets/javascripts/admin/university/edit.js
new file mode 100644
index 0000000000000000000000000000000000000000..9799b2a344ed7d58e4e802377eb1091bc79ba1fe
--- /dev/null
+++ b/app/assets/javascripts/admin/university/edit.js
@@ -0,0 +1,36 @@
+window.osuny.university.edit = {
+    init: function () {
+        'use strict';
+        this.hasSsoInput = document.querySelector('input[type="checkbox"][name="university[has_sso]"]');
+        this.hasSsoInput.addEventListener('change', this.onHasSsoChange.bind(this));
+        this.ssoFields = document.getElementById('sso-inputs');
+        this.onHasSsoChange();
+    },
+
+    onHasSsoChange: function () {
+        'use strict';
+        var value = this.hasSsoInput.checked;
+        if (value) {
+            this.ssoFields.classList.remove('d-none');
+        } else {
+            this.ssoFields.classList.add('d-none');
+        }
+    },
+
+    invoke: function () {
+        'use strict';
+        return {
+            init: this.init.bind(this)
+        };
+    }
+}.invoke();
+
+window.addEventListener('DOMContentLoaded', function () {
+    'use strict';
+    if (document.body.classList.contains('universities-new')
+        || document.body.classList.contains('universities-create')
+        || document.body.classList.contains('universities-edit')
+        || document.body.classList.contains('universities-update')) {
+        window.osuny.university.edit.init();
+    }
+});
diff --git a/app/assets/javascripts/admin/university/init.js b/app/assets/javascripts/admin/university/init.js
new file mode 100644
index 0000000000000000000000000000000000000000..1c33bd9eb5e950e7bc13965380783a4a7a10b073
--- /dev/null
+++ b/app/assets/javascripts/admin/university/init.js
@@ -0,0 +1,4 @@
+//= require_self
+//= require ./edit
+
+window.osuny.university = {};
diff --git a/app/controllers/server/universities_controller.rb b/app/controllers/server/universities_controller.rb
index dd43e821ac5100a5926c4d48dba949fee9a7b2e4..db42c8fb802eb74ae03e190555a1b4bc74d9c2df 100644
--- a/app/controllers/server/universities_controller.rb
+++ b/app/controllers/server/universities_controller.rb
@@ -59,6 +59,10 @@ class Server::UniversitiesController < Server::ApplicationController
   end
 
   def university_params
-    params.require(:university).permit(:name, :address, :zipcode, :city, :country, :private, :identifier, :logo, :logo_delete, :sms_sender_name, :invoice_date, :invoice_amount)
+    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,
+      :invoice_date, :invoice_amount)
   end
 end
diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..be8f1f99dfc7cd5b53c57a3c380f90d5ab0fdea8
--- /dev/null
+++ b/app/controllers/users/omniauth_callbacks_controller.rb
@@ -0,0 +1,48 @@
+class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
+  # include Users::AddBrandToRequestParams
+  # include I18nHelper
+
+  protect_from_forgery except: :saml
+  before_action :redirect_unless_university_has_sso
+  skip_before_action :verify_authenticity_token, only: :saml
+
+  def saml
+    response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
+    ###############################################################################
+    # response.name_id : "pierreandre.boissinot@noesya.coop"
+    # response.attributes.all : {"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"=>["Pierre-André"], "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"=>["Boissinot"], "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"=>["paboissinot@lespoupees.paris"], "b2bylon_role"=>["superadmin"], "language"=>["fr"], "interests"=>["interest_1", "interest_3"]}
+    ###############################################################################
+    puts response.name_id
+    puts response.attributes.to_s
+    puts response.to_s
+    manage_user(response.attributes.all)
+  end
+
+  def saml_setup
+    # SAML config is stored in current brand
+    request.env['omniauth.strategy'].options[:issuer] = "#{user_saml_omniauth_authorize_url}/metadata"
+    request.env['omniauth.strategy'].options[:idp_sso_target_url] = current_university.sso_target_url
+    request.env['omniauth.strategy'].options[:idp_cert] = current_university.sso_cert
+    request.env['omniauth.strategy'].options[:name_identifier_format] = current_university.sso_name_identifier_format
+
+    render plain: "Omniauth SAML setup phase.", status: 404
+  end
+
+  private
+
+  def manage_user(user_infos)
+    @user = User.from_omniauth(current_university, user_infos)
+    
+    if @user&.persisted?
+      @user.remember_me = true
+      sign_in_and_redirect @user, event: :authentication
+    else
+      flash[:notice] = tt('devise.omniauth_callbacks.failure')
+      redirect_to new_user_session_url
+    end
+  end
+
+  def redirect_unless_university_has_sso
+    redirect_to root_path and return unless current_university.has_sso?
+  end
+end
diff --git a/app/models/university/with_sso.rb b/app/models/university/with_sso.rb
index 34f6afb8bccf895f1ab689f5b99552227bb37a5c..08dec1a53fc213d78f7a0515ba35688a90cd5e20 100644
--- a/app/models/university/with_sso.rb
+++ b/app/models/university/with_sso.rb
@@ -2,7 +2,10 @@ module University::WithSso
   extend ActiveSupport::Concern
 
   included do
-    enum sso_provider: { saml: 0, oauth2: 10 }, _prefix: :with_sso_via
+    enum sso_provider: { saml: 0 }, _prefix: :with_sso_via
+
+    validates :sso_cert, :sso_name_identifier_format, :sso_target_url, presence: true, if: :has_sso?
+
   end
 
   # Setter to serialize data as JSON
diff --git a/app/models/user.rb b/app/models/user.rb
index 7b81885d3f973407cb183d57143322436d968872..97c8b7767781e08d02385cc1a8f3b73b6c7b82e7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -56,6 +56,7 @@ class User < ApplicationRecord
   has_one_attached_deletable :picture  # In this order, "resize avatar" callback will be fired after the others.
   include WithUniversity
   include WithAuthentication
+  include WithOmniauth
   include WithPerson
   include WithRoles
   include WithSyncBetweenUniversities
diff --git a/app/models/user/with_authentication.rb b/app/models/user/with_authentication.rb
index 933ba2c8f2b7d7b67672d9da4133ceb74008266a..54de19f4f987d0ac67f895f2ba2dd4ba7ef3c8e7 100644
--- a/app/models/user/with_authentication.rb
+++ b/app/models/user/with_authentication.rb
@@ -3,7 +3,7 @@ module User::WithAuthentication
 
   included do
     devise  :database_authenticatable, :registerable, :recoverable, :rememberable,
-            :timeoutable, :confirmable, :trackable, :lockable, :two_factor_authenticatable
+            :timeoutable, :confirmable, :trackable, :lockable, :two_factor_authenticatable, :omniauthable, omniauth_providers: [:saml]
             # note : i do not use :validatable because of the non-uniqueness of the email. :validatable is replaced by the validation sequences below
 
 
diff --git a/app/models/user/with_omniauth.rb b/app/models/user/with_omniauth.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1a4261360df7a08bb88741d57d59ef76c388db77
--- /dev/null
+++ b/app/models/user/with_omniauth.rb
@@ -0,0 +1,18 @@
+module User::WithOmniauth
+  extend ActiveSupport::Concern
+
+  included do
+
+    def self.from_omniauth(university, attributes)
+      mapping = university.sso_mapping
+      email = 'pierreandre.boissinot@noesya.coop'
+
+      user = User.where(university: university, email: email).first_or_create do |u|
+        u.password = "#{Devise.friendly_token[0,20]}!" # meets password complexity requirements
+      end
+      user
+    end
+
+  end
+
+end
diff --git a/app/views/admin/communication/website/posts/_list.html.erb b/app/views/admin/communication/website/posts/_list.html.erb
index 5fbf2c99b59aab4d456a1f2279dbb01bb8ae0457..76f5e5861d7dccd73629c6d3e4073ee5790b4a45 100644
--- a/app/views/admin/communication/website/posts/_list.html.erb
+++ b/app/views/admin/communication/website/posts/_list.html.erb
@@ -3,7 +3,10 @@
   hide_category |= false
   selectable |= false
 %>
-<table class="<%= table_classes %>" <%= "data-batch-selectable" if selectable %>>
+<% if selectable %>
+<input type="hidden" name="ids[]" value="">
+<% end %>
+<table class="<%= table_classes %>">
   <thead>
     <tr>
       <% if selectable %>
diff --git a/app/views/admin/communication/website/posts/index.html.erb b/app/views/admin/communication/website/posts/index.html.erb
index 2c19431f83b214fc60b6916c97a4e2b9ebdb41d9..5688ab1246448d9cb21c061ef28b70d5c14869bb 100644
--- a/app/views/admin/communication/website/posts/index.html.erb
+++ b/app/views/admin/communication/website/posts/index.html.erb
@@ -3,28 +3,29 @@
 <%= render 'admin/communication/websites/sidebar' do %>
   <%= render 'filters', current_path: admin_communication_website_posts_path, filters: @filters if @filters.any?  %>
 
-  <div class="card">
+  <div class="card" data-batch-selectable>
     <%= form_tag publish_admin_communication_website_posts_path do %>
-      <input type="hidden" name="ids[]" value="">
       <%= render 'admin/communication/website/posts/list', posts: @posts, selectable: true %>
       <div class="card-footer">
         <% if @posts.total_pages > 1 %>
-          <div class="float-end">
+          <div class="float-end mb-3">
             <%= paginate @posts, theme: 'bootstrap-5' %>
           </div>
         <% end %>
-        <div class="row align-items-center">
-          <div class="col-auto">
-            Modifier la sélection
-          </div>
-          <div class="col-auto">
-            <select name="published" class="form-select">
-              <option value="false">Non publiée</option>
-              <option value="true">Publiée</option>
-            </select>
-          </div>
-          <div class="col-auto">
-            <input type="submit" value="Enregistrer" class="btn btn-primary">
+        <div data-batch-selectable-role="actions-container">
+          <div class="d-flex align-items-center">
+            <div class="col-auto me-3">
+              <%= t('batch_selectable.title') %>
+            </div>
+            <div class="col-auto me-3">
+              <select name="published" class="form-select">
+                <option value="false"><%= t('communication.website.posts.unpublished') %></option>
+                <option value="true"><%= t('communication.website.posts.published') %></option>
+              </select>
+            </div>
+            <div class="col-auto me-3">
+              <%= submit_tag t("save"), class: "btn btn-primary" %>
+            </div>
           </div>
         </div>
       </div>
diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb
index 691715882917faff3e85378614de7146a79cae07..f22889e4730e48e6ae0633f5f47641eb51bf69c3 100644
--- a/app/views/devise/sessions/new.html.erb
+++ b/app/views/devise/sessions/new.html.erb
@@ -10,40 +10,49 @@
   </div>
   <div class="col-md-6">
     <h2 class="mb-4"><%= t('login.already_registered') %></h2>
-    <%= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
-      <div class="form-inputs">
-        <%= f.input :email,
-                    required: false,
-                    autofocus: true,
-                    input_html: { autocomplete: "email" } %>
-        <div class="mb-3 password optional user_password password_with_hints">
-          <%= f.input :password,
-                      as: :password_with_hints,
-                      allow_password_uncloaking: true,
+    <% if current_university.has_sso? %>
+      <p><%= link_to t('login.sign_in_with_sso'), omniauth_authorize_path(resource_name, current_university.sso_provider), class: 'btn btn-primary' %></p>
+      <p><%= t('login.or') %></p>
+      <a href="#collapseLoginForm" class="btn btn-primary mb-3" data-bs-toggle="collapse"><%= t('login.sign_in_with_credentials') %></a>
+    <% end %>
+
+    <div class="<%= 'collapse' if current_university.has_sso? %> <%= 'show' unless alert.blank? %>" id="collapseLoginForm">
+
+      <%= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
+        <div class="form-inputs">
+          <%= f.input :email,
                       required: false,
-                      wrapper: false,
-                      input_html: { autocomplete: "current-password" } %>
-          <small>
-            <%= link_to t("devise.passwords.new.forgot_your_password"), new_password_path(resource_name) %>
-          </small>
+                      autofocus: true,
+                      input_html: { autocomplete: "email" } %>
+          <div class="mb-3 password optional user_password password_with_hints">
+            <%= f.input :password,
+                        as: :password_with_hints,
+                        allow_password_uncloaking: true,
+                        required: false,
+                        wrapper: false,
+                        input_html: { autocomplete: "current-password" } %>
+            <small>
+              <%= link_to t("devise.passwords.new.forgot_your_password"), new_password_path(resource_name) %>
+            </small>
+          </div>
+          <%= f.input :remember_me, as: :boolean if devise_mapping.rememberable? %>
         </div>
-        <%= f.input :remember_me, as: :boolean if devise_mapping.rememberable? %>
-      </div>
 
-      <div class="form-actions">
-        <%= f.button :submit, t(".sign_in"), class: 'btn btn-primary' %>
+        <div class="form-actions">
+          <%= f.button :submit, t(".sign_in"), class: 'btn btn-primary' %>
 
-        <div class="mt-3">
+        </div>
+      <% end %>
+    </div>
+    <div class="mt-3">
+      <% if devise_mapping.confirmable? %>
+        <%= link_to t('devise.shared.links.didn_t_receive_confirmation_instructions'), new_confirmation_path(resource_name) %><br />
+      <% end %>
 
-          <% if devise_mapping.confirmable? %>
-            <%= link_to t('devise.shared.links.didn_t_receive_confirmation_instructions'), new_confirmation_path(resource_name) %><br />
-          <% end %>
+      <% if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) %>
+        <%= link_to t('devise.shared.links.didn_t_receive_unlock_instructions'), new_unlock_path(resource_name) %><br />
+      <% end %>
+    </div>
 
-          <% if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) %>
-            <%= link_to t('devise.shared.links.didn_t_receive_unlock_instructions'), new_unlock_path(resource_name) %><br />
-          <% end %>
-        </div>
-      </div>
-    <% end %>
   </div>
 </div>
diff --git a/app/views/server/layouts/application.html.erb b/app/views/server/layouts/application.html.erb
index b3f5fb58c8f5e9ad6bb4448f3dd2b27ac456056d..f03b783ffba7661f48a548598cff361b78abdaf9 100644
--- a/app/views/server/layouts/application.html.erb
+++ b/app/views/server/layouts/application.html.erb
@@ -11,7 +11,7 @@
     <%= stylesheet_link_tag 'admin', media: 'all' %>
     <%= favicon_link_tag 'favicon.png' %>
   </head>
-  <body data-theme="default" data-layout="fluid" data-sidebar-position="left" data-sidebar-behavior="sticky">
+  <body class="<%= body_classes %>" data-theme="default" data-layout="fluid" data-sidebar-position="left" data-sidebar-behavior="sticky">
     <div class="toasts-container" style="position: fixed; top: 20px; right: 20px; z-index: 100000;">
       <% unless notice.nil? %>
         <div class="js-notyf-notice d-none">
diff --git a/app/views/server/universities/_form.html.erb b/app/views/server/universities/_form.html.erb
index 4fbcd19ddbd32ab09c2c29d509ee7c74df4f6015..c3e2a4743b9860e325cb2746712564c7c1375b42 100644
--- a/app/views/server/universities/_form.html.erb
+++ b/app/views/server/universities/_form.html.erb
@@ -27,6 +27,20 @@
                   direct_upload: true %>
     </div>
   </div>
+
+  <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>
+
   <h3 class="mt-5"><%= t('university.invoice_informations') %></h3>
   <div class="row">
     <div class="col-md-4">
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 43596a34d3e4d589b1abaf5020bf94558c1da37d..a410372234a5f728412e3ac50d9693a71c03124b 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -272,6 +272,7 @@ Devise.setup do |config|
   # Add a new OmniAuth provider. Check the wiki for more information on setting
   # up on your models and hooks.
   # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
+  config.omniauth :saml, setup: true
 
   # ==> Warden configuration
   # If you want to use other strategies, that are not supported by Devise, or
diff --git a/config/locales/communication/en.yml b/config/locales/communication/en.yml
index 39a6494acc73829e34e1a92b6d99e77c52ba672f..22b74a1f11458031f17f16d7a46bd3f5a2175128 100644
--- a/config/locales/communication/en.yml
+++ b/config/locales/communication/en.yml
@@ -113,7 +113,7 @@ en:
         featured_image: Featured image
         featured_image_alt: Alt text
         pinned: Pinned
-        published: Published
+        published: Published?
         published_at: Publication date
         slug: Slug
         text: Text
@@ -303,6 +303,9 @@ en:
             title: Educational team
       posts:
         new_curation: New curation
+        published: Published
+        successful_batch_update: Posts have been updated succesfully
+        unpublished: Unpublished
       see_all: See the full list (%{number} elements)
   enums:
     communication:
diff --git a/config/locales/communication/fr.yml b/config/locales/communication/fr.yml
index ae6edc47f0f39fd380c8397264f9d0da3849dc0f..0fce32c9e07ef3652c8a2a1f5ccccd8ae127f402 100644
--- a/config/locales/communication/fr.yml
+++ b/config/locales/communication/fr.yml
@@ -113,7 +113,7 @@ fr:
         featured_image: Image à la une
         featured_image_alt: Texte alternatif
         pinned: Mis en avant
-        published: Publié
+        published: Publié ?
         published_at: Date de publication
         slug: Slug
         text: Texte
@@ -305,7 +305,9 @@ fr:
             title: Équipe pédagogique
       posts:
         new_curation: Nouvelle curation
+        published: Publiée
         successful_batch_update: Les actualités ont bien été mises à jour
+        unpublished: Non publiée
       see_all: Voir la liste complète (%{number} éléments)
   enums:
     communication:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 6d9128aca701db5f2b89b871a73482cc2116ec53..e150188d2bba3cd947c520368fc7cef9d5c8281b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -56,6 +56,8 @@ en:
       not_locked_html: '<i>%{model}</i> was not locked.'
       successfully_unlocked_html: "<i>%{model}</i> was successfully unlocked."
     will_be_published_html: "<i>%{model}</i> will soon be published."
+  batch_selectable:
+    title: Edit the selection
   content: Content
   cookies_consent_choice: Cookies consent choice
   cookies_policy: Cookies policy
@@ -119,6 +121,9 @@ en:
     already_registered: Already registered?
     not_registered_yet: Not registered yet?
     not_registered_yet_details: Register if you have no account yet.
+    or: or
+    sign_in_with_credentials: Sign in with credentials
+    sign_in_with_sso: Sign in through SSO
     subtitle: Sign in to your account to continue
   menu:
     admin: Admin
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index ce749793162a5b13cb214707753688e744513c1f..b04ef9f9d665aa4b5e3397ca89fe670bb5b04fcd 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -56,6 +56,8 @@ fr:
       not_locked_html: "<i>%{model}</i> n'était pas verrouillé(e)."
       successfully_unlocked_html: "<i>%{model}</i> a bien été déverrouillé(e)."
     will_be_published_html: "<i>%{model}</i> va bientôt être publié(e)."
+  batch_selectable:
+    title: Modifier la sélection
   content: Contenu
   cookies_consent_choice: Choix en matière de cookies
   cookies_policy: Politique de cookies
@@ -119,6 +121,9 @@ fr:
     already_registered: Déjà inscrit ?
     not_registered_yet: Pas encore inscrit ?
     not_registered_yet_details: Inscrivez-vous si vous n'avez pas encore de compte.
+    or: ou bien
+    sign_in_with_credentials: Se connecter avec ses identifiants
+    sign_in_with_sso: Se connecter en SSO
     subtitle: Vous devez être authentifié pour continuer
   menu:
     admin: Admin
diff --git a/config/locales/university/en.yml b/config/locales/university/en.yml
index dcd43222b31c92c6049f9ab1ea77c12c41bc0948..63a83a4e13d8231a460c218b51e5f82d13f3b9b9 100644
--- a/config/locales/university/en.yml
+++ b/config/locales/university/en.yml
@@ -5,6 +5,7 @@ en:
         address: Address
         city: City
         country: Country
+        has_sso: Has SSO?
         identifier: Identifier
         invoice_amount: Invoice amount
         invoice_date: Invoice date
@@ -14,6 +15,9 @@ en:
         public: Public
         public_or_private: Public/private
         sms_sender_name: SMS sender name
+        sso_cert: Certificate
+        sso_name_identifier_format: Name Identifier Format
+        sso_target_url: Target URL
         url: URL
         zipcode: Zipcode
       university/person:
@@ -125,3 +129,4 @@ en:
     person:
       administrator_roles: Administrator roles
       taught_programs: Taught programs
+    sso: SSO
diff --git a/config/locales/university/fr.yml b/config/locales/university/fr.yml
index a401225ae0a8c5cf267e499a01385dd8b14804fe..f263aead1db38c3fbd9063474e72394808a2a9e9 100644
--- a/config/locales/university/fr.yml
+++ b/config/locales/university/fr.yml
@@ -5,6 +5,7 @@ fr:
         address: Adresse
         city: Ville
         country: Pays
+        has_sso: A un SSO ?
         identifier: Identifiant
         invoice_amount: Montant de facturation
         invoice_date: Date de facturation
@@ -14,6 +15,9 @@ fr:
         public: Public
         public_or_private: Public/privé
         sms_sender_name: Nom de l'expéditeur SMS
+        sso_cert: Certificat
+        sso_name_identifier_format: Name Identifier Format
+        sso_target_url: URL cible
         url: 'URL'
         zipcode: Code postal
       university/person:
@@ -125,3 +129,4 @@ fr:
     person:
       administrator_roles: Rôles administratifs
       taught_programs: Formations enseignées
+    sso: SSO
diff --git a/config/routes.rb b/config/routes.rb
index 5f10a141b009ccfa85992cf44ce2902944570d54..5e9f0af119a7ec261e2d338a89ae71c11d8b3d96 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -5,12 +5,17 @@ Rails.application.routes.draw do
 
   devise_for :users, controllers: {
     confirmations: 'users/confirmations',
+    omniauth_callbacks: 'users/omniauth_callbacks',
     passwords: 'users/passwords',
     registrations: 'users/registrations',
     sessions: 'users/sessions',
     unlocks: 'users/unlocks'
   }
 
+  devise_scope :user do
+    match '/users/auth/saml/setup' => 'users/omniauth_callbacks#saml_setup', via: [:get, :post]
+  end
+
   namespace :admin do
     resources :users do
       patch 'unlock' => 'users#unlock', on: :member