# SPDX-FileCopyrightText: 2018-2020 IN COMMON Collective # # SPDX-License-Identifier: AGPL-3.0-or-later # frozen_string_literal: true module SSO class FromDiscourse attr_accessor :nonce, :token attr_reader :request_uri, :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