Skip to content

Commit 3e577b6

Browse files
authored
FIX: Preserve padding parameter through user API key authorization flow (#36640)
Users/apps running OpenSSL in FIPS 140-3 mode cannot decrypt user API key responses because FIPS forbids the legacy PKCS1_PADDING scheme. Commit 300ece3 added support for a `padding=oaep` parameter to use PKCS1_OAEP_PADDING instead, but the parameter was only handled in the POST endpoints. When users go through the authorization UI flow (GET /user-api-key/new → login → POST /user-api-key), the padding parameter was lost because it wasn't captured in the controller or passed through the form's hidden fields. This fix: - Captures @padding in the #new and #otp controller actions - Adds hidden field for padding in new.html.erb and otp.html.erb - Removes unused ALLOWED_PADDING_MODES constant - Refactors specs to be more organized and concise Internal ref - t/170427
1 parent 89be127 commit 3e577b6

File tree

5 files changed

+240
-320
lines changed

5 files changed

+240
-320
lines changed

app/controllers/user_api_keys_controller.rb

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ class UserApiKeysController < ApplicationController
1010
skip_before_action :check_xhr, :preload_json
1111

1212
AUTH_API_VERSION = 4
13-
1413
ALLOWED_PADDING_MODES = %w[pkcs1 oaep].freeze
1514

1615
def new
@@ -19,8 +18,9 @@ def new
1918
return
2019
end
2120

22-
find_client
2321
require_params
22+
find_client
23+
require_client_params
2424
validate_params
2525

2626
unless current_user
@@ -47,23 +47,20 @@ def new
4747
@push_url = params[:push_url]
4848
@localized_scopes = params[:scopes].split(",").map { |s| I18n.t("user_api_key.scopes.#{s}") }
4949
@scopes = params[:scopes]
50+
@padding = params[:padding]
5051
rescue Discourse::InvalidAccess
5152
@generic_error = true
5253
end
5354

5455
def create
55-
find_client
5656
require_params
57-
58-
if params.key?(:auth_redirect)
59-
if UserApiKeyClient.invalid_auth_redirect?(params[:auth_redirect], client: @client)
60-
raise Discourse::InvalidAccess
61-
end
62-
end
57+
find_client
58+
require_client_params
59+
validate_params
60+
validate_auth_redirect
6361

6462
raise Discourse::InvalidAccess unless meets_tl?
6563

66-
validate_params
6764
scopes = params[:scopes].split(",")
6865

6966
@client = UserApiKeyClient.new(client_id: params[:client_id]) if @client.blank?
@@ -89,15 +86,12 @@ def create
8986
api: AUTH_API_VERSION,
9087
}.to_json
9188

92-
public_key_str = (@client.public_key.presence || params[:public_key])
93-
public_key = OpenSSL::PKey::RSA.new(public_key_str)
94-
95-
validate_payload_size_for_oaep!(@payload, public_key)
96-
@payload = Base64.encode64(rsa_encrypt(public_key, @payload))
89+
validate_payload_size_for_oaep!(@payload, parsed_public_key)
90+
@payload = Base64.encode64(rsa_encrypt(parsed_public_key, @payload))
9791

9892
if scopes.include?("one_time_password")
9993
# encrypt one_time_password separately to bypass 128 chars encryption limit
100-
otp_payload = one_time_password(public_key, current_user.username)
94+
otp_payload = one_time_password(parsed_public_key, current_user.username)
10195
end
10296

10397
if params[:auth_redirect]
@@ -123,6 +117,7 @@ def create
123117

124118
def otp
125119
require_params_otp
120+
validate_params_otp
126121

127122
unless current_user
128123
cookies[:destination_url] = request.fullpath
@@ -138,30 +133,27 @@ def otp
138133
@application_name = params[:application_name]
139134
@public_key = params[:public_key]
140135
@auth_redirect = params[:auth_redirect]
136+
@padding = params[:padding]
141137
end
142138

143139
def create_otp
144140
require_params_otp
141+
validate_params_otp
142+
validate_auth_redirect
145143

146-
if UserApiKeyClient.invalid_auth_redirect?(params[:auth_redirect])
147-
raise Discourse::InvalidAccess
148-
end
149144
raise Discourse::InvalidAccess unless meets_tl?
150145

