aboutsummaryrefslogtreecommitdiff
path: root/app/lib/sso/from_discourse.rb
diff options
context:
space:
mode:
authorhellekin <hellekin@cepheide.org>2020-10-05 14:39:03 +0200
committerhellekin <hellekin@cepheide.org>2020-10-05 14:39:03 +0200
commit1d53b3c26f4167be4e19e508c96a617d79c67363 (patch)
treea8eacd792ef620e013ca46dec3d890286b944f68 /app/lib/sso/from_discourse.rb
parent270620682c1a3c006af4f9d15f9be58537d4d9ee (diff)
downloadincommon-map-1d53b3c26f4167be4e19e508c96a617d79c67363.tar.gz
Add Discourse SSO code
Diffstat (limited to 'app/lib/sso/from_discourse.rb')
-rw-r--r--app/lib/sso/from_discourse.rb92
1 files changed, 92 insertions, 0 deletions
diff --git a/app/lib/sso/from_discourse.rb b/app/lib/sso/from_discourse.rb
new file mode 100644
index 0000000..66742e2
--- /dev/null
+++ b/app/lib/sso/from_discourse.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+module SSO
+ class FromDiscourse
+ attr_accessor :nonce, :token, :user_info, :status
+
+ class << self
+ # See config/initializers/sso.rb
+ # This is a hash:
+ # SSO::FromDiscourse.config = {
+ # sso_url: 'https://talk.incommon.cc/session/sso_provider',
+ # return_url: "#{API_ROOT_URL}/my/account",
+ # sso_secret: Rails.application.credentials.sso_secret,
+ # }
+ # In config/routes.rb:
+ # ...
+ # get 'my/account/:token' => 'authentications#sso_login'
+ attr_accessor :config
+ end
+
+ def initialize(options = {})
+ @nonce = options[:nonce] || SecureRandom.hex
+ @token = options[:token] || SecureRandom.hex
+ @user_info = nil
+ @status = :unauthorized
+ end
+
+ def parse(params)
+ params[:sig] =~ %r{\A[a-f0-9]{64}\z} || raise(ArgumentError, 'HMAC invalid')
+ hmac_matches?(params[:sso], params[:sig]) || raise(ArgumentError, 'HMAC mismatch')
+ base64?(params[:sso]) || raise(ArgumentError, 'Encoding invalid')
+
+ response = Rack::Utils.parse_query(Base64.decode64(params[:sso]))
+
+ # Of course, the nonces must match!
+ ActiveSupport::SecurityUtils
+ .fixed_length_secure_compare(response['nonce'], @nonce) ||
+ raise(ArgumentError, 'Nonce mismatch')
+
+ response.delete('nonce')
+
+ @user_info = response.symbolize_keys
+ @status = :ok
+
+ self
+ end
+
+ def request_uri
+ format('%<url>s?sso=%<payload>s&sig=%<hmac>s',
+ url: config[:sso_url].to_s,
+ payload: Rack::Utils.escape(b64_payload),
+ hmac: mac_signature)
+ end
+
+ def success?
+ @status == :ok
+ end
+
+ private
+
+ def config
+ self.class.config
+ end
+
+ # Estimate whether given data is encoded in Base64.
+ # This is not fool-proof but good enough for our purpose.
+ def base64?(data)
+ data.is_a?(String) &&
+ (data.length % 4).zero? &&
+ data.match?(%r{\A[a-zA-Z\d+/]+={,2}\z})
+ end
+
+ def hmac_matches?(payload, signature)
+ hmac = mac_signature(payload)
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(hmac, signature)
+ end
+
+ def payload
+ format('nonce=%<n>s&return_sso_url=%<u>s',
+ n: @nonce,
+ u: "#{config[:return_url]}/#{token}")
+ end
+
+ def b64_payload
+ Base64.encode64(payload)
+ end
+
+ def mac_signature(payload = b64_payload)
+ OpenSSL::HMAC.hexdigest('SHA256', self.class.config[:sso_secret], payload)
+ end
+ end
+end