Skip to content

Commit 02bcab9

Browse files
authored
Merge pull request #1 from DmitryBochkarev/fix-state-race-condition
Introduce State Container
2 parents d266cc1 + d03b5ad commit 02bcab9

File tree

4 files changed

+53
-3
lines changed

4 files changed

+53
-3
lines changed

lib/omniauth-oauth2.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
require "omniauth-oauth2/version"
2+
require "omniauth/strategies/oauth2/state_container"
23
require "omniauth/strategies/oauth2"

lib/omniauth/strategies/oauth2.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def self.inherited(subclass)
4040
},
4141
:code_challenge_method => "S256",
4242
}
43+
option :state_container, StateContainer.new
4344

4445
attr_accessor :access_token
4546

@@ -60,7 +61,7 @@ def request_phase
6061
end
6162

6263
def authorize_params # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
63-
options.authorize_params[:state] = SecureRandom.hex(24)
64+
options.authorize_params[:state] = new_state
6465

6566
if OmniAuth.config.test_mode
6667
@env ||= {}
@@ -72,7 +73,7 @@ def authorize_params # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
7273
.merge(pkce_authorize_params)
7374

7475
session["omniauth.pkce.verifier"] = options.pkce_verifier if options.pkce
75-
session["omniauth.state"] = params[:state]
76+
options.state_container.store(self, params[:state])
7677

7778
params
7879
end
@@ -83,7 +84,7 @@ def token_params
8384

8485
def callback_phase # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
8586
error = request.params["error_reason"] || request.params["error"]
86-
if !options.provider_ignores_state && (request.params["state"].to_s.empty? || request.params["state"] != session.delete("omniauth.state"))
87+
if !options.provider_ignores_state && (request.params["state"].to_s.empty? || request.params["state"] != options.state_container.take(self))
8788
fail!(:csrf_detected, CallbackError.new(:csrf_detected, "CSRF detected"))
8889
elsif error
8990
fail!(error, CallbackError.new(request.params["error"], request.params["error_description"] || request.params["error_reason"], request.params["error_uri"]))
@@ -100,6 +101,10 @@ def callback_phase # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexi
100101
fail!(:failed_to_connect, e)
101102
end
102103

104+
def new_state
105+
SecureRandom.hex(24)
106+
end
107+
103108
protected
104109

105110
def pkce_authorize_params
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module OmniAuth
2+
module Strategies
3+
class OAuth2
4+
class StateContainer
5+
def store(oauth2, state)
6+
oauth2.session["omniauth.state"] = state
7+
end
8+
9+
def take(oauth2)
10+
oauth2.session.delete("omniauth.state")
11+
end
12+
end
13+
end
14+
end
15+
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
require "helper"
2+
3+
describe OmniAuth::Strategies::OAuth2::StateContainer do
4+
let(:state) { "random_state" }
5+
let(:oauth2) { double("OAuth2", session: {}) }
6+
7+
describe "#save_state" do
8+
it "saves the state in the session" do
9+
subject.store(oauth2, state)
10+
11+
expect(oauth2.session["omniauth.state"]).to eq(state)
12+
end
13+
end
14+
15+
describe "#take_state" do
16+
before do
17+
subject.store(oauth2, state)
18+
end
19+
20+
it "removes the state from the session" do
21+
expect(oauth2.session).to include("omniauth.state")
22+
23+
taken_state = subject.take(oauth2)
24+
25+
expect(oauth2.session).not_to include("omniauth.state")
26+
expect(taken_state).to eq(state)
27+
end
28+
end
29+
end

0 commit comments

Comments
 (0)