151-
public_key = OpenSSL::PKey::RSA.new(params[:public_key])
152-
otp_payload = one_time_password(public_key, current_user.username)
146+
otp_payload = one_time_password(parsed_public_key, current_user.username)
153147

154148
redirect_path = "#{params[:auth_redirect]}?oneTimePassword=#{CGI.escape(otp_payload)}"
155149
redirect_to(redirect_path, allow_other_host: true)
156150
end
157151

158152
def revoke
159-
revoke_key = find_key if params[:id]
153+
current_key = request.env["HTTP_USER_API_KEY"]
160154

161-
if current_key = request.env["HTTP_USER_API_KEY"]
162-
request_key = UserApiKey.with_key(current_key).first
163-
revoke_key ||= request_key
164-
end
155+
revoke_key = find_key if params[:id]
156+
revoke_key ||= UserApiKey.with_key(current_key).first if current_key.present?
165157

166158
raise Discourse::NotFound unless revoke_key
167159

@@ -187,6 +179,9 @@ def find_client
187179

188180
def require_params
189181
%i[nonce scopes client_id].each { |p| params.require(p) }
182+
end
183+
184+
def require_client_params
190185
params.require(:public_key) if @client&.public_key.blank?
191186
params.require(:application_name) if @client&.application_name.blank?
192187
end
@@ -198,14 +193,40 @@ def validate_params
198193
raise Discourse::InvalidAccess
199194
end
200195

201-
# our pk has got to parse
202-
OpenSSL::PKey::RSA.new(params[:public_key]) if params[:public_key]
196+
parsed_public_key if public_key_str.present?
197+
validate_padding
203198
end
204199

205200
def require_params_otp
206201
%i[public_key auth_redirect application_name].each { |p| params.require(p) }
207202
end
208203

204+
def validate_params_otp
205+
parsed_public_key
206+
validate_padding
207+
end
208+
209+
def validate_padding
210+
return if params[:padding].blank?
211+
return if ALLOWED_PADDING_MODES.include?(params[:padding])
212+
raise Discourse::InvalidParameters.new(:padding)
213+
end
214+
215+
def validate_auth_redirect
216+
return unless params.key?(:auth_redirect)
217+
if UserApiKeyClient.invalid_auth_redirect?(params[:auth_redirect], client: @client)
218+
raise Discourse::InvalidAccess
219+
end
220+
end
221+
222+
def public_key_str
223+
@client&.public_key.presence || params[:public_key]
224+
end
225+
226+
def parsed_public_key
227+
@parsed_public_key ||= OpenSSL::PKey::RSA.new(public_key_str)
228+
end
229+
209230
def meets_tl?
210231
current_user.staff? || current_user.in_any_groups?(SiteSetting.user_api_key_allowed_groups_map)
211232
end

app/models/api_key.rb

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,7 @@ class KeyAccessError < StandardError
1010

1111
scope :active, -> { where("revoked_at IS NULL") }
1212
scope :revoked, -> { where.not(revoked_at: nil) }
13-
14-
scope :with_key,
15-
->(key) do
16-
hashed = self.hash_key(key)
17-
where(key_hash: hashed)
18-
end
13+
scope :with_key, ->(key) { where(key_hash: ApiKey.hash_key(key)) }
1914

2015
validates :description, length: { maximum: 255 }
2116
validate :at_least_one_granular_scope

app/views/user_api_keys/new.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<%= hidden_field_tag 'push_url', @push_url %>
2929
<%= hidden_field_tag 'public_key', @public_key%>
3030
<%= hidden_field_tag 'scopes', @scopes%>
31+
<%= hidden_field_tag('padding', @padding) if @padding %>
3132
<%= submit_tag t('user_api_key.authorize'), class: 'btn btn-primary' %>
3233
<% end %>
3334
</div>

app/views/user_api_keys/otp.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<%= hidden_field_tag 'application_name', @application_name %>
55
<%= hidden_field_tag 'public_key', @public_key%>
66
<%= hidden_field_tag('auth_redirect', @auth_redirect) %>
7+
<%= hidden_field_tag('padding', @padding) if @padding %>
78
<%= submit_tag t('user_api_key.authorize'), class: 'btn btn-primary' %>
89
<% end %>
910
</div>

0 commit comments

Comments
 (0)