From 74f0db1a0d008c1e1eb6c31f0a9a6a227535a68d Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 23 Nov 2024 00:38:00 +0100 Subject: [PATCH 1/3] implement saml provider --- Gemfile.lock | 10 ++++ code0-identities.gemspec | 3 +- lib/code0/identities.rb | 2 + lib/code0/identities/provider/saml.rb | 80 ++++++++++++++++++++++++++ sig/code0/identities/provider/saml.rbs | 11 ++++ 5 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 lib/code0/identities/provider/saml.rb create mode 100644 sig/code0/identities/provider/saml.rbs diff --git a/Gemfile.lock b/Gemfile.lock index 897e5a4..b841a39 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,6 +3,7 @@ PATH specs: code0-identities (0.0.0) httparty (~> 0.22) + ruby-saml (~> 1.17.0) GEM remote: https://rubygems.org/ @@ -24,8 +25,14 @@ GEM json (2.7.2) language_server-protocol (3.17.0.3) mini_mime (1.1.5) + mini_portile2 (2.8.8) multi_xml (0.7.1) bigdecimal (~> 3.1) + nokogiri (1.16.7) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.16.7-x86_64-linux) + racc (~> 1.4) parallel (1.25.1) parser (3.3.4.0) ast (~> 2.4.1) @@ -77,6 +84,9 @@ GEM rubocop-rspec_rails (2.28.3) rubocop (~> 1.40) ruby-progressbar (1.13.0) + ruby-saml (1.17.0) + nokogiri (>= 1.13.10) + rexml strscan (3.1.0) unicode-display_width (2.5.0) webmock (3.23.1) diff --git a/code0-identities.gemspec b/code0-identities.gemspec index 560fde5..bf6ad06 100644 --- a/code0-identities.gemspec +++ b/code0-identities.gemspec @@ -31,8 +31,9 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_dependency "httparty", "~> 0.22" - spec.add_development_dependency "webmock", "~> 3.23.1" + spec.add_dependency "ruby-saml", '~> 1.17.0' + spec.add_development_dependency "webmock", "~> 3.23.1" spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "rspec", "~> 3.0" spec.add_development_dependency "rubocop", "~> 1.21" diff --git a/lib/code0/identities.rb b/lib/code0/identities.rb index 1d397cf..8579d00 100644 --- a/lib/code0/identities.rb +++ b/lib/code0/identities.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "httparty" +require 'onelogin/ruby-saml' require_relative "identities/version" require_relative "identities/identity_provider" @@ -10,6 +11,7 @@ require_relative "identities/provider/google" require_relative "identities/provider/discord" require_relative "identities/provider/github" +require_relative "identities/provider/saml" module Code0 module Identities diff --git a/lib/code0/identities/provider/saml.rb b/lib/code0/identities/provider/saml.rb new file mode 100644 index 0000000..acfe8ba --- /dev/null +++ b/lib/code0/identities/provider/saml.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Code0 + module Identities + module Provider + class Saml + attr_reader :config_loader + + def initialize(config_loader) + @config_loader = config_loader + end + + def authorization_url + request = OneLogin::RubySaml::Authrequest.new + request.create(create_settings) + + request.instance_variable_get :@login_url + end + + def load_identity(**params) + response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], { **config[:response_settings], settings: create_settings }) + attributes = response.attributes + + Identity.new(config[:provider_name], + response.name_id, + find_attribute(attributes, config[:attribute_statements][:username]), + find_attribute(attributes, config[:attribute_statements][:email]), + find_attribute(attributes, config[:attribute_statements][:firstname]), + find_attribute(attributes, config[:attribute_statements][:lastname]) + ) + end + + private + + def find_attribute(attributes, attribute_statements) + attribute_statements.each do |statement| + unless attributes[statement].nil? + return attributes[statement] + end + end + nil + end + + def create_settings + if config[:metadata_url].nil? + settings = OneLogin::RubySaml::Settings.new + else + idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + settings = idp_metadata_parser.parse_remote(config[:metadata_url]) + end + + settings.name_identifier_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + + config[:settings].each do |key, value| + settings.send(:"#{key}=", value) + end + settings + end + + def config + config = config_loader + if config_loader.is_a?(Proc) + config = config_loader.call + end + + config[:provider_name] ||= :saml + config[:response_settings] ||= {} + config[:settings] ||= {} + config[:attribute_statements] ||= {} + config[:attribute_statements][:username] ||= %w[username name http://schemas.goauthentik.io/2021/02/saml/username] + config[:attribute_statements][:email] ||= %w[email mail http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress http://schemas.microsoft.com/ws/2008/06/identity/claims/emailaddress] + config[:attribute_statements][:firstname] ||= %w[first_name firstname firstName http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname http://schemas.microsoft.com/ws/2008/06/identity/claims/givenname] + config[:attribute_statements][:lastname] ||= %w[last_name lastname lastName http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname http://schemas.microsoft.com/ws/2008/06/identity/claims/surname] + + config + end + end + end + end +end \ No newline at end of file diff --git a/sig/code0/identities/provider/saml.rbs b/sig/code0/identities/provider/saml.rbs new file mode 100644 index 0000000..7862e13 --- /dev/null +++ b/sig/code0/identities/provider/saml.rbs @@ -0,0 +1,11 @@ +module Code0 + module Identities + module Provider + class Saml + def authorization_url: () -> String + + def load_identity: (Hash[Symbol, any]) -> Identity + end + end + end +end From 8135ec842125b86803a5c17845b72394e9256a63 Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 23 Nov 2024 00:43:01 +0100 Subject: [PATCH 2/3] fix some rubocop issues --- code0-identities.gemspec | 4 ++-- lib/code0/identities.rb | 2 +- lib/code0/identities/provider/saml.rb | 18 ++++++++---------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/code0-identities.gemspec b/code0-identities.gemspec index bf6ad06..10661ae 100644 --- a/code0-identities.gemspec +++ b/code0-identities.gemspec @@ -31,13 +31,13 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_dependency "httparty", "~> 0.22" - spec.add_dependency "ruby-saml", '~> 1.17.0' + spec.add_dependency "ruby-saml", "~> 1.17.0" - spec.add_development_dependency "webmock", "~> 3.23.1" spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "rspec", "~> 3.0" spec.add_development_dependency "rubocop", "~> 1.21" spec.add_development_dependency "rubocop-rake", "~> 0.6" spec.add_development_dependency "rubocop-rspec", "~> 2.29" # Uncomment to register a new dependency of your gem + spec.add_development_dependency "webmock", "~> 3.23.1" spec.metadata["rubygems_mfa_required"] = "true" end diff --git a/lib/code0/identities.rb b/lib/code0/identities.rb index 8579d00..5590ebe 100644 --- a/lib/code0/identities.rb +++ b/lib/code0/identities.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "httparty" -require 'onelogin/ruby-saml' +require "onelogin/ruby-saml" require_relative "identities/version" require_relative "identities/identity_provider" diff --git a/lib/code0/identities/provider/saml.rb b/lib/code0/identities/provider/saml.rb index acfe8ba..1902e3b 100644 --- a/lib/code0/identities/provider/saml.rb +++ b/lib/code0/identities/provider/saml.rb @@ -18,7 +18,8 @@ def authorization_url end def load_identity(**params) - response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], { **config[:response_settings], settings: create_settings }) + response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], + { **config[:response_settings], settings: create_settings }) attributes = response.attributes Identity.new(config[:provider_name], @@ -26,17 +27,14 @@ def load_identity(**params) find_attribute(attributes, config[:attribute_statements][:username]), find_attribute(attributes, config[:attribute_statements][:email]), find_attribute(attributes, config[:attribute_statements][:firstname]), - find_attribute(attributes, config[:attribute_statements][:lastname]) - ) + find_attribute(attributes, config[:attribute_statements][:lastname])) end private def find_attribute(attributes, attribute_statements) attribute_statements.each do |statement| - unless attributes[statement].nil? - return attributes[statement] - end + return attributes[statement] unless attributes[statement].nil? end nil end @@ -59,10 +57,9 @@ def create_settings def config config = config_loader - if config_loader.is_a?(Proc) - config = config_loader.call - end + config = config_loader.call if config_loader.is_a?(Proc) + # rubocop:disable Layout/LineLength config[:provider_name] ||= :saml config[:response_settings] ||= {} config[:settings] ||= {} @@ -71,10 +68,11 @@ def config config[:attribute_statements][:email] ||= %w[email mail http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress http://schemas.microsoft.com/ws/2008/06/identity/claims/emailaddress] config[:attribute_statements][:firstname] ||= %w[first_name firstname firstName http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname http://schemas.microsoft.com/ws/2008/06/identity/claims/givenname] config[:attribute_statements][:lastname] ||= %w[last_name lastname lastName http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname http://schemas.microsoft.com/ws/2008/06/identity/claims/surname] + # rubocop:enable Layout/LineLength config end end end end -end \ No newline at end of file +end From fcb8eeaa73bdd63c6870ae58281038aaeb65161d Mon Sep 17 00:00:00 2001 From: Dario Date: Sat, 23 Nov 2024 01:14:05 +0100 Subject: [PATCH 3/3] extend readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0b26ae4..6acb71b 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ OAuth: - Microsoft - Github - Gitlab +- SAML ## Installation