diff options
-rw-r--r-- | app/lib/sso.rb | 7 | ||||
-rw-r--r-- | app/lib/sso/from_discourse.rb | 92 | ||||
-rw-r--r-- | config/credentials.yml.enc | 2 | ||||
-rw-r--r-- | config/initializers/sso_config.rb | 16 | ||||
-rw-r--r-- | config/routes.rb | 3 |
5 files changed, 119 insertions, 1 deletions
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('%<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 diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc index fd8449c..d9d6416 100644 --- a/config/credentials.yml.enc +++ b/config/credentials.yml.enc @@ -1 +1 @@ -eWfeBbVzMevHyb6V/ejx3F0e92OMzwE2lES56Etzwpjw6ylGt+GEk+cmBAyL+Oy9cN5xITEHItX4U7Ssy3oLm3OFJGy1TExJqVkETWfkN4RxI5H1xZLfG99smhiAmAJT20Pq4adWavm8m7Hw1M8IAd+J5Mpzq0vynkrSvw9Xpto4WzD9I5yfxId9Qj3SkN17gYoYToljVl13uUfb3uYyDoikbt8lkxIHX+4ZDkOi3qIdOf8rMoiHFHH9LKoSwfib7Frzra4tPqFSgK3nnn9rQxaZYp88E0EzaskHYT70+J/Sx3upGq4S/4MbStAIrxXj11iPRQHYVdUTCTW50DILOKUeqWDD/ayRTqCIyL3z3AioPu0oeIBGv984t6TansDTkJQlmQJiZqqfcQk2UopH/Zu8NssnC2BdoIdS--KbZxqXMAkn6TQ3wH--V2i2mT97pBJ0ci1VuYCVfQ==
\ No newline at end of file +nlpLLu6lgECvrsn7X/wEVg6LDrflx6pMqXovDczQP3+QDBX2XVDk4m+F3c3sWxipglV3iou1yJeuGBcXuVJKJ8QjjDRK3sTLMcYbfT4Ez/OMD4Y4QVF6hnLE1B6nVt/wSjG6l7tcczcCAgZ6HnBbK8+4A2OLfO0xDqkpujL79XaW6B5oPc4j+0B7hZylHqiGW4mx/t/qXvwVvRLQbjGH11jfrBiW6JLqx+6KuYzJmPDvtLhsPYUxLczltEca/mAgGMc0iYxAN7IRk1p0V9sYahgBId0P9/GegUz5TUvaJY2kEcXe0vAHsqN+b2XHu28cCPoa4x0NoWUZVCl+a6MoH1giC9ZPvoQ5DGVcHnyvqO6rF/KLtb+JRACMhMbNIrmbSlr8Mr5SVZrlb3reY1+AVv+im35RuxVfv0hlkfsfMcQ+o1UB/Iq+xmeSBBG/SYOs8S31CK2F2XRtQXIeQG2C53+n9/VCVmM6FGnPO561x6hYF4tocw==--zKXfSMQvDShHEQGq--Bn0S/jeIjlwvbq7aMDSPgQ==
\ No newline at end of file diff --git a/config/initializers/sso_config.rb b/config/initializers/sso_config.rb new file mode 100644 index 0000000..b3f23f7 --- /dev/null +++ b/config/initializers/sso_config.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# See lib/sso/from_discourse.rb +# module SSO +# class FromDiscourse +# class << self +# attr_accessor :config +# end +# end +# end + +SSO::FromDiscourse.config = { + sso_url: 'https://talk.incommon.cc/session/sso_provider', + return_url: "http://localhost:3000/authenticate", + sso_secret: Rails.application.credentials.sso_secret, +} diff --git a/config/routes.rb b/config/routes.rb index c06383a..a2260d6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,6 @@ Rails.application.routes.draw do # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html + + # Discourse SSO + get 'my/account/:token', to: 'authentication#login' end |