diff --git a/Dockerfile.frontend-base b/Dockerfile.frontend-base
index add3446e6d9122ca0e70a1965b469a3cb71e9293..b57ead0e969a22f800264578ef01a31922ec584e 100644
--- a/Dockerfile.frontend-base
+++ b/Dockerfile.frontend-base
@@ -33,6 +33,7 @@ RUN apt-get update \
         ruby-bundler \
         ruby-ffi \
         sphinxsearch \
+        python3-yaml \
         supervisor \
         time \
         tzdata
diff --git a/docker/frontend-docker-entrypoint.sh b/docker/frontend-docker-entrypoint.sh
index 5f1a1c008db47dcda1ee1e73ef1abb769e6d81a2..8fbbcfd0f4aa10ff6593b38cd68c4c879cd0b403 100755
--- a/docker/frontend-docker-entrypoint.sh
+++ b/docker/frontend-docker-entrypoint.sh
@@ -10,7 +10,7 @@ chmod a+rwxt /tmp
 
 /opt/configure-app.sh
 /opt/configure-db.sh
-#/opt/configure-sso.py
+/opt/configure-sso.py
 
 : ${OBS_FRONTEND_WORKERS:=4}
 export OBS_FRONTEND_WORKERS
diff --git a/src/api/Gemfile b/src/api/Gemfile
index c1a70df6ec04e6f64eacae1b19a5d48167a11c0d..ee8ce23fdd83945f08d0d7ace4bd5ca31ca5abcc 100644
--- a/src/api/Gemfile
+++ b/src/api/Gemfile
@@ -85,6 +85,13 @@ gem 'deep_cloneable', '~> 2.4.0'
 # Server-side datatables
 gem 'ajax-datatables-rails'
 
+# SSO
+gem 'omniauth'
+gem 'omniauth-gitlab'
+gem 'omniauth-github'
+gem 'omniauth_openid_connect', '~> 0.4.0'
+gem 'omniauth-rails_csrf_protection'
+
 group :development, :production do
   # to have the delayed job daemon
   gem 'daemons'
diff --git a/src/api/Gemfile.lock b/src/api/Gemfile.lock
index 90e9c5b4212a04b580fa7d64d0c997714cf9b32a..b460054fb3c9713d7d880e16440d8563a1f534e7 100644
--- a/src/api/Gemfile.lock
+++ b/src/api/Gemfile.lock
@@ -52,6 +52,7 @@ GEM
       activerecord (>= 3.0.0)
     addressable (2.6.0)
       public_suffix (>= 2.0.2, < 4.0)
+    aes_key_wrap (1.1.0)
     airbrake (8.0.1)
       airbrake-ruby (~> 3.0)
     airbrake-ruby (3.1.0)
@@ -62,9 +63,11 @@ GEM
     ansi (1.5.0)
     arel (9.0.0)
     ast (2.4.0)
+    attr_required (1.0.1)
     autoprefixer-rails (9.6.0)
       execjs
     bcrypt (3.1.13)
+    bindata (2.4.10)
     bootstrap (4.3.1)
       autoprefixer-rails (>= 9.1.0)
       popper_js (>= 1.14.3, < 2)
@@ -154,6 +157,29 @@ GEM
       tty-pager (~> 0.12.0)
       tty-screen (~> 0.6.5)
       tty-tree (~> 0.3.0)
+    faraday (1.9.3)
+      faraday-em_http (~> 1.0)
+      faraday-em_synchrony (~> 1.0)
+      faraday-excon (~> 1.1)
+      faraday-httpclient (~> 1.0)
+      faraday-multipart (~> 1.0)
+      faraday-net_http (~> 1.0)
+      faraday-net_http_persistent (~> 1.0)
+      faraday-patron (~> 1.0)
+      faraday-rack (~> 1.0)
+      faraday-retry (~> 1.0)
+      ruby2_keywords (>= 0.0.4)
+    faraday-em_http (1.0.0)
+    faraday-em_synchrony (1.0.0)
+    faraday-excon (1.1.0)
+    faraday-httpclient (1.0.1)
+    faraday-multipart (1.0.3)
+      multipart-post (>= 1.2, < 3)
+    faraday-net_http (1.0.1)
+    faraday-net_http_persistent (1.2.0)
+    faraday-patron (1.0.0)
+    faraday-rack (1.0.0)
+    faraday-retry (1.0.3)
     feature (1.4.0)
     ffi (1.11.1)
     flot-rails (0.0.7)
