This is a follow-up question to InvalidSegmentEncoding in trying to implement "Sign in with Google" with doorkeeper-grants_assertion.
I have an endpoint used as a redirect URL for Google OAuth:
def sign_in_3rd_party_redirect
case params[:provider]
when "google"
# Exchange authorization code for access token
# https://developers.google.com/identity/protocols/oauth2/web-server#exchange-authorization-code
response = HTTParty.post(
"https://oauth2.googleapis.com/token",
headers: { "Content-Type" => "application/x-www-form-urlencoded" },
body: google_oauth2_params({
grant_type: "authorization_code",
code: params[:code],
client_secret: ENV["GOOGLE_CLIENT_SECRET"],
}),
)
if response.code == 200
params[:assertion] = response.parsed_response["access_token"]
sign_in_with_strategy("assertion")
else
message = "Unexpected response from the OAuth provider" +
(Rails.env.development? ? ": #{response.parsed_response || response}" : "")
render_error(message, status: :bad_request)
end
end
end
helper_method def google_oauth2_params(options)
URI.encode_www_form(
{
client_id: ENV["GOOGLE_CLIENT_ID"],
redirect_uri: url_for(action: :sign_in_3rd_party_redirect, provider: "google", only_path: false),
**options,
},
)
end
def sign_in_with_strategy(strategy_name)
strategy = server.token_request(strategy_name)
auth = strategy.authorize
Rails.logger.info("sign_in_with_strategy: #{auth.inspect}")
if auth.is_a?(Doorkeeper::OAuth::ErrorResponse)
return render_error("Invalid credentials", status: auth.status)
end
render json: {
data: Api::V1::Auth::AccessTokenViewModel.new(auth.token),
}
end
In config/initializers/doorkeeper.rb
, I have
resource_owner_from_assertion do
provider = params[:provider]
assertion = params[:assertion]
Rails.logger.info("resource_owner_from_assertion: params=#{params.inspect}")
if provider && assertion
# We don't have the same providers for Devise (used in the web app)
# and Doorkeeper (used in the API). If this changes, or we want to allow both,
# add https://github.com/doorkeeper-gem/doorkeeper-grants_assertion?tab=readme-ov-file#reuse-devise-configuration
# to check Devise providers before or instead of this
case provider
when "google"
wrapper = Doorkeeper::GrantsAssertion::OmniAuth.oauth2_wrapper(
provider: :google, # also tried :google_oauth2, doesn't make a difference
strategy_class: ::OmniAuth::Strategies::GoogleOauth2,
client_id: ENV["GOOGLE_CLIENT_ID"],
client_secret: ENV["GOOGLE_CLIENT_SECRET"],
# skip_jwt is a workaround for https://github.com/zquestz/omniauth-google-oauth2/issues/435
client_options: { skip_image_info: true, skip_jwt: true },
assertion: assertion
)
auth = wrapper.auth_hash
Rails.logger.info("resource_owner_from_assertion: auth=#{auth.inspect}")
end
if auth
user = User.from_omniauth(auth)
Rails.logger.info("resource_owner_from_assertion: user=#{user.inspect}")
if user.present?
user
end
end
end
end
When I try to sign in with Google, I see and accept the expected consent screens. Then in logs I see (formatted for clarity):
resource_owner_from_assertion: params=#<ActionController::Parameters
{"code"=>{Google OAuth authorization code},
"scope"=>"email openid https://www.googleapis.com/auth/userinfo.email",
"authuser"=>"2",
"hd"=>{Google account's domain},
"prompt"=>"consent",
"controller"=>"api/v1/users_session",
"action"=>"sign_in_3rd_party_redirect",
"provider"=>"google",
"client_id"=>{Doorkeeper client id},
"client_secret"=>{Doorkeeper client secret},
"assertion"=>{Google OAuth access token from the exchange}
}
permitted: false>
resource_owner_from_assertion: auth=#<OmniAuth::AuthHash
credentials=#<OmniAuth::AuthHash
expires=false
scope="https://www.googleapis.com/auth/userinfo.email openid"
token={same as assertion above}
>
extra=#<OmniAuth::AuthHash
id_token={same as assertion above}
raw_info=#<SnakyHash::StringKeyed
email={Google email}
email_verified=true
hd={as above}
picture={Google account image URL}
sub={same as uid below}
>
>
info=#<OmniAuth::AuthHash::InfoHash
email={as above}
email_verified=true
image={as above}
unverified_email={same email}
>
provider=:google
uid={some uid}
>
resource_owner_from_assertion: user={the expected user model}
so this side appears to work fine. Unfortunately, in sign_in_with_strategy
I get
sign_in_with_strategy: #<Doorkeeper::OAuth::ErrorResponse:0x00007fe36b901b80
@error=#<struct Doorkeeper::OAuth::Error name=:invalid_scope, state=nil>,
@redirect_uri=nil,
@response_on_fragment=nil
>
I am not certain if the invalid scope error is for Google or for the Doorkeeper server; I think the second because if it was for Google I'd expect the error to happen inside wrapper.auth_hash
or even earlier, as it did before. And how can I fix it?
The Doorkeeper application doesn't have any scopes or redirect URI. Could that be the problem?
The issue was scope
in the resource_owner_from_assertion: params
log. Given this setup, it comes from the redirect URL query parameter and is the scope for Google's access token.
But in the resource_owner_from_assertion
block, scope
should be the one we request from Doorkeeper. It's also read from request.parameters
, not from params
. So in sign_in_3rd_party_redirect
, I needed to add
request.parameters.delete(:scope)