Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persist the 'state' parameter until a successful callback. #75

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 29 additions & 10 deletions lib/omniauth/strategies/oauth2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,10 @@ def request_phase
end

def authorize_params
options.authorize_params[:state] = SecureRandom.hex(24)
params = options.authorize_params.merge(options_for("authorize"))
if OmniAuth.config.test_mode
@env ||= {}
@env["rack.session"] ||= {}
end
session["omniauth.state"] = params[:state]
params
site = self.class.name
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would I add a dynamic attribute in the state params? Here is a use case: I have a multi-tenant app and in the google strategy, I have authenticate! method and first thing it does is switch the tenant. Since the tenant name should be dynamic, I want to get the tenant name dynamically from the state_params or from the request.env['omniauth.auth']. I am not sure if I am putting it correctly but I want the tenant name dynamically set somewhere in request params. If the request is coming from the host1.com, tenant_name=host1, host2.com, tenant_name=host2, something of this sort. Since the request is coming from the auth server, does it mean I need to intercept the request and add the tenant in the request params depending upon the origin?

state = state_store.fetch(site) { state_store[site] = SecureRandom.hex(24) }
options.authorize_params[:state] = state
options.authorize_params.merge(options_for("authorize"))
end

def token_params
Expand All @@ -69,11 +65,18 @@ def token_params

def callback_phase # rubocop:disable AbcSize, CyclomaticComplexity, MethodLength, PerceivedComplexity
error = request.params["error_reason"] || request.params["error"]
site = self.class.name
expected_state = state_store[site]
actual_state = request.params["state"].to_s

if error
fail!(error, CallbackError.new(request.params["error"], request.params["error_description"] || request.params["error_reason"], request.params["error_uri"]))
elsif !options.provider_ignores_state && (request.params["state"].to_s.empty? || request.params["state"] != session.delete("omniauth.state"))
description = request.params["error_description"] || request.params["error_reason"]
error_uri = request.params["error_uri"]
fail!(error, CallbackError.new(request.params["error"], description, error_uri))
elsif !options.provider_ignores_state && (actual_state.empty? || actual_state != expected_state)
fail!(:csrf_detected, CallbackError.new(:csrf_detected, "CSRF detected"))
else
state_store.delete(site)
self.access_token = build_access_token
self.access_token = access_token.refresh! if access_token.expired?
super
Expand Down Expand Up @@ -109,6 +112,22 @@ def options_for(option)
hash
end

private

def state_store
if OmniAuth.config.test_mode
@env ||= {}
@env["rack.session"] ||= {}
end

state_key = "omniauth.oauth2.state"
state_store = session[state_key]
unless state_store.is_a?(Hash)
state_store = session[state_key] = {}
end
state_store
end

# An error that is indicated in the OAuth 2.0 callback.
# This could be a `redirect_uri_mismatch` or other
class CallbackError < StandardError
Expand Down
46 changes: 42 additions & 4 deletions spec/omniauth/strategies/oauth2_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,49 @@ def app
expect(instance.authorize_params["scope"]).to eq("bar")
expect(instance.authorize_params["foo"]).to eq("baz")
end
end

it "includes random state in the authorize params" do
instance = subject.new("abc", "def")
expect(instance.authorize_params.keys).to eq(["state"])
expect(instance.session["omniauth.state"]).not_to be_empty
describe "state handling" do
SocialNetwork = Class.new(OmniAuth::Strategies::OAuth2)

let(:client_options) { {:site => "https://graph.example.com"} }
let(:instance) { SocialNetwork.new(-> env {}) }

before do
allow(SecureRandom).to receive(:hex).with(24).and_return("hex-1", "hex-2")
end

it "includes a state scoped to the client" do
expect(instance.authorize_params["state"]).to eq("hex-1")
expect(instance.session["omniauth.oauth2.state"]).to eq("SocialNetwork" => "hex-1")
end

context "once a state value has been generated" do
before do
instance.authorize_params
end

it "does not replace an existing session value" do
expect(instance.authorize_params["state"]).to eq("hex-1")
expect(instance.session["omniauth.oauth2.state"]).to eq("SocialNetwork" => "hex-1")
end
end

context "on a successful callback" do
let(:request) { double("Request", :params => {"code" => "auth-code", "state" => "hex-1"}) }
let(:access_token) { double("AccessToken", :expired? => false, :expires? => false, :token => "access-token") }

before do
allow(instance).to receive(:request).and_return(request)
allow(instance).to receive(:build_access_token).and_return(access_token)

instance.authorize_params
instance.callback_phase
end

it "removes the value from the session" do
expect(instance.session["omniauth.oauth2.state"]).to eq({})
end
end
end

Expand Down