From 1d53b3c26f4167be4e19e508c96a617d79c67363 Mon Sep 17 00:00:00 2001 From: hellekin Date: Mon, 5 Oct 2020 14:39:03 +0200 Subject: Add Discourse SSO code --- app/lib/sso.rb | 7 ++++ app/lib/sso/from_discourse.rb | 92 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 app/lib/sso.rb create mode 100644 app/lib/sso/from_discourse.rb (limited to 'app') diff --git a/app/lib/sso.rb b/app/lib/sso.rb new file mode 100644 index 0000000..ace2830 --- /dev/null +++ b/app/lib/sso.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Perform Single Sign-On using Discourse +module SSO + require 'securerandom' + require_relative '../../config/initializers/sso_config' +end 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('%s?sso=%s&sig=%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=%s&return_sso_url=%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 -- cgit v1.2.3