I apologize for the very long question, but I preferred to err on the side of providing more information in case some of it is important.
I'm trying to implement "Sign in with Google" in a Rails API application using Doorkeeper and hit a wall. I'm hoping I'm missing something obvious or there is a working example somewhere. We already have a working endpoint api/v1/users/sign_in
for obtaining a token given user name and password. The controller action code for it is quite simple:
def sign_in
sign_in_with_strategy("password")
end
private def sign_in_with_strategy(strategy_name)
strategy = server.token_request(strategy_name)
auth = strategy.authorize
if auth.is_a?(Doorkeeper::OAuth::ErrorResponse)
return render_error("Invalid credentials", status: auth.status)
end
render json: {
# Includes more data from auth.token in real code, but that shouldn't matter for the question
data: auth.token.plaintext_token,
}
end
I've set up the following:
Credentials
with Authorized JavaScript origins: http://localhost:3000
and Authorized redirect URIs: http://localhost:3000/api/v1/users/sign_in/3rd_party/redirect/google
.OAuth consent screen
, user type is Internal
. The scopes are .../auth/userinfo.email
, .../auth/userinfo.profile
, and openid
.doorkeeper-grants_assertion
and omniauth-google-oauth2
gems.config/initializers/doorkeeper.rb
, I added (slightly adapted from https://github.com/doorkeeper-gem/doorkeeper-grants_assertion/tree/9d185e44fb1245620ed2e50c0987f9628dc78995?tab=readme-ov-file#direct-omniauth-configuration; in particular, I removed the rescue
to see what part is failing)
resource_owner_from_assertion do
Rails.logger.info("params: #{params}")
if params[:provider] && params[: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,
# use https://github.com/doorkeeper-gem/doorkeeper-grants_assertion?tab=readme-ov-file#reuse-devise-configuration
case params.fetch(:provider)
when "google"
auth0 = Doorkeeper::GrantsAssertion::OmniAuth.oauth2_wrapper(
provider: :google,
strategy_class: OmniAuth::Strategies::GoogleOauth2,
client_id: ENV["GOOGLE_CLIENT_ID"],
client_secret: ENV["GOOGLE_CLIENT_SECRET"],
client_options: { skip_image_info: true },
assertion: params.fetch(:assertion)
)
auth = auth0.auth_hash
Rails.logger.info("inside resource_owner_from_assertion: auth0: #{auth0.inspect}; auth: #{auth.inspect}")
end
if auth
user = User.from_omniauth(auth)
if user.present? && user.active_for_authentication?
user
end
end
end
end
and updated grant_flows
to %w[password assertion]
. def sign_in_3rd_party
end
with this view:
<div>
<p>This page is for testing third party sign-in to the API in the browser.</p>
<p>Clicking any of the "Sign in" links below should show a JSON response including a bearer token.</p>
</div>
<div>
<%=
query = 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),
response_type: "code",
scope: "email"
}
)
link_to "Sign in with Google", "https://accounts.google.com/o/oauth2/v2/auth?#{query}"
%>
</div>
and
def sign_in_3rd_party_redirect
# 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}" : "")
render_error(message, status: :bad_request)
end
end
When I visit the "Sign in with Google" link, I get the expected screens for sign in and get redirected to the sign_in_3rd_party_redirect
action, with provider
parameter (from the redirect URL) and code
in the query string. The exchange is also successful and I get the access token. However, the auth = auth0.auth_hash
line in the resource_owner_from_assertion
block raises this error:
4:00:55 AM web.1 | JWT::DecodeError - Invalid segment encoding:
4:00:55 AM web.1 | config/initializers/doorkeeper.rb:40:in `block (2 levels) in <main>'
4:00:55 AM web.1 | app/controllers/api/v1/users_session_controller.rb:194:in `sign_in_with_strategy'
4:00:55 AM web.1 | app/controllers/api/v1/users_session_controller.rb:54:in `sign_in_3rd_party_redirect'
4:00:55 AM web.1 | app/controllers/api/v1/api_controller.rb:34:in `switch_locale'
Am I doing something wrong? Or is it a bug in one of the libraries used? Versions:
It looks like it's just this issue and can be worked around by adding skip_jwt: true
to client_options
.