@@ -180,11 +206,13 @@ GEM
       rubocop (>= 0.50.0)
       sysexits (~> 1.1)
     hashdiff (0.4.0)
+    hashie (5.0.0)
     html2haml (2.2.0)
       erubis (~> 2.7.0)
       haml (>= 4.0, < 6)
       nokogiri (>= 1.6.0)
       ruby_parser (~> 3.5)
+    httpclient (2.8.3)
     i18n (1.8.11)
       concurrent-ruby (~> 1.0)
     influxdb (0.7.0)
@@ -200,6 +228,11 @@ GEM
     jquery-ui-rails (4.2.1)
       railties (>= 3.2.16)
     json (2.5.1)
+    json-jwt (1.13.0)
+      activesupport (>= 4.2)
+      aes_key_wrap
+      bindata
+    jwt (2.3.0)
     kaminari (1.2.1)
       activesupport (>= 4.1.0)
       kaminari-actionview (= 1.2.1)
@@ -245,6 +278,9 @@ GEM
     momentjs-rails (2.20.1)
       railties (>= 3.1)
     mousetrap-rails (1.4.6)
+    multi_json (1.15.0)
+    multi_xml (0.6.0)
+    multipart-post (2.1.1)
     mysql2 (0.5.2)
     nio4r (2.5.8)
     nokogiri (1.11.7)
@@ -252,6 +288,42 @@ GEM
       racc (~> 1.4)
     nokogumbo (2.0.1)
       nokogiri (~> 1.8, >= 1.8.4)
+    oauth2 (1.4.7)
+      faraday (>= 0.8, < 2.0)
+      jwt (>= 1.0, < 3.0)
+      multi_json (~> 1.3)
+      multi_xml (~> 0.5)
+      rack (>= 1.2, < 3)
+    omniauth (2.0.4)
+      hashie (>= 3.4.6)
+      rack (>= 1.6.2, < 3)
+      rack-protection
+    omniauth-github (2.0.0)
+      omniauth (~> 2.0)
+      omniauth-oauth2 (~> 1.7.1)
+    omniauth-gitlab (3.0.0)
+      omniauth (~> 2.0)
+      omniauth-oauth2 (~> 1.7.1)
+    omniauth-oauth2 (1.7.2)
+      oauth2 (~> 1.4)
+      omniauth (>= 1.9, < 3)
+    omniauth-rails_csrf_protection (1.0.0)
+      actionpack (>= 4.2)
+      omniauth (~> 2.0)
+    omniauth_openid_connect (0.4.0)
+      addressable (~> 2.5)
+      omniauth (>= 1.9, < 3)
+      openid_connect (~> 1.1)
+    openid_connect (1.3.0)
+      activemodel
+      attr_required (>= 1.0.0)
+      json-jwt (>= 1.5.0)
+      rack-oauth2 (>= 1.6.1)
+      swd (>= 1.0.0)
+      tzinfo
+      validate_email
+      validate_url
+      webfinger (>= 1.0.1)
     parallel (1.17.0)
     parser (2.6.3.0)
       ast (~> 2.4.0)
@@ -290,6 +362,14 @@ GEM
       activesupport (>= 3.0.0)
     racc (1.5.2)
     rack (2.2.3)
+    rack-oauth2 (1.19.0)
+      activesupport
+      attr_required
+      httpclient
+      json-jwt (>= 1.11.0)
+      rack (>= 2.1.0)
+    rack-protection (2.1.0)
+      rack
     rack-test (1.1.0)
       rack (>= 1.0, < 3)
     rails (5.2.6.2)
