ruby-on-railsdoorkeeper

InvalidSegmentEncoding in trying to implement "Sign in with Google" with doorkeeper-grants_assertion


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:

  1. I created a project in Google Cloud Console.
    1. It has an OAuth 2.0 Client ID under Credentials with Authorized JavaScript origins: http://localhost:3000 and Authorized redirect URIs: http://localhost:3000/api/v1/users/sign_in/3rd_party/redirect/google.
    2. For OAuth consent screen, user type is Internal. The scopes are .../auth/userinfo.email, .../auth/userinfo.profile, and openid.
  2. Added doorkeeper-grants_assertion and omniauth-google-oauth2 gems.
  3. In 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].
  4. In the same controller which has the password sign in action, I added 2 more:
      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:


Solution

  • It looks like it's just this issue and can be worked around by adding skip_jwt: true to client_options.