Skip to content

Commit 1c18d1e

Browse files
committed
fix: Raise Error on non-2xx responses
Replace silent nil returns in Client#get_token and Client#get_user_info with CognitoIdp::Error exceptions that expose error, error_description, and http_status from Cognito's structured error body. BREAKING CHANGE: Methods now raise CognitoIdp::Error instead of returning nil on error responses.
1 parent ffc38af commit 1c18d1e

5 files changed

Lines changed: 213 additions & 8 deletions

File tree

lib/cognito_idp.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
module CognitoIdp
66
autoload :AuthorizationUri, "cognito_idp/authorization_uri"
77
autoload :Client, "cognito_idp/client"
8+
autoload :Error, "cognito_idp/error"
89
autoload :LogoutUri, "cognito_idp/logout_uri"
910
autoload :Token, "cognito_idp/token"
1011
autoload :UserInfo, "cognito_idp/user_info"

lib/cognito_idp/client.rb

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def get_token(grant_type:, **options)
3535
scope: options[:scope]
3636
}.compact
3737
response = connection.post("/oauth2/token", params, basic_authorization_headers)
38-
return unless response.success?
38+
handle_error_response(response)
3939

4040
token = Token.new(response.body)
4141
yield(token) if block_given?
@@ -50,7 +50,7 @@ def get_user_info(token)
5050
token
5151
end
5252
response = connection.post("/oauth2/userInfo", nil, {"Authorization" => "Bearer #{access_token}"})
53-
return unless response.success?
53+
handle_error_response(response)
5454

5555
user_info = UserInfo.new(response.body)
5656
yield(user_info) if block_given?
@@ -76,6 +76,25 @@ def connection
7676
end
7777
end
7878

79+
def handle_error_response(response)
80+
return if response.success?
81+
82+
body = response.body
83+
if body.is_a?(Hash) && body["error"]
84+
raise Error.new(
85+
error: body["error"],
86+
error_description: body["error_description"],
87+
http_status: response.status
88+
)
89+
else
90+
raise Error.new(
91+
error: "http_error",
92+
error_description: "the server responded with status #{response.status}",
93+
http_status: response.status
94+
)
95+
end
96+
end
97+
7998
def basic_authorization_headers
8099
return if client_secret.nil?
81100

lib/cognito_idp/error.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# frozen_string_literal: true
22

33
module CognitoIdp
4-
class Error < StandardError; end
4+
class Error < StandardError
5+
attr_reader :error, :error_description, :http_status
6+
7+
def initialize(error:, error_description: nil, http_status: nil)
8+
@error = error
9+
@error_description = error_description
10+
@http_status = http_status
11+
super(build_message)
12+
end
13+
14+
private
15+
16+
def build_message
17+
return error if error_description.nil?
18+
19+
"#{error}: #{error_description}"
20+
end
21+
end
522
end

spec/cognito_idp/client_spec.rb

Lines changed: 144 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
Faraday::Adapter::Test::Stubs.new do |stub|
8787
stub.post("https://auth.example.com/oauth2/token") do |env|
8888
fail "Authorization is present.#{env.request_headers}" if env.request_headers.key?("Authorization")
89+
[200, {"Content-Type" => "application/json"}, response_payload.to_json]
8990
end
9091
end
9192
end
@@ -103,6 +104,7 @@
103104
id_and_secret = "#{client_id}:#{client_secret}"
104105
basic_auth = "Basic #{Base64.urlsafe_encode64(id_and_secret)}"
105106
fail "Basic Authorization is missing." unless env.request_headers["Authorization"] == basic_auth
107+
[200, {"Content-Type" => "application/json"}, response_payload.to_json]
106108
end
107109
end
108110
end
@@ -129,7 +131,66 @@
129131
end
130132
let(:error) { "invalid_request" }
131133