@@ -377,6 +457,7 @@ GEM
       rubocop (>= 0.60.0)
     ruby-ldap (0.9.20)
     ruby-progressbar (1.10.1)
+    ruby2_keywords (0.0.5)
     ruby_parser (3.13.1)
       sexp_processor (~> 4.9)
     rubyzip (2.0.0)
@@ -420,6 +501,10 @@ GEM
       unicode-display_width (~> 1.5)
       unicode_utils (~> 1.4)
     strings-ansi (0.1.0)
+    swd (1.3.0)
+      activesupport (>= 3)
+      attr_required (>= 0.0.5)
+      httpclient (>= 2.4)
     sysexits (1.2.0)
     tdigest (0.1.1)
       rbtree (~> 0.4.2)
@@ -455,9 +540,18 @@ GEM
     unicode-display_width (1.6.0)
     unicode_utils (1.4.0)
     uniform_notifier (1.12.1)
+    validate_email (0.1.6)
+      activemodel (>= 3.0)
+      mail (>= 2.2.5)
+    validate_url (1.0.13)
+      activemodel (>= 3.0.0)
+      public_suffix
     vcr (5.0.0)
     voight_kampff (1.1.3)
       rack (>= 1.4, < 3.0)
+    webfinger (1.2.0)
+      activesupport
+      httpclient (>= 2.4)
     webmock (3.6.0)
       addressable (>= 2.3.6)
       crack (>= 0.3.2)
@@ -532,6 +626,11 @@ DEPENDENCIES
   mousetrap-rails
   mysql2
   nokogiri
+  omniauth
+  omniauth-github
+  omniauth-gitlab
+  omniauth-rails_csrf_protection
+  omniauth_openid_connect
   peek
   peek-dalli
   peek-host
diff --git a/src/api/app/controllers/webui/session_controller.rb b/src/api/app/controllers/webui/session_controller.rb
index a6a0032a22b34ebd63ed10a984cd9b6d6aaeefa2..e602cf7fe06bf614f92f787908b1f1ae8ae2d8d3 100644
--- a/src/api/app/controllers/webui/session_controller.rb
+++ b/src/api/app/controllers/webui/session_controller.rb
@@ -1,7 +1,7 @@
 class Webui::SessionController < Webui::WebuiController
   before_action :kerberos_auth, only: [:new]
 
-  skip_before_action :check_anonymous, only: [:create]
+  skip_before_action :check_anonymous, only: [:new, :create, :sso, :sso_callback, :sso_confirm, :do_sso_confirm]
 
   def new
     switch_to_webui2
@@ -40,10 +40,108 @@ class Webui::SessionController < Webui::WebuiController
     redirect_on_logout
   end
 
