omniauth-oauth2 icon indicating copy to clipboard operation
omniauth-oauth2 copied to clipboard

Invalid JWT token type (invalid_credentials: OAuth2::Error)

Open dombarnes opened this issue 1 year ago • 5 comments

I am currently using this gem in a couple of Rails projects without issue connecting to an IdentityServer with openid/oauth. I was previously using a fairly basic handwritten Oauth handler in a modular sinatra project and wanted to migrate to this gem to be consistent. I transferred over my strategy but when I log in now, I hit my oauth server requesting an id_token code method, get back a response with a code. That is then passed to the token endpoint and I get back a response with an id_tokenand an access_token. However when I call access_token.get('connect/userinfo')to get my user info, the HTTP call seems to be using the id_tokennot the access_token and so I am getting a 401 unauthorised error. I can't seem to figure out what's going wrong.

my_company.rb
# frozen_string_literal: true

require 'omniauth-oauth2'
require 'jwt'
require 'securerandom'

module OmniAuth
  module Strategies
    class MyCompany < OmniAuth::Strategies::OAuth2

      args %i[client_id client_secret client_scope]

      option :name, 'my_company'
      option :client_options, {
        site: ENV.fetch('OAUTH_SERVER'),
        authorize_url: '/connect/authorize',
        redirect_uri: "#{ENV['APPLICATION_HOST']}/auth/my_company/callback",
        token_url: '/connect/token'
      }

      option :authorize_params, {
        nonce: SecureRandom.hex(24),
        scope: ENV.fetch('OAUTH_SCOPE', 'openid profile'),
        response_mode: 'form_post',
        response_type: 'code id_token'
      }

      uid { raw_info['sub'] }

      info do
        {
          email: raw_info['email'],
          displayname: raw_info['displayname'],
          role: raw_info['role']
        }
      end

      extra do
        hash = {}
        hash[:raw_info] = raw_info unless skip_info?
        hash[:id_token] = access_token.token
        if !options[:skip_jwt] && !access_token.token.nil?
          hash[:id_info] = validated_token(access_token.token)
        end
        hash
      end

      def callback_url
        options[:redirect_uri] || (full_host + script_name + callback_path)
      end

      def raw_info
        @raw_info ||= access_token.get('connect/userinfo').parsed
      end

      def authorize_params
        super.merge(nonce: new_nonce)
      end

      alias oauth2_access_token access_token

      def access_token
        ::OAuth2::AccessToken.new(client, oauth2_access_token.token, {
          refresh_token: oauth2_access_token.refresh_token,
          expires_in: oauth2_access_token.expires_in,
          expires_at: oauth2_access_token.expires_at
        })
      end

      private

      def new_nonce
        session['omniauth.nonce'] = SecureRandom.urlsafe_base64(16)
      end

      def stored_nonce
        session.delete('omniauth.nonce')
      end

      def verify_options
        { verify_expiration: true,
          verify_not_before: true,
          verify_iat: true,
          verify_iss: true,
          'iss' => issuer,
          verify_aud: true,
          'aud' => client_id }
      end
    end
  end
end

OmniAuth.config.add_camelization 'my_company', 'MyCompany'

Token response

{"id_token":"my_id_token","access_token":"my_access_token","expires_in":3600,"token_type":"Bearer","refresh_token":"my_refresh_token","scope":"openid profile offline_access"}

As such, its not even hitting my callback URL as Puma is catching the error.

