Class: DiscourseConnectBase

Inherits:
Object
  • Object
show all
Defined in:
lib/discourse_connect_base.rb

Direct Known Subclasses

DiscourseConnect, DiscourseConnectProvider

Defined Under Namespace

Classes: ParseError

Constant Summary collapse

ACCESSORS =
%i[
  add_groups
  admin
  avatar_force_update
  avatar_url
  bio
  card_background_url
  confirmed_2fa
  email
  external_id
  failed
  groups
  locale
  locale_force_update
  location
  logout
  moderator
  name
  no_2fa_methods
  nonce
  prompt
  profile_background_url
  remove_groups
  require_2fa
  require_activation
  return_sso_url
  suppress_welcome_message
  title
  username
  website
]
FIXNUMS =
[]
BOOLS =
%i[
  admin
  avatar_force_update
  confirmed_2fa
  failed
  locale_force_update
  logout
  moderator
  no_2fa_methods
  require_2fa
  require_activation
  suppress_welcome_message
]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#sso_secretObject



117
118
119
# File 'lib/discourse_connect_base.rb', line 117

def sso_secret
  @sso_secret || self.class.sso_secret
end

#sso_urlObject



121
122
123
# File 'lib/discourse_connect_base.rb', line 121

def sso_url
  @sso_url || self.class.sso_url
end

Class Method Details

.nonce_expiry_timeObject



55
56
57
# File 'lib/discourse_connect_base.rb', line 55

def self.nonce_expiry_time
  @nonce_expiry_time ||= 10.minutes
end

.nonce_expiry_time=(v) ⇒ Object



59
60
61
# File 'lib/discourse_connect_base.rb', line 59

def self.nonce_expiry_time=(v)
  @nonce_expiry_time = v
end

.parse(payload, sso_secret = nil, **init_kwargs) ⇒ Object



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
111
# File 'lib/discourse_connect_base.rb', line 78

def self.parse(payload, sso_secret = nil, **init_kwargs)
  sso = new(**init_kwargs)
  sso.sso_secret = sso_secret if sso_secret

  parsed = Rack::Utils.parse_query(payload)
  decoded = Base64.decode64(parsed["sso"])
  decoded_hash = Rack::Utils.parse_query(decoded)

  if sso.sign(parsed["sso"]) != parsed["sig"]
    diags =
      "\n\nsso: #{parsed["sso"]}\n\nsig: #{parsed["sig"]}\n\nexpected sig: #{sso.sign(parsed["sso"])}"
    if parsed["sso"] =~ %r{[^a-zA-Z0-9=\r\n/+]}m
      raise ParseError,
            "The SSO field should be Base64 encoded, using only A-Z, a-z, 0-9, +, /, and = characters. Your input contains characters we don't understand as Base64, see http://en.wikipedia.org/wiki/Base64 #{diags}"
    else
      raise ParseError, "Bad signature for payload #{diags}"
    end
  end

  ACCESSORS.each do |k|
    val = decoded_hash[k.to_s]
    val = val.to_i if FIXNUMS.include? k
    val = %w[true false].include?(val) ? val == "true" : nil if BOOLS.include? k
    sso.public_send("#{k}=", val)
  end

  decoded_hash.each do |k, v|
    if field = k[/\Acustom\.(.+)\z/, 1]
      sso.custom_fields[field] = v
    end
  end

  sso
end

.sign(payload, secret) ⇒ Object



129
130
131
# File 'lib/discourse_connect_base.rb', line 129

def self.sign(payload, secret)
  OpenSSL::HMAC.hexdigest("sha256", secret, payload)
end

.sso_secretObject

Raises:

  • (RuntimeError)


70
71
72
# File 'lib/discourse_connect_base.rb', line 70

def self.sso_secret
  raise RuntimeError, "sso_secret not implemented on class, be sure to set it on instance"
end

.sso_urlObject

Raises:

  • (RuntimeError)


74
75
76
# File 'lib/discourse_connect_base.rb', line 74

def self.sso_url
  raise RuntimeError, "sso_url not implemented on class, be sure to set it on instance"
end

.used_nonce_expiry_timeObject



63
64
65
# File 'lib/discourse_connect_base.rb', line 63

def self.used_nonce_expiry_time
  24.hours
end

Instance Method Details

#custom_fieldsObject



125
126
127
# File 'lib/discourse_connect_base.rb', line 125

def custom_fields
  @custom_fields ||= {}
end

#diagnosticsObject



113
114
115
# File 'lib/discourse_connect_base.rb', line 113

def diagnostics
  DiscourseConnectBase::ACCESSORS.map { |a| "#{a}: #{public_send(a)}" }.join("\n")
end

#payload(secret = nil) ⇒ Object



147
148
149
150
# File 'lib/discourse_connect_base.rb', line 147

def payload(secret = nil)
  payload = Base64.strict_encode64(unsigned_payload)
  "sso=#{CGI.escape(payload)}&sig=#{sign(payload, secret)}"
end

#sign(payload, secret = nil) ⇒ Object



133
134
135
136
# File 'lib/discourse_connect_base.rb', line 133

def sign(payload, secret = nil)
  secret = secret || sso_secret
  self.class.sign(payload, secret)
end

#to_hObject



156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/discourse_connect_base.rb', line 156

def to_h
  payload = {}

  ACCESSORS.each do |k|
    next if (val = public_send(k)) == nil
    payload[k] = val
  end

  @custom_fields&.each { |k, v| payload["custom.#{k}"] = v.to_s }

  payload
end

#to_jsonObject



138
139
140
# File 'lib/discourse_connect_base.rb', line 138

def to_json
  self.to_h.to_json
end

#to_url(base_url = nil) ⇒ Object



142
143
144
145
# File 'lib/discourse_connect_base.rb', line 142

def to_url(base_url = nil)
  base = "#{base_url || sso_url}"
  "#{base}#{base.include?("?") ? "&" : "?"}#{payload}"
end

#unsigned_payloadObject



152
153
154
# File 'lib/discourse_connect_base.rb', line 152

def unsigned_payload
  Rack::Utils.build_query(self.to_h)
end