132-
it { is_expected.to be_nil }
134+
it "raises a CognitoIdp::Error" do
135+
expect { token }.to raise_error(CognitoIdp::Error) do |e|
136+
expect(e.error).to eq("invalid_request")
137+
expect(e.http_status).to eq(400)
138+
end
139+
end
140+
end
141+
142+
context "when response is an error with error_description" do
143+
let(:stubs) do
144+
Faraday::Adapter::Test::Stubs.new do |stub|
145+
stub.post("https://auth.example.com/oauth2/token") do |env|
146+
[400, {"Content-Type" => "application/json"}, response_payload.to_json]
147+
end
148+
end
149+
end
150+
let(:response_payload) do
151+
{error: "invalid_grant", error_description: "Authorization code has expired"}
152+
end
153+
154+
it "raises a CognitoIdp::Error with error_description" do
155+
expect { token }.to raise_error(CognitoIdp::Error) do |e|
156+
expect(e.error).to eq("invalid_grant")
157+
expect(e.error_description).to eq("Authorization code has expired")
158+
expect(e.message).to eq("invalid_grant: Authorization code has expired")
159+
expect(e.http_status).to eq(400)
160+
end
161+
end
162+
end
163+
164+
context "when response is a server error" do
165+
let(:stubs) do
166+
Faraday::Adapter::Test::Stubs.new do |stub|
167+
stub.post("https://auth.example.com/oauth2/token") do |env|
168+
[500, {"Content-Type" => "text/plain"}, "Internal Server Error"]
169+
end
170+
end
171+
end
172+
173+
it "raises a CognitoIdp::Error with http_error" do
174+
expect { token }.to raise_error(CognitoIdp::Error) do |e|
175+
expect(e.error).to eq("http_error")
176+
expect(e.error_description).to eq("the server responded with status 500")
177+
expect(e.http_status).to eq(500)
178+
end
179+
end
180+
end
181+
182+
context "when response is an error it does not yield" do
183+
let(:stubs) do
184+
Faraday::Adapter::Test::Stubs.new do |stub|
185+
stub.post("https://auth.example.com/oauth2/token") do |env|
186+
[400, {"Content-Type" => "application/json"}, {error: "invalid_request"}.to_json]
187+
end
188+
end
189+
end
190+
191+
it "does not yield the block" do
192+
expect { |b| client.get_token(grant_type: grant_type, code: code, redirect_uri: redirect_uri, &b) }.to raise_error(CognitoIdp::Error)
193+
end
133194
end
134195
end
135196

@@ -192,7 +253,12 @@
192253
end
193254
let(:error) { "invalid_request" }
194255

195-
it { is_expected.to be_nil }
256+
it "raises a CognitoIdp::Error" do
257+
expect { token }.to raise_error(CognitoIdp::Error) do |e|
258+
expect(e.error).to eq("invalid_request")
259+
expect(e.http_status).to eq(400)
260+
end
261+
end
196262
end
197263
end
198264

@@ -243,6 +309,7 @@
243309
Faraday::Adapter::Test::Stubs.new do |stub|
244310
stub.post("https://auth.example.com/oauth2/token") do |env|
245311
fail "Authorization is present.#{env.request_headers}" if env.request_headers.key?("Authorization")
312+
[200, {"Content-Type" => "application/json"}, response_payload.to_json]
246313
end
247314
end
248315
end
@@ -260,6 +327,7 @@
260327
id_and_secret = "#{client_id}:#{client_secret}"
261328
basic_auth = "Basic #{Base64.urlsafe_encode64(id_and_secret)}"
262329
fail "Basic Authorization is missing." unless env.request_headers["Authorization"] == basic_auth
330+
[200, {"Content-Type" => "application/json"}, response_payload.to_json]
263331
end
264332
end
265333
end
@@ -286,7 +354,12 @@
286354
end
287355
let(:error) { "invalid_request" }
288356

289-
it { is_expected.to be_nil }
357+
it "raises a CognitoIdp::Error" do
358+
expect { token }.to raise_error(CognitoIdp::Error) do |e|
359+
expect(e.error).to eq("invalid_request")
360+
expect(e.http_status).to eq(400)
361+
end
362+
end
290363
end
291364
end
292365

@@ -349,7 +422,12 @@
349422
end
350423
let(:error) { "invalid_request" }
351424