+  def sso
+    switch_to_webui2
+  end
+
+  def sso_callback
+    @auth_hash = request.env['omniauth.auth']
+    user = User.find_with_omniauth(@auth_hash['info'])
+
+    unless user
+      session[:auth] = @auth_hash['info']
+      session[:auth]['provider'] = @auth_hash['provider']
+      redirect_to(sso_confirm_path)
+      return
+    end
+
+    unless user.is_active?
+      RabbitmqBus.send_to_bus('metrics', 'login,access_point=webui,failure=disabled value=1')
+      redirect_to(root_path, error: 'Your account is disabled. Please contact the administrator for details.')
+      return
+    end
+
+    User.session = user
+    session[:login] = user.login
+    Rails.logger.debug "Authenticated user '#{user.login}'"
+
+    redirect_on_login
+  end
+
+  def sso_confirm
+    switch_to_webui2
+    auth_info = session[:auth]
+
+    if !auth_info
+      redirect_to sso_path
+      return
+    end
+
+    # Try to derive a username from the information available,
+    # falling back to full name if nothing else works
+    @derived_username = auth_info['username'] ||
+                        auth_info['nickname'] ||
+                        auth_info['email'] ||
+                        auth_info['name']
+
+    # Some providers set username or nickname to an email address
+    # Derive the username from the local part of the email address,
+    # if possible. The full name with spaces replaced by underscores
+    # is the last resort fallback.
+    @derived_username = @derived_username.rpartition("@")[0] if @derived_username.include? "@"
+    @derived_username = @derived_username.gsub(' ', '_')
+  end
+
+  def do_sso_confirm
+    required_parameters :login
+    auth_info = session[:auth]
+
+    if !auth_info
+      redirect_to sso_path
+      return
+    end
+
+    existing_user = User.find_by_login(params[:login])
+    if existing_user
+      flash[:error] = "Username #{params[:login]} is already taken, choose a different one"
+      redirect_to sso_confirm_path
+      return
+    end
+
+    begin
+      user = User.create_with_omniauth(auth_info, params[:login])
+    rescue ActiveRecord::ActiveRecordError
+      flash[:error] = "Invalid username, please try a different one"
+      redirect_to sso_confirm_path
+      return
+    end
+
+    unless user
+      flash[:error] = "Cannot create user"
+      redirect_to root_path
+      return
+    end
+
+    unless user.is_active?
+      RabbitmqBus.send_to_bus('metrics', 'login,access_point=webui,failure=disabled value=1')
+      redirect_to(root_path, error: 'Your account needs to be confirmed by the administrator.')
+      return
+    end
+
+    User.session = user
+    session[:login] = user.login
+    Rails.logger.debug "Authenticated user '#{user.login}'"
+
+    redirect_on_login
+  end
+
+
   private
 
   def redirect_on_login
-    if referer_was_login?
+    if !referer_was_ours?
+      redirect_to root_path
+    elsif referer_was_login?
       redirect_to user_show_path(User.session!)
     else
       redirect_back(fallback_location: root_path)
@@ -54,11 +152,24 @@ class Webui::SessionController < Webui::WebuiController
     if CONFIG['proxy_auth_mode'] == :on
       redirect_to CONFIG['proxy_auth_logout_page']
     else
-      redirect_back(fallback_location: root_path)
+      redirect_to root_path
     end
   end
 
+  def referer_was_ours?
+    return false unless request.referer
+
+    parsed = URI.parse(request.referer)
+    parsed.host == request.host and parsed.port == request.port
+  end
+
   def referer_was_login?
-    request.referer && request.referer.end_with?(session_new_path)
+    return false unless request.referer
+
+    parsed = URI.parse(request.referer)
+    return false unless parsed.host == request.host
+    return false unless parsed.port == request.port
+
+    parsed.path == session_new_path or parsed.path.starts_with?(sso_path)
   end
 end
diff --git a/src/api/app/controllers/webui/user_controller.rb b/src/api/app/controllers/webui/user_controller.rb
index 6c143616b2a677a19b20c718d3e123983f37b661..aaa84aabadbe66ffe0a9d3f87c6a34c5164cdfd4 100644
--- a/src/api/app/controllers/webui/user_controller.rb
+++ b/src/api/app/controllers/webui/user_controller.rb
@@ -153,9 +153,10 @@ class Webui::UserController < Webui::WebuiController
       return
     end
 
-    if user.authenticate(params[:password])
+    if user.authenticate(params[:password]) or user.password_invalid?
       user.password = params[:new_password]
       user.password_confirmation = params[:repeat_password]
+      user.deprecated_password_hash_type = nil
 
       if user.save
         flash[:success] = 'Your password has been changed successfully.'
diff --git a/src/api/app/models/user.rb b/src/api/app/models/user.rb
index e545dcbf6b39085aad1935cd392fdeab7a20e206..1156e291a755cc7bbd515bb48c693edcbeb97a79 100644
--- a/src/api/app/models/user.rb
+++ b/src/api/app/models/user.rb
@@ -164,12 +164,12 @@ class User < ApplicationRecord
     create!(attributes.merge(password: SecureRandom.base64(48)))
   end
 