Puma caught this error:  (OAuth2::Error)
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/oauth2-2.0.6/lib/oauth2/client.rb:139:in `request'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/oauth2-2.0.6/lib/oauth2/access_token.rb:140:in `request'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/oauth2-2.0.6/lib/oauth2/access_token.rb:147:in `get'
/Users/myuser/workprojects/myproject/lib/omni_auth/strategies/my_company.rb:55:in `raw_info'
/Users/myuser/workprojects/myproject/lib/omni_auth/strategies/my_company.rb:42:in `block in <class:MyCompany>'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/strategy.rb:109:in `instance_eval'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/strategy.rb:109:in `block in compile_stack'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/strategy.rb:108:in `each'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/strategy.rb:108:in `inject'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/strategy.rb:108:in `compile_stack'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/strategy.rb:102:in `extra_stack'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/strategy.rb:387:in `extra'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/strategy.rb:392:in `auth_hash'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/strategy.rb:417:in `callback_phase'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-oauth2-1.8.0/lib/omniauth/strategies/oauth2.rb:93:in `callback_phase'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/strategy.rb:272:in `callback_call'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/strategy.rb:194:in `call!'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/strategy.rb:169:in `call'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/strategy.rb:202:in `call!'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/strategy.rb:169:in `call'
/Users/myuser/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/omniauth-2.1.0/lib/omniauth/builder.rb:44:in `call'

dombarnes avatar Jul 16 '22 23:07 dombarnes

What version of this gem are you on?

BobbyMcWho avatar Jul 17 '22 01:07 BobbyMcWho

* omniauth-oauth2 (1.8.0)

dombarnes avatar Jul 17 '22 05:07 dombarnes

I think its being caused by this change in the oauth2 gem https://github.com/oauth-xx/oauth2/pull/621/files#diff-b665e6fc2096be34f9c5e92cb0f38eb3a229da37ed4da92487f0a834eb6ae336R53 as per discussion At least reverting the version in my gemfile to 2.0.5 gets passed this error.

dombarnes avatar Jul 17 '22 21:07 dombarnes

Okay, report the issue to them if you wouldn't mind, we'll have to PR here to pin the version below the breaking one

BobbyMcWho avatar Jul 17 '22 21:07 BobbyMcWho

After a lot of testing I found a solution to my issue, but not entirely sure of the cause. I think it was a combination of rack middleware misconfiguring, and double-loading my strategy. At one point I was getting errors about not providing an oauth Client ID from my provider and tracked back the issue.

For some reference tho:

Summary: I have a modular Sinatra-based app and had previously written a custom OAuth implementation using the Oauth2 gem. When trying to switch to Omniauth, I had AuthenticityToken errors. I was using rack_encrypted_cookie instead of the standard Rack::Session::Cookie middleware, imported with use Rack::Session::EncryptedCookie along with custom imports of Rack::Protection::HttpOrigin, and Rack::Cors.

After implementing Omniauth, I had to use the standard enable :sessions inside my Sinatra configure block, as well as set :session_secret with a key. Then I later configured Rack::Session::EncryptedCookie.

configure do
  # added these lines
  enable :sessions
  set :session_secret, ENV.fetch('SESSION_SECRET', SecureRandom.hex(32))
  set :session_length, 3.days
end
...
use Rack::Session::EncryptedCookie,
      key: '_myapp_session',
      expire_after: 60 * 60 * 24 * 30, # 30 days in seconds
      secure: true,
      httponly: Sinatra::Base.environment == :production,
      same_site: :none,
      secret: ENV.fetch('SESSION_SECRET') { SecureRandom.hex(64) },
      key_size: 32,
      salt: SecureRandom.hex(32),
      signed_salt: SecureRandom.hex(32)
  use Rack::Protection::SessionHijacking
  use Rack::Protection::RemoteToken

  use Rack::Protection, permitted_origins: [
    ENV.fetch('ALLOWED_URL')
  ], except: %i[http_origin remote_token]

  use Rack::Protection::HttpOrigin,
    allow_if: lambda { |env| env['REQUEST_PATH'].split('/')[1] == 'auth' },
              permitted_origins: [
                ENV.fetch('ALLOWED_URL')
                ]

  use OmniAuth::Builder do
    provider :my_config,
             ENV['OAUTH_CLIENT_ID'],
             ENV['OAUTH_CLIENT_SECRET'],
             origin_param: 'return_url'
  end

dombarnes avatar Jul 24 '22 22:07 dombarnes