1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
# SPDX-FileCopyrightText: 2018-2020 IN COMMON Collective <collective@incommon.cc>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
# frozen_string_literal: true
module SSO
class MissingSecretError < ArgumentError; end
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: 'https://incommon-map.example/authenticate',
# sso_secret: Rails.application.credentials.sso_secret,
# }
# In config/routes.rb:
# ...
# get 'authenticate/(: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', sso_secret, payload)
end
def sso_secret
@sso_secret = begin
self.class.config[:sso_secret] ||
Rails.application.credentials.sso_secret
rescue MissingConstant
nil
end
raise SSO::MissingSecretError if @sso_secret.nil?
self.class.config[:sso_secret] ||= @sso_secret
end
end
end
|