-  def self.create_ldap_user(attributes = {})
-    user = create_user_with_fake_pw!(attributes.merge(state: default_user_state, adminnote: 'User created via LDAP'))
+  def self.create_external_user(attributes = {})
+    user = create_user_with_fake_pw!(attributes.merge(state: default_user_state))
 
     return user if user.errors.empty?
 
-    logger.info("Cannot create ldap userid: '#{login}' on OBS. Full log: #{user.errors.full_messages.to_sentence}")
+    logger.info("Cannot create external userid: '#{login}' on OBS. Full log: #{user.errors.full_messages.to_sentence}")
     return
   end
 
@@ -214,13 +214,43 @@ class User < ApplicationRecord
       logger.debug("Email: #{ldap_info[0]}")
       logger.debug("Name : #{ldap_info[1]}")
 
-      user = create_ldap_user(login: login, email: ldap_info[0], realname: ldap_info[1])
+      user = create_external_user(login: login,
+                                  email: ldap_info[0],
+                                  realname: ldap_info[1],
+                                  adminnote: "User created via LDAP")
     end
 
     user.mark_login!
     user
   end
 
+  def self.find_with_omniauth(auth)
+    if auth
+      email = auth['email']
+      user = find_by_email(email)
+      if user
+        user.mark_login!
+
+        return user
+      end
+    end
+  end
+
+  def self.create_with_omniauth(auth, login)
+    provider = CONFIG['sso_auth'][auth['provider']]['description']
+    email = auth['email']
+    logger.debug("Creating OmniAuth user for #{provider}")
+    logger.debug("Email: #{email}")
+    logger.debug("Name : #{auth['name']}")
+
+    user = create_external_user(login: login,
+                                email: email,
+                                realname: auth['name'],
+                                deprecated_password_hash_type: 'invalid',
+                                adminnote: "User created via #{provider}")
+    user.mark_login!
+    user
+  end
   # Currently logged in user or nobody user if there is no user logged in.
   # Use this to check permissions, but don't treat it as logged in user. Check
   # is_nobody? on the returned object
@@ -870,6 +900,10 @@ class User < ApplicationRecord
     end
   end
 
+  def password_invalid?
+    self.deprecated_password_hash_type == 'invalid'
+  end
+
   private
 
   # The currently logged in user (might be nil). It's reset after
diff --git a/src/api/app/views/layouts/webui2/_login_form.html.haml b/src/api/app/views/layouts/webui2/_login_form.html.haml
index 981f9329bb694b67c4162b050cb0d881d03ef4b2..504bb75155067254a418423c9dc663a68a503481 100644
--- a/src/api/app/views/layouts/webui2/_login_form.html.haml
+++ b/src/api/app/views/layouts/webui2/_login_form.html.haml
@@ -6,7 +6,7 @@
     Log In
   .dropdown-menu.dropdown-menu-right.shadow-lg.bg-dark{ 'aria-labelledby': 'dropdownMenuButton' }
     .px-4.py-3#login-form
-      = form_tag(form_url, options) do
+      = form_tag(form_url, options.merge({class: 'pb-3'})) do
         - if proxy
           = hidden_field_tag(:context, 'default')
           = hidden_field_tag(:proxypath, 'reserve')
@@ -21,3 +21,9 @@
         .float-right
           = submit_tag('Log In', class: 'btn btn-success')
         .clearfix
+      .form-group.border-top.pt-3
+        %p{class: 'text-light'} Or sign in with:
+        .form-inline
+          - CONFIG['sso_auth'].each do |name, options|
+            = form_tag "#{sso_path}/#{name}", method: 'post', class: 'px-1' do
+              = button_tag options['description'], class: 'btn btn-light'
diff --git a/src/api/app/views/webui/session/_form.html.haml b/src/api/app/views/webui/session/_form.html.haml
index 95901a4a3b71b7c2bc5a4aca07993c052e06d002..2d0c15c1f27f478b58f9377967307b5254dc239b 100644
--- a/src/api/app/views/webui/session/_form.html.haml
+++ b/src/api/app/views/webui/session/_form.html.haml
@@ -9,6 +9,12 @@
     %input#user-password{ name: "password", size: "30", type: "password" }/
   %p
     %input.primary#log-in-button{ name: "login", type: "submit", value: "Log In" }/