352-
it { is_expected.to be_nil }
425+
it "raises a CognitoIdp::Error" do
426+
expect { token }.to raise_error(CognitoIdp::Error) do |e|
427+
expect(e.error).to eq("invalid_request")
428+
expect(e.http_status).to eq(400)
429+
end
430+
end
353431
end
354432
end
355433
end
@@ -453,7 +531,68 @@
453531
end
454532
let(:error) { "invalid_request" }
455533

456-
it { is_expected.to be_nil }
534+
it "raises a CognitoIdp::Error" do
535+
expect { user_info }.to raise_error(CognitoIdp::Error) do |e|
536+
expect(e.error).to eq("invalid_request")
537+
expect(e.http_status).to eq(400)
538+
end
539+
end
540+
end
541+
542+
context "when response is an unauthorized error" do
543+
let(:token) { "ACCESS_TOKEN" }
544+
let(:access_token) { token }
545+
let(:stubs) do
546+
Faraday::Adapter::Test::Stubs.new do |stub|
547+
stub.post("https://auth.example.com/oauth2/userInfo") do |env|
548+
[401, {"Content-Type" => "application/json"}, {error: "invalid_token", error_description: "Access token is expired"}.to_json]
549+
end
550+
end
551+
end
552+
553+
it "raises a CognitoIdp::Error with error_description" do
554+
expect { user_info }.to raise_error(CognitoIdp::Error) do |e|
555+
expect(e.error).to eq("invalid_token")
556+
expect(e.error_description).to eq("Access token is expired")
557+
expect(e.http_status).to eq(401)
558+
end
559+
end
560+
end
561+
562+
context "when response is a server error" do
563+
let(:token) { "ACCESS_TOKEN" }
564+
let(:access_token) { token }
565+
let(:stubs) do
566+
Faraday::Adapter::Test::Stubs.new do |stub|
567+
stub.post("https://auth.example.com/oauth2/userInfo") do |env|
568+
[500, {"Content-Type" => "text/plain"}, "Internal Server Error"]
569+
end
570+
end
571+
end
572+
573+
it "raises a CognitoIdp::Error with http_error" do
574+
expect { user_info }.to raise_error(CognitoIdp::Error) do |e|
575+
expect(e.error).to eq("http_error")
576+
expect(e.error_description).to eq("the server responded with status 500")
577+
expect(e.http_status).to eq(500)
578+
end
579+
end
580+
end
581+
582+
context "when response is an error it does not yield" do
583+
let(:token) { "ACCESS_TOKEN" }
584+
let(:access_token) { token }
585+
let(:stubs) do
586+
Faraday::Adapter::Test::Stubs.new do |stub|
587+
stub.post("https://auth.example.com/oauth2/userInfo") do |env|
588+
[400, {"Content-Type" => "application/json"}, {error: "invalid_request"}.to_json]
589+
end
590+
end
591+
end
592+
593+
it "does not yield the block" do
594+
expect { |b| client.get_user_info(access_token, &b) }.to raise_error(CognitoIdp::Error)
595+
end
457596
end
458597
end
459598

spec/cognito_idp/error_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe CognitoIdp::Error do
4+
it "inherits from StandardError" do
5+
expect(described_class).to be < StandardError
6+
end
7+
8+
describe "#message" do
9+
context "when only error is provided" do
10+
subject(:error) { described_class.new(error: "invalid_request") }
11+
12+
it { expect(error.message).to eq("invalid_request") }
13+
end
14+
15+
context "when error and error_description are provided" do
16+
subject(:error) { described_class.new(error: "invalid_grant", error_description: "Authorization code has expired") }
17+
18+
it { expect(error.message).to eq("invalid_grant: Authorization code has expired") }
19+
end
20+
end
21+
22+
describe "attribute readers" do
23+
subject(:error) { described_class.new(error: "invalid_grant", error_description: "Authorization code has expired", http_status: 400) }
24+
25+
it { expect(error.error).to eq("invalid_grant") }
26+
it { expect(error.error_description).to eq("Authorization code has expired") }
27+
it { expect(error.http_status).to eq(400) }
28+
end
29+
end

0 commit comments

Comments
 (0)