accodeing / fortnox-api

Gem that abstracts Fortnox's F3 API
GNU Lesser General Public License v3.0
9 stars 8 forks source link

Add support for OAuth 2 #205

Closed ehannes closed 2 years ago

ehannes commented 2 years ago

Fortnox are changing to OAuth 2 for Authorization. You can read more about it in Authorizing your integration. The old way of authorize integrations will stop to work 9/1-2022.

Note that long lived access tokens used for this type of integration that fortnox-api is has a life span of 10 years. If you need new access tokens, or if your current tokens are deprecating, you will need to use OAuth 2.

Dotenv

Currently, environment variables are hard coded in spec/support/helpers/configuration_helper.rb since those values are included in plain text in the vcr cassettes anyway. But now access_token is short lived and client_secret will not be submitted at all in those requests. So maybe it would be suitable to move environment variables to Dotenv.

Refresh tokens and vcr cassettes

We use vcr to record HTTP requests during testing. We should probably request an access token manually before creating new vcr cassettes. We might even need to request a new refresh token since we don't run the tests at regular basis. The refresh token is only valid for 31 days.

ehannes commented 2 years ago

@infoman I happened to see your thumbs up on this issue. Are you actively using this gem? :slightly_smiling_face:

ehannes commented 2 years ago

Hmm, not sure if I'm missing something obvious here, but this 501 Not Implemented server error response looks a bit weird, doesn't it? :thinking:

$ curl --head 'https://apps.fortnox.se/oauth-v1/auth?client_id=[FILTERD-ID]&response_type=code&state=somestate&scope=customer'
HTTP/2 501 
content-type: text/plain;charset=UTF-8
date: Fri, 20 May 2022 21:55:51 GMT
x-uid: 08e50a0d
server: Fortnox
x-content-type-options: nosniff
x-xss-protection: 0
referrer-policy: strict-origin-when-cross-origin
content-security-policy: upgrade-insecure-requests;frame-ancestors https://*.fortnox.se;report-uri /api/cspreport;connect-src 'self' https://a.storyblok.com wss://*.fortnox.se *.fortnox.se *.findity.com *.livechatinc.com mybusiness.pwc.se themes.googleusercontent.com s3.amazonaws.com/helpjuice-static/ *.helpjuice.com *.vimeo.com fonts.googleapis.com fonts.gstatic.com 'unsafe-inline' 'unsafe-eval' blob: data:
x-frame-options: sameorigin
strict-transport-security: max-age=31536000; includeSubdomains
samuel02 commented 2 years ago

Could it be that you are making a HEAD request? Btw, I have stopped using this gem on my latest applications since it's compatible with new versions of Ruby and Rails. So I had to implement this feature and did it naively so:

  def api_client
    Faraday.new('https://api.fortnox.se/3') do |conn|
      conn.adapter :net_http

      conn.request :authorization, 'Bearer', access_token.token
      conn.request :json

      conn.response :raise_error
      conn.response :json, parser_options: { symbolize_names: true }
      conn.response :logger, nil, { headers: true, bodies: true, log_level: :debug }
    end
  end

  def redis
    @redis ||= Redis.new
  end

  def access_token
    return @access_token if defined? @access_token

    @access_token = begin
      oauth_client = OAuth2::Client.new(
        ENV.fetch('FORTNOX_CLIENT_ID'),
        ENV.fetch('FORTNOX_CLIENT_SECRET'),
        {
          site: 'https://apps.fortnox.se',
          token_url: 'oauth-v1/token',
        }
      ) do |conn|
        conn.adapter :net_http

        conn.request :url_encoded
        conn.request :authorization,
          :basic,
          ENV.fetch('FORTNOX_CLIENT_ID'),
          ENV.fetch('FORTNOX_CLIENT_SECRET')
      end

      cached_token_data = redis.get('fortnox_access_token_data')
      token_data = cached_token_data ? Marshal.load(cached_token_data) : {}

      access_token = OAuth2::AccessToken.new(
        oauth_client,
        token_data[:access_token] || ENV.fetch('FORTNOX_ACCESS_TOKEN'),
        {
          refresh_token: token_data[:refresh_token] || ENV.fetch('FORTNOX_REFRESH_TOKEN'),
          expires_at: token_data[:expires_at] || 5.minutes.ago
        }
      )

      if access_token.expired?
        access_token = access_token.refresh!

        redis.set('fortnox_access_token_data', Marshal.dump({
          access_token: access_token.token,
          refresh_token: access_token.refresh_token,
          expires_at: access_token.expires_at
        }))
      end

      access_token
    end
  end
ehannes commented 2 years ago

Oh, nice @samuel02! I'll look into it.

We are working on better Ruby support in this gem. Would you be interesting in using this gem again if we fixed the support for newer Ruby versions? Which version are you using?

samuel02 commented 2 years ago

Yeah, probably! On this app I'm using Ruby 3.1.2

ehannes commented 2 years ago

@samuel02 there's #212 to support Ruby 3. We hope that will be the next step for us after we have implemented support for OAuth 2.