+- if CONFIG['sso_auth']
+  %p
+    Sign in using one of the following external services:
+  %p
+    - CONFIG['sso_auth'].each do |name, options|
+      = button_tag "Log in with #{options['description']}", formaction: "#{sso_path}/#{name}"
 %p
   - if CONFIG['proxy_auth_mode'] == :on
     Or
diff --git a/src/api/app/views/webui2/webui/session/_sso.html.haml b/src/api/app/views/webui2/webui/session/_sso.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..32d4c98f25968ad2d7581212fb5394286fb1051a
--- /dev/null
+++ b/src/api/app/views/webui2/webui/session/_sso.html.haml
@@ -0,0 +1,8 @@
+%h3= 'Sign in with SSO'
+
+.form-group
+  %p Sign in with your external account:
+  .form-inline
+    - CONFIG['sso_auth'].each do |name, options|
+      = form_tag "#{sso_path}/#{name}", method: 'post', class: 'px-1' do
+        = button_tag options['description'], class: 'btn btn-outline-dark'
diff --git a/src/api/app/views/webui2/webui/session/new.html.haml b/src/api/app/views/webui2/webui/session/new.html.haml
index 7ac24065438ff951059cb09d280eaf80815d148e..3d8e681d860a518343b2ff0da16b40418918d050 100644
--- a/src/api/app/views/webui2/webui/session/new.html.haml
+++ b/src/api/app/views/webui2/webui/session/new.html.haml
@@ -1,8 +1,8 @@
 - @pagetitle = 'Please Log In'
 
 .card
-  .card-body#loginform
-    .col-lg-6.pl-0
+  .card-body.row#loginform
+    .col-lg-6.pl-3
       - if CONFIG['proxy_auth_mode'] == :on
         = render partial: 'form', locals: { form_url: CONFIG['proxy_auth_login_page'],
                                             options: { method: :post, enctype: 'application/x-www-form-urlencoded' },
@@ -17,3 +17,6 @@
                                             options: { method: :post },
                                             proxy: false,
                                             pagetitle: @pagetitle }
+    - if CONFIG['sso_auth']
+      .col-lg-6.pl-3.border-left
+        = render partial: 'sso'
diff --git a/src/api/app/views/webui2/webui/session/sso.html.haml b/src/api/app/views/webui2/webui/session/sso.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..f78e2a98c6da234fa4ca5850addd0075e59220e9
--- /dev/null
+++ b/src/api/app/views/webui2/webui/session/sso.html.haml
@@ -0,0 +1,6 @@
+- @pagetitle = 'Sign In with SSO'
+
+.card
+  .card-body#loginform
+    .col-lg-6.pl-0
+      = render partial: 'sso'
diff --git a/src/api/app/views/webui2/webui/session/sso_confirm.html.haml b/src/api/app/views/webui2/webui/session/sso_confirm.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..04ea69daac387c75cafd12692bf4f086d0cce996
--- /dev/null
+++ b/src/api/app/views/webui2/webui/session/sso_confirm.html.haml
@@ -0,0 +1,17 @@
+- @pagetitle = 'First Login'
+
+.card
+  .card-body#loginform
+    .col-lg-6.pl-0
+      - if can_register
+        %h3= @pagetitle
+        %p Since this is your first time you sign in, you need to choose your username.
+        = form_tag({ controller: 'session', action: 'sso_confirm', method: :post }, class: 'sign-up', autocomplete: 'off') do
+          .form-group
+            = label_tag 'login', 'Username:'
+            %abbr.text-danger{ title: 'required' } *
+            = text_field_tag 'login', @derived_username, placeholder: 'Username', autocomplete: 'off', class: 'form-control', required: true
+          = submit_tag('Confirm and Log In', class: 'btn btn-primary')
+      - else
+        %p Sorry, only existing users can sign in.
+
diff --git a/src/api/app/views/webui2/webui/user/_password_dialog.html.haml b/src/api/app/views/webui2/webui/user/_password_dialog.html.haml
index e3b100b3092b1127e79d4ca92d306bf60f682ec7..534300e03ad038b9c0234634da715375b9098239 100644
--- a/src/api/app/views/webui2/webui/user/_password_dialog.html.haml
+++ b/src/api/app/views/webui2/webui/user/_password_dialog.html.haml
@@ -5,9 +5,11 @@
         %h5.modal-title#branch-modal-label Change Your Password
       = form_tag(action: 'change_password') do
         .modal-body
-          .form-group
-            = label_tag :password, 'Current Password:'
-            = text_field_tag :password, nil, type: 'password', required: 'true', class: 'form-control'
+          - if User.session
+            - unless User.session.password_invalid?
+              .form-group
+                = label_tag :password, 'Current Password:'
+                = text_field_tag :password, nil, type: 'password', required: 'true', class: 'form-control'
           .form-group
             = label_tag :new_password, 'New Password:'
             = text_field_tag :new_password, nil, type: 'password', autocomplete: 'off', required: 'true', class: 'form-control'
diff --git a/src/api/config/auth.yml.example b/src/api/config/auth.yml.example
new file mode 100644
index 0000000000000000000000000000000000000000..c5a9ce375e591e97f097d45384a1bdfbfc8b2a16
--- /dev/null
+++ b/src/api/config/auth.yml.example
@@ -0,0 +1,8 @@
+fdo-gitlab:
+  strategy: gitlab
+  description: Freedesktop.org GitLab
+  scope: read_user openid profile email
+  client_id: hexhexhexhex
+  client_secret: hexhexhexhex
+  client_options:
+    site: https://gitlab.freedesktop.org/api/v4
diff --git a/src/api/config/initializers/omniauth.rb b/src/api/config/initializers/omniauth.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5d8f90823c8e0f9d72f08c17bf453e0614bb3479
--- /dev/null
+++ b/src/api/config/initializers/omniauth.rb
@@ -0,0 +1,21 @@
+OmniAuth.config.path_prefix = '/session/sso'
+
+path = Rails.root.join("config", "auth.yml")
+
+CONFIG['sso_auth'] = Hash.new
+
+if File.exist? path
+  begin
+      CONFIG['sso_auth'] = YAML.load_file(path)
+  rescue Exception
+      puts "Error while parsing config file #{path}"
+  end
+
+  Rails.application.config.middleware.use OmniAuth::Builder do
+      CONFIG['sso_auth'].each do |name, options|
+          options[:name] = name
+          provider (options['strategy'] || name), options
+          options['description'] ||= OmniAuth::Utils.camelize(name)
+      end
+  end
+end
diff --git a/src/api/config/routes.rb b/src/api/config/routes.rb
index d1b40af86d7b61e85982fc98aa5be74a6aea7eb8..7b0b648a0739297d7fd96195b55430f97707a0b9 100644
--- a/src/api/config/routes.rb
+++ b/src/api/config/routes.rb
@@ -424,6 +424,10 @@ OBSApi::Application.routes.draw do
     controller 'webui/session' do
       get 'session/new' => :new
       post 'session/create' => :create
+      get 'session/sso' => :sso, as: 'sso'
+      get 'session/sso/:provider/callback' => :sso_callback
+      get 'session/sso/:provider/confirm' => :sso_confirm, as: 'sso_confirm'
+      post 'session/sso/:provider/confirm' => :do_sso_confirm
       delete 'session/destroy' => :destroy
     end
 
@@ -501,7 +505,6 @@ OBSApi::Application.routes.draw do
   match 'build/:project/:repository' => 'build#index', constraints: cons, via: [:get, :post]
   match 'build/:project' => 'build#project_index', constraints: cons, via: [:get, :post, :put]
   get 'build' => 'source#index'
-
   ### /published
 
   # :arch can be also a ymp for a pattern :/